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,9 +55,8 @@ 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!");
@ -384,7 +387,7 @@ namespace Dalamud
} }
/// <summary> /// <summary>
/// Wait for a queued unload to be finalized. /// Wait for a queued unload to be finalized.
/// </summary> /// </summary>
public void WaitForUnloadFinish() public void WaitForUnloadFinish()
{ {
@ -417,7 +420,7 @@ namespace Dalamud
} }
/// <summary> /// <summary>
/// Dispose Dalamud subsystems. /// Dispose Dalamud subsystems.
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
@ -453,12 +456,11 @@ namespace Dalamud
} }
/// <summary> /// <summary>
/// Replace the built-in exception handler with a debug one. /// Replace the built-in exception handler with a debug one.
/// </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>
if (!disposing) return; internal sealed partial class DalamudSystemMenu : IDisposable
{
private bool disposed = false;
this.hookAgentHudOpenSystemMenu.Dispose(); /// <summary>
this.hookUiModuleRequestMainCommand.Dispose(); /// 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() public void Dispose()
{ {
Dispose(true); this.Dispose(true);
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
#endregion
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
private void Dispose(bool disposing)
{
if (this.disposed)
return;
if (disposing)
{
this.hookAgentHudOpenSystemMenu.Dispose();
this.hookUiModuleRequestMainCommand.Dispose();
}
this.disposed = true;
}
} }
} }

View file

@ -6,111 +6,156 @@ 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>
{"", "<:ffxive071:585847382210642069>"}, /// Chat events and public helper functions.
{"", "<:ffxive083:585848592699490329>"} /// </summary>
public class ChatHandlers
{
private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new()
{
{ "", "<:ffxive071:585847382210642069>" },
{ "", "<:ffxive083:585848592699490329>" },
}; };
private readonly Dalamud dalamud; private readonly Dictionary<XivChatType, Color> handledChatTypeColors = new()
{
private DalamudLinkPayload openInstallerWindowLink; { XivChatType.CrossParty, Color.DodgerBlue },
{ XivChatType.Party, Color.DodgerBlue },
private readonly Dictionary<XivChatType, Color> HandledChatTypeColors = new Dictionary<XivChatType, Color> { { XivChatType.FreeCompany, Color.DeepSkyBlue },
{XivChatType.CrossParty, Color.DodgerBlue}, { XivChatType.CrossLinkShell1, Color.ForestGreen },
{XivChatType.Party, Color.DodgerBlue}, { XivChatType.CrossLinkShell2, Color.ForestGreen },
{XivChatType.FreeCompany, Color.DeepSkyBlue}, { XivChatType.CrossLinkShell3, Color.ForestGreen },
{XivChatType.CrossLinkShell1, Color.ForestGreen}, { XivChatType.CrossLinkShell4, Color.ForestGreen },
{XivChatType.CrossLinkShell2, Color.ForestGreen}, { XivChatType.CrossLinkShell5, Color.ForestGreen },
{XivChatType.CrossLinkShell3, Color.ForestGreen}, { XivChatType.CrossLinkShell6, Color.ForestGreen },
{XivChatType.CrossLinkShell4, Color.ForestGreen}, { XivChatType.CrossLinkShell7, Color.ForestGreen },
{XivChatType.CrossLinkShell5, Color.ForestGreen}, { XivChatType.CrossLinkShell8, Color.ForestGreen },
{XivChatType.CrossLinkShell6, Color.ForestGreen}, { XivChatType.Ls1, Color.ForestGreen },
{XivChatType.CrossLinkShell7, Color.ForestGreen}, { XivChatType.Ls2, Color.ForestGreen },
{XivChatType.CrossLinkShell8, Color.ForestGreen}, { XivChatType.Ls3, Color.ForestGreen },
{XivChatType.Ls1, Color.ForestGreen}, { XivChatType.Ls4, Color.ForestGreen },
{XivChatType.Ls2, Color.ForestGreen}, { XivChatType.Ls5, Color.ForestGreen },
{XivChatType.Ls3, Color.ForestGreen}, { XivChatType.Ls6, Color.ForestGreen },
{XivChatType.Ls4, Color.ForestGreen}, { XivChatType.Ls7, Color.ForestGreen },
{XivChatType.Ls5, Color.ForestGreen}, { XivChatType.Ls8, Color.ForestGreen },
{XivChatType.Ls6, Color.ForestGreen}, { XivChatType.TellIncoming, Color.HotPink },
{XivChatType.Ls7, Color.ForestGreen}, { XivChatType.PvPTeam, Color.SandyBrown },
{XivChatType.Ls8, Color.ForestGreen}, { XivChatType.Urgent, Color.DarkViolet },
{XivChatType.TellIncoming, Color.HotPink}, { XivChatType.NoviceNetwork, Color.SaddleBrown },
{XivChatType.PvPTeam, Color.SandyBrown}, { XivChatType.Echo, Color.Gray },
{XivChatType.Urgent, Color.DarkViolet},
{XivChatType.NoviceNetwork, Color.SaddleBrown},
{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.OnChatMessage += OnChatMessage;
this.openInstallerWindowLink = this.dalamud.Framework.Gui.Chat.AddChatLinkHandler("Dalamud", 1001, (i, m) => { dalamud.Framework.Gui.Chat.OnCheckMessageHandled += this.OnCheckMessageHandled;
dalamud.Framework.Gui.Chat.OnChatMessage += this.OnChatMessage;
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) this.PrintWelcomeMessage();
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,10 +200,10 @@ 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));
break; break;
} }
} }
@ -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,170 +1,224 @@
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>
/// Set up the actor table collection. /// Initializes a new instance of the <see cref="ActorTable"/> class.
/// 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) {
this.ResetCache();
} }
/// <summary> /// <summary>
/// Get an actor at the specified spawn index. /// Gets the amount of currently spawned actors.
/// </summary>
public int Length => this.ActorsCache.Count;
private List<Actor> ActorsCache => this.actorsCache ??= this.GetActorTable();
/// <summary>
/// Get an actor at the specified spawn index.
/// </summary> /// </summary>
/// <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() { /// <summary>
return ActorsCache.Where(a => a != null).GetEnumerator(); /// Implementing IDisposable.
} /// </summary>
public sealed partial class ActorTable : IDisposable
IEnumerator IEnumerable.GetEnumerator() { {
return GetEnumerator(); private bool disposed = false;
}
/// <summary> /// <summary>
/// The amount of currently spawned actors. /// Finalizes an instance of the <see cref="ActorTable"/> class.
/// </summary> /// </summary>
public int Length => ActorsCache.Count; ~ActorTable() => this.Dispose(false);
int IReadOnlyCollection<Actor>.Count => Length; /// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
int ICollection.Count => Length; 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>
@ -35,12 +30,12 @@ namespace Dalamud.Game.ClientState.Actors
/// The model type of the character. /// The model type of the character.
/// </summary> /// </summary>
ModelType = 0x02, // Au Ra: changes horns/tails, everything else: seems to drastically change appearance (flip between two sets, odd/even numbers). sometimes retains hairstyle and other features ModelType = 0x02, // Au Ra: changes horns/tails, everything else: seems to drastically change appearance (flip between two sets, odd/even numbers). sometimes retains hairstyle and other features
/// <summary> /// <summary>
/// The face type of the character. /// The face type of the character.
/// </summary> /// </summary>
FaceType = 0x05, FaceType = 0x05,
/// <summary> /// <summary>
/// The hair of the character. /// The hair of the character.
/// </summary> /// </summary>
@ -50,12 +45,12 @@ namespace Dalamud.Game.ClientState.Actors
/// Whether or not the character has hair highlights. /// Whether or not the character has hair highlights.
/// </summary> /// </summary>
HasHighlights = 0x07, // negative to enable, positive to disable HasHighlights = 0x07, // negative to enable, positive to disable
/// <summary> /// <summary>
/// The skin color of the character. /// The skin color of the character.
/// </summary> /// </summary>
SkinColor = 0x08, SkinColor = 0x08,
/// <summary> /// <summary>
/// The eye color of the character. /// The eye color of the character.
/// </summary> /// </summary>
@ -125,17 +120,17 @@ namespace Dalamud.Game.ClientState.Actors
/// The race feature type of the character. /// The race feature type of the character.
/// </summary> /// </summary>
RaceFeatureType = 0x16, // negative or out of range tail shapes for race result in no tail (e.g. Au Ra has max of 4 tail shapes), incorrect value can crash client RaceFeatureType = 0x16, // negative or out of range tail shapes for race result in no tail (e.g. Au Ra has max of 4 tail shapes), incorrect value can crash client
/// <summary> /// <summary>
/// The bust size of the character. /// The bust size of the character.
/// </summary> /// </summary>
BustSize = 0x17, // char creator allows up to max of 100, i set to 127 cause who wouldnt but no visible difference BustSize = 0x17, // char creator allows up to max of 100, i set to 127 cause who wouldnt but no visible difference
/// <summary> /// <summary>
/// The face paint of the character. /// The face paint of the character.
/// </summary> /// </summary>
Facepaint = 0x18, Facepaint = 0x18,
/// <summary> /// <summary>
/// The face paint color of the character. /// The face paint color of the character.
/// </summary> /// </summary>

View file

@ -1,69 +1,83 @@
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>
None = 0x00, None = 0x00,
/// <summary> /// <summary>
/// Objects representing player characters. /// Objects representing player characters.
/// </summary> /// </summary>
Player = 0x01, Player = 0x01,
/// <summary> /// <summary>
/// Objects representing battle NPCs. /// Objects representing battle NPCs.
/// </summary> /// </summary>
BattleNpc = 0x02, BattleNpc = 0x02,
/// <summary> /// <summary>
/// Objects representing event NPCs. /// Objects representing event NPCs.
/// </summary> /// </summary>
EventNpc = 0x03, EventNpc = 0x03,
/// <summary> /// <summary>
/// Objects representing treasures. /// Objects representing treasures.
/// </summary> /// </summary>
Treasure = 0x04, Treasure = 0x04,
/// <summary> /// <summary>
/// Objects representing aetherytes. /// Objects representing aetherytes.
/// </summary> /// </summary>
Aetheryte = 0x05, Aetheryte = 0x05,
/// <summary> /// <summary>
/// Objects representing gathering points. /// Objects representing gathering points.
/// </summary> /// </summary>
GatheringPoint = 0x06, GatheringPoint = 0x06,
/// <summary> /// <summary>
/// Objects representing event objects. /// Objects representing event objects.
/// </summary> /// </summary>
EventObj = 0x07, EventObj = 0x07,
/// <summary> /// <summary>
/// Objects representing mounts. /// Objects representing mounts.
/// </summary> /// </summary>
MountType = 0x08, MountType = 0x08,
/// <summary> /// <summary>
/// Objects representing minions. /// Objects representing minions.
/// </summary> /// </summary>
Companion = 0x09, // Minion Companion = 0x09, // Minion
/// <summary> /// <summary>
/// Objects representing retainers. /// Objects representing retainers.
/// </summary> /// </summary>
Retainer = 0x0A, Retainer = 0x0A,
/// <summary>
/// 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

@ -5,13 +5,11 @@ using Dalamud.Game.ClientState.Structs;
namespace Dalamud.Game.ClientState.Actors.Types namespace Dalamud.Game.ClientState.Actors.Types
{ {
/// <summary> /// <summary>
/// This class represents a basic FFXIV actor. /// This class represents a basic FFXIV actor.
/// </summary> /// </summary>
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;
@ -102,77 +51,76 @@ namespace Dalamud.Game.ClientState
/// Provides access to the button state of gamepad buttons in game. /// Provides access to the button state of gamepad buttons in game.
/// </summary> /// </summary>
public GamepadState GamepadState; public GamepadState GamepadState;
/// <summary> /// <summary>
/// Provides access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc. /// Provides access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc.
/// </summary> /// </summary>
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
public IntPtr ActorTable { get; private set; }
//public IntPtr ViewportActorTable { get; private set; }
public IntPtr LocalContentId { get; private set; }
public IntPtr JobGaugeData { get; private set; }
public IntPtr KeyboardState { get; private set; }
public IntPtr TargetManager { get; private set; }
// Functions
public IntPtr SetupTerritoryType { get; private set; }
//public IntPtr SomeActorTableAccess { get; private set; }
//public IntPtr PartyListUpdate { get; private set; }
/// <summary> /// <summary>
/// Game function which polls the gamepads for data. /// Gets the address of the actor table.
/// /// </summary>
public IntPtr ActorTable { 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; }
/// <summary>
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { get; private set; }
/// <summary>
/// Gets the address of the keyboard state.
/// </summary>
public IntPtr KeyboardState { get; private set; }
/// <summary>
/// Gets the address of the target manager.
/// </summary>
public IntPtr TargetManager { get; private set; }
/// <summary>
/// Gets the address of the condition flag array.
/// </summary>
public IntPtr ConditionFlags { get; private set; }
// Functions
/// <summary>
/// Gets the address of the method which sets the territory type.
/// </summary>
public IntPtr SetupTerritoryType { get; private set; }
// public IntPtr SomeActorTableAccess { get; private set; }
// public IntPtr PartyListUpdate { get; private set; }
/// <summary>
/// Gets the address of the method which polls the gamepads for data.
/// Called every frame, even when `Enable Gamepad` is off in the settings. /// 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,36 +7,49 @@ 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;
internal Condition( ClientStateAddressResolver resolver ) /// <summary>
/// Initializes a new instance of the <see cref="Condition"/> class.
/// </summary>
/// <param name="resolver">The ClientStateAddressResolver instance.</param>
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
{ {
var idx = ( int )flag; var idx = (int)flag;
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>
@ -94,15 +92,15 @@ namespace Dalamud.Game.ClientState
/// Unable to execute command while performing. /// Unable to execute command while performing.
/// </summary> /// </summary>
Performing = 16, Performing = 16,
//Unknown17 = 17, // Unknown17 = 17,
//Unknown18 = 18, // Unknown18 = 18,
//Unknown19 = 19, // Unknown19 = 19,
//Unknown20 = 20, // Unknown20 = 20,
//Unknown21 = 21, // Unknown21 = 21,
//Unknown22 = 22, // Unknown22 = 22,
//Unknown23 = 23, // Unknown23 = 23,
//Unknown24 = 24, // Unknown24 = 24,
/// <summary> /// <summary>
/// Unable to execute command while occupied. /// Unable to execute command while occupied.
@ -199,8 +197,8 @@ namespace Dalamud.Game.ClientState
/// Unable to execute command while fishing. /// Unable to execute command while fishing.
/// </summary> /// </summary>
Fishing = 43, Fishing = 43,
//Unknown44 = 44, // Unknown44 = 44,
/// <summary> /// <summary>
/// Unable to execute command while between areas. /// Unable to execute command while between areas.
@ -211,8 +209,8 @@ namespace Dalamud.Game.ClientState
/// Unable to execute command while stealthed. /// Unable to execute command while stealthed.
/// </summary> /// </summary>
Stealthed = 46, Stealthed = 46,
//Unknown47 = 47, // Unknown47 = 47,
/// <summary> /// <summary>
/// Unable to execute command while jumping. /// Unable to execute command while jumping.
@ -399,8 +397,8 @@ namespace Dalamud.Game.ClientState
/// Unable to execute command while participating in a cross-world party or alliance. /// Unable to execute command while participating in a cross-world party or alliance.
/// </summary> /// </summary>
ParticipatingInCrossWorldPartyOrAlliance = 84, ParticipatingInCrossWorldPartyOrAlliance = 84,
//Unknown85 = 85, // Unknown85 = 85,
/// <summary> /// <summary>
/// Unable to execute command while playing duty record. /// Unable to execute command while playing duty record.

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);
@ -33,10 +37,10 @@ namespace Dalamud.Game.ClientState
{ {
get get
{ {
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 bool isReady = false;
// private IntPtr partyListBegin;
// private Hook<PartyListUpdateDelegate> partyListUpdateHook;
/// <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)
{
this.address = addressResolver;
this.dalamud = dalamud;
// this.partyListUpdateHook = new Hook<PartyListUpdateDelegate>(Address.PartyListUpdate, new PartyListUpdateDelegate(PartyListUpdateDetour), this);
}
private delegate long PartyListUpdateDelegate(IntPtr structBegin, long param2, char param3); private delegate long PartyListUpdateDelegate(IntPtr structBegin, long param2, char param3);
private Hook<PartyListUpdateDelegate> partyListUpdateHook; /// <summary>
private IntPtr partyListBegin; /// Gets the length of the PartyList.
private bool isReady = false; /// </summary>
public int Length => 0; // !this.isReady ? 0 : Marshal.ReadByte(this.partyListBegin + 0xF0);
public PartyList(Dalamud dalamud, ClientStateAddressResolver addressResolver) /// <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]
{ {
Address = addressResolver; get
this.dalamud = dalamud; {
//this.partyListUpdateHook = new Hook<PartyListUpdateDelegate>(Address.PartyListUpdate, new PartyListUpdateDelegate(PartyListUpdateDetour), this); 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.
get { /// </summary>
if (!this.isReady) public sealed partial class PartyList : IReadOnlyCollection<PartyMember>
return null; {
if (index >= Length) /// <inheritdoc/>
return null; int IReadOnlyCollection<PartyMember>.Count => this.Length;
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++)
{ {
array.SetValue(this[i], index); if (this[i] != null)
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>
/// Create a new CommandInfo with the provided handler.
/// </summary>
/// <param name="handler"></param>
public CommandInfo(HandlerDelegate handler) {
Handler = handler;
LoaderAssemblyName = Assembly.GetCallingAssembly()?.GetName()?.Name;
}
/// <summary>
/// Gets or sets the name of the assembly responsible for this command.
/// </summary>
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>
this.original = new byte[this.nop.Length]; /// Initializes a new instance of the <see cref="AntiDebug"/> class.
if (DebugCheckAddress != IntPtr.Zero && !IsEnabled) { /// </summary>
Log.Information($"Overwriting Debug Check @ 0x{DebugCheckAddress.ToInt64():X}"); /// <param name="scanner">The SigScanner instance.</param>
SafeMemory.ReadBytes(DebugCheckAddress, this.nop.Length, out this.original); public AntiDebug(SigScanner scanner)
SafeMemory.WriteBytes(DebugCheckAddress, this.nop); {
} else { try
Log.Information("DebugCheck already overwritten?"); {
this.debugCheckAddress = scanner.ScanText("FF 15 ?? ?? ?? ?? 85 C0 74 11 41");
}
catch (KeyNotFoundException)
{
this.debugCheckAddress = IntPtr.Zero;
} }
IsEnabled = true; Log.Verbose("DebugCheck address {DebugCheckAddress}", this.debugCheckAddress);
} }
public void Dispose() { /// <summary>
//if (this.DebugCheckAddress != IntPtr.Zero && this.original != null) /// Gets a value indicating whether the anti-debugging is enabled.
// Marshal.Copy(this.original, 0, DebugCheckAddress, this.nop.Length); /// </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];
if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled)
{
Log.Information($"Overwriting debug check @ 0x{this.debugCheckAddress.ToInt64():X}");
SafeMemory.ReadBytes(this.debugCheckAddress, this.nop.Length, out this.original);
SafeMemory.WriteBytes(this.debugCheckAddress, this.nop);
}
else
{
Log.Information("Debug check already overwritten?");
}
this.IsEnabled = true;
}
/// <summary>
/// Disable the anti-debugging by reverting the overwritten code in memory.
/// </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,57 +3,103 @@ 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) {
Setup32Bit(scanner);
} else {
Setup64Bit(scanner);
}
SetupInternal(scanner);
var className = GetType().Name; if (scanner.Is32BitProcess)
{
this.Setup32Bit(scanner);
}
else
{
this.Setup64Bit(scanner);
}
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>
/// Fetch vfunc N from a pointer to the vtable and return a delegate function pointer.
/// </summary>
/// <typeparam name="T">The delegate to marshal the function pointer to.</typeparam>
/// <param name="address">The address of the virtual table.</param>
/// <param name="vtableOffset">The offset from address to the vtable pointer.</param>
/// <param name="count">The vfunc index.</param>
/// <returns>A delegate function pointer that can be invoked.</returns>
public T GetVirtualFunction<T>(IntPtr address, int vtableOffset, int count) where T : class
{
// Get vtable
var vtable = Marshal.ReadIntPtr(address, vtableOffset);
// Get an address to the function
var functionAddress = Marshal.ReadIntPtr(vtable, IntPtr.Size * count);
return Marshal.GetDelegateForFunctionPointer<T>(functionAddress);
}
/// <summary>
/// Setup the resolver by finding any necessary memory addresses.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
protected virtual void Setup32Bit(SigScanner scanner)
{
throw new NotSupportedException("32 bit version is not supported."); throw new NotSupportedException("32 bit version is not supported.");
} }
protected virtual void Setup64Bit(SigScanner sig) { /// <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."); throw new NotSupportedException("64 bit version is not supported.");
} }
protected virtual void SetupInternal(SigScanner scanner) { /// <summary>
// Do nothing /// Setup the resolver by finding any necessary memory addresses.
} /// </summary>
/// <param name="scanner">The SigScanner instance.</param>
public T GetVirtualFunction<T>(IntPtr address, int vtableOffset, int count) where T : class { protected virtual void SetupInternal(SigScanner scanner)
// Get vtable {
var vtable = Marshal.ReadIntPtr(address, vtableOffset); // Do nothing
// Get an address to the function
var functionAddress = Marshal.ReadIntPtr(vtable, IntPtr.Size * count);
return Marshal.GetDelegateForFunctionPointer<T>(functionAddress);
} }
} }
} }

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;
private Hook<OnDestroyDetour> destroyHook;
private Hook<OnRealDestroyDelegate> realDestroyHook;
/// <summary> /// <summary>
/// A raw pointer to the instance of Client::Framework /// Gets or sets a value indicating whether the collection of stats is enabled.
/// </summary> /// </summary>
public FrameworkAddressResolver Address { get; }
#region Stats
public static bool StatsEnabled { get; set; } 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
/// <summary> /// <summary>
/// The GUI subsystem, used to access e.g. chat. /// Gets the stats history mapping.
/// </summary>
public static Dictionary<string, List<double>> StatsHistory = new();
#region Subsystems
/// <summary>
/// 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) {
this.dalamud = dalamud;
Address = new FrameworkAddressResolver();
Address.Setup(scanner);
Log.Verbose("Framework address {FrameworkAddress}", Address.BaseAddress);
if (Address.BaseAddress == IntPtr.Zero) {
throw new InvalidOperationException("Framework is not initalized yet.");
}
// Hook virtual functions
HookVTable();
// Initialize subsystems
Libc = new LibcFunction(scanner);
Gui = new GameGui(Address.GuiManager, scanner, dalamud);
Network = new GameNetwork(scanner); /// <summary>
/// Gets a raw pointer to the instance of Client::Framework.
/// </summary>
public FrameworkAddressResolver Address { get; }
/// <summary>
/// Gets or sets a value indicating whether to dispatch update events.
/// </summary>
internal bool DispatchUpdateEvents { get; set; } = true;
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
{
this.Gui.Enable();
this.Network.Enable();
this.updateHook.Enable();
this.destroyHook.Enable();
this.realDestroyHook.Enable();
} }
private void HookVTable() { /// <summary>
var vtable = Marshal.ReadIntPtr(Address.BaseAddress); /// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.Gui.Dispose();
this.Network.Dispose();
this.updateHook.Dispose();
this.destroyHook.Dispose();
this.realDestroyHook.Dispose();
}
private void HookVTable()
{
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() {
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) { 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,47 +177,62 @@ 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();
var key = $"{d.Target}::{d.Method.Name}"; var key = $"{d.Target}::{d.Method.Name}";
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
@ -30,9 +47,9 @@ namespace Dalamud.Game.Internal {
var fwDtor = scanner.ScanText("48C705????????00000000 E8???????? 488D??????0000 E8???????? 488D"); var fwDtor = scanner.ScanText("48C705????????00000000 E8???????? 488D??????0000 E8???????? 488D");
var fwOffset = Marshal.ReadInt32(fwDtor + 3); var fwOffset = Marshal.ReadInt32(fwDtor + 3);
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);
@ -146,32 +376,35 @@ namespace Dalamud.Game.Internal.Gui {
Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue); Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue);
//Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}"); // Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}");
var originalMessageData = (byte[]) message.RawData.Clone(); var originalMessageData = (byte[])message.RawData.Clone();
var oldEdited = parsedMessage.Encode(); var oldEdited = parsedMessage.Encode();
// 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)}");
} }
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,100 +460,22 @@ 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) {
Log.Error(ex, "Exception on InteractableLinkClicked hook");
} }
} catch (Exception ex)
// 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; Log.Error(ex, "Exception on InteractableLinkClicked hook");
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,97 +1,118 @@
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 .text:00000001405CD210 ; sub_140141D10+220p ...
.text:00000001405CD210 ; sub_140141D10+220p ... .text:00000001405CD210
.text:00000001405CD210 .text:00000001405CD210 var_220 = qword ptr -220h
.text:00000001405CD210 var_220 = qword ptr -220h .text:00000001405CD210 var_218 = byte ptr -218h
.text:00000001405CD210 var_218 = byte ptr -218h .text:00000001405CD210 var_210 = word ptr -210h
.text:00000001405CD210 var_210 = word ptr -210h .text:00000001405CD210 var_208 = byte ptr -208h
.text:00000001405CD210 var_208 = byte ptr -208h .text:00000001405CD210 var_200 = word ptr -200h
.text:00000001405CD210 var_200 = word ptr -200h .text:00000001405CD210 var_1FC = dword ptr -1FCh
.text:00000001405CD210 var_1FC = dword ptr -1FCh .text:00000001405CD210 var_1F8 = qword ptr -1F8h
.text:00000001405CD210 var_1F8 = qword ptr -1F8h .text:00000001405CD210 var_1F0 = qword ptr -1F0h
.text:00000001405CD210 var_1F0 = qword ptr -1F0h .text:00000001405CD210 var_1E8 = qword ptr -1E8h
.text:00000001405CD210 var_1E8 = qword ptr -1E8h .text:00000001405CD210 var_1E0 = dword ptr -1E0h
.text:00000001405CD210 var_1E0 = dword ptr -1E0h .text:00000001405CD210 var_1DC = word ptr -1DCh
.text:00000001405CD210 var_1DC = word ptr -1DCh .text:00000001405CD210 var_1DA = word ptr -1DAh
.text:00000001405CD210 var_1DA = word ptr -1DAh .text:00000001405CD210 var_1D8 = qword ptr -1D8h
.text:00000001405CD210 var_1D8 = qword ptr -1D8h .text:00000001405CD210 var_1D0 = byte ptr -1D0h
.text:00000001405CD210 var_1D0 = byte ptr -1D0h .text:00000001405CD210 var_1C8 = qword ptr -1C8h
.text:00000001405CD210 var_1C8 = qword ptr -1C8h .text:00000001405CD210 var_1B0 = dword ptr -1B0h
.text:00000001405CD210 var_1B0 = dword ptr -1B0h .text:00000001405CD210 var_1AC = dword ptr -1ACh
.text:00000001405CD210 var_1AC = dword ptr -1ACh .text:00000001405CD210 var_1A8 = dword ptr -1A8h
.text:00000001405CD210 var_1A8 = dword ptr -1A8h .text:00000001405CD210 var_1A4 = dword ptr -1A4h
.text:00000001405CD210 var_1A4 = dword ptr -1A4h .text:00000001405CD210 var_1A0 = dword ptr -1A0h
.text:00000001405CD210 var_1A0 = dword ptr -1A0h .text:00000001405CD210 var_160 = dword ptr -160h
.text:00000001405CD210 var_160 = dword ptr -160h .text:00000001405CD210 var_15C = dword ptr -15Ch
.text:00000001405CD210 var_15C = dword ptr -15Ch .text:00000001405CD210 var_140 = dword ptr -140h
.text:00000001405CD210 var_140 = dword ptr -140h .text:00000001405CD210 var_138 = dword ptr -138h
.text:00000001405CD210 var_138 = dword ptr -138h .text:00000001405CD210 var_130 = byte ptr -130h
.text:00000001405CD210 var_130 = byte ptr -130h .text:00000001405CD210 var_C0 = byte ptr -0C0h
.text:00000001405CD210 var_C0 = byte ptr -0C0h .text:00000001405CD210 var_50 = qword ptr -50h
.text:00000001405CD210 var_50 = qword ptr -50h .text:00000001405CD210 var_38 = qword ptr -38h
.text:00000001405CD210 var_38 = qword ptr -38h .text:00000001405CD210 var_30 = qword ptr -30h
.text:00000001405CD210 var_30 = qword ptr -30h .text:00000001405CD210 var_28 = qword ptr -28h
.text:00000001405CD210 var_28 = qword ptr -28h .text:00000001405CD210 var_20 = qword ptr -20h
.text:00000001405CD210 var_20 = qword ptr -20h .text:00000001405CD210 senderActorId = dword ptr 30h
.text:00000001405CD210 senderActorId = dword ptr 30h .text:00000001405CD210 isLocal = byte ptr 38h
.text:00000001405CD210 isLocal = byte ptr 38h .text:00000001405CD210
.text:00000001405CD210 .text:00000001405CD210 ; __unwind { // __GSHandlerCheck
.text:00000001405CD210 ; __unwind { // __GSHandlerCheck .text:00000001405CD210 push rbp
.text:00000001405CD210 push rbp .text:00000001405CD212 push rdi
.text:00000001405CD212 push rdi .text:00000001405CD213 push r14
.text:00000001405CD213 push r14 .text:00000001405CD215 push r15
.text:00000001405CD215 push r15 .text:00000001405CD217 lea rbp, [rsp-128h]
.text:00000001405CD217 lea rbp, [rsp-128h] .text:00000001405CD21F sub rsp, 228h
.text:00000001405CD21F sub rsp, 228h .text:00000001405CD226 mov rax, cs:__security_cookie
.text:00000001405CD226 mov rax, cs:__security_cookie .text:00000001405CD22D xor rax, rsp
.text:00000001405CD22D xor rax, rsp .text:00000001405CD230 mov [rbp+140h+var_50], rax
.text:00000001405CD230 mov [rbp+140h+var_50], rax .text:00000001405CD237 xor r10b, r10b
.text:00000001405CD237 xor r10b, r10b .text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
.text:00000001405CD23A mov [rsp+240h+var_1F8], rcx .text:00000001405CD23F xor eax, eax
.text:00000001405CD23F xor eax, eax .text:00000001405CD241 mov r11, r9
.text:00000001405CD241 mov r11, r9 .text:00000001405CD244 mov r14, r8
.text:00000001405CD244 mov r14, r8 .text:00000001405CD247 mov r9d, eax
.text:00000001405CD247 mov r9d, eax .text:00000001405CD24A movzx r15d, dx
.text:00000001405CD24A movzx r15d, dx .text:00000001405CD24E lea r8, [rcx+0C10h]
.text:00000001405CD24E lea r8, [rcx+0C10h] .text:00000001405CD255 mov rdi, rcx
.text:00000001405CD255 mov rdi, rcx
*/ */
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 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("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"); /// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
// PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1???
this.PrintMessage = sig.ScanText("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
//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"); // PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33");
//PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? 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 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 ?? ?? ?? FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9; // PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0
this.PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? ?? FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9;
} }
} }
} }

View file

@ -1,292 +1,218 @@
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
{ {
private readonly Dalamud dalamud; /// <summary>
/// The delegate of the native method that gets the Client::UI::UIModule address.
private GameGuiAddressResolver Address { get; } /// </summary>
/// <returns>The Client::UI::UIModule address.</returns>
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;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleItemHoverDelegate(IntPtr hoverState, IntPtr a2, IntPtr a3, ulong a4);
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;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void HandleActionHoverDelegate(IntPtr hoverState, HoverActionKind a2, uint a3, int a4, byte a5);
private readonly Hook<HandleActionHoverDelegate> handleActionHoverHook;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr HandleActionOutDelegate(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4);
private Hook<HandleActionOutDelegate> handleActionOutHook;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr GetUIObjectDelegate();
private readonly GetUIObjectDelegate getUIObject;
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr GetUIMapObjectDelegate(IntPtr UIObject);
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; public readonly GetBaseUIObjectDelegate GetBaseUIObject;
[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)] private readonly Dalamud dalamud;
private delegate IntPtr GetUIObjectByNameDelegate(IntPtr thisPtr, string uiName, int index); private readonly GameGuiAddressResolver address;
private readonly GetMatrixSingletonDelegate getMatrixSingleton;
private readonly GetUIObjectDelegate getUIObject;
private readonly ScreenToWorldNativeDelegate screenToWorldNative;
private readonly GetUIObjectByNameDelegate getUIObjectByName; private readonly GetUIObjectByNameDelegate getUIObjectByName;
private delegate IntPtr GetUiModuleDelegate(IntPtr basePtr);
private readonly GetUiModuleDelegate getUiModule; private readonly GetUiModuleDelegate getUiModule;
private readonly GetAgentModuleDelegate getAgentModule;
private delegate IntPtr GetAgentModuleDelegate(IntPtr uiModule); private readonly Hook<SetGlobalBgmDelegate> setGlobalBgmHook;
private GetAgentModuleDelegate getAgentModule; private readonly Hook<HandleItemHoverDelegate> handleItemHoverHook;
private readonly Hook<HandleItemOutDelegate> handleItemOutHook;
private readonly Hook<HandleActionHoverDelegate> handleActionHoverHook;
private readonly Hook<HandleActionOutDelegate> handleActionOutHook;
private readonly Hook<ToggleUiHideDelegate> toggleUiHideHook;
public bool GameUiHidden { get; private set; } private GetUIMapObjectDelegate getUIMapObject;
private OpenMapWithFlagDelegate openMapWithFlag;
/// <summary> /// <summary>
/// Event which is fired when the game UI hiding is toggled. /// 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> /// </summary>
public event EventHandler<bool> OnUiHideToggled; /// <param name="baseAddress">The base address of the native GuiManager class.</param>
/// <param name="scanner">The SigScanner instance.</param>
/// <summary> /// <param name="dalamud">The Dalamud instance.</param>
/// 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>
public ulong HoveredItem { get; set; }
/// <summary>
/// The action ID that is current hovered by the player. 0 when no action is hovered.
/// </summary>
public HoveredAction HoveredAction { get; } = new HoveredAction();
/// <summary>
/// Event that is fired when the currently hovered item changes.
/// </summary>
public EventHandler<ulong> HoveredItemChanged { get; set; }
/// <summary>
/// Event that is fired when the currently hovered action changes.
/// </summary>
public EventHandler<HoveredAction> HoveredActionChanged { get; set; }
public GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud) public GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{ {
this.dalamud = dalamud; this.dalamud = dalamud;
Address = new GameGuiAddressResolver(baseAddress); this.address = new GameGuiAddressResolver(baseAddress);
Address.Setup(scanner); this.address.Setup(scanner);
Log.Verbose("===== G A M E G U I ====="); Log.Verbose("===== G A M E G U I =====");
Log.Verbose("GameGuiManager address {Address}", Address.BaseAddress); Log.Verbose("GameGuiManager address {Address:X}", this.address.BaseAddress.ToInt64());
Log.Verbose("SetGlobalBgm address {Address}", Address.SetGlobalBgm); Log.Verbose("SetGlobalBgm address {Address:X}", this.address.SetGlobalBgm.ToInt64());
Log.Verbose("HandleItemHover address {Address}", Address.HandleItemHover); Log.Verbose("HandleItemHover address {Address:X}", this.address.HandleItemHover.ToInt64());
Log.Verbose("HandleItemOut address {Address}", Address.HandleItemOut); Log.Verbose("HandleItemOut address {Address:X}", this.address.HandleItemOut.ToInt64());
Log.Verbose("GetUIObject address {Address}", Address.GetUIObject); Log.Verbose("GetUIObject address {Address:X}", this.address.GetUIObject.ToInt64());
Log.Verbose("GetAgentModule address {Address}", Address.GetAgentModule); Log.Verbose("GetAgentModule address {Address:X}", this.address.GetAgentModule.ToInt64());
Chat = new ChatGui(Address.ChatManager, scanner, dalamud); this.Chat = new ChatGui(this.address.ChatManager, scanner, dalamud);
PartyFinder = new PartyFinderGui(scanner, dalamud); this.PartyFinder = new PartyFinderGui(scanner, dalamud);
Toast = new ToastGui(scanner, dalamud); this.Toast = new ToastGui(scanner, dalamud);
this.setGlobalBgmHook = this.setGlobalBgmHook = new Hook<SetGlobalBgmDelegate>(this.address.SetGlobalBgm, new SetGlobalBgmDelegate(this.HandleSetGlobalBgmDetour), this);
new Hook<SetGlobalBgmDelegate>(Address.SetGlobalBgm, this.handleItemHoverHook = new Hook<HandleItemHoverDelegate>(this.address.HandleItemHover, new HandleItemHoverDelegate(this.HandleItemHoverDetour), this);
new SetGlobalBgmDelegate(HandleSetGlobalBgmDetour),
this);
this.handleItemHoverHook =
new Hook<HandleItemHoverDelegate>(Address.HandleItemHover,
new HandleItemHoverDelegate(HandleItemHoverDetour),
this);
this.handleItemOutHook = this.handleItemOutHook = new Hook<HandleItemOutDelegate>(this.address.HandleItemOut, new HandleItemOutDelegate(this.HandleItemOutDetour), this);
new Hook<HandleItemOutDelegate>(Address.HandleItemOut,
new HandleItemOutDelegate(HandleItemOutDetour),
this);
this.handleActionHoverHook = this.handleActionHoverHook = new Hook<HandleActionHoverDelegate>(this.address.HandleActionHover, new HandleActionHoverDelegate(this.HandleActionHoverDetour), this);
new Hook<HandleActionHoverDelegate>(Address.HandleActionHover, this.handleActionOutHook = new Hook<HandleActionOutDelegate>(this.address.HandleActionOut, new HandleActionOutDelegate(this.HandleActionOutDetour), this);
new HandleActionHoverDelegate(HandleActionHoverDetour),
this);
this.handleActionOutHook =
new Hook<HandleActionOutDelegate>(Address.HandleActionOut,
new HandleActionOutDelegate(HandleActionOutDetour),
this);
this.getUIObject = Marshal.GetDelegateForFunctionPointer<GetUIObjectDelegate>(Address.GetUIObject);
this.getMatrixSingleton = this.getUIObject = Marshal.GetDelegateForFunctionPointer<GetUIObjectDelegate>(this.address.GetUIObject);
Marshal.GetDelegateForFunctionPointer<GetMatrixSingletonDelegate>(Address.GetMatrixSingleton);
this.screenToWorldNative = this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer<GetMatrixSingletonDelegate>(this.address.GetMatrixSingleton);
Marshal.GetDelegateForFunctionPointer<ScreenToWorldNativeDelegate>(Address.ScreenToWorld);
this.toggleUiHideHook = new Hook<ToggleUiHideDelegate>(Address.ToggleUiHide, new ToggleUiHideDelegate(ToggleUiHideDetour), this); this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer<ScreenToWorldNativeDelegate>(this.address.ScreenToWorld);
this.GetBaseUIObject = Marshal.GetDelegateForFunctionPointer<GetBaseUIObjectDelegate>(Address.GetBaseUIObject); this.toggleUiHideHook = new Hook<ToggleUiHideDelegate>(this.address.ToggleUiHide, new ToggleUiHideDelegate(this.ToggleUiHideDetour), this);
this.getUIObjectByName = Marshal.GetDelegateForFunctionPointer<GetUIObjectByNameDelegate>(Address.GetUIObjectByName);
this.getUiModule = Marshal.GetDelegateForFunctionPointer<GetUiModuleDelegate>(Address.GetUIModule); this.GetBaseUIObject = Marshal.GetDelegateForFunctionPointer<GetBaseUIObjectDelegate>(this.address.GetBaseUIObject);
this.getAgentModule = Marshal.GetDelegateForFunctionPointer<GetAgentModuleDelegate>(Address.GetAgentModule); this.getUIObjectByName = Marshal.GetDelegateForFunctionPointer<GetUIObjectByNameDelegate>(this.address.GetUIObjectByName);
this.getUiModule = Marshal.GetDelegateForFunctionPointer<GetUiModuleDelegate>(this.address.GetUIModule);
this.getAgentModule = Marshal.GetDelegateForFunctionPointer<GetAgentModuleDelegate>(this.address.GetAgentModule);
} }
private IntPtr HandleSetGlobalBgmDetour(UInt16 bgmKey, byte a2, UInt32 a3, UInt32 a4, UInt32 a5, byte a6) { // Marshaled delegates
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 /// The delegate type of the native method that gets the Client::UI::UIModule address.
/// </summary> /// </summary>
/// <param name="mapLink">Link to the map to be opened</param> /// <returns>The Client::UI::UIModule address.</returns>
/// <returns>True if there were no errors and it could open the map</returns> [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public bool OpenMapWithMapLink(MapLinkPayload mapLink) { public delegate IntPtr GetBaseUIObjectDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr GetMatrixSingletonDelegate();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate IntPtr GetUIObjectDelegate();
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private unsafe delegate bool ScreenToWorldNativeDelegate(float* camPos, float* clipPos, float rayDistance, float* worldPos, int* unknown);
[UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
private delegate IntPtr GetUIObjectByNameDelegate(IntPtr thisPtr, string uiName, int index);
private delegate IntPtr GetUiModuleDelegate(IntPtr basePtr);
private delegate IntPtr GetAgentModuleDelegate(IntPtr uiModule);
// 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>
/// Event which is fired when the game UI hiding is toggled.
/// </summary>
public event EventHandler<bool> OnUiHideToggled;
/// <summary>
/// Gets the <see cref="Chat"/> instance.
/// </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>
public ulong HoveredItem { get; set; }
/// <summary>
/// Gets the action ID that is current hovered by the player. 0 when no action is hovered.
/// </summary>
public HoveredAction HoveredAction { get; } = new HoveredAction();
/// <summary>
/// Gets or sets the event that is fired when the currently hovered item changes.
/// </summary>
public EventHandler<ulong> HoveredItemChanged { get; set; }
/// <summary>
/// Gets or sets the event that is fired when the currently hovered action changes.
/// </summary>
public EventHandler<HoveredAction> HoveredActionChanged { get; set; }
/// <summary>
/// Opens the in-game map with a flag on the location of the parameter.
/// </summary>
/// <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>
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,35 +224,36 @@ 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++)
viewProjectionMatrix[i] = *rawMatrix; viewProjectionMatrix[i] = *rawMatrix;
width = *rawMatrix; width = *rawMatrix;
height = *(rawMatrix + 1); height = *(rawMatrix + 1);
} }
Vector3.Transform( ref worldPos, ref viewProjectionMatrix, out Vector3 pCoords); Vector3.Transform(ref worldPos, ref viewProjectionMatrix, out Vector3 pCoords);
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,14 +301,15 @@ 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);
screenPos3D.Z = 1; screenPos3D.Z = 1;
Vector3.TransformCoordinate(ref screenPos3D, ref viewProjectionMatrix, out var camPosOne); Vector3.TransformCoordinate(ref screenPos3D, ref viewProjectionMatrix, out var camPosOne);
@ -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;
} }
public void SetBgm(ushort bgmKey) => this.setGlobalBgmHook.Original(bgmKey, 0, 0, 0, 0, 0); /// <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 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> /// </summary>
public enum HoverActionKind { public enum HoverActionKind
{
/// <summary>
/// No action is hovered.
/// </summary>
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>
/// This class represents the hotbar action currently hovered over by the cursor.
/// </summary>
public class HoveredAction
{
/// <summary> /// <summary>
/// The base action ID /// 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 /// <summary>
/// This class handles interacting with the native PartyFinder window.
public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args); /// </summary>
public sealed class PartyFinderGui : IDisposable
/// <summary> {
/// Event fired each time the game receives an individual Party Finder listing. Cannot modify listings but can private readonly Dalamud dalamud;
/// hide them. private readonly PartyFinderAddressResolver address;
/// </summary> private readonly IntPtr memory;
public event PartyFinderListingEventDelegate ReceiveListing;
#endregion
#region Hooks
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>
/// A player slot in a Party Finder listing.
/// </summary>
public class PartyFinderSlot {
private readonly uint accepting;
private JobFlags[] listAccepting;
/// <summary>
/// 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> /// <summary>
/// Tests if this slot is accepting a job. /// PartyFinder packet constants.
/// </summary> /// </summary>
/// <param name="flag">Job to test</param> public static class PacketInfo
public bool this[JobFlags flag] => (this.accepting & (uint) flag) > 0; {
/// <summary>
internal PartyFinderSlot(uint accepting) { /// The size of the PartyFinder packet.
this.accepting = accepting; /// </summary>
public static readonly int PacketSize = Marshal.SizeOf<Packet>();
} }
} }
[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];
@ -107,7 +157,7 @@ namespace Dalamud.Game.Internal.Gui
var bytes = new List<byte>(); var bytes = new List<byte>();
unsafe unsafe
{ {
var ptr = (byte*) text; var ptr = (byte*)text;
while (*ptr != 0) while (*ptr != 0)
{ {
bytes.Add(*ptr); bytes.Add(*ptr);
@ -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);
@ -180,104 +210,11 @@ namespace Dalamud.Game.Internal.Gui
{ {
fixed (byte* ptr = terminated) fixed (byte* ptr = terminated)
{ {
this.HandleNormalToastDetour(manager, (IntPtr) ptr, 5, (byte) options.Position, (byte) options.Speed, 0); this.HandleNormalToastDetour(manager, (IntPtr)ptr, 5, (byte)options.Position, (byte)options.Speed, 0);
} }
} }
} }
#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)
@ -290,8 +227,8 @@ namespace Dalamud.Game.Internal.Gui
var str = this.ParseString(text); var str = this.ParseString(text);
var options = new ToastOptions var options = new ToastOptions
{ {
Position = (ToastPosition) isTop, Position = (ToastPosition)isTop,
Speed = (ToastSpeed) isFast, Speed = (ToastSpeed)isFast,
}; };
this.OnToast?.Invoke(ref str, ref options, ref isHandled); this.OnToast?.Invoke(ref str, ref options, ref isHandled);
@ -308,7 +245,62 @@ namespace Dalamud.Game.Internal.Gui
{ {
fixed (byte* message = terminated) fixed (byte* message = terminated)
{ {
return this.showNormalToastHook.Original(manager, (IntPtr) message, layer, (byte) options.Position, (byte) options.Speed, logMessageId); return this.showNormalToastHook.Original(manager, (IntPtr)message, layer, (byte)options.Position, (byte)options.Speed, logMessageId);
}
}
}
}
/// <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);
} }
} }
} }
@ -325,7 +317,7 @@ namespace Dalamud.Game.Internal.Gui
var str = this.ParseString(text); var str = this.ParseString(text);
var options = new QuestToastOptions var options = new QuestToastOptions
{ {
Position = (QuestToastPosition) position, Position = (QuestToastPosition)position,
DisplayCheckmark = iconOrCheck1 == QuestToastCheckmarkMagic, DisplayCheckmark = iconOrCheck1 == QuestToastCheckmarkMagic,
IconId = iconOrCheck1 == QuestToastCheckmarkMagic ? iconOrCheck2 : iconOrCheck1, IconId = iconOrCheck1 == QuestToastCheckmarkMagic ? iconOrCheck2 : iconOrCheck1,
PlaySound = playSound == 1, PlaySound = playSound == 1,
@ -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
{ {
@ -349,16 +341,63 @@ namespace Dalamud.Game.Internal.Gui
{ {
return this.showQuestToastHook.Original( return this.showQuestToastHook.Original(
manager, manager,
(int) options.Position, (int)options.Position,
(IntPtr) message, (IntPtr)message,
ioc1, ioc1,
options.PlaySound ? (byte) 1 : (byte) 0, options.PlaySound ? (byte)1 : (byte)0,
ioc2, ioc2,
0); 0);
} }
} }
} }
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)
@ -384,7 +423,7 @@ namespace Dalamud.Game.Internal.Gui
{ {
fixed (byte* message = terminated) fixed (byte* message = terminated)
{ {
return this.showErrorToastHook.Original(manager, (IntPtr) message, respectsHidingMaybe); return this.showErrorToastHook.Original(manager, (IntPtr)message, respectsHidingMaybe);
} }
} }
} }

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,45 +1,65 @@
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);
// 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 StdStringDeallocateDelegate(IntPtr address); private delegate IntPtr StdStringDeallocateDelegate(IntPtr address);
private LibcFunctionAddressResolver Address { get; }
private readonly StdStringFromCStringDelegate stdStringCtorCString; /// <summary>
private readonly StdStringDeallocateDelegate stdStringDeallocate; /// Create a new string from the given bytes.
/// </summary>
public LibcFunction(SigScanner scanner) { /// <param name="content">The bytes to convert.</param>
Address = new LibcFunctionAddressResolver(); /// <returns>An owned std string object.</returns>
Address.Setup(scanner); public OwnedStdString NewString(byte[] content)
{
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);
// Initialize a string // Initialize a string
var size = new IntPtr(content.Length); var size = new IntPtr(content.Length);
var pReallocString = this.stdStringCtorCString(pString, content, size); var pReallocString = this.stdStringCtorCString(pString, content, size);
//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 { namespace Dalamud.Game.Internal.Libc
public sealed class OwnedStdString : IDisposable { {
internal delegate void DeallocatorDelegate(IntPtr address); /// <summary>
/// An address wrapper around the <see cref="StdString"/> class.
// ala. the drop flag /// </summary>
private bool isDisposed; 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);
}
/// <summary>
/// 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);
} }
private void ReleaseUnmanagedResources() { /// <summary>
if (Address == IntPtr.Zero) { /// 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,13 +132,13 @@ 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
// TODO: Implement actor IDs // TODO: Implement actor IDs
this.OnNetworkMessage?.Invoke(dataPtr + 0x20, (ushort) Marshal.ReadInt16(dataPtr), 0x0, 0x0, NetworkMessageDirection.ZoneUp); this.OnNetworkMessage?.Invoke(dataPtr + 0x20, (ushort)Marshal.ReadInt16(dataPtr), 0x0, 0x0, NetworkMessageDirection.ZoneUp);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -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/>
//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"); 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 ?? ?? 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"); // 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");
ProcessZonePacketUp = // 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");
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 ?? ?? ?? ??"); this.ProcessZonePacketDown = sig.ScanText("48 89 5C 24 ?? 56 48 83 EC 50 8B F2");
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 ?? ?? ?? ??");
} }
} }
} }

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.getResourceSyncHook = new Hook<GetResourceSyncDelegate>(this.address.GetResourceSync, new GetResourceSyncDelegate(this.GetResourceSyncDetour), this);
this);
this.getResourceSyncHook =
new Hook<GetResourceSyncDelegate>(Address.GetResourceSync,
new GetResourceSyncDelegate(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) {
try { private IntPtr GetResourceAsyncDetour(IntPtr manager, IntPtr a2, IntPtr a3, IntPtr a4, IntPtr pathPtr, IntPtr a6, byte a7)
var path = Marshal.PtrToStringAnsi(a5); {
try
{
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,20 +1,28 @@
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"); {
//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"); 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");
} }
} }
} }

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,117 +1,140 @@
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";
private const string ApiKey = "GGD6RdSfGyRiHM5WDnAo0Nj9Nv7aC5NDhMj3BebT"; private const string ApiKey = "GGD6RdSfGyRiHM5WDnAo0Nj9Nv7aC5NDhMj3BebT";
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)
client.Headers.Add(HttpRequestHeader.ContentType, "application/json"); {
using var client = new WebClient();
Log.Verbose("Starting Universalis upload."); client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
var uploader = this.dalamud.ClientState.LocalContentId;
var listingsRequestObject = new UniversalisItemListingsUploadRequest(); Log.Verbose("Starting Universalis upload.");
listingsRequestObject.WorldId = this.dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0; var uploader = this.dalamud.ClientState.LocalContentId;
listingsRequestObject.UploaderId = uploader.ToString();
listingsRequestObject.ItemId = request.CatalogId;
listingsRequestObject.Listings = new List<UniversalisItemListingsEntry>(); var listingsRequestObject = new UniversalisItemListingsUploadRequest();
foreach (var marketBoardItemListing in request.Listings) { listingsRequestObject.WorldId = this.dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0;
var universalisListing = new UniversalisItemListingsEntry { listingsRequestObject.UploaderId = uploader.ToString();
Hq = marketBoardItemListing.IsHq, listingsRequestObject.ItemId = request.CatalogId;
SellerId = marketBoardItemListing.RetainerOwnerId.ToString(),
RetainerName = marketBoardItemListing.RetainerName,
RetainerId = marketBoardItemListing.RetainerId.ToString(),
CreatorId = marketBoardItemListing.ArtisanId.ToString(),
CreatorName = marketBoardItemListing.PlayerName,
OnMannequin = marketBoardItemListing.OnMannequin,
LastReviewTime = ((DateTimeOffset) marketBoardItemListing.LastReviewTime).ToUnixTimeSeconds(),
PricePerUnit = marketBoardItemListing.PricePerUnit,
Quantity = marketBoardItemListing.ItemQuantity,
RetainerCity = marketBoardItemListing.RetainerCityId
};
universalisListing.Materia = new List<UniversalisItemMateria>(); listingsRequestObject.Listings = new List<UniversalisItemListingsEntry>();
foreach (var itemMateria in marketBoardItemListing.Materia) foreach (var marketBoardItemListing in request.Listings)
universalisListing.Materia.Add(new UniversalisItemMateria {
MateriaId = itemMateria.MateriaId,
SlotId = itemMateria.Index
});
listingsRequestObject.Listings.Add(universalisListing);
}
var upload = JsonConvert.SerializeObject(listingsRequestObject);
client.UploadString(ApiBase + $"/upload/{ApiKey}", "POST", upload);
Log.Verbose(upload);
var historyRequestObject = new UniversalisHistoryUploadRequest();
historyRequestObject.WorldId = this.dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0;
historyRequestObject.UploaderId = uploader.ToString();
historyRequestObject.ItemId = request.CatalogId;
historyRequestObject.Entries = new List<UniversalisHistoryEntry>();
foreach (var marketBoardHistoryListing in request.History)
historyRequestObject.Entries.Add(new UniversalisHistoryEntry {
BuyerName = marketBoardHistoryListing.BuyerName,
Hq = marketBoardHistoryListing.IsHq,
OnMannequin = marketBoardHistoryListing.OnMannequin,
PricePerUnit = marketBoardHistoryListing.SalePrice,
Quantity = marketBoardHistoryListing.Quantity,
Timestamp = ((DateTimeOffset) marketBoardHistoryListing.PurchaseTime).ToUnixTimeSeconds()
});
client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
var historyUpload = JsonConvert.SerializeObject(historyRequestObject);
client.UploadString(ApiBase + $"/upload/{ApiKey}", "POST", historyUpload);
Log.Verbose(historyUpload);
Log.Verbose("Universalis data upload for item#{0} completed.", request.CatalogId);
}
}
public void UploadTax(MarketTaxRates taxRates) {
using (var client = new WebClient())
{ {
var taxRatesRequest = new UniversalisTaxUploadRequest(); var universalisListing = new UniversalisItemListingsEntry
taxRatesRequest.WorldId = this.dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0; {
taxRatesRequest.UploaderId = this.dalamud.ClientState.LocalContentId.ToString(); Hq = marketBoardItemListing.IsHq,
SellerId = marketBoardItemListing.RetainerOwnerId.ToString(),
taxRatesRequest.TaxData = new UniversalisTaxData { RetainerName = marketBoardItemListing.RetainerName,
LimsaLominsa = taxRates.LimsaLominsaTax, RetainerId = marketBoardItemListing.RetainerId.ToString(),
Gridania = taxRates.GridaniaTax, CreatorId = marketBoardItemListing.ArtisanId.ToString(),
Uldah = taxRates.UldahTax, CreatorName = marketBoardItemListing.PlayerName,
Ishgard = taxRates.IshgardTax, OnMannequin = marketBoardItemListing.OnMannequin,
Kugane = taxRates.KuganeTax, LastReviewTime = ((DateTimeOffset)marketBoardItemListing.LastReviewTime).ToUnixTimeSeconds(),
Crystarium = taxRates.CrystariumTax PricePerUnit = marketBoardItemListing.PricePerUnit,
Quantity = marketBoardItemListing.ItemQuantity,
RetainerCity = marketBoardItemListing.RetainerCityId,
}; };
client.Headers.Add(HttpRequestHeader.ContentType, "application/json"); universalisListing.Materia = new List<UniversalisItemMateria>();
foreach (var itemMateria in marketBoardItemListing.Materia)
{
universalisListing.Materia.Add(new UniversalisItemMateria
{
MateriaId = itemMateria.MateriaId,
SlotId = itemMateria.Index,
});
}
var historyUpload = JsonConvert.SerializeObject(taxRatesRequest); listingsRequestObject.Listings.Add(universalisListing);
client.UploadString(ApiBase + $"/upload/{ApiKey}", "POST", historyUpload);
Log.Verbose(historyUpload);
Log.Verbose("Universalis tax upload completed.");
} }
var upload = JsonConvert.SerializeObject(listingsRequestObject);
client.UploadString(ApiBase + $"/upload/{ApiKey}", "POST", upload);
Log.Verbose(upload);
var historyRequestObject = new UniversalisHistoryUploadRequest();
historyRequestObject.WorldId = this.dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0;
historyRequestObject.UploaderId = uploader.ToString();
historyRequestObject.ItemId = request.CatalogId;
historyRequestObject.Entries = new List<UniversalisHistoryEntry>();
foreach (var marketBoardHistoryListing in request.History)
{
historyRequestObject.Entries.Add(new UniversalisHistoryEntry
{
BuyerName = marketBoardHistoryListing.BuyerName,
Hq = marketBoardHistoryListing.IsHq,
OnMannequin = marketBoardHistoryListing.OnMannequin,
PricePerUnit = marketBoardHistoryListing.SalePrice,
Quantity = marketBoardHistoryListing.Quantity,
Timestamp = ((DateTimeOffset)marketBoardHistoryListing.PurchaseTime).ToUnixTimeSeconds(),
});
}
client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
var historyUpload = JsonConvert.SerializeObject(historyRequestObject);
client.UploadString(ApiBase + $"/upload/{ApiKey}", "POST", historyUpload);
Log.Verbose(historyUpload);
Log.Verbose("Universalis data upload for item#{0} completed.", request.CatalogId);
}
/// <inheritdoc/>
public void UploadTax(MarketTaxRates taxRates)
{
using var client = new WebClient();
var taxRatesRequest = new UniversalisTaxUploadRequest();
taxRatesRequest.WorldId = this.dalamud.ClientState.LocalPlayer?.CurrentWorld.Id ?? 0;
taxRatesRequest.UploaderId = this.dalamud.ClientState.LocalContentId.ToString();
taxRatesRequest.TaxData = new UniversalisTaxData
{
LimsaLominsa = taxRates.LimsaLominsaTax,
Gridania = taxRates.GridaniaTax,
Uldah = taxRates.UldahTax,
Ishgard = taxRates.IshgardTax,
Kugane = taxRates.KuganeTax,
Crystarium = taxRates.CrystariumTax,
};
client.Headers.Add(HttpRequestHeader.ContentType, "application/json");
var historyUpload = JsonConvert.SerializeObject(taxRatesRequest);
client.UploadString(ApiBase + $"/upload/{ApiKey}", "POST", historyUpload);
Log.Verbose(historyUpload);
Log.Verbose("Universalis tax upload completed.");
} }
} }
} }

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> /// <summary>
/// Event which gets fired when a duty is ready. /// Initializes a new instance of the <see cref="NetworkHandlers"/> class.
/// </summary> /// </summary>
public event EventHandler<ContentFinderCondition> CfPop; /// <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) { public NetworkHandlers(Dalamud dalamud, bool optOutMbUploads)
{
this.dalamud = dalamud; this.dalamud = dalamud;
this.optOutMbUploads = optOutMbUploads; this.optOutMbUploads = optOutMbUploads;
this.uploader = new UniversalisMarketBoardUploader(dalamud); this.uploader = new UniversalisMarketBoardUploader(dalamud);
dalamud.Framework.Network.OnNetworkMessage += OnNetworkMessage; dalamud.Framework.Network.OnNetworkMessage += this.OnNetworkMessage;
} }
private void OnNetworkMessage(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) { /// <summary>
/// Event which gets fired when a duty is ready.
/// </summary>
public event EventHandler<ContentFinderCondition> CfPop;
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 {
var flashInfo = new NativeFunctions.FlashWindowInfo
{ {
cbSize = (uint)Marshal.SizeOf<NativeFunctions.FLASHWINFO>(), 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"]) { {
var catalogId = (uint) Marshal.ReadInt32(dataPtr); if (opCode == this.dalamud.Data.ServerOpCodes["MarketBoardItemRequestStart"])
{
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,69 +17,66 @@ 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);
output.ItemListings = new List<MarketBoardItemListing>();
for (var i = 0; i < 10; i++)
{ {
using (var reader = new BinaryReader(stream)) var listingEntry = new MarketBoardItemListing();
listingEntry.ListingId = reader.ReadUInt64();
listingEntry.RetainerId = reader.ReadUInt64();
listingEntry.RetainerOwnerId = reader.ReadUInt64();
listingEntry.ArtisanId = reader.ReadUInt64();
listingEntry.PricePerUnit = reader.ReadUInt32();
listingEntry.TotalTax = reader.ReadUInt32();
listingEntry.ItemQuantity = reader.ReadUInt32();
listingEntry.CatalogId = reader.ReadUInt32();
listingEntry.LastReviewTime = DateTimeOffset.UtcNow.AddSeconds(-reader.ReadUInt16()).DateTime;
reader.ReadUInt16(); // container
reader.ReadUInt32(); // slot
reader.ReadUInt16(); // durability
reader.ReadUInt16(); // spiritbond
listingEntry.Materia = new List<MarketBoardItemListing.ItemMateria>();
for (var materiaIndex = 0; materiaIndex < 5; materiaIndex++)
{ {
output.ItemListings = new List<MarketBoardItemListing>(); var materiaVal = reader.ReadUInt16();
for (var i = 0; i < 10; i++) var materiaEntry = new MarketBoardItemListing.ItemMateria();
{ materiaEntry.MateriaId = (materiaVal & 0xFF0) >> 4;
var listingEntry = new MarketBoardItemListing(); materiaEntry.Index = materiaVal & 0xF;
listingEntry.ListingId = reader.ReadUInt64(); if (materiaEntry.MateriaId != 0)
listingEntry.RetainerId = reader.ReadUInt64(); listingEntry.Materia.Add(materiaEntry);
listingEntry.RetainerOwnerId = reader.ReadUInt64();
listingEntry.ArtisanId = reader.ReadUInt64();
listingEntry.PricePerUnit = reader.ReadUInt32();
listingEntry.TotalTax = reader.ReadUInt32();
listingEntry.ItemQuantity = reader.ReadUInt32();
listingEntry.CatalogId = reader.ReadUInt32();
listingEntry.LastReviewTime = DateTimeOffset.UtcNow.AddSeconds(-reader.ReadUInt16()).DateTime;
reader.ReadUInt16(); // container
reader.ReadUInt32(); // slot
reader.ReadUInt16(); // durability
reader.ReadUInt16(); // spiritbond
listingEntry.Materia = new List<MarketBoardItemListing.ItemMateria>();
for (var materiaIndex = 0; materiaIndex < 5; materiaIndex++)
{
var materiaVal = reader.ReadUInt16();
var materiaEntry = new MarketBoardItemListing.ItemMateria();
materiaEntry.MateriaId = (materiaVal & 0xFF0) >> 4;
materiaEntry.Index = materiaVal & 0xF;
if (materiaEntry.MateriaId != 0)
listingEntry.Materia.Add(materiaEntry);
}
reader.ReadUInt16();
reader.ReadUInt32();
listingEntry.RetainerName = Encoding.UTF8.GetString(reader.ReadBytes(32)).TrimEnd('\u0000');
listingEntry.PlayerName = Encoding.UTF8.GetString(reader.ReadBytes(32)).TrimEnd('\u0000');
listingEntry.IsHq = reader.ReadBoolean();
listingEntry.MateriaCount = reader.ReadByte();
listingEntry.OnMannequin = reader.ReadBoolean();
listingEntry.RetainerCityId = reader.ReadByte();
listingEntry.StainId = reader.ReadUInt16();
reader.ReadUInt16();
reader.ReadUInt32();
if (listingEntry.CatalogId != 0)
output.ItemListings.Add(listingEntry);
}
output.ListingIndexEnd = reader.ReadByte();
output.ListingIndexStart = reader.ReadByte();
output.RequestId = reader.ReadUInt16();
} }
reader.ReadUInt16();
reader.ReadUInt32();
listingEntry.RetainerName = Encoding.UTF8.GetString(reader.ReadBytes(32)).TrimEnd('\u0000');
listingEntry.PlayerName = Encoding.UTF8.GetString(reader.ReadBytes(32)).TrimEnd('\u0000');
listingEntry.IsHq = reader.ReadBoolean();
listingEntry.MateriaCount = reader.ReadByte();
listingEntry.OnMannequin = reader.ReadBoolean();
listingEntry.RetainerCityId = reader.ReadByte();
listingEntry.StainId = reader.ReadUInt16();
reader.ReadUInt16();
reader.ReadUInt32();
if (listingEntry.CatalogId != 0)
output.ItemListings.Add(listingEntry);
} }
output.ListingIndexEnd = reader.ReadByte();
output.ListingIndexStart = reader.ReadByte();
output.RequestId = reader.ReadUInt16();
return output; return output;
} }

View file

@ -3,47 +3,52 @@ 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.CatalogId2 = reader.ReadUInt32();
output.HistoryListings = new List<MarketBoardHistoryListing>(); output.CatalogId = reader.ReadUInt32();
output.CatalogId2 = reader.ReadUInt32();
for (var i = 0; i < 10; i++) { output.HistoryListings = new List<MarketBoardHistoryListing>();
var listingEntry = new MarketBoardHistoryListing();
listingEntry.SalePrice = reader.ReadUInt32(); for (var i = 0; i < 10; i++)
listingEntry.PurchaseTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadUInt32()).UtcDateTime; {
listingEntry.Quantity = reader.ReadUInt32(); var listingEntry = new MarketBoardHistoryListing
listingEntry.IsHq = reader.ReadBoolean(); {
SalePrice = reader.ReadUInt32(),
PurchaseTime = DateTimeOffset.FromUnixTimeSeconds(reader.ReadUInt32()).UtcDateTime,
Quantity = reader.ReadUInt32(),
IsHq = reader.ReadBoolean(),
};
reader.ReadBoolean(); reader.ReadBoolean();
listingEntry.OnMannequin = reader.ReadBoolean(); listingEntry.OnMannequin = reader.ReadBoolean();
listingEntry.BuyerName = Encoding.UTF8.GetString(reader.ReadBytes(33)).TrimEnd('\u0000'); listingEntry.BuyerName = Encoding.UTF8.GetString(reader.ReadBytes(33)).TrimEnd('\u0000');
listingEntry.CatalogId = reader.ReadUInt32(); listingEntry.CatalogId = reader.ReadUInt32();
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);
@ -30,26 +40,22 @@ namespace Dalamud.Game
// IPPROTO_TCP // IPPROTO_TCP
if (type == 1) if (type == 1)
{ {
// INVALID_SOCKET // INVALID_SOCKET
if (socket != new IntPtr(-1)) if (socket != new IntPtr(-1))
{ {
// In case you're not aware of it: (albeit you should) // In case you're not aware of it: (albeit you should)
// 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,
Hexagon = 0xE042,
/// <summary>
/// The hexagon icon unicode character.
/// </summary>
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>
ArrowUp, DPS = 84,
ArrowDown,
Crystarium,
MentorProblem,
FateUnknownGold, /// <summary>
/// The crafter role icon.
/// </summary>
Crafter = 85,
OrangeDiamond, /// <summary>
FateCrafting /// The gatherer role icon.
/// </summary>
Gatherer = 86,
/// <summary>
/// The "any" role icon.
/// </summary>
AnyClass = 87,
/// <summary>
/// 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;
@ -164,18 +165,20 @@ namespace Dalamud.Game.Text.SeStringHandling
break; break;
case EmbeddedInfoType.LinkTerminator: case EmbeddedInfoType.LinkTerminator:
// this has no custom handling and so needs to fallthrough to ensure it is captured // this has no custom handling and so needs to fallthrough to ensure it is captured
default: default:
// but I'm also tired of this log // but I'm also tired of this log
if (subType != EmbeddedInfoType.LinkTerminator) if (subType != EmbeddedInfoType.LinkTerminator)
{ {
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,48 +1,62 @@
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");
} }
var bytes = new List<byte> {START_BYTE, (byte) SeStringChunkType.Interactable, (byte) chunkLen, (byte) EmbeddedInfoType.DalamudLink}; var bytes = new List<byte> { START_BYTE, (byte)SeStringChunkType.Interactable, (byte)chunkLen, (byte)EmbeddedInfoType.DalamudLink };
bytes.Add((byte) pluginBytes.Length); bytes.Add((byte)pluginBytes.Length);
bytes.AddRange(pluginBytes); bytes.AddRange(pluginBytes);
bytes.AddRange(commandBytes); bytes.AddRange(commandBytes);
bytes.Add(END_BYTE); bytes.Add(END_BYTE);
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>
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon.
/// </summary>
/// <param name="icon">The Icon.</param>
public IconPayload(BitmapFontIcon icon)
{
this.Icon = icon;
}
/// <summary> /// <summary>
/// Index of the icon /// 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).")]
public IconPayload(uint iconIndex)
: this((BitmapFontIcon)iconIndex)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="IconPayload"/> class.
/// Create a Icon payload for the specified icon.
/// </summary>
internal IconPayload()
{
}
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Icon;
/// <summary>
/// Gets the index of the icon.
/// </summary> /// </summary>
[Obsolete("Use IconPayload.Icon")] [Obsolete("Use IconPayload.Icon")]
public uint IconIndex => (uint) Icon; public uint IconIndex => (uint)this.Icon;
/// <summary> /// <summary>
/// Icon the payload represents. /// Gets or sets the icon the payload represents.
/// </summary> /// </summary>
public BitmapFontIcon Icon { get; set; } = BitmapFontIcon.None; public BitmapFontIcon Icon { get; set; } = BitmapFontIcon.None;
internal IconPayload() { } /// <inheritdoc />
public override string ToString()
/// <summary> {
/// Create a Icon payload for the specified icon. return $"{this.Type} - {this.Icon}";
/// </summary>
/// <param name="iconIndex">Index of the icon</param>
[Obsolete("IconPayload(uint) is deprecated, please use IconPayload(BitmapFontIcon).")]
public IconPayload(uint iconIndex) : this((BitmapFontIcon) iconIndex) { }
/// <summary>
/// Create a Icon payload for the specified icon.
/// </summary>
/// <param name="icon">The Icon</param>
public IconPayload(BitmapFontIcon icon) {
Icon = icon;
} }
/// <inheritdoc /> /// <inheritdoc />
public override PayloadType Type => PayloadType.Icon; protected override byte[] EncodeImpl()
{
/// <inheritdoc /> var indexBytes = MakeInteger((uint)this.Icon);
protected override byte[] EncodeImpl() {
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