StyleCop: everything else

This commit is contained in:
Raymond Lynch 2021-05-30 07:10:00 -04:00
parent f64c9b8321
commit 595fd3f1e4
134 changed files with 16346 additions and 6202 deletions

View file

@ -55,8 +55,7 @@ namespace Dalamud.Configuration
File.ReadAllText(path.FullName), File.ReadAllText(path.FullName),
new JsonSerializerSettings new JsonSerializerSettings
{ {
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects, TypeNameHandling = TypeNameHandling.Objects,
}); });
} }
@ -108,8 +107,8 @@ namespace Dalamud.Configuration
/// </summary> /// </summary>
/// <param name="pluginName">InternalName of the plugin.</param> /// <param name="pluginName">InternalName of the plugin.</param>
/// <returns>FileInfo of the config file.</returns> /// <returns>FileInfo of the config file.</returns>
public FileInfo GetConfigFile(string pluginName) => new FileInfo(Path.Combine(this.configDirectory.FullName, $"{pluginName}.json")); public FileInfo GetConfigFile(string pluginName) => new(Path.Combine(this.configDirectory.FullName, $"{pluginName}.json"));
private DirectoryInfo GetDirectoryPath(string pluginName) => new DirectoryInfo(Path.Combine(this.configDirectory.FullName, pluginName)); private DirectoryInfo GetDirectoryPath(string pluginName) => new(Path.Combine(this.configDirectory.FullName, pluginName));
} }
} }

View file

@ -3,15 +3,16 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.Addon; using Dalamud.Game.Addon;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.ClientState; using Dalamud.Game.ClientState;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Game.Internal; using Dalamud.Game.Internal;
using Dalamud.Game.Network; using Dalamud.Game.Network;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Plugin; using Dalamud.Plugin;
using Serilog; using Serilog;
@ -182,12 +183,15 @@ namespace Dalamud
/// </summary> /// </summary>
internal bool IsReady { get; private set; } internal bool IsReady { get; private set; }
/// <summary>
/// Gets a value indicating whether the plugin system is loaded.
/// </summary>
internal bool IsLoadedPluginSystem => this.PluginManager != null; internal bool IsLoadedPluginSystem => this.PluginManager != null;
/// <summary> /// <summary>
/// Gets location of stored assets. /// Gets location of stored assets.
/// </summary> /// </summary>
internal DirectoryInfo AssetDirectory => new DirectoryInfo(this.StartInfo.AssetDirectory); internal DirectoryInfo AssetDirectory => new(this.StartInfo.AssetDirectory);
/// <summary> /// <summary>
/// Runs tier 1 of the Dalamud initialization process. /// Runs tier 1 of the Dalamud initialization process.
@ -231,7 +235,6 @@ namespace Dalamud
Log.Information("[T2] AntiDebug OK!"); Log.Information("[T2] AntiDebug OK!");
this.WinSock2 = new WinSockHandlers(); this.WinSock2 = new WinSockHandlers();
Log.Information("[T2] WinSock OK!"); Log.Information("[T2] WinSock OK!");
@ -457,8 +460,7 @@ namespace Dalamud
/// </summary> /// </summary>
internal void ReplaceExceptionHandler() internal void ReplaceExceptionHandler()
{ {
var releaseFilter = this.SigScanner.ScanText( var releaseFilter = this.SigScanner.ScanText("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 ?? ?? ?? ?? ??");
"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 ?? ?? ?? ?? ??");
Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}"); Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}");
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter); var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter);

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Target"> <PropertyGroup Label="Target">
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
<TargetFramework>net472</TargetFramework> <TargetFramework>net472</TargetFramework>
@ -34,6 +34,18 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<NoWarn>IDE0017;IDE0044;IDE0047;IDE0048;IDE1006;CS1573;CS1591;CS1701;CS1702</NoWarn>
<!-- IDE0017 - Use object initializers -->
<!-- IDE0044 - Add readonly modifier -->
<!-- IDE0047 - Parentheses preferences -->
<!-- IDE0048 - Parentheses preferences -->
<!-- IDE1006 - Naming preferences -->
<!-- CS1573 - Parameter has no matching param tag in the XML comment -->
<!-- CS1591 - Missing XML comment for publicly visible type or member -->
<!-- CS1701 - Runtime policy may be needed -->
<!-- CS1702 - Runtime policy may be needed -->
</PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="Resources\Lumina.Generated.dll" /> <None Remove="Resources\Lumina.Generated.dll" />
<None Remove="stylecop.json" /> <None Remove="stylecop.json" />
@ -54,7 +66,7 @@
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="EasyHook" Version="2.7.6270" /> <PackageReference Include="EasyHook" Version="2.7.6270" />
<PackageReference Include="SharpDX.Desktop" Version="4.2.0" /> <PackageReference Include="SharpDX.Desktop" Version="4.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118"> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View file

@ -1,5 +1,4 @@
using System; using System;
#pragma warning disable SA1401 // Fields should be private
namespace Dalamud namespace Dalamud
{ {
@ -50,5 +49,3 @@ namespace Dalamud
public bool OptOutMbCollection; public bool OptOutMbCollection;
} }
} }
#pragma warning restore SA1401 // Fields should be private

View file

@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using Dalamud.Data.LuminaExtensions; using Dalamud.Data.LuminaExtensions;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiScene; using ImGuiScene;
@ -22,8 +23,8 @@ namespace Dalamud.Data
/// </summary> /// </summary>
public class DataManager : IDisposable public class DataManager : IDisposable
{ {
private readonly InterfaceManager interfaceManager;
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private readonly InterfaceManager interfaceManager;
/// <summary> /// <summary>
/// A <see cref="Lumina"/> object which gives access to any excel/game data. /// A <see cref="Lumina"/> object which gives access to any excel/game data.
@ -36,6 +37,7 @@ namespace Dalamud.Data
/// Initializes a new instance of the <see cref="DataManager"/> class. /// Initializes a new instance of the <see cref="DataManager"/> class.
/// </summary> /// </summary>
/// <param name="language">The language to load data with by default.</param> /// <param name="language">The language to load data with by default.</param>
/// <param name="interfaceManager">An <see cref="InterfaceManager"/> instance to parse the data with.</param>
internal DataManager(ClientLanguage language, InterfaceManager interfaceManager) internal DataManager(ClientLanguage language, InterfaceManager interfaceManager)
{ {
this.interfaceManager = interfaceManager; this.interfaceManager = interfaceManager;
@ -71,87 +73,6 @@ namespace Dalamud.Data
/// </summary> /// </summary>
public bool IsDataReady { get; private set; } public bool IsDataReady { get; private set; }
/// <summary>
/// Initialize this data manager.
/// </summary>
/// <param name="baseDir">The directory to load data from.</param>
internal void Initialize(string baseDir)
{
try
{
Log.Verbose("Starting data load...");
var zoneOpCodeDict =
JsonConvert.DeserializeObject<Dictionary<string, ushort>>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")));
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(zoneOpCodeDict);
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
var clientOpCodeDict =
JsonConvert.DeserializeObject<Dictionary<string, ushort>>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")));
this.ClientOpCodes = new ReadOnlyDictionary<string, ushort>(clientOpCodeDict);
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
var luminaOptions = new LuminaOptions
{
CacheFileResources = true,
#if DEBUG
PanicOnSheetChecksumMismatch = true,
#else
PanicOnSheetChecksumMismatch = false,
#endif
DefaultExcelLanguage = this.Language switch {
ClientLanguage.Japanese => Lumina.Data.Language.Japanese,
ClientLanguage.English => Lumina.Data.Language.English,
ClientLanguage.German => Lumina.Data.Language.German,
ClientLanguage.French => Lumina.Data.Language.French,
_ => throw new ArgumentOutOfRangeException(
nameof(this.Language),
@"Unknown Language: " + this.Language),
},
};
var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null)
{
this.gameData =
new GameData(
Path.Combine(
Path.GetDirectoryName(processModule.FileName) !,
"sqpack"), luminaOptions);
}
Log.Information("Lumina is ready: {0}", this.gameData.DataPath);
this.IsDataReady = true;
this.luminaResourceThread = new Thread(() =>
{
while (true)
{
if (this.gameData.FileHandleManager.HasPendingFileLoads)
{
this.gameData.ProcessFileHandleQueue();
}
else
{
Thread.Sleep(5);
}
}
// ReSharper disable once FunctionNeverReturns
});
this.luminaResourceThread.Start();
}
catch (Exception ex)
{
Log.Error(ex, "Could not download data.");
}
}
#region Lumina Wrappers #region Lumina Wrappers
/// <summary> /// <summary>
@ -172,12 +93,13 @@ namespace Dalamud.Data
/// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns> /// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns>
public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage language) where T : ExcelRow public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage language) where T : ExcelRow
{ {
var lang = language switch { var lang = language switch
{
ClientLanguage.Japanese => Lumina.Data.Language.Japanese, ClientLanguage.Japanese => Lumina.Data.Language.Japanese,
ClientLanguage.English => Lumina.Data.Language.English, ClientLanguage.English => Lumina.Data.Language.English,
ClientLanguage.German => Lumina.Data.Language.German, ClientLanguage.German => Lumina.Data.Language.German,
ClientLanguage.French => Lumina.Data.Language.French, ClientLanguage.French => Lumina.Data.Language.French,
_ => throw new ArgumentOutOfRangeException(nameof(this.Language), @"Unknown Language: " + this.Language) _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"),
}; };
return this.Excel.GetSheet<T>(lang); return this.Excel.GetSheet<T>(lang);
} }
@ -203,7 +125,7 @@ namespace Dalamud.Data
var filePath = GameData.ParseFilePath(path); var filePath = GameData.ParseFilePath(path);
if (filePath == null) if (filePath == null)
return default; return default;
return this.gameData.Repositories.TryGetValue(filePath.Repository, out var repository) ? repository.GetFile<T>(filePath.Category, filePath) : default(T); return this.gameData.Repositories.TryGetValue(filePath.Repository, out var repository) ? repository.GetFile<T>(filePath.Category, filePath) : default;
} }
/// <summary> /// <summary>
@ -234,12 +156,13 @@ namespace Dalamud.Data
/// <returns>The <see cref="TexFile"/> containing the icon.</returns> /// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile GetIcon(ClientLanguage iconLanguage, int iconId) public TexFile GetIcon(ClientLanguage iconLanguage, int iconId)
{ {
var type = iconLanguage switch { var type = iconLanguage switch
{
ClientLanguage.Japanese => "ja/", ClientLanguage.Japanese => "ja/",
ClientLanguage.English => "en/", ClientLanguage.English => "en/",
ClientLanguage.German => "de/", ClientLanguage.German => "de/",
ClientLanguage.French => "fr/", ClientLanguage.French => "fr/",
_ => throw new ArgumentOutOfRangeException(nameof(this.Language), @"Unknown Language: " + this.Language) _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"),
}; };
return this.GetIcon(type, iconId); return this.GetIcon(type, iconId);
@ -273,15 +196,16 @@ namespace Dalamud.Data
/// </summary> /// </summary>
/// <param name="tex">The Lumina <see cref="TexFile"/>.</param> /// <param name="tex">The Lumina <see cref="TexFile"/>.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns> /// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap GetImGuiTexture(TexFile tex) => public TextureWrap GetImGuiTexture(TexFile tex)
this.interfaceManager.LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4); => this.interfaceManager.LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4);
/// <summary> /// <summary>
/// Get the passed texture path as a drawable ImGui TextureWrap. /// Get the passed texture path as a drawable ImGui TextureWrap.
/// </summary> /// </summary>
/// <param name="path">The internal path to the texture.</param> /// <param name="path">The internal path to the texture.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns> /// <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)); public TextureWrap GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile<TexFile>(path));
/// <summary> /// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given language. /// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given language.
@ -289,8 +213,8 @@ namespace Dalamud.Data
/// <param name="iconLanguage">The requested language.</param> /// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param> /// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns> /// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap GetImGuiTextureIcon(ClientLanguage iconLanguage, int iconId) => public TextureWrap GetImGuiTextureIcon(ClientLanguage iconLanguage, int iconId)
this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId)); => this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
/// <summary> /// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given type. /// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given type.
@ -298,8 +222,8 @@ namespace Dalamud.Data
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param> /// <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> /// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns> /// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap GetImGuiTextureIcon(string type, int iconId) => public TextureWrap GetImGuiTextureIcon(string type, int iconId)
this.GetImGuiTexture(this.GetIcon(type, iconId)); => this.GetImGuiTexture(this.GetIcon(type, iconId));
#endregion #endregion
@ -310,5 +234,83 @@ namespace Dalamud.Data
{ {
this.luminaResourceThread.Abort(); this.luminaResourceThread.Abort();
} }
/// <summary>
/// Initialize this data manager.
/// </summary>
/// <param name="baseDir">The directory to load data from.</param>
internal void Initialize(string baseDir)
{
try
{
Log.Verbose("Starting data load...");
var zoneOpCodeDict =
JsonConvert.DeserializeObject<Dictionary<string, ushort>>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")));
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(zoneOpCodeDict);
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
var clientOpCodeDict =
JsonConvert.DeserializeObject<Dictionary<string, ushort>>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")));
this.ClientOpCodes = new ReadOnlyDictionary<string, ushort>(clientOpCodeDict);
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
var luminaOptions = new LuminaOptions
{
CacheFileResources = true,
#if DEBUG
PanicOnSheetChecksumMismatch = true,
#else
PanicOnSheetChecksumMismatch = false,
#endif
DefaultExcelLanguage = this.Language switch
{
ClientLanguage.Japanese => Lumina.Data.Language.Japanese,
ClientLanguage.English => Lumina.Data.Language.English,
ClientLanguage.German => Lumina.Data.Language.German,
ClientLanguage.French => Lumina.Data.Language.French,
_ => throw new ArgumentOutOfRangeException(
nameof(this.Language),
@"Unknown Language: " + this.Language),
},
};
var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null)
{
this.gameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName), "sqpack"), luminaOptions);
}
Log.Information("Lumina is ready: {0}", this.gameData.DataPath);
this.IsDataReady = true;
this.luminaResourceThread = new Thread(() =>
{
while (true)
{
if (this.gameData.FileHandleManager.HasPendingFileLoads)
{
this.gameData.ProcessFileHandleQueue();
}
else
{
Thread.Sleep(5);
}
}
// ReSharper disable once FunctionNeverReturns
});
this.luminaResourceThread.Start();
}
catch (Exception ex)
{
Log.Error(ex, "Could not download data.");
}
}
} }
} }

View file

@ -77,7 +77,7 @@ namespace Dalamud
} }
} }
private (Logger logger, LoggingLevelSwitch levelSwitch) NewLogger(string baseDirectory) private (Logger Logger, LoggingLevelSwitch LevelSwitch) NewLogger(string baseDirectory)
{ {
#if DEBUG #if DEBUG
var logPath = Path.Combine(baseDirectory, "dalamud.log"); var logPath = Path.Combine(baseDirectory, "dalamud.log");
@ -95,7 +95,7 @@ namespace Dalamud
var newLogger = new LoggerConfiguration() var newLogger = new LoggerConfiguration()
.WriteTo.Async(a => a.File(logPath)) .WriteTo.Async(a => a.File(logPath))
.WriteTo.EventSink() .WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(levelSwitch) .MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger(); .CreateLogger();

View file

@ -10,24 +10,15 @@ using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Dalamud.Game.Addon namespace Dalamud.Game.Addon
{ {
internal unsafe class DalamudSystemMenu /// <summary>
/// This class implements in-game Dalamud options in the in-game System menu.
/// </summary>
internal sealed unsafe partial class DalamudSystemMenu
{ {
private readonly Dalamud dalamud; private readonly Dalamud dalamud;
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize);
private Hook<AgentHudOpenSystemMenuPrototype> hookAgentHudOpenSystemMenu;
private delegate void AtkValueChangeType(AtkValue* thisPtr, ValueType type);
private AtkValueChangeType atkValueChangeType; private AtkValueChangeType atkValueChangeType;
private delegate void AtkValueSetString(AtkValue* thisPtr, byte* bytes);
private AtkValueSetString atkValueSetString; private AtkValueSetString atkValueSetString;
private Hook<AgentHudOpenSystemMenuPrototype> hookAgentHudOpenSystemMenu;
private delegate void UiModuleRequestMainCommand(void* thisPtr, int commandId);
// TODO: Make this into events in Framework.Gui // TODO: Make this into events in Framework.Gui
private Hook<UiModuleRequestMainCommand> hookUiModuleRequestMainCommand; private Hook<UiModuleRequestMainCommand> hookUiModuleRequestMainCommand;
@ -55,14 +46,24 @@ namespace Dalamud.Game.Addon
this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 41 03 ED"); this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 41 03 ED");
this.atkValueSetString = Marshal.GetDelegateForFunctionPointer<AtkValueSetString>(atkValueSetStringAddress); this.atkValueSetString = Marshal.GetDelegateForFunctionPointer<AtkValueSetString>(atkValueSetStringAddress);
var uiModuleRequestMainCommandAddress = this.dalamud.SigScanner.ScanText( var uiModuleRequestMainCommandAddress = this.dalamud.SigScanner.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B 01 8B DA 48 8B F1 FF 90 ?? ?? ?? ??");
"40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B 01 8B DA 48 8B F1 FF 90 ?? ?? ?? ??");
this.hookUiModuleRequestMainCommand = new Hook<UiModuleRequestMainCommand>( this.hookUiModuleRequestMainCommand = new Hook<UiModuleRequestMainCommand>(
uiModuleRequestMainCommandAddress, uiModuleRequestMainCommandAddress,
new UiModuleRequestMainCommand(this.UiModuleRequestMainCommandDetour), new UiModuleRequestMainCommand(this.UiModuleRequestMainCommandDetour),
this); this);
} }
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize);
private delegate void AtkValueChangeType(AtkValue* thisPtr, ValueType type);
private delegate void AtkValueSetString(AtkValue* thisPtr, byte* bytes);
private delegate void UiModuleRequestMainCommand(void* thisPtr, int commandId);
/// <summary>
/// Enables the <see cref="DalamudSystemMenu"/>.
/// </summary>
public void Enable() public void Enable()
{ {
this.hookAgentHudOpenSystemMenu.Enable(); this.hookAgentHudOpenSystemMenu.Enable();
@ -95,7 +96,7 @@ namespace Dalamud.Game.Addon
this.atkValueChangeType(&atkValueArgs[menuSize + 5], ValueType.Int); // currently this value has no type, set it to int this.atkValueChangeType(&atkValueArgs[menuSize + 5], ValueType.Int); // currently this value has no type, set it to int
this.atkValueChangeType(&atkValueArgs[menuSize + 5 + 1], ValueType.Int); this.atkValueChangeType(&atkValueArgs[menuSize + 5 + 1], ValueType.Int);
for (uint i = menuSize + 2; i > 1; i--) for (var i = menuSize + 2; i > 1; i--)
{ {
var curEntry = &atkValueArgs[i + 5 - 2]; var curEntry = &atkValueArgs[i + 5 - 2];
var nextEntry = &atkValueArgs[i + 5]; var nextEntry = &atkValueArgs[i + 5];
@ -155,21 +156,44 @@ namespace Dalamud.Game.Addon
break; break;
} }
} }
}
#region IDisposable Support /// <summary>
protected virtual void Dispose(bool disposing) /// Implements IDisposable.
/// </summary>
internal sealed partial class DalamudSystemMenu : IDisposable
{ {
if (!disposing) return; private bool disposed = false;
/// <summary>
/// Finalizes an instance of the <see cref="DalamudSystemMenu"/> class.
/// </summary>
~DalamudSystemMenu() => this.Dispose(false);
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
private void Dispose(bool disposing)
{
if (this.disposed)
return;
if (disposing)
{
this.hookAgentHudOpenSystemMenu.Dispose(); this.hookAgentHudOpenSystemMenu.Dispose();
this.hookUiModuleRequestMainCommand.Dispose(); this.hookUiModuleRequestMainCommand.Dispose();
} }
public void Dispose() this.disposed = true;
{ }
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
} }
} }

View file

@ -6,27 +6,29 @@ using System.Reflection;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CheapLoc; using CheapLoc;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Game.Internal.Libc;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Plugin;
using Serilog; using Serilog;
namespace Dalamud.Game { namespace Dalamud.Game
public class ChatHandlers { {
private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new Dictionary<string, string> { /// <summary>
/// Chat events and public helper functions.
/// </summary>
public class ChatHandlers
{
private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new()
{
{ "", "<:ffxive071:585847382210642069>" }, { "", "<:ffxive071:585847382210642069>" },
{"", "<:ffxive083:585848592699490329>"} { "", "<:ffxive083:585848592699490329>" },
}; };
private readonly Dalamud dalamud; private readonly Dictionary<XivChatType, Color> handledChatTypeColors = new()
{
private DalamudLinkPayload openInstallerWindowLink;
private readonly Dictionary<XivChatType, Color> HandledChatTypeColors = new Dictionary<XivChatType, Color> {
{ XivChatType.CrossParty, Color.DodgerBlue }, { XivChatType.CrossParty, Color.DodgerBlue },
{ XivChatType.Party, Color.DodgerBlue }, { XivChatType.Party, Color.DodgerBlue },
{ XivChatType.FreeCompany, Color.DeepSkyBlue }, { XivChatType.FreeCompany, Color.DeepSkyBlue },
@ -50,67 +52,110 @@ namespace Dalamud.Game {
{ XivChatType.PvPTeam, Color.SandyBrown }, { XivChatType.PvPTeam, Color.SandyBrown },
{ XivChatType.Urgent, Color.DarkViolet }, { XivChatType.Urgent, Color.DarkViolet },
{ XivChatType.NoviceNetwork, Color.SaddleBrown }, { XivChatType.NoviceNetwork, Color.SaddleBrown },
{XivChatType.Echo, Color.Gray} { XivChatType.Echo, Color.Gray },
}; };
private readonly Regex rmtRegex = private readonly Regex rmtRegex = new(
new Regex(
@"4KGOLD|We have sufficient stock|VPK\.OM|Gil for free|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5オ|Off Code( *):|offers Fantasia", @"4KGOLD|We have sufficient stock|VPK\.OM|Gil for free|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5オ|Off Code( *):|offers Fantasia",
RegexOptions.Compiled); RegexOptions.Compiled);
private readonly Dictionary<ClientLanguage, Regex[]> retainerSaleRegexes = new Dictionary<ClientLanguage, Regex[]>() { { private readonly Dictionary<ClientLanguage, Regex[]> retainerSaleRegexes = new()
ClientLanguage.Japanese, new Regex[] { {
{
ClientLanguage.Japanese,
new Regex[]
{
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)×(?<count>[\d,.]+)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled), new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)×(?<count>[\d,.]+)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled) } new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
}, {
ClientLanguage.English, new Regex[] {
new Regex(@"^(?<item>.+) you put up for sale in the (?:.+) markets (?:have|has) sold for (?<value>[\d,.]+) gil \(after fees\)\.$", RegexOptions.Compiled)
} }
}, { },
ClientLanguage.German, new Regex[] { {
ClientLanguage.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 (?:.+) für (?<value>[\d,.]+) Gil verkauft\.$", RegexOptions.Compiled),
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) verkauft und (?<value>[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled) new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) verkauft und (?<value>[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled),
}
}, {
ClientLanguage.French, new Regex[] {
new Regex(@"^Un servant a vendu (?<item>.+) pour (?<value>[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled)
} }
},
{
ClientLanguage.French,
new Regex[]
{
new Regex(@"^Un servant a vendu (?<item>.+) pour (?<value>[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled),
} }
},
}; };
private readonly Regex urlRegex = private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
new Regex(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?",
RegexOptions.Compiled);
private readonly Dalamud dalamud;
private DalamudLinkPayload openInstallerWindowLink;
private bool hasSeenLoadingMsg; private bool hasSeenLoadingMsg;
public string LastLink { get; private set; } /// <summary>
/// Initializes a new instance of the <see cref="ChatHandlers"/> class.
public ChatHandlers(Dalamud dalamud) { /// </summary>
/// <param name="dalamud">Dalamud instance.</param>
public ChatHandlers(Dalamud dalamud)
{
this.dalamud = dalamud; this.dalamud = dalamud;
dalamud.Framework.Gui.Chat.OnCheckMessageHandled += OnCheckMessageHandled; dalamud.Framework.Gui.Chat.OnCheckMessageHandled += this.OnCheckMessageHandled;
dalamud.Framework.Gui.Chat.OnChatMessage += OnChatMessage; dalamud.Framework.Gui.Chat.OnChatMessage += this.OnChatMessage;
this.openInstallerWindowLink = this.dalamud.Framework.Gui.Chat.AddChatLinkHandler("Dalamud", 1001, (i, m) => { this.openInstallerWindowLink = this.dalamud.Framework.Gui.Chat.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
{
this.dalamud.DalamudUi.OpenPluginInstaller(); this.dalamud.DalamudUi.OpenPluginInstaller();
}); });
} }
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) { /// <summary>
/// Gets the last URL seen in chat.
/// </summary>
public string LastLink { get; private set; }
/// <summary>
/// Convert a string to SeString and wrap in italics payloads.
/// </summary>
/// <param name="text">Text to convert.</param>
/// <returns>SeString payload of italicized text.</returns>
private static SeString MakeItalics(string text)
{
// TODO: when the code OnCharMessage is switched to SeString, this can be a straight insertion of the
// italics payloads only, and be a lot cleaner
var italicString = new SeString(new List<Payload>(new Payload[]
{
EmphasisItalicPayload.ItalicsOn,
new TextPayload(text),
EmphasisItalicPayload.ItalicsOff,
}));
return italicString;
}
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{
var textVal = message.TextValue; var textVal = message.TextValue;
var matched = this.rmtRegex.IsMatch(textVal); var matched = this.rmtRegex.IsMatch(textVal);
if (matched) { if (matched)
{
// This seems to be a RMT ad - let's not show it // This seems to be a RMT ad - let's not show it
Log.Debug("Handled RMT ad: " + message.TextValue); Log.Debug("Handled RMT ad: " + message.TextValue);
isHandled = true; isHandled = true;
return; return;
} }
if (this.dalamud.Configuration.BadWords != null && if (this.dalamud.Configuration.BadWords != null &&
this.dalamud.Configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x))) { this.dalamud.Configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
{
// This seems to be in the user block list - let's not show it // This seems to be in the user block list - let's not show it
Log.Debug("Blocklist triggered"); Log.Debug("Blocklist triggered");
isHandled = true; isHandled = true;
@ -118,23 +163,24 @@ namespace Dalamud.Game {
} }
} }
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
ref SeString message, ref bool isHandled) { {
if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) if (type == XivChatType.Notice && !this.hasSeenLoadingMsg)
PrintWelcomeMessage(); this.PrintWelcomeMessage();
// For injections while logged in // For injections while logged in
if (this.dalamud.ClientState.LocalPlayer != null && this.dalamud.ClientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) if (this.dalamud.ClientState.LocalPlayer != null && this.dalamud.ClientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
PrintWelcomeMessage(); this.PrintWelcomeMessage();
#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 retainerSaleRegexes[dalamud.StartInfo.Language]) { {
foreach (var regex in this.retainerSaleRegexes[this.dalamud.StartInfo.Language])
{
var matchInfo = regex.Match(message.TextValue); 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 // we no longer really need to do/validate the item matching since we read the id from the byte array
@ -143,10 +189,9 @@ namespace Dalamud.Game {
if (!itemInfo.Success) if (!itemInfo.Success)
continue; continue;
var itemLink = var itemLink = message.Payloads.FirstOrDefault(x => x.Type == PayloadType.Item) as ItemPayload;
message.Payloads.First(x => x.Type == PayloadType.Item) as ItemPayload; if (itemLink == default)
{
if (itemLink == null) {
Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode())); Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode()));
break; break;
} }
@ -155,7 +200,7 @@ namespace Dalamud.Game {
var valueInfo = matchInfo.Groups["value"]; var valueInfo = matchInfo.Groups["value"];
// not sure if using a culture here would work correctly, so just strip symbols instead // not sure if using a culture here would work correctly, so just strip symbols instead
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", "").Replace(".", ""), out var itemValue)) if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", string.Empty).Replace(".", string.Empty), out var itemValue))
continue; continue;
// Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ)); // Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ));
@ -168,7 +213,7 @@ namespace Dalamud.Game {
var linkMatch = this.urlRegex.Match(message.TextValue); var linkMatch = this.urlRegex.Match(message.TextValue);
if (linkMatch.Value.Length > 0) if (linkMatch.Value.Length > 0)
LastLink = linkMatch.Value; this.LastLink = linkMatch.Value;
// Handle all of this with SeString some day // Handle all of this with SeString some day
/* /*
@ -193,43 +238,60 @@ namespace Dalamud.Game {
*/ */
} }
private void PrintWelcomeMessage() { private void PrintWelcomeMessage()
{
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion) this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion)
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), this.dalamud.PluginManager.Plugins.Count)); + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), this.dalamud.PluginManager.Plugins.Count));
if (this.dalamud.Configuration.PrintPluginsWelcomeMsg) { if (this.dalamud.Configuration.PrintPluginsWelcomeMsg)
foreach (var plugin in this.dalamud.PluginManager.Plugins.OrderBy(x => x.Plugin.Name)) { {
foreach (var plugin in this.dalamud.PluginManager.Plugins.OrderBy(x => x.Plugin.Name))
{
this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Plugin.Name, plugin.Definition.AssemblyVersion)); this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Plugin.Name, plugin.Definition.AssemblyVersion));
} }
} }
if (string.IsNullOrEmpty(this.dalamud.Configuration.LastVersion) || !assemblyVersion.StartsWith(this.dalamud.Configuration.LastVersion)) { if (string.IsNullOrEmpty(this.dalamud.Configuration.LastVersion) || !assemblyVersion.StartsWith(this.dalamud.Configuration.LastVersion))
this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry { {
this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry
{
MessageBytes = Encoding.UTF8.GetBytes(Loc.Localize("DalamudUpdated", "The In-Game addon has been updated or was reinstalled successfully! Please check the discord for a full changelog.")), MessageBytes = Encoding.UTF8.GetBytes(Loc.Localize("DalamudUpdated", "The In-Game addon has been updated or was reinstalled successfully! Please check the discord for a full changelog.")),
Type = XivChatType.Notice Type = XivChatType.Notice,
}); });
if (DalamudChangelogWindow.WarrantsChangelog) if (DalamudChangelogWindow.WarrantsChangelog)
#pragma warning disable CS0162 // Unreachable code detected
this.dalamud.DalamudUi.OpenChangelog(); this.dalamud.DalamudUi.OpenChangelog();
#pragma warning restore CS0162 // Unreachable code detected
this.dalamud.Configuration.LastVersion = assemblyVersion; this.dalamud.Configuration.LastVersion = assemblyVersion;
this.dalamud.Configuration.Save(); this.dalamud.Configuration.Save();
} }
Task.Run(() => this.dalamud.PluginRepository.UpdatePlugins(!this.dalamud.Configuration.AutoUpdatePlugins)).ContinueWith(t => { Task.Run(() => this.dalamud.PluginRepository.UpdatePlugins(!this.dalamud.Configuration.AutoUpdatePlugins)).ContinueWith(t =>
if (t.IsFaulted) { {
if (t.IsFaulted)
{
Log.Error(t.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates.")); Log.Error(t.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates."));
} else { }
else
{
var updatedPlugins = t.Result.UpdatedPlugins; var updatedPlugins = t.Result.UpdatedPlugins;
if (updatedPlugins != null && updatedPlugins.Any()) { if (updatedPlugins != null && updatedPlugins.Any())
if (this.dalamud.Configuration.AutoUpdatePlugins) { {
if (this.dalamud.Configuration.AutoUpdatePlugins)
{
this.dalamud.PluginRepository.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:")); this.dalamud.PluginRepository.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
} else { }
this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry { else
MessageBytes = new SeString(new List<Payload>() { {
this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry
{
MessageBytes = new SeString(new List<Payload>()
{
new TextPayload(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")), new TextPayload(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")),
new TextPayload(" ["), new TextPayload(" ["),
new UIForegroundPayload(this.dalamud.Data, 500), new UIForegroundPayload(this.dalamud.Data, 500),
@ -239,7 +301,7 @@ namespace Dalamud.Game {
new UIForegroundPayload(this.dalamud.Data, 0), new UIForegroundPayload(this.dalamud.Data, 0),
new TextPayload("]"), new TextPayload("]"),
}).Encode(), }).Encode(),
Type = XivChatType.Urgent Type = XivChatType.Urgent,
}); });
} }
} }
@ -248,17 +310,5 @@ namespace Dalamud.Game {
this.hasSeenLoadingMsg = true; this.hasSeenLoadingMsg = true;
} }
private static SeString MakeItalics(string text) {
// TODO: when the above code is switched to SeString, this can be a straight insertion of the
// italics payloads only, and be a lot cleaner
var italicString = new SeString(new List<Payload>(new Payload[] {
EmphasisItalicPayload.ItalicsOn,
new TextPayload(text),
EmphasisItalicPayload.ItalicsOff
}));
return italicString;
}
} }
} }

View file

@ -1,71 +1,55 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Actors.Types; using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.ClientState.Actors.Types.NonPlayer; using Dalamud.Game.ClientState.Actors.Types.NonPlayer;
using JetBrains.Annotations; using JetBrains.Annotations;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Actors { namespace Dalamud.Game.ClientState.Actors
{
/// <summary> /// <summary>
/// This collection represents the currently spawned FFXIV actors. /// This collection represents the currently spawned FFXIV actors.
/// </summary> /// </summary>
public class ActorTable : IReadOnlyCollection<Actor>, ICollection, IDisposable { public sealed partial class ActorTable : IReadOnlyCollection<Actor>, ICollection, IDisposable
{
private const int ActorTableLength = 424; private const int ActorTableLength = 424;
#region Actor Table Cache #region ReadProcessMemory Hack
private static readonly int ActorMemSize = Marshal.SizeOf(typeof(Structs.Actor));
private static readonly IntPtr ActorMem = Marshal.AllocHGlobal(ActorMemSize);
private static readonly IntPtr CurrentProcessHandle = new(-1);
#endregion
private Dalamud dalamud;
private ClientStateAddressResolver address;
private List<Actor> actorsCache; private List<Actor> actorsCache;
private List<Actor> ActorsCache {
get {
if (this.actorsCache != null) return this.actorsCache;
this.actorsCache = GetActorTable();
return this.actorsCache;
}
}
private void ResetCache() => actorsCache = null;
#endregion
#region ReadProcessMemory Hack
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool ReadProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
IntPtr lpBuffer,
int dwSize,
out IntPtr lpNumberOfBytesRead);
private static readonly int ActorMemSize = Marshal.SizeOf(typeof(Structs.Actor));
private IntPtr actorMem = Marshal.AllocHGlobal(ActorMemSize);
private IntPtr currentProcessHandle = new IntPtr(-1);
#endregion
private ClientStateAddressResolver Address { get; }
private Dalamud dalamud;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ActorTable"/> class.
/// Set up the actor table collection. /// Set up the actor table collection.
/// </summary> /// </summary>
/// <param name="addressResolver">Client state address resolver.</param> /// <param name="dalamud">The Dalamud instance.</param>
public ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver) { /// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
Address = addressResolver; public ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver)
{
this.address = addressResolver;
this.dalamud = dalamud; this.dalamud = dalamud;
dalamud.Framework.OnUpdateEvent += Framework_OnUpdateEvent; dalamud.Framework.OnUpdateEvent += this.Framework_OnUpdateEvent;
Log.Verbose("Actor table address {ActorTable}", Address.ActorTable); Log.Verbose("Actor table address {ActorTable}", this.address.ActorTable);
} }
private void Framework_OnUpdateEvent(Internal.Framework framework) { /// <summary>
this.ResetCache(); /// Gets the amount of currently spawned actors.
} /// </summary>
public int Length => this.ActorsCache.Count;
private List<Actor> ActorsCache => this.actorsCache ??= this.GetActorTable();
/// <summary> /// <summary>
/// Get an actor at the specified spawn index. /// Get an actor at the specified spawn index.
@ -73,98 +57,168 @@ namespace Dalamud.Game.ClientState.Actors {
/// <param name="index">Spawn index.</param> /// <param name="index">Spawn index.</param>
/// <returns><see cref="Actor" /> at the specified spawn index.</returns> /// <returns><see cref="Actor" /> at the specified spawn index.</returns>
[CanBeNull] [CanBeNull]
public Actor this[int index] { public Actor this[int index] => this.ActorsCache[index];
get => ActorsCache[index];
}
/// <summary>
/// Read an actor struct from memory and create the appropriate <see cref="ObjectKind"/> type of actor.
/// </summary>
/// <param name="offset">Offset of the actor in the actor table.</param>
/// <returns>An instantiated actor.</returns>
internal Actor ReadActorFromMemory(IntPtr offset) internal Actor ReadActorFromMemory(IntPtr offset)
{ {
try { try
{
// FIXME: hack workaround for trying to access the player on logout, after the main object has been deleted // FIXME: hack workaround for trying to access the player on logout, after the main object has been deleted
if (!ReadProcessMemory(this.currentProcessHandle, offset, this.actorMem, ActorMemSize, out _)) if (!NativeFunctions.ReadProcessMemory(CurrentProcessHandle, offset, ActorMem, ActorMemSize, out _))
{ {
Log.Debug("ActorTable - ReadProcessMemory failed: likely player deletion during logout"); Log.Debug("ActorTable - ReadProcessMemory failed: likely player deletion during logout");
return null; return null;
} }
var actorStruct = Marshal.PtrToStructure<Structs.Actor>(this.actorMem); var actorStruct = Marshal.PtrToStructure<Structs.Actor>(ActorMem);
return actorStruct.ObjectKind switch { return actorStruct.ObjectKind switch
{
ObjectKind.Player => new PlayerCharacter(offset, actorStruct, this.dalamud), ObjectKind.Player => new PlayerCharacter(offset, actorStruct, this.dalamud),
ObjectKind.BattleNpc => new BattleNpc(offset, actorStruct, this.dalamud), ObjectKind.BattleNpc => new BattleNpc(offset, actorStruct, this.dalamud),
ObjectKind.EventObj => new EventObj(offset, actorStruct, this.dalamud), ObjectKind.EventObj => new EventObj(offset, actorStruct, this.dalamud),
ObjectKind.Companion => new Npc(offset, actorStruct, this.dalamud), ObjectKind.Companion => new Npc(offset, actorStruct, this.dalamud),
_ => new Actor(offset, actorStruct, this.dalamud) _ => new Actor(offset, actorStruct, this.dalamud),
}; };
} }
catch (Exception e) { catch (Exception e)
{
Log.Error(e, "Could not read actor from memory."); Log.Error(e, "Could not read actor from memory.");
return null; return null;
} }
} }
private IntPtr[] GetPointerTable() { private void ResetCache() => this.actorsCache = null;
private void Framework_OnUpdateEvent(Internal.Framework framework)
{
this.ResetCache();
}
private IntPtr[] GetPointerTable()
{
var ret = new IntPtr[ActorTableLength]; var ret = new IntPtr[ActorTableLength];
Marshal.Copy(Address.ActorTable, ret, 0, ActorTableLength); Marshal.Copy(this.address.ActorTable, ret, 0, ActorTableLength);
return ret; return ret;
} }
private List<Actor> GetActorTable() { private List<Actor> GetActorTable()
{
var actors = new List<Actor>(); var actors = new List<Actor>();
var ptrTable = GetPointerTable(); var ptrTable = this.GetPointerTable();
for (var i = 0; i < ActorTableLength; i++) { for (var i = 0; i < ActorTableLength; i++)
actors.Add(ptrTable[i] != IntPtr.Zero ? ReadActorFromMemory(ptrTable[i]) : null); {
actors.Add(ptrTable[i] != IntPtr.Zero ? this.ReadActorFromMemory(ptrTable[i]) : null);
} }
return actors; return actors;
} }
public IEnumerator<Actor> GetEnumerator() {
return ActorsCache.Where(a => a != null).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
} }
/// <summary> /// <summary>
/// The amount of currently spawned actors. /// Implementing IDisposable.
/// </summary> /// </summary>
public int Length => ActorsCache.Count; public sealed partial class ActorTable : IDisposable
{
private bool disposed = false;
int IReadOnlyCollection<Actor>.Count => Length; /// <summary>
/// Finalizes an instance of the <see cref="ActorTable"/> class.
/// </summary>
~ActorTable() => this.Dispose(false);
int ICollection.Count => Length; /// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (this.disposed)
return;
if (disposing)
{
this.dalamud.Framework.OnUpdateEvent -= this.Framework_OnUpdateEvent;
Marshal.FreeHGlobal(ActorMem);
}
this.disposed = true;
}
}
/// <summary>
/// Implementing IReadOnlyCollection, IEnumerable, and Enumerable.
/// </summary>
public sealed partial class ActorTable : IReadOnlyCollection<Actor>
{
/// <summary>
/// Gets the number of elements in the collection.
/// </summary>
/// <returns>The number of elements in the collection.</returns>
int IReadOnlyCollection<Actor>.Count => this.Length;
/// <summary>
/// Gets an enumerator capable of iterating through the actor table.
/// </summary>
/// <returns>An actor enumerable.</returns>
public IEnumerator<Actor> GetEnumerator() => this.ActorsCache.Where(a => a != null).GetEnumerator();
/// <summary>
/// Gets an enumerator capable of iterating through the actor table.
/// </summary>
/// <returns>An actor enumerable.</returns>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
/// <summary>
/// Implementing ICollection.
/// </summary>
public sealed partial class ActorTable : ICollection
{
/// <summary>
/// Gets the number of elements in the collection.
/// </summary>
/// <returns>The number of elements in the collection.</returns>
int ICollection.Count => this.Length;
/// <summary>
/// Gets a value indicating whether access to the collection is synchronized (thread safe).
/// </summary>
/// <returns>Whether access is synchronized (thread safe) or not.</returns>
bool ICollection.IsSynchronized => false; bool ICollection.IsSynchronized => false;
/// <summary>
/// Gets an object that can be used to synchronize access to the collection.
/// </summary>
/// <returns>An object that can be used to synchronize access to the collection.</returns>
object ICollection.SyncRoot => this; object ICollection.SyncRoot => this;
void ICollection.CopyTo(Array array, int index) { /// <summary>
for (var i = 0; i < Length; i++) { /// Copies the elements of the collection to an array, starting at a particular index.
/// </summary>
/// <param name="array">
/// The one-dimensional array that is the destination of the elements copied from the collection. The array must have zero-based indexing.
/// </param>
/// <param name="index">
/// The zero-based index in array at which copying begins.
/// </param>
void ICollection.CopyTo(Array array, int index)
{
for (var i = 0; i < this.Length; i++)
{
array.SetValue(this[i], index); array.SetValue(this[i], index);
index++; index++;
} }
} }
#region IDisposable Pattern
private bool disposed = false;
private void Dispose(bool disposing)
{
if (this.disposed) return;
this.dalamud.Framework.OnUpdateEvent -= Framework_OnUpdateEvent;
Marshal.FreeHGlobal(this.actorMem);
this.disposed = true;
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
~ActorTable() {
Dispose(false);
}
#endregion
} }
} }

View file

@ -1,16 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Game.ClientState.Actors namespace Dalamud.Game.ClientState.Actors
{ {
/// <summary> /// <summary>
/// This enum describes the indices of the Customize array. /// This enum describes the indices of the Customize array.
/// </summary> /// </summary>
// TODO: This may need some rework since it may not be entirely accurate (stolen from Sapphire) // TODO: This may need some rework since it may not be entirely accurate (stolen from Sapphire)
public enum CustomizeIndex { public enum CustomizeIndex
{
/// <summary> /// <summary>
/// The race of the character. /// The race of the character.
/// </summary> /// </summary>

View file

@ -1,8 +1,10 @@
namespace Dalamud.Game.ClientState.Actors { namespace Dalamud.Game.ClientState.Actors
{
/// <summary> /// <summary>
/// Enum describing possible entity kinds. /// Enum describing possible entity kinds.
/// </summary> /// </summary>
public enum ObjectKind : byte { public enum ObjectKind : byte
{
/// <summary> /// <summary>
/// Invalid actor. /// Invalid actor.
/// </summary> /// </summary>
@ -57,13 +59,25 @@ namespace Dalamud.Game.ClientState.Actors {
/// Objects representing retainers. /// Objects representing retainers.
/// </summary> /// </summary>
Retainer = 0x0A, Retainer = 0x0A,
/// <summary>
/// Objects representing area objects.
/// </summary>
Area = 0x0B, Area = 0x0B,
/// <summary> /// <summary>
/// Objects representing housing objects. /// Objects representing housing objects.
/// </summary> /// </summary>
Housing = 0x0C, Housing = 0x0C,
/// <summary>
/// Objects representing cutscene objects.
/// </summary>
Cutscene = 0x0D, Cutscene = 0x0D,
CardStand = 0x0E
/// <summary>
/// Objects representing card stand objects.
/// </summary>
CardStand = 0x0E,
} }
} }

View file

@ -1,22 +1,38 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Actors { namespace Dalamud.Game.ClientState.Actors
{
/// <summary>
/// A game native equivalent of a Vector3.
/// </summary>
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public struct Position3 { public struct Position3
{
/// <summary>
/// The X of (X,Z,Y).
/// </summary>
public float X; public float X;
/// <summary>
/// The Z of (X,Z,Y).
/// </summary>
public float Z; public float Z;
/// <summary>
/// The Y of (X,Z,Y).
/// </summary>
public float Y; public float Y;
/// <summary> /// <summary>
/// Convert this Position3 to a System.Numerics.Vector3 /// Convert this Position3 to a System.Numerics.Vector3.
/// </summary> /// </summary>
/// <param name="pos">Position to convert.</param> /// <param name="pos">Position to convert.</param>
public static implicit operator System.Numerics.Vector3(Position3 pos) => new System.Numerics.Vector3(pos.X, pos.Y, pos.Z); public static implicit operator System.Numerics.Vector3(Position3 pos) => new(pos.X, pos.Y, pos.Z);
/// <summary> /// <summary>
/// Convert this Position3 to a SharpDX.Vector3 /// Convert this Position3 to a SharpDX.Vector3.
/// </summary> /// </summary>
/// <param name="pos">Position to convert.</param> /// <param name="pos">Position to convert.</param>
public static implicit operator SharpDX.Vector3(Position3 pos) => new SharpDX.Vector3(pos.X, pos.Z, pos.Y); public static implicit operator SharpDX.Vector3(Position3 pos) => new(pos.X, pos.Z, pos.Y);
} }
} }

View file

@ -1,17 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Game.ClientState.Actors.Resolvers namespace Dalamud.Game.ClientState.Actors.Resolvers
{ {
public abstract class BaseResolver { /// <summary>
protected Dalamud dalamud; /// Base object resolver.
/// </summary>
public abstract class BaseResolver
{
private Dalamud dalamud;
public BaseResolver(Dalamud dalamud) { /// <summary>
/// Initializes a new instance of the <see cref="BaseResolver"/> class.
/// </summary>
/// <param name="dalamud">The Dalamud instance.</param>
public BaseResolver(Dalamud dalamud)
{
this.dalamud = dalamud; this.dalamud = dalamud;
} }
/// <summary>
/// Gets the Dalamud instance.
/// </summary>
protected Dalamud Dalamud => this.dalamud;
} }
} }

View file

@ -1,32 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Game.ClientState.Actors.Resolvers namespace Dalamud.Game.ClientState.Actors.Resolvers
{ {
/// <summary> /// <summary>
/// This object represents a class or job. /// This object represents a class or job.
/// </summary> /// </summary>
public class ClassJob : BaseResolver { public class ClassJob : BaseResolver
{
/// <summary> /// <summary>
/// ID of the ClassJob. /// ID of the ClassJob.
/// </summary> /// </summary>
public readonly uint Id; public readonly uint Id;
/// <summary> /// <summary>
/// GameData linked to this ClassJob. /// Initializes a new instance of the <see cref="ClassJob"/> class.
/// </summary>
public Lumina.Excel.GeneratedSheets.ClassJob GameData =>
this.dalamud.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.ClassJob>().GetRow(this.Id);
/// <summary>
/// Set up the ClassJob resolver with the provided ID. /// Set up the ClassJob resolver with the provided ID.
/// </summary> /// </summary>
/// <param name="id">The ID of the world.</param> /// <param name="id">The ID of the classJob.</param>
public ClassJob(byte id, Dalamud dalamud) : base(dalamud) { /// <param name="dalamud">The Dalamud instance.</param>
public ClassJob(byte id, Dalamud dalamud)
: base(dalamud)
{
this.Id = id; this.Id = id;
} }
/// <summary>
/// Gets GameData linked to this ClassJob.
/// </summary>
public Lumina.Excel.GeneratedSheets.ClassJob GameData =>
this.Dalamud.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.ClassJob>().GetRow(this.Id);
} }
} }

View file

@ -1,32 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Game.ClientState.Actors.Resolvers namespace Dalamud.Game.ClientState.Actors.Resolvers
{ {
/// <summary> /// <summary>
/// This object represents a world a character can reside on. /// This object represents a world a character can reside on.
/// </summary> /// </summary>
public class World : BaseResolver { public class World : BaseResolver
{
/// <summary> /// <summary>
/// ID of the world. /// ID of the world.
/// </summary> /// </summary>
public readonly uint Id; public readonly uint Id;
/// <summary> /// <summary>
/// GameData linked to this world. /// Initializes a new instance of the <see cref="World"/> class.
/// </summary>
public Lumina.Excel.GeneratedSheets.World GameData =>
this.dalamud.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.World>().GetRow(this.Id);
/// <summary>
/// Set up the world resolver with the provided ID. /// Set up the world resolver with the provided ID.
/// </summary> /// </summary>
/// <param name="id">The ID of the world.</param> /// <param name="id">The ID of the world.</param>
public World(ushort id, Dalamud dalamud) : base(dalamud) { /// <param name="dalamud">The Dalamud instance.</param>
public World(ushort id, Dalamud dalamud)
: base(dalamud)
{
this.Id = id; this.Id = id;
} }
/// <summary>
/// Gets GameData linked to this world.
/// </summary>
public Lumina.Excel.GeneratedSheets.World GameData =>
this.Dalamud.Data.GetExcelSheet<Lumina.Excel.GeneratedSheets.World>().GetRow(this.Id);
} }
} }

View file

@ -1,50 +1,118 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Actors.Types; using Dalamud.Game.ClientState.Actors.Types;
namespace Dalamud.Game.ClientState.Actors { namespace Dalamud.Game.ClientState.Actors
public static class TargetOffsets { {
public const int CurrentTarget = 0x80; /// <summary>
public const int MouseOverTarget = 0xD0; /// Get and set various kinds of targets for the player.
public const int FocusTarget = 0xF8; /// </summary>
public const int PreviousTarget = 0x110; public sealed class Targets
public const int SoftTarget = 0x88; {
}
public sealed class Targets {
private ClientStateAddressResolver Address { get; }
private Dalamud dalamud; private Dalamud dalamud;
private ClientStateAddressResolver address;
public Actor CurrentTarget => GetActorByOffset(TargetOffsets.CurrentTarget); /// <summary>
public Actor MouseOverTarget => GetActorByOffset(TargetOffsets.MouseOverTarget); /// Initializes a new instance of the <see cref="Targets"/> class.
public Actor FocusTarget => GetActorByOffset(TargetOffsets.FocusTarget); /// </summary>
public Actor PreviousTarget => GetActorByOffset(TargetOffsets.PreviousTarget); /// <param name="dalamud">The Dalamud instance.</param>
public Actor SoftTarget => GetActorByOffset(TargetOffsets.SoftTarget); /// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
internal Targets(Dalamud dalamud, ClientStateAddressResolver addressResolver)
internal Targets(Dalamud dalamud, ClientStateAddressResolver addressResolver) { {
this.dalamud = dalamud; this.dalamud = dalamud;
Address = addressResolver; this.address = addressResolver;
} }
public void SetCurrentTarget(Actor actor) => SetTarget(actor?.Address ?? IntPtr.Zero, TargetOffsets.CurrentTarget); /// <summary>
public void SetCurrentTarget(IntPtr actorAddress) => SetTarget(actorAddress, TargetOffsets.CurrentTarget); /// Gets the current target.
/// </summary>
public Actor CurrentTarget => this.GetActorByOffset(TargetOffsets.CurrentTarget);
public void SetFocusTarget(Actor actor) => SetTarget(actor?.Address ?? IntPtr.Zero, TargetOffsets.FocusTarget); /// <summary>
public void SetFocusTarget(IntPtr actorAddress) => SetTarget(actorAddress, TargetOffsets.FocusTarget); /// Gets the mouseover target.
/// </summary>
public Actor MouseOverTarget => this.GetActorByOffset(TargetOffsets.MouseOverTarget);
public void ClearCurrentTarget() => SetCurrentTarget(IntPtr.Zero); /// <summary>
public void ClearFocusTarget() => SetFocusTarget(IntPtr.Zero); /// Gets the focus target.
/// </summary>
public Actor FocusTarget => this.GetActorByOffset(TargetOffsets.FocusTarget);
private void SetTarget(IntPtr actorAddress, int offset) { /// <summary>
if (Address.TargetManager == IntPtr.Zero) return; /// Gets the previous target.
Marshal.WriteIntPtr(Address.TargetManager, offset, actorAddress); /// </summary>
public Actor PreviousTarget => this.GetActorByOffset(TargetOffsets.PreviousTarget);
/// <summary>
/// Gets the soft target.
/// </summary>
public Actor SoftTarget => this.GetActorByOffset(TargetOffsets.SoftTarget);
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetCurrentTarget(Actor actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero, TargetOffsets.CurrentTarget);
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetCurrentTarget(IntPtr actorAddress) => this.SetTarget(actorAddress, TargetOffsets.CurrentTarget);
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actor">Actor to focus.</param>
public void SetFocusTarget(Actor actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero, TargetOffsets.FocusTarget);
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actorAddress">Actor (address) to focus.</param>
public void SetFocusTarget(IntPtr actorAddress) => this.SetTarget(actorAddress, TargetOffsets.FocusTarget);
/// <summary>
/// Clears the current target.
/// </summary>
public void ClearCurrentTarget() => this.SetCurrentTarget(IntPtr.Zero);
/// <summary>
/// Clears the focus target.
/// </summary>
public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero);
private void SetTarget(IntPtr actorAddress, int offset)
{
if (this.address.TargetManager == IntPtr.Zero)
return;
Marshal.WriteIntPtr(this.address.TargetManager, offset, actorAddress);
} }
private Actor GetActorByOffset(int offset) { private Actor GetActorByOffset(int offset)
if (Address.TargetManager == IntPtr.Zero) return null; {
var actorAddress = Marshal.ReadIntPtr(Address.TargetManager + offset); if (this.address.TargetManager == IntPtr.Zero)
if (actorAddress == IntPtr.Zero) return null; return null;
var actorAddress = Marshal.ReadIntPtr(this.address.TargetManager + offset);
if (actorAddress == IntPtr.Zero)
return null;
return this.dalamud.ClientState.Actors.ReadActorFromMemory(actorAddress); return this.dalamud.ClientState.Actors.ReadActorFromMemory(actorAddress);
} }
} }
/// <summary>
/// Memory offsets for the <see cref="Targets"/> type.
/// </summary>
public static class TargetOffsets
{
public const int CurrentTarget = 0x80;
public const int SoftTarget = 0x88;
public const int MouseOverTarget = 0xD0;
public const int FocusTarget = 0xF8;
public const int PreviousTarget = 0x110;
}
} }

View file

@ -10,8 +10,6 @@ namespace Dalamud.Game.ClientState.Actors.Types
public class Actor : IEquatable<Actor> public class Actor : IEquatable<Actor>
{ {
private readonly Structs.Actor actorStruct; private readonly Structs.Actor actorStruct;
// This is a breaking change. StyleCop demands it.
// private readonly IntPtr address;
private readonly Dalamud dalamud; private readonly Dalamud dalamud;
/// <summary> /// <summary>
@ -83,8 +81,6 @@ namespace Dalamud.Game.ClientState.Actors.Types
/// <summary> /// <summary>
/// Gets the address of this actor in memory. /// Gets the address of this actor in memory.
/// </summary> /// </summary>
// TODO: This is a breaking change, StyleCop demands it.
// public IntPtr Address => this.address;
public readonly IntPtr Address; public readonly IntPtr Address;
/// <summary> /// <summary>

View file

@ -29,7 +29,7 @@ namespace Dalamud.Game.ClientState.Actors.Types
/// <summary> /// <summary>
/// Gets the ClassJob of this Chara. /// Gets the ClassJob of this Chara.
/// </summary> /// </summary>
public ClassJob ClassJob => new ClassJob(this.ActorStruct.ClassJob, this.Dalamud); public ClassJob ClassJob => new(this.ActorStruct.ClassJob, this.Dalamud);
/// <summary> /// <summary>
/// Gets the current HP of this Chara. /// Gets the current HP of this Chara.

View file

@ -2,27 +2,51 @@ using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Actors.Types namespace Dalamud.Game.ClientState.Actors.Types
{ {
/// <summary>
/// This class represents a party member.
/// </summary>
public class PartyMember public class PartyMember
{ {
/// <summary>
/// The name of the character.
/// </summary>
public string CharacterName; public string CharacterName;
/// <summary>
/// Unknown.
/// </summary>
public long Unknown; public long Unknown;
/// <summary>
/// The actor object that corresponds to this party member.
/// </summary>
public Actor Actor; public Actor Actor;
/// <summary>
/// The kind or type of actor.
/// </summary>
public ObjectKind ObjectKind; public ObjectKind ObjectKind;
/// <summary>
/// Initializes a new instance of the <see cref="PartyMember"/> class.
/// </summary>
/// <param name="table">The ActorTable instance.</param>
/// <param name="rawData">The interop data struct.</param>
public PartyMember(ActorTable table, Structs.PartyMember rawData) public PartyMember(ActorTable table, Structs.PartyMember rawData)
{ {
CharacterName = Marshal.PtrToStringAnsi(rawData.namePtr); this.CharacterName = Marshal.PtrToStringAnsi(rawData.namePtr);
Unknown = rawData.unknown; this.Unknown = rawData.unknown;
Actor = null; this.Actor = null;
for (var i = 0; i < table.Length; i++) for (var i = 0; i < table.Length; i++)
{ {
if (table[i] != null && table[i].ActorId == rawData.actorId) if (table[i] != null && table[i].ActorId == rawData.actorId)
{ {
Actor = table[i]; this.Actor = table[i];
break; break;
} }
} }
ObjectKind = rawData.objectKind;
this.ObjectKind = rawData.objectKind;
} }
} }
} }

View file

@ -31,12 +31,12 @@ namespace Dalamud.Game.ClientState.Actors.Types
/// <summary> /// <summary>
/// Gets the current <see cref="World">world</see> of the character. /// Gets the current <see cref="World">world</see> of the character.
/// </summary> /// </summary>
public World CurrentWorld => new World(this.ActorStruct.CurrentWorld, this.Dalamud); public World CurrentWorld => new(this.ActorStruct.CurrentWorld, this.Dalamud);
/// <summary> /// <summary>
/// Gets the home <see cref="World">world</see> of the character. /// Gets the home <see cref="World">world</see> of the character.
/// </summary> /// </summary>
public World HomeWorld => new World(this.ActorStruct.HomeWorld, this.Dalamud); public World HomeWorld => new(this.ActorStruct.HomeWorld, this.Dalamud);
/// <summary> /// <summary>
/// Gets the Free Company tag of this player. /// Gets the Free Company tag of this player.

View file

@ -1,10 +1,10 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Actors; using Dalamud.Game.ClientState.Actors;
using Dalamud.Game.ClientState.Actors.Types; using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.Internal; using Dalamud.Game.Internal;
using Dalamud.Game.Internal.Network;
using Dalamud.Hooking; using Dalamud.Hooking;
using JetBrains.Annotations; using JetBrains.Annotations;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
@ -15,41 +15,17 @@ namespace Dalamud.Game.ClientState
/// <summary> /// <summary>
/// This class represents the state of the game client at the time of access. /// This class represents the state of the game client at the time of access.
/// </summary> /// </summary>
public class ClientState : INotifyPropertyChanged, IDisposable { public class ClientState : INotifyPropertyChanged, IDisposable
private readonly Dalamud dalamud; {
public event PropertyChangedEventHandler PropertyChanged;
private ClientStateAddressResolver Address { get; }
public readonly ClientLanguage ClientLanguage;
/// <summary> /// <summary>
/// The table of all present actors. /// The table of all present actors.
/// </summary> /// </summary>
public readonly ActorTable Actors; public readonly ActorTable Actors;
/// <summary> /// <summary>
/// The local player character, if one is present. /// Gets the language of the client.
/// </summary> /// </summary>
[CanBeNull] public readonly ClientLanguage ClientLanguage;
public PlayerCharacter LocalPlayer {
get {
var actor = this.Actors[0];
if (actor is PlayerCharacter pc)
return pc;
return null;
}
}
#region TerritoryType
// TODO: The hooking logic for this should go into a separate class.
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType);
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
/// <summary> /// <summary>
/// The current Territory the player resides in. /// The current Territory the player resides in.
@ -57,39 +33,12 @@ namespace Dalamud.Game.ClientState
public ushort TerritoryType; public ushort TerritoryType;
/// <summary> /// <summary>
/// Event that gets fired when the current Territory changes. /// The class facilitating Job Gauge data access.
/// </summary>
public EventHandler<ushort> TerritoryChanged;
/// <summary>
/// Event that gets fired when a duty is ready.
/// </summary>
public event EventHandler<ContentFinderCondition> CfPop;
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
{
this.TerritoryType = terriType;
this.TerritoryChanged?.Invoke(this, terriType);
Log.Debug("TerritoryType changed: {0}", terriType);
return this.setupTerritoryTypeHook.Original(manager, terriType);
}
#endregion
/// <summary>
/// The content ID of the local character.
/// </summary>
public ulong LocalContentId => (ulong) Marshal.ReadInt64(Address.LocalContentId);
/// <summary>
/// The class facilitating Job Gauge data access
/// </summary> /// </summary>
public JobGauges JobGauges; public JobGauges JobGauges;
/// <summary> /// <summary>
/// The class facilitating party list data access /// The class facilitating party list data access.
/// </summary> /// </summary>
public PartyList PartyList; public PartyList PartyList;
@ -109,70 +58,69 @@ namespace Dalamud.Game.ClientState
public Condition Condition; public Condition Condition;
/// <summary> /// <summary>
/// The class facilitating target data access /// The class facilitating target data access.
/// </summary> /// </summary>
public Targets Targets; public Targets Targets;
/// <summary> /// <summary>
/// Event that gets fired when the current Territory changes.
/// </summary>
public EventHandler<ushort> TerritoryChanged;
private readonly Dalamud dalamud;
private readonly ClientStateAddressResolver address;
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
private bool lastConditionNone = true;
/// <summary>
/// Initializes a new instance of the <see cref="ClientState"/> class.
/// Set up client state access. /// Set up client state access.
/// </summary> /// </summary>
/// <param name="dalamud">Dalamud instance</param> /// <param name="dalamud">Dalamud instance.</param>
/// /// <param name="startInfo">StartInfo of the current Dalamud launch</param> /// <param name="startInfo">StartInfo of the current Dalamud launch.</param>
/// <param name="scanner">Sig scanner</param> /// <param name="scanner">Sig scanner.</param>
public ClientState(Dalamud dalamud, DalamudStartInfo startInfo, SigScanner scanner) { public ClientState(Dalamud dalamud, DalamudStartInfo startInfo, SigScanner scanner)
{
this.dalamud = dalamud; this.dalamud = dalamud;
Address = new ClientStateAddressResolver(); this.address = new ClientStateAddressResolver();
Address.Setup(scanner); this.address.Setup(scanner);
Log.Verbose("===== C L I E N T S T A T E ====="); Log.Verbose("===== C L I E N T S T A T E =====");
this.ClientLanguage = startInfo.Language; this.ClientLanguage = startInfo.Language;
this.Actors = new ActorTable(dalamud, Address); this.Actors = new ActorTable(dalamud, this.address);
this.PartyList = new PartyList(dalamud, Address); this.PartyList = new PartyList(dalamud, this.address);
this.JobGauges = new JobGauges(Address); this.JobGauges = new JobGauges(this.address);
this.KeyState = new KeyState(Address, scanner.Module.BaseAddress); this.KeyState = new KeyState(this.address, scanner.Module.BaseAddress);
this.GamepadState = new GamepadState(this.Address); this.GamepadState = new GamepadState(this.address);
this.Condition = new Condition( Address ); this.Condition = new Condition(this.address);
this.Targets = new Targets(dalamud, Address); this.Targets = new Targets(dalamud, this.address);
Log.Verbose("SetupTerritoryType address {SetupTerritoryType}", Address.SetupTerritoryType); Log.Verbose("SetupTerritoryType address {SetupTerritoryType}", this.address.SetupTerritoryType);
this.setupTerritoryTypeHook = new Hook<SetupTerritoryTypeDelegate>(Address.SetupTerritoryType, this.setupTerritoryTypeHook = new Hook<SetupTerritoryTypeDelegate>(this.address.SetupTerritoryType, new SetupTerritoryTypeDelegate(this.SetupTerritoryTypeDetour), this);
new SetupTerritoryTypeDelegate(SetupTerritoryTypeDetour),
this);
dalamud.Framework.OnUpdateEvent += FrameworkOnOnUpdateEvent; dalamud.Framework.OnUpdateEvent += this.FrameworkOnOnUpdateEvent;
dalamud.NetworkHandlers.CfPop += NetworkHandlersOnCfPop; dalamud.NetworkHandlers.CfPop += this.NetworkHandlersOnCfPop;
} }
private void NetworkHandlersOnCfPop(object sender, ContentFinderCondition e) { [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
CfPop?.Invoke(this, e); private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType);
}
public void Enable() { /// <summary>
this.GamepadState.Enable(); /// Event that fires when a property changes.
this.PartyList.Enable(); /// </summary>
this.setupTerritoryTypeHook.Enable(); #pragma warning disable CS0067
} public event PropertyChangedEventHandler PropertyChanged;
#pragma warning restore
public void Dispose() {
this.PartyList.Dispose();
this.setupTerritoryTypeHook.Dispose();
this.Actors.Dispose();
this.GamepadState.Dispose();
this.dalamud.Framework.OnUpdateEvent -= FrameworkOnOnUpdateEvent;
this.dalamud.NetworkHandlers.CfPop += NetworkHandlersOnCfPop;
}
private bool lastConditionNone = true;
/// <summary> /// <summary>
/// Event that fires when a character is logging in. /// Event that fires when a character is logging in.
@ -184,24 +132,93 @@ namespace Dalamud.Game.ClientState
/// </summary> /// </summary>
public event EventHandler OnLogout; public event EventHandler OnLogout;
/// <summary>
/// Event that gets fired when a duty is ready.
/// </summary>
public event EventHandler<ContentFinderCondition> CfPop;
/// <summary>
/// Gets the local player character, if one is present.
/// </summary>
[CanBeNull]
public PlayerCharacter LocalPlayer
{
get
{
var actor = this.Actors[0];
if (actor is PlayerCharacter pc)
return pc;
return null;
}
}
/// <summary>
/// Gets the content ID of the local character.
/// </summary>
public ulong LocalContentId => (ulong)Marshal.ReadInt64(this.address.LocalContentId);
/// <summary> /// <summary>
/// Gets a value indicating whether a character is logged in. /// Gets a value indicating whether a character is logged in.
/// </summary> /// </summary>
public bool IsLoggedIn { get; private set; } public bool IsLoggedIn { get; private set; }
private void FrameworkOnOnUpdateEvent(Framework framework) { /// <summary>
if (this.Condition.Any() && this.lastConditionNone == true) { /// Enable this module.
/// </summary>
public void Enable()
{
this.GamepadState.Enable();
this.PartyList.Enable();
this.setupTerritoryTypeHook.Enable();
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.PartyList.Dispose();
this.setupTerritoryTypeHook.Dispose();
this.Actors.Dispose();
this.GamepadState.Dispose();
this.dalamud.Framework.OnUpdateEvent -= this.FrameworkOnOnUpdateEvent;
this.dalamud.NetworkHandlers.CfPop += this.NetworkHandlersOnCfPop;
}
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
{
this.TerritoryType = terriType;
this.TerritoryChanged?.Invoke(this, terriType);
Log.Debug("TerritoryType changed: {0}", terriType);
return this.setupTerritoryTypeHook.Original(manager, terriType);
}
private void NetworkHandlersOnCfPop(object sender, ContentFinderCondition e)
{
this.CfPop?.Invoke(this, e);
}
private void FrameworkOnOnUpdateEvent(Framework framework)
{
if (this.Condition.Any() && this.lastConditionNone == true)
{
Log.Debug("Is login"); Log.Debug("Is login");
this.lastConditionNone = false; this.lastConditionNone = false;
this.IsLoggedIn = true; this.IsLoggedIn = true;
OnLogin?.Invoke(this, null); this.OnLogin?.Invoke(this, null);
} }
if (!this.Condition.Any() && this.lastConditionNone == false) { if (!this.Condition.Any() && this.lastConditionNone == false)
{
Log.Debug("Is logout"); Log.Debug("Is logout");
this.lastConditionNone = true; this.lastConditionNone = true;
this.IsLoggedIn = false; this.IsLoggedIn = false;
OnLogout?.Invoke(this, null); this.OnLogout?.Invoke(this, null);
} }
} }
} }

View file

@ -1,50 +1,88 @@
using System; using System;
using Dalamud.Game.Internal; using Dalamud.Game.Internal;
namespace Dalamud.Game.ClientState namespace Dalamud.Game.ClientState
{ {
public sealed class ClientStateAddressResolver : BaseAddressResolver { /// <summary>
/// Client state memory address resolver.
/// </summary>
public sealed class ClientStateAddressResolver : BaseAddressResolver
{
// Static offsets // Static offsets
/// <summary>
/// Gets the address of the actor table.
/// </summary>
public IntPtr ActorTable { get; private set; } public IntPtr ActorTable { get; private set; }
// public IntPtr ViewportActorTable { get; private set; } // public IntPtr ViewportActorTable { get; private set; }
/// <summary>
/// Gets the address of the local content id.
/// </summary>
public IntPtr LocalContentId { get; private set; } public IntPtr LocalContentId { get; private set; }
/// <summary>
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { get; private set; } public IntPtr JobGaugeData { get; private set; }
/// <summary>
/// Gets the address of the keyboard state.
/// </summary>
public IntPtr KeyboardState { get; private set; } public IntPtr KeyboardState { get; private set; }
/// <summary>
/// Gets the address of the target manager.
/// </summary>
public IntPtr TargetManager { get; private set; } public IntPtr TargetManager { get; private set; }
/// <summary>
/// Gets the address of the condition flag array.
/// </summary>
public IntPtr ConditionFlags { get; private set; }
// Functions // Functions
/// <summary>
/// Gets the address of the method which sets the territory type.
/// </summary>
public IntPtr SetupTerritoryType { get; private set; } public IntPtr SetupTerritoryType { get; private set; }
// public IntPtr SomeActorTableAccess { get; private set; } // public IntPtr SomeActorTableAccess { get; private set; }
// public IntPtr PartyListUpdate { get; private set; } // public IntPtr PartyListUpdate { get; private set; }
/// <summary> /// <summary>
/// Game function which polls the gamepads for data. /// Gets the address of the method which polls the gamepads for data.
///
/// Called every frame, even when `Enable Gamepad` is off in the settings. /// Called every frame, even when `Enable Gamepad` is off in the settings.
/// </summary> /// </summary>
public IntPtr GamepadPoll { get; private set; } public IntPtr GamepadPoll { get; private set; }
public IntPtr ConditionFlags { get; private set; } /// <summary>
/// Scan for and setup any configured address pointers.
protected override void Setup64Bit(SigScanner sig) { /// </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 // 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; // ViewportActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 85 ED", 0) + 0x148;
// SomeActorTableAccess = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 55 A0 48 8D 8E ?? ?? ?? ??"); // SomeActorTableAccess = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 55 A0 48 8D 8E ?? ?? ?? ??");
ActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83"); this.ActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83");
LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 05 ?? ?? ?? ?? 48 39 07"); this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 05 ?? ?? ?? ?? 48 39 07");
JobGaugeData = sig.GetStaticAddressFromSig("E8 ?? ?? ?? ?? FF C6 48 8D 5B 0C", 0xB9) + 0x10; this.JobGaugeData = sig.GetStaticAddressFromSig("E8 ?? ?? ?? ?? FF C6 48 8D 5B 0C", 0xB9) + 0x10;
SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F9 66 89 91 ?? ?? ?? ??"); this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F9 66 89 91 ?? ?? ?? ??");
// This resolves to a fixed offset only, without the base address added in, so GetStaticAddressFromSig() can't be used // This resolves to a fixed offset only, without the base address added in, so GetStaticAddressFromSig() can't be used
KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4; this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4;
// PartyListUpdate = sig.ScanText("E8 ?? ?? ?? ?? 49 8B D7 4C 8D 86 ?? ?? ?? ??"); // PartyListUpdate = sig.ScanText("E8 ?? ?? ?? ?? 49 8B D7 4C 8D 86 ?? ?? ?? ??");
ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? BA ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 01 48 83 C4 30"); this.ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? BA ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 01 48 83 C4 30");
TargetManager = sig.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? FF 50 ?? 48 85 DB", 3); this.TargetManager = sig.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? FF 50 ?? 48 85 DB", 3);
this.GamepadPoll = sig.ScanText("40 ?? 57 41 ?? 48 81 EC ?? ?? ?? ?? 44 0F ?? ?? ?? ?? ?? ?? ?? 48 8B"); this.GamepadPoll = sig.ScanText("40 ?? 57 41 ?? 48 81 EC ?? ?? ?? ?? 44 0F ?? ?? ?? ?? ?? ?? ?? 48 8B");
} }

View file

@ -1,7 +1,4 @@
using System; using System;
using System.Runtime.CompilerServices;
using Dalamud.Hooking;
using Serilog;
namespace Dalamud.Game.ClientState namespace Dalamud.Game.ClientState
{ {
@ -10,22 +7,30 @@ namespace Dalamud.Game.ClientState
/// </summary> /// </summary>
public class Condition public class Condition
{ {
internal readonly IntPtr conditionArrayBase;
/// <summary> /// <summary>
/// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// 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>
public const int MaxConditionEntries = 100; public const int MaxConditionEntries = 100;
/// <summary>
/// Initializes a new instance of the <see cref="Condition"/> class.
/// </summary>
/// <param name="resolver">The ClientStateAddressResolver instance.</param>
internal Condition(ClientStateAddressResolver resolver) internal Condition(ClientStateAddressResolver resolver)
{ {
this.conditionArrayBase = resolver.ConditionFlags; this.ConditionArrayBase = resolver.ConditionFlags;
} }
/// <summary>
/// Gets the condition array base pointer.
/// Would typically be private but is used in /xldata windows.
/// </summary>
internal IntPtr ConditionArrayBase { get; private set; }
/// <summary> /// <summary>
/// Check the value of a specific condition/state flag. /// Check the value of a specific condition/state flag.
/// </summary> /// </summary>
/// <param name="flag">The condition flag to check</param> /// <param name="flag">The condition flag to check.</param>
public unsafe bool this[ConditionFlag flag] public unsafe bool this[ConditionFlag flag]
{ {
get get
@ -35,11 +40,16 @@ namespace Dalamud.Game.ClientState
if (idx > MaxConditionEntries || idx < 0) if (idx > MaxConditionEntries || idx < 0)
return false; return false;
return *( bool* )( this.conditionArrayBase + idx ); return *(bool*)(this.ConditionArrayBase + idx);
} }
} }
public bool Any() { /// <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++) for (var i = 0; i < MaxConditionEntries; i++)
{ {
var typedCondition = (ConditionFlag)i; var typedCondition = (ConditionFlag)i;

View file

@ -6,7 +6,8 @@ namespace Dalamud.Game.ClientState
/// These come from LogMessage (somewhere) and directly map to each state field managed by the client. As of 5.25, it maps to /// 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. /// 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 { public enum ConditionFlag
{
/// <summary> /// <summary>
/// Unused. /// Unused.
/// </summary> /// </summary>
@ -27,9 +28,6 @@ namespace Dalamud.Game.ClientState
/// </summary> /// </summary>
Emoting = 3, Emoting = 3,
/// <summary>
/// Unable to execute command while mounted.
/// </summary>
/// <summary> /// <summary>
/// Unable to execute command while mounted. /// Unable to execute command while mounted.
/// </summary> /// </summary>

View file

@ -126,7 +126,7 @@ namespace Dalamud.Game.ClientState
/// Gets or sets a value indicating whether detour should block gamepad input for game. /// Gets or sets a value indicating whether detour should block gamepad input for game.
/// ///
/// Ideally, we would use /// Ideally, we would use
/// (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0 /// (ImGui.GetIO().ConfigFlags &amp; ImGuiConfigFlags.NavEnableGamepad) > 0
/// but this has a race condition during load with the detour which sets up ImGui /// 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. /// and throws if our detour gets called before the other.
/// </summary> /// </summary>

View file

@ -1,22 +1,26 @@
using Serilog;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Serilog;
namespace Dalamud.Game.ClientState namespace Dalamud.Game.ClientState
{ {
/// <summary> /// <summary>
/// Wrapper around the game keystate buffer, which contains the pressed state for /// Wrapper around the game keystate buffer, which contains the pressed state for all keyboard keys, indexed by virtual vkCode.
/// all keyboard keys, indexed by virtual vkCode
/// </summary> /// </summary>
public class KeyState public class KeyState
{ {
private IntPtr bufferBase;
// The array is accessed in a way that this limit doesn't appear to exist // The array is accessed in a way that this limit doesn't appear to exist
// but there is other state data past this point, and keys beyond here aren't // but there is other state data past this point, and keys beyond here aren't
// generally valid for most things anyway // generally valid for most things anyway
private const int MaxKeyCodeIndex = 0xA0; private const int MaxKeyCodeIndex = 0xA0;
private IntPtr bufferBase;
/// <summary>
/// Initializes a new instance of the <see cref="KeyState"/> class.
/// </summary>
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
/// <param name="moduleBaseAddress">The base address of the main process module.</param>
public KeyState(ClientStateAddressResolver addressResolver, IntPtr moduleBaseAddress) public KeyState(ClientStateAddressResolver addressResolver, IntPtr moduleBaseAddress)
{ {
this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState); this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState);
@ -36,7 +40,7 @@ namespace Dalamud.Game.ClientState
if (vkCode < 0 || vkCode > MaxKeyCodeIndex) if (vkCode < 0 || vkCode > MaxKeyCodeIndex)
throw new ArgumentException($"Keycode state only appears to be valid up to {MaxKeyCodeIndex}"); throw new ArgumentException($"Keycode state only appears to be valid up to {MaxKeyCodeIndex}");
return (Marshal.ReadInt32(this.bufferBase + (4 * vkCode)) != 0); return Marshal.ReadInt32(this.bufferBase + (4 * vkCode)) != 0;
} }
set set

View file

@ -1,96 +1,139 @@
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Hooking;
using Dalamud.Plugin;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks; using Dalamud.Game.ClientState.Actors.Types;
// using Dalamud.Hooking;
namespace Dalamud.Game.ClientState namespace Dalamud.Game.ClientState
{ {
public class PartyList : IReadOnlyCollection<PartyMember>, ICollection, IDisposable /// <summary>
/// This class represents the members of your party.
/// </summary>
public sealed partial class PartyList
{ {
private ClientStateAddressResolver Address { get; } private readonly Dalamud dalamud;
private Dalamud dalamud; private readonly ClientStateAddressResolver address;
private delegate long PartyListUpdateDelegate(IntPtr structBegin, long param2, char param3); // private bool isReady = false;
// private IntPtr partyListBegin;
private Hook<PartyListUpdateDelegate> partyListUpdateHook; // private Hook<PartyListUpdateDelegate> partyListUpdateHook;
private IntPtr partyListBegin;
private bool isReady = false;
/// <summary>
/// Initializes a new instance of the <see cref="PartyList"/> class.
/// </summary>
/// <param name="dalamud">The Dalamud instance.</param>
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
public PartyList(Dalamud dalamud, ClientStateAddressResolver addressResolver) public PartyList(Dalamud dalamud, ClientStateAddressResolver addressResolver)
{ {
Address = addressResolver; this.address = addressResolver;
this.dalamud = dalamud; this.dalamud = dalamud;
// this.partyListUpdateHook = new Hook<PartyListUpdateDelegate>(Address.PartyListUpdate, new PartyListUpdateDelegate(PartyListUpdateDetour), this); // this.partyListUpdateHook = new Hook<PartyListUpdateDelegate>(Address.PartyListUpdate, new PartyListUpdateDelegate(PartyListUpdateDetour), this);
} }
private delegate long PartyListUpdateDelegate(IntPtr structBegin, long param2, char param3);
/// <summary>
/// Gets the length of the PartyList.
/// </summary>
public int Length => 0; // !this.isReady ? 0 : Marshal.ReadByte(this.partyListBegin + 0xF0);
/// <summary>
/// Get the nth party member.
/// </summary>
/// <param name="index">Index of the party member.</param>
/// <returns>The party member.</returns>
public PartyMember this[int index]
{
get
{
return null;
// if (!this.isReady)
// return null;
// if (index >= this.Length)
// return null;
// var tblIndex = this.partyListBegin + (index * 24);
// var memberStruct = Marshal.PtrToStructure<Structs.PartyMember>(tblIndex);
// return new PartyMember(this.dalamud.ClientState.Actors, memberStruct);
}
}
/// <summary>
/// Enable this module.
/// </summary>
public void Enable() public void Enable()
{ {
// TODO Fix for 5.3 // TODO Fix for 5.3
// this.partyListUpdateHook.Enable(); // this.partyListUpdateHook.Enable();
} }
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose() public void Dispose()
{ {
// if (!this.isReady) // if (!this.isReady)
// this.partyListUpdateHook.Dispose(); // this.partyListUpdateHook.Dispose();
this.isReady = false; // this.isReady = false;
} }
private long PartyListUpdateDetour(IntPtr structBegin, long param2, char param3) // private long PartyListUpdateDetour(IntPtr structBegin, long param2, char param3)
{ // {
var result = this.partyListUpdateHook.Original(structBegin, param2, param3); // var result = this.partyListUpdateHook.Original(structBegin, param2, param3);
this.partyListBegin = structBegin + 0xB48; // this.partyListBegin = structBegin + 0xB48;
this.partyListUpdateHook.Dispose(); // this.partyListUpdateHook.Dispose();
this.isReady = true; // this.isReady = true;
return result; // return result;
// }
} }
public PartyMember this[int index] /// <summary>
/// Implements IReadOnlyCollection, IEnumerable.
/// </summary>
public sealed partial class PartyList : IReadOnlyCollection<PartyMember>
{ {
get { /// <inheritdoc/>
if (!this.isReady) int IReadOnlyCollection<PartyMember>.Count => this.Length;
return null;
if (index >= Length)
return null;
var tblIndex = partyListBegin + index * 24;
var memberStruct = Marshal.PtrToStructure<Structs.PartyMember>(tblIndex);
return new PartyMember(this.dalamud.ClientState.Actors, memberStruct);
}
}
public void CopyTo(Array array, int index) /// <inheritdoc/>
public IEnumerator<PartyMember> GetEnumerator()
{ {
for (var i = 0; i < Length; i++) for (var i = 0; i < this.Length; i++)
{
if (this[i] != null)
{ {
array.SetValue(this[i], index);
index++;
}
}
public IEnumerator<PartyMember> GetEnumerator() {
for (var i = 0; i < Length; i++) {
if (this[i] != null) {
yield return this[i]; yield return this[i];
} }
} }
} }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
public int Length => !this.isReady ? 0 : Marshal.ReadByte(partyListBegin + 0xF0); /// <summary>
/// Implements ICollection.
int IReadOnlyCollection<PartyMember>.Count => Length; /// </summary>
public sealed partial class PartyList : ICollection
public int Count => Length; {
/// <inheritdoc/>
public int Count => this.Length;
/// <inheritdoc/>
public object SyncRoot => this; public object SyncRoot => this;
/// <inheritdoc/>
public bool IsSynchronized => false; public bool IsSynchronized => false;
/// <inheritdoc/>
public void CopyTo(Array array, int index)
{
for (var i = 0; i < this.Length; i++)
{
array.SetValue(this[i], index);
index++;
}
}
} }
} }

View file

@ -4,7 +4,245 @@ using Dalamud.Game.ClientState.Actors;
namespace Dalamud.Game.ClientState.Structs namespace Dalamud.Game.ClientState.Structs
{ {
public class ActorOffsets /// <summary>
/// Native memory representation of an FFXIV actor.
/// </summary>
[StructLayout(LayoutKind.Explicit, Pack = 2)]
public struct Actor
{
/// <summary>
/// The actor name.
/// </summary>
[FieldOffset(ActorOffsets.Name)]
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 30)]
public string Name;
/// <summary>
/// The actor's internal id.
/// </summary>
[FieldOffset(ActorOffsets.ActorId)]
public int ActorId;
/// <summary>
/// The actor's data id.
/// </summary>
[FieldOffset(ActorOffsets.DataId)]
public int DataId;
/// <summary>
/// The actor's owner id. This is useful for pets, summons, and the like.
/// </summary>
[FieldOffset(ActorOffsets.OwnerId)]
public int OwnerId;
/// <summary>
/// The type or kind of actor.
/// </summary>
[FieldOffset(ActorOffsets.ObjectKind)]
public ObjectKind ObjectKind;
/// <summary>
/// The sub-type or sub-kind of actor.
/// </summary>
[FieldOffset(ActorOffsets.SubKind)]
public byte SubKind;
/// <summary>
/// Whether the actor is friendly.
/// </summary>
[FieldOffset(ActorOffsets.IsFriendly)]
public bool IsFriendly;
/// <summary>
/// The horizontal distance in game units from the player.
/// </summary>
[FieldOffset(ActorOffsets.YalmDistanceFromPlayerX)]
public byte YalmDistanceFromPlayerX;
/// <summary>
/// The player target status.
/// </summary>
/// <remarks>
/// This is some kind of enum.
/// </remarks>
[FieldOffset(ActorOffsets.PlayerTargetStatus)]
public byte PlayerTargetStatus;
/// <summary>
/// The vertical distance in game units from the player.
/// </summary>
[FieldOffset(ActorOffsets.YalmDistanceFromPlayerY)]
public byte YalmDistanceFromPlayerY;
/// <summary>
/// The (X,Z,Y) position of the actor.
/// </summary>
[FieldOffset(ActorOffsets.Position)]
public Position3 Position;
/// <summary>
/// The rotation of the actor.
/// </summary>
/// <remarks>
/// The rotation is around the vertical axis (yaw), from -pi to pi radians.
/// </remarks>
[FieldOffset(ActorOffsets.Rotation)]
public float Rotation;
/// <summary>
/// The hitbox radius of the actor.
/// </summary>
[FieldOffset(ActorOffsets.HitboxRadius)]
public float HitboxRadius;
/// <summary>
/// The current HP of the actor.
/// </summary>
[FieldOffset(ActorOffsets.CurrentHp)]
public int CurrentHp;
/// <summary>
/// The max HP of the actor.
/// </summary>
[FieldOffset(ActorOffsets.MaxHp)]
public int MaxHp;
/// <summary>
/// The current MP of the actor.
/// </summary>
[FieldOffset(ActorOffsets.CurrentMp)]
public int CurrentMp;
/// <summary>
/// The max MP of the actor.
/// </summary>
[FieldOffset(ActorOffsets.MaxMp)]
public short MaxMp;
/// <summary>
/// The current GP of the actor.
/// </summary>
[FieldOffset(ActorOffsets.CurrentGp)]
public short CurrentGp;
/// <summary>
/// The max GP of the actor.
/// </summary>
[FieldOffset(ActorOffsets.MaxGp)]
public short MaxGp;
/// <summary>
/// The current CP of the actor.
/// </summary>
[FieldOffset(ActorOffsets.CurrentCp)]
public short CurrentCp;
/// <summary>
/// The max CP of the actor.
/// </summary>
[FieldOffset(ActorOffsets.MaxCp)]
public short MaxCp;
/// <summary>
/// The class-job of the actor.
/// </summary>
[FieldOffset(ActorOffsets.ClassJob)]
public byte ClassJob;
/// <summary>
/// The level of the actor.
/// </summary>
[FieldOffset(ActorOffsets.Level)]
public byte Level;
/// <summary>
/// The (player character) actor ID being targeted by the actor.
/// </summary>
[FieldOffset(ActorOffsets.PlayerCharacterTargetActorId)]
public int PlayerCharacterTargetActorId;
/// <summary>
/// The customization byte/bitfield of the actor.
/// </summary>
[FieldOffset(ActorOffsets.Customize)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)]
public byte[] Customize;
// Normally pack=2 should work, but ByTVal or Injection breaks this.
// [FieldOffset(ActorOffsets.CompanyTag)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public string CompanyTag;
/// <summary>
/// The (battle npc) actor ID being targeted by the actor.
/// </summary>
[FieldOffset(ActorOffsets.BattleNpcTargetActorId)]
public int BattleNpcTargetActorId;
/// <summary>
/// The name ID of the actor.
/// </summary>
[FieldOffset(ActorOffsets.NameId)]
public int NameId;
/// <summary>
/// The current world ID of the actor.
/// </summary>
[FieldOffset(ActorOffsets.CurrentWorld)]
public ushort CurrentWorld;
/// <summary>
/// The home world ID of the actor.
/// </summary>
[FieldOffset(ActorOffsets.HomeWorld)]
public ushort HomeWorld;
/// <summary>
/// Whether the actor is currently casting.
/// </summary>
[FieldOffset(ActorOffsets.IsCasting)]
public bool IsCasting;
/// <summary>
/// Whether the actor is currently casting (dup?).
/// </summary>
[FieldOffset(ActorOffsets.IsCasting2)]
public bool IsCasting2;
/// <summary>
/// The spell action ID currently being cast by the actor.
/// </summary>
[FieldOffset(ActorOffsets.CurrentCastSpellActionId)]
public uint CurrentCastSpellActionId;
/// <summary>
/// The actor ID of the target currently being cast at by the actor.
/// </summary>
[FieldOffset(ActorOffsets.CurrentCastTargetActorId)]
public uint CurrentCastTargetActorId;
/// <summary>
/// The current casting time of the spell being cast by the actor.
/// </summary>
[FieldOffset(ActorOffsets.CurrentCastTime)]
public float CurrentCastTime;
/// <summary>
/// The total casting time of the spell being cast by the actor.
/// </summary>
[FieldOffset(ActorOffsets.TotalCastTime)]
public float TotalCastTime;
/// <summary>
/// The array of status effects that the actor is currently affected by.
/// </summary>
[FieldOffset(ActorOffsets.UIStatusEffects)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public StatusEffect[] UIStatusEffects;
}
/// <summary>
/// Memory offsets for the <see cref="Actor"/> type.
/// </summary>
public static class ActorOffsets
{ {
// Reference https://github.com/FFXIVAPP/sharlayan-resources/blob/master/structures/5.4/x64.json for more // Reference https://github.com/FFXIVAPP/sharlayan-resources/blob/master/structures/5.4/x64.json for more
public const int Name = 48; // 0x0030 public const int Name = 48; // 0x0030
@ -48,51 +286,4 @@ namespace Dalamud.Game.ClientState.Structs
public const int TotalCastTime = 0x1BB8; public const int TotalCastTime = 0x1BB8;
public const int UIStatusEffects = 0x19F8; public const int UIStatusEffects = 0x19F8;
} }
/// <summary>
/// Native memory representation of a FFXIV actor.
/// </summary>
[StructLayout(LayoutKind.Explicit, Pack = 2)]
public struct Actor
{
[FieldOffset(ActorOffsets.Name)] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 30)] public string Name;
[FieldOffset(ActorOffsets.ActorId)] public int ActorId;
[FieldOffset(ActorOffsets.DataId)] public int DataId;
[FieldOffset(ActorOffsets.OwnerId)] public int OwnerId;
[FieldOffset(ActorOffsets.ObjectKind)] public ObjectKind ObjectKind;
[FieldOffset(ActorOffsets.SubKind)] public byte SubKind;
[FieldOffset(ActorOffsets.IsFriendly)] public bool IsFriendly;
[FieldOffset(ActorOffsets.YalmDistanceFromPlayerX)] public byte YalmDistanceFromPlayerX; // Demo says one of these is x distance
[FieldOffset(ActorOffsets.PlayerTargetStatus)] public byte PlayerTargetStatus; // This is some kind of enum
[FieldOffset(ActorOffsets.YalmDistanceFromPlayerY)] public byte YalmDistanceFromPlayerY; // and the other is z distance
[FieldOffset(ActorOffsets.Position)] public Position3 Position;
[FieldOffset(ActorOffsets.Rotation)] public float Rotation; // Rotation around the vertical axis (yaw), from -pi to pi radians
[FieldOffset(ActorOffsets.HitboxRadius)] public float HitboxRadius;
[FieldOffset(ActorOffsets.CurrentHp)] public int CurrentHp;
[FieldOffset(ActorOffsets.MaxHp)] public int MaxHp;
[FieldOffset(ActorOffsets.CurrentMp)] public int CurrentMp;
[FieldOffset(ActorOffsets.MaxMp)] public short MaxMp;
[FieldOffset(ActorOffsets.CurrentGp)] public short CurrentGp;
[FieldOffset(ActorOffsets.MaxGp)] public short MaxGp;
[FieldOffset(ActorOffsets.CurrentCp)] public short CurrentCp;
[FieldOffset(ActorOffsets.MaxCp)] public short MaxCp;
[FieldOffset(ActorOffsets.ClassJob)] public byte ClassJob;
[FieldOffset(ActorOffsets.Level)] public byte Level;
[FieldOffset(ActorOffsets.PlayerCharacterTargetActorId)] public int PlayerCharacterTargetActorId;
[FieldOffset(ActorOffsets.Customize)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)] public byte[] Customize;
// Normally pack=2 should work, but ByTVal or Injection breaks this.
// [FieldOffset(ActorOffsets.CompanyTag)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public string CompanyTag;
[FieldOffset(ActorOffsets.BattleNpcTargetActorId)] public int BattleNpcTargetActorId;
[FieldOffset(ActorOffsets.NameId)] public int NameId;
[FieldOffset(ActorOffsets.CurrentWorld)] public ushort CurrentWorld;
[FieldOffset(ActorOffsets.HomeWorld)] public ushort HomeWorld;
[FieldOffset(ActorOffsets.IsCasting)] public bool IsCasting;
[FieldOffset(ActorOffsets.IsCasting2)] public bool IsCasting2;
[FieldOffset(ActorOffsets.CurrentCastSpellActionId)] public uint CurrentCastSpellActionId;
[FieldOffset(ActorOffsets.CurrentCastTargetActorId)] public uint CurrentCastTargetActorId;
[FieldOffset(ActorOffsets.CurrentCastTime)] public float CurrentCastTime;
[FieldOffset(ActorOffsets.TotalCastTime)] public float TotalCastTime;
[FieldOffset(ActorOffsets.UIStatusEffects)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] public StatusEffect[] UIStatusEffects;
}
} }

View file

@ -1,5 +1,7 @@
using System; using System;
#pragma warning disable SA1402 // File may only contain a single type
namespace Dalamud.Game.ClientState.Structs.JobGauge namespace Dalamud.Game.ClientState.Structs.JobGauge
{ {
#region AST #region AST
@ -270,3 +272,5 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
#endregion #endregion
} }
#pragma warning restore SA1402 // File may only contain a single type

View file

@ -1,19 +1,26 @@
using Dalamud.Game.ClientState.Actors;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks; using Dalamud.Game.ClientState.Actors;
namespace Dalamud.Game.ClientState.Structs namespace Dalamud.Game.ClientState.Structs
{ {
/// <summary>
/// This represents a native PartyMember class in memory.
/// </summary>
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
public struct PartyMember public struct PartyMember
{ {
[FieldOffset(0x0)] public IntPtr namePtr; [FieldOffset(0x0)]
[FieldOffset(0x8)] public long unknown; public IntPtr namePtr;
[FieldOffset(0x10)] public int actorId;
[FieldOffset(0x14)] public ObjectKind objectKind; [FieldOffset(0x8)]
public long unknown;
[FieldOffset(0x10)]
public int actorId;
[FieldOffset(0x14)]
public ObjectKind objectKind;
} }
} }

View file

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

View file

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

View file

@ -2,45 +2,37 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Internal.Libc;
using Serilog; using Serilog;
namespace Dalamud.Game.Command { namespace Dalamud.Game.Command
{
/// <summary> /// <summary>
/// This class manages registered in-game slash commands. /// This class manages registered in-game slash commands.
/// </summary> /// </summary>
public sealed class CommandManager { public sealed class CommandManager
{
private readonly Dalamud dalamud; private readonly Dalamud dalamud;
private readonly Dictionary<string, CommandInfo> commandMap = new();
private readonly Dictionary<string, CommandInfo> commandMap = new Dictionary<string, CommandInfo>(); private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?<command>.+)$", RegexOptions.Compiled);
/// <summary> private readonly Regex commandRegexDe = new(@"^„(?<command>.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled);
/// Read-only list of all registered commands. private readonly Regex commandRegexFr = new(@"^La commande texte “(?<command>.+)” n'existe pas\.$", RegexOptions.Compiled);
/// </summary>
public ReadOnlyDictionary<string, CommandInfo> Commands =>
new ReadOnlyDictionary<string, CommandInfo>(this.commandMap);
private readonly Regex commandRegexEn =
new Regex(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new Regex(@"^そのコマンドはありません。: (?<command>.+)$", RegexOptions.Compiled);
private readonly Regex commandRegexDe =
new Regex(@"^„(?<command>.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled);
private readonly Regex commandRegexFr =
new Regex(@"^La commande texte “(?<command>.+)” n'existe pas\.$",
RegexOptions.Compiled);
private readonly Regex currentLangCommandRegex; private readonly Regex currentLangCommandRegex;
/// <summary>
public CommandManager(Dalamud dalamud, ClientLanguage language) { /// Initializes a new instance of the <see cref="CommandManager"/> class.
/// </summary>
/// <param name="dalamud">The Dalamud instance.</param>
/// <param name="language">The client language requested.</param>
public CommandManager(Dalamud dalamud, ClientLanguage language)
{
this.dalamud = dalamud; this.dalamud = dalamud;
switch (language) { switch (language)
{
case ClientLanguage.Japanese: case ClientLanguage.Japanese:
this.currentLangCommandRegex = this.commandRegexJp; this.currentLangCommandRegex = this.commandRegexJp;
break; break;
@ -55,40 +47,42 @@ namespace Dalamud.Game.Command {
break; break;
} }
dalamud.Framework.Gui.Chat.OnCheckMessageHandled += OnCheckMessageHandled; dalamud.Framework.Gui.Chat.OnCheckMessageHandled += this.OnCheckMessageHandled;
} }
private void OnCheckMessageHandled(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) { /// <summary>
if (type == XivChatType.ErrorMessage && senderId == 0) { /// Gets a read-only list of all registered commands.
var cmdMatch = this.currentLangCommandRegex.Match(message.TextValue).Groups["command"]; /// </summary>
if (cmdMatch.Success) { public ReadOnlyDictionary<string, CommandInfo> Commands => new(this.commandMap);
// Yes, it's a chat command.
var command = cmdMatch.Value;
if (ProcessCommand(command)) isHandled = true;
}
}
}
/// <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 command;
string argument; string argument;
var separatorPosition = content.IndexOf(' '); var separatorPosition = content.IndexOf(' ');
if (separatorPosition == -1 || separatorPosition + 1 >= content.Length) { if (separatorPosition == -1 || separatorPosition + 1 >= content.Length)
{
// If no space was found or ends with the space. Process them as a no argument // If no space was found or ends with the space. Process them as a no argument
if (separatorPosition + 1 >= content.Length) { if (separatorPosition + 1 >= content.Length)
{
// Remove the trailing space // Remove the trailing space
command = content.Substring(0, separatorPosition); command = content.Substring(0, separatorPosition);
} else { }
else
{
command = content; command = content;
} }
argument = string.Empty; argument = string.Empty;
} else { }
else
{
// e.g.) // e.g.)
// /testcommand arg1 // /testcommand arg1
// => Total of 17 chars // => Total of 17 chars
@ -98,13 +92,13 @@ namespace Dalamud.Game.Command {
command = content.Substring(0, separatorPosition); command = content.Substring(0, separatorPosition);
var argStart = separatorPosition + 1; var argStart = separatorPosition + 1;
argument = content.Substring(argStart, content.Length - argStart); argument = content[argStart..];
} }
if (!this.commandMap.TryGetValue(command, out var handler)) // Commad was not found. if (!this.commandMap.TryGetValue(command, out var handler)) // Commad was not found.
return false; return false;
DispatchCommand(command, argument, handler); this.DispatchCommand(command, argument, handler);
return true; return true;
} }
@ -114,12 +108,15 @@ namespace Dalamud.Game.Command {
/// <param name="command">The command to dispatch.</param> /// <param name="command">The command to dispatch.</param>
/// <param name="argument">The provided arguments.</param> /// <param name="argument">The provided arguments.</param>
/// <param name="info">A <see cref="CommandInfo"/> object describing this command.</param> /// <param name="info">A <see cref="CommandInfo"/> object describing this command.</param>
public void DispatchCommand(string command, string argument, CommandInfo info) { public void DispatchCommand(string command, string argument, CommandInfo info)
try { {
try
{
info.Handler(command, argument); info.Handler(command, argument);
} catch (Exception ex) { }
Log.Error(ex, "Error while dispatching command {CommandName} (Argument: {Argument})", command, catch (Exception ex)
argument); {
Log.Error(ex, "Error while dispatching command {CommandName} (Argument: {Argument})", command, argument);
} }
} }
@ -129,13 +126,17 @@ namespace Dalamud.Game.Command {
/// <param name="command">The command to register.</param> /// <param name="command">The command to register.</param>
/// <param name="info">A <see cref="CommandInfo"/> object describing the command.</param> /// <param name="info">A <see cref="CommandInfo"/> object describing the command.</param>
/// <returns>If adding was successful.</returns> /// <returns>If adding was successful.</returns>
public bool AddHandler(string command, CommandInfo info) { public bool AddHandler(string command, CommandInfo info)
{
if (info == null) throw new ArgumentNullException(nameof(info), "Command handler is null."); if (info == null) throw new ArgumentNullException(nameof(info), "Command handler is null.");
try { try
{
this.commandMap.Add(command, info); this.commandMap.Add(command, info);
return true; return true;
} catch (ArgumentException) { }
catch (ArgumentException)
{
Log.Error("Command {CommandName} is already registered.", command); Log.Error("Command {CommandName} is already registered.", command);
return false; return false;
} }
@ -146,8 +147,23 @@ namespace Dalamud.Game.Command {
/// </summary> /// </summary>
/// <param name="command">The command to remove.</param> /// <param name="command">The command to remove.</param>
/// <returns>If the removal was successful.</returns> /// <returns>If the removal was successful.</returns>
public bool RemoveHandler(string command) { public bool RemoveHandler(string command)
{
return this.commandMap.Remove(command); return this.commandMap.Remove(command);
} }
private void OnCheckMessageHandled(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
{
if (type == XivChatType.ErrorMessage && senderId == 0)
{
var cmdMatch = this.currentLangCommandRegex.Match(message.TextValue).Groups["command"];
if (cmdMatch.Success)
{
// Yes, it's a chat command.
var command = cmdMatch.Value;
if (this.ProcessCommand(command)) isHandled = true;
}
}
}
} }
} }

View file

@ -1,48 +1,118 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Hooking;
using EasyHook;
using Serilog; using Serilog;
namespace Dalamud.Game.Internal namespace Dalamud.Game.Internal
{ {
public class AntiDebug : IDisposable /// <summary>
/// This class disables anti-debug functionality in the game client.
/// </summary>
public sealed partial class AntiDebug
{ {
private IntPtr DebugCheckAddress { get; set; }
public bool IsEnabled { get; private set; }
public AntiDebug(SigScanner scanner) {
try {
DebugCheckAddress = scanner.ScanText("FF 15 ?? ?? ?? ?? 85 C0 74 11 41");
} catch (KeyNotFoundException) {
DebugCheckAddress = IntPtr.Zero;
}
Log.Verbose("DebugCheck address {DebugCheckAddress}", DebugCheckAddress);
}
private readonly byte[] nop = new byte[] { 0x31, 0xC0, 0x90, 0x90, 0x90, 0x90 }; private readonly byte[] nop = new byte[] { 0x31, 0xC0, 0x90, 0x90, 0x90, 0x90 };
private byte[] original; private byte[] original;
private IntPtr debugCheckAddress;
public void Enable() { /// <summary>
/// Initializes a new instance of the <see cref="AntiDebug"/> class.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
public AntiDebug(SigScanner scanner)
{
try
{
this.debugCheckAddress = scanner.ScanText("FF 15 ?? ?? ?? ?? 85 C0 74 11 41");
}
catch (KeyNotFoundException)
{
this.debugCheckAddress = IntPtr.Zero;
}
Log.Verbose("DebugCheck address {DebugCheckAddress}", this.debugCheckAddress);
}
/// <summary>
/// Gets a value indicating whether the anti-debugging is enabled.
/// </summary>
public bool IsEnabled { get; private set; }
/// <summary>
/// Enables the anti-debugging by overwriting code in memory.
/// </summary>
public void Enable()
{
this.original = new byte[this.nop.Length]; this.original = new byte[this.nop.Length];
if (DebugCheckAddress != IntPtr.Zero && !IsEnabled) { if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled)
Log.Information($"Overwriting Debug Check @ 0x{DebugCheckAddress.ToInt64():X}"); {
SafeMemory.ReadBytes(DebugCheckAddress, this.nop.Length, out this.original); Log.Information($"Overwriting debug check @ 0x{this.debugCheckAddress.ToInt64():X}");
SafeMemory.WriteBytes(DebugCheckAddress, this.nop); SafeMemory.ReadBytes(this.debugCheckAddress, this.nop.Length, out this.original);
} else { SafeMemory.WriteBytes(this.debugCheckAddress, this.nop);
Log.Information("DebugCheck already overwritten?"); }
else
{
Log.Information("Debug check already overwritten?");
} }
IsEnabled = true; this.IsEnabled = true;
} }
public void Dispose() { /// <summary>
//if (this.DebugCheckAddress != IntPtr.Zero && this.original != null) /// Disable the anti-debugging by reverting the overwritten code in memory.
// Marshal.Copy(this.original, 0, DebugCheckAddress, this.nop.Length); /// </summary>
public void Disable()
{
if (this.debugCheckAddress != IntPtr.Zero && this.original != null)
{
Log.Information($"Reverting debug check @ 0x{this.debugCheckAddress.ToInt64():X}");
SafeMemory.WriteBytes(this.debugCheckAddress, this.original);
}
else
{
Log.Information("Debug check was not overwritten?");
}
this.IsEnabled = false;
}
}
/// <summary>
/// Implementing IDisposable.
/// </summary>
public sealed partial class AntiDebug : IDisposable
{
private bool disposed = false;
/// <summary>
/// Finalizes an instance of the <see cref="AntiDebug"/> class.
/// </summary>
~AntiDebug() => this.Dispose(false);
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
/// <param name="disposing">If this was disposed through calling Dispose() or from being finalized.</param>
private void Dispose(bool disposing)
{
if (this.disposed)
return;
if (disposing)
{
// If anti-debug is enabled and is being disposed, odds are either the game is exiting, or Dalamud is being reloaded.
// If it is the latter, there's half a chance a debugger is currently attached. There's no real need to disable the
// check in either situation anyways.
// this.Disable();
}
} }
} }
} }

View file

@ -3,50 +3,69 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.Internal { namespace Dalamud.Game.Internal
public abstract class BaseAddressResolver { {
/// <summary>
/// Base memory address resolver.
/// </summary>
public abstract class BaseAddressResolver
{
/// <summary>
/// A list of memory addresses that were found, to list in /xldata.
/// </summary>
public static Dictionary<string, List<(string, IntPtr)>> DebugScannedValues = 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; } protected bool IsResolved { get; set; }
public static Dictionary<string, List<(string, IntPtr)>> DebugScannedValues = new Dictionary<string, List<(string, IntPtr)>>(); /// <summary>
/// Setup the resolver, calling the appopriate method based on the process architecture.
public void Setup(SigScanner scanner) { /// </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 // Because C# don't allow to call virtual function while in ctor
// we have to do this shit :\ // we have to do this shit :\
if (IsResolved) { if (this.IsResolved)
{
return; return;
} }
if (scanner.Is32BitProcess) { if (scanner.Is32BitProcess)
Setup32Bit(scanner); {
} else { this.Setup32Bit(scanner);
Setup64Bit(scanner); }
else
{
this.Setup64Bit(scanner);
} }
SetupInternal(scanner);
var className = GetType().Name; this.SetupInternal(scanner);
var className = this.GetType().Name;
DebugScannedValues[className] = new List<(string, IntPtr)>(); DebugScannedValues[className] = new List<(string, IntPtr)>();
foreach (var property in GetType().GetProperties().Where(x => x.PropertyType == typeof(IntPtr))) { foreach (var property in this.GetType().GetProperties().Where(x => x.PropertyType == typeof(IntPtr)))
{
DebugScannedValues[className].Add((property.Name, (IntPtr)property.GetValue(this))); DebugScannedValues[className].Add((property.Name, (IntPtr)property.GetValue(this)));
} }
IsResolved = true; this.IsResolved = true;
} }
protected virtual void Setup32Bit(SigScanner scanner) { /// <summary>
throw new NotSupportedException("32 bit version is not supported."); /// 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>
protected virtual void Setup64Bit(SigScanner sig) { /// <param name="address">The address of the virtual table.</param>
throw new NotSupportedException("64 bit version is not supported."); /// <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>
protected virtual void SetupInternal(SigScanner scanner) { public T GetVirtualFunction<T>(IntPtr address, int vtableOffset, int count) where T : class
// Do nothing {
}
public T GetVirtualFunction<T>(IntPtr address, int vtableOffset, int count) where T : class {
// Get vtable // Get vtable
var vtable = Marshal.ReadIntPtr(address, vtableOffset); var vtable = Marshal.ReadIntPtr(address, vtableOffset);
@ -55,5 +74,32 @@ namespace Dalamud.Game.Internal {
return Marshal.GetDelegateForFunctionPointer<T>(functionAddress); 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

@ -1,8 +1,20 @@
using System; using System;
namespace Dalamud.Game.Internal.DXGI { namespace Dalamud.Game.Internal.DXGI
public interface ISwapChainAddressResolver { {
/// <summary>
/// An interface binding for the address resolvers that attempt to find native D3D11 methods.
/// </summary>
public interface ISwapChainAddressResolver
{
/// <summary>
/// Gets or sets the address of the native D3D11.Present method.
/// </summary>
IntPtr Present { get; set; } IntPtr Present { get; set; }
/// <summary>
/// Gets or sets the address of the native D3D11.ResizeBuffers method.
/// </summary>
IntPtr ResizeBuffers { get; set; } IntPtr ResizeBuffers { get; set; }
} }
} }

View file

@ -1,18 +1,23 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Serilog; using Serilog;
namespace Dalamud.Game.Internal.DXGI namespace Dalamud.Game.Internal.DXGI
{ {
/// <summary>
/// The address resolver for native D3D11 methods to facilitate displaying the Dalamud UI.
/// </summary>
public sealed class SwapChainSigResolver : BaseAddressResolver, ISwapChainAddressResolver public sealed class SwapChainSigResolver : BaseAddressResolver, ISwapChainAddressResolver
{ {
/// <inheritdoc/>
public IntPtr Present { get; set; } public IntPtr Present { get; set; }
/// <inheritdoc/>
public IntPtr ResizeBuffers { get; set; } public IntPtr ResizeBuffers { get; set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig) protected override void Setup64Bit(SigScanner sig)
{ {
var module = Process.GetCurrentProcess().Modules.Cast<ProcessModule>().First(m => m.ModuleName == "dxgi.dll"); var module = Process.GetCurrentProcess().Modules.Cast<ProcessModule>().First(m => m.ModuleName == "dxgi.dll");
@ -22,9 +27,9 @@ namespace Dalamud.Game.Internal.DXGI
var scanner = new SigScanner(module); var scanner = new SigScanner(module);
// This(code after the function head - offset of it) was picked to avoid running into issues with other hooks being installed into this function. // This(code after the function head - offset of it) was picked to avoid running into issues with other hooks being installed into this function.
Present = scanner.ScanModule("41 8B F0 8B FA 89 54 24 ?? 48 8B D9 48 89 4D ?? C6 44 24 ?? 00") - 0x37; this.Present = scanner.ScanModule("41 8B F0 8B FA 89 54 24 ?? 48 8B D9 48 89 4D ?? C6 44 24 ?? 00") - 0x37;
ResizeBuffers = scanner.ScanModule("48 8B C4 55 41 54 41 55 41 56 41 57 48 8D 68 B1 48 81 EC ?? ?? ?? ?? 48 C7 45 ?? ?? ?? ?? ?? 48 89 58 10 48 89 70 18 48 89 78 20 45 8B F9 45 8B E0 44 8B EA 48 8B F9 8B 45 7F 89 44 24 30 8B 75 77 89 74 24 28 44 89 4C 24"); this.ResizeBuffers = scanner.ScanModule("48 8B C4 55 41 54 41 55 41 56 41 57 48 8D 68 B1 48 81 EC ?? ?? ?? ?? 48 C7 45 ?? ?? ?? ?? ?? 48 89 58 10 48 89 70 18 48 89 78 20 45 8B F9 45 8B E0 44 8B EA 48 8B F9 8B 45 7F 89 44 24 30 8B 75 77 89 74 24 28 44 89 4C 24");
} }
} }
} }

View file

@ -1,25 +1,69 @@
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SharpDX.Windows; using System.Windows.Forms;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using Device = SharpDX.Direct3D11.Device; using Device = SharpDX.Direct3D11.Device;
namespace Dalamud.Game.Internal.DXGI namespace Dalamud.Game.Internal.DXGI
{ {
/* /// <summary>
* This method of getting the SwapChain Addresses is currently not used. /// This class attempts to determine the D3D11 SwapChain vtable addresses via instantiating a new form and inspecting it.
* If the normal AddressResolver(SigScanner) fails, we should use it as a fallback.(Linux?) /// </summary>
*/ /// <remarks>
/// If the normal signature based method of resolution fails, this is the backup.
/// </remarks>
public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver
{ {
private const int DxgiSwapchainMethodCount = 18; private const int DxgiSwapchainMethodCount = 18;
private const int D3D11DeviceMethodCount = 43; private const int D3D11DeviceMethodCount = 43;
private static SwapChainDescription CreateSwapChainDescription(IntPtr renderForm) { private List<IntPtr> d3d11VTblAddresses;
return new SwapChainDescription { private List<IntPtr> dxgiSwapChainVTblAddresses;
/// <inheritdoc/>
public IntPtr Present { get; set; }
/// <inheritdoc/>
public IntPtr ResizeBuffers { get; set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
if (this.d3d11VTblAddresses == null)
{
// Create temporary device + swapchain and determine method addresses
var renderForm = new Form();
Device.CreateWithSwapChain(
DriverType.Hardware,
DeviceCreationFlags.BgraSupport,
CreateSwapChainDescription(renderForm.Handle),
out var device,
out var swapChain);
if (device != null && swapChain != null)
{
this.d3d11VTblAddresses = this.GetVTblAddresses(device.NativePointer, D3D11DeviceMethodCount);
this.dxgiSwapChainVTblAddresses = this.GetVTblAddresses(swapChain.NativePointer, DxgiSwapchainMethodCount);
}
device?.Dispose();
swapChain?.Dispose();
}
this.Present = this.dxgiSwapChainVTblAddresses[8];
this.ResizeBuffers = this.dxgiSwapChainVTblAddresses[13];
}
private static SwapChainDescription CreateSwapChainDescription(IntPtr renderForm)
{
return new SwapChainDescription
{
BufferCount = 1, BufferCount = 1,
Flags = SwapChainFlags.None, Flags = SwapChainFlags.None,
IsWindowed = true, IsWindowed = true,
@ -27,74 +71,23 @@ namespace Dalamud.Game.Internal.DXGI
OutputHandle = renderForm, OutputHandle = renderForm,
SampleDescription = new SampleDescription(1, 0), SampleDescription = new SampleDescription(1, 0),
SwapEffect = SwapEffect.Discard, SwapEffect = SwapEffect.Discard,
Usage = Usage.RenderTargetOutput Usage = Usage.RenderTargetOutput,
}; };
} }
private IntPtr[] GetVTblAddresses(IntPtr pointer, int numberOfMethods) private List<IntPtr> GetVTblAddresses(IntPtr pointer, int numberOfMethods)
{ {
return GetVTblAddresses(pointer, 0, numberOfMethods); return this.GetVTblAddresses(pointer, 0, numberOfMethods);
} }
private IntPtr[] GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods) private List<IntPtr> GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods)
{ {
List<IntPtr> vtblAddresses = new List<IntPtr>(); var vtblAddresses = new List<IntPtr>();
IntPtr vTable = Marshal.ReadIntPtr(pointer); var vTable = Marshal.ReadIntPtr(pointer);
for (int i = startIndex; i < startIndex + numberOfMethods; i++) for (var i = startIndex; i < startIndex + numberOfMethods; i++)
vtblAddresses.Add(Marshal.ReadIntPtr(vTable, i * IntPtr.Size)); // using IntPtr.Size allows us to support both 32 and 64-bit processes vtblAddresses.Add(Marshal.ReadIntPtr(vTable, i * IntPtr.Size)); // using IntPtr.Size allows us to support both 32 and 64-bit processes
return vtblAddresses.ToArray(); return vtblAddresses;
}
private List<IntPtr> d3d11VTblAddresses = null;
private List<IntPtr> dxgiSwapChainVTblAddresses = null;
#region Internal device resources
private Device device;
private SwapChain swapChain;
private RenderForm renderForm;
#endregion
#region Addresses
public IntPtr Present { get; set; }
public IntPtr ResizeBuffers { get; set; }
#endregion
protected override void Setup64Bit(SigScanner sig) {
if (this.d3d11VTblAddresses == null) {
this.d3d11VTblAddresses = new List<IntPtr>();
this.dxgiSwapChainVTblAddresses = new List<IntPtr>();
#region Get Device and SwapChain method addresses
// Create temporary device + swapchain and determine method addresses
this.renderForm = new RenderForm();
Device.CreateWithSwapChain(
DriverType.Hardware,
DeviceCreationFlags.BgraSupport,
CreateSwapChainDescription(this.renderForm.Handle),
out this.device,
out this.swapChain
);
if (this.device != null && this.swapChain != null) {
this.d3d11VTblAddresses.AddRange(
GetVTblAddresses(this.device.NativePointer, D3D11DeviceMethodCount));
this.dxgiSwapChainVTblAddresses.AddRange(
GetVTblAddresses(this.swapChain.NativePointer, DxgiSwapchainMethodCount));
}
this.device?.Dispose();
this.swapChain?.Dispose();
#endregion
}
Present = this.dxgiSwapChainVTblAddresses[8];
ResizeBuffers = this.dxgiSwapChainVTblAddresses[13];
} }
} }
} }

View file

@ -3,95 +3,154 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Game.Internal.Gui; using Dalamud.Game.Internal.Gui;
using Dalamud.Game.Internal.Libc; using Dalamud.Game.Internal.Libc;
using Dalamud.Game.Internal.Network; using Dalamud.Game.Internal.Network;
using Dalamud.Hooking; using Dalamud.Hooking;
using Serilog; using Serilog;
namespace Dalamud.Game.Internal { namespace Dalamud.Game.Internal
{
/// <summary> /// <summary>
/// This class represents the Framework of the native game client and grants access to various subsystems. /// This class represents the Framework of the native game client and grants access to various subsystems.
/// </summary> /// </summary>
public sealed class Framework : IDisposable { public sealed class Framework : IDisposable
private readonly Dalamud dalamud; {
private static Stopwatch statsStopwatch = new();
internal bool DispatchUpdateEvents { get; set; } = true; private readonly Dalamud dalamud;
private Hook<OnUpdateDetour> updateHook;
private Hook<OnDestroyDetour> destroyHook;
private Hook<OnRealDestroyDelegate> realDestroyHook;
/// <summary>
/// Initializes a new instance of the <see cref="Framework"/> class.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
public Framework(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.Address = new FrameworkAddressResolver();
this.Address.Setup(scanner);
Log.Verbose("Framework address {FrameworkAddress}", this.Address.BaseAddress);
if (this.Address.BaseAddress == IntPtr.Zero)
{
throw new InvalidOperationException("Framework is not initalized yet.");
}
// Hook virtual functions
this.HookVTable();
// Initialize subsystems
this.Libc = new LibcFunction(scanner);
this.Gui = new GameGui(this.Address.GuiManager, scanner, dalamud);
this.Network = new GameNetwork(scanner);
}
/// <summary>
/// A delegate type used with the <see cref="OnUpdateEvent"/> event.
/// </summary>
/// <param name="framework">The Framework instance.</param>
public delegate void OnUpdateDelegate(Framework framework);
/// <summary>
/// A delegate type used during the native Framework::destroy.
/// </summary>
/// <param name="framework">The native Framework address.</param>
/// <returns>A value indicating if the call was successful.</returns>
public delegate bool OnRealDestroyDelegate(IntPtr framework);
/// <summary>
/// A delegate type used during the native Framework::free.
/// </summary>
/// <returns>The native Framework address.</returns>
public delegate IntPtr OnDestroyDelegate();
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate bool OnUpdateDetour(IntPtr framework); private delegate bool OnUpdateDetour(IntPtr framework);
private delegate IntPtr OnDestroyDetour(); private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate
public delegate void OnUpdateDelegate(Framework framework);
public delegate IntPtr OnDestroyDelegate();
public delegate bool OnRealDestroyDelegate(IntPtr framework);
/// <summary> /// <summary>
/// Event that gets fired every time the game framework updates. /// Event that gets fired every time the game framework updates.
/// </summary> /// </summary>
public event OnUpdateDelegate OnUpdateEvent; public event OnUpdateDelegate OnUpdateEvent;
private Hook<OnUpdateDetour> updateHook; /// <summary>
/// Gets or sets a value indicating whether the collection of stats is enabled.
private Hook<OnDestroyDetour> destroyHook; /// </summary>
public static bool StatsEnabled { get; set; }
private Hook<OnRealDestroyDelegate> realDestroyHook;
/// <summary> /// <summary>
/// A raw pointer to the instance of Client::Framework /// Gets the stats history mapping.
/// </summary> /// </summary>
public FrameworkAddressResolver Address { get; } public static Dictionary<string, List<double>> StatsHistory = new();
#region Stats
public static bool StatsEnabled { get; set; }
public static Dictionary<string, List<double>> StatsHistory = new Dictionary<string, List<double>>();
private static Stopwatch statsStopwatch = new Stopwatch();
#endregion
#region Subsystems #region Subsystems
/// <summary> /// <summary>
/// The GUI subsystem, used to access e.g. chat. /// Gets the GUI subsystem, used to access e.g. chat.
/// </summary> /// </summary>
public GameGui Gui { get; private set; } public GameGui Gui { get; private set; }
/// <summary> /// <summary>
/// The Network subsystem, used to access network data. /// Gets the Network subsystem, used to access network data.
/// </summary> /// </summary>
public GameNetwork Network { get; private set; } public GameNetwork Network { get; private set; }
// public ResourceManager Resource { get; private set; } // public ResourceManager Resource { get; private set; }
/// <summary>
/// Gets the Libc subsystem, used to facilitate interop with std::strings.
/// </summary>
public LibcFunction Libc { get; private set; } public LibcFunction Libc { get; private set; }
#endregion #endregion
public Framework(SigScanner scanner, Dalamud dalamud) { /// <summary>
this.dalamud = dalamud; /// Gets a raw pointer to the instance of Client::Framework.
Address = new FrameworkAddressResolver(); /// </summary>
Address.Setup(scanner); public FrameworkAddressResolver Address { get; }
Log.Verbose("Framework address {FrameworkAddress}", Address.BaseAddress); /// <summary>
if (Address.BaseAddress == IntPtr.Zero) { /// Gets or sets a value indicating whether to dispatch update events.
throw new InvalidOperationException("Framework is not initalized yet."); /// </summary>
internal bool DispatchUpdateEvents { get; set; } = true;
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
{
this.Gui.Enable();
this.Network.Enable();
this.updateHook.Enable();
this.destroyHook.Enable();
this.realDestroyHook.Enable();
} }
// Hook virtual functions /// <summary>
HookVTable(); /// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.Gui.Dispose();
this.Network.Dispose();
// Initialize subsystems this.updateHook.Dispose();
Libc = new LibcFunction(scanner); this.destroyHook.Dispose();
this.realDestroyHook.Dispose();
Gui = new GameGui(Address.GuiManager, scanner, dalamud);
Network = new GameNetwork(scanner);
} }
private void HookVTable() { private void HookVTable()
var vtable = Marshal.ReadIntPtr(Address.BaseAddress); {
var vtable = Marshal.ReadIntPtr(this.Address.BaseAddress);
// Virtual function layout: // Virtual function layout:
// .rdata:00000001411F1FE0 dq offset Xiv__Framework___dtor // .rdata:00000001411F1FE0 dq offset Xiv__Framework___dtor
// .rdata:00000001411F1FE8 dq offset Xiv__Framework__init // .rdata:00000001411F1FE8 dq offset Xiv__Framework__init
@ -100,36 +159,17 @@ namespace Dalamud.Game.Internal {
// .rdata:00000001411F2000 dq offset Xiv__Framework__update // .rdata:00000001411F2000 dq offset Xiv__Framework__update
var pUpdate = Marshal.ReadIntPtr(vtable, IntPtr.Size * 4); var pUpdate = Marshal.ReadIntPtr(vtable, IntPtr.Size * 4);
this.updateHook = new Hook<OnUpdateDetour>(pUpdate, new OnUpdateDetour(HandleFrameworkUpdate), this); this.updateHook = new Hook<OnUpdateDetour>(pUpdate, new OnUpdateDetour(this.HandleFrameworkUpdate), this);
var pDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 3); var pDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 3);
this.destroyHook = this.destroyHook = new Hook<OnDestroyDetour>(pDestroy, new OnDestroyDelegate(this.HandleFrameworkDestroy), this);
new Hook<OnDestroyDetour>(pDestroy, new OnDestroyDelegate(HandleFrameworkDestroy), this);
var pRealDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 2); var pRealDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 2);
this.realDestroyHook = this.realDestroyHook = new Hook<OnRealDestroyDelegate>(pRealDestroy, new OnRealDestroyDelegate(this.HandleRealDestroy), this);
new Hook<OnRealDestroyDelegate>(pRealDestroy, new OnRealDestroyDelegate(HandleRealDestroy), this);
} }
public void Enable() { private bool HandleFrameworkUpdate(IntPtr framework)
Gui.Enable(); {
Network.Enable();
this.updateHook.Enable();
this.destroyHook.Enable();
this.realDestroyHook.Enable();
}
public void Dispose() {
Gui.Dispose();
Network.Dispose();
this.updateHook.Dispose();
this.destroyHook.Dispose();
this.realDestroyHook.Dispose();
}
private bool HandleFrameworkUpdate(IntPtr framework) {
// If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously // If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously
if (!this.dalamud.IsReady) if (!this.dalamud.IsReady)
this.dalamud.LoadTier2(); this.dalamud.LoadTier2();
@ -137,23 +177,29 @@ namespace Dalamud.Game.Internal {
if (!this.dalamud.IsLoadedPluginSystem && this.dalamud.InterfaceManager.IsReady) if (!this.dalamud.IsLoadedPluginSystem && this.dalamud.InterfaceManager.IsReady)
this.dalamud.LoadTier3(); this.dalamud.LoadTier3();
try { try
Gui.Chat.UpdateQueue(this); {
Gui.Toast.UpdateQueue(); this.Gui.Chat.UpdateQueue(this);
Network.UpdateQueue(this); this.Gui.Toast.UpdateQueue();
} catch (Exception ex) { this.Network.UpdateQueue(this);
}
catch (Exception ex)
{
Log.Error(ex, "Exception while handling Framework::Update hook."); Log.Error(ex, "Exception while handling Framework::Update hook.");
} }
if (this.DispatchUpdateEvents) if (this.DispatchUpdateEvents)
{ {
try { try
if (StatsEnabled && OnUpdateEvent != null) { {
if (StatsEnabled && this.OnUpdateEvent != null)
{
// Stat Tracking for Framework Updates // Stat Tracking for Framework Updates
var invokeList = OnUpdateEvent.GetInvocationList(); var invokeList = this.OnUpdateEvent.GetInvocationList();
var notUpdated = StatsHistory.Keys.ToList(); var notUpdated = StatsHistory.Keys.ToList();
// Individually invoke OnUpdate handlers and time them. // Individually invoke OnUpdate handlers and time them.
foreach (var d in invokeList) { foreach (var d in invokeList)
{
statsStopwatch.Restart(); statsStopwatch.Restart();
d.Method.Invoke(d.Target, new object[] { this }); d.Method.Invoke(d.Target, new object[] { this });
statsStopwatch.Stop(); statsStopwatch.Stop();
@ -161,23 +207,32 @@ namespace Dalamud.Game.Internal {
if (notUpdated.Contains(key)) notUpdated.Remove(key); if (notUpdated.Contains(key)) notUpdated.Remove(key);
if (!StatsHistory.ContainsKey(key)) StatsHistory.Add(key, new List<double>()); if (!StatsHistory.ContainsKey(key)) StatsHistory.Add(key, new List<double>());
StatsHistory[key].Add(statsStopwatch.Elapsed.TotalMilliseconds); StatsHistory[key].Add(statsStopwatch.Elapsed.TotalMilliseconds);
if (StatsHistory[key].Count > 1000) { if (StatsHistory[key].Count > 1000)
{
StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000); StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000);
} }
} }
// Cleanup handlers that are no longer being called // Cleanup handlers that are no longer being called
foreach (var key in notUpdated) { foreach (var key in notUpdated)
if (StatsHistory[key].Count > 0) { {
if (StatsHistory[key].Count > 0)
{
StatsHistory[key].RemoveAt(0); StatsHistory[key].RemoveAt(0);
} else { }
else
{
StatsHistory.Remove(key); StatsHistory.Remove(key);
} }
} }
} else {
OnUpdateEvent?.Invoke(this);
} }
} catch (Exception ex) { else
{
this.OnUpdateEvent?.Invoke(this);
}
}
catch (Exception ex)
{
Log.Error(ex, "Exception while dispatching Framework::Update event."); Log.Error(ex, "Exception while dispatching Framework::Update event.");
} }
} }
@ -199,7 +254,8 @@ namespace Dalamud.Game.Internal {
return this.realDestroyHook.Original(framework); return this.realDestroyHook.Original(framework);
} }
private IntPtr HandleFrameworkDestroy() { private IntPtr HandleFrameworkDestroy()
{
Log.Information("Framework::Free!"); Log.Information("Framework::Free!");
// Store the pointer to the original trampoline location // Store the pointer to the original trampoline location

View file

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

View file

@ -1,27 +1,66 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Game.Internal.Gui.Addon { namespace Dalamud.Game.Internal.Gui.Addon
public class Addon { {
/// <summary>
/// This class represents an in-game UI "Addon".
/// </summary>
public class Addon
{
/// <summary>
/// The address of the addon.
/// </summary>
public IntPtr Address; public IntPtr Address;
/// <summary>
/// The addon interop data.
/// </summary>
protected Structs.Addon addonStruct; protected Structs.Addon addonStruct;
public Addon(IntPtr address, Structs.Addon addonStruct) { /// <summary>
/// Initializes a new instance of the <see cref="Addon"/> class.
/// </summary>
/// <param name="address">The address of the addon.</param>
/// <param name="addonStruct">The addon interop data.</param>
public Addon(IntPtr address, Structs.Addon addonStruct)
{
this.Address = address; this.Address = address;
this.addonStruct = addonStruct; this.addonStruct = addonStruct;
} }
/// <summary>
/// Gets the name of the addon.
/// </summary>
public string Name => this.addonStruct.Name; public string Name => this.addonStruct.Name;
public short X => this.addonStruct.X;
public short Y => this.addonStruct.Y;
public float Scale => this.addonStruct.Scale;
public unsafe float Width => this.addonStruct.RootNode->Width * Scale;
public unsafe float Height => this.addonStruct.RootNode->Height * Scale;
/// <summary>
/// Gets the X position of the addon on screen.
/// </summary>
public short X => this.addonStruct.X;
/// <summary>
/// Gets the Y position of the addon on screen.
/// </summary>
public short Y => this.addonStruct.Y;
/// <summary>
/// Gets the scale of the addon.
/// </summary>
public float Scale => this.addonStruct.Scale;
/// <summary>
/// Gets the width of the addon. This may include non-visible parts.
/// </summary>
public unsafe float Width => this.addonStruct.RootNode->Width * this.Scale;
/// <summary>
/// Gets the height of the addon. This may include non-visible parts.
/// </summary>
public unsafe float Height => this.addonStruct.RootNode->Height * this.Scale;
/// <summary>
/// Gets a value indicating whether the addon is visible.
/// </summary>
public bool Visible => (this.addonStruct.Flags & 0x20) == 0x20; public bool Visible => (this.addonStruct.Flags & 0x20) == 0x20;
} }
} }

View file

@ -3,35 +3,127 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Dalamud.Game.Internal.Libc;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Game.Internal.Libc;
using Dalamud.Hooking; using Dalamud.Hooking;
using Serilog; using Serilog;
namespace Dalamud.Game.Internal.Gui { namespace Dalamud.Game.Internal.Gui
public sealed class ChatGui : IDisposable { {
private readonly Queue<XivChatEntry> chatQueue = new Queue<XivChatEntry>(); /// <summary>
/// This class handles interacting with the native chat UI.
/// </summary>
public sealed class ChatGui : IDisposable
{
private readonly Dalamud dalamud;
private readonly ChatGuiAddressResolver address;
#region Events private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); private readonly Hook<PrintMessageDelegate> printMessageHook;
public delegate void OnMessageRawDelegate(XivChatType type, uint senderId, ref StdString sender, ref StdString message, ref bool isHandled); private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
public delegate void OnCheckMessageHandledDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
public delegate void OnMessageHandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message);
public delegate void OnMessageUnhandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message); private IntPtr baseAddress = IntPtr.Zero;
/// <summary> /// <summary>
/// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true. /// Initializes a new instance of the <see cref="ChatGui"/> class.
/// </summary> /// </summary>
public event OnCheckMessageHandledDelegate OnCheckMessageHandled; /// <param name="baseAddress">The base address of the ChatManager.</param>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
public ChatGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.address = new ChatGuiAddressResolver(baseAddress);
this.address.Setup(scanner);
Log.Verbose("Chat manager address {ChatManager}", this.address.BaseAddress);
this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, new PrintMessageDelegate(this.HandlePrintMessageDetour), this);
this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, new PopulateItemLinkDelegate(this.HandlePopulateItemLinkDetour), this);
this.interactableLinkClickedHook = new Hook<InteractableLinkClickedDelegate>(this.address.InteractableLinkClicked, new InteractableLinkClickedDelegate(this.InteractableLinkClickedDetour));
}
/// <summary>
/// A delegate type used with the <see cref="OnChatMessage"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
/// <param name="isHandled">A value indicating whether the message was handled or should be propagated.</param>
public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled);
/// <summary>
/// A delegate type used with the <see cref="OnChatMessageRaw"/> 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>
[Obsolete("Please use OnMessageDelegate instead. For modifications, it will take precedence.")]
public delegate void OnMessageRawDelegate(XivChatType type, uint senderId, ref StdString sender, ref StdString message, ref bool isHandled);
/// <summary>
/// A delegate type used with the <see cref="OnCheckMessageHandled"/> 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="OnChatMessageHandled"/> 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="OnChatMessageUnhandled"/> 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> /// <summary>
/// Event that will be fired when a chat message is sent to chat by the game. /// Event that will be fired when a chat message is sent to chat by the game.
/// </summary> /// </summary>
public event OnMessageDelegate OnChatMessage; public event OnMessageDelegate OnChatMessage;
/// <summary>
/// Event that will be fired when a chat message is sent by the game, containing raw, unparsed data.
/// </summary>
[Obsolete("Please use OnChatMessage instead. For modifications, it will take precedence.")]
public event OnMessageRawDelegate OnChatMessageRaw;
/// <summary>
/// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true.
/// </summary>
public event OnCheckMessageHandledDelegate OnCheckMessageHandled;
/// <summary> /// <summary>
/// Event that will be fired when a chat message is handled by Dalamud or a Plugin. /// Event that will be fired when a chat message is handled by Dalamud or a Plugin.
/// </summary> /// </summary>
@ -43,101 +135,239 @@ namespace Dalamud.Game.Internal.Gui {
public event OnMessageUnhandledDelegate OnChatMessageUnhandled; public event OnMessageUnhandledDelegate OnChatMessageUnhandled;
/// <summary> /// <summary>
/// Event that will be fired when a chat message is sent by the game, containing raw, unparsed data. /// Gets the ID of the last linked item.
/// </summary> /// </summary>
[Obsolete("Please use OnChatMessage instead. For modifications, it will take precedence.")]
public event OnMessageRawDelegate OnChatMessageRaw;
#endregion
#region Hooks
private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
#endregion
#region Delegates
[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);
#endregion
public int LastLinkedItemId { get; private set; } public int LastLinkedItemId { get; private set; }
/// <summary>
/// Gets the flags of the last linked item.
/// </summary>
public byte LastLinkedItemFlags { get; private set; } public byte LastLinkedItemFlags { get; private set; }
private ChatGuiAddressResolver Address { get; } /// <summary>
/// Enables this module.
private IntPtr baseAddress = IntPtr.Zero; /// </summary>
public void Enable()
private readonly Dalamud dalamud; {
public ChatGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud) {
this.dalamud = dalamud;
Address = new ChatGuiAddressResolver(baseAddress);
Address.Setup(scanner);
Log.Verbose("Chat manager address {ChatManager}", Address.BaseAddress);
this.printMessageHook =
new Hook<PrintMessageDelegate>(Address.PrintMessage, new PrintMessageDelegate(HandlePrintMessageDetour),
this);
this.populateItemLinkHook =
new Hook<PopulateItemLinkDelegate>(Address.PopulateItemLinkObject,
new PopulateItemLinkDelegate(HandlePopulateItemLinkDetour),
this);
this.interactableLinkClickedHook =
new Hook<InteractableLinkClickedDelegate>(Address.InteractableLinkClicked,
new InteractableLinkClickedDelegate(InteractableLinkClickedDetour));
}
public void Enable() {
this.printMessageHook.Enable(); this.printMessageHook.Enable();
this.populateItemLinkHook.Enable(); this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable(); this.interactableLinkClickedHook.Enable();
} }
public void Dispose() { /// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.printMessageHook.Dispose(); this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose(); this.populateItemLinkHook.Dispose();
this.interactableLinkClickedHook.Dispose(); this.interactableLinkClickedHook.Dispose();
} }
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr) { /// <summary>
try { /// 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
{
MessageBytes = Encoding.UTF8.GetBytes(message),
Type = this.dalamud.Configuration.GeneralChatType,
});
}
/// <summary>
/// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void Print(SeString message)
{
Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
MessageBytes = message.Encode(),
Type = this.dalamud.Configuration.GeneralChatType,
});
}
/// <summary>
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// the queue, later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void PrintError(string message)
{
Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message);
this.PrintChat(new XivChatEntry
{
MessageBytes = Encoding.UTF8.GetBytes(message),
Type = XivChatType.Urgent,
});
}
/// <summary>
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// the queue, later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void PrintError(SeString message)
{
Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
MessageBytes = message.Encode(),
Type = XivChatType.Urgent,
});
}
/// <summary>
/// Process a chat queue.
/// </summary>
/// <param name="framework">The Framework instance.</param>
public void UpdateQueue(Framework framework)
{
while (this.chatQueue.Count > 0)
{
var chat = this.chatQueue.Dequeue();
if (this.baseAddress == IntPtr.Zero)
{
continue;
}
var senderRaw = Encoding.UTF8.GetBytes(chat.Name ?? string.Empty);
using var senderOwned = framework.Libc.NewString(senderRaw);
var messageRaw = chat.MessageBytes ?? new byte[0];
using var messageOwned = framework.Libc.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
}
}
/// <summary>
/// Create a link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to run.</param>
/// <param name="commandAction">The command action itself.</param>
/// <returns>A payload for handling.</returns>
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
{
var payload = new DalamudLinkPayload() { Plugin = pluginName, CommandId = commandId };
this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction);
return payload;
}
/// <summary>
/// Remove all handlers owned by a plugin.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the links.</param>
internal void RemoveChatLinkHandler(string pluginName)
{
foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.PluginName == pluginName))
{
this.dalamudLinkHandlers.Remove(handler);
}
}
/// <summary>
/// Remove a registered link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to be removed.</param>
internal void RemoveChatLinkHandler(string pluginName, uint commandId)
{
if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId)))
{
this.dalamudLinkHandlers.Remove((pluginName, commandId));
}
}
private static unsafe bool FastByteArrayCompare(byte[] a1, byte[] a2)
{
// Copyright (c) 2008-2013 Hafthor Stefansson
// Distributed under the MIT/X11 software license
// Ref: http://www.opensource.org/licenses/mit-license.php.
// https://stackoverflow.com/a/8808245
if (a1 == a2) return true;
if (a1 == null || a2 == null || a1.Length != a2.Length)
return false;
fixed (byte* p1 = a1, p2 = a2)
{
byte* x1 = p1, x2 = p2;
var l = a1.Length;
for (var i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
{
if (*((long*)x1) != *((long*)x2))
return false;
}
if ((l & 4) != 0)
{
if (*((int*)x1) != *((int*)x2))
return false;
x1 += 4;
x2 += 4;
}
if ((l & 2) != 0)
{
if (*((short*)x1) != *((short*)x2))
return false;
x1 += 2;
x2 += 2;
}
if ((l & 1) != 0)
{
if (*((byte*)x1) != *((byte*)x2))
return false;
}
return true;
}
}
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr)
{
try
{
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr); this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8); this.LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8);
LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14); this.LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14);
Log.Debug($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{LastLinkedItemId}"); Log.Debug($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{this.LastLinkedItemId}");
} catch (Exception ex) { }
catch (Exception ex)
{
Log.Error(ex, "Exception onPopulateItemLink hook."); Log.Error(ex, "Exception onPopulateItemLink hook.");
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr); this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
} }
} }
private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chattype, IntPtr pSenderName, IntPtr pMessage, private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chattype, IntPtr pSenderName, IntPtr pMessage, uint senderid, IntPtr parameter)
uint senderid, IntPtr parameter) { {
var retVal = IntPtr.Zero; var retVal = IntPtr.Zero;
try { try
{
var sender = StdString.ReadFromPointer(pSenderName); var sender = StdString.ReadFromPointer(pSenderName);
var message = StdString.ReadFromPointer(pMessage); var message = StdString.ReadFromPointer(pMessage);
@ -153,16 +383,18 @@ namespace Dalamud.Game.Internal.Gui {
// Call events // Call events
var isHandled = false; var isHandled = false;
OnCheckMessageHandled?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); this.OnCheckMessageHandled?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
if (!isHandled) { if (!isHandled)
OnChatMessage?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); {
OnChatMessageRaw?.Invoke(chattype, senderid, ref sender, ref message, ref isHandled); this.OnChatMessage?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
this.OnChatMessageRaw?.Invoke(chattype, senderid, ref sender, ref message, ref isHandled);
} }
var newEdited = parsedMessage.Encode(); var newEdited = parsedMessage.Encode();
if (!FastByteArrayCompare(oldEdited, newEdited)) { if (!FastByteArrayCompare(oldEdited, newEdited))
{
Log.Verbose("SeString was edited, taking precedence over StdString edit."); Log.Verbose("SeString was edited, taking precedence over StdString edit.");
message.RawData = newEdited; message.RawData = newEdited;
Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}"); Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
@ -171,7 +403,8 @@ namespace Dalamud.Game.Internal.Gui {
var messagePtr = pMessage; var messagePtr = pMessage;
OwnedStdString allocatedString = null; OwnedStdString allocatedString = null;
if (!FastByteArrayCompare(originalMessageData, message.RawData)) { if (!FastByteArrayCompare(originalMessageData, message.RawData))
{
allocatedString = this.dalamud.Framework.Libc.NewString(message.RawData); allocatedString = this.dalamud.Framework.Libc.NewString(message.RawData);
Log.Debug( Log.Debug(
$"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})"); $"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
@ -181,19 +414,21 @@ namespace Dalamud.Game.Internal.Gui {
// Print the original chat if it's handled. // Print the original chat if it's handled.
if (isHandled) if (isHandled)
{ {
OnChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage); this.OnChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
} }
else else
{ {
retVal = this.printMessageHook.Original(manager, chattype, pSenderName, messagePtr, senderid, parameter); retVal = this.printMessageHook.Original(manager, chattype, pSenderName, messagePtr, senderid, parameter);
OnChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage); this.OnChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
} }
if (this.baseAddress == IntPtr.Zero) if (this.baseAddress == IntPtr.Zero)
this.baseAddress = manager; this.baseAddress = manager;
allocatedString?.Dispose(); allocatedString?.Dispose();
} catch (Exception ex) { }
catch (Exception ex)
{
Log.Error(ex, "Exception on OnChatMessage hook."); Log.Error(ex, "Exception on OnChatMessage hook.");
retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter); retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter);
} }
@ -201,47 +436,14 @@ namespace Dalamud.Game.Internal.Gui {
return retVal; return retVal;
} }
private readonly Dictionary<(string pluginName, uint commandId), Action<uint, SeString>> dalamudLinkHandlers = new Dictionary<(string, uint), Action<uint, SeString>>(); private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr)
{
/// <summary> try
/// Create a link handler {
/// </summary>
/// <param name="pluginName"></param>
/// <param name="commandId"></param>
/// <param name="commandAction"></param>
/// <returns></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 a registered link handler
/// </summary>
/// <param name="pluginName"></param>
/// <param name="commandId"></param>
internal void RemoveChatLinkHandler(string pluginName, uint commandId) {
if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId))) {
this.dalamudLinkHandlers.Remove((pluginName, commandId));
}
}
/// <summary>
/// Remove all handlers owned by a plugin.
/// </summary>
/// <param name="pluginName"></param>
internal void RemoveChatLinkHandler(string pluginName) {
foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.pluginName == pluginName)) {
this.dalamudLinkHandlers.Remove(handler);
}
}
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr) {
try {
var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1); var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1);
if (interactableType != Payload.EmbeddedInfoType.DalamudLink) { if (interactableType != Payload.EmbeddedInfoType.DalamudLink)
{
this.interactableLinkClickedHook.Original(managerPtr, messagePtr); this.interactableLinkClickedHook.Original(managerPtr, messagePtr);
return; return;
} }
@ -258,101 +460,23 @@ namespace Dalamud.Game.Internal.Gui {
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads; var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
if (payloads.Count == 0) return; if (payloads.Count == 0) return;
var linkPayload = payloads[0]; var linkPayload = payloads[0];
if (linkPayload is DalamudLinkPayload link) { if (linkPayload is DalamudLinkPayload link)
if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId))) { {
if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId)))
{
Log.Verbose($"Sending DalamudLink to {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)); this.dalamudLinkHandlers[(link.Plugin, link.CommandId)].Invoke(link.CommandId, new SeString(payloads));
} else { }
else
{
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}"); Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
} }
} }
} catch (Exception ex) { }
catch (Exception ex)
{
Log.Error(ex, "Exception on InteractableLinkClicked hook"); Log.Error(ex, "Exception on InteractableLinkClicked hook");
} }
} }
// Copyright (c) 2008-2013 Hafthor Stefansson
// Distributed under the MIT/X11 software license
// Ref: http://www.opensource.org/licenses/mit-license.php.
// https://stackoverflow.com/a/8808245
static unsafe bool FastByteArrayCompare(byte[] a1, byte[] a2)
{
if (a1 == a2) return true;
if (a1 == null || a2 == null || a1.Length != a2.Length)
return false;
fixed (byte* p1 = a1, p2 = a2)
{
byte* x1 = p1, x2 = p2;
int l = a1.Length;
for (int i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
if (*((long*)x1) != *((long*)x2)) return false;
if ((l & 4) != 0) { if (*((int*)x1) != *((int*)x2)) return false; x1 += 4; x2 += 4; }
if ((l & 2) != 0) { if (*((short*)x1) != *((short*)x2)) return false; x1 += 2; x2 += 2; }
if ((l & 1) != 0) if (*((byte*)x1) != *((byte*)x2)) return false;
return true;
}
}
/// <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);
}
public void Print(string message) {
Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
PrintChat(new XivChatEntry {
MessageBytes = Encoding.UTF8.GetBytes(message),
Type = this.dalamud.Configuration.GeneralChatType
});
}
public void Print(SeString message) {
Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
PrintChat(new XivChatEntry {
MessageBytes = message.Encode(),
Type = this.dalamud.Configuration.GeneralChatType
});
}
public void PrintError(string message) {
Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message);
PrintChat(new XivChatEntry {
MessageBytes = Encoding.UTF8.GetBytes(message),
Type = XivChatType.Urgent
});
}
public void PrintError(SeString message) {
Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue);
PrintChat(new XivChatEntry {
MessageBytes = message.Encode(),
Type = XivChatType.Urgent
});
}
/// <summary>
/// Process a chat queue.
/// </summary>
public void UpdateQueue(Framework framework) {
while (this.chatQueue.Count > 0) {
var chat = this.chatQueue.Dequeue();
if (this.baseAddress == IntPtr.Zero) {
continue;
}
var senderRaw = Encoding.UTF8.GetBytes(chat.Name ?? "");
using var senderOwned = framework.Libc.NewString(senderRaw);
var messageRaw = chat.MessageBytes ?? new byte[0];
using var messageOwned = framework.Libc.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
}
}
} }
} }

View file

@ -1,21 +1,43 @@
using System; using System;
namespace Dalamud.Game.Internal.Gui { namespace Dalamud.Game.Internal.Gui
public sealed class ChatGuiAddressResolver : BaseAddressResolver { {
public IntPtr BaseAddress { get; } /// <summary>
/// The address resolver for the <see cref="ChatGui"/> class.
public IntPtr PrintMessage { get; private set; } /// </summary>
public IntPtr PopulateItemLinkObject { get; private set; } public sealed class ChatGuiAddressResolver : BaseAddressResolver
public IntPtr InteractableLinkClicked { get; private set; } {
/// <summary>
public ChatGuiAddressResolver(IntPtr baseAddres) { /// Initializes a new instance of the <see cref="ChatGuiAddressResolver"/> class.
BaseAddress = baseAddres; /// </summary>
/// <param name="baseAddres">The base address of the native ChatManager class.</param>
public ChatGuiAddressResolver(IntPtr baseAddres)
{
this.BaseAddress = baseAddres;
} }
/// <summary>
/// Gets the base address of the native ChatManager class.
/// </summary>
public IntPtr BaseAddress { get; }
/// <summary>
/// Gets the address of the native PrintMessage method.
/// </summary>
public IntPtr PrintMessage { get; private set; }
/// <summary>
/// Gets the address of the native 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 --- --- for reference: 4.57 ---
.text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal) .text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal)
.text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near .text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near
.text:00000001405CD210 ; CODE XREF: sub_1401419F0+201p .text:00000001405CD210 ; CODE XREF: sub_1401419F0+201p
@ -76,12 +98,11 @@ namespace Dalamud.Game.Internal.Gui {
.text:00000001405CD255 mov rdi, rcx .text:00000001405CD255 mov rdi, rcx
*/ */
protected override void Setup64Bit(SigScanner sig) { /// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
// PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1??? // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1???
PrintMessage = this.PrintMessage = sig.ScanText("4055 53 56 4154 4157 48 8d ac 24 ?? ?? ?? ?? 48 81 ec 20 02 00 00 48 8b 05");
sig.ScanText(
"4055 53 56 4154 4157 48 8d ac 24 ?? ?? ?? ?? 48 81 ec 20 02 00 00 48 8b 05"
);
// PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old
// PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33"); // PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33");
@ -89,9 +110,9 @@ namespace Dalamud.Game.Internal.Gui {
// PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); // PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
// PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0 // PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0
PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? ?? FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); this.PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? ?? FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9; this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9;
} }
} }
} }

View file

@ -1,84 +1,145 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Interface; using Dalamud.Interface;
using ImGuiNET;
using Serilog; using Serilog;
using SharpDX; using SharpDX;
namespace Dalamud.Game.Internal.Gui { namespace Dalamud.Game.Internal.Gui
{
/// <summary>
/// A class handling many aspects of the in-game UI.
/// </summary>
public sealed class GameGui : IDisposable public sealed class GameGui : IDisposable
{ {
/// <summary>
/// The delegate of the native method that gets the Client::UI::UIModule address.
/// </summary>
/// <returns>The Client::UI::UIModule address.</returns>
public readonly GetBaseUIObjectDelegate GetBaseUIObject;
private readonly Dalamud dalamud; private readonly Dalamud dalamud;
private readonly GameGuiAddressResolver address;
private GameGuiAddressResolver Address { get; } private readonly GetMatrixSingletonDelegate getMatrixSingleton;
private readonly GetUIObjectDelegate getUIObject;
private readonly ScreenToWorldNativeDelegate screenToWorldNative;
private readonly GetUIObjectByNameDelegate getUIObjectByName;
private readonly GetUiModuleDelegate getUiModule;
private readonly GetAgentModuleDelegate getAgentModule;
public ChatGui Chat { get; private set; }
public PartyFinderGui PartyFinder { get; private set; }
public ToastGui Toast { get; private set; }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetGlobalBgmDelegate(UInt16 bgmKey, byte a2, UInt32 a3, UInt32 a4, UInt32 a5, byte a6);
private readonly Hook<SetGlobalBgmDelegate> setGlobalBgmHook; private readonly Hook<SetGlobalBgmDelegate> setGlobalBgmHook;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleItemHoverDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4);
private readonly Hook<HandleItemHoverDelegate> handleItemHoverHook; private readonly Hook<HandleItemHoverDelegate> handleItemHoverHook;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleItemOutDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4);
private readonly Hook<HandleItemOutDelegate> handleItemOutHook; private readonly Hook<HandleItemOutDelegate> handleItemOutHook;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void HandleActionHoverDelegate(IntPtr hoverState, HoverActionKind a2, uint a3, int a4, byte a5);
private readonly Hook<HandleActionHoverDelegate> handleActionHoverHook; private readonly Hook<HandleActionHoverDelegate> handleActionHoverHook;
private readonly Hook<HandleActionOutDelegate> handleActionOutHook;
private readonly Hook<ToggleUiHideDelegate> toggleUiHideHook;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private GetUIMapObjectDelegate getUIMapObject;
private delegate IntPtr HandleActionOutDelegate(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4); private OpenMapWithFlagDelegate openMapWithFlag;
private Hook<HandleActionOutDelegate> handleActionOutHook;
/// <summary>
/// Initializes a new instance of the <see cref="GameGui"/> class.
/// This class is responsible for many aspects of interacting with the native game UI.
/// </summary>
/// <param name="baseAddress">The base address of the native GuiManager class.</param>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
public GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.address = new GameGuiAddressResolver(baseAddress);
this.address.Setup(scanner);
Log.Verbose("===== G A M E G U I =====");
Log.Verbose("GameGuiManager address {Address:X}", this.address.BaseAddress.ToInt64());
Log.Verbose("SetGlobalBgm address {Address:X}", this.address.SetGlobalBgm.ToInt64());
Log.Verbose("HandleItemHover address {Address:X}", this.address.HandleItemHover.ToInt64());
Log.Verbose("HandleItemOut address {Address:X}", this.address.HandleItemOut.ToInt64());
Log.Verbose("GetUIObject address {Address:X}", this.address.GetUIObject.ToInt64());
Log.Verbose("GetAgentModule address {Address:X}", this.address.GetAgentModule.ToInt64());
this.Chat = new ChatGui(this.address.ChatManager, scanner, dalamud);
this.PartyFinder = new PartyFinderGui(scanner, dalamud);
this.Toast = new ToastGui(scanner, dalamud);
this.setGlobalBgmHook = new Hook<SetGlobalBgmDelegate>(this.address.SetGlobalBgm, new SetGlobalBgmDelegate(this.HandleSetGlobalBgmDetour), this);
this.handleItemHoverHook = new Hook<HandleItemHoverDelegate>(this.address.HandleItemHover, new HandleItemHoverDelegate(this.HandleItemHoverDetour), this);
this.handleItemOutHook = new Hook<HandleItemOutDelegate>(this.address.HandleItemOut, new HandleItemOutDelegate(this.HandleItemOutDetour), this);
this.handleActionHoverHook = new Hook<HandleActionHoverDelegate>(this.address.HandleActionHover, new HandleActionHoverDelegate(this.HandleActionHoverDetour), this);
this.handleActionOutHook = new Hook<HandleActionOutDelegate>(this.address.HandleActionOut, new HandleActionOutDelegate(this.HandleActionOutDetour), this);
this.getUIObject = Marshal.GetDelegateForFunctionPointer<GetUIObjectDelegate>(this.address.GetUIObject);
this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer<GetMatrixSingletonDelegate>(this.address.GetMatrixSingleton);
this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer<ScreenToWorldNativeDelegate>(this.address.ScreenToWorld);
this.toggleUiHideHook = new Hook<ToggleUiHideDelegate>(this.address.ToggleUiHide, new ToggleUiHideDelegate(this.ToggleUiHideDetour), this);
this.GetBaseUIObject = Marshal.GetDelegateForFunctionPointer<GetBaseUIObjectDelegate>(this.address.GetBaseUIObject);
this.getUIObjectByName = Marshal.GetDelegateForFunctionPointer<GetUIObjectByNameDelegate>(this.address.GetUIObjectByName);
this.getUiModule = Marshal.GetDelegateForFunctionPointer<GetUiModuleDelegate>(this.address.GetUIModule);
this.getAgentModule = Marshal.GetDelegateForFunctionPointer<GetAgentModuleDelegate>(this.address.GetAgentModule);
}
// Marshaled delegates
/// <summary>
/// The delegate type of the native method that gets the Client::UI::UIModule address.
/// </summary>
/// <returns>The Client::UI::UIModule address.</returns>
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate IntPtr GetBaseUIObjectDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr GetMatrixSingletonDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr GetUIObjectDelegate(); private delegate IntPtr GetUIObjectDelegate();
private readonly GetUIObjectDelegate getUIObject;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr GetUIMapObjectDelegate(IntPtr UIObject); private unsafe delegate bool ScreenToWorldNativeDelegate(float* camPos, float* clipPos, float rayDistance, float* worldPos, int* unknown);
private GetUIMapObjectDelegate getUIMapObject;
[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
private delegate bool OpenMapWithFlagDelegate(IntPtr UIMapObject, string flag);
private OpenMapWithFlagDelegate openMapWithFlag;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
internal delegate IntPtr GetMatrixSingletonDelegate();
internal readonly GetMatrixSingletonDelegate getMatrixSingleton;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private unsafe delegate bool ScreenToWorldNativeDelegate(
float *camPos, float *clipPos, float rayDistance, float *worldPos, int *unknown);
private readonly ScreenToWorldNativeDelegate screenToWorldNative;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, byte unknownByte);
private readonly Hook<ToggleUiHideDelegate> toggleUiHideHook;
// Return a Client::UI::UIModule
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate IntPtr GetBaseUIObjectDelegate();
public readonly GetBaseUIObjectDelegate GetBaseUIObject;
[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
private delegate IntPtr GetUIObjectByNameDelegate(IntPtr thisPtr, string uiName, int index); private delegate IntPtr GetUIObjectByNameDelegate(IntPtr thisPtr, string uiName, int index);
private readonly GetUIObjectByNameDelegate getUIObjectByName;
private delegate IntPtr GetUiModuleDelegate(IntPtr basePtr); private delegate IntPtr GetUiModuleDelegate(IntPtr basePtr);
private readonly GetUiModuleDelegate getUiModule;
private delegate IntPtr GetAgentModuleDelegate(IntPtr uiModule); private delegate IntPtr GetAgentModuleDelegate(IntPtr uiModule);
private GetAgentModuleDelegate getAgentModule;
public bool GameUiHidden { get; private set; } // Hooked delegates
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr GetUIMapObjectDelegate(IntPtr uiObject);
[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
private delegate bool OpenMapWithFlagDelegate(IntPtr uiMapObject, string flag);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetGlobalBgmDelegate(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleItemHoverDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleItemOutDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void HandleActionHoverDelegate(IntPtr hoverState, HoverActionKind a2, uint a3, int a4, byte a5);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleActionOutDelegate(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, byte unknownByte);
/// <summary> /// <summary>
/// Event which is fired when the game UI hiding is toggled. /// Event which is fired when the game UI hiding is toggled.
@ -86,207 +147,72 @@ namespace Dalamud.Game.Internal.Gui {
public event EventHandler<bool> OnUiHideToggled; public event EventHandler<bool> OnUiHideToggled;
/// <summary> /// <summary>
/// The item ID that is currently hovered by the player. 0 when no item is hovered. /// Gets the <see cref="Chat"/> instance.
/// If > 1.000.000, subtract 1.000.000 and treat it as HQ /// </summary>
public ChatGui Chat { get; private set; }
/// <summary>
/// Gets the <see cref="PartyFinder"/> instance.
/// </summary>
public PartyFinderGui PartyFinder { get; private set; }
/// <summary>
/// Gets the <see cref="Toast"/> instance.
/// </summary>
public ToastGui Toast { get; private set; }
/// <summary>
/// Gets a value indicating whether the game UI is hidden.
/// </summary>
public bool GameUiHidden { get; private set; }
/// <summary>
/// Gets or sets the item ID that is currently hovered by the player. 0 when no item is hovered.
/// If > 1.000.000, subtract 1.000.000 and treat it as HQ.
/// </summary> /// </summary>
public ulong HoveredItem { get; set; } public ulong HoveredItem { get; set; }
/// <summary> /// <summary>
/// The action ID that is current hovered by the player. 0 when no action is hovered. /// Gets the action ID that is current hovered by the player. 0 when no action is hovered.
/// </summary> /// </summary>
public HoveredAction HoveredAction { get; } = new HoveredAction(); public HoveredAction HoveredAction { get; } = new HoveredAction();
/// <summary> /// <summary>
/// Event that is fired when the currently hovered item changes. /// Gets or sets the event that is fired when the currently hovered item changes.
/// </summary> /// </summary>
public EventHandler<ulong> HoveredItemChanged { get; set; } public EventHandler<ulong> HoveredItemChanged { get; set; }
/// <summary> /// <summary>
/// Event that is fired when the currently hovered action changes. /// Gets or sets the event that is fired when the currently hovered action changes.
/// </summary> /// </summary>
public EventHandler<HoveredAction> HoveredActionChanged { get; set; } public EventHandler<HoveredAction> HoveredActionChanged { get; set; }
public GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
Address = new GameGuiAddressResolver(baseAddress);
Address.Setup(scanner);
Log.Verbose("===== G A M E G U I =====");
Log.Verbose("GameGuiManager address {Address}", Address.BaseAddress);
Log.Verbose("SetGlobalBgm address {Address}", Address.SetGlobalBgm);
Log.Verbose("HandleItemHover address {Address}", Address.HandleItemHover);
Log.Verbose("HandleItemOut address {Address}", Address.HandleItemOut);
Log.Verbose("GetUIObject address {Address}", Address.GetUIObject);
Log.Verbose("GetAgentModule address {Address}", Address.GetAgentModule);
Chat = new ChatGui(Address.ChatManager, scanner, dalamud);
PartyFinder = new PartyFinderGui(scanner, dalamud);
Toast = new ToastGui(scanner, dalamud);
this.setGlobalBgmHook =
new Hook<SetGlobalBgmDelegate>(Address.SetGlobalBgm,
new SetGlobalBgmDelegate(HandleSetGlobalBgmDetour),
this);
this.handleItemHoverHook =
new Hook<HandleItemHoverDelegate>(Address.HandleItemHover,
new HandleItemHoverDelegate(HandleItemHoverDetour),
this);
this.handleItemOutHook =
new Hook<HandleItemOutDelegate>(Address.HandleItemOut,
new HandleItemOutDelegate(HandleItemOutDetour),
this);
this.handleActionHoverHook =
new Hook<HandleActionHoverDelegate>(Address.HandleActionHover,
new HandleActionHoverDelegate(HandleActionHoverDetour),
this);
this.handleActionOutHook =
new Hook<HandleActionOutDelegate>(Address.HandleActionOut,
new HandleActionOutDelegate(HandleActionOutDetour),
this);
this.getUIObject = Marshal.GetDelegateForFunctionPointer<GetUIObjectDelegate>(Address.GetUIObject);
this.getMatrixSingleton =
Marshal.GetDelegateForFunctionPointer<GetMatrixSingletonDelegate>(Address.GetMatrixSingleton);
this.screenToWorldNative =
Marshal.GetDelegateForFunctionPointer<ScreenToWorldNativeDelegate>(Address.ScreenToWorld);
this.toggleUiHideHook = new Hook<ToggleUiHideDelegate>(Address.ToggleUiHide, new ToggleUiHideDelegate(ToggleUiHideDetour), this);
this.GetBaseUIObject = Marshal.GetDelegateForFunctionPointer<GetBaseUIObjectDelegate>(Address.GetBaseUIObject);
this.getUIObjectByName = Marshal.GetDelegateForFunctionPointer<GetUIObjectByNameDelegate>(Address.GetUIObjectByName);
this.getUiModule = Marshal.GetDelegateForFunctionPointer<GetUiModuleDelegate>(Address.GetUIModule);
this.getAgentModule = Marshal.GetDelegateForFunctionPointer<GetAgentModuleDelegate>(Address.GetAgentModule);
}
private IntPtr HandleSetGlobalBgmDetour(UInt16 bgmKey, byte a2, UInt32 a3, UInt32 a4, UInt32 a5, byte a6) {
var retVal = this.setGlobalBgmHook.Original(bgmKey, a2, a3, a4, a5, a6);
Log.Verbose("SetGlobalBgm: {0} {1} {2} {3} {4} {5} -> {6}", bgmKey, a2, a3, a4, a5, a6, retVal);
return retVal;
}
private IntPtr HandleItemHoverDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4) {
var retVal = this.handleItemHoverHook.Original(hoverState, a2, a3, a4);
if (retVal.ToInt64() == 22) {
var itemId = (ulong)Marshal.ReadInt32(hoverState, 0x138);
this.HoveredItem = itemId;
try {
HoveredItemChanged?.Invoke(this, itemId);
} catch (Exception e) {
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverItemId:{0} this:{1}", itemId, hoverState.ToInt64().ToString("X"));
}
return retVal;
}
private IntPtr HandleItemOutDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4)
{
var retVal = this.handleItemOutHook.Original(hoverState, a2, a3, a4);
if (a3 != IntPtr.Zero && a4 == 1) {
var a3Val = Marshal.ReadByte(a3, 0x8);
if (a3Val == 255) {
this.HoveredItem = 0ul;
try {
HoveredItemChanged?.Invoke(this, 0ul);
} catch (Exception e) {
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverItemId: 0");
}
}
return retVal;
}
private void HandleActionHoverDetour(IntPtr hoverState, HoverActionKind actionKind, uint actionId, int a4, byte a5)
{
handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
HoveredAction.ActionKind = actionKind;
HoveredAction.BaseActionID = actionId;
HoveredAction.ActionID = (uint) Marshal.ReadInt32(hoverState, 0x3C);
try
{
HoveredActionChanged?.Invoke(this, this.HoveredAction);
} catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverActionId: {0}/{1} this:{2}", actionKind, actionId, hoverState.ToInt64().ToString("X"));
}
private IntPtr HandleActionOutDetour(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4)
{
var retVal = handleActionOutHook.Original(agentActionDetail, a2, a3, a4);
if (a3 != IntPtr.Zero && a4 == 1)
{
var a3Val = Marshal.ReadByte(a3, 0x8);
if (a3Val == 255)
{
this.HoveredAction.ActionKind = HoverActionKind.None;
HoveredAction.BaseActionID = 0;
HoveredAction.ActionID = 0;
try
{
HoveredActionChanged?.Invoke(this, this.HoveredAction);
} catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredActionChanged event.");
}
Log.Verbose("HoverActionId: 0");
}
}
return retVal;
}
/// <summary> /// <summary>
/// Opens the in-game map with a flag on the location of the parameter /// Opens the in-game map with a flag on the location of the parameter.
/// </summary> /// </summary>
/// <param name="mapLink">Link to the map to be opened</param> /// <param name="mapLink">Link to the map to be opened.</param>
/// <returns>True if there were no errors and it could open the map</returns> /// <returns>True if there were no errors and it could open the map.</returns>
public bool OpenMapWithMapLink(MapLinkPayload mapLink) { public bool OpenMapWithMapLink(MapLinkPayload mapLink)
{
var uiObjectPtr = this.getUIObject(); var uiObjectPtr = this.getUIObject();
if (uiObjectPtr.Equals(IntPtr.Zero)) { if (uiObjectPtr.Equals(IntPtr.Zero))
{
Log.Error("OpenMapWithMapLink: Null pointer returned from getUIObject()"); Log.Error("OpenMapWithMapLink: Null pointer returned from getUIObject()");
return false; return false;
} }
this.getUIMapObject = this.getUIMapObject = this.address.GetVirtualFunction<GetUIMapObjectDelegate>(uiObjectPtr, 0, 8);
Address.GetVirtualFunction<GetUIMapObjectDelegate>(uiObjectPtr, 0, 8);
var uiMapObjectPtr = this.getUIMapObject(uiObjectPtr); var uiMapObjectPtr = this.getUIMapObject(uiObjectPtr);
if (uiMapObjectPtr.Equals(IntPtr.Zero)) { if (uiMapObjectPtr.Equals(IntPtr.Zero))
{
Log.Error("OpenMapWithMapLink: Null pointer returned from GetUIMapObject()"); Log.Error("OpenMapWithMapLink: Null pointer returned from GetUIMapObject()");
return false; return false;
} }
this.openMapWithFlag = this.openMapWithFlag = this.address.GetVirtualFunction<OpenMapWithFlagDelegate>(uiMapObjectPtr, 0, 63);
Address.GetVirtualFunction<OpenMapWithFlagDelegate>(uiMapObjectPtr, 0, 63);
var mapLinkString = mapLink.DataString; var mapLinkString = mapLink.DataString;
@ -298,20 +224,21 @@ namespace Dalamud.Game.Internal.Gui {
/// <summary> /// <summary>
/// Converts in-world coordinates to screen coordinates (upper left corner origin). /// Converts in-world coordinates to screen coordinates (upper left corner origin).
/// </summary> /// </summary>
/// <param name="worldPos">Coordinates in the world</param> /// <param name="worldPos">Coordinates in the world.</param>
/// <param name="screenPos">Converted coordinates</param> /// <param name="screenPos">Converted coordinates.</param>
/// <returns>True if worldPos corresponds to a position in front of the camera</returns> /// <returns>True if worldPos corresponds to a position in front of the camera.</returns>
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos) public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos)
{ {
// Get base object with matrices // Get base object with matrices
var matrixSingleton = this.getMatrixSingleton(); var matrixSingleton = this.getMatrixSingleton();
// Read current ViewProjectionMatrix plus game window size // Read current ViewProjectionMatrix plus game window size
var viewProjectionMatrix = new Matrix(); var viewProjectionMatrix = default(Matrix);
float width, height; float width, height;
var windowPos = ImGuiHelpers.MainViewport.Pos; var windowPos = ImGuiHelpers.MainViewport.Pos;
unsafe { unsafe
{
var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer(); var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer();
for (var i = 0; i < 16; i++, rawMatrix++) for (var i = 0; i < 16; i++, rawMatrix++)
@ -325,8 +252,8 @@ namespace Dalamud.Game.Internal.Gui {
screenPos = new Vector2(pCoords.X / pCoords.Z, pCoords.Y / pCoords.Z); screenPos = new Vector2(pCoords.X / pCoords.Z, pCoords.Y / pCoords.Z);
screenPos.X = 0.5f * width * (screenPos.X + 1f) + windowPos.X; screenPos.X = (0.5f * width * (screenPos.X + 1f)) + windowPos.X;
screenPos.Y = 0.5f * height * (1f - screenPos.Y) + windowPos.Y; screenPos.Y = (0.5f * height * (1f - screenPos.Y)) + windowPos.Y;
return pCoords.Z > 0 && return pCoords.Z > 0 &&
screenPos.X > windowPos.X && screenPos.X < windowPos.X + width && screenPos.X > windowPos.X && screenPos.X < windowPos.X + width &&
@ -336,10 +263,10 @@ namespace Dalamud.Game.Internal.Gui {
/// <summary> /// <summary>
/// Converts screen coordinates to in-world coordinates via raycasting. /// Converts screen coordinates to in-world coordinates via raycasting.
/// </summary> /// </summary>
/// <param name="screenPos">Screen coordinates</param> /// <param name="screenPos">Screen coordinates.</param>
/// <param name="worldPos">Converted coordinates</param> /// <param name="worldPos">Converted coordinates.</param>
/// <param name="rayDistance">How far to search for a collision</param> /// <param name="rayDistance">How far to search for a collision.</param>
/// <returns>True if successful. On false, worldPos's contents are undefined</returns> /// <returns>True if successful. On false, worldPos's contents are undefined.</returns>
public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f) public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000.0f)
{ {
// The game is only visible in the main viewport, so if the cursor is outside // The game is only visible in the main viewport, so if the cursor is outside
@ -350,7 +277,7 @@ namespace Dalamud.Game.Internal.Gui {
if (screenPos.X < windowPos.X || screenPos.X > windowPos.X + windowSize.X || if (screenPos.X < windowPos.X || screenPos.X > windowPos.X + windowSize.X ||
screenPos.Y < windowPos.Y || screenPos.Y > windowPos.Y + windowSize.Y) screenPos.Y < windowPos.Y || screenPos.Y > windowPos.Y + windowSize.Y)
{ {
worldPos = new Vector3(); worldPos = default;
return false; return false;
} }
@ -358,7 +285,7 @@ namespace Dalamud.Game.Internal.Gui {
var matrixSingleton = this.getMatrixSingleton(); var matrixSingleton = this.getMatrixSingleton();
// Read current ViewProjectionMatrix plus game window size // Read current ViewProjectionMatrix plus game window size
var viewProjectionMatrix = new Matrix(); var viewProjectionMatrix = default(Matrix);
float width, height; float width, height;
unsafe unsafe
{ {
@ -374,10 +301,11 @@ namespace Dalamud.Game.Internal.Gui {
viewProjectionMatrix.Invert(); viewProjectionMatrix.Invert();
var localScreenPos = new Vector2(screenPos.X - windowPos.X, screenPos.Y - windowPos.Y); var localScreenPos = new Vector2(screenPos.X - windowPos.X, screenPos.Y - windowPos.Y);
var screenPos3D = new Vector3 { var screenPos3D = new Vector3
X = localScreenPos.X / width * 2.0f - 1.0f, {
Y = -(localScreenPos.Y / height * 2.0f - 1.0f), X = (localScreenPos.X / width * 2.0f) - 1.0f,
Z = 0 Y = -((localScreenPos.Y / height * 2.0f) - 1.0f),
Z = 0,
}; };
Vector3.TransformCoordinate(ref screenPos3D, ref viewProjectionMatrix, out var camPos); Vector3.TransformCoordinate(ref screenPos3D, ref viewProjectionMatrix, out var camPos);
@ -389,7 +317,8 @@ namespace Dalamud.Game.Internal.Gui {
clipPos.Normalize(); clipPos.Normalize();
bool isSuccess; bool isSuccess;
unsafe { unsafe
{
var camPosArray = camPos.ToArray(); var camPosArray = camPos.ToArray();
var clipPosArray = clipPos.ToArray(); var clipPosArray = clipPos.ToArray();
@ -397,54 +326,46 @@ namespace Dalamud.Game.Internal.Gui {
var worldPosArray = stackalloc float[32]; var worldPosArray = stackalloc float[32];
// Theory: this is some kind of flag on what type of things the ray collides with // Theory: this is some kind of flag on what type of things the ray collides with
var unknown = stackalloc int[3] { 0x4000, 0x4000, 0x0 }; var unknown = stackalloc int[3]
{
0x4000,
0x4000,
0x0,
};
fixed (float* pCamPos = camPosArray) { fixed (float* pCamPos = camPosArray)
fixed (float* pClipPos = clipPosArray) { {
fixed (float* pClipPos = clipPosArray)
{
isSuccess = this.screenToWorldNative(pCamPos, pClipPos, rayDistance, worldPosArray, unknown); isSuccess = this.screenToWorldNative(pCamPos, pClipPos, rayDistance, worldPosArray, unknown);
} }
} }
worldPos = new Vector3 { worldPos = new Vector3
{
X = worldPosArray[0], X = worldPosArray[0],
Y = worldPosArray[1], Y = worldPosArray[1],
Z = worldPosArray[2] Z = worldPosArray[2],
}; };
} }
return isSuccess; return isSuccess;
} }
private IntPtr ToggleUiHideDetour(IntPtr thisPtr, byte unknownByte) {
GameUiHidden = !GameUiHidden;
try {
OnUiHideToggled?.Invoke(this, GameUiHidden);
} catch (Exception ex) {
Log.Error(ex, "Error on OnUiHideToggled event dispatch");
}
Log.Debug("UiHide toggled: {0}", GameUiHidden);
return this.toggleUiHideHook.Original(thisPtr, unknownByte);
}
/// <summary> /// <summary>
/// Gets a pointer to the game's UI module. /// Gets a pointer to the game's UI module.
/// </summary> /// </summary>
/// <returns>IntPtr pointing to UI module</returns> /// <returns>IntPtr pointing to UI module.</returns>
public IntPtr GetUIModule() public IntPtr GetUIModule() => this.getUiModule(this.dalamud.Framework.Address.BaseAddress);
{
return this.getUiModule(this.dalamud.Framework.Address.BaseAddress);
}
/// <summary> /// <summary>
/// Gets the pointer to the UI Object with the given name and index. /// Gets the pointer to the UI Object with the given name and index.
/// </summary> /// </summary>
/// <param name="name">Name of UI to find</param> /// <param name="name">Name of UI to find.</param>
/// <param name="index">Index of UI to find (1-indexed)</param> /// <param name="index">Index of UI to find (1-indexed).</param>
/// <returns>IntPtr.Zero if unable to find UI, otherwise IntPtr pointing to the start of the UI Object</returns> /// <returns>IntPtr.Zero if unable to find UI, otherwise IntPtr pointing to the start of the UI Object.</returns>
public IntPtr GetUiObjectByName(string name, int index) { public IntPtr GetUiObjectByName(string name, int index)
{
var baseUi = this.GetBaseUIObject(); var baseUi = this.GetBaseUIObject();
if (baseUi == IntPtr.Zero) return IntPtr.Zero; if (baseUi == IntPtr.Zero) return IntPtr.Zero;
var baseUiProperties = Marshal.ReadIntPtr(baseUi, 0x20); var baseUiProperties = Marshal.ReadIntPtr(baseUi, 0x20);
@ -452,19 +373,36 @@ namespace Dalamud.Game.Internal.Gui {
return this.getUIObjectByName(baseUiProperties, name, index); return this.getUIObjectByName(baseUiProperties, name, index);
} }
public Addon.Addon GetAddonByName(string name, int index) { /// <summary>
var addonMem = GetUiObjectByName(name, index); /// Gets an Addon by it's internal name.
/// </summary>
/// <param name="name">The addon name.</param>
/// <param name="index">The index of the addon, starting at 1.</param>
/// <returns>The native memory representation of the addon, if it exists.</returns>
public Addon.Addon GetAddonByName(string name, int index)
{
var addonMem = this.GetUiObjectByName(name, index);
if (addonMem == IntPtr.Zero) return null; if (addonMem == IntPtr.Zero) return null;
var addonStruct = Marshal.PtrToStructure<Structs.Addon>(addonMem); var addonStruct = Marshal.PtrToStructure<Structs.Addon>(addonMem);
return new Addon.Addon(addonMem, addonStruct); return new Addon.Addon(addonMem, addonStruct);
} }
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addonName">The addon name.</param>
/// <returns>A pointer to the agent interface.</returns>
public IntPtr FindAgentInterface(string addonName) public IntPtr FindAgentInterface(string addonName)
{ {
var addon = this.dalamud.Framework.Gui.GetUiObjectByName(addonName, 1); var addon = this.dalamud.Framework.Gui.GetUiObjectByName(addonName, 1);
return this.FindAgentInterface(addon); return this.FindAgentInterface(addon);
} }
/// <summary>
/// Find the agent associated with an addon, if possible.
/// </summary>
/// <param name="addon">The addon address.</param>
/// <returns>A pointer to the agent interface.</returns>
public IntPtr FindAgentInterface(IntPtr addon) public IntPtr FindAgentInterface(IntPtr addon)
{ {
if (addon == IntPtr.Zero) if (addon == IntPtr.Zero)
@ -501,12 +439,20 @@ namespace Dalamud.Game.Internal.Gui {
return IntPtr.Zero; return IntPtr.Zero;
} }
/// <summary>
/// Set the current background music.
/// </summary>
/// <param name="bgmKey">The background music key.</param>
public void SetBgm(ushort bgmKey) => this.setGlobalBgmHook.Original(bgmKey, 0, 0, 0, 0, 0); public void SetBgm(ushort bgmKey) => this.setGlobalBgmHook.Original(bgmKey, 0, 0, 0, 0, 0);
public void Enable() { /// <summary>
Chat.Enable(); /// Enables the hooks and submodules of this module.
Toast.Enable(); /// </summary>
PartyFinder.Enable(); public void Enable()
{
this.Chat.Enable();
this.Toast.Enable();
this.PartyFinder.Enable();
this.setGlobalBgmHook.Enable(); this.setGlobalBgmHook.Enable();
this.handleItemHoverHook.Enable(); this.handleItemHoverHook.Enable();
this.handleItemOutHook.Enable(); this.handleItemOutHook.Enable();
@ -515,10 +461,14 @@ namespace Dalamud.Game.Internal.Gui {
this.handleActionOutHook.Enable(); this.handleActionOutHook.Enable();
} }
public void Dispose() { /// <summary>
Chat.Dispose(); /// Disables the hooks and submodules of this module.
Toast.Dispose(); /// </summary>
PartyFinder.Dispose(); public void Dispose()
{
this.Chat.Dispose();
this.Toast.Dispose();
this.PartyFinder.Dispose();
this.setGlobalBgmHook.Dispose(); this.setGlobalBgmHook.Dispose();
this.handleItemHoverHook.Dispose(); this.handleItemHoverHook.Dispose();
this.handleItemOutHook.Dispose(); this.handleItemOutHook.Dispose();
@ -526,5 +476,132 @@ namespace Dalamud.Game.Internal.Gui {
this.handleActionHoverHook.Dispose(); this.handleActionHoverHook.Dispose();
this.handleActionOutHook.Dispose(); this.handleActionOutHook.Dispose();
} }
private IntPtr HandleSetGlobalBgmDetour(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6)
{
var retVal = this.setGlobalBgmHook.Original(bgmKey, a2, a3, a4, a5, a6);
Log.Verbose("SetGlobalBgm: {0} {1} {2} {3} {4} {5} -> {6}", bgmKey, a2, a3, a4, a5, a6, retVal);
return retVal;
}
private IntPtr HandleItemHoverDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4)
{
var retVal = this.handleItemHoverHook.Original(hoverState, a2, a3, a4);
if (retVal.ToInt64() == 22)
{
var itemId = (ulong)Marshal.ReadInt32(hoverState, 0x138);
this.HoveredItem = itemId;
try
{
this.HoveredItemChanged?.Invoke(this, itemId);
}
catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverItemId:{0} this:{1}", itemId, hoverState.ToInt64().ToString("X"));
}
return retVal;
}
private IntPtr HandleItemOutDetour(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4)
{
var retVal = this.handleItemOutHook.Original(hoverState, a2, a3, a4);
if (a3 != IntPtr.Zero && a4 == 1)
{
var a3Val = Marshal.ReadByte(a3, 0x8);
if (a3Val == 255)
{
this.HoveredItem = 0ul;
try
{
this.HoveredItemChanged?.Invoke(this, 0ul);
}
catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverItemId: 0");
}
}
return retVal;
}
private void HandleActionHoverDetour(IntPtr hoverState, HoverActionKind actionKind, uint actionId, int a4, byte a5)
{
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
this.HoveredAction.ActionKind = actionKind;
this.HoveredAction.BaseActionID = actionId;
this.HoveredAction.ActionID = (uint)Marshal.ReadInt32(hoverState, 0x3C);
try
{
this.HoveredActionChanged?.Invoke(this, this.HoveredAction);
}
catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredItemChanged event.");
}
Log.Verbose("HoverActionId: {0}/{1} this:{2}", actionKind, actionId, hoverState.ToInt64().ToString("X"));
}
private IntPtr HandleActionOutDetour(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4)
{
var retVal = this.handleActionOutHook.Original(agentActionDetail, a2, a3, a4);
if (a3 != IntPtr.Zero && a4 == 1)
{
var a3Val = Marshal.ReadByte(a3, 0x8);
if (a3Val == 255)
{
this.HoveredAction.ActionKind = HoverActionKind.None;
this.HoveredAction.BaseActionID = 0;
this.HoveredAction.ActionID = 0;
try
{
this.HoveredActionChanged?.Invoke(this, this.HoveredAction);
}
catch (Exception e)
{
Log.Error(e, "Could not dispatch HoveredActionChanged event.");
}
Log.Verbose("HoverActionId: 0");
}
}
return retVal;
}
private IntPtr ToggleUiHideDetour(IntPtr thisPtr, byte unknownByte)
{
this.GameUiHidden = !this.GameUiHidden;
try
{
this.OnUiHideToggled?.Invoke(this, this.GameUiHidden);
}
catch (Exception ex)
{
Log.Error(ex, "Error on OnUiHideToggled event dispatch");
}
Log.Debug("UiHide toggled: {0}", this.GameUiHidden);
return this.toggleUiHideHook.Original(thisPtr, unknownByte);
}
} }
} }

View file

@ -1,56 +1,123 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Serilog;
namespace Dalamud.Game.Internal.Gui { namespace Dalamud.Game.Internal.Gui
internal sealed class GameGuiAddressResolver : BaseAddressResolver { {
/// <summary>
/// The address resolver for the <see cref="GameGui"/> class.
/// </summary>
internal sealed class GameGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Initializes a new instance of the <see cref="GameGuiAddressResolver"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the native GuiManager class.</param>
public GameGuiAddressResolver(IntPtr baseAddress)
{
this.BaseAddress = baseAddress;
}
/// <summary>
/// Gets the base address of the native GuiManager class.
/// </summary>
public IntPtr BaseAddress { get; private set; } public IntPtr BaseAddress { get; private set; }
/// <summary>
/// Gets the address of the native ChatManager class.
/// </summary>
public IntPtr ChatManager { get; private set; } public IntPtr ChatManager { get; private set; }
/// <summary>
/// Gets the address of the native SetGlobalBgm method.
/// </summary>
public IntPtr SetGlobalBgm { get; private set; } public IntPtr SetGlobalBgm { get; private set; }
public IntPtr HandleItemHover { get; set; }
public IntPtr HandleItemOut { get; set; } /// <summary>
public IntPtr HandleActionHover { get; set; } /// Gets the address of the native HandleItemHover method.
public IntPtr HandleActionOut { get; set; } /// </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 GetUIObject method.
/// </summary>
public IntPtr GetUIObject { get; private set; } public IntPtr GetUIObject { get; private set; }
/// <summary>
/// Gets the address of the native GetMatrixSingleton method.
/// </summary>
public IntPtr GetMatrixSingleton { get; private set; } public IntPtr GetMatrixSingleton { get; private set; }
/// <summary>
/// Gets the address of the native ScreenToWorld method.
/// </summary>
public IntPtr ScreenToWorld { get; private set; } public IntPtr ScreenToWorld { get; private set; }
public IntPtr ToggleUiHide { get; set; }
/// <summary>
/// Gets the address of the native ToggleUiHide method.
/// </summary>
public IntPtr ToggleUiHide { get; private set; }
/// <summary>
/// Gets the address of the native Client::UI::UIModule getter method.
/// </summary>
public IntPtr GetBaseUIObject { get; private set; } public IntPtr GetBaseUIObject { get; private set; }
/// <summary>
/// Gets the address of the native GetUIObjectByName method.
/// </summary>
public IntPtr GetUIObjectByName { get; private set; } public IntPtr GetUIObjectByName { get; private set; }
/// <summary>
/// Gets the address of the native GetUIModule method.
/// </summary>
public IntPtr GetUIModule { get; private set; } public IntPtr GetUIModule { get; private set; }
/// <summary>
/// Gets the address of the native GetAgentModule method.
/// </summary>
public IntPtr GetAgentModule { get; private set; } public IntPtr GetAgentModule { get; private set; }
public GameGuiAddressResolver(IntPtr baseAddress) { /// <inheritdoc/>
BaseAddress = baseAddress; protected override void Setup64Bit(SigScanner sig)
} {
this.SetGlobalBgm = sig.ScanText("4C 8B 15 ?? ?? ?? ?? 4D 85 D2 74 58");
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ??");
private delegate IntPtr GetChatManagerDelegate(IntPtr guiManager); 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");
protected override void SetupInternal(SigScanner scanner) { this.HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F");
// Xiv__UiManager__GetChatManager 000 lea rax, [rcx+13E0h] this.GetUIObject = sig.ScanText("E8 ?? ?? ?? ?? 48 8B C8 48 8B 10 FF 52 40 80 88 ?? ?? ?? ?? 01 E9");
// Xiv__UiManager__GetChatManager+7 000 retn this.GetMatrixSingleton = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??");
ChatManager = BaseAddress + 0x13E0; 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.GetBaseUIObject = sig.ScanText("E8 ?? ?? ?? ?? 41 B8 01 00 00 00 48 8D 15 ?? ?? ?? ?? 48 8B 48 20 E8 ?? ?? ?? ?? 48 8B CF");
protected override void Setup64Bit(SigScanner sig) { this.GetUIObjectByName = sig.ScanText("E8 ?? ?? ?? ?? 48 8B CF 48 89 87 ?? ?? 00 00 E8 ?? ?? ?? ?? 41 B8 01 00 00 00");
SetGlobalBgm = sig.ScanText("4C 8B 15 ?? ?? ?? ?? 4D 85 D2 74 58"); this.GetUIModule = sig.ScanText("E8 ?? ?? ?? ?? 48 8B C8 48 85 C0 75 2D");
HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ??");
HandleItemOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 4D");
HandleActionHover = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 83 F8 0F");
HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F");
GetUIObject = sig.ScanText("E8 ?? ?? ?? ?? 48 8B C8 48 8B 10 FF 52 40 80 88 ?? ?? ?? ?? 01 E9");
GetMatrixSingleton = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??");
ScreenToWorld = sig.ScanText("48 83 EC 48 48 8B 05 ?? ?? ?? ?? 4D 8B D1");
ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??");
GetBaseUIObject = sig.ScanText("E8 ?? ?? ?? ?? 41 B8 01 00 00 00 48 8D 15 ?? ?? ?? ?? 48 8B 48 20 E8 ?? ?? ?? ?? 48 8B CF");
GetUIObjectByName = sig.ScanText("E8 ?? ?? ?? ?? 48 8B CF 48 89 87 ?? ?? 00 00 E8 ?? ?? ?? ?? 41 B8 01 00 00 00");
GetUIModule = sig.ScanText("E8 ?? ?? ?? ?? 48 8B C8 48 85 C0 75 2D");
var uiModuleVtableSig = sig.GetStaticAddressFromSig("48 8D 05 ?? ?? ?? ?? 4C 89 61 28"); var uiModuleVtableSig = sig.GetStaticAddressFromSig("48 8D 05 ?? ?? ?? ?? 4C 89 61 28");
this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size); this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size);
} }
/// <inheritdoc/>
protected override void SetupInternal(SigScanner scanner)
{
// Xiv__UiManager__GetChatManager 000 lea rax, [rcx+13E0h]
// Xiv__UiManager__GetChatManager+7 000 retn
this.ChatManager = this.BaseAddress + 0x13E0;
}
} }
} }

View file

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

View file

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

View file

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

View file

@ -1,71 +1,90 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.Internal.Gui.Structs; using Dalamud.Game.Internal.Gui.Structs;
using Dalamud.Hooking; using Dalamud.Hooking;
using Serilog; using Serilog;
namespace Dalamud.Game.Internal.Gui { namespace Dalamud.Game.Internal.Gui
public sealed class PartyFinderGui : IDisposable { {
#region Events
public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args);
/// <summary> /// <summary>
/// Event fired each time the game receives an individual Party Finder listing. Cannot modify listings but can /// This class handles interacting with the native PartyFinder window.
/// hide them.
/// </summary> /// </summary>
public event PartyFinderListingEventDelegate ReceiveListing; public sealed class PartyFinderGui : IDisposable
{
#endregion private readonly Dalamud dalamud;
private readonly PartyFinderAddressResolver address;
#region Hooks private readonly IntPtr memory;
private readonly Hook<ReceiveListingDelegate> receiveListingHook; private readonly Hook<ReceiveListingDelegate> receiveListingHook;
#endregion /// <summary>
/// Initializes a new instance of the <see cref="PartyFinderGui"/> class.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
public PartyFinderGui(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
#region Delegates this.address = new PartyFinderAddressResolver();
this.address.Setup(scanner);
this.memory = Marshal.AllocHGlobal(PartyFinder.PacketInfo.PacketSize);
this.receiveListingHook = new Hook<ReceiveListingDelegate>(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour));
}
/// <summary>
/// Event type fired each time the game receives an individual Party Finder listing.
/// Cannot modify listings but can hide them.
/// </summary>
/// <param name="listing">The listings received.</param>
/// <param name="args">Additional arguments passed by the game.</param>
public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data); private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data);
#endregion /// <summary>
/// Event fired each time the game receives an individual Party Finder listing.
/// Cannot modify listings but can hide them.
/// </summary>
public event PartyFinderListingEventDelegate ReceiveListing;
private Dalamud Dalamud { get; } /// <summary>
private PartyFinderAddressResolver Address { get; } /// Enables this module.
private IntPtr Memory { get; } /// </summary>
public void Enable()
public PartyFinderGui(SigScanner scanner, Dalamud dalamud) { {
Dalamud = dalamud;
Address = new PartyFinderAddressResolver();
Address.Setup(scanner);
Memory = Marshal.AllocHGlobal(PartyFinder.PacketInfo.PacketSize);
this.receiveListingHook = new Hook<ReceiveListingDelegate>(Address.ReceiveListing, new ReceiveListingDelegate(HandleReceiveListingDetour));
}
public void Enable() {
this.receiveListingHook.Enable(); this.receiveListingHook.Enable();
} }
public void Dispose() { /// <summary>
/// Dispose of m anaged and unmanaged resources.
/// </summary>
public void Dispose()
{
this.receiveListingHook.Dispose(); this.receiveListingHook.Dispose();
Marshal.FreeHGlobal(Memory); Marshal.FreeHGlobal(this.memory);
} }
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data) { private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
try { {
HandleListingEvents(data); try
} catch (Exception ex) { {
this.HandleListingEvents(data);
}
catch (Exception ex)
{
Log.Error(ex, "Exception on ReceiveListing hook."); Log.Error(ex, "Exception on ReceiveListing hook.");
} }
this.receiveListingHook.Original(managerPtr, data); this.receiveListingHook.Original(managerPtr, data);
} }
private void HandleListingEvents(IntPtr data) { private void HandleListingEvents(IntPtr data)
{
var dataPtr = data + 0x10; var dataPtr = data + 0x10;
var packet = Marshal.PtrToStructure<PartyFinder.Packet>(dataPtr); var packet = Marshal.PtrToStructure<PartyFinder.Packet>(dataPtr);
@ -73,51 +92,66 @@ namespace Dalamud.Game.Internal.Gui {
// rewriting is an expensive operation, so only do it if necessary // rewriting is an expensive operation, so only do it if necessary
var needToRewrite = false; var needToRewrite = false;
for (var i = 0; i < packet.listings.Length; i++) { for (var i = 0; i < packet.Listings.Length; i++)
{
// these are empty slots that are not shown to the player // these are empty slots that are not shown to the player
if (packet.listings[i].IsNull()) { if (packet.Listings[i].IsNull())
{
continue; continue;
} }
var listing = new PartyFinderListing(packet.listings[i], Dalamud.Data, Dalamud.SeStringManager); var listing = new PartyFinderListing(packet.Listings[i], this.dalamud.Data, this.dalamud.SeStringManager);
var args = new PartyFinderListingEventArgs(packet.batchNumber); var args = new PartyFinderListingEventArgs(packet.BatchNumber);
ReceiveListing?.Invoke(listing, args); this.ReceiveListing?.Invoke(listing, args);
if (args.Visible) { if (args.Visible)
{
continue; continue;
} }
// hide the listing from the player by setting it to a null listing // hide the listing from the player by setting it to a null listing
packet.listings[i] = new PartyFinder.Listing(); packet.Listings[i] = default;
needToRewrite = true; needToRewrite = true;
} }
if (!needToRewrite) { if (!needToRewrite)
{
return; return;
} }
// write our struct into the memory (doing this directly crashes the game) // write our struct into the memory (doing this directly crashes the game)
Marshal.StructureToPtr(packet, Memory, false); Marshal.StructureToPtr(packet, this.memory, false);
// copy our new memory over the game's // copy our new memory over the game's
unsafe { unsafe
Buffer.MemoryCopy( {
(void*) Memory, Buffer.MemoryCopy((void*)this.memory, (void*)dataPtr, PartyFinder.PacketInfo.PacketSize, PartyFinder.PacketInfo.PacketSize);
(void*) dataPtr,
PartyFinder.PacketInfo.PacketSize,
PartyFinder.PacketInfo.PacketSize
);
} }
} }
} }
public class PartyFinderListingEventArgs { /// <summary>
/// This class represents additional arguments passed by the game.
/// </summary>
public class PartyFinderListingEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderListingEventArgs"/> class.
/// </summary>
/// <param name="batchNumber">The batch number.</param>
internal PartyFinderListingEventArgs(int batchNumber)
{
this.BatchNumber = batchNumber;
}
/// <summary>
/// Gets the batch number.
/// </summary>
public int BatchNumber { get; } public int BatchNumber { get; }
/// <summary>
/// Gets or sets a value indicating whether the listing is visible.
/// </summary>
public bool Visible { get; set; } = true; public bool Visible { get; set; } = true;
internal PartyFinderListingEventArgs(int batchNumber) {
BatchNumber = batchNumber;
}
} }
} }

View file

@ -1,8 +1,59 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.Internal.Gui.Structs { namespace Dalamud.Game.Internal.Gui.Structs
{
/// <summary>
/// Native memory representation of an FFXIV UI addon.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct Addon
{
/// <summary>
/// The name of the addon.
/// </summary>
[FieldOffset(AddonOffsets.Name)]
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 20)]
public string Name;
public class AddonOffsets { /// <summary>
/// Various flags that can be set on the addon.
/// </summary>
/// <remarks>
/// This is a bitfield.
/// </remarks>
[FieldOffset(AddonOffsets.Flags)]
public byte Flags;
/// <summary>
/// The X position of the addon on screen.
/// </summary>
[FieldOffset(AddonOffsets.X)]
public short X;
/// <summary>
/// The Y position of the addon on screen.
/// </summary>
[FieldOffset(AddonOffsets.Y)]
public short Y;
/// <summary>
/// The scale of the addon.
/// </summary>
[FieldOffset(AddonOffsets.Scale)]
public float Scale;
/// <summary>
/// The root node of the addon's node tree.
/// </summary>
[FieldOffset(AddonOffsets.RootNode)]
public unsafe AtkResNode* RootNode;
}
/// <summary>
/// Memory offsets for the <see cref="Addon"/> type.
/// </summary>
public static class AddonOffsets
{
public const int Name = 0x8; public const int Name = 0x8;
public const int RootNode = 0xC8; public const int RootNode = 0xC8;
public const int Flags = 0x182; public const int Flags = 0x182;
@ -10,17 +61,4 @@ namespace Dalamud.Game.Internal.Gui.Structs {
public const int Y = 0x1BE; public const int Y = 0x1BE;
public const int Scale = 0x1AC; public const int Scale = 0x1AC;
} }
[StructLayout(LayoutKind.Explicit)]
public struct Addon {
[FieldOffset(AddonOffsets.Name), MarshalAs(UnmanagedType.ByValTStr, SizeConst = 20)]
public string Name;
[FieldOffset(AddonOffsets.Flags)] public byte Flags;
[FieldOffset(AddonOffsets.X)] public short X;
[FieldOffset(AddonOffsets.Y)] public short Y;
[FieldOffset(AddonOffsets.Scale)] public float Scale;
[FieldOffset(AddonOffsets.RootNode)] public unsafe AtkResNode* RootNode;
}
} }

View file

@ -1,49 +1,130 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.Internal.Gui.Structs { namespace Dalamud.Game.Internal.Gui.Structs
{
/// <summary>
/// Native memory representation of a UI resource node.
/// </summary>
/// <remarks>
/// This is copied from https://github.com/aers/FFXIVClientStructs/blob/main/Component/GUI/AtkResNode.cs.
/// If you need newer features, include FFXIVClientStructs and ILMerge the assembly.
/// </remarks>
[StructLayout(LayoutKind.Explicit, Size = 0xA8)] [StructLayout(LayoutKind.Explicit, Size = 0xA8)]
public unsafe struct AtkResNode
{
[FieldOffset(0x0)]
public IntPtr AtkEventTarget;
// https://github.com/aers/FFXIVClientStructs/blob/main/Component/GUI/AtkResNode.cs [FieldOffset(0x8)]
public unsafe struct AtkResNode { public uint NodeID;
[FieldOffset(0x0)] public IntPtr AtkEventTarget;
[FieldOffset(0x8)] public uint NodeID; [FieldOffset(0x20)]
[FieldOffset(0x20)] public AtkResNode* ParentNode; public AtkResNode* ParentNode;
[FieldOffset(0x28)] public AtkResNode* PrevSiblingNode;
[FieldOffset(0x30)] public AtkResNode* NextSiblingNode; [FieldOffset(0x28)]
[FieldOffset(0x38)] public AtkResNode* ChildNode; public AtkResNode* PrevSiblingNode;
[FieldOffset(0x40)] public ushort Type;
[FieldOffset(0x42)] public ushort ChildCount; [FieldOffset(0x30)]
[FieldOffset(0x44)] public float X; public AtkResNode* NextSiblingNode;
[FieldOffset(0x48)] public float Y;
[FieldOffset(0x4C)] public float ScaleX; [FieldOffset(0x38)]
[FieldOffset(0x50)] public float ScaleY; public AtkResNode* ChildNode;
[FieldOffset(0x54)] public float Rotation;
[FieldOffset(0x58)] public fixed float UnkMatrix[3 * 2]; [FieldOffset(0x40)]
[FieldOffset(0x70)] public uint Color; public ushort Type;
[FieldOffset(0x74)] public float Depth;
[FieldOffset(0x78)] public float Depth_2; [FieldOffset(0x42)]
[FieldOffset(0x7C)] public ushort AddRed; public ushort ChildCount;
[FieldOffset(0x7E)] public ushort AddGreen;
[FieldOffset(0x80)] public ushort AddBlue; [FieldOffset(0x44)]
[FieldOffset(0x82)] public ushort AddRed_2; public float X;
[FieldOffset(0x84)] public ushort AddGreen_2;
[FieldOffset(0x86)] public ushort AddBlue_2; [FieldOffset(0x48)]
[FieldOffset(0x88)] public byte MultiplyRed; public float Y;
[FieldOffset(0x89)] public byte MultiplyGreen;
[FieldOffset(0x8A)] public byte MultiplyBlue; [FieldOffset(0x4C)]
[FieldOffset(0x8B)] public byte MultiplyRed_2; public float ScaleX;
[FieldOffset(0x8C)] public byte MultiplyGreen_2;
[FieldOffset(0x8D)] public byte MultiplyBlue_2; [FieldOffset(0x50)]
[FieldOffset(0x8E)] public byte Alpha_2; public float ScaleY;
[FieldOffset(0x8F)] public byte UnkByte_1;
[FieldOffset(0x90)] public ushort Width; [FieldOffset(0x54)]
[FieldOffset(0x92)] public ushort Height; public float Rotation;
[FieldOffset(0x94)] public float OriginX;
[FieldOffset(0x98)] public float OriginY; [FieldOffset(0x58)]
[FieldOffset(0x9C)] public ushort Priority; public fixed float UnkMatrix[3 * 2];
[FieldOffset(0x9E)] public short Flags;
[FieldOffset(0xA0)] public uint Flags_2; [FieldOffset(0x70)]
public uint Color;
[FieldOffset(0x74)]
public float Depth;
[FieldOffset(0x78)]
public float Depth_2;
[FieldOffset(0x7C)]
public ushort AddRed;
[FieldOffset(0x7E)]
public ushort AddGreen;
[FieldOffset(0x80)]
public ushort AddBlue;
[FieldOffset(0x82)]
public ushort AddRed_2;
[FieldOffset(0x84)]
public ushort AddGreen_2;
[FieldOffset(0x86)]
public ushort AddBlue_2;
[FieldOffset(0x88)]
public byte MultiplyRed;
[FieldOffset(0x89)]
public byte MultiplyGreen;
[FieldOffset(0x8A)]
public byte MultiplyBlue;
[FieldOffset(0x8B)]
public byte MultiplyRed_2;
[FieldOffset(0x8C)]
public byte MultiplyGreen_2;
[FieldOffset(0x8D)]
public byte MultiplyBlue_2;
[FieldOffset(0x8E)]
public byte Alpha_2;
[FieldOffset(0x8F)]
public byte UnkByte_1;
[FieldOffset(0x90)]
public ushort Width;
[FieldOffset(0x92)]
public ushort Height;
[FieldOffset(0x94)]
public float OriginX;
[FieldOffset(0x98)]
public float OriginY;
[FieldOffset(0x9C)]
public ushort Priority;
[FieldOffset(0x9E)]
public short Flags;
[FieldOffset(0xA0)]
public uint Flags_2;
} }
} }

View file

@ -1,455 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Internal.Gui.Structs {
#region Raw structs
internal static class PartyFinder {
public static class PacketInfo {
public static readonly int PacketSize = Marshal.SizeOf<Packet>();
}
namespace Dalamud.Game.Internal.Gui.Structs
{
/// <summary>
/// PartyFinder related network structs and static constants.
/// </summary>
internal static class PartyFinder
{
/// <summary>
/// The structure of the PartyFinder packet.
/// </summary>
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public readonly struct Packet { internal readonly struct Packet
public readonly int batchNumber; {
internal readonly int BatchNumber;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] padding1; private readonly byte[] padding1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public readonly Listing[] listings; internal readonly Listing[] Listings;
} }
/// <summary>
/// The structure of an individual listing within a packet.
/// </summary>
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public readonly struct Listing { internal readonly struct Listing
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
private readonly byte[] header1; private readonly byte[] header1;
internal readonly uint id; internal readonly uint Id;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
private readonly byte[] header2; private readonly byte[] header2;
internal readonly uint contentIdLower; internal readonly uint ContentIdLower;
private readonly ushort unknownShort1; private readonly ushort unknownShort1;
private readonly ushort unknownShort2; private readonly ushort unknownShort2;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
private readonly byte[] header3; private readonly byte[] header3;
internal readonly byte category; internal readonly byte Category;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header4; private readonly byte[] header4;
internal readonly ushort duty; internal readonly ushort Duty;
internal readonly byte dutyType; internal readonly byte DutyType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)]
private readonly byte[] header5; private readonly byte[] header5;
internal readonly ushort world; internal readonly ushort World;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] header6; private readonly byte[] header6;
internal readonly byte objective; internal readonly byte Objective;
internal readonly byte beginnersWelcome; internal readonly byte BeginnersWelcome;
internal readonly byte conditions; internal readonly byte Conditions;
internal readonly byte dutyFinderSettings; internal readonly byte DutyFinderSettings;
internal readonly byte lootRules; internal readonly byte LootRules;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header7; // all zero in every pf I've examined private readonly byte[] header7; // all zero in every pf I've examined
private readonly uint lastPatchHotfixTimestamp; // last time the servers were restarted? private readonly uint lastPatchHotfixTimestamp; // last time the servers were restarted?
internal readonly ushort secondsRemaining; internal readonly ushort SecondsRemaining;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
private readonly byte[] header8; // 00 00 01 00 00 00 in every pf I've examined private readonly byte[] header8; // 00 00 01 00 00 00 in every pf I've examined
internal readonly ushort minimumItemLevel; internal readonly ushort MinimumItemLevel;
internal readonly ushort homeWorld; internal readonly ushort HomeWorld;
internal readonly ushort currentWorld; internal readonly ushort CurrentWorld;
private readonly byte header9; private readonly byte header9;
internal readonly byte numSlots; internal readonly byte NumSlots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header10; private readonly byte[] header10;
internal readonly byte searchArea; internal readonly byte SearchArea;
private readonly byte header11; private readonly byte header11;
internal readonly byte numParties; internal readonly byte NumParties;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header12; // 00 00 00 always. maybe numParties is a u32? private readonly byte[] header12; // 00 00 00 always. maybe numParties is a u32?
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly uint[] slots; internal readonly uint[] Slots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly byte[] jobsPresent; internal readonly byte[] JobsPresent;
// Note that ByValTStr will not work here because the strings are UTF-8 and there's only a CharSet for UTF-16 in C#. // Note that ByValTStr will not work here because the strings are UTF-8 and there's only a CharSet for UTF-16 in C#.
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
internal readonly byte[] name; internal readonly byte[] Name;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 192)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 192)]
internal readonly byte[] description; internal readonly byte[] Description;
internal bool IsNull() { internal bool IsNull()
{
// a valid party finder must have at least one slot set // a valid party finder must have at least one slot set
return this.slots.All(slot => slot == 0); return this.Slots.All(slot => slot == 0);
}
}
}
#endregion
#region Read-only classes
public class PartyFinderListing {
/// <summary>
/// The ID assigned to this listing by the game's server.
/// </summary>
public uint Id { get; }
/// <summary>
/// The lower bits of the player's content ID.
/// </summary>
public uint ContentIdLower { get; }
/// <summary>
/// The name of the player hosting this listing.
/// </summary>
public SeString Name { get; }
/// <summary>
/// The description of this listing as set by the host. May be multiple lines.
/// </summary>
public SeString Description { get; }
/// <summary>
/// The world that this listing was created on.
/// </summary>
public Lazy<World> World { get; }
/// <summary>
/// The home world of the listing's host.
/// </summary>
public Lazy<World> HomeWorld { get; }
/// <summary>
/// The current world of the listing's host.
/// </summary>
public Lazy<World> CurrentWorld { get; }
/// <summary>
/// The Party Finder category this listing is listed under.
/// </summary>
public Category Category { get; }
/// <summary>
/// The row ID of the duty this listing is for. May be 0 for non-duty listings.
/// </summary>
public ushort RawDuty { get; }
/// <summary>
/// The duty this listing is for. May be null for non-duty listings.
/// </summary>
public Lazy<ContentFinderCondition> Duty { get; }
/// <summary>
/// The type of duty this listing is for.
/// </summary>
public DutyType DutyType { get; }
/// <summary>
/// If this listing is beginner-friendly. Shown with a sprout icon in-game.
/// </summary>
public bool BeginnersWelcome { get; }
/// <summary>
/// How many seconds this listing will continue to be available for. It may end before this time if the party
/// fills or the host ends it early.
/// </summary>
public ushort SecondsRemaining { get; }
/// <summary>
/// The minimum item level required to join this listing.
/// </summary>
public ushort MinimumItemLevel { get; }
/// <summary>
/// The number of parties this listing is recruiting for.
/// </summary>
public byte Parties { get; }
/// <summary>
/// The number of player slots this listing is recruiting for.
/// </summary>
public byte SlotsAvailable { get; }
/// <summary>
/// A list of player slots that the Party Finder is accepting.
/// </summary>
public IReadOnlyCollection<PartyFinderSlot> Slots => this.slots;
/// <summary>
/// The objective of this listing.
/// </summary>
public ObjectiveFlags Objective => (ObjectiveFlags) this.objective;
/// <summary>
/// The conditions of this listing.
/// </summary>
public ConditionFlags Conditions => (ConditionFlags) this.conditions;
/// <summary>
/// The Duty Finder settings that will be used for this listing.
/// </summary>
public DutyFinderSettingsFlags DutyFinderSettings => (DutyFinderSettingsFlags) this.dutyFinderSettings;
/// <summary>
/// The loot rules that will be used for this listing.
/// </summary>
public LootRuleFlags LootRules => (LootRuleFlags) this.lootRules;
/// <summary>
/// Where this listing is searching. Note that this is also used for denoting alliance raid listings and one
/// player per job.
/// </summary>
public SearchAreaFlags SearchArea => (SearchAreaFlags) this.searchArea;
/// <summary>
/// A list of the class/job IDs that are currently present in the party.
/// </summary>
public IReadOnlyCollection<byte> RawJobsPresent => this.jobsPresent;
/// <summary>
/// A list of the classes/jobs that are currently present in the party.
/// </summary>
public IReadOnlyCollection<Lazy<ClassJob>> JobsPresent { get; }
#region Backing fields
private readonly byte objective;
private readonly byte conditions;
private readonly byte dutyFinderSettings;
private readonly byte lootRules;
private readonly byte searchArea;
private readonly PartyFinderSlot[] slots;
private readonly byte[] jobsPresent;
#endregion
#region Indexers
public bool this[ObjectiveFlags flag] => this.objective == 0 || (this.objective & (uint) flag) > 0;
public bool this[ConditionFlags flag] => this.conditions == 0 || (this.conditions & (uint) flag) > 0;
public bool this[DutyFinderSettingsFlags flag] => this.dutyFinderSettings == 0 || (this.dutyFinderSettings & (uint) flag) > 0;
public bool this[LootRuleFlags flag] => this.lootRules == 0 || (this.lootRules & (uint) flag) > 0;
public bool this[SearchAreaFlags flag] => this.searchArea == 0 || (this.searchArea & (uint) flag) > 0;
#endregion
internal PartyFinderListing(PartyFinder.Listing listing, DataManager dataManager, SeStringManager seStringManager) {
this.objective = listing.objective;
this.conditions = listing.conditions;
this.dutyFinderSettings = listing.dutyFinderSettings;
this.lootRules = listing.lootRules;
this.searchArea = listing.searchArea;
this.slots = listing.slots.Select(accepting => new PartyFinderSlot(accepting)).ToArray();
this.jobsPresent = listing.jobsPresent;
Id = listing.id;
ContentIdLower = listing.contentIdLower;
Name = seStringManager.Parse(listing.name.TakeWhile(b => b != 0).ToArray());
Description = seStringManager.Parse(listing.description.TakeWhile(b => b != 0).ToArray());
World = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.world));
HomeWorld = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.homeWorld));
CurrentWorld = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.currentWorld));
Category = (Category) listing.category;
RawDuty = listing.duty;
Duty = new Lazy<ContentFinderCondition>(() => dataManager.GetExcelSheet<ContentFinderCondition>().GetRow(listing.duty));
DutyType = (DutyType) listing.dutyType;
BeginnersWelcome = listing.beginnersWelcome == 1;
SecondsRemaining = listing.secondsRemaining;
MinimumItemLevel = listing.minimumItemLevel;
Parties = listing.numParties;
SlotsAvailable = listing.numSlots;
JobsPresent = listing.jobsPresent
.Select(id => new Lazy<ClassJob>(() => id == 0
? null
: dataManager.GetExcelSheet<ClassJob>().GetRow(id)))
.ToArray();
} }
} }
/// <summary> /// <summary>
/// A player slot in a Party Finder listing. /// PartyFinder packet constants.
/// </summary> /// </summary>
public class PartyFinderSlot { public static class PacketInfo
private readonly uint accepting; {
private JobFlags[] listAccepting;
/// <summary> /// <summary>
/// List of jobs that this slot is accepting. /// The size of the PartyFinder packet.
/// </summary> /// </summary>
public IReadOnlyCollection<JobFlags> Accepting { public static readonly int PacketSize = Marshal.SizeOf<Packet>();
get {
if (this.listAccepting != null) {
return this.listAccepting;
}
this.listAccepting = Enum.GetValues(typeof(JobFlags))
.Cast<JobFlags>()
.Where(flag => this[flag])
.ToArray();
return this.listAccepting;
} }
} }
/// <summary>
/// Tests if this slot is accepting a job.
/// </summary>
/// <param name="flag">Job to test</param>
public bool this[JobFlags flag] => (this.accepting & (uint) flag) > 0;
internal PartyFinderSlot(uint accepting) {
this.accepting = accepting;
}
}
[Flags]
public enum SearchAreaFlags : uint {
DataCentre = 1 << 0,
Private = 1 << 1,
AllianceRaid = 1 << 2,
World = 1 << 3,
OnePlayerPerJob = 1 << 5,
}
[Flags]
public enum JobFlags {
Gladiator = 1 << 1,
Pugilist = 1 << 2,
Marauder = 1 << 3,
Lancer = 1 << 4,
Archer = 1 << 5,
Conjurer = 1 << 6,
Thaumaturge = 1 << 7,
Paladin = 1 << 8,
Monk = 1 << 9,
Warrior = 1 << 10,
Dragoon = 1 << 11,
Bard = 1 << 12,
WhiteMage = 1 << 13,
BlackMage = 1 << 14,
Arcanist = 1 << 15,
Summoner = 1 << 16,
Scholar = 1 << 17,
Rogue = 1 << 18,
Ninja = 1 << 19,
Machinist = 1 << 20,
DarkKnight = 1 << 21,
Astrologian = 1 << 22,
Samurai = 1 << 23,
RedMage = 1 << 24,
BlueMage = 1 << 25,
Gunbreaker = 1 << 26,
Dancer = 1 << 27,
}
public static class JobFlagsExt {
/// <summary>
/// Get the actual ClassJob from the in-game sheets for this JobFlags.
/// </summary>
/// <param name="job">A JobFlags enum member</param>
/// <param name="data">A DataManager to get the ClassJob from</param>
/// <returns>A ClassJob if found or null if not</returns>
public static ClassJob ClassJob(this JobFlags job, DataManager data) {
var jobs = data.GetExcelSheet<ClassJob>();
uint? row = job switch {
JobFlags.Gladiator => 1,
JobFlags.Pugilist => 2,
JobFlags.Marauder => 3,
JobFlags.Lancer => 4,
JobFlags.Archer => 5,
JobFlags.Conjurer => 6,
JobFlags.Thaumaturge => 7,
JobFlags.Paladin => 19,
JobFlags.Monk => 20,
JobFlags.Warrior => 21,
JobFlags.Dragoon => 22,
JobFlags.Bard => 23,
JobFlags.WhiteMage => 24,
JobFlags.BlackMage => 25,
JobFlags.Arcanist => 26,
JobFlags.Summoner => 27,
JobFlags.Scholar => 28,
JobFlags.Rogue => 29,
JobFlags.Ninja => 30,
JobFlags.Machinist => 31,
JobFlags.DarkKnight => 32,
JobFlags.Astrologian => 33,
JobFlags.Samurai => 34,
JobFlags.RedMage => 35,
JobFlags.BlueMage => 36,
JobFlags.Gunbreaker => 37,
JobFlags.Dancer => 38,
_ => null,
};
return row == null ? null : jobs.GetRow((uint) row);
}
}
[Flags]
public enum ObjectiveFlags : uint {
None = 0,
DutyCompletion = 1,
Practice = 2,
Loot = 4,
}
[Flags]
public enum ConditionFlags : uint {
None = 1,
DutyComplete = 2,
DutyIncomplete = 4,
}
[Flags]
public enum DutyFinderSettingsFlags : uint {
None = 0,
UndersizedParty = 1 << 0,
MinimumItemLevel = 1 << 1,
SilenceEcho = 1 << 2,
}
[Flags]
public enum LootRuleFlags : uint {
None = 0,
GreedOnly = 1,
Lootmaster = 2,
}
public enum Category {
Duty = 0,
QuestBattles = 1 << 0,
Fates = 1 << 1,
TreasureHunt = 1 << 2,
TheHunt = 1 << 3,
GatheringForays = 1 << 4,
DeepDungeons = 1 << 5,
AdventuringForays = 1 << 6,
}
public enum DutyType {
Other = 0,
Roulette = 1 << 0,
Normal = 1 << 1,
}
#endregion
} }

View file

@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Internal.Gui.Structs
{
/// <summary>
/// A single listing in party finder.
/// </summary>
public class PartyFinderListing
{
#region Backing fields
private readonly byte objective;
private readonly byte conditions;
private readonly byte dutyFinderSettings;
private readonly byte lootRules;
private readonly byte searchArea;
private readonly PartyFinderSlot[] slots;
private readonly byte[] jobsPresent;
#endregion
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderListing"/> class.
/// </summary>
/// <param name="listing">The interop listing data.</param>
/// <param name="dataManager">The DataManager instance.</param>
/// <param name="seStringManager">The SeStringManager instance.</param>
internal PartyFinderListing(PartyFinder.Listing listing, DataManager dataManager, SeStringManager seStringManager)
{
this.objective = listing.Objective;
this.conditions = listing.Conditions;
this.dutyFinderSettings = listing.DutyFinderSettings;
this.lootRules = listing.LootRules;
this.searchArea = listing.SearchArea;
this.slots = listing.Slots.Select(accepting => new PartyFinderSlot(accepting)).ToArray();
this.jobsPresent = listing.JobsPresent;
this.Id = listing.Id;
this.ContentIdLower = listing.ContentIdLower;
this.Name = seStringManager.Parse(listing.Name.TakeWhile(b => b != 0).ToArray());
this.Description = seStringManager.Parse(listing.Description.TakeWhile(b => b != 0).ToArray());
this.World = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.World));
this.HomeWorld = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.HomeWorld));
this.CurrentWorld = new Lazy<World>(() => dataManager.GetExcelSheet<World>().GetRow(listing.CurrentWorld));
this.Category = (Category)listing.Category;
this.RawDuty = listing.Duty;
this.Duty = new Lazy<ContentFinderCondition>(() => dataManager.GetExcelSheet<ContentFinderCondition>().GetRow(listing.Duty));
this.DutyType = (DutyType)listing.DutyType;
this.BeginnersWelcome = listing.BeginnersWelcome == 1;
this.SecondsRemaining = listing.SecondsRemaining;
this.MinimumItemLevel = listing.MinimumItemLevel;
this.Parties = listing.NumParties;
this.SlotsAvailable = listing.NumSlots;
this.JobsPresent = listing.JobsPresent
.Select(id => new Lazy<ClassJob>(
() => id == 0
? null
: dataManager.GetExcelSheet<ClassJob>().GetRow(id)))
.ToArray();
}
/// <summary>
/// Gets the ID assigned to this listing by the game's server.
/// </summary>
public uint Id { get; }
/// <summary>
/// Gets the lower bits of the player's content ID.
/// </summary>
public uint ContentIdLower { get; }
/// <summary>
/// Gets the name of the player hosting this listing.
/// </summary>
public SeString Name { get; }
/// <summary>
/// Gets the description of this listing as set by the host. May be multiple lines.
/// </summary>
public SeString Description { get; }
/// <summary>
/// Gets the world that this listing was created on.
/// </summary>
public Lazy<World> World { get; }
/// <summary>
/// Gets the home world of the listing's host.
/// </summary>
public Lazy<World> HomeWorld { get; }
/// <summary>
/// Gets the current world of the listing's host.
/// </summary>
public Lazy<World> CurrentWorld { get; }
/// <summary>
/// Gets the Party Finder category this listing is listed under.
/// </summary>
public Category Category { get; }
/// <summary>
/// Gets the row ID of the duty this listing is for. May be 0 for non-duty listings.
/// </summary>
public ushort RawDuty { get; }
/// <summary>
/// Gets the duty this listing is for. May be null for non-duty listings.
/// </summary>
public Lazy<ContentFinderCondition> Duty { get; }
/// <summary>
/// Gets the type of duty this listing is for.
/// </summary>
public DutyType DutyType { get; }
/// <summary>
/// Gets a value indicating whether if this listing is beginner-friendly. Shown with a sprout icon in-game.
/// </summary>
public bool BeginnersWelcome { get; }
/// <summary>
/// Gets how many seconds this listing will continue to be available for. It may end before this time if the party
/// fills or the host ends it early.
/// </summary>
public ushort SecondsRemaining { get; }
/// <summary>
/// Gets the minimum item level required to join this listing.
/// </summary>
public ushort MinimumItemLevel { get; }
/// <summary>
/// Gets the number of parties this listing is recruiting for.
/// </summary>
public byte Parties { get; }
/// <summary>
/// Gets the number of player slots this listing is recruiting for.
/// </summary>
public byte SlotsAvailable { get; }
/// <summary>
/// Gets a list of player slots that the Party Finder is accepting.
/// </summary>
public IReadOnlyCollection<PartyFinderSlot> Slots => this.slots;
/// <summary>
/// Gets the objective of this listing.
/// </summary>
public ObjectiveFlags Objective => (ObjectiveFlags)this.objective;
/// <summary>
/// Gets the conditions of this listing.
/// </summary>
public ConditionFlags Conditions => (ConditionFlags)this.conditions;
/// <summary>
/// Gets the Duty Finder settings that will be used for this listing.
/// </summary>
public DutyFinderSettingsFlags DutyFinderSettings => (DutyFinderSettingsFlags)this.dutyFinderSettings;
/// <summary>
/// Gets the loot rules that will be used for this listing.
/// </summary>
public LootRuleFlags LootRules => (LootRuleFlags)this.lootRules;
/// <summary>
/// Gets where this listing is searching. Note that this is also used for denoting alliance raid listings and one
/// player per job.
/// </summary>
public SearchAreaFlags SearchArea => (SearchAreaFlags)this.searchArea;
/// <summary>
/// Gets a list of the class/job IDs that are currently present in the party.
/// </summary>
public IReadOnlyCollection<byte> RawJobsPresent => this.jobsPresent;
/// <summary>
/// Gets a list of the classes/jobs that are currently present in the party.
/// </summary>
public IReadOnlyCollection<Lazy<ClassJob>> JobsPresent { get; }
#region Indexers
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[ObjectiveFlags flag] => this.objective == 0 || (this.objective & (uint)flag) > 0;
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[ConditionFlags flag] => this.conditions == 0 || (this.conditions & (uint)flag) > 0;
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[DutyFinderSettingsFlags flag] => this.dutyFinderSettings == 0 || (this.dutyFinderSettings & (uint)flag) > 0;
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[LootRuleFlags flag] => this.lootRules == 0 || (this.lootRules & (uint)flag) > 0;
/// <summary>
/// Check if the given flag is present.
/// </summary>
/// <param name="flag">The flag to check for.</param>
/// <returns>A value indicating whether the flag is present.</returns>
public bool this[SearchAreaFlags flag] => this.searchArea == 0 || (this.searchArea & (uint)flag) > 0;
#endregion
}
}

View file

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Dalamud.Game.Internal.Gui.Structs
{
/// <summary>
/// A player slot in a Party Finder listing.
/// </summary>
public class PartyFinderSlot
{
private readonly uint accepting;
private JobFlags[] listAccepting;
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderSlot"/> class.
/// </summary>
/// <param name="accepting">The flag value of accepted jobs.</param>
internal PartyFinderSlot(uint accepting)
{
this.accepting = accepting;
}
/// <summary>
/// Gets a list of jobs that this slot is accepting.
/// </summary>
public IReadOnlyCollection<JobFlags> Accepting
{
get
{
if (this.listAccepting != null)
{
return this.listAccepting;
}
this.listAccepting = Enum.GetValues(typeof(JobFlags))
.Cast<JobFlags>()
.Where(flag => this[flag])
.ToArray();
return this.listAccepting;
}
}
/// <summary>
/// Tests if this slot is accepting a job.
/// </summary>
/// <param name="flag">Job to test.</param>
public bool this[JobFlags flag] => (this.accepting & (uint)flag) > 0;
}
}

View file

@ -0,0 +1,397 @@
using System;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.Internal.Gui.Structs
{
/// <summary>
/// Search area flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum SearchAreaFlags : uint
{
/// <summary>
/// Datacenter.
/// </summary>
DataCentre = 1 << 0,
/// <summary>
/// Private.
/// </summary>
Private = 1 << 1,
/// <summary>
/// Alliance raid.
/// </summary>
AllianceRaid = 1 << 2,
/// <summary>
/// World.
/// </summary>
World = 1 << 3,
/// <summary>
/// One player per job.
/// </summary>
OnePlayerPerJob = 1 << 5,
}
/// <summary>
/// Job flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum JobFlags
{
/// <summary>
/// Gladiator (GLD).
/// </summary>
Gladiator = 1 << 1,
/// <summary>
/// Pugilist (PGL).
/// </summary>
Pugilist = 1 << 2,
/// <summary>
/// Marauder (MRD).
/// </summary>
Marauder = 1 << 3,
/// <summary>
/// Lancer (LNC).
/// </summary>
Lancer = 1 << 4,
/// <summary>
/// Archer (ARC).
/// </summary>
Archer = 1 << 5,
/// <summary>
/// Conjurer (CNJ).
/// </summary>
Conjurer = 1 << 6,
/// <summary>
/// Thaumaturge (THM).
/// </summary>
Thaumaturge = 1 << 7,
/// <summary>
/// Paladin (PLD).
/// </summary>
Paladin = 1 << 8,
/// <summary>
/// Monk (MNK).
/// </summary>
Monk = 1 << 9,
/// <summary>
/// Warrior (WAR).
/// </summary>
Warrior = 1 << 10,
/// <summary>
/// Dragoon (DRG).
/// </summary>
Dragoon = 1 << 11,
/// <summary>
/// Bard (BRD).
/// </summary>
Bard = 1 << 12,
/// <summary>
/// White mage (WHM).
/// </summary>
WhiteMage = 1 << 13,
/// <summary>
/// Black mage (BLM).
/// </summary>
BlackMage = 1 << 14,
/// <summary>
/// Arcanist (ACN).
/// </summary>
Arcanist = 1 << 15,
/// <summary>
/// Summoner (SMN).
/// </summary>
Summoner = 1 << 16,
/// <summary>
/// Scholar (SCH).
/// </summary>
Scholar = 1 << 17,
/// <summary>
/// Rogue (ROG).
/// </summary>
Rogue = 1 << 18,
/// <summary>
/// Ninja (NIN).
/// </summary>
Ninja = 1 << 19,
/// <summary>
/// Machinist (MCH).
/// </summary>
Machinist = 1 << 20,
/// <summary>
/// Dark Knight (DRK).
/// </summary>
DarkKnight = 1 << 21,
/// <summary>
/// Astrologian (AST).
/// </summary>
Astrologian = 1 << 22,
/// <summary>
/// Samurai (SAM).
/// </summary>
Samurai = 1 << 23,
/// <summary>
/// Red mage (RDM).
/// </summary>
RedMage = 1 << 24,
/// <summary>
/// Blue mage (BLM).
/// </summary>
BlueMage = 1 << 25,
/// <summary>
/// Gunbreaker (GNB).
/// </summary>
Gunbreaker = 1 << 26,
/// <summary>
/// Dancer (DNC).
/// </summary>
Dancer = 1 << 27,
}
/// <summary>
/// Objective flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum ObjectiveFlags : uint
{
/// <summary>
/// No objective.
/// </summary>
None = 0,
/// <summary>
/// The duty completion objective.
/// </summary>
DutyCompletion = 1,
/// <summary>
/// The practice objective.
/// </summary>
Practice = 2,
/// <summary>
/// The loot objective.
/// </summary>
Loot = 4,
}
/// <summary>
/// Condition flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum ConditionFlags : uint
{
/// <summary>
/// No duty condition.
/// </summary>
None = 1,
/// <summary>
/// The duty complete condition.
/// </summary>
DutyComplete = 2,
/// <summary>
/// The duty incomplete condition.
/// </summary>
DutyIncomplete = 4,
}
/// <summary>
/// Duty finder settings flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum DutyFinderSettingsFlags : uint
{
/// <summary>
/// No duty finder settings.
/// </summary>
None = 0,
/// <summary>
/// The undersized party setting.
/// </summary>
UndersizedParty = 1 << 0,
/// <summary>
/// The minimum item level setting.
/// </summary>
MinimumItemLevel = 1 << 1,
/// <summary>
/// The silence echo setting.
/// </summary>
SilenceEcho = 1 << 2,
}
/// <summary>
/// Loot rule flags for the <see cref="PartyFinder"/> class.
/// </summary>
[Flags]
public enum LootRuleFlags : uint
{
/// <summary>
/// No loot rules.
/// </summary>
None = 0,
/// <summary>
/// The greed only rule.
/// </summary>
GreedOnly = 1,
/// <summary>
/// The lootmaster rule.
/// </summary>
Lootmaster = 2,
}
/// <summary>
/// Category flags for the <see cref="PartyFinder"/> class.
/// </summary>
public enum Category
{
/// <summary>
/// The duty category.
/// </summary>
Duty = 0,
/// <summary>
/// The quest battle category.
/// </summary>
QuestBattles = 1 << 0,
/// <summary>
/// The fate category.
/// </summary>
Fates = 1 << 1,
/// <summary>
/// The treasure hunt category.
/// </summary>
TreasureHunt = 1 << 2,
/// <summary>
/// The hunt category.
/// </summary>
TheHunt = 1 << 3,
/// <summary>
/// The gathering forays category.
/// </summary>
GatheringForays = 1 << 4,
/// <summary>
/// The deep dungeons category.
/// </summary>
DeepDungeons = 1 << 5,
/// <summary>
/// The adventuring forays category.
/// </summary>
AdventuringForays = 1 << 6,
}
/// <summary>
/// Duty type flags for the <see cref="PartyFinder"/> class.
/// </summary>
public enum DutyType
{
/// <summary>
/// No duty type.
/// </summary>
Other = 0,
/// <summary>
/// The roulette duty type.
/// </summary>
Roulette = 1 << 0,
/// <summary>
/// The normal duty type.
/// </summary>
Normal = 1 << 1,
}
/// <summary>
/// Extensions for the <see cref="JobFlags"/> enum.
/// </summary>
public static class JobFlagsExtensions
{
/// <summary>
/// Get the actual ClassJob from the in-game sheets for this JobFlags.
/// </summary>
/// <param name="job">A JobFlags enum member.</param>
/// <param name="data">A DataManager to get the ClassJob from.</param>
/// <returns>A ClassJob if found or null if not.</returns>
public static ClassJob ClassJob(this JobFlags job, DataManager data)
{
var jobs = data.GetExcelSheet<ClassJob>();
uint? row = job switch
{
JobFlags.Gladiator => 1,
JobFlags.Pugilist => 2,
JobFlags.Marauder => 3,
JobFlags.Lancer => 4,
JobFlags.Archer => 5,
JobFlags.Conjurer => 6,
JobFlags.Thaumaturge => 7,
JobFlags.Paladin => 19,
JobFlags.Monk => 20,
JobFlags.Warrior => 21,
JobFlags.Dragoon => 22,
JobFlags.Bard => 23,
JobFlags.WhiteMage => 24,
JobFlags.BlackMage => 25,
JobFlags.Arcanist => 26,
JobFlags.Summoner => 27,
JobFlags.Scholar => 28,
JobFlags.Rogue => 29,
JobFlags.Ninja => 30,
JobFlags.Machinist => 31,
JobFlags.DarkKnight => 32,
JobFlags.Astrologian => 33,
JobFlags.Samurai => 34,
JobFlags.RedMage => 35,
JobFlags.BlueMage => 36,
JobFlags.Gunbreaker => 37,
JobFlags.Dancer => 38,
_ => null,
};
return row == null ? null : jobs.GetRow((uint)row);
}
}
}

View file

@ -1,54 +0,0 @@
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.ClientState.Structs.JobGauge;
using Dalamud.Hooking;
using Serilog;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Dalamud.Game.Internal.Gui {
public class TargetManager {
public delegate IntPtr GetTargetDelegate(IntPtr manager);
private Hook<GetTargetDelegate> getTargetHook;
private TargetManagerAddressResolver Address;
public unsafe TargetManager(Dalamud dalamud, SigScanner scanner) {
this.Address = new TargetManagerAddressResolver();
this.Address.Setup(scanner);
Log.Verbose("===== T A R G E T M A N A G E R =====");
Log.Verbose("GetTarget address {GetTarget}", Address.GetTarget);
this.getTargetHook = new Hook<GetTargetDelegate>(this.Address.GetTarget, new GetTargetDelegate(GetTargetDetour), this);
}
public void Enable() {
this.getTargetHook.Enable();
}
public void Dispose() {
this.getTargetHook.Dispose();
}
private IntPtr GetTargetDetour(IntPtr manager)
{
try {
var res = this.getTargetHook.Original(manager);
var test = Marshal.ReadInt32(res);
Log.Debug($"GetTargetDetour {manager.ToInt64():X} -> RET: {res:X}");
return res;
}
catch (Exception ex)
{
Log.Error(ex, "Exception GetTargetDetour hook.");
return this.getTargetHook.Original(manager);
}
}
}
}

View file

@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Game.Internal.Gui {
class TargetManagerAddressResolver : BaseAddressResolver {
public IntPtr GetTarget { get; private set; }
protected override void Setup64Bit(SigScanner sig) {
this.GetTarget = sig.ScanText("40 57 48 83 EC 40 48 8B F9 48 8B 49 08 48 8B 01 FF 50 40 66 83 B8 CA 81 00 00 00 74 33 48 8B 4F 08 48 8B 01 FF 50 40 66 83 B8 CA 81 00 00 04 74");
}
}
}

View file

@ -1,5 +1,8 @@
namespace Dalamud.Game.Internal.Gui.Toast namespace Dalamud.Game.Internal.Gui.Toast
{ {
/// <summary>
/// This class represents options that can be used with the <see cref="ToastGui"/> class for the quest toast variant.
/// </summary>
public sealed class QuestToastOptions public sealed class QuestToastOptions
{ {
/// <summary> /// <summary>
@ -25,12 +28,5 @@
/// This only works if <see cref="IconId"/> is non-zero or <see cref="DisplayCheckmark"/> is true. /// This only works if <see cref="IconId"/> is non-zero or <see cref="DisplayCheckmark"/> is true.
/// </summary> /// </summary>
public bool PlaySound { get; set; } = false; public bool PlaySound { get; set; } = false;
internal (uint, uint) DetermineParameterOrder()
{
return this.DisplayCheckmark
? (ToastGui.QuestToastCheckmarkMagic, this.IconId)
: (this.IconId, 0);
}
} }
} }

View file

@ -1,9 +1,23 @@
namespace Dalamud.Game.Internal.Gui.Toast namespace Dalamud.Game.Internal.Gui.Toast
{ {
/// <summary>
/// The alignment of native quest toast windows.
/// </summary>
public enum QuestToastPosition public enum QuestToastPosition
{ {
/// <summary>
/// The toast will be aligned screen centre.
/// </summary>
Centre = 0, Centre = 0,
/// <summary>
/// The toast will be aligned screen right.
/// </summary>
Right = 1, Right = 1,
/// <summary>
/// The toast will be aligned screen left.
/// </summary>
Left = 2, Left = 2,
} }
} }

View file

@ -1,5 +1,8 @@
namespace Dalamud.Game.Internal.Gui.Toast namespace Dalamud.Game.Internal.Gui.Toast
{ {
/// <summary>
/// This class represents options that can be used with the <see cref="ToastGui"/> class.
/// </summary>
public sealed class ToastOptions public sealed class ToastOptions
{ {
/// <summary> /// <summary>

View file

@ -1,8 +1,18 @@
namespace Dalamud.Game.Internal.Gui.Toast namespace Dalamud.Game.Internal.Gui.Toast
{ {
/// <summary>
/// The positioning of native toast windows.
/// </summary>
public enum ToastPosition : byte public enum ToastPosition : byte
{ {
/// <summary>
/// The toast will be towards the bottom.
/// </summary>
Bottom = 0, Bottom = 0,
/// <summary>
/// The toast will be towards the top.
/// </summary>
Top = 1, Top = 1,
} }
} }

View file

@ -1,5 +1,8 @@
namespace Dalamud.Game.Internal.Gui.Toast namespace Dalamud.Game.Internal.Gui.Toast
{ {
/// <summary>
/// The speed at which native toast windows will persist.
/// </summary>
public enum ToastSpeed : byte public enum ToastSpeed : byte
{ {
/// <summary> /// <summary>

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@ -8,18 +8,80 @@ using Dalamud.Hooking;
namespace Dalamud.Game.Internal.Gui namespace Dalamud.Game.Internal.Gui
{ {
public sealed class ToastGui : IDisposable /// <summary>
/// This class facilitates interacting with and creating native toast windows.
/// </summary>
public sealed partial class ToastGui : IDisposable
{ {
internal const uint QuestToastCheckmarkMagic = 60081; private const uint QuestToastCheckmarkMagic = 60081;
#region Events private readonly Dalamud dalamud;
private readonly ToastGuiAddressResolver address;
private readonly Queue<(byte[] Message, ToastOptions Options)> normalQueue = new();
private readonly Queue<(byte[] Message, QuestToastOptions Options)> questQueue = new();
private readonly Queue<byte[]> errorQueue = new();
private readonly Hook<ShowNormalToastDelegate> showNormalToastHook;
private readonly Hook<ShowQuestToastDelegate> showQuestToastHook;
private readonly Hook<ShowErrorToastDelegate> showErrorToastHook;
/// <summary>
/// Initializes a new instance of the <see cref="ToastGui"/> class.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
/// <param name="dalamud">The Dalamud instance.</param>
public ToastGui(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.address = new ToastGuiAddressResolver();
this.address.Setup(scanner);
this.showNormalToastHook = new Hook<ShowNormalToastDelegate>(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour));
this.showQuestToastHook = new Hook<ShowQuestToastDelegate>(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour));
this.showErrorToastHook = new Hook<ShowErrorToastDelegate>(this.address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour));
}
#region Event delegates
/// <summary>
/// A delegate type used when a normal toast window appears.
/// </summary>
/// <param name="message">The message displayed.</param>
/// <param name="options">Assorted toast options.</param>
/// <param name="isHandled">Whether the toast has been handled or should be propagated.</param>
public delegate void OnNormalToastDelegate(ref SeString message, ref ToastOptions options, ref bool isHandled); public delegate void OnNormalToastDelegate(ref SeString message, ref ToastOptions options, ref bool isHandled);
/// <summary>
/// A delegate type used when a quest toast window appears.
/// </summary>
/// <param name="message">The message displayed.</param>
/// <param name="options">Assorted toast options.</param>
/// <param name="isHandled">Whether the toast has been handled or should be propagated.</param>
public delegate void OnQuestToastDelegate(ref SeString message, ref QuestToastOptions options, ref bool isHandled); public delegate void OnQuestToastDelegate(ref SeString message, ref QuestToastOptions options, ref bool isHandled);
/// <summary>
/// A delegate type used when an error toast window appears.
/// </summary>
/// <param name="message">The message displayed.</param>
/// <param name="isHandled">Whether the toast has been handled or should be propagated.</param>
public delegate void OnErrorToastDelegate(ref SeString message, ref bool isHandled); public delegate void OnErrorToastDelegate(ref SeString message, ref bool isHandled);
#endregion
#region Marshal delegates
private delegate IntPtr ShowNormalToastDelegate(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId);
private delegate byte ShowQuestToastDelegate(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound);
private delegate byte ShowErrorToastDelegate(IntPtr manager, IntPtr text, byte respectsHidingMaybe);
#endregion
#region Events
/// <summary> /// <summary>
/// Event that will be fired when a toast is sent by the game or a plugin. /// Event that will be fired when a toast is sent by the game or a plugin.
/// </summary> /// </summary>
@ -37,48 +99,9 @@ namespace Dalamud.Game.Internal.Gui
#endregion #endregion
#region Hooks /// <summary>
/// Enables this module.
private readonly Hook<ShowNormalToastDelegate> showNormalToastHook; /// </summary>
private readonly Hook<ShowQuestToastDelegate> showQuestToastHook;
private readonly Hook<ShowErrorToastDelegate> showErrorToastHook;
#endregion
#region Delegates
private delegate IntPtr ShowNormalToastDelegate(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId);
private delegate byte ShowQuestToastDelegate(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound);
private delegate byte ShowErrorToastDelegate(IntPtr manager, IntPtr text, byte respectsHidingMaybe);
#endregion
private Dalamud Dalamud { get; }
private ToastGuiAddressResolver Address { get; }
private Queue<(byte[], ToastOptions)> NormalQueue { get; } = new Queue<(byte[], ToastOptions)>();
private Queue<(byte[], QuestToastOptions)> QuestQueue { get; } = new Queue<(byte[], QuestToastOptions)>();
private Queue<byte[]> ErrorQueue { get; } = new Queue<byte[]>();
public ToastGui(SigScanner scanner, Dalamud dalamud)
{
this.Dalamud = dalamud;
this.Address = new ToastGuiAddressResolver();
this.Address.Setup(scanner);
this.showNormalToastHook = new Hook<ShowNormalToastDelegate>(this.Address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour));
this.showQuestToastHook = new Hook<ShowQuestToastDelegate>(this.Address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour));
this.showErrorToastHook = new Hook<ShowErrorToastDelegate>(this.Address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour));
}
public void Enable() public void Enable()
{ {
this.showNormalToastHook.Enable(); this.showNormalToastHook.Enable();
@ -86,6 +109,9 @@ namespace Dalamud.Game.Internal.Gui
this.showErrorToastHook.Enable(); this.showErrorToastHook.Enable();
} }
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose() public void Dispose()
{ {
this.showNormalToastHook.Dispose(); this.showNormalToastHook.Dispose();
@ -93,6 +119,30 @@ namespace Dalamud.Game.Internal.Gui
this.showErrorToastHook.Dispose(); this.showErrorToastHook.Dispose();
} }
/// <summary>
/// Process the toast queue.
/// </summary>
internal void UpdateQueue()
{
while (this.normalQueue.Count > 0)
{
var (message, options) = this.normalQueue.Dequeue();
this.ShowNormal(message, options);
}
while (this.questQueue.Count > 0)
{
var (message, options) = this.questQueue.Dequeue();
this.ShowQuest(message, options);
}
while (this.errorQueue.Count > 0)
{
var message = this.errorQueue.Dequeue();
this.ShowError(message);
}
}
private static byte[] Terminate(byte[] source) private static byte[] Terminate(byte[] source)
{ {
var terminated = new byte[source.Length + 1]; var terminated = new byte[source.Length + 1];
@ -116,62 +166,42 @@ namespace Dalamud.Game.Internal.Gui
} }
// call events // call events
return this.Dalamud.SeStringManager.Parse(bytes.ToArray()); return this.dalamud.SeStringManager.Parse(bytes.ToArray());
}
} }
/// <summary> /// <summary>
/// Process the toast queue. /// Handles normal toasts.
/// </summary> /// </summary>
internal void UpdateQueue() public sealed partial class ToastGui
{ {
while (this.NormalQueue.Count > 0)
{
var (message, options) = this.NormalQueue.Dequeue();
this.ShowNormal(message, options);
}
while (this.QuestQueue.Count > 0)
{
var (message, options) = this.QuestQueue.Dequeue();
this.ShowQuest(message, options);
}
while (this.ErrorQueue.Count > 0)
{
var message = this.ErrorQueue.Dequeue();
this.ShowError(message);
}
}
#region Normal API
/// <summary> /// <summary>
/// Show a toast message with the given content. /// Show a toast message with the given content.
/// </summary> /// </summary>
/// <param name="message">The message to be shown</param> /// <param name="message">The message to be shown.</param>
/// <param name="options">Options for the toast</param> /// <param name="options">Options for the toast.</param>
public void ShowNormal(string message, ToastOptions options = null) public void ShowNormal(string message, ToastOptions options = null)
{ {
options ??= new ToastOptions(); options ??= new ToastOptions();
this.NormalQueue.Enqueue((Encoding.UTF8.GetBytes(message), options)); this.normalQueue.Enqueue((Encoding.UTF8.GetBytes(message), options));
} }
/// <summary> /// <summary>
/// Show a toast message with the given content. /// Show a toast message with the given content.
/// </summary> /// </summary>
/// <param name="message">The message to be shown</param> /// <param name="message">The message to be shown.</param>
/// <param name="options">Options for the toast</param> /// <param name="options">Options for the toast.</param>
public void ShowNormal(SeString message, ToastOptions options = null) public void ShowNormal(SeString message, ToastOptions options = null)
{ {
options ??= new ToastOptions(); options ??= new ToastOptions();
this.NormalQueue.Enqueue((message.Encode(), options)); this.normalQueue.Enqueue((message.Encode(), options));
} }
private void ShowNormal(byte[] bytes, ToastOptions options = null) private void ShowNormal(byte[] bytes, ToastOptions options = null)
{ {
options ??= new ToastOptions(); options ??= new ToastOptions();
var manager = this.Dalamud.Framework.Gui.GetUIModule(); var manager = this.dalamud.Framework.Gui.GetUIModule();
// terminate the string // terminate the string
var terminated = Terminate(bytes); var terminated = Terminate(bytes);
@ -185,99 +215,6 @@ namespace Dalamud.Game.Internal.Gui
} }
} }
#endregion
#region Quest API
/// <summary>
/// Show a quest toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown</param>
/// <param name="options">Options for the toast</param>
public void ShowQuest(string message, QuestToastOptions options = null)
{
options ??= new QuestToastOptions();
this.QuestQueue.Enqueue((Encoding.UTF8.GetBytes(message), options));
}
/// <summary>
/// Show a quest toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown</param>
/// <param name="options">Options for the toast</param>
public void ShowQuest(SeString message, QuestToastOptions options = null)
{
options ??= new QuestToastOptions();
this.QuestQueue.Enqueue((message.Encode(), options));
}
private void ShowQuest(byte[] bytes, QuestToastOptions options = null)
{
options ??= new QuestToastOptions();
var manager = this.Dalamud.Framework.Gui.GetUIModule();
// terminate the string
var terminated = Terminate(bytes);
var (ioc1, ioc2) = options.DetermineParameterOrder();
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleQuestToastDetour(
manager,
(int) options.Position,
(IntPtr) ptr,
ioc1,
options.PlaySound ? (byte) 1 : (byte) 0,
ioc2,
0);
}
}
}
#endregion
#region Error API
/// <summary>
/// Show an error toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown</param>
public void ShowError(string message)
{
this.ErrorQueue.Enqueue(Encoding.UTF8.GetBytes(message));
}
/// <summary>
/// Show an error toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown</param>
public void ShowError(SeString message)
{
this.ErrorQueue.Enqueue(message.Encode());
}
private void ShowError(byte[] bytes)
{
var manager = this.Dalamud.Framework.Gui.GetUIModule();
// terminate the string
var terminated = Terminate(bytes);
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleErrorToastDetour(manager, (IntPtr) ptr, 0);
}
}
}
#endregion
private IntPtr HandleNormalToastDetour(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId) private IntPtr HandleNormalToastDetour(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId)
{ {
if (text == IntPtr.Zero) if (text == IntPtr.Zero)
@ -312,6 +249,61 @@ namespace Dalamud.Game.Internal.Gui
} }
} }
} }
}
/// <summary>
/// Handles quest toasts.
/// </summary>
public sealed partial class ToastGui
{
/// <summary>
/// Show a quest toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
/// <param name="options">Options for the toast.</param>
public void ShowQuest(string message, QuestToastOptions options = null)
{
options ??= new QuestToastOptions();
this.questQueue.Enqueue((Encoding.UTF8.GetBytes(message), options));
}
/// <summary>
/// Show a quest toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
/// <param name="options">Options for the toast.</param>
public void ShowQuest(SeString message, QuestToastOptions options = null)
{
options ??= new QuestToastOptions();
this.questQueue.Enqueue((message.Encode(), options));
}
private void ShowQuest(byte[] bytes, QuestToastOptions options = null)
{
options ??= new QuestToastOptions();
var manager = this.dalamud.Framework.Gui.GetUIModule();
// terminate the string
var terminated = Terminate(bytes);
var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleQuestToastDetour(
manager,
(int)options.Position,
(IntPtr)ptr,
ioc1,
options.PlaySound ? (byte)1 : (byte)0,
ioc2,
0);
}
}
}
private byte HandleQuestToastDetour(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound) private byte HandleQuestToastDetour(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound)
{ {
@ -341,7 +333,7 @@ namespace Dalamud.Game.Internal.Gui
var terminated = Terminate(str.Encode()); var terminated = Terminate(str.Encode());
var (ioc1, ioc2) = options.DetermineParameterOrder(); var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe unsafe
{ {
@ -359,6 +351,53 @@ namespace Dalamud.Game.Internal.Gui
} }
} }
private (uint IconOrCheck1, uint IconOrCheck2) DetermineParameterOrder(QuestToastOptions options)
{
return options.DisplayCheckmark
? (QuestToastCheckmarkMagic, options.IconId)
: (options.IconId, 0);
}
}
/// <summary>
/// Handles error toasts.
/// </summary>
public sealed partial class ToastGui
{
/// <summary>
/// Show an error toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
public void ShowError(string message)
{
this.errorQueue.Enqueue(Encoding.UTF8.GetBytes(message));
}
/// <summary>
/// Show an error toast message with the given content.
/// </summary>
/// <param name="message">The message to be shown.</param>
public void ShowError(SeString message)
{
this.errorQueue.Enqueue(message.Encode());
}
private void ShowError(byte[] bytes)
{
var manager = this.dalamud.Framework.Gui.GetUIModule();
// terminate the string
var terminated = Terminate(bytes);
unsafe
{
fixed (byte* ptr = terminated)
{
this.HandleErrorToastDetour(manager, (IntPtr)ptr, 0);
}
}
}
private byte HandleErrorToastDetour(IntPtr manager, IntPtr text, byte respectsHidingMaybe) private byte HandleErrorToastDetour(IntPtr manager, IntPtr text, byte respectsHidingMaybe)
{ {
if (text == IntPtr.Zero) if (text == IntPtr.Zero)

View file

@ -1,15 +1,28 @@
using System; using System;
namespace Dalamud.Game.Internal.Gui namespace Dalamud.Game.Internal.Gui
{ {
/// <summary>
/// An address resolver for the <see cref="ToastGui"/> class.
/// </summary>
public class ToastGuiAddressResolver : BaseAddressResolver public class ToastGuiAddressResolver : BaseAddressResolver
{ {
/// <summary>
/// Gets the address of the native ShowNormalToast method.
/// </summary>
public IntPtr ShowNormalToast { get; private set; } public IntPtr ShowNormalToast { get; private set; }
/// <summary>
/// Gets the address of the native ShowQuestToast method.
/// </summary>
public IntPtr ShowQuestToast { get; private set; } public IntPtr ShowQuestToast { get; private set; }
/// <summary>
/// Gets the address of the ShowErrorToast method.
/// </summary>
public IntPtr ShowErrorToast { get; private set; } public IntPtr ShowErrorToast { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig) protected override void Setup64Bit(SigScanner sig)
{ {
this.ShowNormalToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 83 3D ?? ?? ?? ?? ??"); this.ShowNormalToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 83 3D ?? ?? ?? ?? ??");

View file

@ -1,10 +1,31 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Serilog;
namespace Dalamud.Game.Internal.Libc { namespace Dalamud.Game.Internal.Libc
public sealed class LibcFunction { {
/// <summary>
/// This class handles creating cstrings utilizing native game methods.
/// </summary>
public sealed class LibcFunction
{
private readonly LibcFunctionAddressResolver address;
private readonly StdStringFromCStringDelegate stdStringCtorCString;
private readonly StdStringDeallocateDelegate stdStringDeallocate;
/// <summary>
/// Initializes a new instance of the <see cref="LibcFunction"/> class.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
public LibcFunction(SigScanner scanner)
{
this.address = new LibcFunctionAddressResolver();
this.address.Setup(scanner);
this.stdStringCtorCString = Marshal.GetDelegateForFunctionPointer<StdStringFromCStringDelegate>(this.address.StdStringFromCstring);
this.stdStringDeallocate = Marshal.GetDelegateForFunctionPointer<StdStringDeallocateDelegate>(this.address.StdStringDeallocate);
}
// TODO: prolly callconv is not okay in x86 // TODO: prolly callconv is not okay in x86
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr StdStringFromCStringDelegate(IntPtr pStdString, [MarshalAs(UnmanagedType.LPArray)] byte[] content, IntPtr size); private delegate IntPtr StdStringFromCStringDelegate(IntPtr pStdString, [MarshalAs(UnmanagedType.LPArray)] byte[] content, IntPtr size);
@ -13,20 +34,13 @@ namespace Dalamud.Game.Internal.Libc {
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr StdStringDeallocateDelegate(IntPtr address); private delegate IntPtr StdStringDeallocateDelegate(IntPtr address);
private LibcFunctionAddressResolver Address { get; } /// <summary>
/// Create a new string from the given bytes.
private readonly StdStringFromCStringDelegate stdStringCtorCString; /// </summary>
private readonly StdStringDeallocateDelegate stdStringDeallocate; /// <param name="content">The bytes to convert.</param>
/// <returns>An owned std string object.</returns>
public LibcFunction(SigScanner scanner) { public OwnedStdString NewString(byte[] content)
Address = new LibcFunctionAddressResolver(); {
Address.Setup(scanner);
this.stdStringCtorCString = Marshal.GetDelegateForFunctionPointer<StdStringFromCStringDelegate>(Address.StdStringFromCstring);
this.stdStringDeallocate = Marshal.GetDelegateForFunctionPointer<StdStringDeallocateDelegate>(Address.StdStringDeallocate);
}
public OwnedStdString NewString(byte[] content) {
// While 0x70 bytes in the memory should be enough in DX11 version, // While 0x70 bytes in the memory should be enough in DX11 version,
// I don't trust my analysis so we're just going to allocate almost two times more than that. // I don't trust my analysis so we're just going to allocate almost two times more than that.
var pString = Marshal.AllocHGlobal(256); var pString = Marshal.AllocHGlobal(256);
@ -37,9 +51,15 @@ namespace Dalamud.Game.Internal.Libc {
// Log.Verbose("Prev: {Prev} Now: {Now}", pString, pReallocString); // Log.Verbose("Prev: {Prev} Now: {Now}", pString, pReallocString);
return new OwnedStdString(pReallocString, DeallocateStdString); return new OwnedStdString(pReallocString, this.DeallocateStdString);
} }
/// <summary>
/// Create a new string form the given bytes.
/// </summary>
/// <param name="content">The bytes to convert.</param>
/// <param name="encoding">A non-default encoding.</param>
/// <returns>An owned std string object.</returns>
public OwnedStdString NewString(string content, Encoding encoding = null) public OwnedStdString NewString(string content, Encoding encoding = null)
{ {
encoding ??= Encoding.UTF8; encoding ??= Encoding.UTF8;
@ -47,7 +67,8 @@ namespace Dalamud.Game.Internal.Libc {
return this.NewString(encoding.GetBytes(content)); return this.NewString(encoding.GetBytes(content));
} }
private void DeallocateStdString(IntPtr address) { private void DeallocateStdString(IntPtr address)
{
this.stdStringDeallocate(address); this.stdStringDeallocate(address);
} }
} }

View file

@ -1,16 +1,29 @@
using System; using System;
using System.Security.Policy;
namespace Dalamud.Game.Internal.Libc { namespace Dalamud.Game.Internal.Libc
public sealed class LibcFunctionAddressResolver : BaseAddressResolver { {
/// <summary>
/// The address resolver for the <see cref="LibcFunction"/> class.
/// </summary>
public sealed class LibcFunctionAddressResolver : BaseAddressResolver
{
private delegate IntPtr StringFromCString(); private delegate IntPtr StringFromCString();
/// <summary>
/// Gets the address of the native StdStringFromCstring method.
/// </summary>
public IntPtr StdStringFromCstring { get; private set; } public IntPtr StdStringFromCstring { get; private set; }
/// <summary>
/// Gets the address of the native StdStringDeallocate method.
/// </summary>
public IntPtr StdStringDeallocate { get; private set; } public IntPtr StdStringDeallocate { get; private set; }
protected override void Setup64Bit(SigScanner sig) { /// <inheritdoc/>
StdStringFromCstring = sig.ScanText("48895C2408 4889742410 57 4883EC20 488D4122 66C741200101 488901 498BD8"); protected override void Setup64Bit(SigScanner sig)
StdStringDeallocate = sig.ScanText("80792100 7512 488B5108 41B833000000 488B09 E9??????00 C3"); {
this.StdStringFromCstring = sig.ScanText("48895C2408 4889742410 57 4883EC20 488D4122 66C741200101 488901 498BD8");
this.StdStringDeallocate = sig.ScanText("80792100 7512 488B5108 41B833000000 488B09 E9??????00 C3");
} }
} }
} }

View file

@ -1,20 +1,18 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Serilog;
namespace Dalamud.Game.Internal.Libc {
public sealed class OwnedStdString : IDisposable {
internal delegate void DeallocatorDelegate(IntPtr address);
// ala. the drop flag
private bool isDisposed;
namespace Dalamud.Game.Internal.Libc
{
/// <summary>
/// An address wrapper around the <see cref="StdString"/> class.
/// </summary>
public sealed partial class OwnedStdString
{
private readonly DeallocatorDelegate dealloc; private readonly DeallocatorDelegate dealloc;
public IntPtr Address { get; private set; }
/// <summary> /// <summary>
/// Construct a wrapper around std::string /// Initializes a new instance of the <see cref="OwnedStdString"/> class.
/// Construct a wrapper around std::string.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Violating any of these might cause an undefined hehaviour. /// Violating any of these might cause an undefined hehaviour.
@ -22,47 +20,82 @@ namespace Dalamud.Game.Internal.Libc {
/// 2. A memory pointed by address argument is assumed to be allocated by Marshal.AllocHGlobal thus will try to call Marshal.FreeHGlobal on the address. /// 2. A memory pointed by address argument is assumed to be allocated by Marshal.AllocHGlobal thus will try to call Marshal.FreeHGlobal on the address.
/// 3. std::string object pointed by address must be initialized before calling this function. /// 3. std::string object pointed by address must be initialized before calling this function.
/// </remarks> /// </remarks>
/// <param name="address"></param> /// <param name="address">The address of the owned std string.</param>
/// <param name="dealloc">A deallocator function.</param> /// <param name="dealloc">A deallocator function.</param>
/// <returns></returns> internal OwnedStdString(IntPtr address, DeallocatorDelegate dealloc)
internal OwnedStdString(IntPtr address, DeallocatorDelegate dealloc) { {
Address = address; this.Address = address;
this.dealloc = dealloc; this.dealloc = dealloc;
} }
~OwnedStdString() { /// <summary>
ReleaseUnmanagedResources(); /// The delegate type that deallocates a std string.
/// </summary>
/// <param name="address">Address to deallocate.</param>
internal delegate void DeallocatorDelegate(IntPtr address);
/// <summary>
/// Gets the address of the std string.
/// </summary>
public IntPtr Address { get; private set; }
/// <summary>
/// Read the wrapped StdString.
/// </summary>
/// <returns>The StdString.</returns>
public StdString Read() => StdString.ReadFromPointer(this.Address);
} }
private void ReleaseUnmanagedResources() { /// <summary>
if (Address == IntPtr.Zero) { /// Implements IDisposable.
/// </summary>
public sealed partial class OwnedStdString : IDisposable
{
private bool isDisposed;
/// <summary>
/// Finalizes an instance of the <see cref="OwnedStdString"/> class.
/// </summary>
~OwnedStdString() => this.Dispose(false);
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
this.Dispose(true);
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
/// <param name="disposing">A value indicating whether this was called via Dispose or finalized.</param>
public void Dispose(bool disposing)
{
if (this.isDisposed)
return;
this.isDisposed = true;
if (disposing)
{
}
if (this.Address == IntPtr.Zero)
{
// Something got seriously fucked. // Something got seriously fucked.
throw new AccessViolationException(); throw new AccessViolationException();
} }
// Deallocate inner string first // Deallocate inner string first
this.dealloc(Address); this.dealloc(this.Address);
// Free the heap // Free the heap
Marshal.FreeHGlobal(Address); Marshal.FreeHGlobal(this.Address);
// Better safe (running on a nullptr) than sorry. (running on a dangling pointer) // Better safe (running on a nullptr) than sorry. (running on a dangling pointer)
Address = IntPtr.Zero; this.Address = IntPtr.Zero;
}
public void Dispose() {
// No double free plz, kthx.
if (this.isDisposed) {
return;
}
this.isDisposed = true;
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
public StdString Read() {
return StdString.ReadFromPointer(Address);
} }
} }
} }

View file

@ -1,31 +1,56 @@
using System; using System;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text; using System.Text;
using Newtonsoft.Json.Linq;
using Serilog;
namespace Dalamud.Game.Internal.Libc { namespace Dalamud.Game.Internal.Libc
{
/// <summary> /// <summary>
/// Interation with std::string /// Interation with std::string.
/// </summary> /// </summary>
public class StdString { public class StdString
public static StdString ReadFromPointer(IntPtr cstring) { {
unsafe { /// <summary>
if (cstring == IntPtr.Zero) { /// Initializes a new instance of the <see cref="StdString"/> class.
/// </summary>
private StdString()
{
}
/// <summary>
/// Gets the value of the cstring.
/// </summary>
public string Value { get; private set; }
/// <summary>
/// Gets or sets the raw byte representation of the cstring.
/// </summary>
public byte[] RawData { get; set; }
/// <summary>
/// Marshal a null terminated cstring from memory to a UTF-8 encoded string.
/// </summary>
/// <param name="cstring">Address of the cstring.</param>
/// <returns>A UTF-8 encoded string.</returns>
public static StdString ReadFromPointer(IntPtr cstring)
{
unsafe
{
if (cstring == IntPtr.Zero)
{
throw new ArgumentNullException(nameof(cstring)); throw new ArgumentNullException(nameof(cstring));
} }
var innerAddress = Marshal.ReadIntPtr(cstring); var innerAddress = Marshal.ReadIntPtr(cstring);
if (innerAddress == IntPtr.Zero) { if (innerAddress == IntPtr.Zero)
{
throw new NullReferenceException("Inner reference to the cstring is null."); throw new NullReferenceException("Inner reference to the cstring is null.");
} }
var count = 0;
// Count the number of chars. String is assumed to be zero-terminated. // Count the number of chars. String is assumed to be zero-terminated.
while (Marshal.ReadByte(innerAddress + count) != 0) {
var count = 0;
while (Marshal.ReadByte(innerAddress, count) != 0)
{
count += 1; count += 1;
} }
@ -33,17 +58,12 @@ namespace Dalamud.Game.Internal.Libc {
var rawData = new byte[count]; var rawData = new byte[count];
Marshal.Copy(innerAddress, rawData, 0, count); Marshal.Copy(innerAddress, rawData, 0, count);
return new StdString { return new StdString
{
RawData = rawData, RawData = rawData,
Value = Encoding.UTF8.GetString(rawData) Value = Encoding.UTF8.GetString(rawData),
}; };
} }
} }
private StdString() { }
public string Value { get; private set; }
public byte[] RawData { get; set; }
} }
} }

View file

@ -1,88 +1,128 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Hooking; using Dalamud.Hooking;
using Serilog; using Serilog;
using SharpDX.DXGI;
namespace Dalamud.Game.Internal.Network {
public sealed class GameNetwork : IDisposable {
#region Hooks
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ProcessZonePacketDownDelegate(IntPtr a, uint targetId, IntPtr dataPtr);
private readonly Hook<ProcessZonePacketDownDelegate> processZonePacketDownHook;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate byte ProcessZonePacketUpDelegate(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4);
private readonly Hook<ProcessZonePacketUpDelegate> processZonePacketUpHook;
#endregion
private GameNetworkAddressResolver Address { get; }
private IntPtr baseAddress;
public delegate void OnNetworkMessageDelegate(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction);
namespace Dalamud.Game.Internal.Network
{
/// <summary>
/// This class handles interacting with game network events.
/// </summary>
public sealed class GameNetwork : IDisposable
{
/// <summary> /// <summary>
/// Event that is called when a network message is sent/received. /// Event that is called when a network message is sent/received.
/// </summary> /// </summary>
public OnNetworkMessageDelegate OnNetworkMessage; public OnNetworkMessageDelegate OnNetworkMessage;
private readonly GameNetworkAddressResolver address;
private readonly Hook<ProcessZonePacketDownDelegate> processZonePacketDownHook;
private readonly Hook<ProcessZonePacketUpDelegate> processZonePacketUpHook;
private readonly Queue<byte[]> zoneInjectQueue = new();
private IntPtr baseAddress;
private readonly Queue<byte[]> zoneInjectQueue = new Queue<byte[]>(); /// <summary>
/// Initializes a new instance of the <see cref="GameNetwork"/> class.
public GameNetwork(SigScanner scanner) { /// </summary>
Address = new GameNetworkAddressResolver(); /// <param name="scanner">The SigScanner instance.</param>
Address.Setup(scanner); public GameNetwork(SigScanner scanner)
{
this.address = new GameNetworkAddressResolver();
this.address.Setup(scanner);
Log.Verbose("===== G A M E N E T W O R K ====="); Log.Verbose("===== G A M E N E T W O R K =====");
Log.Verbose("ProcessZonePacketDown address {ProcessZonePacketDown}", Address.ProcessZonePacketDown); Log.Verbose("ProcessZonePacketDown address {ProcessZonePacketDown}", this.address.ProcessZonePacketDown);
Log.Verbose("ProcessZonePacketUp address {ProcessZonePacketUp}", Address.ProcessZonePacketUp); Log.Verbose("ProcessZonePacketUp address {ProcessZonePacketUp}", this.address.ProcessZonePacketUp);
this.processZonePacketDownHook = this.processZonePacketDownHook = new Hook<ProcessZonePacketDownDelegate>(this.address.ProcessZonePacketDown, new ProcessZonePacketDownDelegate(this.ProcessZonePacketDownDetour), this);
new Hook<ProcessZonePacketDownDelegate>(Address.ProcessZonePacketDown,
new ProcessZonePacketDownDelegate(ProcessZonePacketDownDetour),
this);
this.processZonePacketUpHook = this.processZonePacketUpHook = new Hook<ProcessZonePacketUpDelegate>(this.address.ProcessZonePacketUp, new ProcessZonePacketUpDelegate(this.ProcessZonePacketUpDetour), this);
new Hook<ProcessZonePacketUpDelegate>(Address.ProcessZonePacketUp,
new ProcessZonePacketUpDelegate(ProcessZonePacketUpDetour),
this);
} }
public void Enable() { /// <summary>
/// The delegate type of a network message event.
/// </summary>
/// <param name="dataPtr">The pointer to the raw data.</param>
/// <param name="opCode">The operation ID code.</param>
/// <param name="sourceActorId">The source actor ID.</param>
/// <param name="targetActorId">The taret actor ID.</param>
/// <param name="direction">The direction of the packed.</param>
public delegate void OnNetworkMessageDelegate(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ProcessZonePacketDownDelegate(IntPtr a, uint targetId, IntPtr dataPtr);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate byte ProcessZonePacketUpDelegate(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4);
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
{
this.processZonePacketDownHook.Enable(); this.processZonePacketDownHook.Enable();
this.processZonePacketUpHook.Enable(); this.processZonePacketUpHook.Enable();
} }
public void Dispose() { /// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.processZonePacketDownHook.Dispose(); this.processZonePacketDownHook.Dispose();
this.processZonePacketUpHook.Dispose(); this.processZonePacketUpHook.Dispose();
} }
private void ProcessZonePacketDownDetour(IntPtr a, uint targetId, IntPtr dataPtr) { /// <summary>
/// Process a chat queue.
/// </summary>
/// <param name="framework">The Framework instance.</param>
public void UpdateQueue(Framework framework)
{
while (this.zoneInjectQueue.Count > 0)
{
var packetData = this.zoneInjectQueue.Dequeue();
var unmanagedPacketData = Marshal.AllocHGlobal(packetData.Length);
Marshal.Copy(packetData, 0, unmanagedPacketData, packetData.Length);
if (this.baseAddress != IntPtr.Zero)
{
this.processZonePacketDownHook.Original(this.baseAddress, 0, unmanagedPacketData);
}
Marshal.FreeHGlobal(unmanagedPacketData);
}
}
private void ProcessZonePacketDownDetour(IntPtr a, uint targetId, IntPtr dataPtr)
{
this.baseAddress = a; this.baseAddress = a;
// Go back 0x10 to get back to the start of the packet header // Go back 0x10 to get back to the start of the packet header
dataPtr -= 0x10; dataPtr -= 0x10;
try { try
{
// Call events // Call events
this.OnNetworkMessage?.Invoke(dataPtr + 0x20, (ushort)Marshal.ReadInt16(dataPtr, 0x12), 0, targetId, NetworkMessageDirection.ZoneDown); this.OnNetworkMessage?.Invoke(dataPtr + 0x20, (ushort)Marshal.ReadInt16(dataPtr, 0x12), 0, targetId, NetworkMessageDirection.ZoneDown);
this.processZonePacketDownHook.Original(a, targetId, dataPtr + 0x10); this.processZonePacketDownHook.Original(a, targetId, dataPtr + 0x10);
} catch (Exception ex) { }
catch (Exception ex)
{
string header; string header;
try { try
{
var data = new byte[32]; var data = new byte[32];
Marshal.Copy(dataPtr, data, 0, 32); Marshal.Copy(dataPtr, data, 0, 32);
header = BitConverter.ToString(data); header = BitConverter.ToString(data);
} catch (Exception) { }
catch (Exception)
{
header = "failed"; header = "failed";
} }
@ -92,8 +132,8 @@ namespace Dalamud.Game.Internal.Network {
} }
} }
private byte ProcessZonePacketUpDetour(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4) { private byte ProcessZonePacketUpDetour(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4)
{
try try
{ {
// Call events // Call events
@ -121,43 +161,26 @@ namespace Dalamud.Game.Internal.Network {
} }
#if DEBUG #if DEBUG
public void InjectZoneProtoPacket(byte[] data) { private void InjectZoneProtoPacket(byte[] data)
{
this.zoneInjectQueue.Enqueue(data); this.zoneInjectQueue.Enqueue(data);
} }
private void InjectActorControl(short cat, int param1) { private void InjectActorControl(short cat, int param1)
var packetData = new byte[] { {
var packetData = new byte[]
{
0x14, 0x00, 0x8D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x17, 0x7C, 0xC5, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x8D, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x17, 0x7C, 0xC5, 0x5D, 0x00, 0x00, 0x00, 0x00,
0x05, 0x00, 0x48, 0xB2, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x48, 0xB2, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x43, 0x7F, 0x00, 0x00 0x00, 0x00, 0x00, 0x00, 0x43, 0x7F, 0x00, 0x00,
}; };
BitConverter.GetBytes((short)cat).CopyTo(packetData, 0x10); BitConverter.GetBytes((short)cat).CopyTo(packetData, 0x10);
BitConverter.GetBytes((UInt32) param1).CopyTo(packetData, 0x14); BitConverter.GetBytes((uint)param1).CopyTo(packetData, 0x14);
InjectZoneProtoPacket(packetData); this.InjectZoneProtoPacket(packetData);
} }
#endif #endif
/// <summary>
/// Process a chat queue.
/// </summary>
public void UpdateQueue(Framework framework)
{
while (this.zoneInjectQueue.Count > 0)
{
var packetData = this.zoneInjectQueue.Dequeue();
var unmanagedPacketData = Marshal.AllocHGlobal(packetData.Length);
Marshal.Copy(packetData, 0, unmanagedPacketData, packetData.Length);
if (this.baseAddress != IntPtr.Zero) {
this.processZonePacketDownHook.Original(this.baseAddress, 0, unmanagedPacketData);
}
Marshal.FreeHGlobal(unmanagedPacketData);
}
}
} }
} }

View file

@ -1,16 +1,29 @@
using System; using System;
namespace Dalamud.Game.Internal.Network { namespace Dalamud.Game.Internal.Network
public sealed class GameNetworkAddressResolver : BaseAddressResolver { {
/// <summary>
/// The address resolver for the <see cref="GameNetwork"/> class.
/// </summary>
public sealed class GameNetworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the ProcessZonePacketDown method.
/// </summary>
public IntPtr ProcessZonePacketDown { get; private set; } public IntPtr ProcessZonePacketDown { get; private set; }
/// <summary>
/// Gets the address of the ProcessZonePacketUp method.
/// </summary>
public IntPtr ProcessZonePacketUp { get; private set; } public IntPtr ProcessZonePacketUp { get; private set; }
protected override void Setup64Bit(SigScanner sig) { /// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
// ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 7A FF 0F B7 57 02 8D 42 89 3D 5F 02 00 00 0F 87 60 01 00 00 4C 8D 05"); // ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 7A FF 0F B7 57 02 8D 42 89 3D 5F 02 00 00 0F 87 60 01 00 00 4C 8D 05");
// ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 73 FF 0F B7 57 02 8D 42 ?? 3D ?? ?? 00 00 0F 87 60 01 00 00 4C 8D 05"); // ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 73 FF 0F B7 57 02 8D 42 ?? 3D ?? ?? 00 00 0F 87 60 01 00 00 4C 8D 05");
ProcessZonePacketDown = sig.ScanText("48 89 5C 24 ?? 56 48 83 EC 50 8B F2"); this.ProcessZonePacketDown = sig.ScanText("48 89 5C 24 ?? 56 48 83 EC 50 8B F2");
ProcessZonePacketUp = this.ProcessZonePacketUp = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC 70 8B 81 ?? ?? ?? ??");
sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC 70 8B 81 ?? ?? ?? ??");
} }
} }
} }

View file

@ -1,6 +1,18 @@
namespace Dalamud.Game.Internal.Network { namespace Dalamud.Game.Internal.Network
public enum NetworkMessageDirection { {
/// <summary>
/// This represents the direction of a network message.
/// </summary>
public enum NetworkMessageDirection
{
/// <summary>
/// A zone down message.
/// </summary>
ZoneDown, ZoneDown,
ZoneUp
/// <summary>
/// A zone up message.
/// </summary>
ZoneUp,
} }
} }

View file

@ -1,135 +1,148 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Game.Internal.Libc;
using Dalamud.Hooking; using Dalamud.Hooking;
using Serilog; using Serilog;
namespace Dalamud.Game.Internal.File namespace Dalamud.Game.Internal.File
{ {
public class ResourceManager { /// <summary>
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] /// This class facilitates modifying how the game loads resources from disk.
private delegate IntPtr GetResourceAsyncDelegate(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr a5, IntPtr a6, byte a7); /// </summary>
public class ResourceManager
{
private readonly Dalamud dalamud;
private readonly ResourceManagerAddressResolver address;
private readonly Hook<GetResourceAsyncDelegate> getResourceAsyncHook; private readonly Hook<GetResourceAsyncDelegate> getResourceAsyncHook;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr GetResourceSyncDelegate(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr a5, IntPtr a6);
private readonly Hook<GetResourceSyncDelegate> getResourceSyncHook; private readonly Hook<GetResourceSyncDelegate> getResourceSyncHook;
private ResourceManagerAddressResolver Address { get; } private Dictionary<IntPtr, ResourceHandleHookInfo> resourceHookMap = new();
private readonly Dalamud dalamud;
class ResourceHandleHookInfo { /// <summary>
public string Path { get; set; } /// Initializes a new instance of the <see cref="ResourceManager"/> class.
public Stream DetourFile { get; set; } /// </summary>
} /// <param name="dalamud">The Dalamud instance.</param>
/// <param name="scanner">The SigScanner instance.</param>
private Dictionary<IntPtr, ResourceHandleHookInfo> resourceHookMap = new Dictionary<IntPtr, ResourceHandleHookInfo>(); public ResourceManager(Dalamud dalamud, SigScanner scanner)
{
public ResourceManager(Dalamud dalamud, SigScanner scanner) {
this.dalamud = dalamud; this.dalamud = dalamud;
Address = new ResourceManagerAddressResolver(); this.address = new ResourceManagerAddressResolver();
Address.Setup(scanner); this.address.Setup(scanner);
Log.Verbose("===== R E S O U R C E M A N A G E R ====="); Log.Verbose("===== R E S O U R C E M A N A G E R =====");
Log.Verbose("GetResourceAsync address {GetResourceAsync}", Address.GetResourceAsync); Log.Verbose("GetResourceAsync address {GetResourceAsync}", this.address.GetResourceAsync);
Log.Verbose("GetResourceSync address {GetResourceSync}", Address.GetResourceSync); Log.Verbose("GetResourceSync address {GetResourceSync}", this.address.GetResourceSync);
this.getResourceAsyncHook = this.getResourceAsyncHook = new Hook<GetResourceAsyncDelegate>(this.address.GetResourceAsync, new GetResourceAsyncDelegate(this.GetResourceAsyncDetour), this);
new Hook<GetResourceAsyncDelegate>(Address.GetResourceAsync,
new GetResourceAsyncDelegate(GetResourceAsyncDetour),
this);
this.getResourceSyncHook =
new Hook<GetResourceSyncDelegate>(Address.GetResourceSync,
new GetResourceSyncDelegate(GetResourceSyncDetour),
this);
this.getResourceSyncHook = new Hook<GetResourceSyncDelegate>(this.address.GetResourceSync, new GetResourceSyncDelegate(this.GetResourceSyncDetour), this);
} }
public void Enable() { [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr GetResourceAsyncDelegate(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr pathPtr, IntPtr a6, byte a7);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr GetResourceSyncDelegate(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr pathPtr, IntPtr a6);
/// <summary>
/// Check if a filepath has any invalid characters.
/// </summary>
/// <param name="path">The filepath to check.</param>
/// <returns>A value indicating whether the filepath is safe to use.</returns>
public static bool FilePathHasInvalidChars(string path)
{
return !string.IsNullOrEmpty(path) && path.IndexOfAny(Path.GetInvalidPathChars()) >= 0;
}
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
{
this.getResourceAsyncHook.Enable(); this.getResourceAsyncHook.Enable();
this.getResourceSyncHook.Enable(); this.getResourceSyncHook.Enable();
} }
public void Dispose() { /// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.getResourceAsyncHook.Dispose(); this.getResourceAsyncHook.Dispose();
this.getResourceSyncHook.Dispose(); this.getResourceSyncHook.Dispose();
} }
private IntPtr GetResourceAsyncDetour(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr a5, IntPtr a6, byte a7) { private IntPtr GetResourceAsyncDetour(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr pathPtr, IntPtr a6, byte a7)
{
try { try
var path = Marshal.PtrToStringAnsi(a5); {
var path = Marshal.PtrToStringAnsi(pathPtr);
var resourceHandle = this.getResourceAsyncHook.Original(manager, a2, a3, a4, IntPtr.Zero, a6, a7); var resourceHandle = this.getResourceAsyncHook.Original(manager, a2, a3, a4, IntPtr.Zero, a6, a7);
// var resourceHandle = IntPtr.Zero; // var resourceHandle = IntPtr.Zero;
Log.Verbose("GetResourceAsync CALL - this:{0} a2:{1} a3:{2} a4:{3} a5:{4} a6:{5} a7:{6} => RET:{7}", manager, a2, a3, a4, a5, a6, a7, resourceHandle); Log.Verbose("GetResourceAsync CALL - this:{0} a2:{1} a3:{2} a4:{3} a5:{4} a6:{5} a7:{6} => RET:{7}", manager, a2, a3, a4, pathPtr, a6, a7, resourceHandle);
Log.Verbose($"->{path}"); Log.Verbose($"->{path}");
HandleGetResourceHookAcquire(resourceHandle, path); this.HandleGetResourceHookAcquire(resourceHandle, path);
return resourceHandle; return resourceHandle;
} catch (Exception ex) { }
catch (Exception ex)
{
Log.Error(ex, "Exception on ReadResourceAsync hook."); Log.Error(ex, "Exception on ReadResourceAsync hook.");
return this.getResourceAsyncHook.Original(manager, a2, a3, a4, a5, a6, a7); return this.getResourceAsyncHook.Original(manager, a2, a3, a4, pathPtr, a6, a7);
} }
} }
private void DumpMem(IntPtr address, int len = 512) { private IntPtr GetResourceSyncDetour(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr pathPtr, IntPtr a6)
if (address == IntPtr.Zero) {
return; try
{
var resourceHandle = this.getResourceSyncHook.Original(manager, a2, a3, a4, pathPtr, a6);
var data = new byte[len]; Log.Verbose("GetResourceSync CALL - this:{0} a2:{1} a3:{2} a4:{3} a5:{4} a6:{5} => RET:{6}", manager, a2, a3, a4, pathPtr, a6, resourceHandle);
Marshal.Copy(address, data, 0, len);
Log.Verbose($"MEMDMP at {address.ToInt64():X} for {len:X}\n{Util.ByteArrayToHex(data)}"); var path = Marshal.PtrToStringAnsi(pathPtr);
}
private IntPtr GetResourceSyncDetour(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr a5, IntPtr a6) {
try {
var resourceHandle = this.getResourceSyncHook.Original(manager, a2, a3, a4, a5, a6);
Log.Verbose("GetResourceSync CALL - this:{0} a2:{1} a3:{2} a4:{3} a5:{4} a6:{5} => RET:{6}", manager, a2, a3, a4, a5, a6, resourceHandle);
var path = Marshal.PtrToStringAnsi(a5);
Log.Verbose($"->{path}"); Log.Verbose($"->{path}");
HandleGetResourceHookAcquire(resourceHandle, path); this.HandleGetResourceHookAcquire(resourceHandle, path);
return resourceHandle; return resourceHandle;
} catch (Exception ex) { }
catch (Exception ex)
{
Log.Error(ex, "Exception on ReadResourceSync hook."); Log.Error(ex, "Exception on ReadResourceSync hook.");
return this.getResourceSyncHook.Original(manager, a2, a3, a4, a5, a6); return this.getResourceSyncHook.Original(manager, a2, a3, a4, pathPtr, a6);
} }
} }
private void HandleGetResourceHookAcquire(IntPtr handlePtr, string path) { private void HandleGetResourceHookAcquire(IntPtr handlePtr, string path)
{
if (FilePathHasInvalidChars(path)) if (FilePathHasInvalidChars(path))
return; return;
if (this.resourceHookMap.ContainsKey(handlePtr)) { if (this.resourceHookMap.ContainsKey(handlePtr))
{
Log.Verbose($"-> Handle {handlePtr.ToInt64():X}({path}) was cached!"); Log.Verbose($"-> Handle {handlePtr.ToInt64():X}({path}) was cached!");
return; return;
} }
var hookInfo = new ResourceHandleHookInfo { var hookInfo = new ResourceHandleHookInfo
Path = path {
Path = path,
}; };
var hookPath = Path.Combine(this.dalamud.StartInfo.WorkingDirectory, "ResourceHook", path); var hookPath = Path.Combine(this.dalamud.StartInfo.WorkingDirectory, "ResourceHook", path);
if (System.IO.File.Exists(hookPath)) { if (System.IO.File.Exists(hookPath))
{
hookInfo.DetourFile = new FileStream(hookPath, FileMode.Open); hookInfo.DetourFile = new FileStream(hookPath, FileMode.Open);
Log.Verbose("-> Added resource hook detour at {0}", hookPath); Log.Verbose("-> Added resource hook detour at {0}", hookPath);
} }
@ -137,10 +150,11 @@ namespace Dalamud.Game.Internal.File
this.resourceHookMap.Add(handlePtr, hookInfo); this.resourceHookMap.Add(handlePtr, hookInfo);
} }
public static bool FilePathHasInvalidChars(string path) private class ResourceHandleHookInfo
{ {
public string Path { get; set; }
return (!string.IsNullOrEmpty(path) && path.IndexOfAny(System.IO.Path.GetInvalidPathChars()) >= 0); public Stream DetourFile { get; set; }
} }
} }
} }

View file

@ -1,19 +1,27 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Game.Internal.File namespace Dalamud.Game.Internal.File
{ {
class ResourceManagerAddressResolver : BaseAddressResolver /// <summary>
/// The address resolver for the <see cref="ResourceManager"/> class.
/// </summary>
internal class ResourceManagerAddressResolver : BaseAddressResolver
{ {
/// <summary>
/// Gets the address of the GetResourceAsync method.
/// </summary>
public IntPtr GetResourceAsync { get; private set; } public IntPtr GetResourceAsync { get; private set; }
/// <summary>
/// Gets the address of the GetResourceSync method.
/// </summary>
public IntPtr GetResourceSync { get; private set; } public IntPtr GetResourceSync { get; private set; }
protected override void Setup64Bit(SigScanner sig) { /// <inheritdoc/>
GetResourceAsync = sig.ScanText("48 89 5C 24 08 48 89 54 24 10 57 48 83 EC 20 B8 03 00 00 00 48 8B F9 86 82 A1 00 00 00 48 8B 5C 24 38 B8 01 00 00 00 87 83 90 00 00 00 85 C0 74"); protected override void Setup64Bit(SigScanner sig)
GetResourceSync = sig.ScanText("48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 41 54 41 55 41 56 41 57 48 83 EC 30 48 8B F9 49 8B E9 48 83 C1 30 4D 8B F0 4C 8B EA FF 15 CE F6"); {
this.GetResourceAsync = sig.ScanText("48 89 5C 24 08 48 89 54 24 10 57 48 83 EC 20 B8 03 00 00 00 48 8B F9 86 82 A1 00 00 00 48 8B 5C 24 38 B8 01 00 00 00 87 83 90 00 00 00 85 C0 74");
this.GetResourceSync = sig.ScanText("48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 41 54 41 55 41 56 41 57 48 83 EC 30 48 8B F9 49 8B E9 48 83 C1 30 4D 8B F0 4C 8B EA FF 15 CE F6");
// ReadResourceSync = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 7A FF 0F B7 57 02 8D 42 89 3D 5F 02 00 00 0F 87 60 01 00 00 4C 8D 05"); // ReadResourceSync = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 7A FF 0F B7 57 02 8D 42 89 3D 5F 02 00 00 0F 87 60 01 00 00 4C 8D 05");
} }
} }

View file

@ -1,16 +1,21 @@
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Game.Network.Structures; using Dalamud.Game.Network.Structures;
namespace Dalamud.Game.Network { namespace Dalamud.Game.Network
internal class MarketBoardItemRequest { {
internal class MarketBoardItemRequest
{
public uint CatalogId { get; set; } public uint CatalogId { get; set; }
public byte AmountToArrive { get; set; } public byte AmountToArrive { get; set; }
public List<MarketBoardCurrentOfferings.MarketBoardItemListing> Listings { get; set; } public List<MarketBoardCurrentOfferings.MarketBoardItemListing> Listings { get; set; }
public List<MarketBoardHistory.MarketBoardHistoryListing> History { get; set; } public List<MarketBoardHistory.MarketBoardHistoryListing> History { get; set; }
public int ListingsRequestId { get; set; } = -1; public int ListingsRequestId { get; set; } = -1;
public bool IsDone => Listings.Count == AmountToArrive && History.Count != 0; public bool IsDone => this.Listings.Count == this.AmountToArrive && this.History.Count != 0;
} }
} }

View file

@ -1,8 +1,22 @@
using Dalamud.Game.Network.Structures; using Dalamud.Game.Network.Structures;
namespace Dalamud.Game.Network.MarketBoardUploaders { namespace Dalamud.Game.Network.MarketBoardUploaders
internal interface IMarketBoardUploader { {
void Upload(MarketBoardItemRequest itemRequest); /// <summary>
/// An interface binding for the Universalis uploader.
/// </summary>
internal interface IMarketBoardUploader
{
/// <summary>
/// Upload data about an item.
/// </summary>
/// <param name="item">The item request data being uploaded.</param>
void Upload(MarketBoardItemRequest item);
/// <summary>
/// Upload tax rate data.
/// </summary>
/// <param name="taxRates">The tax rate data being uploaded.</param>
void UploadTax(MarketTaxRates taxRates); void UploadTax(MarketTaxRates taxRates);
} }
} }

View file

@ -1,28 +1,57 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis { namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis
internal class UniversalisHistoryEntry { {
/// <summary>
/// A Universalis API structure.
/// </summary>
internal class UniversalisHistoryEntry
{
/// <summary>
/// Gets or sets a value indicating whether the item is HQ or not.
/// </summary>
[JsonProperty("hq")] [JsonProperty("hq")]
public bool Hq { get; set; } public bool Hq { get; set; }
/// <summary>
/// Gets or sets the item price per unit.
/// </summary>
[JsonProperty("pricePerUnit")] [JsonProperty("pricePerUnit")]
public uint PricePerUnit { get; set; } public uint PricePerUnit { get; set; }
/// <summary>
/// Gets or sets the quantity of items available.
/// </summary>
[JsonProperty("quantity")] [JsonProperty("quantity")]
public uint Quantity { get; set; } public uint Quantity { get; set; }
/// <summary>
/// Gets or sets the name of the buyer.
/// </summary>
[JsonProperty("buyerName")] [JsonProperty("buyerName")]
public string BuyerName { get; set; } public string BuyerName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this item was on a mannequin.
/// </summary>
[JsonProperty("onMannequin")] [JsonProperty("onMannequin")]
public bool OnMannequin { get; set; } public bool OnMannequin { get; set; }
/// <summary>
/// Gets or sets the seller ID.
/// </summary>
[JsonProperty("sellerID")] [JsonProperty("sellerID")]
public string SellerId { get; set; } public string SellerId { get; set; }
/// <summary>
/// Gets or sets the buyer ID.
/// </summary>
[JsonProperty("buyerID")] [JsonProperty("buyerID")]
public string BuyerId { get; set; } public string BuyerId { get; set; }
/// <summary>
/// Gets or sets the timestamp of the transaction.
/// </summary>
[JsonProperty("timestamp")] [JsonProperty("timestamp")]
public long Timestamp { get; set; } public long Timestamp { get; set; }
} }

View file

@ -1,17 +1,35 @@
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis { namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis
internal class UniversalisHistoryUploadRequest { {
/// <summary>
/// A Universalis API structure.
/// </summary>
internal class UniversalisHistoryUploadRequest
{
/// <summary>
/// Gets or sets the world ID.
/// </summary>
[JsonProperty("worldID")] [JsonProperty("worldID")]
public uint WorldId { get; set; } public uint WorldId { get; set; }
/// <summary>
/// Gets or sets the item ID.
/// </summary>
[JsonProperty("itemID")] [JsonProperty("itemID")]
public uint ItemId { get; set; } public uint ItemId { get; set; }
/// <summary>
/// Gets or sets the list of available entries.
/// </summary>
[JsonProperty("entries")] [JsonProperty("entries")]
public List<UniversalisHistoryEntry> Entries { get; set; } public List<UniversalisHistoryEntry> Entries { get; set; }
/// <summary>
/// Gets or sets the uploader ID.
/// </summary>
[JsonProperty("uploaderID")] [JsonProperty("uploaderID")]
public string UploaderId { get; set; } public string UploaderId { get; set; }
} }

View file

@ -1,47 +1,95 @@
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis { namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis
internal class UniversalisItemListingsEntry { {
/// <summary>
/// A Universalis API structure.
/// </summary>
internal class UniversalisItemListingsEntry
{
/// <summary>
/// Gets or sets the listing ID.
/// </summary>
[JsonProperty("listingID")] [JsonProperty("listingID")]
public string ListingId { get; set; } public string ListingId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the item is HQ.
/// </summary>
[JsonProperty("hq")] [JsonProperty("hq")]
public bool Hq { get; set; } public bool Hq { get; set; }
/// <summary>
/// Gets or sets the item price per unit.
/// </summary>
[JsonProperty("pricePerUnit")] [JsonProperty("pricePerUnit")]
public uint PricePerUnit { get; set; } public uint PricePerUnit { get; set; }
/// <summary>
/// Gets or sets the item quantity.
/// </summary>
[JsonProperty("quantity")] [JsonProperty("quantity")]
public uint Quantity { get; set; } public uint Quantity { get; set; }
/// <summary>
/// Gets or sets the name of the retainer selling the item.
/// </summary>
[JsonProperty("retainerName")] [JsonProperty("retainerName")]
public string RetainerName { get; set; } public string RetainerName { get; set; }
/// <summary>
/// Gets or sets the ID of the retainer selling the item.
/// </summary>
[JsonProperty("retainerID")] [JsonProperty("retainerID")]
public string RetainerId { get; set; } public string RetainerId { get; set; }
/// <summary>
/// Gets or sets the name of the user who created the entry.
/// </summary>
[JsonProperty("creatorName")] [JsonProperty("creatorName")]
public string CreatorName { get; set; } public string CreatorName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the item is on a mannequin.
/// </summary>
[JsonProperty("onMannequin")] [JsonProperty("onMannequin")]
public bool OnMannequin { get; set; } public bool OnMannequin { get; set; }
/// <summary>
/// Gets or sets the seller ID.
/// </summary>
[JsonProperty("sellerID")] [JsonProperty("sellerID")]
public string SellerId { get; set; } public string SellerId { get; set; }
/// <summary>
/// Gets or sets the ID of the user who created the entry.
/// </summary>
[JsonProperty("creatorID")] [JsonProperty("creatorID")]
public string CreatorId { get; set; } public string CreatorId { get; set; }
/// <summary>
/// Gets or sets the ID of the dye on the item.
/// </summary>
[JsonProperty("stainID")] [JsonProperty("stainID")]
public int StainId { get; set; } public int StainId { get; set; }
/// <summary>
/// Gets or sets the city where the selling retainer resides.
/// </summary>
[JsonProperty("retainerCity")] [JsonProperty("retainerCity")]
public int RetainerCity { get; set; } public int RetainerCity { get; set; }
/// <summary>
/// Gets or sets the last time the entry was reviewed.
/// </summary>
[JsonProperty("lastReviewTime")] [JsonProperty("lastReviewTime")]
public long LastReviewTime { get; set; } public long LastReviewTime { get; set; }
/// <summary>
/// Gets or sets the materia attached to the item.
/// </summary>
[JsonProperty("materia")] [JsonProperty("materia")]
public List<UniversalisItemMateria> Materia { get; set; } public List<UniversalisItemMateria> Materia { get; set; }
} }

View file

@ -1,17 +1,35 @@
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis { namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis
internal class UniversalisItemListingsUploadRequest { {
/// <summary>
/// A Universalis API structure.
/// </summary>
internal class UniversalisItemListingsUploadRequest
{
/// <summary>
/// Gets or sets the world ID.
/// </summary>
[JsonProperty("worldID")] [JsonProperty("worldID")]
public uint WorldId { get; set; } public uint WorldId { get; set; }
/// <summary>
/// Gets or sets the item ID.
/// </summary>
[JsonProperty("itemID")] [JsonProperty("itemID")]
public uint ItemId { get; set; } public uint ItemId { get; set; }
/// <summary>
/// Gets or sets the list of available items.
/// </summary>
[JsonProperty("listings")] [JsonProperty("listings")]
public List<UniversalisItemListingsEntry> Listings { get; set; } public List<UniversalisItemListingsEntry> Listings { get; set; }
/// <summary>
/// Gets or sets the uploader ID.
/// </summary>
[JsonProperty("uploaderID")] [JsonProperty("uploaderID")]
public string UploaderId { get; set; } public string UploaderId { get; set; }
} }

View file

@ -1,10 +1,21 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis { namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis
internal class UniversalisItemMateria { {
/// <summary>
/// A Universalis API structure.
/// </summary>
internal class UniversalisItemMateria
{
/// <summary>
/// Gets or sets the item slot ID.
/// </summary>
[JsonProperty("slotID")] [JsonProperty("slotID")]
public int SlotId { get; set; } public int SlotId { get; set; }
/// <summary>
/// Gets or sets the materia ID.
/// </summary>
[JsonProperty("materiaID")] [JsonProperty("materiaID")]
public int MateriaId { get; set; } public int MateriaId { get; set; }
} }

View file

@ -1,14 +1,20 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using Dalamud.Game.Network.MarketBoardUploaders; using Dalamud.Game.Network.MarketBoardUploaders;
using Dalamud.Game.Network.MarketBoardUploaders.Universalis; using Dalamud.Game.Network.MarketBoardUploaders.Universalis;
using Dalamud.Game.Network.Structures; using Dalamud.Game.Network.Structures;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
namespace Dalamud.Game.Network.Universalis.MarketBoardUploaders { namespace Dalamud.Game.Network.Universalis.MarketBoardUploaders
internal class UniversalisMarketBoardUploader : IMarketBoardUploader { {
/// <summary>
/// This class represents an uploader for contributing data to Universalis.
/// </summary>
internal class UniversalisMarketBoardUploader : IMarketBoardUploader
{
private const string ApiBase = "https://universalis.app"; private const string ApiBase = "https://universalis.app";
// private const string ApiBase = "https://127.0.0.1:443"; // private const string ApiBase = "https://127.0.0.1:443";
@ -16,12 +22,20 @@ namespace Dalamud.Game.Network.Universalis.MarketBoardUploaders {
private readonly Dalamud dalamud; private readonly Dalamud dalamud;
public UniversalisMarketBoardUploader(Dalamud dalamud) { /// <summary>
/// Initializes a new instance of the <see cref="UniversalisMarketBoardUploader"/> class.
/// </summary>
/// <param name="dalamud">The Dalamud instance.</param>
public UniversalisMarketBoardUploader(Dalamud dalamud)
{
this.dalamud = dalamud; this.dalamud = dalamud;
} }
public void Upload(MarketBoardItemRequest request) { /// <inheritdoc/>
using (var client = new WebClient()) { public void Upload(MarketBoardItemRequest request)
{
using var client = new WebClient();
client.Headers.Add(HttpRequestHeader.ContentType, "application/json"); client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
Log.Verbose("Starting Universalis upload."); Log.Verbose("Starting Universalis upload.");
@ -33,8 +47,10 @@ namespace Dalamud.Game.Network.Universalis.MarketBoardUploaders {
listingsRequestObject.ItemId = request.CatalogId; listingsRequestObject.ItemId = request.CatalogId;
listingsRequestObject.Listings = new List<UniversalisItemListingsEntry>(); listingsRequestObject.Listings = new List<UniversalisItemListingsEntry>();
foreach (var marketBoardItemListing in request.Listings) { foreach (var marketBoardItemListing in request.Listings)
var universalisListing = new UniversalisItemListingsEntry { {
var universalisListing = new UniversalisItemListingsEntry
{
Hq = marketBoardItemListing.IsHq, Hq = marketBoardItemListing.IsHq,
SellerId = marketBoardItemListing.RetainerOwnerId.ToString(), SellerId = marketBoardItemListing.RetainerOwnerId.ToString(),
RetainerName = marketBoardItemListing.RetainerName, RetainerName = marketBoardItemListing.RetainerName,
@ -45,15 +61,18 @@ namespace Dalamud.Game.Network.Universalis.MarketBoardUploaders {
LastReviewTime = ((DateTimeOffset)marketBoardItemListing.LastReviewTime).ToUnixTimeSeconds(), LastReviewTime = ((DateTimeOffset)marketBoardItemListing.LastReviewTime).ToUnixTimeSeconds(),
PricePerUnit = marketBoardItemListing.PricePerUnit, PricePerUnit = marketBoardItemListing.PricePerUnit,
Quantity = marketBoardItemListing.ItemQuantity, Quantity = marketBoardItemListing.ItemQuantity,
RetainerCity = marketBoardItemListing.RetainerCityId RetainerCity = marketBoardItemListing.RetainerCityId,
}; };
universalisListing.Materia = new List<UniversalisItemMateria>(); universalisListing.Materia = new List<UniversalisItemMateria>();
foreach (var itemMateria in marketBoardItemListing.Materia) foreach (var itemMateria in marketBoardItemListing.Materia)
universalisListing.Materia.Add(new UniversalisItemMateria { {
universalisListing.Materia.Add(new UniversalisItemMateria
{
MateriaId = itemMateria.MateriaId, MateriaId = itemMateria.MateriaId,
SlotId = itemMateria.Index SlotId = itemMateria.Index,
}); });
}
listingsRequestObject.Listings.Add(universalisListing); listingsRequestObject.Listings.Add(universalisListing);
} }
@ -69,14 +88,17 @@ namespace Dalamud.Game.Network.Universalis.MarketBoardUploaders {
historyRequestObject.Entries = new List<UniversalisHistoryEntry>(); historyRequestObject.Entries = new List<UniversalisHistoryEntry>();
foreach (var marketBoardHistoryListing in request.History) foreach (var marketBoardHistoryListing in request.History)
historyRequestObject.Entries.Add(new UniversalisHistoryEntry { {
historyRequestObject.Entries.Add(new UniversalisHistoryEntry
{
BuyerName = marketBoardHistoryListing.BuyerName, BuyerName = marketBoardHistoryListing.BuyerName,
Hq = marketBoardHistoryListing.IsHq, Hq = marketBoardHistoryListing.IsHq,
OnMannequin = marketBoardHistoryListing.OnMannequin, OnMannequin = marketBoardHistoryListing.OnMannequin,
PricePerUnit = marketBoardHistoryListing.SalePrice, PricePerUnit = marketBoardHistoryListing.SalePrice,
Quantity = marketBoardHistoryListing.Quantity, Quantity = marketBoardHistoryListing.Quantity,
Timestamp = ((DateTimeOffset) marketBoardHistoryListing.PurchaseTime).ToUnixTimeSeconds() Timestamp = ((DateTimeOffset)marketBoardHistoryListing.PurchaseTime).ToUnixTimeSeconds(),
}); });
}
client.Headers.Add(HttpRequestHeader.ContentType, "application/json"); client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
@ -86,22 +108,24 @@ namespace Dalamud.Game.Network.Universalis.MarketBoardUploaders {
Log.Verbose("Universalis data upload for item#{0} completed.", request.CatalogId); Log.Verbose("Universalis data upload for item#{0} completed.", request.CatalogId);
} }
}
public void UploadTax(MarketTaxRates taxRates) { /// <inheritdoc/>
using (var client = new WebClient()) public void UploadTax(MarketTaxRates taxRates)
{ {
using var client = new WebClient();
var taxRatesRequest = new UniversalisTaxUploadRequest(); var taxRatesRequest = new UniversalisTaxUploadRequest();
taxRatesRequest.WorldId = this.dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0; taxRatesRequest.WorldId = this.dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0;
taxRatesRequest.UploaderId = this.dalamud.ClientState.LocalContentId.ToString(); taxRatesRequest.UploaderId = this.dalamud.ClientState.LocalContentId.ToString();
taxRatesRequest.TaxData = new UniversalisTaxData { taxRatesRequest.TaxData = new UniversalisTaxData
{
LimsaLominsa = taxRates.LimsaLominsaTax, LimsaLominsa = taxRates.LimsaLominsaTax,
Gridania = taxRates.GridaniaTax, Gridania = taxRates.GridaniaTax,
Uldah = taxRates.UldahTax, Uldah = taxRates.UldahTax,
Ishgard = taxRates.IshgardTax, Ishgard = taxRates.IshgardTax,
Kugane = taxRates.KuganeTax, Kugane = taxRates.KuganeTax,
Crystarium = taxRates.CrystariumTax Crystarium = taxRates.CrystariumTax,
}; };
client.Headers.Add(HttpRequestHeader.ContentType, "application/json"); client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
@ -114,4 +138,3 @@ namespace Dalamud.Game.Network.Universalis.MarketBoardUploaders {
} }
} }
} }
}

View file

@ -0,0 +1,46 @@
using Newtonsoft.Json;
namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis
{
/// <summary>
/// A Universalis API structure.
/// </summary>
internal class UniversalisTaxData
{
/// <summary>
/// Gets or sets Limsa Lominsa's current tax rate.
/// </summary>
[JsonProperty("limsaLominsa")]
public uint LimsaLominsa { get; set; }
/// <summary>
/// Gets or sets Gridania's current tax rate.
/// </summary>
[JsonProperty("gridania")]
public uint Gridania { get; set; }
/// <summary>
/// Gets or sets Ul'dah's current tax rate.
/// </summary>
[JsonProperty("uldah")]
public uint Uldah { get; set; }
/// <summary>
/// Gets or sets Ishgard's current tax rate.
/// </summary>
[JsonProperty("ishgard")]
public uint Ishgard { get; set; }
/// <summary>
/// Gets or sets Kugane's current tax rate.
/// </summary>
[JsonProperty("kugane")]
public uint Kugane { get; set; }
/// <summary>
/// Gets or sets The Crystarium's current tax rate.
/// </summary>
[JsonProperty("crystarium")]
public uint Crystarium { get; set; }
}
}

View file

@ -1,41 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis namespace Dalamud.Game.Network.MarketBoardUploaders.Universalis
{ {
class UniversalisTaxUploadRequest /// <summary>
/// A Universalis API structure.
/// </summary>
internal class UniversalisTaxUploadRequest
{ {
/// <summary>
/// Gets or sets the uploader's ID.
/// </summary>
[JsonProperty("uploaderID")] [JsonProperty("uploaderID")]
public string UploaderId { get; set; } public string UploaderId { get; set; }
/// <summary>
/// Gets or sets the world to retrieve data from.
/// </summary>
[JsonProperty("worldID")] [JsonProperty("worldID")]
public uint WorldId { get; set; } public uint WorldId { get; set; }
/// <summary>
/// Gets or sets tax data for each city's market.
/// </summary>
[JsonProperty("marketTaxRates")] [JsonProperty("marketTaxRates")]
public UniversalisTaxData TaxData { get; set; } public UniversalisTaxData TaxData { get; set; }
} }
class UniversalisTaxData {
[JsonProperty("limsaLominsa")]
public uint LimsaLominsa { get; set; }
[JsonProperty("gridania")]
public uint Gridania { get; set; }
[JsonProperty("uldah")]
public uint Uldah { get; set; }
[JsonProperty("ishgard")]
public uint Ishgard { get; set; }
[JsonProperty("kugane")]
public uint Kugane { get; set; }
[JsonProperty("crystarium")]
public uint Crystarium { get; set; }
}
} }

View file

@ -4,47 +4,58 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Game.Internal.Network; using Dalamud.Game.Internal.Network;
using Dalamud.Game.Network.MarketBoardUploaders; using Dalamud.Game.Network.MarketBoardUploaders;
using Dalamud.Game.Network.Structures; using Dalamud.Game.Network.Structures;
using Dalamud.Game.Network.Universalis.MarketBoardUploaders; using Dalamud.Game.Network.Universalis.MarketBoardUploaders;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json.Linq;
using Serilog; using Serilog;
namespace Dalamud.Game.Network { namespace Dalamud.Game.Network
public class NetworkHandlers { {
/// <summary>
/// This class handles network notifications and uploading Marketboard data.
/// </summary>
public class NetworkHandlers
{
private readonly Dalamud dalamud; private readonly Dalamud dalamud;
private readonly List<MarketBoardItemRequest> marketBoardRequests = new List<MarketBoardItemRequest>(); private readonly List<MarketBoardItemRequest> marketBoardRequests = new();
private readonly bool optOutMbUploads; private readonly bool optOutMbUploads;
private readonly IMarketBoardUploader uploader; private readonly IMarketBoardUploader uploader;
/// <summary>
/// Initializes a new instance of the <see cref="NetworkHandlers"/> class.
/// </summary>
/// <param name="dalamud">The Dalamud instance.</param>
/// <param name="optOutMbUploads">Whether the client should opt out of marketboard uploads.</param>
public NetworkHandlers(Dalamud dalamud, bool optOutMbUploads)
{
this.dalamud = dalamud;
this.optOutMbUploads = optOutMbUploads;
this.uploader = new UniversalisMarketBoardUploader(dalamud);
dalamud.Framework.Network.OnNetworkMessage += this.OnNetworkMessage;
}
/// <summary> /// <summary>
/// Event which gets fired when a duty is ready. /// Event which gets fired when a duty is ready.
/// </summary> /// </summary>
public event EventHandler<ContentFinderCondition> CfPop; public event EventHandler<ContentFinderCondition> CfPop;
public NetworkHandlers(Dalamud dalamud, bool optOutMbUploads) { private void OnNetworkMessage(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction)
this.dalamud = dalamud; {
this.optOutMbUploads = optOutMbUploads;
this.uploader = new UniversalisMarketBoardUploader(dalamud);
dalamud.Framework.Network.OnNetworkMessage += OnNetworkMessage;
}
private void OnNetworkMessage(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) {
if (direction != NetworkMessageDirection.ZoneDown) if (direction != NetworkMessageDirection.ZoneDown)
return; return;
if (!this.dalamud.Data.IsDataReady) if (!this.dalamud.Data.IsDataReady)
return; return;
if (opCode == this.dalamud.Data.ServerOpCodes["CfNotifyPop"]) { if (opCode == this.dalamud.Data.ServerOpCodes["CfNotifyPop"])
{
var data = new byte[64]; var data = new byte[64];
Marshal.Copy(dataPtr, data, 0, 64); Marshal.Copy(dataPtr, data, 0, 64);
@ -63,98 +74,114 @@ namespace Dalamud.Game.Network {
} }
var cfcName = contentFinderCondition.Name.ToString(); var cfcName = contentFinderCondition.Name.ToString();
if (string.IsNullOrEmpty(contentFinderCondition.Name)) { if (string.IsNullOrEmpty(contentFinderCondition.Name))
{
cfcName = "Duty Roulette"; cfcName = "Duty Roulette";
contentFinderCondition.Image = 112324; contentFinderCondition.Image = 112324;
} }
if (this.dalamud.Configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated()) { if (this.dalamud.Configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated())
var flashInfo = new NativeFunctions.FLASHWINFO
{ {
cbSize = (uint)Marshal.SizeOf<NativeFunctions.FLASHWINFO>(), var flashInfo = new NativeFunctions.FlashWindowInfo
{
cbSize = (uint)Marshal.SizeOf<NativeFunctions.FlashWindowInfo>(),
uCount = uint.MaxValue, uCount = uint.MaxValue,
dwTimeout = 0, dwTimeout = 0,
dwFlags = NativeFunctions.FlashWindow.FLASHW_ALL | dwFlags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG,
NativeFunctions.FlashWindow.FLASHW_TIMERNOFG, hwnd = Process.GetCurrentProcess().MainWindowHandle,
hwnd = Process.GetCurrentProcess().MainWindowHandle
}; };
NativeFunctions.FlashWindowEx(ref flashInfo); NativeFunctions.FlashWindowEx(ref flashInfo);
} }
Task.Run(() => { Task.Run(() =>
{
if (this.dalamud.Configuration.DutyFinderChatMessage) if (this.dalamud.Configuration.DutyFinderChatMessage)
this.dalamud.Framework.Gui.Chat.Print("Duty pop: " + cfcName); this.dalamud.Framework.Gui.Chat.Print("Duty pop: " + cfcName);
CfPop?.Invoke(this, contentFinderCondition); this.CfPop?.Invoke(this, contentFinderCondition);
}); });
return; return;
} }
if (!this.optOutMbUploads) { if (!this.optOutMbUploads)
if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardItemRequestStart"]) { {
if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardItemRequestStart"])
{
var catalogId = (uint)Marshal.ReadInt32(dataPtr); var catalogId = (uint)Marshal.ReadInt32(dataPtr);
var amount = Marshal.ReadByte(dataPtr + 0xB); var amount = Marshal.ReadByte(dataPtr + 0xB);
this.marketBoardRequests.Add(new MarketBoardItemRequest { this.marketBoardRequests.Add(new MarketBoardItemRequest
{
CatalogId = catalogId, CatalogId = catalogId,
AmountToArrive = amount, AmountToArrive = amount,
Listings = new List<MarketBoardCurrentOfferings.MarketBoardItemListing>(), Listings = new List<MarketBoardCurrentOfferings.MarketBoardItemListing>(),
History = new List<MarketBoardHistory.MarketBoardHistoryListing>() History = new List<MarketBoardHistory.MarketBoardHistoryListing>(),
}); });
Log.Verbose($"NEW MB REQUEST START: item#{catalogId} amount#{amount}"); Log.Verbose($"NEW MB REQUEST START: item#{catalogId} amount#{amount}");
return; return;
} }
if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardOfferings"]) { if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardOfferings"])
{
var listing = MarketBoardCurrentOfferings.Read(dataPtr); var listing = MarketBoardCurrentOfferings.Read(dataPtr);
var request = var request = this.marketBoardRequests.LastOrDefault(r => r.CatalogId == listing.ItemListings[0].CatalogId && !r.IsDone);
this.marketBoardRequests.LastOrDefault(
r => r.CatalogId == listing.ItemListings[0].CatalogId && !r.IsDone);
if (request == null) { if (request == null)
Log.Error( {
$"Market Board data arrived without a corresponding request: item#{listing.ItemListings[0].CatalogId}"); Log.Error($"Market Board data arrived without a corresponding request: item#{listing.ItemListings[0].CatalogId}");
return; return;
} }
if (request.Listings.Count + listing.ItemListings.Count > request.AmountToArrive) { if (request.Listings.Count + listing.ItemListings.Count > request.AmountToArrive)
Log.Error( {
$"Too many Market Board listings received for request: {request.Listings.Count + listing.ItemListings.Count} > {request.AmountToArrive} item#{listing.ItemListings[0].CatalogId}"); Log.Error($"Too many Market Board listings received for request: {request.Listings.Count + listing.ItemListings.Count} > {request.AmountToArrive} item#{listing.ItemListings[0].CatalogId}");
return; return;
} }
if (request.ListingsRequestId != -1 && request.ListingsRequestId != listing.RequestId) { if (request.ListingsRequestId != -1 && request.ListingsRequestId != listing.RequestId)
Log.Error( {
$"Non-matching RequestIds for Market Board data request: {request.ListingsRequestId}, {listing.RequestId}"); Log.Error($"Non-matching RequestIds for Market Board data request: {request.ListingsRequestId}, {listing.RequestId}");
return; return;
} }
if (request.ListingsRequestId == -1 && request.Listings.Count > 0) { if (request.ListingsRequestId == -1 && request.Listings.Count > 0)
Log.Error( {
$"Market Board data request sequence break: {request.ListingsRequestId}, {request.Listings.Count}"); Log.Error($"Market Board data request sequence break: {request.ListingsRequestId}, {request.Listings.Count}");
return; return;
} }
if (request.ListingsRequestId == -1) { if (request.ListingsRequestId == -1)
{
request.ListingsRequestId = listing.RequestId; request.ListingsRequestId = listing.RequestId;
Log.Verbose($"First Market Board packet in sequence: {listing.RequestId}"); Log.Verbose($"First Market Board packet in sequence: {listing.RequestId}");
} }
request.Listings.AddRange(listing.ItemListings); request.Listings.AddRange(listing.ItemListings);
Log.Verbose("Added {0} ItemListings to request#{1}, now {2}/{3}, item#{4}", Log.Verbose(
listing.ItemListings.Count, request.ListingsRequestId, request.Listings.Count, "Added {0} ItemListings to request#{1}, now {2}/{3}, item#{4}",
request.AmountToArrive, request.CatalogId); listing.ItemListings.Count,
request.ListingsRequestId,
request.Listings.Count,
request.AmountToArrive,
request.CatalogId);
if (request.IsDone) { if (request.IsDone)
Log.Verbose("Market Board request finished, starting upload: request#{0} item#{1} amount#{2}", {
request.ListingsRequestId, request.CatalogId, request.AmountToArrive); Log.Verbose(
try { "Market Board request finished, starting upload: request#{0} item#{1} amount#{2}",
request.ListingsRequestId,
request.CatalogId,
request.AmountToArrive);
try
{
Task.Run(() => this.uploader.Upload(request)); Task.Run(() => this.uploader.Upload(request));
} catch (Exception ex) { }
catch (Exception ex)
{
Log.Error(ex, "Market Board data upload failed."); Log.Error(ex, "Market Board data upload failed.");
} }
} }
@ -162,18 +189,21 @@ namespace Dalamud.Game.Network {
return; return;
} }
if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardHistory"]) { if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardHistory"])
{
var listing = MarketBoardHistory.Read(dataPtr); var listing = MarketBoardHistory.Read(dataPtr);
var request = this.marketBoardRequests.LastOrDefault(r => r.CatalogId == listing.CatalogId); var request = this.marketBoardRequests.LastOrDefault(r => r.CatalogId == listing.CatalogId);
if (request == null) { if (request == null)
{
Log.Error( Log.Error(
$"Market Board data arrived without a corresponding request: item#{listing.CatalogId}"); $"Market Board data arrived without a corresponding request: item#{listing.CatalogId}");
return; return;
} }
if (request.ListingsRequestId != -1) { if (request.ListingsRequestId != -1)
{
Log.Error( Log.Error(
$"Market Board data history sequence break: {request.ListingsRequestId}, {request.Listings.Count}"); $"Market Board data history sequence break: {request.ListingsRequestId}, {request.Listings.Count}");
return; return;
@ -183,7 +213,8 @@ namespace Dalamud.Game.Network {
Log.Verbose("Added history for item#{0}", listing.CatalogId); Log.Verbose("Added history for item#{0}", listing.CatalogId);
if (request.AmountToArrive == 0) { if (request.AmountToArrive == 0)
{
Log.Verbose("Request had 0 amount, uploading now"); Log.Verbose("Request had 0 amount, uploading now");
try try
@ -197,17 +228,25 @@ namespace Dalamud.Game.Network {
} }
} }
if (opCode == this.dalamud.Data.ServerOpCodes["MarketTaxRates"]) { if (opCode == this.dalamud.Data.ServerOpCodes["MarketTaxRates"])
{
var category = (uint)Marshal.ReadInt32(dataPtr); var category = (uint)Marshal.ReadInt32(dataPtr);
// Result dialog packet does not contain market tax rates // Result dialog packet does not contain market tax rates
if (category != 720905) { if (category != 720905)
{
return; return;
} }
var taxes = MarketTaxRates.Read(dataPtr); var taxes = MarketTaxRates.Read(dataPtr);
Log.Verbose("MarketTaxRates: limsa#{0} grid#{1} uldah#{2} ish#{3} kugane#{4} cr#{5}", Log.Verbose(
taxes.LimsaLominsaTax, taxes.GridaniaTax, taxes.UldahTax, taxes.IshgardTax, taxes.KuganeTax, taxes.CrystariumTax); "MarketTaxRates: limsa#{0} grid#{1} uldah#{2} ish#{3} kugane#{4} cr#{5}",
taxes.LimsaLominsaTax,
taxes.GridaniaTax,
taxes.UldahTax,
taxes.IshgardTax,
taxes.KuganeTax,
taxes.CrystariumTax);
try try
{ {
Task.Run(() => this.uploader.UploadTax(taxes)); Task.Run(() => this.uploader.UploadTax(taxes));

View file

@ -17,10 +17,9 @@ namespace Dalamud.Game.Network.Structures
{ {
var output = new MarketBoardCurrentOfferings(); var output = new MarketBoardCurrentOfferings();
using (var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 1544)) using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 1544);
{ using var reader = new BinaryReader(stream);
using (var reader = new BinaryReader(stream))
{
output.ItemListings = new List<MarketBoardItemListing>(); output.ItemListings = new List<MarketBoardItemListing>();
for (var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
@ -77,8 +76,6 @@ namespace Dalamud.Game.Network.Structures
output.ListingIndexEnd = reader.ReadByte(); output.ListingIndexEnd = reader.ReadByte();
output.ListingIndexStart = reader.ReadByte(); output.ListingIndexStart = reader.ReadByte();
output.RequestId = reader.ReadUInt16(); output.RequestId = reader.ReadUInt16();
}
}
return output; return output;
} }

View file

@ -3,30 +3,36 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
namespace Dalamud.Game.Network.Structures { namespace Dalamud.Game.Network.Structures
public class MarketBoardHistory { {
public class MarketBoardHistory
{
public uint CatalogId; public uint CatalogId;
public uint CatalogId2; public uint CatalogId2;
public List<MarketBoardHistoryListing> HistoryListings; public List<MarketBoardHistoryListing> HistoryListings;
public static unsafe MarketBoardHistory Read(IntPtr dataPtr) { public static unsafe MarketBoardHistory Read(IntPtr dataPtr)
{
var output = new MarketBoardHistory(); var output = new MarketBoardHistory();
using (var stream = new UnmanagedMemoryStream((byte*) dataPtr.ToPointer(), 1544)) { using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 1544);
using (var reader = new BinaryReader(stream)) { using var reader = new BinaryReader(stream);
output.CatalogId = reader.ReadUInt32(); output.CatalogId = reader.ReadUInt32();
output.CatalogId2 = reader.ReadUInt32(); output.CatalogId2 = reader.ReadUInt32();
output.HistoryListings = new List<MarketBoardHistoryListing>(); output.HistoryListings = new List<MarketBoardHistoryListing>();
for (var i = 0; i < 10; i++) { for (var i = 0; i < 10; i++)
var listingEntry = new MarketBoardHistoryListing(); {
var listingEntry = new MarketBoardHistoryListing
listingEntry.SalePrice = reader.ReadUInt32(); {
listingEntry.PurchaseTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadUInt32()).UtcDateTime; SalePrice = reader.ReadUInt32(),
listingEntry.Quantity = reader.ReadUInt32(); PurchaseTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadUInt32()).UtcDateTime,
listingEntry.IsHq = reader.ReadBoolean(); Quantity = reader.ReadUInt32(),
IsHq = reader.ReadBoolean(),
};
reader.ReadBoolean(); reader.ReadBoolean();
@ -37,13 +43,12 @@ namespace Dalamud.Game.Network.Structures {
if (listingEntry.CatalogId != 0) if (listingEntry.CatalogId != 0)
output.HistoryListings.Add(listingEntry); output.HistoryListings.Add(listingEntry);
} }
}
}
return output; return output;
} }
public class MarketBoardHistoryListing { public class MarketBoardHistoryListing
{
public string BuyerName; public string BuyerName;
public uint CatalogId; public uint CatalogId;

View file

@ -1,41 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Text;
namespace Dalamud.Game.Network.Structures
{
public class MarketTaxRates
{
public uint LimsaLominsaTax;
public uint GridaniaTax;
public uint UldahTax;
public uint IshgardTax;
public uint KuganeTax;
public uint CrystariumTax;
public static unsafe MarketTaxRates Read(IntPtr dataPtr)
{
var output = new MarketTaxRates();
using (var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 1544))
{
using (var reader = new BinaryReader(stream))
{
stream.Position += 8;
output.LimsaLominsaTax = reader.ReadUInt32();
output.GridaniaTax = reader.ReadUInt32();
output.UldahTax = reader.ReadUInt32();
output.IshgardTax = reader.ReadUInt32();
output.KuganeTax = reader.ReadUInt32();
output.CrystariumTax = reader.ReadUInt32();
}
}
return output;
}
}
}

View file

@ -0,0 +1,34 @@
using System;
using System.IO;
namespace Dalamud.Game.Network.Structures
{
public class MarketTaxRates
{
public uint LimsaLominsaTax;
public uint GridaniaTax;
public uint UldahTax;
public uint IshgardTax;
public uint KuganeTax;
public uint CrystariumTax;
public static unsafe MarketTaxRates Read(IntPtr dataPtr)
{
var output = new MarketTaxRates();
using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 1544);
using var reader = new BinaryReader(stream);
stream.Position += 8;
output.LimsaLominsaTax = reader.ReadUInt32();
output.GridaniaTax = reader.ReadUInt32();
output.UldahTax = reader.ReadUInt32();
output.IshgardTax = reader.ReadUInt32();
output.KuganeTax = reader.ReadUInt32();
output.CrystariumTax = reader.ReadUInt32();
return output;
}
}
}

View file

@ -1,28 +1,38 @@
using Dalamud.Hooking;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks; using Dalamud.Hooking;
namespace Dalamud.Game namespace Dalamud.Game
{ {
/// <summary>
/// This class enables TCP optimizations in the game socket for better performance.
/// </summary>
internal sealed class WinSockHandlers : IDisposable internal sealed class WinSockHandlers : IDisposable
{ {
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
private delegate IntPtr SocketDelegate(int af, int type, int protocol);
private Hook<SocketDelegate> ws2SocketHook; private Hook<SocketDelegate> ws2SocketHook;
[DllImport("ws2_32.dll", CallingConvention = CallingConvention.Winapi)] /// <summary>
private static extern int setsockopt(IntPtr socket, SocketOptionLevel level, SocketOptionName optName, ref IntPtr optVal, int optLen); /// Initializes a new instance of the <see cref="WinSockHandlers"/> class.
/// </summary>
public WinSockHandlers() { public WinSockHandlers()
this.ws2SocketHook = Hook<SocketDelegate>.FromSymbol("ws2_32.dll", "socket", new SocketDelegate(OnSocket)); {
this.ws2SocketHook = Hook<SocketDelegate>.FromSymbol("ws2_32.dll", "socket", new SocketDelegate(this.OnSocket));
this.ws2SocketHook.Enable(); this.ws2SocketHook.Enable();
} }
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
private delegate IntPtr SocketDelegate(int af, int type, int protocol);
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.ws2SocketHook.Dispose();
}
private IntPtr OnSocket(int af, int type, int protocol) private IntPtr OnSocket(int af, int type, int protocol)
{ {
var socket = this.ws2SocketHook.Original(af, type, protocol); var socket = this.ws2SocketHook.Original(af, type, protocol);
@ -37,19 +47,15 @@ namespace Dalamud.Game
// https://linux.die.net/man/7/tcp // https://linux.die.net/man/7/tcp
// https://assets.extrahop.com/whitepapers/TCP-Optimization-Guide-by-ExtraHop.pdf // https://assets.extrahop.com/whitepapers/TCP-Optimization-Guide-by-ExtraHop.pdf
var value = new IntPtr(1); var value = new IntPtr(1);
setsockopt(socket, SocketOptionLevel.Tcp, SocketOptionName.NoDelay, ref value, 4); NativeFunctions.SetSockOpt(socket, SocketOptionLevel.Tcp, SocketOptionName.NoDelay, ref value, 4);
// Enable tcp_quickack option. This option is undocumented in MSDN but it is supported in Windows 7 and onwards. // Enable tcp_quickack option. This option is undocumented in MSDN but it is supported in Windows 7 and onwards.
value = new IntPtr(1); value = new IntPtr(1);
setsockopt(socket, SocketOptionLevel.Tcp, (SocketOptionName)12, ref value, 4); NativeFunctions.SetSockOpt(socket, SocketOptionLevel.Tcp, SocketOptionName.AddMembership, ref value, 4);
} }
} }
return socket; return socket;
} }
public void Dispose() {
ws2SocketHook.Dispose();
}
} }
} }

View file

@ -56,7 +56,7 @@ namespace Dalamud.Game
/// <summary> /// <summary>
/// Gets the base address of the .text section search area. /// Gets the base address of the .text section search area.
/// </summary> /// </summary>
public IntPtr TextSectionBase => new IntPtr(this.SearchBase.ToInt64() + this.TextSectionOffset); public IntPtr TextSectionBase => new(this.SearchBase.ToInt64() + this.TextSectionOffset);
/// <summary> /// <summary>
/// Gets the offset of the .text section from the base of the module. /// Gets the offset of the .text section from the base of the module.
@ -71,7 +71,7 @@ namespace Dalamud.Game
/// <summary> /// <summary>
/// Gets the base address of the .data section search area. /// Gets the base address of the .data section search area.
/// </summary> /// </summary>
public IntPtr DataSectionBase => new IntPtr(this.SearchBase.ToInt64() + this.DataSectionOffset); public IntPtr DataSectionBase => new(this.SearchBase.ToInt64() + this.DataSectionOffset);
/// <summary> /// <summary>
/// Gets the offset of the .data section from the base of the module. /// Gets the offset of the .data section from the base of the module.
@ -86,7 +86,7 @@ namespace Dalamud.Game
/// <summary> /// <summary>
/// Gets the base address of the .rdata section search area. /// Gets the base address of the .rdata section search area.
/// </summary> /// </summary>
public IntPtr RDataSectionBase => new IntPtr(this.SearchBase.ToInt64() + this.RDataSectionOffset); public IntPtr RDataSectionBase => new(this.SearchBase.ToInt64() + this.RDataSectionOffset);
/// <summary> /// <summary>
/// Gets the offset of the .rdata section from the base of the module. /// Gets the offset of the .rdata section from the base of the module.
@ -230,7 +230,7 @@ namespace Dalamud.Game
return IntPtr.Add(sigLocation, 5 + jumpOffset); return IntPtr.Add(sigLocation, 5 + jumpOffset);
} }
private static (byte[] needle, bool[] mask) ParseSignature(string signature) private static (byte[] Needle, bool[] Mask) ParseSignature(string signature)
{ {
signature = signature.Replace(" ", string.Empty); signature = signature.Replace(" ", string.Empty);
if (signature.Length % 2 != 0) if (signature.Length % 2 != 0)

View file

@ -9,12 +9,12 @@ namespace Dalamud.Game.Text.Sanitizer
/// </summary> /// </summary>
public class Sanitizer : ISanitizer public class Sanitizer : ISanitizer
{ {
private static readonly Dictionary<string, string> DESanitizationDict = new Dictionary<string, string> private static readonly Dictionary<string, string> DESanitizationDict = new()
{ {
{ "\u0020\u2020", string.Empty }, // dagger { "\u0020\u2020", string.Empty }, // dagger
}; };
private static readonly Dictionary<string, string> FRSanitizationDict = new Dictionary<string, string> private static readonly Dictionary<string, string> FRSanitizationDict = new()
{ {
{ "\u0153", "\u006F\u0065" }, // ligature oe { "\u0153", "\u006F\u0065" }, // ligature oe
}; };
@ -75,18 +75,13 @@ namespace Dalamud.Game.Text.Sanitizer
private static string SanitizeByLanguage(string unsanitizedString, ClientLanguage clientLanguage) private static string SanitizeByLanguage(string unsanitizedString, ClientLanguage clientLanguage)
{ {
var sanitizedString = FilterUnprintableCharacters(unsanitizedString); var sanitizedString = FilterUnprintableCharacters(unsanitizedString);
switch (clientLanguage) return clientLanguage switch
{ {
case ClientLanguage.Japanese: ClientLanguage.Japanese or ClientLanguage.English => sanitizedString,
case ClientLanguage.English: ClientLanguage.German => FilterByDict(sanitizedString, DESanitizationDict),
return sanitizedString; ClientLanguage.French => FilterByDict(sanitizedString, FRSanitizationDict),
case ClientLanguage.German: _ => throw new ArgumentOutOfRangeException(nameof(clientLanguage), clientLanguage, null),
return FilterByDict(sanitizedString, DESanitizationDict); };
case ClientLanguage.French:
return FilterByDict(sanitizedString, FRSanitizationDict);
default:
throw new ArgumentOutOfRangeException(nameof(clientLanguage), clientLanguage, null);
}
} }
private static IEnumerable<string> SanitizeByLanguage( private static IEnumerable<string> SanitizeByLanguage(

View file

@ -1,182 +1,743 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
#pragma warning disable 1591
namespace Dalamud.Game.Text namespace Dalamud.Game.Text
{ {
/// <summary> /// <summary>
/// Special unicode characters with game-related symbols that work both in-game and in any dalamud window. /// Special unicode characters with game-related symbols that work both in-game and in any dalamud window.
/// </summary> /// </summary>
public enum SeIconChar { public enum SeIconChar
{
/// <summary>
/// The sprout icon unicode character.
/// </summary>
BotanistSprout = 0xE034, BotanistSprout = 0xE034,
/// <summary>
/// The item level icon unicode character.
/// </summary>
ItemLevel = 0xE033, ItemLevel = 0xE033,
/// <summary>
/// The auto translate open icon unicode character.
/// </summary>
AutoTranslateOpen = 0xE040, AutoTranslateOpen = 0xE040,
/// <summary>
/// The auto translate close icon unicode character.
/// </summary>
AutoTranslateClose = 0xE041, AutoTranslateClose = 0xE041,
/// <summary>
/// The high quality icon unicode character.
/// </summary>
HighQuality = 0xE03C, HighQuality = 0xE03C,
/// <summary>
/// The clock icon unicode character.
/// </summary>
Clock = 0xE031, Clock = 0xE031,
/// <summary>
/// The gil icon unicode character.
/// </summary>
Gil = 0xE049, Gil = 0xE049,
/// <summary>
/// The Hydaelyn icon unicode character.
/// </summary>
Hyadelyn = 0xE048, Hyadelyn = 0xE048,
/// <summary>
/// The no mouse click icon unicode character.
/// </summary>
MouseNoClick = 0xE050, MouseNoClick = 0xE050,
/// <summary>
/// The left mouse click icon unicode character.
/// </summary>
MouseLeftClick = 0xE051, MouseLeftClick = 0xE051,
/// <summary>
/// The right mouse click icon unicode character.
/// </summary>
MouseRightClick = 0xE052, MouseRightClick = 0xE052,
/// <summary>
/// The left/right mouse click icon unicode character.
/// </summary>
MouseBothClick = 0xE053, MouseBothClick = 0xE053,
/// <summary>
/// The mouse wheel icon unicode character.
/// </summary>
MouseWheel = 0xE054, MouseWheel = 0xE054,
/// <summary>
/// The mouse with a 1 icon unicode character.
/// </summary>
Mouse1 = 0xE055, Mouse1 = 0xE055,
/// <summary>
/// The mouse with a 2 icon unicode character.
/// </summary>
Mouse2 = 0xE056, Mouse2 = 0xE056,
/// <summary>
/// The mouse with a 3 icon unicode character.
/// </summary>
Mouse3 = 0xE057, Mouse3 = 0xE057,
/// <summary>
/// The mouse with a 4 icon unicode character.
/// </summary>
Mouse4 = 0xE058, Mouse4 = 0xE058,
/// <summary>
/// The mouse with a 5 icon unicode character.
/// </summary>
Mouse5 = 0xE059, Mouse5 = 0xE059,
/// <summary>
/// The level English icon unicode character.
/// </summary>
LevelEn = 0xE06A, LevelEn = 0xE06A,
/// <summary>
/// The level German icon unicode character.
/// </summary>
LevelDe = 0xE06B, LevelDe = 0xE06B,
/// <summary>
/// The level French icon unicode character.
/// </summary>
LevelFr = 0xE06C, LevelFr = 0xE06C,
/// <summary>
/// The experience icon unicode character.
/// </summary>
Experience = 0xE0BC, Experience = 0xE0BC,
/// <summary>
/// The experience filled icon unicode character.
/// </summary>
ExperienceFilled = 0xE0BD, ExperienceFilled = 0xE0BD,
/// <summary>
/// The A.M. time icon unicode character.
/// </summary>
TimeAm = 0xE06D, TimeAm = 0xE06D,
/// <summary>
/// The P.M. time icon unicode character.
/// </summary>
TimePm = 0xE06E, TimePm = 0xE06E,
/// <summary>
/// The right arrow icon unicode character.
/// </summary>
ArrowRight = 0xE06F, ArrowRight = 0xE06F,
/// <summary>
/// The down arrow icon unicode character.
/// </summary>
ArrowDown = 0xE035, ArrowDown = 0xE035,
/// <summary>
/// The number 0 icon unicode character.
/// </summary>
Number0 = 0xE060, Number0 = 0xE060,
/// <summary>
/// The number 1 icon unicode character.
/// </summary>
Number1 = 0xE061, Number1 = 0xE061,
/// <summary>
/// The number 2 icon unicode character.
/// </summary>
Number2 = 0xE062, Number2 = 0xE062,
/// <summary>
/// The number 3 icon unicode character.
/// </summary>
Number3 = 0xE063, Number3 = 0xE063,
/// <summary>
/// The number 4 icon unicode character.
/// </summary>
Number4 = 0xE064, Number4 = 0xE064,
/// <summary>
/// The number 5 icon unicode character.
/// </summary>
Number5 = 0xE065, Number5 = 0xE065,
/// <summary>
/// The number 6 icon unicode character.
/// </summary>
Number6 = 0xE066, Number6 = 0xE066,
/// <summary>
/// The number 7 icon unicode character.
/// </summary>
Number7 = 0xE067, Number7 = 0xE067,
/// <summary>
/// The number 8 icon unicode character.
/// </summary>
Number8 = 0xE068, Number8 = 0xE068,
/// <summary>
/// The number 9 icon unicode character.
/// </summary>
Number9 = 0xE069, Number9 = 0xE069,
/// <summary>
/// The boxed number 0 icon unicode character.
/// </summary>
BoxedNumber0 = 0xE08F, BoxedNumber0 = 0xE08F,
/// <summary>
/// The boxed number 1 icon unicode character.
/// </summary>
BoxedNumber1 = 0xE090, BoxedNumber1 = 0xE090,
/// <summary>
/// The boxed number 2 icon unicode character.
/// </summary>
BoxedNumber2 = 0xE091, BoxedNumber2 = 0xE091,
/// <summary>
/// The boxed number 3 icon unicode character.
/// </summary>
BoxedNumber3 = 0xE092, BoxedNumber3 = 0xE092,
/// <summary>
/// The boxed number 4 icon unicode character.
/// </summary>
BoxedNumber4 = 0xE093, BoxedNumber4 = 0xE093,
/// <summary>
/// The boxed number 5 icon unicode character.
/// </summary>
BoxedNumber5 = 0xE094, BoxedNumber5 = 0xE094,
/// <summary>
/// The boxed number 6 icon unicode character.
/// </summary>
BoxedNumber6 = 0xE095, BoxedNumber6 = 0xE095,
/// <summary>
/// The boxed number 7 icon unicode character.
/// </summary>
BoxedNumber7 = 0xE096, BoxedNumber7 = 0xE096,
/// <summary>
/// The boxed number 8 icon unicode character.
/// </summary>
BoxedNumber8 = 0xE097, BoxedNumber8 = 0xE097,
/// <summary>
/// The boxed number 9 icon unicode character.
/// </summary>
BoxedNumber9 = 0xE098, BoxedNumber9 = 0xE098,
/// <summary>
/// The boxed number 10 icon unicode character.
/// </summary>
BoxedNumber10 = 0xE099, BoxedNumber10 = 0xE099,
/// <summary>
/// The boxed number 11 icon unicode character.
/// </summary>
BoxedNumber11 = 0xE09A, BoxedNumber11 = 0xE09A,
/// <summary>
/// The boxed number 12 icon unicode character.
/// </summary>
BoxedNumber12 = 0xE09B, BoxedNumber12 = 0xE09B,
/// <summary>
/// The boxed number 13 icon unicode character.
/// </summary>
BoxedNumber13 = 0xE09C, BoxedNumber13 = 0xE09C,
/// <summary>
/// The boxed number 14 icon unicode character.
/// </summary>
BoxedNumber14 = 0xE09D, BoxedNumber14 = 0xE09D,
/// <summary>
/// The boxed number 15 icon unicode character.
/// </summary>
BoxedNumber15 = 0xE09E, BoxedNumber15 = 0xE09E,
/// <summary>
/// The boxed number 16 icon unicode character.
/// </summary>
BoxedNumber16 = 0xE09F, BoxedNumber16 = 0xE09F,
/// <summary>
/// The boxed number 17 icon unicode character.
/// </summary>
BoxedNumber17 = 0xE0A0, BoxedNumber17 = 0xE0A0,
/// <summary>
/// The boxed number 18 icon unicode character.
/// </summary>
BoxedNumber18 = 0xE0A1, BoxedNumber18 = 0xE0A1,
/// <summary>
/// The boxed number 19 icon unicode character.
/// </summary>
BoxedNumber19 = 0xE0A2, BoxedNumber19 = 0xE0A2,
/// <summary>
/// The boxed number 20 icon unicode character.
/// </summary>
BoxedNumber20 = 0xE0A3, BoxedNumber20 = 0xE0A3,
/// <summary>
/// The boxed number 21 icon unicode character.
/// </summary>
BoxedNumber21 = 0xE0A4, BoxedNumber21 = 0xE0A4,
/// <summary>
/// The boxed number 22 icon unicode character.
/// </summary>
BoxedNumber22 = 0xE0A5, BoxedNumber22 = 0xE0A5,
/// <summary>
/// The boxed number 23 icon unicode character.
/// </summary>
BoxedNumber23 = 0xE0A6, BoxedNumber23 = 0xE0A6,
/// <summary>
/// The boxed number 24 icon unicode character.
/// </summary>
BoxedNumber24 = 0xE0A7, BoxedNumber24 = 0xE0A7,
/// <summary>
/// The boxed number 25 icon unicode character.
/// </summary>
BoxedNumber25 = 0xE0A8, BoxedNumber25 = 0xE0A8,
/// <summary>
/// The boxed number 26 icon unicode character.
/// </summary>
BoxedNumber26 = 0xE0A9, BoxedNumber26 = 0xE0A9,
/// <summary>
/// The boxed number 27 icon unicode character.
/// </summary>
BoxedNumber27 = 0xE0AA, BoxedNumber27 = 0xE0AA,
/// <summary>
/// The boxed number 28 icon unicode character.
/// </summary>
BoxedNumber28 = 0xE0AB, BoxedNumber28 = 0xE0AB,
/// <summary>
/// The boxed number 29 icon unicode character.
/// </summary>
BoxedNumber29 = 0xE0AC, BoxedNumber29 = 0xE0AC,
/// <summary>
/// The boxed number 30 icon unicode character.
/// </summary>
BoxedNumber30 = 0xE0AD, BoxedNumber30 = 0xE0AD,
/// <summary>
/// The boxed number 31 icon unicode character.
/// </summary>
BoxedNumber31 = 0xE0AE, BoxedNumber31 = 0xE0AE,
/// <summary>
/// The boxed plus icon unicode character.
/// </summary>
BoxedPlus = 0xE0AF, BoxedPlus = 0xE0AF,
/// <summary>
/// The bosed question mark icon unicode character.
/// </summary>
BoxedQuestionMark = 0xE070, BoxedQuestionMark = 0xE070,
/// <summary>
/// The boxed star icon unicode character.
/// </summary>
BoxedStar = 0xE0C0, BoxedStar = 0xE0C0,
/// <summary>
/// The boxed Roman numeral 1 (I) icon unicode character.
/// </summary>
BoxedRoman1 = 0xE0C1, BoxedRoman1 = 0xE0C1,
/// <summary>
/// The boxed Roman numeral 2 (II) icon unicode character.
/// </summary>
BoxedRoman2 = 0xE0C2, BoxedRoman2 = 0xE0C2,
/// <summary>
/// The boxed Roman numeral 3 (III) icon unicode character.
/// </summary>
BoxedRoman3 = 0xE0C3, BoxedRoman3 = 0xE0C3,
/// <summary>
/// The boxed Roman numeral 4 (IV) icon unicode character.
/// </summary>
BoxedRoman4 = 0xE0C4, BoxedRoman4 = 0xE0C4,
/// <summary>
/// The boxed Roman numeral 5 (V) icon unicode character.
/// </summary>
BoxedRoman5 = 0xE0C5, BoxedRoman5 = 0xE0C5,
/// <summary>
/// The boxed Roman numeral 6 (VI) icon unicode character.
/// </summary>
BoxedRoman6 = 0xE0C6, BoxedRoman6 = 0xE0C6,
/// <summary>
/// The boxed letter A icon unicode character.
/// </summary>
BoxedLetterA = 0xE071, BoxedLetterA = 0xE071,
/// <summary>
/// The boxed letter B icon unicode character.
/// </summary>
BoxedLetterB = 0xE072, BoxedLetterB = 0xE072,
/// <summary>
/// The boxed letter C icon unicode character.
/// </summary>
BoxedLetterC = 0xE073, BoxedLetterC = 0xE073,
/// <summary>
/// The boxed letter D icon unicode character.
/// </summary>
BoxedLetterD = 0xE074, BoxedLetterD = 0xE074,
/// <summary>
/// The boxed letter E icon unicode character.
/// </summary>
BoxedLetterE = 0xE075, BoxedLetterE = 0xE075,
/// <summary>
/// The boxed letter F icon unicode character.
/// </summary>
BoxedLetterF = 0xE076, BoxedLetterF = 0xE076,
/// <summary>
/// The boxed letter G icon unicode character.
/// </summary>
BoxedLetterG = 0xE077, BoxedLetterG = 0xE077,
/// <summary>
/// The boxed letter H icon unicode character.
/// </summary>
BoxedLetterH = 0xE078, BoxedLetterH = 0xE078,
/// <summary>
/// The boxed letter I icon unicode character.
/// </summary>
BoxedLetterI = 0xE079, BoxedLetterI = 0xE079,
/// <summary>
/// The boxed letter J icon unicode character.
/// </summary>
BoxedLetterJ = 0xE07A, BoxedLetterJ = 0xE07A,
/// <summary>
/// The boxed letter K icon unicode character.
/// </summary>
BoxedLetterK = 0xE07B, BoxedLetterK = 0xE07B,
/// <summary>
/// The boxed letter L icon unicode character.
/// </summary>
BoxedLetterL = 0xE07C, BoxedLetterL = 0xE07C,
/// <summary>
/// The boxed letter M icon unicode character.
/// </summary>
BoxedLetterM = 0xE07D, BoxedLetterM = 0xE07D,
/// <summary>
/// The boxed letter N icon unicode character.
/// </summary>
BoxedLetterN = 0xE07E, BoxedLetterN = 0xE07E,
/// <summary>
/// The boxed letter O icon unicode character.
/// </summary>
BoxedLetterO = 0xE07F, BoxedLetterO = 0xE07F,
/// <summary>
/// The boxed letter P icon unicode character.
/// </summary>
BoxedLetterP = 0xE080, BoxedLetterP = 0xE080,
/// <summary>
/// The boxed letter Q icon unicode character.
/// </summary>
BoxedLetterQ = 0xE081, BoxedLetterQ = 0xE081,
/// <summary>
/// The boxed letter R icon unicode character.
/// </summary>
BoxedLetterR = 0xE082, BoxedLetterR = 0xE082,
/// <summary>
/// The boxed letter S icon unicode character.
/// </summary>
BoxedLetterS = 0xE083, BoxedLetterS = 0xE083,
/// <summary>
/// The boxed letter T icon unicode character.
/// </summary>
BoxedLetterT = 0xE084, BoxedLetterT = 0xE084,
/// <summary>
/// The boxed letter U icon unicode character.
/// </summary>
BoxedLetterU = 0xE085, BoxedLetterU = 0xE085,
/// <summary>
/// The boxed letter V icon unicode character.
/// </summary>
BoxedLetterV = 0xE086, BoxedLetterV = 0xE086,
/// <summary>
/// The boxed letter W icon unicode character.
/// </summary>
BoxedLetterW = 0xE087, BoxedLetterW = 0xE087,
/// <summary>
/// The boxed letter X icon unicode character.
/// </summary>
BoxedLetterX = 0xE088, BoxedLetterX = 0xE088,
/// <summary>
/// The boxed letter Y icon unicode character.
/// </summary>
BoxedLetterY = 0xE089, BoxedLetterY = 0xE089,
/// <summary>
/// The boxed letter Z icon unicode character.
/// </summary>
BoxedLetterZ = 0xE08A, BoxedLetterZ = 0xE08A,
/// <summary>
/// The circle icon unicode character.
/// </summary>
Circle = 0xE04A, Circle = 0xE04A,
/// <summary>
/// The square icon unicode character.
/// </summary>
Square = 0xE04B, Square = 0xE04B,
/// <summary>
/// The cross icon unicode character.
/// </summary>
Cross = 0xE04C, Cross = 0xE04C,
/// <summary>
/// The triangle icon unicode character.
/// </summary>
Triangle = 0xE04D, Triangle = 0xE04D,
/// <summary>
/// The hexagon icon unicode character.
/// </summary>
Hexagon = 0xE042, Hexagon = 0xE042,
/// <summary>
/// The no-circle/prohobited icon unicode character.
/// </summary>
Prohibited = 0xE043, Prohibited = 0xE043,
/// <summary>
/// The dice icon unicode character.
/// </summary>
Dice = 0xE03E, Dice = 0xE03E,
/// <summary>
/// The debuff icon unicode character.
/// </summary>
Debuff = 0xE05B, Debuff = 0xE05B,
/// <summary>
/// The buff icon unicode character.
/// </summary>
Buff = 0xE05C, Buff = 0xE05C,
/// <summary>
/// The cross-world icon unicode character.
/// </summary>
CrossWorld = 0xE05D, CrossWorld = 0xE05D,
/// <summary>
/// The Eureka level icon unicode character.
/// </summary>
EurekaLevel = 0xE03A, EurekaLevel = 0xE03A,
/// <summary>
/// The link marker icon unicode character.
/// </summary>
LinkMarker = 0xE0BB, LinkMarker = 0xE0BB,
/// <summary>
/// The glamoured icon unicode character.
/// </summary>
Glamoured = 0xE03B, Glamoured = 0xE03B,
/// <summary>
/// The glamoured and dyed icon unicode character.
/// </summary>
GlamouredDyed = 0xE04E, GlamouredDyed = 0xE04E,
/// <summary>
/// The synced quest icon unicode character.
/// </summary>
QuestSync = 0xE0BE, QuestSync = 0xE0BE,
/// <summary>
/// The repeatable quest icon unicode character.
/// </summary>
QuestRepeatable = 0xE0BF, QuestRepeatable = 0xE0BF,
/// <summary>
/// The IME hiragana icon unicode character.
/// </summary>
ImeHiragana = 0xE020, ImeHiragana = 0xE020,
/// <summary>
/// The IME katakana icon unicode character.
/// </summary>
ImeKatakana = 0xE021, ImeKatakana = 0xE021,
/// <summary>
/// The IME alphanumeric icon unicode character.
/// </summary>
ImeAlphanumeric = 0xE022, ImeAlphanumeric = 0xE022,
/// <summary>
/// The IME katakana half-width icon unicode character.
/// </summary>
ImeKatakanaHalfWidth = 0xE023, ImeKatakanaHalfWidth = 0xE023,
/// <summary>
/// The IME alphanumeric half-width icon unicode character.
/// </summary>
ImeAlphanumericHalfWidth = 0xE024, ImeAlphanumericHalfWidth = 0xE024,
/// <summary>
/// The instance (1) icon unicode character.
/// </summary>
Instance1 = 0xE0B1, Instance1 = 0xE0B1,
/// <summary>
/// The instance (2) icon unicode character.
/// </summary>
Instance2 = 0xE0B2, Instance2 = 0xE0B2,
/// <summary>
/// The instance (3) icon unicode character.
/// </summary>
Instance3 = 0xE0B3, Instance3 = 0xE0B3,
/// <summary>
/// The instance (4) icon unicode character.
/// </summary>
Instance4 = 0xE0B4, Instance4 = 0xE0B4,
/// <summary>
/// The instance (5) icon unicode character.
/// </summary>
Instance5 = 0xE0B5, Instance5 = 0xE0B5,
/// <summary>
/// The instance (6) icon unicode character.
/// </summary>
Instance6 = 0xE0B6, Instance6 = 0xE0B6,
/// <summary>
/// The instance (7) icon unicode character.
/// </summary>
Instance7 = 0xE0B7, Instance7 = 0xE0B7,
/// <summary>
/// The instance (8) icon unicode character.
/// </summary>
Instance8 = 0xE0B8, Instance8 = 0xE0B8,
/// <summary>
/// The instance (9) icon unicode character.
/// </summary>
Instance9 = 0xE0B9, Instance9 = 0xE0B9,
/// <summary>
/// The instance merged icon unicode character.
/// </summary>
InstanceMerged = 0xE0BA, InstanceMerged = 0xE0BA,
/// <summary>
/// The English local time icon unicode character.
/// </summary>
LocalTimeEn = 0xE0D0, LocalTimeEn = 0xE0D0,
/// <summary>
/// The English server time icon unicode character.
/// </summary>
ServerTimeEn = 0xE0D1, ServerTimeEn = 0xE0D1,
/// <summary>
/// The English Eorzea time icon unicode character.
/// </summary>
EorzeaTimeEn = 0xE0D2, EorzeaTimeEn = 0xE0D2,
/// <summary>
/// The German local time icon unicode character.
/// </summary>
LocalTimeDe = 0xE0D3, LocalTimeDe = 0xE0D3,
/// <summary>
/// The German server time icon unicode character.
/// </summary>
ServerTimeDe = 0xE0D4, ServerTimeDe = 0xE0D4,
/// <summary>
/// The German Eorzea time icon unicode character.
/// </summary>
EorzeaTimeDe = 0xE0D5, EorzeaTimeDe = 0xE0D5,
/// <summary>
/// The French local time icon unicode character.
/// </summary>
LocalTimeFr = 0xE0D6, LocalTimeFr = 0xE0D6,
/// <summary>
/// The French server time icon unicode character.
/// </summary>
ServerTimeFr = 0xE0D7, ServerTimeFr = 0xE0D7,
/// <summary>
/// The French Eorzea time icon unicode character.
/// </summary>
EorzeaTimeFr = 0xE0D8, EorzeaTimeFr = 0xE0D8,
/// <summary>
/// The Japanese local time icon unicode character.
/// </summary>
LocalTimeJa = 0xE0D9, LocalTimeJa = 0xE0D9,
/// <summary>
/// The Japanese server time icon unicode character.
/// </summary>
ServerTimeJa = 0xE0DA, ServerTimeJa = 0xE0DA,
/// <summary>
/// The Japanese Eorzea time icon unicode character.
/// </summary>
EorzeaTimeJa = 0xE0DB, EorzeaTimeJa = 0xE0DB,
} }
} }

View file

@ -1,120 +1,438 @@
#pragma warning disable 1591 namespace Dalamud.Game.Text.SeStringHandling
{
/// <summary>
/// This class represents special icons that can appear in chat naturally or as IconPayloads.
/// </summary>
public enum BitmapFontIcon : uint
{
/// <summary>
/// No icon.
/// </summary>
None = 0,
namespace Dalamud.Game.Text.SeStringHandling { /// <summary>
public enum BitmapFontIcon : uint { /// The controller D-pad up icon.
None, /// </summary>
ControllerDPadUp, ControllerDPadUp = 1,
ControllerDPadDown,
ControllerDPadLeft,
ControllerDPadRight,
ControllerDPadUpDown,
ControllerDPadLeftRight,
ControllerDPadAll,
ControllerButton0, // Xbox B / PS Circle /// <summary>
ControllerButton1, // Xbox A / PS Cross /// The controller D-pad down icon.
ControllerButton2, // Xbox X / PS Square /// </summary>
ControllerButton3, // Xbox Y / PS Triangle ControllerDPadDown = 2,
ControllerShoulderLeft, /// <summary>
ControllerShoulderRight, /// The controller D-pad left icon.
/// </summary>
ControllerDPadLeft = 3,
ControllerTriggerLeft, /// <summary>
ControllerTriggerRight, /// The controller D-pad right icon.
/// </summary>
ControllerDPadRight = 4,
ControllerAnalogLeftStickIn, /// <summary>
ControllerAnalogRightStickIn, /// The controller D-pad up/down icon.
/// </summary>
ControllerDPadUpDown = 5,
ControllerStart, /// <summary>
ControllerBack, /// The controller D-pad left/right icon.
/// </summary>
ControllerDPadLeftRight = 6,
ControllerAnalogLeftStick, /// <summary>
ControllerAnalogLeftStickUpDown, /// The controller D-pad all directions icon.
ControllerAnalogLeftStickLeftRight, /// </summary>
ControllerDPadAll = 7,
ControllerAnalogRightStick, /// <summary>
ControllerAnalogRightStickUpDown, /// The controller button 0 icon (Xbox: B, PlayStation: Circle).
ControllerAnalogRightStickLeftRight, /// </summary>
ControllerButton0 = 8,
/// <summary>
/// The controller button 1 icon (XBox: A, PlayStation: Cross).
/// </summary>
ControllerButton1 = 9,
/// <summary>
/// The controller button 2 icon (XBox: X, PlayStation: Square).
/// </summary>
ControllerButton2 = 10,
/// <summary>
/// The controller button 3 icon (BBox: Y, PlayStation: Triangle).
/// </summary>
ControllerButton3 = 11,
/// <summary>
/// The controller left shoulder button icon.
/// </summary>
ControllerShoulderLeft = 12,
/// <summary>
/// The controller right shoulder button icon.
/// </summary>
ControllerShoulderRight = 13,
/// <summary>
/// The controller left trigger button icon.
/// </summary>
ControllerTriggerLeft = 14,
/// <summary>
/// The controller right trigger button icon.
/// </summary>
ControllerTriggerRight = 15,
/// <summary>
/// The controller left analog stick in icon.
/// </summary>
ControllerAnalogLeftStickIn = 16,
/// <summary>
/// The controller right analog stick in icon.
/// </summary>
ControllerAnalogRightStickIn = 17,
/// <summary>
/// The controller start button icon.
/// </summary>
ControllerStart = 18,
/// <summary>
/// The controller back button icon.
/// </summary>
ControllerBack = 19,
/// <summary>
/// The controller left analog stick icon.
/// </summary>
ControllerAnalogLeftStick = 20,
/// <summary>
/// The controller left analog stick up/down icon.
/// </summary>
ControllerAnalogLeftStickUpDown = 21,
/// <summary>
/// The controller left analog stick left/right icon.
/// </summary>
ControllerAnalogLeftStickLeftRight = 22,
/// <summary>
/// The controller right analog stick icon.
/// </summary>
ControllerAnalogRightStick = 23,
/// <summary>
/// The controller right analog stick up/down icon.
/// </summary>
ControllerAnalogRightStickUpDown = 24,
/// <summary>
/// The controller right analog stick left/right icon.
/// </summary>
ControllerAnalogRightStickLeftRight = 25,
/// <summary>
/// The La Noscea region icon.
/// </summary>
LaNoscea = 51, LaNoscea = 51,
BlackShroud,
Thanalan,
AutoTranslateBegin,
AutoTranslateEnd,
ElementFire,
ElementIce,
ElementWind,
ElementEarth,
ElementLightning,
ElementWater,
LevelSync,
Warning,
Ishgard,
Aetheryte,
Aethernet,
GoldStar, /// <summary>
SilverStar, /// The Black Shroud region icon.
/// </summary>
BlackShroud = 52,
/// <summary>
/// The Thanalan region icon.
/// </summary>
Thanalan = 53,
/// <summary>
/// The auto translate begin icon.
/// </summary>
AutoTranslateBegin = 54,
/// <summary>
/// The auto translate end icon.
/// </summary>
AutoTranslateEnd = 55,
/// <summary>
/// The fire element icon.
/// </summary>
ElementFire = 56,
/// <summary>
/// The ice element icon.
/// </summary>
ElementIce = 57,
/// <summary>
/// The wind element icon.
/// </summary>
ElementWind = 58,
/// <summary>
/// The earth element icon.
/// </summary>
ElementEarth = 59,
/// <summary>
/// The lightning element icon.
/// </summary>
ElementLightning = 60,
/// <summary>
/// The water element icon.
/// </summary>
ElementWater = 61,
/// <summary>
/// The level sync icon.
/// </summary>
LevelSync = 62,
/// <summary>
/// The warning icon.
/// </summary>
Warning = 63,
/// <summary>
/// The Ishgard region icon.
/// </summary>
Ishgard = 64,
/// <summary>
/// The Aetheryte icon.
/// </summary>
Aetheryte = 65,
/// <summary>
/// The Aethernet icon.
/// </summary>
Aethernet = 66,
/// <summary>
/// The gold star icon.
/// </summary>
GoldStar = 67,
/// <summary>
/// The silver star icon.
/// </summary>
SilverStar = 68,
/// <summary>
/// The green dot icon.
/// </summary>
GreenDot = 70, GreenDot = 70,
SwordUnsheathed,
SwordSheathed,
Dice, /// <summary>
/// The unsheathed sword icon.
/// </summary>
SwordUnsheathed = 71,
FlyZone, /// <summary>
FlyZoneLocked, /// The sheathed sword icon.
/// </summary>
SwordSheathed = 72,
NoCircle, /// <summary>
/// The dice icon.
/// </summary>
Dice = 73,
NewAdventurer, /// <summary>
Mentor, /// The flyable zone icon.
MentorPvE, /// </summary>
MentorCrafting, FlyZone = 74,
MentorPvP,
Tank, /// <summary>
Healer, /// The no-flying zone icon.
DPS, /// </summary>
Crafter, FlyZoneLocked = 75,
Gatherer,
AnyClass,
CrossWorld, /// <summary>
/// The no-circle/prohibited icon.
/// </summary>
NoCircle = 76,
FateSlay, /// <summary>
FateBoss, /// The sprout icon.
FateGather, /// </summary>
FateDefend, NewAdventurer = 77,
FateEscort,
FateSpecial1,
Returner, /// <summary>
/// The mentor icon.
/// </summary>
Mentor = 78,
FarEast, /// <summary>
GyrAbania, /// The PvE mentor icon.
/// </summary>
MentorPvE = 79,
FateSpecial2, /// <summary>
/// The crafting mentor icon.
/// </summary>
MentorCrafting = 80,
PriorityWorld, /// <summary>
/// The PvP mentor icon.
/// </summary>
MentorPvP = 81,
ElementalLevel, /// <summary>
ExclamationRectangle, /// The tank role icon.
/// </summary>
Tank = 82,
NotoriousMonster, /// <summary>
/// The healer role icon.
/// </summary>
Healer = 83,
Recording, /// <summary>
Alarm, /// The DPS role icon.
/// </summary>
DPS = 84,
ArrowUp, /// <summary>
ArrowDown, /// The crafter role icon.
Crystarium, /// </summary>
Crafter = 85,
MentorProblem, /// <summary>
/// The gatherer role icon.
/// </summary>
Gatherer = 86,
FateUnknownGold, /// <summary>
/// The "any" role icon.
/// </summary>
AnyClass = 87,
OrangeDiamond, /// <summary>
FateCrafting /// The cross-world icon.
/// </summary>
CrossWorld = 88,
/// <summary>
/// The slay type Fate icon.
/// </summary>
FateSlay = 89,
/// <summary>
/// The boss type Fate icon.
/// </summary>
FateBoss = 90,
/// <summary>
/// The gather type Fate icon.
/// </summary>
FateGather = 91,
/// <summary>
/// The defend type Fate icon.
/// </summary>
FateDefend = 92,
/// <summary>
/// The escort type Fate icon.
/// </summary>
FateEscort = 93,
/// <summary>
/// The special type 1 Fate icon.
/// </summary>
FateSpecial1 = 94,
/// <summary>
/// The returner icon.
/// </summary>
Returner = 95,
/// <summary>
/// The Far-East region icon.
/// </summary>
FarEast = 96,
/// <summary>
/// The Gyr Albania region icon.
/// </summary>
GyrAbania = 97,
/// <summary>
/// The special type 2 Fate icon.
/// </summary>
FateSpecial2 = 98,
/// <summary>
/// The priority world icon.
/// </summary>
PriorityWorld = 99,
/// <summary>
/// The elemental level icon.
/// </summary>
ElementalLevel = 100,
/// <summary>
/// The exclamation rectangle icon.
/// </summary>
ExclamationRectangle = 101,
/// <summary>
/// The notorious monster icon.
/// </summary>
NotoriousMonster = 102,
/// <summary>
/// The recording icon.
/// </summary>
Recording = 103,
/// <summary>
/// The alarm icon.
/// </summary>
Alarm = 104,
/// <summary>
/// The arrow up icon.
/// </summary>
ArrowUp = 105,
/// <summary>
/// The arrow down icon.
/// </summary>
ArrowDown = 106,
/// <summary>
/// The Crystarium region icon.
/// </summary>
Crystarium = 107,
/// <summary>
/// The mentor problem icon.
/// </summary>
MentorProblem = 108,
/// <summary>
/// The unknown gold type Fate icon.
/// </summary>
FateUnknownGold = 109,
/// <summary>
/// The orange diamond icon.
/// </summary>
OrangeDiamond = 110,
/// <summary>
/// The crafting type Fate icon.
/// </summary>
FateCrafting = 111,
} }
} }

View file

@ -1,9 +1,13 @@
using System;
namespace Dalamud.Game.Text.SeStringHandling namespace Dalamud.Game.Text.SeStringHandling
{ {
/// <summary>
/// An interface binding for a payload that can provide readable Text.
/// </summary>
public interface ITextProvider public interface ITextProvider
{ {
/// <summary>
/// Gets the readable text.
/// </summary>
string Text { get; } string Text { get; }
} }
} }

View file

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Serilog; using Serilog;
@ -19,35 +19,8 @@ namespace Dalamud.Game.Text.SeStringHandling
/// <summary> /// <summary>
/// This class represents a parsed SeString payload. /// This class represents a parsed SeString payload.
/// </summary> /// </summary>
public abstract class Payload public abstract partial class Payload
{ {
/// <summary>
/// The type of this payload.
/// </summary>
public abstract PayloadType Type { get; }
/// <summary>
/// Whether this payload has been modified since the last Encode().
/// </summary>
public bool Dirty { get; protected set; } = true;
/// <summary>
/// Encodes the internal state of this payload into a byte[] suitable for sending to in-game
/// handlers such as the chat log.
/// </summary>
/// <returns>Encoded binary payload data suitable for use with in-game handlers.</returns>
protected abstract byte[] EncodeImpl();
// TODO: endOfStream is somewhat legacy now that payload length is always handled correctly.
// This could be changed to just take a straight byte[], but that would complicate reading
// but we could probably at least remove the end param
/// <summary>
/// Decodes a byte stream from the game into a payload object.
/// </summary>
/// <param name="reader">A BinaryReader containing at least all the data for this payload.</param>
/// <param name="endOfStream">The location holding the end of the data for this payload.</param>
protected abstract void DecodeImpl(BinaryReader reader, long endOfStream);
/// <summary> /// <summary>
/// The Lumina instance to use for any necessary data lookups. /// The Lumina instance to use for any necessary data lookups.
/// </summary> /// </summary>
@ -58,31 +31,26 @@ namespace Dalamud.Game.Text.SeStringHandling
private byte[] encodedData; private byte[] encodedData;
/// <summary> /// <summary>
/// Encode this payload object into a byte[] useable in-game for things like the chat log. /// Gets the type of this payload.
/// </summary> /// </summary>
/// <param name="force">If true, ignores any cached value and forcibly reencodes the payload from its internal representation.</param> public abstract PayloadType Type { get; }
/// <returns>A byte[] suitable for use with in-game handlers such as the chat log.</returns>
public byte[] Encode(bool force = false)
{
if (Dirty || force)
{
this.encodedData = EncodeImpl();
Dirty = false;
}
return this.encodedData; /// <summary>
} /// Gets or sets a value indicating whether whether this payload has been modified since the last Encode().
/// </summary>
public bool Dirty { get; protected set; } = true;
/// <summary> /// <summary>
/// Decodes a binary representation of a payload into its corresponding nice object payload. /// Decodes a binary representation of a payload into its corresponding nice object payload.
/// </summary> /// </summary>
/// <param name="reader">A reader positioned at the start of the payload, and containing at least one entire payload.</param> /// <param name="reader">A reader positioned at the start of the payload, and containing at least one entire payload.</param>
/// <param name="data">The DataManager instance.</param>
/// <returns>The constructed Payload-derived object that was decoded from the binary data.</returns> /// <returns>The constructed Payload-derived object that was decoded from the binary data.</returns>
public static Payload Decode(BinaryReader reader, DataManager data) public static Payload Decode(BinaryReader reader, DataManager data)
{ {
var payloadStartPos = reader.BaseStream.Position; var payloadStartPos = reader.BaseStream.Position;
Payload payload = null; Payload payload;
var initialByte = reader.ReadByte(); var initialByte = reader.ReadByte();
reader.BaseStream.Position--; reader.BaseStream.Position--;
@ -113,6 +81,39 @@ namespace Dalamud.Game.Text.SeStringHandling
return payload; return payload;
} }
/// <summary>
/// Encode this payload object into a byte[] useable in-game for things like the chat log.
/// </summary>
/// <param name="force">If true, ignores any cached value and forcibly reencodes the payload from its internal representation.</param>
/// <returns>A byte[] suitable for use with in-game handlers such as the chat log.</returns>
public byte[] Encode(bool force = false)
{
if (this.Dirty || force)
{
this.encodedData = this.EncodeImpl();
this.Dirty = false;
}
return this.encodedData;
}
/// <summary>
/// Encodes the internal state of this payload into a byte[] suitable for sending to in-game
/// handlers such as the chat log.
/// </summary>
/// <returns>Encoded binary payload data suitable for use with in-game handlers.</returns>
protected abstract byte[] EncodeImpl();
/// <summary>
/// Decodes a byte stream from the game into a payload object.
/// </summary>
/// <param name="reader">A BinaryReader containing at least all the data for this payload.</param>
/// <param name="endOfStream">The location holding the end of the data for this payload.</param>
// TODO: endOfStream is somewhat legacy now that payload length is always handled correctly.
// This could be changed to just take a straight byte[], but that would complicate reading
// but we could probably at least remove the end param
protected abstract void DecodeImpl(BinaryReader reader, long endOfStream);
private static Payload DecodeChunk(BinaryReader reader) private static Payload DecodeChunk(BinaryReader reader)
{ {
Payload payload = null; Payload payload = null;
@ -171,11 +172,13 @@ namespace Dalamud.Game.Text.SeStringHandling
{ {
Log.Verbose("Unhandled EmbeddedInfoType: {0}", subType); Log.Verbose("Unhandled EmbeddedInfoType: {0}", subType);
} }
// rewind so we capture the Interactable byte in the raw data // rewind so we capture the Interactable byte in the raw data
reader.BaseStream.Seek(-1, SeekOrigin.Current); reader.BaseStream.Seek(-1, SeekOrigin.Current);
break; break;
} }
} }
break; break;
case SeStringChunkType.AutoTranslateKey: case SeStringChunkType.AutoTranslateKey:
@ -216,43 +219,126 @@ namespace Dalamud.Game.Text.SeStringHandling
return payload; return payload;
} }
}
#region parse constants and helpers /// <summary>
/// Parsing helpers.
/// </summary>
public abstract partial class Payload
{
/// <summary>
/// The start byte of a payload.
/// </summary>
protected const byte START_BYTE = 0x02; protected const byte START_BYTE = 0x02;
/// <summary>
/// The end byte of a payload.
/// </summary>
protected const byte END_BYTE = 0x03; protected const byte END_BYTE = 0x03;
protected enum SeStringChunkType /// <summary>
{ /// This represents the type of embedded info in a payload.
Icon = 0x12, /// </summary>
EmphasisItalic = 0x1A,
SeHyphen = 0x1F,
Interactable = 0x27,
AutoTranslateKey = 0x2E,
UIForeground = 0x48,
UIGlow = 0x49
}
public enum EmbeddedInfoType public enum EmbeddedInfoType
{ {
/// <summary>
/// A player's name.
/// </summary>
PlayerName = 0x01, PlayerName = 0x01,
/// <summary>
/// The link to an iteme.
/// </summary>
ItemLink = 0x03, ItemLink = 0x03,
/// <summary>
/// The link to a map position.
/// </summary>
MapPositionLink = 0x04, MapPositionLink = 0x04,
/// <summary>
/// The link to a quest.
/// </summary>
QuestLink = 0x05, QuestLink = 0x05,
/// <summary>
/// A status effect.
/// </summary>
Status = 0x09, Status = 0x09,
DalamudLink = 0x0F, // Dalamud Custom /// <summary>
/// A custom Dalamud link.
/// </summary>
DalamudLink = 0x0F,
LinkTerminator = 0xCF // not clear but seems to always follow a link /// <summary>
/// A link terminator.
/// </summary>
/// <remarks>
/// It is not exactly clear what this is, but seems to always follow a link.
/// </remarks>
LinkTerminator = 0xCF,
} }
/// <summary>
/// This represents the type of payload and how it should be encoded.
/// </summary>
protected enum SeStringChunkType
{
/// <summary>
/// See the <see cref="IconPayload"/> class.
/// </summary>
Icon = 0x12,
/// <summary>
/// See the <see cref="EmphasisItalicPayload"/> class.
/// </summary>
EmphasisItalic = 0x1A,
/// <summary>
/// See the <see cref="SeHyphenPayload"/> class.
/// </summary>
SeHyphen = 0x1F,
/// <summary>
/// See any of the link-type classes:
/// <see cref="PlayerPayload"/>,
/// <see cref="ItemPayload"/>,
/// <see cref="MapLinkPayload"/>,
/// <see cref="StatusPayload"/>,
/// <see cref="QuestPayload"/>,
/// <see cref="DalamudLinkPayload"/>.
/// </summary>
Interactable = 0x27,
/// <summary>
/// See the <see cref="AutoTranslatePayload"/> class.
/// </summary>
AutoTranslateKey = 0x2E,
/// <summary>
/// See the <see cref="UIForegroundPayload"/> class.
/// </summary>
UIForeground = 0x48,
/// <summary>
/// See the <see cref="UIGlowPayload"/> class.
/// </summary>
UIGlow = 0x49,
}
/// <summary>
/// Retrieve the packed integer from SE's native data format.
/// </summary>
/// <param name="input">The BinaryReader instance.</param>
/// <returns>An integer.</returns>
// made protected, unless we actually want to use it externally // made protected, unless we actually want to use it externally
// in which case it should probably go live somewhere else // in which case it should probably go live somewhere else
protected static uint GetInteger(BinaryReader input) protected static uint GetInteger(BinaryReader input)
{ {
uint marker = input.ReadByte(); uint marker = input.ReadByte();
if (marker < 0xD0) return marker - 1; if (marker < 0xD0)
return marker - 1;
// the game adds 0xF0 marker for values >= 0xCF // the game adds 0xF0 marker for values >= 0xCF
// uasge of 0xD0-0xEF is unknown, should we throw here? // uasge of 0xD0-0xEF is unknown, should we throw here?
@ -269,6 +355,11 @@ namespace Dalamud.Game.Text.SeStringHandling
return BitConverter.ToUInt32(ret, 0); return BitConverter.ToUInt32(ret, 0);
} }
/// <summary>
/// Create a packed integer in Se's native data format.
/// </summary>
/// <param name="value">The value to pack.</param>
/// <returns>A packed integer.</returns>
protected static byte[] MakeInteger(uint value) protected static byte[] MakeInteger(uint value)
{ {
if (value < 0xCF) if (value < 0xCF)
@ -287,22 +378,33 @@ namespace Dalamud.Game.Text.SeStringHandling
ret[0] |= (byte)(1 << i); ret[0] |= (byte)(1 << i);
} }
} }
ret[0] -= 1; ret[0] -= 1;
return ret.ToArray(); return ret.ToArray();
} }
protected static (uint, uint) GetPackedIntegers(BinaryReader input) /// <summary>
/// From a binary packed integer, get the high and low bytes.
/// </summary>
/// <param name="input">The BinaryReader instance.</param>
/// <returns>The high and low bytes.</returns>
protected static (uint High, uint Low) GetPackedIntegers(BinaryReader input)
{ {
var value = GetInteger(input); var value = GetInteger(input);
return (value >> 16, value & 0xFFFF); return (value >> 16, value & 0xFFFF);
} }
protected static byte[] MakePackedInteger(uint val1, uint val2) /// <summary>
/// Create a packed integer from the given high and low bytes.
/// </summary>
/// <param name="high">The high order bytes.</param>
/// <param name="low">The low order bytes.</param>
/// <returns>A packed integer.</returns>
protected static byte[] MakePackedInteger(uint high, uint low)
{ {
var value = (val1 << 16) | (val2 & 0xFFFF); var value = (high << 16) | (low & 0xFFFF);
return MakeInteger(value); return MakeInteger(value);
} }
#endregion
} }
} }

View file

@ -1,4 +1,3 @@
namespace Dalamud.Game.Text.SeStringHandling namespace Dalamud.Game.Text.SeStringHandling
{ {
/// <summary> /// <summary>
@ -10,54 +9,70 @@ namespace Dalamud.Game.Text.SeStringHandling
/// An SeString payload representing a player link. /// An SeString payload representing a player link.
/// </summary> /// </summary>
Player, Player,
/// <summary> /// <summary>
/// An SeString payload representing an Item link. /// An SeString payload representing an Item link.
/// </summary> /// </summary>
Item, Item,
/// <summary> /// <summary>
/// An SeString payload representing an Status Effect link. /// An SeString payload representing an Status Effect link.
/// </summary> /// </summary>
Status, Status,
/// <summary> /// <summary>
/// An SeString payload representing raw, typed text. /// An SeString payload representing raw, typed text.
/// </summary> /// </summary>
RawText, RawText,
/// <summary> /// <summary>
/// An SeString payload representing a text foreground color. /// An SeString payload representing a text foreground color.
/// </summary> /// </summary>
UIForeground, UIForeground,
/// <summary> /// <summary>
/// An SeString payload representing a text glow color. /// An SeString payload representing a text glow color.
/// </summary> /// </summary>
UIGlow, UIGlow,
/// <summary> /// <summary>
/// An SeString payload representing a map position link, such as from &lt;flag&gt; or &lt;pos&gt;. /// An SeString payload representing a map position link, such as from &lt;flag&gt; or &lt;pos&gt;.
/// </summary> /// </summary>
MapLink, MapLink,
/// <summary> /// <summary>
/// An SeString payload representing an auto-translate dictionary entry. /// An SeString payload representing an auto-translate dictionary entry.
/// </summary> /// </summary>
AutoTranslateText, AutoTranslateText,
/// <summary> /// <summary>
/// An SeString payload representing italic emphasis formatting on text. /// An SeString payload representing italic emphasis formatting on text.
/// </summary> /// </summary>
EmphasisItalic, EmphasisItalic,
/// <summary> /// <summary>
/// An SeString payload representing a bitmap icon. /// An SeString payload representing a bitmap icon.
/// </summary> /// </summary>
Icon, Icon,
/// <summary> /// <summary>
/// A SeString payload representing a quest link. /// A SeString payload representing a quest link.
/// </summary> /// </summary>
Quest, Quest,
/// <summary> /// <summary>
/// A SeString payload representing a custom clickable link for dalamud plugins /// A SeString payload representing a custom clickable link for dalamud plugins.
/// </summary> /// </summary>
DalamudLink, DalamudLink,
/// <summary> /// <summary>
/// An SeString payload representing any data we don't handle. /// An SeString payload representing any data we don't handle.
/// </summary> /// </summary>
Unknown, Unknown,
/// <summary>
/// An SeString payload representing a doublewide SE hypen.
/// </summary>
SeHyphen, SeHyphen,
} }
} }

View file

@ -1,11 +1,12 @@
using Lumina.Excel.GeneratedSheets;
using Serilog;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Data; using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Game.Text.SeStringHandling.Payloads namespace Dalamud.Game.Text.SeStringHandling.Payloads
{ {
@ -14,25 +15,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// </summary> /// </summary>
public class AutoTranslatePayload : Payload, ITextProvider public class AutoTranslatePayload : Payload, ITextProvider
{ {
public override PayloadType Type => PayloadType.AutoTranslateText;
private string text; private string text;
/// <summary>
/// The actual text displayed in-game for this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public string Text
{
get
{
// wrap the text in the colored brackets that is uses in-game, since those
// are not actually part of any of the payloads
this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {Resolve()} {(char)SeIconChar.AutoTranslateClose}";
return this.text;
}
}
[JsonProperty] [JsonProperty]
private uint group; private uint group;
@ -40,9 +23,8 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
[JsonProperty] [JsonProperty]
private uint key; private uint key;
internal AutoTranslatePayload() { }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AutoTranslatePayload"/> class.
/// Creates a new auto-translate payload. /// Creates a new auto-translate payload.
/// </summary> /// </summary>
/// <param name="data">DataManager instance needed to resolve game data.</param> /// <param name="data">DataManager instance needed to resolve game data.</param>
@ -52,19 +34,49 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
/// This table is somewhat complicated in structure, and so using this constructor may not be very nice. /// This table is somewhat complicated in structure, and so using this constructor may not be very nice.
/// There is probably little use to create one of these, however. /// There is probably little use to create one of these, however.
/// </remarks> /// </remarks>
public AutoTranslatePayload(DataManager data, uint group, uint key) { public AutoTranslatePayload(DataManager data, uint group, uint key)
{
// TODO: friendlier ctor? not sure how to handle that given how weird the tables are
this.DataResolver = data; this.DataResolver = data;
this.group = group; this.group = group;
this.key = key; this.key = key;
} }
// TODO: friendlier ctor? not sure how to handle that given how weird the tables are /// <summary>
/// Initializes a new instance of the <see cref="AutoTranslatePayload"/> class.
public override string ToString() /// </summary>
internal AutoTranslatePayload()
{ {
return $"{Type} - Group: {group}, Key: {key}, Text: {Text}";
} }
/// <inheritdoc/>
public override PayloadType Type => PayloadType.AutoTranslateText;
/// <summary>
/// Gets the actual text displayed in-game for this payload.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
public string Text
{
get
{
// wrap the text in the colored brackets that is uses in-game, since those are not actually part of any of the payloads
return this.text ??= $"{(char)SeIconChar.AutoTranslateOpen} {this.Resolve()} {(char)SeIconChar.AutoTranslateClose}";
}
}
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString()
{
return $"{this.Type} - Group: {this.group}, Key: {this.key}, Text: {this.Text}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl() protected override byte[] EncodeImpl()
{ {
var keyBytes = MakeInteger(this.key); var keyBytes = MakeInteger(this.key);
@ -74,7 +86,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
{ {
START_BYTE, START_BYTE,
(byte)SeStringChunkType.AutoTranslateKey, (byte)chunkLen, (byte)SeStringChunkType.AutoTranslateKey, (byte)chunkLen,
(byte)this.group (byte)this.group,
}; };
bytes.AddRange(keyBytes); bytes.AddRange(keyBytes);
bytes.Add(END_BYTE); bytes.Add(END_BYTE);
@ -82,6 +94,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return bytes.ToArray(); return bytes.ToArray();
} }
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream) protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{ {
// this seems to always be a bare byte, and not following normal integer encoding // this seems to always be a bare byte, and not following normal integer encoding
@ -105,7 +118,9 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
// (again, if it's meant for another table) // (again, if it's meant for another table)
row = sheet.GetRow(this.key); row = sheet.GetRow(this.key);
} }
catch { } // don't care, row will be null catch
{
} // don't care, row will be null
if (row?.Group == this.group) if (row?.Group == this.group)
{ {
@ -142,7 +157,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
"TextCommand" => this.DataResolver.GetExcelSheet<TextCommand>().GetRow(this.key).Command, "TextCommand" => this.DataResolver.GetExcelSheet<TextCommand>().GetRow(this.key).Command,
"Tribe" => this.DataResolver.GetExcelSheet<Tribe>().GetRow(this.key).Masculine, "Tribe" => this.DataResolver.GetExcelSheet<Tribe>().GetRow(this.key).Masculine,
"Weather" => this.DataResolver.GetExcelSheet<Weather>().GetRow(this.key).Name, "Weather" => this.DataResolver.GetExcelSheet<Weather>().GetRow(this.key).Name,
_ => throw new Exception(actualTableName) _ => throw new Exception(actualTableName),
}; };
value = name; value = name;

View file

@ -1,30 +1,46 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace Dalamud.Game.Text.SeStringHandling.Payloads { namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <summary> /// <summary>
/// /// This class represents a custom Dalamud clickable chat link.
/// </summary> /// </summary>
public class DalamudLinkPayload : Payload { public class DalamudLinkPayload : Payload
{
/// <inheritdoc/>
public override PayloadType Type => PayloadType.DalamudLink; public override PayloadType Type => PayloadType.DalamudLink;
/// <summary>
/// Gets the plugin command ID to be linked.
/// </summary>
public uint CommandId { get; internal set; } = 0; public uint CommandId { get; internal set; } = 0;
/// <summary>
/// Gets the plugin name to be linked.
/// </summary>
[NotNull] [NotNull]
public string Plugin { get; internal set; } = string.Empty; public string Plugin { get; internal set; } = string.Empty;
protected override byte[] EncodeImpl() { /// <inheritdoc/>
var pluginBytes = Encoding.UTF8.GetBytes(Plugin); public override string ToString()
var commandBytes = MakeInteger(CommandId); {
return $"{this.Type} - Plugin: {this.Plugin}, Command: {this.CommandId}";
}
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
var pluginBytes = Encoding.UTF8.GetBytes(this.Plugin);
var commandBytes = MakeInteger(this.CommandId);
var chunkLen = 3 + pluginBytes.Length + commandBytes.Length; var chunkLen = 3 + pluginBytes.Length + commandBytes.Length;
if (chunkLen > 255) { if (chunkLen > 255)
{
throw new Exception("Chunk is too long. Plugin name exceeds limits for DalamudLinkPayload"); throw new Exception("Chunk is too long. Plugin name exceeds limits for DalamudLinkPayload");
} }
@ -36,13 +52,11 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads {
return bytes.ToArray(); return bytes.ToArray();
} }
protected override void DecodeImpl(BinaryReader reader, long endOfStream) { /// <inheritdoc/>
Plugin = Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadByte())); protected override void DecodeImpl(BinaryReader reader, long endOfStream)
CommandId = GetInteger(reader); {
} this.Plugin = Encoding.UTF8.GetString(reader.ReadBytes(reader.ReadByte()));
this.CommandId = GetInteger(reader);
public override string ToString() {
return $"{Type} - Plugin: {Plugin}, Command: {CommandId}";
} }
} }
} }

View file

@ -14,47 +14,58 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
public class EmphasisItalicPayload : Payload public class EmphasisItalicPayload : Payload
{ {
/// <summary> /// <summary>
/// Payload representing enabling italics on following text. /// Initializes a new instance of the <see cref="EmphasisItalicPayload"/> class.
/// </summary>
public static EmphasisItalicPayload ItalicsOn => new EmphasisItalicPayload(true);
/// <summary>
/// Payload representing disabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOff => new EmphasisItalicPayload(false);
public override PayloadType Type => PayloadType.EmphasisItalic;
/// <summary>
/// Whether this payload enables italics formatting for following text.
/// </summary>
public bool IsEnabled { get; private set; }
internal EmphasisItalicPayload() { }
/// <summary>
/// Creates an EmphasisItalicPayload. /// Creates an EmphasisItalicPayload.
/// </summary> /// </summary>
/// <param name="enabled">Whether italics formatting should be enabled or disabled for following text.</param> /// <param name="enabled">Whether italics formatting should be enabled or disabled for following text.</param>
public EmphasisItalicPayload(bool enabled) public EmphasisItalicPayload(bool enabled)
{ {
IsEnabled = enabled; this.IsEnabled = enabled;
} }
/// <summary>
/// Initializes a new instance of the <see cref="EmphasisItalicPayload"/> class.
/// Creates an EmphasisItalicPayload.
/// </summary>
internal EmphasisItalicPayload()
{
}
/// <summary>
/// Gets a payload representing enabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOn => new(true);
/// <summary>
/// Gets a payload representing disabling italics on following text.
/// </summary>
public static EmphasisItalicPayload ItalicsOff => new(false);
/// <summary>
/// Gets a value indicating whether this payload enables italics formatting for following text.
/// </summary>
public bool IsEnabled { get; private set; }
/// <inheritdoc/>
public override PayloadType Type => PayloadType.EmphasisItalic;
/// <inheritdoc/>
public override string ToString() public override string ToString()
{ {
return $"{Type} - Enabled: {IsEnabled}"; return $"{this.Type} - Enabled: {this.IsEnabled}";
} }
/// <inheritdoc/>
protected override byte[] EncodeImpl() protected override byte[] EncodeImpl()
{ {
// realistically this will always be a single byte of value 1 or 2 // realistically this will always be a single byte of value 1 or 2
// but we'll treat it normally anyway // but we'll treat it normally anyway
var enabledBytes = MakeInteger(IsEnabled ? (uint)1 : 0); var enabledBytes = MakeInteger(this.IsEnabled ? 1u : 0);
var chunkLen = enabledBytes.Length + 1; var chunkLen = enabledBytes.Length + 1;
var bytes = new List<byte>() var bytes = new List<byte>()
{ {
START_BYTE, (byte)SeStringChunkType.EmphasisItalic, (byte)chunkLen START_BYTE, (byte)SeStringChunkType.EmphasisItalic, (byte)chunkLen,
}; };
bytes.AddRange(enabledBytes); bytes.AddRange(enabledBytes);
bytes.Add(END_BYTE); bytes.Add(END_BYTE);
@ -62,9 +73,10 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
return bytes.ToArray(); return bytes.ToArray();
} }
/// <inheritdoc/>
protected override void DecodeImpl(BinaryReader reader, long endOfStream) protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{ {
IsEnabled = (GetInteger(reader) == 1); this.IsEnabled = GetInteger(reader) == 1;
} }
} }
} }

View file

@ -1,51 +1,71 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System;
namespace Dalamud.Game.Text.SeStringHandling.Payloads {
namespace Dalamud.Game.Text.SeStringHandling.Payloads
{
/// <summary> /// <summary>
/// SeString payload representing a bitmap icon from fontIcon /// SeString payload representing a bitmap icon from fontIcon.
/// </summary> /// </summary>
public class IconPayload : Payload { public class IconPayload : Payload
{
/// <summary>
/// Index of the icon
/// </summary>
[Obsolete("Use IconPayload.Icon")]
public uint IconIndex => (uint) Icon;
/// <summary>
/// Icon the payload represents.
/// </summary>
public BitmapFontIcon Icon { get; set; } = BitmapFontIcon.None;
internal IconPayload() { }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon. /// Create a Icon payload for the specified icon.
/// </summary> /// </summary>
/// <param name="iconIndex">Index of the icon</param> /// <param name="icon">The Icon.</param>
public IconPayload(BitmapFontIcon icon)
{
this.Icon = icon;
}
/// <summary>
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon.
/// </summary>
/// <param name="iconIndex">Index of the icon.</param>
[Obsolete("IconPayload(uint) is deprecated, please use IconPayload(BitmapFontIcon).")] [Obsolete("IconPayload(uint) is deprecated, please use IconPayload(BitmapFontIcon).")]
public IconPayload(uint iconIndex) : this((BitmapFontIcon) iconIndex) { } public IconPayload(uint iconIndex)
: this((BitmapFontIcon)iconIndex)
{
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon. /// Create a Icon payload for the specified icon.
/// </summary> /// </summary>
/// <param name="icon">The Icon</param> internal IconPayload()
public IconPayload(BitmapFontIcon icon) { {
Icon = icon;
} }
/// <inheritdoc/> /// <inheritdoc/>
public override PayloadType Type => PayloadType.Icon; public override PayloadType Type => PayloadType.Icon;
/// <summary>
/// Gets the index of the icon.
/// </summary>
[Obsolete("Use IconPayload.Icon")]
public uint IconIndex => (uint)this.Icon;
/// <summary>
/// Gets or sets the icon the payload represents.
/// </summary>
public BitmapFontIcon Icon { get; set; } = BitmapFontIcon.None;
/// <inheritdoc /> /// <inheritdoc />
protected override byte[] EncodeImpl() { public override string ToString()
{
return $"{this.Type} - {this.Icon}";
}
/// <inheritdoc />
protected override byte[] EncodeImpl()
{
var indexBytes = MakeInteger((uint)this.Icon); var indexBytes = MakeInteger((uint)this.Icon);
var chunkLen = indexBytes.Length + 1; var chunkLen = indexBytes.Length + 1;
var bytes = new List<byte>(new byte[] { var bytes = new List<byte>(new byte[]
START_BYTE, (byte)SeStringChunkType.Icon, (byte)chunkLen {
START_BYTE, (byte)SeStringChunkType.Icon, (byte)chunkLen,
}); });
bytes.AddRange(indexBytes); bytes.AddRange(indexBytes);
bytes.Add(END_BYTE); bytes.Add(END_BYTE);
@ -53,14 +73,9 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads {
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void DecodeImpl(BinaryReader reader, long endOfStream) { protected override void DecodeImpl(BinaryReader reader, long endOfStream)
Icon = (BitmapFontIcon) GetInteger(reader); {
} this.Icon = (BitmapFontIcon)GetInteger(reader);
}
/// <inheritdoc />
public override string ToString() {
return $"{Type} - {Icon}";
}
} }
} }

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