Merge pull request #2049 from goatcorp/api11

Dalamud API 11: Echoes of Vana'diel
This commit is contained in:
goat 2024-11-15 19:30:20 +01:00 committed by GitHub
commit a93c54340c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
127 changed files with 1959 additions and 2062 deletions

View file

@ -27,8 +27,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lumina" Version="4.2.1" />
<PackageReference Include="Lumina.Excel" Version="7.0.1" />
<PackageReference Include="Lumina" Version="5.2.1" />
<PackageReference Include="Lumina.Excel" Version="7.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets>

View file

@ -9,7 +9,7 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>10.0.0.15</DalamudVersion>
<DalamudVersion>11.0.0.0</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
@ -71,18 +71,18 @@
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="4.2.1" />
<PackageReference Include="Lumina.Excel" Version="7.0.1" />
<PackageReference Include="Lumina" Version="5.2.1" />
<PackageReference Include="Lumina.Excel" Version="7.1.0" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MinSharp" Version="1.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
@ -160,7 +160,7 @@
<Exec Command="echo|set /P =&quot;$(CommitHash)&quot; &gt; $(CommitHashFile)" IgnoreExitCode="true" />
<Exec Command="echo|set /P =&quot;$(SCMVersion)&quot; &gt; $(TempVerFile)" IgnoreExitCode="true" />
</Target>
<Target Name="GenerateStubVersionData" BeforeTargets="WriteVersionData" Condition="'$(SCMVersion)'=='' And '$(Configuration)'!='Release'">
<!-- stub out version since it takes a while. -->
<PropertyGroup>
@ -168,7 +168,7 @@
<CommitHashClientStructs>???</CommitHashClientStructs>
</PropertyGroup>
</Target>
<Target Name="WriteVersionData" BeforeTargets="CoreCompile">
<!-- names the obj/.../CustomAssemblyInfo.cs file -->
<PropertyGroup>

View file

@ -11,6 +11,7 @@ using Dalamud.Utility.Timing;
using Lumina;
using Lumina.Data;
using Lumina.Excel;
using Newtonsoft.Json;
using Serilog;
@ -28,12 +29,15 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
{
private readonly Thread luminaResourceThread;
private readonly CancellationTokenSource luminaCancellationTokenSource;
private readonly RsvResolver rsvResolver;
[ServiceManager.ServiceConstructor]
private DataManager(Dalamud dalamud)
{
this.Language = (ClientLanguage)dalamud.StartInfo.Language;
this.rsvResolver = new();
try
{
Log.Verbose("Starting data load...");
@ -44,11 +48,8 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
{
LoadMultithreaded = true,
CacheFileResources = true,
#if NEVER // Lumina bug
PanicOnSheetChecksumMismatch = true,
#else
PanicOnSheetChecksumMismatch = false,
#endif
RsvResolver = this.rsvResolver.TryResolve,
DefaultExcelLanguage = this.Language.ToLumina(),
};
@ -129,12 +130,12 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
#region Lumina Wrappers
/// <inheritdoc/>
public ExcelSheet<T>? GetExcelSheet<T>() where T : ExcelRow
=> this.Excel.GetSheet<T>();
public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow<T>
=> this.Excel.GetSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/>
public ExcelSheet<T>? GetExcelSheet<T>(ClientLanguage language) where T : ExcelRow
=> this.Excel.GetSheet<T>(language.ToLumina());
public SubrowExcelSheet<T> GetSubrowExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelSubrow<T>
=> this.Excel.GetSubrowSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/>
public FileResource? GetFile(string path)
@ -170,6 +171,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
{
this.luminaCancellationTokenSource.Cancel();
this.GameData.Dispose();
this.rsvResolver.Dispose();
}
private class LauncherTroubleshootingInfo

View file

@ -0,0 +1,22 @@
using Lumina.Excel;
namespace Dalamud.Data;
/// <summary>
/// A helper class to easily resolve Lumina data within Dalamud.
/// </summary>
internal static class LuminaUtils
{
private static ExcelModule Module => Service<DataManager>.Get().Excel;
/// <summary>
/// Initializes a new instance of the <see cref="RowRef{T}"/> class using the default <see cref="ExcelModule"/>.
/// </summary>
/// <typeparam name="T">The type of Lumina sheet to resolve.</typeparam>
/// <param name="rowId">The id of the row to resolve.</param>
/// <returns>A new <see cref="RowRef{T}"/> object.</returns>
public static RowRef<T> CreateRef<T>(uint rowId) where T : struct, IExcelRow<T>
{
return new(Module, rowId);
}
}

View file

@ -0,0 +1,51 @@
using System.Collections.Generic;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.LayoutEngine;
using Lumina.Text.ReadOnly;
namespace Dalamud.Data;
/// <summary>
/// Provides functionality for resolving RSV strings.
/// </summary>
internal sealed unsafe class RsvResolver : IDisposable
{
private static readonly ModuleLog Log = new("RsvProvider");
private readonly Hook<LayoutWorld.Delegates.AddRsvString> addRsvStringHook;
/// <summary>
/// Initializes a new instance of the <see cref="RsvResolver"/> class.
/// </summary>
public RsvResolver()
{
this.addRsvStringHook = Hook<LayoutWorld.Delegates.AddRsvString>.FromAddress((nint)LayoutWorld.MemberFunctionPointers.AddRsvString, this.AddRsvStringDetour);
this.addRsvStringHook.Enable();
}
private Dictionary<ReadOnlySeString, ReadOnlySeString> Lookup { get; } = [];
/// <summary>Attemps to resolve an RSV string.</summary>
/// <inheritdoc cref="Lumina.Excel.ExcelModule.ResolveRsvDelegate"/>
public bool TryResolve(ReadOnlySeString rsvString, out ReadOnlySeString resolvedString) =>
this.Lookup.TryGetValue(rsvString, out resolvedString);
/// <inheritdoc/>
public void Dispose()
{
this.addRsvStringHook.Dispose();
}
private bool AddRsvStringDetour(LayoutWorld* @this, byte* rsvString, byte* resolvedString, nuint resolvedStringSize)
{
var rsv = new ReadOnlySeString(MemoryHelper.ReadRawNullTerminated((nint)rsvString));
var resolved = new ReadOnlySeString(new ReadOnlySpan<byte>(resolvedString, (int)resolvedStringSize).ToArray());
Log.Debug($"Resolving RSV \"{rsv}\" to \"{resolved}\".");
this.Lookup[rsv] = resolved;
return this.addRsvStringHook.Original(@this, rsvString, resolvedString, resolvedStringSize);
}
}

View file

@ -16,6 +16,6 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver
/// <param name="scanner">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner scanner)
{
this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE");
this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS
}
}

View file

@ -165,7 +165,7 @@ internal unsafe class PluginEventController : IDisposable
{
var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey;
var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address;
var eventTypeMatches = currentEvent->Type == eventType;
var eventTypeMatches = currentEvent->State.EventType == eventType;
if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches)
{

View file

@ -1,22 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Utility;
@ -27,49 +19,10 @@ namespace Dalamud.Game;
/// Chat events and public helper functions.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class ChatHandlers : IServiceType
internal partial class ChatHandlers : IServiceType
{
private static readonly ModuleLog Log = new("CHATHANDLER");
private static readonly ModuleLog Log = new("ChatHandlers");
private readonly Dictionary<ClientLanguage, Regex[]> retainerSaleRegexes = new()
{
{
ClientLanguage.Japanese,
new Regex[]
{
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)×(?<count>[\d,.]+)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
}
},
{
ClientLanguage.English,
new Regex[]
{
new Regex(@"^(?<item>.+) you put up for sale in the (?:.+) markets (?:have|has) sold for (?<value>[\d,.]+) gil \(after fees\)\.$", RegexOptions.Compiled),
}
},
{
ClientLanguage.German,
new Regex[]
{
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) für (?<value>[\d,.]+) Gil verkauft\.$", RegexOptions.Compiled),
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) verkauft und (?<value>[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled),
}
},
{
ClientLanguage.French,
new Regex[]
{
new Regex(@"^Un servant a vendu (?<item>.+) pour (?<value>[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled),
}
},
};
private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
[ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service<Dalamud>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -92,6 +45,9 @@ internal class ChatHandlers : IServiceType
/// </summary>
public bool IsAutoUpdateComplete { get; private set; }
[GeneratedRegex(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled)]
private static partial Regex CompiledUrlRegex();
private void OnCheckMessageHandled(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled)
{
var textVal = message.TextValue;
@ -100,7 +56,7 @@ internal class ChatHandlers : IServiceType
this.configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
{
// This seems to be in the user block list - let's not show it
Log.Debug("Blocklist triggered");
Log.Debug("Filtered a message that contained a muted word");
isHandled = true;
return;
}
@ -127,41 +83,10 @@ internal class ChatHandlers : IServiceType
return;
#endif
if (type == XivChatType.RetainerSale)
{
foreach (var regex in this.retainerSaleRegexes[(ClientLanguage)this.dalamud.StartInfo.Language])
{
var matchInfo = regex.Match(message.TextValue);
// we no longer really need to do/validate the item matching since we read the id from the byte array
// but we'd be checking the main match anyway
var itemInfo = matchInfo.Groups["item"];
if (!itemInfo.Success)
continue;
var itemLink = message.Payloads.FirstOrDefault(x => x.Type == PayloadType.Item) as ItemPayload;
if (itemLink == default)
{
Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode()));
break;
}
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.Item.RowId}, HQ {itemLink.IsHQ}");
var valueInfo = matchInfo.Groups["value"];
// not sure if using a culture here would work correctly, so just strip symbols instead
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", string.Empty).Replace(".", string.Empty), out var itemValue))
continue;
// Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ));
break;
}
}
var messageCopy = message;
var senderCopy = sender;
var linkMatch = this.urlRegex.Match(message.TextValue);
var linkMatch = CompiledUrlRegex().Match(message.TextValue);
if (linkMatch.Value.Length > 0)
this.LastLink = linkMatch.Value;
}

View file

@ -1,6 +1,9 @@
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Data;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Aetherytes;
/// <summary>
@ -56,7 +59,7 @@ public interface IAetheryteEntry
/// <summary>
/// Gets the Aetheryte data related to this aetheryte.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.Aetheryte> AetheryteData { get; }
RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData { get; }
}
/// <summary>
@ -103,5 +106,5 @@ internal sealed class AetheryteEntry : IAetheryteEntry
public bool IsApartment => this.data.IsApartment;
/// <inheritdoc />
public ExcelResolver<Lumina.Excel.GeneratedSheets.Aetheryte> AetheryteData => new(this.AetheryteId);
public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);
}

View file

@ -1,14 +1,12 @@
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
namespace Dalamud.Game.ClientState.Buddy;
@ -28,14 +26,9 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor]
private BuddyList()
{
this.address = this.clientState.AddressResolver;
Log.Verbose($"Buddy list address {Util.DescribeAddress(this.address.BuddyList)}");
}
/// <inheritdoc/>
@ -76,14 +69,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
}
/// <summary>
/// Gets the address of the buddy list.
/// </summary>
internal IntPtr BuddyListAddress => this.address.BuddyList;
private static int BuddyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember>();
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy*)this.BuddyListAddress;
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy;
/// <inheritdoc/>
public IBuddyMember? this[int index]

View file

@ -1,6 +1,8 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Buddy;
@ -45,17 +47,17 @@ public interface IBuddyMember
/// <summary>
/// Gets the Mount data related to this buddy. It should only be used with companion buddies.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.Mount> MountData { get; }
RowRef<Lumina.Excel.Sheets.Mount> MountData { get; }
/// <summary>
/// Gets the Pet data related to this buddy. It should only be used with pet buddies.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.Pet> PetData { get; }
RowRef<Lumina.Excel.Sheets.Pet> PetData { get; }
/// <summary>
/// Gets the Trust data related to this buddy. It should only be used with battle buddies.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.DawnGrowMember> TrustData { get; }
RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData { get; }
}
/// <summary>
@ -94,13 +96,13 @@ internal unsafe class BuddyMember : IBuddyMember
public uint DataID => this.Struct->DataId;
/// <inheritdoc />
public ExcelResolver<Lumina.Excel.GeneratedSheets.Mount> MountData => new(this.DataID);
public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
/// <inheritdoc />
public ExcelResolver<Lumina.Excel.GeneratedSheets.Pet> PetData => new(this.DataID);
public RowRef<Lumina.Excel.Sheets.Pet> PetData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Pet>(this.DataID);
/// <inheritdoc />
public ExcelResolver<Lumina.Excel.GeneratedSheets.DawnGrowMember> TrustData => new(this.DataID);
public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID);
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
}

View file

@ -1,5 +1,4 @@
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Conditions;
@ -14,12 +13,14 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Application.Network;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using Action = System.Action;
@ -35,17 +36,18 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address;
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
private readonly Hook<EventFramework.Delegates.SetTerritoryTypeId> setupTerritoryTypeHook;
private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook;
private readonly Hook<LogoutCallbackInterface.Delegates.OnLogout> onLogoutHook;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get();
private bool lastConditionNone = true;
private bool lastFramePvP;
[ServiceManager.ServiceConstructor]
private unsafe ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle)
@ -58,21 +60,22 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language;
Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(this.address.SetupTerritoryType)}");
var setTerritoryTypeAddr = EventFramework.Addresses.SetTerritoryTypeId.Value;
Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(setTerritoryTypeAddr)}");
this.setupTerritoryTypeHook = Hook<SetupTerritoryTypeDelegate>.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
this.setupTerritoryTypeHook = Hook<EventFramework.Delegates.SetTerritoryTypeId>.FromAddress(setTerritoryTypeAddr, this.SetupTerritoryTypeDetour);
this.uiModuleHandlePacketHook = Hook<UIModule.Delegates.HandlePacket>.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour);
this.onLogoutHook = Hook<LogoutCallbackInterface.Delegates.OnLogout>.FromAddress((nint)LogoutCallbackInterface.StaticVirtualTablePointer->OnLogout, this.OnLogoutDetour);
this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.setupTerritoryTypeHook.Enable();
this.uiModuleHandlePacketHook.Enable();
this.onLogoutHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private unsafe delegate void SetupTerritoryTypeDelegate(EventFramework* eventFramework, ushort terriType);
private unsafe delegate void ProcessPacketPlayerSetupDelegate(nint a1, nint packet);
/// <inheritdoc/>
public event Action<ushort>? TerritoryChanged;
@ -87,7 +90,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
public event Action? Login;
/// <inheritdoc/>
public event Action? Logout;
public event IClientState.LogoutDelegate? Logout;
/// <inheritdoc/>
public event Action? EnterPvP;
@ -110,7 +113,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
get
{
var agentMap = AgentMap.Instance();
return agentMap != null ? AgentMap.Instance()->CurrentMapId : 0;
return agentMap != null ? agentMap->CurrentMapId : 0;
}
}
@ -118,10 +121,17 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
public IPlayerCharacter? LocalPlayer => Service<ObjectTable>.GetNullable()?[0] as IPlayerCharacter;
/// <inheritdoc/>
public ulong LocalContentId => (ulong)Marshal.ReadInt64(this.address.LocalContentId);
public unsafe ulong LocalContentId => PlayerState.Instance()->ContentId;
/// <inheritdoc/>
public bool IsLoggedIn { get; private set; }
public unsafe bool IsLoggedIn
{
get
{
var agentLobby = AgentLobby.Instance();
return agentLobby != null && agentLobby->IsLoggedIn;
}
}
/// <inheritdoc/>
public bool IsPvP { get; private set; }
@ -165,23 +175,47 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
{
this.setupTerritoryTypeHook.Dispose();
this.uiModuleHandlePacketHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.onLogoutHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
}
private unsafe void SetupTerritoryTypeDetour(EventFramework* eventFramework, ushort territoryType)
{
Log.Debug("TerritoryType changed: {0}", territoryType);
this.TerritoryType = territoryType;
this.TerritoryChanged?.InvokeSafely(territoryType);
Log.Debug("TerritoryType changed: {0}", territoryType);
var rowRef = LuminaUtils.CreateRef<TerritoryType>(territoryType);
if (rowRef.IsValid)
{
var isPvP = rowRef.Value.IsPvpZone;
if (isPvP != this.IsPvP)
{
this.IsPvP = isPvP;
this.IsPvPExcludingDen = this.IsPvP && this.TerritoryType != 250;
if (this.IsPvP)
{
Log.Debug("EnterPvP");
this.EnterPvP?.InvokeSafely();
}
else
{
Log.Debug("LeavePvP");
this.LeavePvP?.InvokeSafely();
}
}
}
this.setupTerritoryTypeHook.Original(eventFramework, territoryType);
}
private unsafe void UIModuleHandlePacketDetour(UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet)
{
this.uiModuleHandlePacketHook!.Original(thisPtr, type, uintParam, packet);
this.uiModuleHandlePacketHook.Original(thisPtr, type, uintParam, packet);
switch (type)
{
@ -226,11 +260,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
}
}
private void NetworkHandlersOnCfPop(ContentFinderCondition e)
{
this.CfPop?.InvokeSafely(e);
}
private void FrameworkOnOnUpdateEvent(IFramework framework1)
{
var condition = Service<Conditions.Condition>.GetNullable();
@ -244,40 +273,58 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
{
Log.Debug("Is login");
this.lastConditionNone = false;
this.IsLoggedIn = true;
this.Login?.InvokeSafely();
gameGui.ResetUiHideState();
this.lifecycle.ResetLogout();
}
}
if (!condition.Any() && this.lastConditionNone == false)
private unsafe void OnLogoutDetour(LogoutCallbackInterface* thisPtr, LogoutCallbackInterface.LogoutParams* logoutParams)
{
var gameGui = Service<GameGui>.GetNullable();
if (logoutParams != null)
{
Log.Debug("Is logout");
this.lastConditionNone = true;
this.IsLoggedIn = false;
this.Logout?.InvokeSafely();
gameGui.ResetUiHideState();
this.lifecycle.SetLogout();
}
this.IsPvP = GameMain.IsInPvPArea();
this.IsPvPExcludingDen = this.IsPvP && this.TerritoryType != 250;
if (this.IsPvP != this.lastFramePvP)
{
this.lastFramePvP = this.IsPvP;
if (this.IsPvP)
try
{
this.EnterPvP?.InvokeSafely();
var type = logoutParams->Type;
var code = logoutParams->Code;
Log.Debug("Logout: Type {type}, Code {code}", type, code);
if (this.Logout is { } callback)
{
foreach (var action in callback.GetInvocationList().Cast<IClientState.LogoutDelegate>())
{
try
{
action(type, code);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
}
gameGui?.ResetUiHideState();
this.lastConditionNone = true; // unblock login flag
this.lifecycle.SetLogout();
}
else
catch (Exception ex)
{
this.LeavePvP?.InvokeSafely();
Log.Error(ex, "Exception during OnLogoutDetour");
}
}
this.onLogoutHook.Original(thisPtr, logoutParams);
}
private void NetworkHandlersOnCfPop(ContentFinderCondition e)
{
this.CfPop?.InvokeSafely(e);
}
}
@ -322,7 +369,7 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
public event Action? Login;
/// <inheritdoc/>
public event Action? Logout;
public event IClientState.LogoutDelegate? Logout;
/// <inheritdoc/>
public event Action? EnterPvP;
@ -394,7 +441,7 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
private void LoginForward() => this.Login?.Invoke();
private void LogoutForward() => this.Logout?.Invoke();
private void LogoutForward(int type, int code) => this.Logout?.Invoke(type, code);
private void EnterPvPForward() => this.EnterPvP?.Invoke();

View file

@ -7,39 +7,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
{
// Static offsets
/// <summary>
/// Gets the address of the actor table.
/// </summary>
public IntPtr ObjectTable { get; private set; }
/// <summary>
/// Gets the address of the buddy list.
/// </summary>
public IntPtr BuddyList { get; private set; }
/// <summary>
/// Gets the address of a pointer to the fate table.
/// </summary>
/// <remarks>
/// This is a static address to a pointer, not the address of the table itself.
/// </remarks>
public IntPtr FateTablePtr { get; private set; }
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManager { get; private set; }
/// <summary>
/// Gets the address of the local content id.
/// </summary>
public IntPtr LocalContentId { get; private set; }
/// <summary>
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { get; private set; }
/// <summary>
/// Gets the address of the keyboard state.
/// </summary>
@ -50,17 +17,12 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// </summary>
public IntPtr KeyboardStateIndexArray { 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.
/// Gets the address of the method which sets up the player.
/// </summary>
public IntPtr SetupTerritoryType { get; private set; }
public IntPtr ProcessPacketPlayerSetup { get; private set; }
/// <summary>
/// Gets the address of the method which polls the gamepads for data.
@ -74,18 +36,7 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig)
{
this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ??");
this.BuddyList = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 45 84 E4 75 1A F6 45 12 04");
this.FateTablePtr = sig.GetStaticAddressFromSig("48 8B 15 ?? ?? ?? ?? 48 8B F1 44 0F B7 41");
this.GroupManager = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 80 B8 ?? ?? ?? ?? ?? 77 71");
this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 0D ?? ?? ?? ?? 48 8D 57 08");
this.JobGaugeData = sig.GetStaticAddressFromSig("48 8B 3D ?? ?? ?? ?? 33 ED") + 0x8;
this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 57 48 83 EC 20 0F B7 DA");
this.ProcessPacketPlayerSetup = sig.ScanText("40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B D3"); // not in cs struct
// These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used.
// lea rcx, ds:1DB9F74h[rax*4] KeyboardState
@ -93,8 +44,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4;
this.KeyboardStateIndexArray = sig.ScanText("0F B6 94 33 ?? ?? ?? ?? 84 D2") + 0x4;
this.ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 8B D3 E8 ?? ?? ?? ?? 32 C0 48 83 C4 20");
this.GamepadPoll = sig.ScanText("40 55 53 57 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 44 0F 29 B4 24");
this.GamepadPoll = sig.ScanText("40 55 53 57 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 44 0F 29 B4 24"); // unnamed in cs
}
}

View file

@ -28,10 +28,9 @@ internal sealed class Condition : IInternalDisposableService, ICondition
private bool isDisposed;
[ServiceManager.ServiceConstructor]
private Condition(ClientState clientState)
private unsafe Condition()
{
var resolver = clientState.AddressResolver;
this.Address = resolver.ConditionFlags;
this.Address = (nint)FFXIVClientStructs.FFXIV.Client.Game.Conditions.Instance();
// Initialization
for (var i = 0; i < MaxConditionEntries; i++)

View file

@ -1,10 +1,10 @@
using System.Numerics;
using Dalamud.Data;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
using Dalamud.Utility;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Fates;
@ -21,7 +21,7 @@ public interface IFate : IEquatable<IFate>
/// <summary>
/// Gets game data linked to this Fate.
/// </summary>
Lumina.Excel.GeneratedSheets.Fate GameData { get; }
RowRef<Lumina.Excel.Sheets.Fate> GameData { get; }
/// <summary>
/// Gets the time this <see cref="Fate"/> started.
@ -71,11 +71,11 @@ public interface IFate : IEquatable<IFate>
/// <summary>
/// Gets a value indicating whether or not this <see cref="Fate"/> has a EXP bonus.
/// </summary>
[Api11ToDo("Remove this")]
[Obsolete($"Use {nameof(HasBonus)} instead")]
bool HasExpBonus { get; }
/// <summary>
/// Gets a value indicating whether or not this <see cref="Fate"/> has a EXP bonus.
/// Gets a value indicating whether or not this <see cref="Fate"/> has a bonus.
/// </summary>
bool HasBonus { get; }
@ -112,7 +112,7 @@ public interface IFate : IEquatable<IFate>
/// <summary>
/// Gets the territory this <see cref="Fate"/> is located in.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> TerritoryType { get; }
RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType { get; }
/// <summary>
/// Gets the address of this Fate in memory.
@ -192,7 +192,7 @@ internal unsafe partial class Fate : IFate
public ushort FateId => this.Struct->FateId;
/// <inheritdoc/>
public Lumina.Excel.GeneratedSheets.Fate GameData => Service<DataManager>.Get().GetExcelSheet<Lumina.Excel.GeneratedSheets.Fate>().GetRow(this.FateId);
public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId);
/// <inheritdoc/>
public int StartTimeEpoch => this.Struct->StartTimeEpoch;
@ -222,8 +222,9 @@ internal unsafe partial class Fate : IFate
public byte Progress => this.Struct->Progress;
/// <inheritdoc/>
public bool HasExpBonus => this.Struct->IsBonus;
[Obsolete($"Use {nameof(HasBonus)} instead")]
public bool HasExpBonus => this.Struct->IsExpBonus;
/// <inheritdoc/>
public bool HasBonus => this.Struct->IsBonus;
@ -248,5 +249,5 @@ internal unsafe partial class Fate : IFate
/// <summary>
/// Gets the territory this <see cref="Fate"/> is located in.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> TerritoryType => new(this.Struct->TerritoryId);
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryId);
}

View file

@ -4,9 +4,8 @@ using System.Collections.Generic;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
namespace Dalamud.Game.ClientState.Fates;
@ -20,55 +19,34 @@ namespace Dalamud.Game.ClientState.Fates;
#pragma warning restore SA1015
internal sealed partial class FateTable : IServiceType, IFateTable
{
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor]
private FateTable(ClientState clientState)
private FateTable()
{
this.address = clientState.AddressResolver;
Log.Verbose($"Fate table address {Util.DescribeAddress(this.address.FateTablePtr)}");
}
/// <inheritdoc/>
public IntPtr Address => this.address.FateTablePtr;
public unsafe IntPtr Address => (nint)CSFateManager.Instance();
/// <inheritdoc/>
public unsafe int Length
{
get
{
var fateTable = this.FateTableAddress;
if (fateTable == IntPtr.Zero)
var fateManager = CSFateManager.Instance();
if (fateManager == null)
return 0;
// Sonar used this to check if the table was safe to read
if (Struct->FateDirector == null)
if (fateManager->FateDirector == null)
return 0;
if (Struct->Fates.First == null || Struct->Fates.Last == null)
if (fateManager->Fates.First == null || fateManager->Fates.Last == null)
return 0;
return Struct->Fates.Count;
return fateManager->Fates.Count;
}
}
/// <summary>
/// Gets the address of the Fate table.
/// </summary>
internal unsafe IntPtr FateTableAddress
{
get
{
if (this.address.FateTablePtr == IntPtr.Zero)
return IntPtr.Zero;
return *(IntPtr*)this.address.FateTablePtr;
}
}
private unsafe FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager*)this.FateTableAddress;
/// <inheritdoc/>
public IFate? this[int index]
{
@ -99,11 +77,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable
if (index >= this.Length)
return IntPtr.Zero;
var fateTable = this.FateTableAddress;
if (fateTable == IntPtr.Zero)
var fateManager = CSFateManager.Instance();
if (fateManager == null)
return IntPtr.Zero;
return (IntPtr)this.Struct->Fates[index].Value;
return (IntPtr)fateManager->Fates[index].Value;
}
/// <inheritdoc/>

View file

@ -16,25 +16,25 @@ public struct GamepadInput
/// <summary>
/// Left analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x88)]
[FieldOffset(0x78)]
public int LeftStickX;
/// <summary>
/// Left analogue stick's vertical value, -99 for down, 99 for up.
/// </summary>
[FieldOffset(0x8C)]
[FieldOffset(0x7C)]
public int LeftStickY;
/// <summary>
/// Right analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x90)]
[FieldOffset(0x80)]
public int RightStickX;
/// <summary>
/// Right analogue stick's vertical value, -99 for down, 99 for up.
/// </summary>
[FieldOffset(0x94)]
[FieldOffset(0x84)]
public int RightStickY;
/// <summary>
@ -43,7 +43,7 @@ public struct GamepadInput
/// <remarks>
/// This is a bitfield.
/// </remarks>
[FieldOffset(0x98)]
[FieldOffset(0x88)]
public ushort ButtonsRaw;
/// <summary>
@ -52,7 +52,7 @@ public struct GamepadInput
/// <remarks>
/// This is a bitfield.
/// </remarks>
[FieldOffset(0x9C)]
[FieldOffset(0x8C)]
public ushort ButtonsPressed;
/// <summary>
@ -61,7 +61,7 @@ public struct GamepadInput
/// <remarks>
/// This is a bitfield.
/// </remarks>
[FieldOffset(0xA0)]
[FieldOffset(0x90)]
public ushort ButtonsReleased;
/// <summary>
@ -70,6 +70,6 @@ public struct GamepadInput
/// <remarks>
/// This is a bitfield.
/// </remarks>
[FieldOffset(0xA4)]
[FieldOffset(0x94)]
public ushort ButtonsRepeat;
}

View file

@ -5,9 +5,8 @@ using Dalamud.Game.ClientState.JobGauge.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
using CSJobGaugeManager = FFXIVClientStructs.FFXIV.Client.Game.JobGaugeManager;
namespace Dalamud.Game.ClientState.JobGauge;
@ -21,18 +20,15 @@ namespace Dalamud.Game.ClientState.JobGauge;
#pragma warning restore SA1015
internal class JobGauges : IServiceType, IJobGauges
{
private Dictionary<Type, JobGaugeBase> cache = new();
private Dictionary<Type, JobGaugeBase> cache = [];
[ServiceManager.ServiceConstructor]
private JobGauges(ClientState clientState)
private JobGauges()
{
this.Address = clientState.AddressResolver.JobGaugeData;
Log.Verbose($"JobGaugeData address {Util.DescribeAddress(this.Address)}");
}
/// <inheritdoc/>
public IntPtr Address { get; }
public unsafe IntPtr Address => (nint)(&CSJobGaugeManager.Instance()->CurrentGauge);
/// <inheritdoc/>
public T Get<T>() where T : JobGaugeBase

View file

@ -29,13 +29,13 @@ public unsafe class SMNGauge : JobGaugeBase<SummonerGauge>
/// <summary>
/// Gets the summon that will return after the current summon expires.
/// This maps to the <see cref="Lumina.Excel.GeneratedSheets.Pet"/> sheet.
/// This maps to the <see cref="Lumina.Excel.Sheets.Pet"/> sheet.
/// </summary>
public SummonPet ReturnSummon => (SummonPet)this.Struct->ReturnSummon;
/// <summary>
/// Gets the summon glam for the <see cref="ReturnSummon"/>.
/// This maps to the <see cref="Lumina.Excel.GeneratedSheets.PetMirage"/> sheet.
/// This maps to the <see cref="Lumina.Excel.Sheets.PetMirage"/> sheet.
/// </summary>
public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam;

View file

@ -7,15 +7,17 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.Interop;
using Microsoft.Extensions.ObjectPool;
using Serilog;
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
namespace Dalamud.Game.ClientState.Objects;
@ -29,10 +31,12 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning restore SA1015
internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
private const int ObjectTableLength = 599;
private static readonly ModuleLog Log = new("ObjectTable");
private static int objectTableLength;
private readonly ClientState clientState;
private readonly CachedEntry[] cachedObjectTable = new CachedEntry[ObjectTableLength];
private readonly CachedEntry[] cachedObjectTable;
private readonly ObjectPool<Enumerator> multiThreadedEnumerators =
new DefaultObjectPoolProvider().Create<Enumerator>();
@ -46,29 +50,30 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
this.clientState = clientState;
var nativeObjectTableAddress = (CSGameObject**)this.clientState.AddressResolver.ObjectTable;
var nativeObjectTable = CSGameObjectManager.Instance()->Objects.IndexSorted;
objectTableLength = nativeObjectTable.Length;
this.cachedObjectTable = new CachedEntry[objectTableLength];
for (var i = 0; i < this.cachedObjectTable.Length; i++)
this.cachedObjectTable[i] = new(nativeObjectTableAddress, i);
this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
this.frameworkThreadEnumerators[i] = new(this, i);
Log.Verbose($"Object table address {Util.DescribeAddress(this.clientState.AddressResolver.ObjectTable)}");
}
/// <inheritdoc/>
public nint Address
public unsafe nint Address
{
get
{
_ = this.WarnMultithreadedUsage();
return this.clientState.AddressResolver.ObjectTable;
return (nint)(&CSGameObjectManager.Instance()->Objects);
}
}
/// <inheritdoc/>
public int Length => ObjectTableLength;
public int Length => objectTableLength;
/// <inheritdoc/>
public IGameObject? this[int index]
@ -77,7 +82,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
_ = this.WarnMultithreadedUsage();
return index is >= ObjectTableLength or < 0 ? null : this.cachedObjectTable[index].Update();
return (index >= objectTableLength || index < 0) ? null : this.cachedObjectTable[index].Update();
}
}
@ -120,7 +125,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
_ = this.WarnMultithreadedUsage();
return index is < 0 or >= ObjectTableLength ? nint.Zero : (nint)this.cachedObjectTable[index].Address;
return (index >= objectTableLength || index < 0) ? nint.Zero : (nint)this.cachedObjectTable[index].Address;
}
/// <inheritdoc/>
@ -172,33 +177,21 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
}
/// <summary>Stores an object table entry, with preallocated concrete types.</summary>
internal readonly unsafe struct CachedEntry
/// <remarks>Initializes a new instance of the <see cref="CachedEntry"/> struct.</remarks>
/// <param name="gameObjectPtr">A pointer to the object table entry this entry should be pointing to.</param>
internal readonly unsafe struct CachedEntry(Pointer<CSGameObject>* gameObjectPtr)
{
private readonly CSGameObject** gameObjectPtrPtr;
private readonly PlayerCharacter playerCharacter;
private readonly BattleNpc battleNpc;
private readonly Npc npc;
private readonly EventObj eventObj;
private readonly GameObject gameObject;
/// <summary>Initializes a new instance of the <see cref="CachedEntry"/> struct.</summary>
/// <param name="ownerTable">The object table that this entry should be pointing to.</param>
/// <param name="slot">The slot index inside the table.</param>
public CachedEntry(CSGameObject** ownerTable, int slot)
{
this.gameObjectPtrPtr = ownerTable + slot;
this.playerCharacter = new(nint.Zero);
this.battleNpc = new(nint.Zero);
this.npc = new(nint.Zero);
this.eventObj = new(nint.Zero);
this.gameObject = new(nint.Zero);
}
private readonly PlayerCharacter playerCharacter = new(nint.Zero);
private readonly BattleNpc battleNpc = new(nint.Zero);
private readonly Npc npc = new(nint.Zero);
private readonly EventObj eventObj = new(nint.Zero);
private readonly GameObject gameObject = new(nint.Zero);
/// <summary>Gets the address of the underlying native object. May be null.</summary>
public CSGameObject* Address
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => *this.gameObjectPtrPtr;
get => gameObjectPtr->Value;
}
/// <summary>Updates and gets the wrapped game object pointed by this struct.</summary>
@ -284,11 +277,11 @@ internal sealed partial class ObjectTable
public bool MoveNext()
{
if (this.index == ObjectTableLength)
if (this.index == objectTableLength)
return false;
var cache = this.owner!.cachedObjectTable.AsSpan();
for (this.index++; this.index < ObjectTableLength; this.index++)
for (this.index++; this.index < objectTableLength; this.index++)
{
if (cache[this.index].Update() is { } ao)
{

View file

@ -1,12 +1,8 @@
using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.ClientState.Objects.SubKinds;
@ -16,14 +12,14 @@ namespace Dalamud.Game.ClientState.Objects.SubKinds;
public interface IPlayerCharacter : IBattleChara
{
/// <summary>
/// Gets the current <see cref="ExcelResolver{T}">world</see> of the character.
/// Gets the current <see cref="RowRef{T}">world</see> of the character.
/// </summary>
ExcelResolver<World> CurrentWorld { get; }
RowRef<World> CurrentWorld { get; }
/// <summary>
/// Gets the home <see cref="ExcelResolver{T}">world</see> of the character.
/// Gets the home <see cref="RowRef{T}">world</see> of the character.
/// </summary>
ExcelResolver<World> HomeWorld { get; }
RowRef<World> HomeWorld { get; }
}
/// <summary>
@ -42,10 +38,10 @@ internal unsafe class PlayerCharacter : BattleChara, IPlayerCharacter
}
/// <inheritdoc/>
public ExcelResolver<World> CurrentWorld => new(this.Struct->CurrentWorld);
public RowRef<World> CurrentWorld => LuminaUtils.CreateRef<World>(this.Struct->CurrentWorld);
/// <inheritdoc/>
public ExcelResolver<World> HomeWorld => new(this.Struct->HomeWorld);
public RowRef<World> HomeWorld => LuminaUtils.CreateRef<World>(this.Struct->HomeWorld);
/// <summary>
/// Gets the target actor ID of the PlayerCharacter.

View file

@ -1,10 +1,12 @@
using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.ClientState.Objects.Types;
@ -61,7 +63,7 @@ public interface ICharacter : IGameObject
/// <summary>
/// Gets the ClassJob of this Chara.
/// </summary>
public ExcelResolver<ClassJob> ClassJob { get; }
public RowRef<ClassJob> ClassJob { get; }
/// <summary>
/// Gets the level of this Chara.
@ -87,7 +89,7 @@ public interface ICharacter : IGameObject
/// <summary>
/// Gets the current online status of the character.
/// </summary>
public ExcelResolver<OnlineStatus> OnlineStatus { get; }
public RowRef<OnlineStatus> OnlineStatus { get; }
/// <summary>
/// Gets the status flags.
@ -97,14 +99,14 @@ public interface ICharacter : IGameObject
/// <summary>
/// Gets the current mount for this character. Will be <c>null</c> if the character doesn't have a mount.
/// </summary>
public ExcelResolver<Mount>? CurrentMount { get; }
public RowRef<Mount>? CurrentMount { get; }
/// <summary>
/// Gets the current minion summoned for this character. Will be <c>null</c> if the character doesn't have a minion.
/// This method *will* return information about a spawned (but invisible) minion, e.g. if the character is riding a
/// mount.
/// </summary>
public ExcelResolver<Companion>? CurrentMinion { get; }
public RowRef<Companion>? CurrentMinion { get; }
}
/// <summary>
@ -150,7 +152,7 @@ internal unsafe class Character : GameObject, ICharacter
public byte ShieldPercentage => this.Struct->CharacterData.ShieldValue;
/// <inheritdoc/>
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->CharacterData.ClassJob);
public RowRef<ClassJob> ClassJob => LuminaUtils.CreateRef<ClassJob>(this.Struct->CharacterData.ClassJob);
/// <inheritdoc/>
public byte Level => this.Struct->CharacterData.Level;
@ -159,7 +161,7 @@ internal unsafe class Character : GameObject, ICharacter
public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray();
/// <inheritdoc/>
public SeString CompanyTag => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref this.Struct->FreeCompanyTag[0]), 6);
public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag);
/// <summary>
/// Gets the target object ID of the character.
@ -170,7 +172,7 @@ internal unsafe class Character : GameObject, ICharacter
public uint NameId => this.Struct->NameId;
/// <inheritdoc/>
public ExcelResolver<OnlineStatus> OnlineStatus => new(this.Struct->CharacterData.OnlineStatus);
public RowRef<OnlineStatus> OnlineStatus => LuminaUtils.CreateRef<OnlineStatus>(this.Struct->CharacterData.OnlineStatus);
/// <summary>
/// Gets the status flags.
@ -186,28 +188,28 @@ internal unsafe class Character : GameObject, ICharacter
(this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None);
/// <inheritdoc />
public ExcelResolver<Mount>? CurrentMount
public RowRef<Mount>? CurrentMount
{
get
{
if (this.Struct->IsNotMounted()) return null; // just for safety.
var mountId = this.Struct->Mount.MountId;
return mountId == 0 ? null : new ExcelResolver<Mount>(mountId);
return mountId == 0 ? null : LuminaUtils.CreateRef<Mount>(mountId);
}
}
/// <inheritdoc />
public ExcelResolver<Companion>? CurrentMinion
public RowRef<Companion>? CurrentMinion
{
get
{
if (this.Struct->CompanionObject != null)
return new ExcelResolver<Companion>(this.Struct->CompanionObject->BaseId);
return LuminaUtils.CreateRef<Companion>(this.Struct->CompanionObject->BaseId);
// this is only present if a minion is summoned but hidden (e.g. the player's on a mount).
var hiddenCompanionId = this.Struct->CompanionData.CompanionId;
return hiddenCompanionId == 0 ? null : new ExcelResolver<Companion>(hiddenCompanionId);
return hiddenCompanionId == 0 ? null : LuminaUtils.CreateRef<Companion>(hiddenCompanionId);
}
}

View file

@ -197,7 +197,7 @@ internal partial class GameObject
internal unsafe partial class GameObject : IGameObject
{
/// <inheritdoc/>
public SeString Name => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref this.Struct->Name[0]), 64);
public SeString Name => SeString.Parse(this.Struct->Name);
/// <inheritdoc/>
public ulong GameObjectId => this.Struct->GetGameObjectId();

View file

@ -6,9 +6,8 @@ using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
namespace Dalamud.Game.ClientState.Party;
@ -28,14 +27,9 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor]
private PartyList()
{
this.address = this.clientState.AddressResolver;
Log.Verbose($"Group manager address {Util.DescribeAddress(this.address.GroupManager)}");
}
/// <inheritdoc/>
@ -48,7 +42,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
/// <inheritdoc/>
public IntPtr GroupManagerAddress => this.address.GroupManager;
public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
/// <inheritdoc/>
public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);

View file

@ -1,13 +1,15 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Party;
/// <summary>
@ -71,12 +73,12 @@ public interface IPartyMember
/// <summary>
/// Gets the territory this party member is located in.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> Territory { get; }
RowRef<Lumina.Excel.Sheets.TerritoryType> Territory { get; }
/// <summary>
/// Gets the World this party member resides in.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.World> World { get; }
RowRef<Lumina.Excel.Sheets.World> World { get; }
/// <summary>
/// Gets the displayname of this party member.
@ -91,7 +93,7 @@ public interface IPartyMember
/// <summary>
/// Gets the classjob of this party member.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.ClassJob> ClassJob { get; }
RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob { get; }
/// <summary>
/// Gets the level of this party member.
@ -169,17 +171,17 @@ internal unsafe class PartyMember : IPartyMember
/// <summary>
/// Gets the territory this party member is located in.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> Territory => new(this.Struct->TerritoryType);
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryType);
/// <summary>
/// Gets the World this party member resides in.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.World> World => new(this.Struct->HomeWorld);
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(this.Struct->HomeWorld);
/// <summary>
/// Gets the displayname of this party member.
/// </summary>
public SeString Name => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref Struct->Name[0]), 0x40);
public SeString Name => SeString.Parse(this.Struct->Name);
/// <summary>
/// Gets the sex of this party member.
@ -189,7 +191,7 @@ internal unsafe class PartyMember : IPartyMember
/// <summary>
/// Gets the classjob of this party member.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.ClassJob> ClassJob => new(this.Struct->ClassJob);
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(this.Struct->ClassJob);
/// <summary>
/// Gets the level of this party member.

View file

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

View file

@ -1,6 +1,8 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Statuses;
@ -31,7 +33,7 @@ public unsafe class Status
/// <summary>
/// Gets the GameData associated with this status.
/// </summary>
public Lumina.Excel.GeneratedSheets.Status GameData => new ExcelResolver<Lumina.Excel.GeneratedSheets.Status>(this.Struct->StatusId).GameData;
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(this.Struct->StatusId);
/// <summary>
/// Gets the parameter value of the status.

View file

@ -13,6 +13,6 @@ internal sealed class GameConfigAddressResolver : BaseAddressResolver
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner scanner)
{
this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E");
this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E"); // unnamed in CS
}
}

View file

@ -16,6 +16,6 @@ internal class DutyStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig)
{
this.ContentDirectorNetworkMessage = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 33 FF 48 8B D9 41 0F B7 08");
this.ContentDirectorNetworkMessage = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 33 FF 48 8B D9 41 0F B7 08"); // unnamed in cs
}
}

View file

@ -2,7 +2,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -16,6 +15,8 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace Dalamud.Game;
/// <summary>
@ -31,11 +32,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework
private readonly Stopwatch updateStopwatch = new();
private readonly HitchDetector hitchDetector;
private readonly Hook<OnUpdateDetour> updateHook;
private readonly Hook<OnRealDestroyDelegate> destroyHook;
private readonly Hook<CSFramework.Delegates.Tick> updateHook;
private readonly Hook<CSFramework.Delegates.Destroy> destroyHook;
private readonly FrameworkAddressResolver addressResolver;
[ServiceManager.ServiceDependency]
private readonly GameLifecycle lifecycle = Service<GameLifecycle>.Get();
@ -51,13 +50,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
private ulong tickCounter;
[ServiceManager.ServiceConstructor]
private Framework(TargetSigScanner sigScanner)
private unsafe Framework()
{
this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch);
this.addressResolver = new FrameworkAddressResolver();
this.addressResolver.Setup(sigScanner);
this.frameworkDestroy = new();
this.frameworkThreadTaskScheduler = new();
this.FrameworkThreadTaskFactory = new(
@ -66,23 +62,13 @@ internal sealed class Framework : IInternalDisposableService, IFramework
TaskContinuationOptions.None,
this.frameworkThreadTaskScheduler);
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
this.updateHook = Hook<CSFramework.Delegates.Tick>.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Tick, this.HandleFrameworkUpdate);
this.destroyHook = Hook<CSFramework.Delegates.Destroy>.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Destroy, this.HandleFrameworkDestroy);
this.updateHook.Enable();
this.destroyHook.Enable();
}
/// <summary>
/// A delegate type used during the native Framework::destroy.
/// </summary>
/// <param name="framework">The native Framework address.</param>
/// <returns>A value indicating if the call was successful.</returns>
public delegate bool OnRealDestroyDelegate(IntPtr framework);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate bool OnUpdateDetour(IntPtr framework);
/// <inheritdoc/>
public event IFramework.OnUpdateDelegate? Update;
@ -390,7 +376,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
}
}
private bool HandleFrameworkUpdate(IntPtr framework)
private unsafe bool HandleFrameworkUpdate(CSFramework* thisPtr)
{
this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread;
@ -483,10 +469,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
this.hitchDetector.Stop();
original:
return this.updateHook.OriginalDisposeSafe(framework);
return this.updateHook.OriginalDisposeSafe(thisPtr);
}
private bool HandleFrameworkDestroy(IntPtr framework)
private unsafe bool HandleFrameworkDestroy(CSFramework* thisPtr)
{
this.frameworkDestroy.Cancel();
this.DispatchUpdateEvents = false;
@ -504,7 +490,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
ServiceManager.WaitForServiceUnload();
Log.Information("Framework::Destroy OK!");
return this.destroyHook.OriginalDisposeSafe(framework);
return this.destroyHook.OriginalDisposeSafe(thisPtr);
}
}

View file

@ -1,40 +0,0 @@
namespace Dalamud.Game;
/// <summary>
/// The address resolver for the <see cref="Framework"/> class.
/// </summary>
internal sealed class FrameworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address for the function that is called once the Framework is destroyed.
/// </summary>
public IntPtr DestroyAddress { get; private set; }
/// <summary>
/// Gets the address for the function that is called once the Framework is free'd.
/// </summary>
public IntPtr FreeAddress { get; private set; }
/// <summary>
/// Gets the function that is called every tick.
/// </summary>
public IntPtr TickAddress { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.SetupFramework(sig);
}
private void SetupFramework(ISigScanner scanner)
{
this.DestroyAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 3D ?? ?? ?? ?? 48 8B D9 48 85 FF");
this.FreeAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B D9 48 8B 0D ?? ?? ?? ?? 48 85 C9");
this.TickAddress =
scanner.ScanText("40 53 48 83 EC 20 FF 81 ?? ?? ?? ?? 48 8B D9 48 8D 4C 24 ??");
}
}

View file

@ -11,11 +11,19 @@ using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LinkMacroPayloadType = Lumina.Text.Payloads.LinkMacroPayloadType;
using LuminaSeStringBuilder = Lumina.Text.SeStringBuilder;
using ReadOnlySePayloadType = Lumina.Text.ReadOnly.ReadOnlySePayloadType;
using ReadOnlySeStringSpan = Lumina.Text.ReadOnly.ReadOnlySeStringSpan;
namespace Dalamud.Game.Gui;
@ -27,14 +35,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
{
private static readonly ModuleLog Log = new("ChatGui");
private readonly ChatGuiAddressResolver address;
private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
private readonly Hook<InventoryItem.Delegates.Copy> inventoryItemCopyHook;
private readonly Hook<LogViewer.Delegates.HandleLinkClick> handleLinkClickHook;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -42,29 +48,20 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
private ImmutableDictionary<(string PluginName, uint CommandId), Action<uint, SeString>>? dalamudLinkHandlersCopy;
[ServiceManager.ServiceConstructor]
private ChatGui(TargetSigScanner sigScanner)
private ChatGui()
{
this.address = new ChatGuiAddressResolver();
this.address.Setup(sigScanner);
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress((nint)RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.inventoryItemCopyHook = Hook<InventoryItem.Delegates.Copy>.FromAddress((nint)InventoryItem.StaticVirtualTablePointer->Copy, this.InventoryItemCopyDetour);
this.handleLinkClickHook = Hook<LogViewer.Delegates.HandleLinkClick>.FromAddress(LogViewer.Addresses.HandleLinkClick.Value, this.HandleLinkClickDetour);
this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
this.inventoryItemCopyHook.Enable();
this.handleLinkClickHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr);
/// <inheritdoc/>
public event IChatGui.OnMessageDelegate? ChatMessage;
@ -78,7 +75,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/>
public int LastLinkedItemId { get; private set; }
public uint LastLinkedItemId { get; private set; }
/// <inheritdoc/>
public byte LastLinkedItemFlags { get; private set; }
@ -106,8 +103,8 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
void IInternalDisposableService.DisposeService()
{
this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose();
this.interactableLinkClickedHook.Dispose();
this.inventoryItemCopyHook.Dispose();
this.handleLinkClickHook.Dispose();
}
/// <inheritdoc/>
@ -171,8 +168,10 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
var sender = Utf8String.FromSequence(chat.Name.Encode());
var message = Utf8String.FromSequence(replacedMessage.BuiltString.Encode());
var targetChannel = chat.Type ?? this.configuration.GeneralChatType;
this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, chat.Timestamp, (byte)(chat.Silent ? 1 : 0));
this.HandlePrintMessageDetour(RaptureLogModule.Instance(), targetChannel, sender, message, chat.Timestamp, (byte)(chat.Silent ? 1 : 0));
sender->Dtor(true);
message->Dtor(true);
@ -275,21 +274,20 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
});
}
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr)
private void InventoryItemCopyDetour(InventoryItem* thisPtr, InventoryItem* otherPtr)
{
this.inventoryItemCopyHook.Original(thisPtr, otherPtr);
try
{
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
this.LastLinkedItemId = otherPtr->ItemId;
this.LastLinkedItemFlags = (byte)otherPtr->Flags;
this.LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8);
this.LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14);
// Log.Verbose($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{this.LastLinkedItemId}");
// Log.Verbose($"InventoryItemCopyDetour {thisPtr} {otherPtr} - linked:{this.LastLinkedItemId}");
}
catch (Exception ex)
{
Log.Error(ex, "Exception onPopulateItemLink hook.");
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
Log.Error(ex, "Exception in InventoryItemCopyHook");
}
}
@ -299,58 +297,57 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
try
{
var originalSenderData = sender->AsSpan().ToArray();
var originalMessageData = message->AsSpan().ToArray();
var parsedSender = SeString.Parse(sender->AsSpan());
var parsedMessage = SeString.Parse(message->AsSpan());
var parsedSender = SeString.Parse(originalSenderData);
var parsedMessage = SeString.Parse(originalMessageData);
var terminatedSender = parsedSender.EncodeWithNullTerminator();
var terminatedMessage = parsedMessage.EncodeWithNullTerminator();
// Call events
var isHandled = false;
var invocationList = this.CheckMessageHandled!.GetInvocationList();
foreach (var @delegate in invocationList)
if (this.CheckMessageHandled is { } handledCallback)
{
try
{
var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate;
messageHandledDelegate!.Invoke(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", @delegate.Method.Name);
}
}
if (!isHandled)
{
invocationList = this.ChatMessage!.GetInvocationList();
foreach (var @delegate in invocationList)
foreach (var action in handledCallback.GetInvocationList().Cast<IChatGui.OnCheckMessageHandledDelegate>())
{
try
{
var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate;
messageHandledDelegate!.Invoke(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
action(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", @delegate.Method.Name);
Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", action.Method);
}
}
}
var possiblyModifiedSenderData = parsedSender.Encode();
var possiblyModifiedMessageData = parsedMessage.Encode();
if (!Util.FastByteArrayCompare(originalSenderData, possiblyModifiedSenderData))
if (!isHandled && this.ChatMessage is { } callback)
{
Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(originalSenderData)} -> {parsedSender}");
foreach (var action in callback.GetInvocationList().Cast<IChatGui.OnMessageDelegate>())
{
try
{
action(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", action.Method);
}
}
}
var possiblyModifiedSenderData = parsedSender.EncodeWithNullTerminator();
var possiblyModifiedMessageData = parsedMessage.EncodeWithNullTerminator();
if (!terminatedSender.SequenceEqual(possiblyModifiedSenderData))
{
Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(terminatedSender)} -> {parsedSender}");
sender->SetString(possiblyModifiedSenderData);
}
if (!Util.FastByteArrayCompare(originalMessageData, possiblyModifiedMessageData))
if (!terminatedMessage.SequenceEqual(possiblyModifiedMessageData))
{
Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(originalMessageData)} -> {parsedMessage}");
Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(terminatedMessage)} -> {parsedMessage}");
message->SetString(possiblyModifiedMessageData);
}
@ -374,42 +371,57 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
return messageId;
}
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr)
private void HandleLinkClickDetour(LogViewer* thisPtr, LinkData* linkData)
{
if ((Payload.EmbeddedInfoType)(linkData->LinkType + 1) != Payload.EmbeddedInfoType.DalamudLink)
{
this.handleLinkClickHook.Original(thisPtr, linkData);
return;
}
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
var sb = LuminaSeStringBuilder.SharedPool.Get();
try
{
var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1);
var seStringSpan = new ReadOnlySeStringSpan(linkData->Payload);
if (interactableType != Payload.EmbeddedInfoType.DalamudLink)
// read until link terminator
foreach (var payload in seStringSpan)
{
this.interactableLinkClickedHook.Original(managerPtr, messagePtr);
return;
sb.Append(payload);
if (payload.Type == ReadOnlySePayloadType.Macro &&
payload.MacroCode == Lumina.Text.Payloads.MacroCode.Link &&
payload.TryGetExpression(out var expr1) &&
expr1.TryGetInt(out var expr1Val) &&
expr1Val == (int)LinkMacroPayloadType.Terminator)
{
break;
}
}
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
var seStr = SeString.Parse(sb.ToArray());
if (seStr.Payloads.Count == 0 || seStr.Payloads[0] is not DalamudLinkPayload link)
return;
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10);
var seStr = MemoryHelper.ReadSeStringNullTerminated(payloadPtr);
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
if (payloads.Count == 0) return;
var linkPayload = payloads[0];
if (linkPayload is DalamudLinkPayload link)
if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
{
if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
{
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
value.Invoke(link.CommandId, new SeString(payloads));
}
else
{
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
}
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
value.Invoke(link.CommandId, seStr);
}
else
{
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
}
}
catch (Exception ex)
{
Log.Error(ex, "Exception on InteractableLinkClicked hook");
Log.Error(ex, "Exception in HandleLinkClickDetour");
}
finally
{
LuminaSeStringBuilder.SharedPool.Return(sb);
}
}
}
@ -451,7 +463,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/>
public int LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
public uint LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
/// <inheritdoc/>
public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags;

View file

@ -1,28 +0,0 @@
namespace Dalamud.Game.Gui;
/// <summary>
/// The address resolver for the <see cref="ChatGui"/> class.
/// </summary>
internal sealed class ChatGuiAddressResolver : BaseAddressResolver
{
/// <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; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
// PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
// PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0
this.PopulateItemLinkObject = sig.ScanText("E8 ?? ?? ?? ?? 8B 4E FC");
this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 4B ?? E8 ?? ?? ?? ?? 33 D2");
}
}

View file

@ -1,7 +1,7 @@
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.Gui.ContextMenu;

View file

@ -1,11 +1,12 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Network.Structures.InfoProxy;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.Gui.ContextMenu;
@ -46,7 +47,7 @@ public sealed unsafe class MenuTargetDefault : MenuTarget
/// <summary>
/// Gets the home world id of the target.
/// </summary>
public ExcelResolver<World> TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId);
public RowRef<World> TargetHomeWorld => LuminaUtils.CreateRef<World>((uint)this.Context->TargetHomeWorldId);
/// <summary>
/// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members.

View file

@ -85,8 +85,7 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
/// <summary>
/// Class representing an entry in the server info bar.
/// </summary>
[Api11ToDo(Api11ToDoAttribute.MakeInternal)]
public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
{
private readonly DalamudConfiguration configuration;

View file

@ -1,3 +1,4 @@
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
@ -8,6 +9,9 @@ using Dalamud.IoC.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
namespace Dalamud.Game.Gui.FlyText;
@ -19,62 +23,21 @@ namespace Dalamud.Game.Gui.FlyText;
internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
{
/// <summary>
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>.
/// The hook that fires when the game creates a fly text element.
/// </summary>
private readonly AddFlyTextDelegate addFlyTextNative;
/// <summary>
/// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>.
/// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
private readonly Hook<AddonFlyText.Delegates.CreateFlyText> createFlyTextHook;
[ServiceManager.ServiceConstructor]
private FlyTextGui(TargetSigScanner sigScanner)
private unsafe FlyTextGui(TargetSigScanner sigScanner)
{
this.Address = new FlyTextGuiAddressResolver();
this.Address.Setup(sigScanner);
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour);
this.createFlyTextHook = Hook<AddonFlyText.Delegates.CreateFlyText>.FromAddress(AddonFlyText.Addresses.CreateFlyText.Value, this.CreateFlyTextDetour);
this.createFlyTextHook.Enable();
}
/// <summary>
/// Private delegate for the native CreateFlyText function's hook.
/// </summary>
private delegate IntPtr CreateFlyTextDelegate(
IntPtr addonFlyText,
FlyTextKind kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
uint damageTypeIcon,
IntPtr text1,
float yOffset);
/// <summary>
/// Private delegate for the native AddFlyText function pointer.
/// </summary>
private delegate void AddFlyTextDelegate(
IntPtr addonFlyText,
uint actorIndex,
uint messageMax,
IntPtr numbers,
uint offsetNum,
uint offsetNumMax,
IntPtr strings,
uint offsetStr,
uint offsetStrMax,
int unknown);
/// <inheritdoc/>
public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated;
private FlyTextGuiAddressResolver Address { get; }
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
@ -87,26 +50,16 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon)
{
// Known valid flytext region within the atk arrays
var numIndex = 30;
var strIndex = 27;
var numOffset = 161u;
var strOffset = 28u;
// Get the UI module and flytext addon pointers
var gameGui = Service<GameGui>.GetNullable();
if (gameGui == null)
return;
var ui = (FFXIVClientStructs.FFXIV.Client.UI.UIModule*)gameGui.GetUIModule();
var flytext = gameGui.GetAddonByName("_FlyText");
if (ui == null || flytext == IntPtr.Zero)
var flytext = (AddonFlyText*)RaptureAtkUnitManager.Instance()->GetAddonByName("_FlyText");
if (flytext == null)
return;
// Get the number and string arrays we need
var atkArrayDataHolder = ui->GetRaptureAtkModule()->AtkModule.AtkArrayDataHolder;
var numArray = atkArrayDataHolder._NumberArrays[numIndex];
var strArray = atkArrayDataHolder._StringArrays[strIndex];
var numArray = AtkStage.Instance()->GetNumberArrayData(NumberArrayType.FlyText);
var strArray = AtkStage.Instance()->GetStringArrayData(StringArrayType.FlyText);
// Write the values to the arrays using a known valid flytext region
numArray->IntArray[numOffset + 0] = 1; // Some kind of "Enabled" flag for this section
@ -120,66 +73,47 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
numArray->IntArray[numOffset + 8] = 0; // Unknown
numArray->IntArray[numOffset + 9] = 0; // Unknown, has something to do with yOffset
strArray->SetValue((int)strOffset + 0, text1.Encode(), false, true, false);
strArray->SetValue((int)strOffset + 1, text2.Encode(), false, true, false);
this.addFlyTextNative(
flytext,
actorIndex,
1,
(IntPtr)numArray,
numOffset,
10,
(IntPtr)strArray,
strOffset,
2,
0);
strArray->SetValue((int)strOffset + 0, text1.EncodeWithNullTerminator(), false, true, false);
strArray->SetValue((int)strOffset + 1, text2.EncodeWithNullTerminator(), false, true, false);
flytext->AddFlyText(actorIndex, 1, numArray, numOffset, 10, strArray, strOffset, 2, 0);
}
private static byte[] Terminate(byte[] source)
{
var terminated = new byte[source.Length + 1];
Array.Copy(source, 0, terminated, 0, source.Length);
terminated[^1] = 0;
return terminated;
}
private IntPtr CreateFlyTextDetour(
IntPtr addonFlyText,
FlyTextKind kind,
private unsafe nint CreateFlyTextDetour(
AddonFlyText* thisPtr,
int kind,
int val1,
int val2,
IntPtr text2,
byte* text2,
uint color,
uint icon,
uint damageTypeIcon,
IntPtr text1,
byte* text1,
float yOffset)
{
var retVal = IntPtr.Zero;
var retVal = nint.Zero;
try
{
Log.Verbose("[FlyText] Enter CreateFlyText detour!");
var handled = false;
var tmpKind = kind;
var tmpKind = (FlyTextKind)kind;
var tmpVal1 = val1;
var tmpVal2 = val2;
var tmpText1 = text1 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text1);
var tmpText2 = text2 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text2);
var tmpText1 = text1 == null ? string.Empty : MemoryHelper.ReadSeStringNullTerminated((nint)text1);
var tmpText2 = text2 == null ? string.Empty : MemoryHelper.ReadSeStringNullTerminated((nint)text2);
var tmpColor = color;
var tmpIcon = icon;
var tmpDamageTypeIcon = damageTypeIcon;
var tmpYOffset = yOffset;
var cmpText1 = tmpText1.ToString();
var cmpText2 = tmpText2.ToString();
var originalText1 = tmpText1.EncodeWithNullTerminator();
var originalText2 = tmpText2.EncodeWithNullTerminator();
Log.Verbose($"[FlyText] Called with addonFlyText({addonFlyText.ToInt64():X}) " +
Log.Verbose($"[FlyText] Called with addonFlyText({(nint)thisPtr:X}) " +
$"kind({kind}) val1({val1}) val2({val2}) damageTypeIcon({damageTypeIcon}) " +
$"text1({text1.ToInt64():X}, \"{tmpText1}\") text2({text2.ToInt64():X}, \"{tmpText2}\") " +
$"text1({(nint)text1:X}, \"{tmpText1}\") text2({(nint)text2:X}, \"{tmpText2}\") " +
$"color({color:X}) icon({icon}) yOffset({yOffset})");
Log.Verbose("[FlyText] Calling flytext events!");
this.FlyTextCreated?.Invoke(
@ -204,12 +138,15 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
return IntPtr.Zero;
}
var maybeModifiedText1 = tmpText1.EncodeWithNullTerminator();
var maybeModifiedText2 = tmpText2.EncodeWithNullTerminator();
// Check if any values have changed
var dirty = tmpKind != kind ||
var dirty = (int)tmpKind != kind ||
tmpVal1 != val1 ||
tmpVal2 != val2 ||
tmpText1.ToString() != cmpText1 ||
tmpText2.ToString() != cmpText2 ||
!maybeModifiedText1.SequenceEqual(originalText1) ||
!maybeModifiedText2.SequenceEqual(originalText2) ||
tmpDamageTypeIcon != damageTypeIcon ||
tmpColor != color ||
tmpIcon != icon ||
@ -219,28 +156,26 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
if (!dirty)
{
Log.Verbose("[FlyText] Calling flytext with original args.");
return this.createFlyTextHook.Original(addonFlyText, kind, val1, val2, text2, color, icon,
return this.createFlyTextHook.Original(thisPtr, kind, val1, val2, text2, color, icon,
damageTypeIcon, text1, yOffset);
}
var terminated1 = Terminate(tmpText1.Encode());
var terminated2 = Terminate(tmpText2.Encode());
var pText1 = Marshal.AllocHGlobal(terminated1.Length);
var pText2 = Marshal.AllocHGlobal(terminated2.Length);
Marshal.Copy(terminated1, 0, pText1, terminated1.Length);
Marshal.Copy(terminated2, 0, pText2, terminated2.Length);
var pText1 = Marshal.AllocHGlobal(maybeModifiedText1.Length);
var pText2 = Marshal.AllocHGlobal(maybeModifiedText2.Length);
Marshal.Copy(maybeModifiedText1, 0, pText1, maybeModifiedText1.Length);
Marshal.Copy(maybeModifiedText2, 0, pText2, maybeModifiedText2.Length);
Log.Verbose("[FlyText] Allocated and set strings.");
retVal = this.createFlyTextHook.Original(
addonFlyText,
tmpKind,
thisPtr,
(int)tmpKind,
tmpVal1,
tmpVal2,
pText2,
(byte*)pText2,
tmpColor,
tmpIcon,
tmpDamageTypeIcon,
pText1,
(byte*)pText1,
tmpYOffset);
Log.Verbose("[FlyText] Returned from original. Delaying free task.");

View file

@ -1,29 +0,0 @@
namespace Dalamud.Game.Gui.FlyText;
/// <summary>
/// An address resolver for the <see cref="FlyTextGui"/> class.
/// </summary>
internal class FlyTextGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native AddFlyText method, which occurs
/// when the game adds fly text elements to the UI. Multiple fly text
/// elements can be added in a single AddFlyText call.
/// </summary>
public IntPtr AddFlyText { get; private set; }
/// <summary>
/// Gets the address of the native CreateFlyText method, which occurs
/// when the game creates a new fly text element. This method is called
/// once per fly text element, and can be called multiple times per
/// AddFlyText call.
/// </summary>
public IntPtr CreateFlyText { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.AddFlyText = sig.ScanText("E8 ?? ?? ?? ?? FF C7 41 D1 C7");
this.CreateFlyText = sig.ScanText("E8 ?? ?? ?? ?? 48 8B F8 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B 18");
}
}

View file

@ -1,4 +1,3 @@
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Game.Text.SeStringHandling.Payloads;
@ -18,7 +17,6 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Common.Component.BGCollision;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using SharpDX;
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
@ -33,20 +31,18 @@ namespace Dalamud.Game.Gui;
internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
{
private static readonly ModuleLog Log = new("GameGui");
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly GameGuiAddressResolver address;
private readonly Hook<SetGlobalBgmDelegate> setGlobalBgmHook;
private readonly Hook<HandleItemHoverDelegate> handleItemHoverHook;
private readonly Hook<HandleItemOutDelegate> handleItemOutHook;
private readonly Hook<HandleActionHoverDelegate> handleActionHoverHook;
private readonly Hook<HandleActionOutDelegate> handleActionOutHook;
private readonly Hook<AgentActionDetail.Delegates.HandleActionHover> handleActionHoverHook;
private readonly Hook<AgentActionDetail.Delegates.ReceiveEvent> handleActionOutHook;
private readonly Hook<HandleImmDelegate> handleImmHook;
private readonly Hook<ToggleUiHideDelegate> toggleUiHideHook;
private readonly Hook<Utf8StringFromSequenceDelegate> utf8StringFromSequenceHook;
private GetUIMapObjectDelegate? getUIMapObject;
private OpenMapWithFlagDelegate? openMapWithFlag;
private readonly Hook<RaptureAtkModule.Delegates.SetUiVisibility> setUiVisibilityHook;
private readonly Hook<Utf8String.Delegates.Ctor_FromSequence> utf8StringFromSequenceHook;
[ServiceManager.ServiceConstructor]
private GameGui(TargetSigScanner sigScanner)
@ -57,66 +53,37 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
Log.Verbose("===== G A M E G U I =====");
Log.Verbose($"GameGuiManager address {Util.DescribeAddress(this.address.BaseAddress)}");
Log.Verbose($"SetGlobalBgm address {Util.DescribeAddress(this.address.SetGlobalBgm)}");
Log.Verbose($"HandleItemHover address {Util.DescribeAddress(this.address.HandleItemHover)}");
Log.Verbose($"HandleItemOut address {Util.DescribeAddress(this.address.HandleItemOut)}");
Log.Verbose($"HandleImm address {Util.DescribeAddress(this.address.HandleImm)}");
this.setGlobalBgmHook = Hook<SetGlobalBgmDelegate>.FromAddress(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour);
this.handleItemHoverHook = Hook<HandleItemHoverDelegate>.FromAddress(this.address.HandleItemHover, this.HandleItemHoverDetour);
this.handleItemOutHook = Hook<HandleItemOutDelegate>.FromAddress(this.address.HandleItemOut, this.HandleItemOutDetour);
this.handleActionHoverHook = Hook<HandleActionHoverDelegate>.FromAddress(this.address.HandleActionHover, this.HandleActionHoverDetour);
this.handleActionOutHook = Hook<HandleActionOutDelegate>.FromAddress(this.address.HandleActionOut, this.HandleActionOutDetour);
this.handleActionHoverHook = Hook<AgentActionDetail.Delegates.HandleActionHover>.FromAddress(AgentActionDetail.Addresses.HandleActionHover.Value, this.HandleActionHoverDetour);
this.handleActionOutHook = Hook<AgentActionDetail.Delegates.ReceiveEvent>.FromAddress((nint)AgentActionDetail.StaticVirtualTablePointer->ReceiveEvent, this.HandleActionOutDetour);
this.handleImmHook = Hook<HandleImmDelegate>.FromAddress(this.address.HandleImm, this.HandleImmDetour);
this.toggleUiHideHook = Hook<ToggleUiHideDelegate>.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour);
this.setUiVisibilityHook = Hook<RaptureAtkModule.Delegates.SetUiVisibility>.FromAddress((nint)RaptureAtkModule.StaticVirtualTablePointer->SetUiVisibility, this.SetUiVisibilityDetour);
this.utf8StringFromSequenceHook = Hook<Utf8StringFromSequenceDelegate>.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour);
this.utf8StringFromSequenceHook = Hook<Utf8String.Delegates.Ctor_FromSequence>.FromAddress(Utf8String.Addresses.Ctor_FromSequence.Value, this.Utf8StringFromSequenceDetour);
this.setGlobalBgmHook.Enable();
this.handleItemHoverHook.Enable();
this.handleItemOutHook.Enable();
this.handleImmHook.Enable();
this.toggleUiHideHook.Enable();
this.setUiVisibilityHook.Enable();
this.handleActionHoverHook.Enable();
this.handleActionOutHook.Enable();
this.utf8StringFromSequenceHook.Enable();
this.framework.Update += this.FrameworkUpdate;
}
// Hooked delegates
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate Utf8String* Utf8StringFromSequenceDelegate(Utf8String* thisPtr, byte* sourcePtr, nuint sourceLen);
[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 char HandleImmDelegate(IntPtr framework, char a2, byte a3);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, bool uiVisible);
/// <inheritdoc/>
public event EventHandler<bool>? UiHideToggled;
@ -137,33 +104,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
/// <inheritdoc/>
public bool OpenMapWithMapLink(MapLinkPayload mapLink)
{
var uiModule = this.GetUIModule();
if (uiModule == IntPtr.Zero)
{
Log.Error("OpenMapWithMapLink: Null pointer returned from getUIObject()");
return false;
}
this.getUIMapObject ??= this.address.GetVirtualFunction<GetUIMapObjectDelegate>(uiModule, 0, 8);
var uiMapObjectPtr = this.getUIMapObject(uiModule);
if (uiMapObjectPtr == IntPtr.Zero)
{
Log.Error("OpenMapWithMapLink: Null pointer returned from GetUIMapObject()");
return false;
}
this.openMapWithFlag ??= this.address.GetVirtualFunction<OpenMapWithFlagDelegate>(uiMapObjectPtr, 0, 63);
var mapLinkString = mapLink.DataString;
Log.Debug($"OpenMapWithMapLink: Opening Map Link: {mapLinkString}");
return this.openMapWithFlag(uiMapObjectPtr, mapLinkString);
}
=> RaptureAtkModule.Instance()->OpenMapWithMapLink(mapLink.DataString);
/// <inheritdoc/>
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos)
@ -188,7 +129,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
inView = false;
return false;
}
pCoords *= MathF.Abs(1.0f / pCoords.W);
screenPos = new Vector2(pCoords.X, pCoords.Y);
@ -216,7 +157,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
worldPos = default;
return false;
}
var camera = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.CameraManager.Instance()->CurrentCamera;
if (camera == null)
{
@ -271,7 +212,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
/// <inheritdoc/>
public IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon);
/// <inheritdoc/>
public IntPtr FindAgentInterface(IntPtr addonPtr)
{
@ -311,11 +252,11 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
/// </summary>
void IInternalDisposableService.DisposeService()
{
this.framework.Update -= this.FrameworkUpdate;
this.setGlobalBgmHook.Dispose();
this.handleItemHoverHook.Dispose();
this.handleItemOutHook.Dispose();
this.handleImmHook.Dispose();
this.toggleUiHideHook.Dispose();
this.setUiVisibilityHook.Dispose();
this.handleActionHoverHook.Dispose();
this.handleActionOutHook.Dispose();
this.utf8StringFromSequenceHook.Dispose();
@ -359,69 +300,24 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
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;
this.HoveredItemChanged?.InvokeSafely(this, itemId);
Log.Verbose($"HoverItemId:{itemId} this:{hoverState.ToInt64()}");
}
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)
private void HandleActionHoverDetour(AgentActionDetail* hoverState, ActionKind actionKind, uint actionId, int a4, byte a5)
{
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
this.HoveredAction.ActionKind = actionKind;
this.HoveredAction.ActionKind = (HoverActionKind)actionKind;
this.HoveredAction.BaseActionID = actionId;
this.HoveredAction.ActionID = (uint)Marshal.ReadInt32(hoverState, 0x3C);
this.HoveredAction.ActionID = hoverState->ActionId;
this.HoveredActionChanged?.InvokeSafely(this, this.HoveredAction);
Log.Verbose($"HoverActionId: {actionKind}/{actionId} this:{hoverState.ToInt64():X}");
Log.Verbose($"HoverActionId: {actionKind}/{actionId} this:{(nint)hoverState:X}");
}
private IntPtr HandleActionOutDetour(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4)
private AtkValue* HandleActionOutDetour(AgentActionDetail* agentActionDetail, AtkValue* a2, AtkValue* a3, uint a4, ulong a5)
{
var retVal = this.handleActionOutHook.Original(agentActionDetail, a2, a3, a4);
var retVal = this.handleActionOutHook.Original(agentActionDetail, a2, a3, a4, a5);
if (a3 != IntPtr.Zero && a4 == 1)
if (a3 != null && a4 == 1)
{
var a3Val = Marshal.ReadByte(a3, 0x8);
var a3Val = a3->Int;
if (a3Val == 255)
{
@ -445,16 +341,14 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return retVal;
}
private IntPtr ToggleUiHideDetour(IntPtr thisPtr, bool unknownByte)
private unsafe void SetUiVisibilityDetour(RaptureAtkModule* thisPtr, bool uiVisible)
{
var result = this.toggleUiHideHook.Original(thisPtr, unknownByte);
this.setUiVisibilityHook.Original(thisPtr, uiVisible);
this.GameUiHidden = !RaptureAtkModule.Instance()->IsUiVisible;
this.UiHideToggled?.InvokeSafely(this, this.GameUiHidden);
Log.Debug("UiHide toggled: {0}", this.GameUiHidden);
return result;
Log.Debug("GameUiHidden: {0}", this.GameUiHidden);
}
private char HandleImmDetour(IntPtr framework, char a2, byte a3)
@ -477,6 +371,24 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return thisPtr; // this function shouldn't need to return but the original asm moves this into rax before returning so be safe?
}
private unsafe void FrameworkUpdate(IFramework framework)
{
var agentItemDetail = AgentItemDetail.Instance();
if (agentItemDetail != null)
{
var itemId = agentItemDetail->ItemId;
if (this.HoveredItem != itemId)
{
Log.Verbose($"HoveredItem changed: {itemId}");
this.HoveredItem = itemId;
this.HoveredItemChanged?.InvokeSafely(this, itemId);
}
}
}
}
/// <summary>
@ -501,13 +413,13 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
this.gameGuiService.HoveredItemChanged += this.HoveredItemForward;
this.gameGuiService.HoveredActionChanged += this.HoveredActionForward;
}
/// <inheritdoc/>
public event EventHandler<bool>? UiHideToggled;
/// <inheritdoc/>
public event EventHandler<ulong>? HoveredItemChanged;
/// <inheritdoc/>
public event EventHandler<HoveredAction>? HoveredActionChanged;
@ -523,7 +435,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
/// <inheritdoc/>
public HoveredAction HoveredAction => this.gameGuiService.HoveredAction;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
@ -535,7 +447,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
this.HoveredItemChanged = null;
this.HoveredActionChanged = null;
}
/// <inheritdoc/>
public bool OpenMapWithMapLink(MapLinkPayload mapLink)
=> this.gameGuiService.OpenMapWithMapLink(mapLink);
@ -543,7 +455,7 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
/// <inheritdoc/>
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos)
=> this.gameGuiService.WorldToScreen(worldPos, out screenPos);
/// <inheritdoc/>
public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos, out bool inView)
=> this.gameGuiService.WorldToScreen(worldPos, out screenPos, out inView);
@ -555,26 +467,26 @@ internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui
/// <inheritdoc/>
public IntPtr GetUIModule()
=> this.gameGuiService.GetUIModule();
/// <inheritdoc/>
public IntPtr GetAddonByName(string name, int index = 1)
=> this.gameGuiService.GetAddonByName(name, index);
/// <inheritdoc/>
public IntPtr FindAgentInterface(string addonName)
=> this.gameGuiService.FindAgentInterface(addonName);
/// <inheritdoc/>
public unsafe IntPtr FindAgentInterface(void* addon)
public unsafe IntPtr FindAgentInterface(void* addon)
=> this.gameGuiService.FindAgentInterface(addon);
/// <inheritdoc/>
public IntPtr FindAgentInterface(IntPtr addonPtr)
public IntPtr FindAgentInterface(IntPtr addonPtr)
=> this.gameGuiService.FindAgentInterface(addonPtr);
private void UiHideToggledForward(object sender, bool toggled) => this.UiHideToggled?.Invoke(sender, toggled);
private void HoveredItemForward(object sender, ulong itemId) => this.HoveredItemChanged?.Invoke(sender, itemId);
private void HoveredActionForward(object sender, HoveredAction hoverAction) => this.HoveredActionChanged?.Invoke(sender, hoverAction);
}

View file

@ -15,52 +15,15 @@ internal sealed class GameGuiAddressResolver : BaseAddressResolver
/// </summary>
public IntPtr SetGlobalBgm { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemHover method.
/// </summary>
public IntPtr HandleItemHover { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemOut method.
/// </summary>
public IntPtr HandleItemOut { get; private set; }
/// <summary>
/// Gets the address of the native HandleActionHover method.
/// </summary>
public IntPtr HandleActionHover { get; private set; }
/// <summary>
/// Gets the address of the native HandleActionOut method.
/// </summary>
public IntPtr HandleActionOut { get; private set; }
/// <summary>
/// Gets the address of the native HandleImm method.
/// </summary>
public IntPtr HandleImm { get; private set; }
/// <summary>
/// Gets the address of the native ToggleUiHide method.
/// </summary>
public IntPtr ToggleUiHide { get; private set; }
/// <summary>
/// Gets the address of the native Utf8StringFromSequence method.
/// </summary>
public IntPtr Utf8StringFromSequence { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.SetGlobalBgm = sig.ScanText("E8 ?? ?? ?? ?? 8B 2F");
this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 6C 24 48 48 8B 74 24 50 4C 89 B7 08 01 00 00");
this.HandleItemOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 4D");
this.HandleActionHover = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 83 F8 0F");
this.HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F");
this.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09");
this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 44 0F B6 81");
this.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8");
this.SetGlobalBgm = sig.ScanText("E8 ?? ?? ?? ?? 8B 2F"); // unnamed in CS
this.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09"); // unnamed in CS
}
}

View file

@ -16,6 +16,11 @@ public enum HoverActionKind
/// </summary>
Action = 28,
/// <summary>
/// A crafting action is hovered.
/// </summary>
CraftingAction = 29,
/// <summary>
/// A general action is hovered.
/// </summary>
@ -24,7 +29,7 @@ public enum HoverActionKind
/// <summary>
/// A companion order type of action is hovered.
/// </summary>
CompanionOrder = 31,
CompanionOrder = 31, // Game Term: BuddyOrder
/// <summary>
/// A main command type of action is hovered.
@ -36,6 +41,11 @@ public enum HoverActionKind
/// </summary>
ExtraCommand = 33,
/// <summary>
/// A companion action is hovered.
/// </summary>
Companion = 34,
/// <summary>
/// A pet order type of action is hovered.
/// </summary>
@ -45,4 +55,99 @@ public enum HoverActionKind
/// A trait is hovered.
/// </summary>
Trait = 36,
/// <summary>
/// A buddy action is hovered.
/// </summary>
BuddyAction = 37,
/// <summary>
/// A company action is hovered.
/// </summary>
CompanyAction = 38,
/// <summary>
/// A mount is hovered.
/// </summary>
Mount = 39,
/// <summary>
/// A chocobo race action is hovered.
/// </summary>
ChocoboRaceAction = 40,
/// <summary>
/// A chocobo race item is hovered.
/// </summary>
ChocoboRaceItem = 41,
/// <summary>
/// A deep dungeon equipment is hovered.
/// </summary>
DeepDungeonEquipment = 42,
/// <summary>
/// A deep dungeon equipment 2 is hovered.
/// </summary>
DeepDungeonEquipment2 = 43,
/// <summary>
/// A deep dungeon item is hovered.
/// </summary>
DeepDungeonItem = 44,
/// <summary>
/// A quick chat is hovered.
/// </summary>
QuickChat = 45,
/// <summary>
/// An action combo route is hovered.
/// </summary>
ActionComboRoute = 46,
/// <summary>
/// A pvp trait is hovered.
/// </summary>
PvPSelectTrait = 47,
/// <summary>
/// A squadron action is hovered.
/// </summary>
BgcArmyAction = 48,
/// <summary>
/// A perform action is hovered.
/// </summary>
Perform = 49,
/// <summary>
/// A deep dungeon magic stone is hovered.
/// </summary>
DeepDungeonMagicStone = 50,
/// <summary>
/// A deep dungeon demiclone is hovered.
/// </summary>
DeepDungeonDemiclone = 51,
/// <summary>
/// An eureka magia action is hovered.
/// </summary>
EurekaMagiaAction = 52,
/// <summary>
/// An island sanctuary temporary item is hovered.
/// </summary>
MYCTemporaryItem = 53,
/// <summary>
/// An ornament is hovered.
/// </summary>
Ornament = 54,
/// <summary>
/// Glasses are hovered.
/// </summary>
Glasses = 55,
}

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Objects;
@ -20,16 +20,6 @@ namespace Dalamud.Game.Gui.NamePlate;
[ServiceManager.EarlyLoadedService]
internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
{
/// <summary>
/// The index for the number array used by the NamePlate addon.
/// </summary>
public const int NumberArrayIndex = 5;
/// <summary>
/// The index for the string array used by the NamePlate addon.
/// </summary>
public const int StringArrayIndex = 4;
/// <summary>
/// The index for of the FullUpdate entry in the NamePlate number array.
/// </summary>
@ -81,18 +71,11 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
/// <inheritdoc/>
public unsafe void RequestRedraw()
{
var addon = this.gameGui.GetAddonByName("NamePlate");
if (addon != 0)
var addon = (AddonNamePlate*)this.gameGui.GetAddonByName("NamePlate");
if (addon != null)
{
var raptureAtkModule = RaptureAtkModule.Instance();
if (raptureAtkModule == null)
{
return;
}
((AddonNamePlate*)addon)->DoFullUpdate = 1;
var namePlateNumberArrayData = raptureAtkModule->AtkArrayDataHolder.NumberArrays[NumberArrayIndex];
namePlateNumberArrayData->SetValue(NumberArrayFullUpdateIndex, 1);
addon->DoFullUpdate = 1;
AtkStage.Instance()->GetNumberArrayData(NumberArrayType.NamePlate)->SetValue(NumberArrayFullUpdateIndex, 1);
}
}

View file

@ -140,9 +140,9 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
public void ResetState(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
this.Addon = (AddonNamePlate*)addon;
this.NumberData = numberArrayData[NamePlateGui.NumberArrayIndex];
this.NumberData = AtkStage.Instance()->GetNumberArrayData(NumberArrayType.NamePlate);
this.NumberStruct = (AddonNamePlate.AddonNamePlateNumberArray*)this.NumberData->IntArray;
this.StringData = stringArrayData[NamePlateGui.StringArrayIndex];
this.StringData = AtkStage.Instance()->GetStringArrayData(StringArrayType.NamePlate);
this.HasParts = false;
this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount;

View file

@ -1,4 +1,4 @@
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

View file

@ -1,18 +0,0 @@
namespace Dalamud.Game.Gui.PartyFinder;
/// <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; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.ReceiveListing = sig.ScanText("40 53 41 57 48 83 EC ?? 48 8B D9 4C 8B FA");
}
}

View file

@ -7,6 +7,8 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Serilog;
namespace Dalamud.Game.Gui.PartyFinder;
@ -15,12 +17,11 @@ namespace Dalamud.Game.Gui.PartyFinder;
/// This class handles interacting with the native PartyFinder window.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed class PartyFinderGui : IInternalDisposableService, IPartyFinderGui
internal sealed unsafe class PartyFinderGui : IInternalDisposableService, IPartyFinderGui
{
private readonly PartyFinderAddressResolver address;
private readonly nint memory;
private readonly Hook<ReceiveListingDelegate> receiveListingHook;
private readonly Hook<InfoProxyCrossRealm.Delegates.ReceiveListing> receiveListingHook;
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderGui"/> class.
@ -29,18 +30,12 @@ internal sealed class PartyFinderGui : IInternalDisposableService, IPartyFinderG
[ServiceManager.ServiceConstructor]
private PartyFinderGui(TargetSigScanner sigScanner)
{
this.address = new PartyFinderAddressResolver();
this.address.Setup(sigScanner);
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
this.receiveListingHook = Hook<ReceiveListingDelegate>.FromAddress(this.address.ReceiveListing, this.HandleReceiveListingDetour);
this.receiveListingHook = Hook<InfoProxyCrossRealm.Delegates.ReceiveListing>.FromAddress(InfoProxyCrossRealm.Addresses.ReceiveListing.Value, this.HandleReceiveListingDetour);
this.receiveListingHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ReceiveListingDelegate(nint managerPtr, nint data);
/// <inheritdoc/>
public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing;
@ -61,18 +56,18 @@ internal sealed class PartyFinderGui : IInternalDisposableService, IPartyFinderG
}
}
private void HandleReceiveListingDetour(nint managerPtr, nint data)
private void HandleReceiveListingDetour(InfoProxyCrossRealm* infoProxy, nint packet)
{
try
{
this.HandleListingEvents(data);
this.HandleListingEvents(packet);
}
catch (Exception ex)
{
Log.Error(ex, "Exception on ReceiveListing hook.");
}
this.receiveListingHook.Original(managerPtr, data);
this.receiveListingHook.Original(infoProxy, packet);
}
private void HandleListingEvents(nint data)

View file

@ -1,5 +1,5 @@
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.Gui.PartyFinder.Types;

View file

@ -5,7 +5,8 @@ using Dalamud.Data;
using Dalamud.Game.Gui.PartyFinder.Internal;
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.Gui.PartyFinder.Types;
@ -48,7 +49,7 @@ public interface IPartyFinderListing
/// <summary>
/// Gets a list of the classes/jobs that are currently present in the party.
/// </summary>
IReadOnlyCollection<Lazy<ClassJob>> JobsPresent { get; }
IReadOnlyCollection<RowRef<ClassJob>> JobsPresent { get; }
/// <summary>
/// Gets the ID assigned to this listing by the game's server.
@ -73,17 +74,17 @@ public interface IPartyFinderListing
/// <summary>
/// Gets the world that this listing was created on.
/// </summary>
Lazy<World> World { get; }
RowRef<World> World { get; }
/// <summary>
/// Gets the home world of the listing's host.
/// </summary>
Lazy<World> HomeWorld { get; }
RowRef<World> HomeWorld { get; }
/// <summary>
/// Gets the current world of the listing's host.
/// </summary>
Lazy<World> CurrentWorld { get; }
RowRef<World> CurrentWorld { get; }
/// <summary>
/// Gets the Party Finder category this listing is listed under.
@ -98,7 +99,7 @@ public interface IPartyFinderListing
/// <summary>
/// Gets the duty this listing is for. May be null for non-duty listings.
/// </summary>
Lazy<ContentFinderCondition> Duty { get; }
RowRef<ContentFinderCondition> Duty { get; }
/// <summary>
/// Gets the type of duty this listing is for.
@ -216,12 +217,12 @@ internal class PartyFinderListing : IPartyFinderListing
this.ContentId = listing.ContentId;
this.Name = SeString.Parse(listing.Name.TakeWhile(b => b != 0).ToArray());
this.Description = SeString.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.World = LuminaUtils.CreateRef<World>(listing.World);
this.HomeWorld = LuminaUtils.CreateRef<World>(listing.HomeWorld);
this.CurrentWorld = LuminaUtils.CreateRef<World>(listing.CurrentWorld);
this.Category = (DutyCategory)listing.Category;
this.RawDuty = listing.Duty;
this.Duty = new Lazy<ContentFinderCondition>(() => dataManager.GetExcelSheet<ContentFinderCondition>().GetRow(listing.Duty));
this.Duty = LuminaUtils.CreateRef<ContentFinderCondition>(listing.Duty);
this.DutyType = (DutyType)listing.DutyType;
this.BeginnersWelcome = listing.BeginnersWelcome == 1;
this.SecondsRemaining = listing.SecondsRemaining;
@ -231,10 +232,7 @@ internal class PartyFinderListing : IPartyFinderListing
this.SlotsFilled = listing.NumSlotsFilled;
this.LastPatchHotfixTimestamp = listing.LastPatchHotfixTimestamp;
this.JobsPresent = listing.JobsPresent
.Select(id => new Lazy<ClassJob>(
() => id == 0
? null
: dataManager.GetExcelSheet<ClassJob>().GetRow(id)))
.Select(id => LuminaUtils.CreateRef<ClassJob>(id))
.ToArray();
}
@ -251,13 +249,13 @@ internal class PartyFinderListing : IPartyFinderListing
public SeString Description { get; }
/// <inheritdoc/>
public Lazy<World> World { get; }
public RowRef<World> World { get; }
/// <inheritdoc/>
public Lazy<World> HomeWorld { get; }
public RowRef<World> HomeWorld { get; }
/// <inheritdoc/>
public Lazy<World> CurrentWorld { get; }
public RowRef<World> CurrentWorld { get; }
/// <inheritdoc/>
public DutyCategory Category { get; }
@ -266,7 +264,7 @@ internal class PartyFinderListing : IPartyFinderListing
public ushort RawDuty { get; }
/// <inheritdoc/>
public Lazy<ContentFinderCondition> Duty { get; }
public RowRef<ContentFinderCondition> Duty { get; }
/// <inheritdoc/>
public DutyType DutyType { get; }
@ -314,7 +312,7 @@ internal class PartyFinderListing : IPartyFinderListing
public IReadOnlyCollection<byte> RawJobsPresent => this.jobsPresent;
/// <inheritdoc/>
public IReadOnlyCollection<Lazy<ClassJob>> JobsPresent { get; }
public IReadOnlyCollection<RowRef<ClassJob>> JobsPresent { get; }
#region Indexers

View file

@ -5,8 +5,11 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Dalamud.Game.Gui.Toast;
/// <summary>
@ -17,8 +20,6 @@ internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
{
private const uint QuestToastCheckmarkMagic = 60081;
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();
@ -30,16 +31,12 @@ internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
/// <summary>
/// Initializes a new instance of the <see cref="ToastGui"/> class.
/// </summary>
/// <param name="sigScanner">Sig scanner to use.</param>
[ServiceManager.ServiceConstructor]
private ToastGui(TargetSigScanner sigScanner)
private unsafe ToastGui()
{
this.address = new ToastGuiAddressResolver();
this.address.Setup(sigScanner);
this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour);
this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour);
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour);
this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress((nint)UIModule.StaticVirtualTablePointer->ShowWideText, this.HandleNormalToastDetour);
this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress((nint)UIModule.StaticVirtualTablePointer->ShowText, this.HandleQuestToastDetour);
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress((nint)UIModule.StaticVirtualTablePointer->ShowErrorText, this.HandleErrorToastDetour);
this.showNormalToastHook.Enable();
this.showQuestToastHook.Enable();
@ -48,16 +45,16 @@ internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
#region Marshal delegates
private delegate IntPtr ShowNormalToastDelegate(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId);
private unsafe delegate void ShowNormalToastDelegate(UIModule* thisPtr, byte* text, int layer, byte isTop, byte isFast, uint logMessageId);
private delegate byte ShowQuestToastDelegate(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound);
private unsafe delegate void ShowQuestToastDelegate(UIModule* thisPtr, int position, byte* text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound);
private delegate byte ShowErrorToastDelegate(IntPtr manager, IntPtr text, byte respectsHidingMaybe);
private unsafe delegate void ShowErrorToastDelegate(UIModule* thisPtr, byte* text, byte respectsHidingMaybe);
#endregion
#region Events
/// <inheritdoc/>
public event IToastGui.OnNormalToastDelegate? Toast;
@ -102,32 +99,6 @@ internal sealed partial class ToastGui : IInternalDisposableService, IToastGui
this.ShowError(message);
}
}
private static byte[] Terminate(byte[] source)
{
var terminated = new byte[source.Length + 1];
Array.Copy(source, 0, terminated, 0, source.Length);
terminated[^1] = 0;
return terminated;
}
private SeString ParseString(IntPtr text)
{
var bytes = new List<byte>();
unsafe
{
var ptr = (byte*)text;
while (*ptr != 0)
{
bytes.Add(*ptr);
ptr += 1;
}
}
// call events
return SeString.Parse(bytes.ToArray());
}
}
/// <summary>
@ -149,36 +120,30 @@ internal sealed partial class ToastGui
this.normalQueue.Enqueue((message.Encode(), options));
}
private void ShowNormal(byte[] bytes, ToastOptions? options = null)
private unsafe void ShowNormal(byte[] bytes, ToastOptions? options = null)
{
options ??= new ToastOptions();
var manager = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
unsafe
fixed (byte* ptr = bytes.NullTerminate())
{
fixed (byte* ptr = terminated)
{
this.HandleNormalToastDetour(manager!.Value, (IntPtr)ptr, 5, (byte)options.Position, (byte)options.Speed, 0);
}
this.HandleNormalToastDetour(
UIModule.Instance(),
ptr,
5,
(byte)options.Position,
(byte)options.Speed,
0);
}
}
private IntPtr HandleNormalToastDetour(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId)
private unsafe void HandleNormalToastDetour(UIModule* thisPtr, byte* text, int layer, byte isTop, byte isFast, uint logMessageId)
{
if (text == IntPtr.Zero)
{
return IntPtr.Zero;
}
if (text == null)
return;
// call events
var isHandled = false;
var str = this.ParseString(text);
var str = MemoryHelper.ReadSeStringNullTerminated((nint)text);
var options = new ToastOptions
{
Position = (ToastPosition)isTop,
@ -189,18 +154,17 @@ internal sealed partial class ToastGui
// do nothing if handled
if (isHandled)
{
return IntPtr.Zero;
}
return;
var terminated = Terminate(str.Encode());
unsafe
fixed (byte* ptr = str.EncodeWithNullTerminator())
{
fixed (byte* message = terminated)
{
return this.showNormalToastHook.Original(manager, (IntPtr)message, layer, (byte)options.Position, (byte)options.Speed, logMessageId);
}
this.showNormalToastHook.Original(
thisPtr,
ptr,
layer,
(byte)(options.Position == ToastPosition.Top ? 1 : 0),
(byte)(options.Speed == ToastSpeed.Fast ? 1 : 0),
logMessageId);
}
}
}
@ -224,45 +188,33 @@ internal sealed partial class ToastGui
this.questQueue.Enqueue((message.Encode(), options));
}
private void ShowQuest(byte[] bytes, QuestToastOptions? options = null)
private unsafe void ShowQuest(byte[] bytes, QuestToastOptions? options = null)
{
options ??= new QuestToastOptions();
var manager = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe
fixed (byte* ptr = bytes.NullTerminate())
{
fixed (byte* ptr = terminated)
{
this.HandleQuestToastDetour(
manager!.Value,
(int)options.Position,
(IntPtr)ptr,
ioc1,
options.PlaySound ? (byte)1 : (byte)0,
ioc2,
0);
}
this.HandleQuestToastDetour(
UIModule.Instance(),
(int)options.Position,
ptr,
ioc1,
(byte)(options.PlaySound ? 1 : 0),
ioc2,
0);
}
}
private byte HandleQuestToastDetour(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound)
private unsafe void HandleQuestToastDetour(UIModule* thisPtr, int position, byte* text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound)
{
if (text == IntPtr.Zero)
{
return 0;
}
if (text == null)
return;
// call events
var isHandled = false;
var str = this.ParseString(text);
var str = SeString.Parse(text);
var options = new QuestToastOptions
{
Position = (QuestToastPosition)position,
@ -275,27 +227,20 @@ internal sealed partial class ToastGui
// do nothing if handled
if (isHandled)
{
return 0;
}
var terminated = Terminate(str.Encode());
return;
var (ioc1, ioc2) = this.DetermineParameterOrder(options);
unsafe
fixed (byte* ptr = str.EncodeWithNullTerminator())
{
fixed (byte* message = terminated)
{
return this.showQuestToastHook.Original(
manager,
(int)options.Position,
(IntPtr)message,
ioc1,
options.PlaySound ? (byte)1 : (byte)0,
ioc2,
0);
}
this.showQuestToastHook.Original(
UIModule.Instance(),
(int)options.Position,
ptr,
ioc1,
(byte)(options.PlaySound ? 1 : 0),
ioc2,
0);
}
}
@ -324,51 +269,32 @@ internal sealed partial class ToastGui
this.errorQueue.Enqueue(message.Encode());
}
private void ShowError(byte[] bytes)
private unsafe void ShowError(byte[] bytes)
{
var manager = Service<GameGui>.GetNullable()?.GetUIModule();
if (manager == null)
return;
// terminate the string
var terminated = Terminate(bytes);
unsafe
fixed (byte* ptr = bytes.NullTerminate())
{
fixed (byte* ptr = terminated)
{
this.HandleErrorToastDetour(manager!.Value, (IntPtr)ptr, 0);
}
this.HandleErrorToastDetour(UIModule.Instance(), ptr, 0);
}
}
private byte HandleErrorToastDetour(IntPtr manager, IntPtr text, byte respectsHidingMaybe)
private unsafe void HandleErrorToastDetour(UIModule* thisPtr, byte* text, byte respectsHidingMaybe)
{
if (text == IntPtr.Zero)
{
return 0;
}
if (text == null)
return;
// call events
var isHandled = false;
var str = this.ParseString(text);
var str = SeString.Parse(text);
this.ErrorToast?.Invoke(ref str, ref isHandled);
// do nothing if handled
if (isHandled)
{
return 0;
}
return;
var terminated = Terminate(str.Encode());
unsafe
fixed (byte* ptr = str.EncodeWithNullTerminator())
{
fixed (byte* message = terminated)
{
return this.showErrorToastHook.Original(manager, (IntPtr)message, respectsHidingMaybe);
}
this.showErrorToastHook.Original(thisPtr, ptr, respectsHidingMaybe);
}
}
}

View file

@ -1,30 +0,0 @@
namespace Dalamud.Game.Gui.Toast;
/// <summary>
/// An address resolver for the <see cref="ToastGui"/> class.
/// </summary>
internal class ToastGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native ShowNormalToast method.
/// </summary>
public IntPtr ShowNormalToast { get; private set; }
/// <summary>
/// Gets the address of the native ShowQuestToast method.
/// </summary>
public IntPtr ShowQuestToast { get; private set; }
/// <summary>
/// Gets the address of the ShowErrorToast method.
/// </summary>
public IntPtr ShowErrorToast { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner 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.ShowQuestToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 40 83 3D ?? ?? ?? ?? ??");
this.ShowErrorToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 83 3D ?? ?? ?? ?? ?? 41 0F B6 F0");
}
}

View file

@ -6,9 +6,11 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Windowing;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
@ -20,12 +22,14 @@ namespace Dalamud.Game.Internal;
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
{
private readonly Hook<AgentHudOpenSystemMenuPrototype> hookAgentHudOpenSystemMenu;
private static readonly ModuleLog Log = new("DalamudAtkTweaks");
private readonly Hook<AgentHUD.Delegates.OpenSystemMenu> hookAgentHudOpenSystemMenu;
// TODO: Make this into events in Framework.Gui
private readonly Hook<UiModuleRequestMainCommand> hookUiModuleRequestMainCommand;
private readonly Hook<UIModule.Delegates.ExecuteMainCommand> hookUiModuleExecuteMainCommand;
private readonly Hook<AtkUnitBaseReceiveGlobalEvent> hookAtkUnitBaseReceiveGlobalEvent;
private readonly Hook<AtkUnitBase.Delegates.ReceiveGlobalEvent> hookAtkUnitBaseReceiveGlobalEvent;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -41,15 +45,9 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
[ServiceManager.ServiceConstructor]
private DalamudAtkTweaks(TargetSigScanner sigScanner)
{
var openSystemMenuAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8B CF 4C 89 B4 24 B8 08 00 00");
this.hookAgentHudOpenSystemMenu = Hook<AgentHudOpenSystemMenuPrototype>.FromAddress(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour);
var uiModuleRequestMainCommandAddress = 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 ?? ?? ?? ??");
this.hookUiModuleRequestMainCommand = Hook<UiModuleRequestMainCommand>.FromAddress(uiModuleRequestMainCommandAddress, this.UiModuleRequestMainCommandDetour);
var atkUnitBaseReceiveGlobalEventAddress = sigScanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 54 41 57");
this.hookAtkUnitBaseReceiveGlobalEvent = Hook<AtkUnitBaseReceiveGlobalEvent>.FromAddress(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour);
this.hookAgentHudOpenSystemMenu = Hook<AgentHUD.Delegates.OpenSystemMenu>.FromAddress(AgentHUD.Addresses.OpenSystemMenu.Value, this.AgentHudOpenSystemMenuDetour);
this.hookUiModuleExecuteMainCommand = Hook<UIModule.Delegates.ExecuteMainCommand>.FromAddress((nint)UIModule.StaticVirtualTablePointer->ExecuteMainCommand, this.UiModuleExecuteMainCommandDetour);
this.hookAtkUnitBaseReceiveGlobalEvent = Hook<AtkUnitBase.Delegates.ReceiveGlobalEvent>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveGlobalEvent, this.AtkUnitBaseReceiveGlobalEventDetour);
this.locDalamudPlugins = Loc.Localize("SystemMenuPlugins", "Dalamud Plugins");
this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings");
@ -57,18 +55,14 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
// this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
this.hookAgentHudOpenSystemMenu.Enable();
this.hookUiModuleRequestMainCommand.Enable();
this.hookUiModuleExecuteMainCommand.Enable();
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
}
/// <summary>Finalizes an instance of the <see cref="DalamudAtkTweaks"/> class.</summary>
~DalamudAtkTweaks() => this.Dispose(false);
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize);
private delegate void UiModuleRequestMainCommand(void* thisPtr, int commandId);
private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5);
private delegate void AgentHudOpenSystemMenuPrototype(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.Dispose(true);
@ -81,7 +75,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
if (disposing)
{
this.hookAgentHudOpenSystemMenu.Dispose();
this.hookUiModuleRequestMainCommand.Dispose();
this.hookUiModuleExecuteMainCommand.Dispose();
this.hookAtkUnitBaseReceiveGlobalEvent.Dispose();
// this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
@ -116,22 +110,19 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
}
*/
private IntPtr AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* arg)
private void AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
// Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", MemoryHelper.ReadSeStringAsString(out _, new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus);
// "SendHotkey"
// 3 == Close
if (cmd == 12 && WindowSystem.HasAnyWindowSystemFocus && *arg == 3 && this.configuration.IsFocusManagementEnabled)
if (eventType == AtkEventType.InputReceived && WindowSystem.HasAnyWindowSystemFocus && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled)
{
Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return IntPtr.Zero;
return;
}
return this.hookAtkUnitBaseReceiveGlobalEvent.Original(thisPtr, cmd, a3, a4, arg);
this.hookAtkUnitBaseReceiveGlobalEvent.Original(thisPtr, eventType, eventParam, atkEvent, atkEventData);
}
private void AgentHudOpenSystemMenuDetour(void* thisPtr, AtkValue* atkValueArgs, uint menuSize)
private void AgentHudOpenSystemMenuDetour(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize)
{
if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled)
{
@ -213,7 +204,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
this.hookAgentHudOpenSystemMenu.Original(thisPtr, atkValueArgs, menuSize + 2);
}
private void UiModuleRequestMainCommandDetour(void* thisPtr, int commandId)
private unsafe void UiModuleExecuteMainCommandDetour(UIModule* thisPtr, uint commandId)
{
var dalamudInterface = Service<DalamudInterface>.GetNullable();
@ -226,7 +217,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
dalamudInterface?.OpenSettings();
break;
default:
this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId);
this.hookUiModuleExecuteMainCommand.Original(thisPtr, commandId);
break;
}
}

View file

@ -9,7 +9,7 @@ namespace Dalamud.Game.Inventory;
/// <summary>
/// Dalamud wrapper around a ClientStructs InventoryItem.
/// </summary>
[StructLayout(LayoutKind.Explicit, Size = StructSizeInBytes)]
[StructLayout(LayoutKind.Explicit, Size = InventoryItem.StructSize)]
public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
{
/// <summary>
@ -17,22 +17,12 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// </summary>
[FieldOffset(0)]
internal readonly InventoryItem InternalItem;
private const int StructSizeInBytes = 0x40;
/// <summary>
/// The view of the backing data, in <see cref="ulong"/>.
/// </summary>
[FieldOffset(0)]
private fixed ulong dataUInt64[StructSizeInBytes / 0x8];
static GameInventoryItem()
{
Debug.Assert(
sizeof(InventoryItem) == StructSizeInBytes,
$"Definition of {nameof(InventoryItem)} has been changed. " +
$"Update {nameof(StructSizeInBytes)} to {sizeof(InventoryItem)} to accommodate for the size change.");
}
private fixed ulong dataUInt64[InventoryItem.StructSize / 0x8];
/// <summary>
/// Initializes a new instance of the <see cref="GameInventoryItem"/> struct.
@ -63,7 +53,7 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// <summary>
/// Gets the quantity of items in this item stack.
/// </summary>
public uint Quantity => this.InternalItem.Quantity;
public int Quantity => this.InternalItem.Quantity;
/// <summary>
/// Gets the spiritbond of this item.
@ -157,7 +147,7 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// <returns><c>true</c> if the current object is equal to the <paramref name="other" /> parameter; otherwise, <c>false</c>.</returns>
public readonly bool Equals(in GameInventoryItem other)
{
for (var i = 0; i < StructSizeInBytes / 8; i++)
for (var i = 0; i < InventoryItem.StructSize / 8; i++)
{
if (this.dataUInt64[i] != other.dataUInt64[i])
return false;
@ -173,7 +163,7 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
public override int GetHashCode()
{
var k = 0x5a8447b91aff51b4UL;
for (var i = 0; i < StructSizeInBytes / 8; i++)
for (var i = 0; i < InventoryItem.StructSize / 8; i++)
k ^= this.dataUInt64[i];
return unchecked((int)(k ^ (k >> 32)));
}

View file

@ -6,6 +6,9 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Network;
using Serilog;
namespace Dalamud.Game.Network;
@ -14,10 +17,10 @@ namespace Dalamud.Game.Network;
/// This class handles interacting with game network events.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork
internal sealed unsafe class GameNetwork : IInternalDisposableService, IGameNetwork
{
private readonly GameNetworkAddressResolver address;
private readonly Hook<ProcessZonePacketDownDelegate> processZonePacketDownHook;
private readonly Hook<PacketDispatcher.Delegates.OnReceivePacket> processZonePacketDownHook;
private readonly Hook<ProcessZonePacketUpDelegate> processZonePacketUpHook;
private readonly HitchDetector hitchDetectorUp;
@ -25,11 +28,9 @@ internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private IntPtr baseAddress;
[ServiceManager.ServiceConstructor]
private GameNetwork(TargetSigScanner sigScanner)
private unsafe GameNetwork(TargetSigScanner sigScanner)
{
this.hitchDetectorUp = new HitchDetector("GameNetworkUp", this.configuration.GameNetworkUpHitch);
this.hitchDetectorDown = new HitchDetector("GameNetworkDown", this.configuration.GameNetworkDownHitch);
@ -37,20 +38,19 @@ internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork
this.address = new GameNetworkAddressResolver();
this.address.Setup(sigScanner);
var onReceivePacketAddress = (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket;
Log.Verbose("===== G A M E N E T W O R K =====");
Log.Verbose($"ProcessZonePacketDown address {Util.DescribeAddress(this.address.ProcessZonePacketDown)}");
Log.Verbose($"OnReceivePacket address {Util.DescribeAddress(onReceivePacketAddress)}");
Log.Verbose($"ProcessZonePacketUp address {Util.DescribeAddress(this.address.ProcessZonePacketUp)}");
this.processZonePacketDownHook = Hook<ProcessZonePacketDownDelegate>.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
this.processZonePacketDownHook = Hook<PacketDispatcher.Delegates.OnReceivePacket>.FromAddress(onReceivePacketAddress, this.ProcessZonePacketDownDetour);
this.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
this.processZonePacketDownHook.Enable();
this.processZonePacketUpHook.Enable();
}
[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);
@ -64,10 +64,8 @@ internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork
this.processZonePacketUpHook.Dispose();
}
private void ProcessZonePacketDownDetour(IntPtr a, uint targetId, IntPtr dataPtr)
private void ProcessZonePacketDownDetour(PacketDispatcher* dispatcher, uint targetId, IntPtr dataPtr)
{
this.baseAddress = a;
this.hitchDetectorDown.Start();
// Go back 0x10 to get back to the start of the packet header
@ -78,7 +76,7 @@ internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork
// Call events
this.NetworkMessage?.Invoke(dataPtr + 0x20, (ushort)Marshal.ReadInt16(dataPtr, 0x12), 0, targetId, NetworkMessageDirection.ZoneDown);
this.processZonePacketDownHook.Original(a, targetId, dataPtr + 0x10);
this.processZonePacketDownHook.Original(dispatcher, targetId, dataPtr + 0x10);
}
catch (Exception ex)
{
@ -96,7 +94,7 @@ internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork
Log.Error(ex, "Exception on ProcessZonePacketDown hook. Header: " + header);
this.processZonePacketDownHook.Original(a, targetId, dataPtr + 0x10);
this.processZonePacketDownHook.Original(dispatcher, targetId, dataPtr + 0x10);
}
this.hitchDetectorDown.Stop();

View file

@ -5,11 +5,6 @@ namespace Dalamud.Game.Network;
/// </summary>
internal sealed class GameNetworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the ProcessZonePacketDown method.
/// </summary>
public IntPtr ProcessZonePacketDown { get; private set; }
/// <summary>
/// Gets the address of the ProcessZonePacketUp method.
/// </summary>
@ -18,9 +13,6 @@ internal sealed class GameNetworkAddressResolver : BaseAddressResolver
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
// ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 7A FF 0F B7 57 02 8D 42 89 3D 5F 02 00 00 0F 87 60 01 00 00 4C 8D 05");
// ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 73 FF 0F B7 57 02 8D 42 ?? 3D ?? ?? 00 00 0F 87 60 01 00 00 4C 8D 05");
this.ProcessZonePacketDown = sig.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 44 24 ?? 8B F2");
this.ProcessZonePacketUp = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 4C 89 64 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 70");
this.ProcessZonePacketUp = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 4C 89 64 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 70"); // unnamed in cs
}
}

View file

@ -30,6 +30,11 @@ internal class MarketBoardItemRequest
/// </summary>
public uint AmountToArrive { get; private set; }
/// <summary>
/// Gets or sets the offered catalog id used in this listing.
/// </summary>
public uint CatalogId { get; internal set; }
/// <summary>
/// Gets the offered item listings.
/// </summary>

View file

@ -48,4 +48,10 @@ internal class UniversalisTaxData
/// </summary>
[JsonProperty("sharlayan")]
public uint Sharlayan { get; set; }
/// <summary>
/// Gets or sets Tuliyollal's current tax rate.
/// </summary>
[JsonProperty("tuliyollal")]
public uint Tuliyollal { get; set; }
}

View file

@ -46,9 +46,9 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
var uploadObject = new UniversalisItemUploadRequest
{
WorldId = clientState.LocalPlayer?.CurrentWorld.Id ?? 0,
WorldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0,
UploaderId = uploader.ToString(),
ItemId = request.Listings.FirstOrDefault()?.CatalogId ?? 0,
ItemId = request.CatalogId,
Listings = [],
Sales = [],
};
@ -106,7 +106,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
// ====================================================================================
Log.Verbose("Universalis data upload for item#{CatalogId} completed", request.Listings.FirstOrDefault()?.CatalogId ?? 0);
Log.Verbose("Universalis data upload for item#{CatalogId} completed", request.CatalogId);
}
/// <inheritdoc/>
@ -120,7 +120,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
var taxUploadObject = new UniversalisTaxUploadRequest
{
WorldId = clientState.LocalPlayer?.CurrentWorld.Id ?? 0,
WorldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0,
UploaderId = clientState.LocalContentId.ToString(),
TaxData = new UniversalisTaxData
{
@ -131,6 +131,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
Kugane = taxRates.KuganeTax,
Crystarium = taxRates.CrystariumTax,
Sharlayan = taxRates.SharlayanTax,
Tuliyollal = taxRates.TuliyollalTax,
},
};
@ -158,7 +159,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
return;
var itemId = purchaseHandler.CatalogId;
var worldId = clientState.LocalPlayer?.CurrentWorld.Id ?? 0;
var worldId = clientState.LocalPlayer?.CurrentWorld.RowId ?? 0;
// ====================================================================================

View file

@ -15,8 +15,11 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Networking.Http;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent;
using FFXIVClientStructs.FFXIV.Client.Network;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using Serilog;
namespace Dalamud.Game.Network.Internal;
@ -35,13 +38,13 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private readonly NetworkHandlersAddressResolver addressResolver;
private readonly Hook<CfPopDelegate> cfPopHook;
private readonly Hook<MarketBoardPurchasePacketHandler> mbPurchaseHook;
private readonly Hook<MarketBoardHistoryPacketHandler> mbHistoryHook;
private readonly Hook<PublicContentDirector.Delegates.HandleEnterContentInfoPacket> cfPopHook;
private readonly Hook<PacketDispatcher.Delegates.HandleMarketBoardPurchasePacket> mbPurchaseHook;
private readonly Hook<InfoProxyItemSearch.Delegates.ProcessItemHistory> mbHistoryHook;
private readonly Hook<CustomTalkReceiveResponse> customTalkHook; // used for marketboard taxes
private readonly Hook<MarketBoardItemRequestStartPacketHandler> mbItemRequestStartHook;
private readonly Hook<InfoProxyItemSearchAddPage> mbOfferingsHook;
private readonly Hook<MarketBoardSendPurchaseRequestPacket> mbSendPurchaseRequestHook;
private readonly Hook<PacketDispatcher.Delegates.HandleMarketBoardItemRequestStartPacket> mbItemRequestStartHook;
private readonly Hook<InfoProxyItemSearch.Delegates.AddPage> mbOfferingsHook;
private readonly Hook<InfoProxyItemSearch.Delegates.SendPurchaseRequestPacket> mbSendPurchaseRequestHook;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -134,14 +137,14 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
this.handleMarketBoardPurchaseHandler = this.HandleMarketBoardPurchaseHandler();
this.mbPurchaseHook =
Hook<MarketBoardPurchasePacketHandler>.FromAddress(
this.addressResolver.MarketBoardPurchasePacketHandler,
Hook<PacketDispatcher.Delegates.HandleMarketBoardPurchasePacket>.FromAddress(
PacketDispatcher.Addresses.HandleMarketBoardPurchasePacket.Value,
this.MarketPurchasePacketDetour);
this.mbPurchaseHook.Enable();
this.mbHistoryHook =
Hook<MarketBoardHistoryPacketHandler>.FromAddress(
this.addressResolver.MarketBoardHistoryPacketHandler,
Hook<InfoProxyItemSearch.Delegates.ProcessItemHistory>.FromAddress(
InfoProxyItemSearch.Addresses.ProcessItemHistory.Value,
this.MarketHistoryPacketDetour);
this.mbHistoryHook.Enable();
@ -151,22 +154,22 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
this.CustomTalkReceiveResponseDetour);
this.customTalkHook.Enable();
this.mbItemRequestStartHook = Hook<MarketBoardItemRequestStartPacketHandler>.FromAddress(
this.addressResolver.MarketBoardItemRequestStartPacketHandler,
this.mbItemRequestStartHook = Hook<PacketDispatcher.Delegates.HandleMarketBoardItemRequestStartPacket>.FromAddress(
PacketDispatcher.Addresses.HandleMarketBoardItemRequestStartPacket.Value,
this.MarketItemRequestStartDetour);
this.mbItemRequestStartHook.Enable();
this.mbOfferingsHook = Hook<InfoProxyItemSearchAddPage>.FromAddress(
this.addressResolver.InfoProxyItemSearchAddPage,
this.mbOfferingsHook = Hook<InfoProxyItemSearch.Delegates.AddPage>.FromAddress(
(nint)InfoProxyItemSearch.StaticVirtualTablePointer->AddPage,
this.MarketBoardOfferingsDetour);
this.mbOfferingsHook.Enable();
this.mbSendPurchaseRequestHook = Hook<MarketBoardSendPurchaseRequestPacket>.FromAddress(
this.addressResolver.BuildMarketBoardPurchaseHandlerPacket,
this.mbSendPurchaseRequestHook = Hook<InfoProxyItemSearch.Delegates.SendPurchaseRequestPacket>.FromAddress(
InfoProxyItemSearch.Addresses.SendPurchaseRequestPacket.Value,
this.MarketBoardSendPurchaseRequestDetour);
this.mbSendPurchaseRequestHook.Enable();
this.cfPopHook = Hook<CfPopDelegate>.FromAddress(this.addressResolver.CfPopPacketHandler, this.CfPopDetour);
this.cfPopHook = Hook<PublicContentDirector.Delegates.HandleEnterContentInfoPacket>.FromAddress(PublicContentDirector.Addresses.HandleEnterContentInfoPacket.Value, this.CfPopDetour);
this.cfPopHook.Enable();
}
@ -183,8 +186,6 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private delegate byte MarketBoardSendPurchaseRequestPacket(InfoProxyItemSearch* infoProxy);
private delegate nint CfPopDelegate(nint packetData);
/// <summary>
/// Event which gets fired when a duty is ready.
/// </summary>
@ -263,7 +264,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
this.cfPopHook.Dispose();
}
private unsafe nint CfPopDetour(nint packetData)
private unsafe nint CfPopDetour(PublicContentDirector.EnterContentInfoPacket* packetData)
{
var result = this.cfPopHook.OriginalDisposeSafe(packetData);
@ -282,21 +283,17 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
if (this.configuration.DutyFinderTaskbarFlash)
Util.FlashWindow();
var cfConditionSheet = Service<DataManager>.Get().GetExcelSheet<ContentFinderCondition>()!;
var cfCondition = cfConditionSheet.GetRow(conditionId);
var cfCondition = LuminaUtils.CreateRef<ContentFinderCondition>(conditionId);
if (cfCondition == null)
if (!cfCondition.IsValid)
{
Log.Error("CFC key {ConditionId} not in Lumina data", conditionId);
return result;
}
var cfcName = cfCondition.Name.ToDalamudString();
var cfcName = cfCondition.Value.Name.ToDalamudString();
if (cfcName.Payloads.Count == 0)
{
cfcName = "Duty Roulette";
cfCondition.Image = 112324;
}
Task.Run(() =>
{
@ -308,7 +305,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
Service<ChatGui>.GetNullable()?.Print(b.Build());
}
this.CfPop.InvokeSafely(cfCondition);
this.CfPop.InvokeSafely(cfCondition.Value);
}).ContinueWith(
task => Log.Error(task.Exception, "CfPop.Invoke failed"),
TaskContinuationOptions.OnlyOnFaulted);
@ -371,7 +368,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
}));
}
private IObservable<List<MarketBoardHistory.MarketBoardHistoryListing>> OnMarketBoardSalesBatch(
private IObservable<(uint CatalogId, List<MarketBoardHistory.MarketBoardHistoryListing> Sales)> OnMarketBoardSalesBatch(
IObservable<MarketBoardItemRequest> start)
{
var historyObservable = this.MbHistoryObservable.Publish().RefCount();
@ -394,6 +391,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
// When a start packet is observed, begin observing a window of history packets.
// We should only get one packet, which the window closing function ensures.
// This packet is flattened to its sale entries and emitted.
uint catalogId = 0;
return historyObservable
.Do(LogHistoryObserved)
.Window(start, UntilBatchEnd)
@ -402,9 +400,12 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
new List<MarketBoardHistory.MarketBoardHistoryListing>(),
(agg, next) =>
{
catalogId = next.CatalogId;
agg.AddRange(next.InternalHistoryListings);
return agg;
}));
}))
.Select(o => (catalogId, o));
}
private IDisposable HandleMarketBoardItemRequest()
@ -415,7 +416,8 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
}
var startObservable = this.MbItemRequestObservable
.Where(request => request.Ok).Do(LogStartObserved)
.Where(request => request.Ok)
.Do(LogStartObserved)
.Publish()
.RefCount();
return Observable.When(
@ -436,10 +438,10 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private void UploadMarketBoardData(
MarketBoardItemRequest request,
ICollection<MarketBoardHistory.MarketBoardHistoryListing> sales,
(uint CatalogId, ICollection<MarketBoardHistory.MarketBoardHistoryListing> Sales) sales,
ICollection<MarketBoardCurrentOfferings.MarketBoardItemListing> listings)
{
var catalogId = listings.FirstOrDefault()?.CatalogId ?? 0;
var catalogId = sales.CatalogId;
if (listings.Count != request.AmountToArrive)
{
Log.Error(
@ -452,10 +454,11 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
"Market Board request resolved, starting upload: item#{CatalogId} listings#{ListingsObserved} sales#{SalesObserved}",
catalogId,
listings.Count,
sales.Count);
sales.Sales.Count);
request.CatalogId = catalogId;
request.Listings.AddRange(listings);
request.History.AddRange(sales);
request.History.AddRange(sales.Sales);
Task.Run(() => this.uploader.Upload(request))
.ContinueWith(
@ -527,7 +530,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
return this.configuration.IsMbCollect;
}
private nint MarketPurchasePacketDetour(nint a1, nint packetData)
private void MarketPurchasePacketDetour(PacketDispatcher* a1, nint packetData)
{
try
{
@ -538,10 +541,10 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
Log.Error(ex, "MarketPurchasePacketHandler threw an exception");
}
return this.mbPurchaseHook.OriginalDisposeSafe(a1, packetData);
this.mbPurchaseHook.OriginalDisposeSafe(a1, packetData);
}
private nint MarketHistoryPacketDetour(nint a1, nint packetData, uint a3, char a4)
private void MarketHistoryPacketDetour(InfoProxyItemSearch* a1, nint packetData)
{
try
{
@ -552,7 +555,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
Log.Error(ex, "MarketHistoryPacketDetour threw an exception");
}
return this.mbHistoryHook.OriginalDisposeSafe(a1, packetData, a3, a4);
this.mbHistoryHook.OriginalDisposeSafe(a1, packetData);
}
private void CustomTalkReceiveResponseDetour(nuint a1, ushort eventId, byte responseId, uint* args, byte argCount)
@ -571,7 +574,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
this.customTalkHook.OriginalDisposeSafe(a1, eventId, responseId, args, argCount);
}
private nint MarketItemRequestStartDetour(nint a1, nint packetRef)
private void MarketItemRequestStartDetour(PacketDispatcher* a1, nint packetRef)
{
try
{
@ -582,10 +585,10 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
Log.Error(ex, "MarketItemRequestStartDetour threw an exception");
}
return this.mbItemRequestStartHook.OriginalDisposeSafe(a1, packetRef);
this.mbItemRequestStartHook.OriginalDisposeSafe(a1, packetRef);
}
private byte MarketBoardOfferingsDetour(nint a1, nint packetRef)
private void MarketBoardOfferingsDetour(InfoProxyItemSearch* a1, nint packetRef)
{
try
{
@ -596,10 +599,10 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
Log.Error(ex, "MarketBoardOfferingsDetour threw an exception");
}
return this.mbOfferingsHook.OriginalDisposeSafe(a1, packetRef);
this.mbOfferingsHook.OriginalDisposeSafe(a1, packetRef);
}
private byte MarketBoardSendPurchaseRequestDetour(InfoProxyItemSearch* infoProxyItemSearch)
private bool MarketBoardSendPurchaseRequestDetour(InfoProxyItemSearch* infoProxyItemSearch)
{
try
{

View file

@ -5,62 +5,17 @@
/// </summary>
internal class NetworkHandlersAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets or sets the pointer to the method responsible for handling CfPop packets.
/// </summary>
public nint CfPopPacketHandler { get; set; }
/// <summary>
/// Gets or sets the pointer to the method responsible for handling market board history. In this case, we are
/// sigging the packet handler method directly.
/// </summary>
public nint MarketBoardHistoryPacketHandler { get; set; }
/// <summary>
/// Gets or sets the pointer to the method responsible for processing the market board purchase packet. In this
/// case, we are sigging the packet handler method directly.
/// </summary>
public nint MarketBoardPurchasePacketHandler { get; set; }
/// <summary>
/// Gets or sets the pointer to the method responsible for custom talk events. Necessary for marketboard tax data,
/// as this isn't really exposed anywhere else.
/// </summary>
public nint CustomTalkEventResponsePacketHandler { get; set; }
/// <summary>
/// Gets or sets the pointer to the method responsible for the marketboard ItemRequestStart packet.
/// </summary>
public nint MarketBoardItemRequestStartPacketHandler { get; set; }
/// <summary>
/// Gets or sets the pointer to the InfoProxyItemSearch.AddPage method, used to load market data.
/// </summary>
public nint InfoProxyItemSearchAddPage { get; set; }
/// <summary>
/// Gets or sets the pointer to the method inside InfoProxyItemSearch that is responsible for building and sending
/// a purchase request packet.
/// </summary>
public nint BuildMarketBoardPurchaseHandlerPacket { get; set; }
/// <inheritdoc />
protected override void Setup64Bit(ISigScanner scanner)
{
this.CfPopPacketHandler = scanner.ScanText("40 53 57 48 83 EC 78 48 8B D9 48 8D 0D");
// TODO: I know this is a CC. I want things working for now. (KW)
this.MarketBoardHistoryPacketHandler = scanner.ScanText(
"40 53 48 83 EC 20 48 8B 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 85 C0 74 2F 4C 8B 00 48 8B C8 41 FF 90 18 01 00 00 48 8B C8 BA 0B 00 00 00 E8 ?? ?? ?? ?? 48 85 C0 74 10 48 8B D3 48 8B C8 48 83 C4 20 5B E9 ?? ?? ?? ?? 48 83 C4 20 5B C3 CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC 40 53");
this.MarketBoardPurchasePacketHandler =
scanner.ScanText("40 55 56 41 56 48 8B EC 48 83 EC ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 45 ?? 48 8B 0D ?? ?? ?? ?? 4C 8B F2");
this.CustomTalkEventResponsePacketHandler =
scanner.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 49 8B D9 41 0F B6 F8 0F B7 F2 8B E9 E8 ?? ?? ?? ?? 48 8B C8 44 0F B6 CF 0F B6 44 24 ?? 44 0F B7 C6 88 44 24 ?? 8B D5 48 89 5C 24");
this.MarketBoardItemRequestStartPacketHandler =
scanner.ScanText("48 89 5C 24 08 57 48 83 EC 20 48 8B 0D ?? ?? ?? ?? 48 8B FA E8 ?? ?? ?? ?? 48 8B D8 48 85 C0 74 4A");
this.InfoProxyItemSearchAddPage =
scanner.ScanText("48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 0F B6 82 ?? ?? ?? ?? 48 8B FA 48 8B D9 38 41 19 74 54");
this.BuildMarketBoardPurchaseHandlerPacket =
scanner.ScanText("40 53 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B D9 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 4C 8B D0 48 85 C0 0F 84 ?? ?? ?? ?? 8B 8B");
scanner.ScanText(
"48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 49 8B D9 41 0F B6 F8 0F B7 F2 8B E9 E8 ?? ?? ?? ?? 48 8B C8 44 0F B6 CF 0F B6 44 24 ?? 44 0F B7 C6 88 44 24 ?? 8B D5 48 89 5C 24"); // unnamed in CS
}
}

View file

@ -1,11 +1,11 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Memory;
using Dalamud.Data;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.Network.Structures.InfoProxy;
@ -92,15 +92,15 @@ public unsafe class CharacterData
/// <summary>
/// Gets the applicable statues of the character.
/// </summary>
public IReadOnlyList<ExcelResolver<OnlineStatus>> Statuses
public IReadOnlyList<RowRef<OnlineStatus>> Statuses
{
get
{
var statuses = new List<ExcelResolver<OnlineStatus>>();
var statuses = new List<RowRef<OnlineStatus>>();
for (var i = 0; i < 64; i++)
{
if ((this.StatusMask & (1UL << i)) != 0)
statuses.Add(new((uint)i));
statuses.Add(LuminaUtils.CreateRef<OnlineStatus>((uint)i));
}
return statuses;
@ -125,22 +125,22 @@ public unsafe class CharacterData
/// <summary>
/// Gets the current world of the character.
/// </summary>
public ExcelResolver<World> CurrentWorld => new(this.Struct->CurrentWorld);
public RowRef<World> CurrentWorld => LuminaUtils.CreateRef<World>(this.Struct->CurrentWorld);
/// <summary>
/// Gets the home world of the character.
/// </summary>
public ExcelResolver<World> HomeWorld => new(this.Struct->HomeWorld);
public RowRef<World> HomeWorld => LuminaUtils.CreateRef<World>(this.Struct->HomeWorld);
/// <summary>
/// Gets the location of the character.
/// </summary>
public ExcelResolver<TerritoryType> Location => new(this.Struct->Location);
public RowRef<TerritoryType> Location => LuminaUtils.CreateRef<TerritoryType>(this.Struct->Location);
/// <summary>
/// Gets the grand company of the character.
/// </summary>
public ExcelResolver<GrandCompany> GrandCompany => new((uint)this.Struct->GrandCompany);
public RowRef<GrandCompany> GrandCompany => LuminaUtils.CreateRef<GrandCompany>((uint)this.Struct->GrandCompany);
/// <summary>
/// Gets the primary client language of the character.
@ -178,7 +178,7 @@ public unsafe class CharacterData
/// <summary>
/// Gets the job of the character.
/// </summary>
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->Job);
public RowRef<ClassJob> ClassJob => LuminaUtils.CreateRef<ClassJob>(this.Struct->Job);
/// <summary>
/// Gets the name of the character.

View file

@ -38,13 +38,6 @@ public abstract partial class Payload
/// </summary>
public bool Dirty { get; protected set; } = true;
/// <summary>
/// Gets the Lumina instance to use for any necessary data lookups.
/// </summary>
[JsonIgnore]
// TODO: We should refactor this. It should not be possible to get IDataManager through here.
protected IDataManager DataResolver => Service<DataManager>.Get();
/// <summary>
/// Decodes a binary representation of a payload into its corresponding nice object payload.
/// </summary>

View file

@ -2,7 +2,11 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Lumina.Excel.GeneratedSheets;
using Dalamud.Data;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
using Newtonsoft.Json;
using Serilog;
@ -106,28 +110,28 @@ public class AutoTranslatePayload : Payload, ITextProvider
this.Key = GetInteger(reader);
}
private static ReadOnlySeString ResolveTextCommand(TextCommand command)
{
// TextCommands prioritize the `Alias` field, if it not empty
// Example for this is /rangerpose2l which becomes /blackrangerposeb in chat
return !command.Alias.IsEmpty ? command.Alias : command.Command;
}
private string Resolve()
{
string value = null;
var sheet = this.DataResolver.GetExcelSheet<Completion>();
var excelModule = Service<DataManager>.Get().Excel;
var completionSheet = excelModule.GetSheet<Completion>();
Completion row = null;
try
{
// try to get the row in the Completion table itself, because this is 'easiest'
// The row may not exist at all (if the Key is for another table), or it could be the wrong row
// (again, if it's meant for another table)
row = sheet.GetRow(this.Key);
}
catch
{
} // don't care, row will be null
// try to get the row in the Completion table itself, because this is 'easiest'
// The row may not exist at all (if the Key is for another table), or it could be the wrong row
// (again, if it's meant for another table)
if (row?.Group == this.Group)
if (completionSheet.GetRowOrDefault(this.Key) is { } completion && completion.Group == this.Group)
{
// if the row exists in this table and the group matches, this is actually the correct data
value = row.Text;
value = completion.Text.ExtractText();
}
else
{
@ -135,34 +139,34 @@ public class AutoTranslatePayload : Payload, ITextProvider
{
// we need to get the linked table and do the lookup there instead
// in this case, there will only be one entry for this group id
row = sheet.First(r => r.Group == this.Group);
var row = completionSheet.First(r => r.Group == this.Group);
// many of the names contain valid id ranges after the table name, but we don't need those
var actualTableName = row.LookupTable.RawString.Split('[')[0];
var actualTableName = row.LookupTable.ExtractText().Split('[')[0];
var name = actualTableName switch
{
"Action" => this.DataResolver.GetExcelSheet<Lumina.Excel.GeneratedSheets.Action>().GetRow(this.Key).Name,
"ActionComboRoute" => this.DataResolver.GetExcelSheet<ActionComboRoute>().GetRow(this.Key).Name,
"BuddyAction" => this.DataResolver.GetExcelSheet<BuddyAction>().GetRow(this.Key).Name,
"ClassJob" => this.DataResolver.GetExcelSheet<ClassJob>().GetRow(this.Key).Name,
"Companion" => this.DataResolver.GetExcelSheet<Companion>().GetRow(this.Key).Singular,
"CraftAction" => this.DataResolver.GetExcelSheet<CraftAction>().GetRow(this.Key).Name,
"GeneralAction" => this.DataResolver.GetExcelSheet<GeneralAction>().GetRow(this.Key).Name,
"GuardianDeity" => this.DataResolver.GetExcelSheet<GuardianDeity>().GetRow(this.Key).Name,
"MainCommand" => this.DataResolver.GetExcelSheet<MainCommand>().GetRow(this.Key).Name,
"Mount" => this.DataResolver.GetExcelSheet<Mount>().GetRow(this.Key).Singular,
"Pet" => this.DataResolver.GetExcelSheet<Pet>().GetRow(this.Key).Name,
"PetAction" => this.DataResolver.GetExcelSheet<PetAction>().GetRow(this.Key).Name,
"PetMirage" => this.DataResolver.GetExcelSheet<PetMirage>().GetRow(this.Key).Name,
"PlaceName" => this.DataResolver.GetExcelSheet<PlaceName>().GetRow(this.Key).Name,
"Race" => this.DataResolver.GetExcelSheet<Race>().GetRow(this.Key).Masculine,
"TextCommand" => this.ResolveTextCommand(),
"Tribe" => this.DataResolver.GetExcelSheet<Tribe>().GetRow(this.Key).Masculine,
"Weather" => this.DataResolver.GetExcelSheet<Weather>().GetRow(this.Key).Name,
"Action" => excelModule.GetSheet<Lumina.Excel.Sheets.Action>().GetRow(this.Key).Name,
"ActionComboRoute" => excelModule.GetSheet<ActionComboRoute>().GetRow(this.Key).Name,
"BuddyAction" => excelModule.GetSheet<BuddyAction>().GetRow(this.Key).Name,
"ClassJob" => excelModule.GetSheet<ClassJob>().GetRow(this.Key).Name,
"Companion" => excelModule.GetSheet<Companion>().GetRow(this.Key).Singular,
"CraftAction" => excelModule.GetSheet<CraftAction>().GetRow(this.Key).Name,
"GeneralAction" => excelModule.GetSheet<GeneralAction>().GetRow(this.Key).Name,
"GuardianDeity" => excelModule.GetSheet<GuardianDeity>().GetRow(this.Key).Name,
"MainCommand" => excelModule.GetSheet<MainCommand>().GetRow(this.Key).Name,
"Mount" => excelModule.GetSheet<Mount>().GetRow(this.Key).Singular,
"Pet" => excelModule.GetSheet<Pet>().GetRow(this.Key).Name,
"PetAction" => excelModule.GetSheet<PetAction>().GetRow(this.Key).Name,
"PetMirage" => excelModule.GetSheet<PetMirage>().GetRow(this.Key).Name,
"PlaceName" => excelModule.GetSheet<PlaceName>().GetRow(this.Key).Name,
"Race" => excelModule.GetSheet<Race>().GetRow(this.Key).Masculine,
"TextCommand" => AutoTranslatePayload.ResolveTextCommand(excelModule.GetSheet<TextCommand>().GetRow(this.Key)),
"Tribe" => excelModule.GetSheet<Tribe>().GetRow(this.Key).Masculine,
"Weather" => excelModule.GetSheet<Weather>().GetRow(this.Key).Name,
_ => throw new Exception(actualTableName),
};
value = name;
value = name.ExtractText();
}
catch (Exception e)
{
@ -172,12 +176,4 @@ public class AutoTranslatePayload : Payload, ITextProvider
return value;
}
private Lumina.Text.SeString ResolveTextCommand()
{
// TextCommands prioritize the `Alias` field, if it not empty
// Example for this is /rangerpose2l which becomes /blackrangerposeb in chat
var result = this.DataResolver.GetExcelSheet<TextCommand>().GetRow(this.Key);
return result.Alias.Payloads.Count > 0 ? result.Alias : result.Command;
}
}

View file

@ -3,9 +3,10 @@ using System.IO;
using System.Linq;
using System.Text;
using Lumina.Excel.GeneratedSheets;
using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -14,8 +15,6 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class ItemPayload : Payload
{
private Item? item;
// mainly to allow overriding the name (for things like owo)
// TODO: even though this is present in some item links, it may not really have a use at all
// For things like owo, changing the text payload is probably correct, whereas changing the
@ -131,27 +130,13 @@ public class ItemPayload : Payload
public uint RawItemId => this.rawItemId;
/// <summary>
/// Gets the underlying Lumina Item represented by this payload.
/// Gets the underlying Lumina data represented by this payload. This is either a Item or EventItem <see cref="RowRef{T}"/>.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Item? Item
{
get
{
// TODO(goat): This should be revamped/removed on an API level change.
if (this.Kind == ItemKind.EventItem)
{
Log.Warning("Event items cannot be fetched from the ItemPayload");
return null;
}
this.item ??= this.DataResolver.GetExcelSheet<Item>()!.GetRow(this.ItemId);
return this.item;
}
}
public RowRef Item =>
this.Kind == ItemKind.EventItem
? (RowRef)LuminaUtils.CreateRef<EventItem>(this.ItemId)
: (RowRef)LuminaUtils.CreateRef<Item>(this.ItemId);
/// <summary>
/// Gets a value indicating whether or not this item link is for a high-quality version of the item.
@ -183,7 +168,8 @@ public class ItemPayload : Payload
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - ItemId: {this.ItemId}, Kind: {this.Kind}, Name: {this.displayName ?? this.Item?.Name}";
var name = this.displayName ?? (this.Item.GetValueOrDefault<Item>()?.Name ?? this.Item.GetValueOrDefault<EventItem>()?.Name)?.ExtractText();
return $"{this.Type} - ItemId: {this.ItemId}, Kind: {this.Kind}, Name: {name}";
}
/// <inheritdoc/>

View file

@ -1,7 +1,10 @@
using System.Collections.Generic;
using System.IO;
using Lumina.Excel.GeneratedSheets;
using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -11,11 +14,6 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class MapLinkPayload : Payload
{
private Map map;
private TerritoryType territoryType;
private string placeNameRegion;
private string placeName;
[JsonProperty]
private uint territoryTypeId;
@ -38,8 +36,8 @@ public class MapLinkPayload : Payload
// this fudge is necessary basically to ensure we don't shift down a full tenth
// because essentially values are truncated instead of rounded, so 3.09999f will become
// 3.0f and not 3.1f
this.RawX = this.ConvertMapCoordinateToRawPosition(niceXCoord + fudgeFactor, this.Map.SizeFactor, this.Map.OffsetX);
this.RawY = this.ConvertMapCoordinateToRawPosition(niceYCoord + fudgeFactor, this.Map.SizeFactor, this.Map.OffsetY);
this.RawX = this.ConvertMapCoordinateToRawPosition(niceXCoord + fudgeFactor, this.Map.Value.SizeFactor, this.Map.Value.OffsetX);
this.RawY = this.ConvertMapCoordinateToRawPosition(niceYCoord + fudgeFactor, this.Map.Value.SizeFactor, this.Map.Value.OffsetY);
}
/// <summary>
@ -72,20 +70,14 @@ public class MapLinkPayload : Payload
/// <summary>
/// Gets the Map specified for this map link.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Map Map => this.map ??= this.DataResolver.GetExcelSheet<Map>().GetRow(this.mapId);
public RowRef<Map> Map => LuminaUtils.CreateRef<Map>(this.mapId);
/// <summary>
/// Gets the TerritoryType specified for this map link.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public TerritoryType TerritoryType => this.territoryType ??= this.DataResolver.GetExcelSheet<TerritoryType>().GetRow(this.territoryTypeId);
public RowRef<TerritoryType> TerritoryType => LuminaUtils.CreateRef<TerritoryType>(this.territoryTypeId);
/// <summary>
/// Gets the internal x-coordinate for this map position.
@ -102,13 +94,13 @@ public class MapLinkPayload : Payload
/// <summary>
/// Gets the readable x-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
public float XCoord => this.ConvertRawPositionToMapCoordinate(this.RawX, this.Map.SizeFactor, this.Map.OffsetX);
public float XCoord => this.ConvertRawPositionToMapCoordinate(this.RawX, this.Map.Value.SizeFactor, this.Map.Value.OffsetX);
/// <summary>
/// Gets the readable y-coordinate position for this map link. This value is approximate and unrounded.
/// </summary>
[JsonIgnore]
public float YCoord => this.ConvertRawPositionToMapCoordinate(this.RawY, this.Map.SizeFactor, this.Map.OffsetY);
public float YCoord => this.ConvertRawPositionToMapCoordinate(this.RawY, this.Map.Value.SizeFactor, this.Map.Value.OffsetY);
// there is no Z; it's purely in the text payload where applicable
@ -143,18 +135,18 @@ public class MapLinkPayload : Payload
/// Gets the region name for this map link. This corresponds to the upper zone name found in the actual in-game map UI. eg, "La Noscea".
/// </summary>
[JsonIgnore]
public string PlaceNameRegion => this.placeNameRegion ??= this.TerritoryType.PlaceNameRegion.Value?.Name;
public string PlaceNameRegion => this.TerritoryType.Value.PlaceNameRegion.Value.Name.ExtractText();
/// <summary>
/// Gets the place name for this map link. This corresponds to the lower zone name found in the actual in-game map UI. eg, "Limsa Lominsa Upper Decks".
/// </summary>
[JsonIgnore]
public string PlaceName => this.placeName ??= this.TerritoryType.PlaceName.Value?.Name;
public string PlaceName => this.TerritoryType.Value.PlaceName.Value.Name.ExtractText();
/// <summary>
/// Gets the data string for this map link, for use by internal game functions that take a string variant and not a binary payload.
/// </summary>
public string DataString => $"m:{this.TerritoryType.RowId},{this.Map.RowId},{this.RawX},{this.RawY}";
public string DataString => $"m:{this.territoryTypeId},{this.mapId},{this.RawX},{this.RawY}";
/// <inheritdoc/>
public override string ToString()

View file

@ -2,7 +2,10 @@ using System.Collections.Generic;
using System.IO;
using System.Text;
using Lumina.Excel.GeneratedSheets;
using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -12,8 +15,6 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class PlayerPayload : Payload
{
private World world;
[JsonProperty]
private uint serverId;
@ -43,11 +44,8 @@ public class PlayerPayload : Payload
/// <summary>
/// Gets the Lumina object representing the player's home server.
/// </summary>
/// <remarks>
/// Value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public World World => this.world ??= this.DataResolver.GetExcelSheet<World>().GetRow(this.serverId);
public RowRef<World> World => LuminaUtils.CreateRef<World>(this.serverId);
/// <summary>
/// Gets or sets the player's displayed name. This does not contain the server name.
@ -72,7 +70,7 @@ public class PlayerPayload : Payload
/// The world name will always be present.
/// </summary>
[JsonIgnore]
public string DisplayedName => $"{this.PlayerName}{(char)SeIconChar.CrossWorld}{this.World.Name}";
public string DisplayedName => $"{this.PlayerName}{(char)SeIconChar.CrossWorld}{this.World.ValueNullable?.Name}";
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Player;
@ -80,7 +78,7 @@ public class PlayerPayload : Payload
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - PlayerName: {this.PlayerName}, ServerId: {this.serverId}, ServerName: {this.World.Name}";
return $"{this.Type} - PlayerName: {this.PlayerName}, ServerId: {this.serverId}, ServerName: {this.World.ValueNullable?.Name}";
}
/// <inheritdoc/>

View file

@ -1,7 +1,10 @@
using System.Collections.Generic;
using System.IO;
using Lumina.Excel.GeneratedSheets;
using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -11,8 +14,6 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class QuestPayload : Payload
{
private Quest quest;
[JsonProperty]
private uint questId;
@ -40,16 +41,13 @@ public class QuestPayload : Payload
/// <summary>
/// Gets the underlying Lumina Quest represented by this payload.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Quest Quest => this.quest ??= this.DataResolver.GetExcelSheet<Quest>().GetRow(this.questId);
public RowRef<Quest> Quest => LuminaUtils.CreateRef<Quest>(this.questId);
/// <inheritdoc />
public override string ToString()
{
return $"{this.Type} - QuestId: {this.questId}, Name: {this.Quest?.Name ?? "QUEST NOT FOUND"}";
return $"{this.Type} - QuestId: {this.questId}, Name: {this.Quest.ValueNullable?.Name.ExtractText() ?? "QUEST NOT FOUND"}";
}
/// <inheritdoc/>

View file

@ -1,7 +1,10 @@
using System.Collections.Generic;
using System.IO;
using Lumina.Excel.GeneratedSheets;
using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -11,8 +14,6 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class StatusPayload : Payload
{
private Status status;
[JsonProperty]
private uint statusId;
@ -40,16 +41,13 @@ public class StatusPayload : Payload
/// <summary>
/// Gets the Lumina Status object represented by this payload.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public Status Status => this.status ??= this.DataResolver.GetExcelSheet<Status>().GetRow(this.statusId);
public RowRef<Status> Status => LuminaUtils.CreateRef<Status>(this.statusId);
/// <inheritdoc/>
public override string ToString()
{
return $"{this.Type} - StatusId: {this.statusId}, Name: {this.Status.Name}";
return $"{this.Type} - StatusId: {this.statusId}, Name: {this.Status.ValueNullable?.Name}";
}
/// <inheritdoc/>

View file

@ -1,7 +1,10 @@
using System.Collections.Generic;
using System.IO;
using Lumina.Excel.GeneratedSheets;
using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -11,8 +14,6 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class UIForegroundPayload : Payload
{
private UIColor color;
[JsonProperty]
private ushort colorKey;
@ -51,11 +52,8 @@ public class UIForegroundPayload : Payload
/// <summary>
/// Gets a Lumina UIColor object representing this payload. The actual color data is at UIColor.UIForeground.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public UIColor UIColor => this.color ??= this.DataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
public RowRef<UIColor> UIColor => LuminaUtils.CreateRef<UIColor>(this.colorKey);
/// <summary>
/// Gets or sets the color key used as a lookup in the UIColor table for this foreground color.
@ -63,15 +61,11 @@ public class UIForegroundPayload : Payload
[JsonIgnore]
public ushort ColorKey
{
get
{
return this.colorKey;
}
get => this.colorKey;
set
{
this.colorKey = value;
this.color = null;
this.Dirty = true;
}
}
@ -80,13 +74,13 @@ public class UIForegroundPayload : Payload
/// Gets the Red/Green/Blue/Alpha values for this foreground color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGBA => this.UIColor.UIForeground;
public uint RGBA => this.UIColor.Value.UIForeground;
/// <summary>
/// Gets the ABGR value for this foreground color, as ImGui requires it in PushColor.
/// </summary>
[JsonIgnore]
public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.UIForeground);
public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.UIForeground);
/// <inheritdoc/>
public override string ToString()

View file

@ -1,7 +1,10 @@
using System.Collections.Generic;
using System.IO;
using Lumina.Excel.GeneratedSheets;
using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -11,8 +14,6 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class UIGlowPayload : Payload
{
private UIColor color;
[JsonProperty]
private ushort colorKey;
@ -57,7 +58,6 @@ public class UIGlowPayload : Payload
set
{
this.colorKey = value;
this.color = null;
this.Dirty = true;
}
}
@ -71,22 +71,19 @@ public class UIGlowPayload : Payload
/// Gets the Red/Green/Blue/Alpha values for this glow color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGBA => this.UIColor.UIGlow;
public uint RGBA => this.UIColor.Value.UIGlow;
/// <summary>
/// Gets the ABGR value for this glow color, as ImGui requires it in PushColor.
/// </summary>
[JsonIgnore]
public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.UIGlow);
public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.UIGlow);
/// <summary>
/// Gets a Lumina UIColor object representing this payload. The actual color data is at UIColor.UIGlow.
/// </summary>
/// <remarks>
/// The value is evaluated lazily and cached.
/// </remarks>
[JsonIgnore]
public UIColor UIColor => this.color ??= this.DataResolver.GetExcelSheet<UIColor>().GetRow(this.colorKey);
public RowRef<UIColor> UIColor => LuminaUtils.CreateRef<UIColor>(this.colorKey);
/// <inheritdoc/>
public override string ToString()

View file

@ -7,7 +7,7 @@ using System.Text;
using Dalamud.Data;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Utility;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling;
@ -200,12 +200,12 @@ public class SeString
case ItemPayload.ItemKind.Normal:
case ItemPayload.ItemKind.Collectible:
case ItemPayload.ItemKind.Hq:
var item = data.GetExcelSheet<Item>()?.GetRow(itemId);
displayName = item?.Name;
var item = data.GetExcelSheet<Item>()?.GetRowOrDefault(itemId);
displayName = item?.Name.ExtractText();
rarity = item?.Rarity ?? 1;
break;
case ItemPayload.ItemKind.EventItem:
displayName = data.GetExcelSheet<EventItem>()?.GetRow(itemId)?.Name;
displayName = data.GetExcelSheet<EventItem>()?.GetRowOrDefault(itemId)?.Name.ExtractText();
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
@ -251,7 +251,7 @@ public class SeString
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(Item item, bool isHq, string? displayNameOverride = null)
{
return CreateItemLink(item.RowId, isHq, displayNameOverride ?? item.Name);
return CreateItemLink(item.RowId, isHq, displayNameOverride ?? item.Name.ExtractText());
}
/// <summary>
@ -360,15 +360,14 @@ public class SeString
var mapSheet = data.GetExcelSheet<Map>();
var matches = data.GetExcelSheet<PlaceName>()
.Where(row => row.Name.ToString().ToLowerInvariant() == placeName.ToLowerInvariant())
.ToArray();
.Where(row => row.Name.ExtractText().Equals(placeName, StringComparison.InvariantCultureIgnoreCase));
foreach (var place in matches)
{
var map = mapSheet.FirstOrDefault(row => row.PlaceName.Row == place.RowId);
if (map != null && map.TerritoryType.Row != 0)
var map = mapSheet.Cast<Map?>().FirstOrDefault(row => row!.Value.PlaceName.RowId == place.RowId);
if (map.HasValue && map.Value.TerritoryType.RowId != 0)
{
return CreateMapLinkWithInstance(map.TerritoryType.Row, map.RowId, instance, xCoord, yCoord, fudgeFactor);
return CreateMapLinkWithInstance(map.Value.TerritoryType.RowId, map.Value.RowId, instance, xCoord, yCoord, fudgeFactor);
}
}

View file

@ -10,7 +10,7 @@ public sealed class XivChatEntry
/// <summary>
/// Gets or sets the type of entry.
/// </summary>
public XivChatType Type { get; set; } = XivChatType.Debug;
public XivChatType? Type { get; set; }
/// <summary>
/// Gets or sets the message timestamp.

View file

@ -1,6 +1,8 @@
using System.Numerics;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Components;
@ -41,7 +43,9 @@ public static partial class ImGuiComponents
ImGui.OpenPopup($"###ColorPickerPopup{id}");
}
if (ImGui.BeginPopup($"###ColorPickerPopup{id}"))
using var popup = ImRaii.Popup($"###ColorPickerPopup{id}");
if (popup)
{
if (ImGui.ColorPicker4($"###ColorPicker{id}", ref existingColor, flags))
{
@ -61,8 +65,6 @@ public static partial class ImGuiComponents
ImGui.SameLine();
}
}
ImGui.EndPopup();
}
return selectedColor;

View file

@ -1,5 +1,7 @@
using System.Numerics;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Components;
@ -21,17 +23,16 @@ public static partial class ImGuiComponents
/// <returns>Indicator if button is clicked.</returns>
public static bool DisabledButton(FontAwesomeIcon icon, int? id = null, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, float alphaMult = .5f)
{
ImGui.PushFont(UiBuilder.IconFont);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var text = icon.ToIconString();
if (id.HasValue)
{
text = $"{text}##{id}";
}
var text = icon.ToIconString();
if (id.HasValue)
text = $"{text}##{id}";
var button = DisabledButton(text, defaultColor, activeColor, hoveredColor, alphaMult);
ImGui.PopFont();
return button;
return DisabledButton(text, defaultColor, activeColor, hoveredColor, alphaMult);
}
}
/// <summary>
@ -45,31 +46,28 @@ public static partial class ImGuiComponents
/// <returns>Indicator if button is clicked.</returns>
public static bool DisabledButton(string labelWithId, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, float alphaMult = .5f)
{
using var col = new ImRaii.Color();
if (defaultColor.HasValue)
ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
{
col.Push(ImGuiCol.Button, defaultColor.Value);
}
if (activeColor.HasValue)
ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value);
{
col.Push(ImGuiCol.ButtonActive, activeColor.Value);
}
if (hoveredColor.HasValue)
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value);
{
col.Push(ImGuiCol.ButtonHovered, hoveredColor.Value);
}
var style = ImGui.GetStyle();
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, style.Alpha * alphaMult);
var button = ImGui.Button(labelWithId);
ImGui.PopStyleVar();
if (defaultColor.HasValue)
ImGui.PopStyleColor();
if (activeColor.HasValue)
ImGui.PopStyleColor();
if (hoveredColor.HasValue)
ImGui.PopStyleColor();
return button;
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, style.Alpha * alphaMult))
{
return ImGui.Button(labelWithId);
}
}
}

View file

@ -1,3 +1,7 @@
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Common.Math;
using ImGuiNET;
namespace Dalamud.Interface.Components;
@ -18,17 +22,32 @@ public static partial class ImGuiComponents
/// </summary>
/// <param name="helpText">The text to display on hover.</param>
/// <param name="icon">The icon to use.</param>
public static void HelpMarker(string helpText, FontAwesomeIcon icon)
/// <param name="color">The color of the icon.</param>
public static void HelpMarker(string helpText, FontAwesomeIcon icon, Vector4? color = null)
{
using var col = new ImRaii.Color();
if (color.HasValue)
{
col.Push(ImGuiCol.TextDisabled, color.Value);
}
ImGui.SameLine();
ImGui.PushFont(UiBuilder.IconFont);
ImGui.TextDisabled(icon.ToIconString());
ImGui.PopFont();
if (!ImGui.IsItemHovered()) return;
ImGui.BeginTooltip();
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0f);
ImGui.TextUnformatted(helpText);
ImGui.PopTextWrapPos();
ImGui.EndTooltip();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextDisabled(icon.ToIconString());
}
if (ImGui.IsItemHovered())
{
using (ImRaii.Tooltip())
{
using (ImRaii.TextWrapPos(ImGui.GetFontSize() * 35.0f))
{
ImGui.TextUnformatted(helpText);
}
}
}
}
}

View file

@ -1,6 +1,8 @@
using System.Numerics;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Components;
@ -15,8 +17,26 @@ public static partial class ImGuiComponents
/// </summary>
/// <param name="icon">The icon for the button.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(FontAwesomeIcon icon)
=> IconButton(icon, null, null, null);
public static bool IconButton(FontAwesomeIcon icon) => IconButton(icon, null);
/// <summary>
/// IconButton component to use an icon as a button.
/// </summary>
/// <param name="icon">The icon for the button.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(FontAwesomeIcon icon, Vector2 size) => IconButton(icon, null, null, null, size);
/// <summary>
/// IconButton component to use an icon as a button.
/// </summary>
/// <param name="icon">The icon for the button.</param>
/// <param name="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</param>
/// <param name="hoveredColor">The color of the button when hovered.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(FontAwesomeIcon icon, Vector4? defaultColor, Vector4? activeColor = null, Vector4? hoveredColor = null, Vector2? size = null) => IconButton($"{icon.ToIconString()}", defaultColor, activeColor, hoveredColor, size);
/// <summary>
/// IconButton component to use an icon as a button.
@ -24,8 +44,28 @@ public static partial class ImGuiComponents
/// <param name="id">The ID of the button.</param>
/// <param name="icon">The icon for the button.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(int id, FontAwesomeIcon icon)
=> IconButton(id, icon, null, null, null);
public static bool IconButton(int id, FontAwesomeIcon icon) => IconButton(id, icon, null);
/// <summary>
/// IconButton component to use an icon as a button.
/// </summary>
/// <param name="id">The ID of the button.</param>
/// <param name="icon">The icon for the button.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(int id, FontAwesomeIcon icon, Vector2 size) => IconButton(id, icon, null, null, null, size);
/// <summary>
/// IconButton component to use an icon as a button with color options.
/// </summary>
/// <param name="id">The ID of the button.</param>
/// <param name="icon">The icon for the button.</param>
/// <param name="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</param>
/// <param name="hoveredColor">The color of the button when hovered.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(int id, FontAwesomeIcon icon, Vector4? defaultColor, Vector4? activeColor = null, Vector4? hoveredColor = null, Vector2? size = null) => IconButton($"{icon.ToIconString()}##{id}", defaultColor, activeColor, hoveredColor, size);
/// <summary>
/// IconButton component to use an icon as a button.
@ -33,51 +73,45 @@ public static partial class ImGuiComponents
/// <param name="id">The ID of the button.</param>
/// <param name="icon">The icon for the button.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(string id, FontAwesomeIcon icon)
=> IconButton(id, icon, null, null, null);
public static bool IconButton(string id, FontAwesomeIcon icon) => IconButton(id, icon, null);
/// <summary>
/// IconButton component to use an icon as a button.
/// </summary>
/// <param name="id">The ID of the button.</param>
/// <param name="icon">The icon for the button.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(string id, FontAwesomeIcon icon, Vector2 size)
=> IconButton(id, icon, null, null, null, size);
/// <summary>
/// IconButton component to use an icon as a button with color options.
/// </summary>
/// <param name="id">The ID of the button.</param>
/// <param name="icon">The icon for the button.</param>
/// <param name="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</param>
/// <param name="hoveredColor">The color of the button when hovered.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(string id, FontAwesomeIcon icon, Vector4? defaultColor, Vector4? activeColor = null, Vector4? hoveredColor = null, Vector2? size = null)
=> IconButton($"{icon.ToIconString()}##{id}", defaultColor, activeColor, hoveredColor, size);
/// <summary>
/// IconButton component to use an icon as a button.
/// </summary>
/// <param name="iconText">Text already containing the icon string.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(string iconText)
=> IconButton(iconText, null, null, null);
public static bool IconButton(string iconText) => IconButton(iconText, null);
/// <summary>
/// IconButton component to use an icon as a button.
/// </summary>
/// <param name="icon">The icon for the button.</param>
/// <param name="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</param>
/// <param name="hoveredColor">The color of the button when hovered.</param>
/// <param name="iconText">Text already containing the icon string.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(FontAwesomeIcon icon, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
=> IconButton($"{icon.ToIconString()}", defaultColor, activeColor, hoveredColor);
/// <summary>
/// IconButton component to use an icon as a button with color options.
/// </summary>
/// <param name="id">The ID of the button.</param>
/// <param name="icon">The icon for the button.</param>
/// <param name="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</param>
/// <param name="hoveredColor">The color of the button when hovered.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(int id, FontAwesomeIcon icon, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
=> IconButton($"{icon.ToIconString()}##{id}", defaultColor, activeColor, hoveredColor);
/// <summary>
/// IconButton component to use an icon as a button with color options.
/// </summary>
/// <param name="id">The ID of the button.</param>
/// <param name="icon">The icon for the button.</param>
/// <param name="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</param>
/// <param name="hoveredColor">The color of the button when hovered.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(string id, FontAwesomeIcon icon, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
=> IconButton($"{icon.ToIconString()}##{id}", defaultColor, activeColor, hoveredColor);
public static bool IconButton(string iconText, Vector2 size) => IconButton(iconText, null, null, null, size);
/// <summary>
/// IconButton component to use an icon as a button with color options.
@ -86,62 +120,72 @@ public static partial class ImGuiComponents
/// <param name="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</param>
/// <param name="hoveredColor">The color of the button when hovered.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButton(string iconText, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
public static bool IconButton(string iconText, Vector4? defaultColor, Vector4? activeColor = null, Vector4? hoveredColor = null, Vector2? size = null)
{
var numColors = 0;
using var col = new ImRaii.Color();
if (defaultColor.HasValue)
{
ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
numColors++;
col.Push(ImGuiCol.Button, defaultColor.Value);
}
if (activeColor.HasValue)
{
ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value);
numColors++;
col.Push(ImGuiCol.ButtonActive, activeColor.Value);
}
if (hoveredColor.HasValue)
{
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value);
numColors++;
col.Push(ImGuiCol.ButtonHovered, hoveredColor.Value);
}
if (size.HasValue)
{
size *= ImGuiHelpers.GlobalScale;
}
var icon = iconText;
if (icon.Contains("#"))
icon = icon[..icon.IndexOf("#", StringComparison.Ordinal)];
if (icon.Contains('#'))
{
icon = icon[..icon.IndexOf('#', StringComparison.Ordinal)];
}
ImGui.PushID(iconText);
bool button;
ImGui.PushFont(UiBuilder.IconFont);
var iconSize = ImGui.CalcTextSize(icon);
ImGui.PopFont();
var dl = ImGui.GetWindowDrawList();
var cursor = ImGui.GetCursorScreenPos();
// Draw an ImGui button with the icon and text
var buttonWidth = iconSize.X + (ImGui.GetStyle().FramePadding.X * 2);
var buttonHeight = ImGui.GetFrameHeight();
var button = ImGui.Button(string.Empty, new Vector2(buttonWidth, buttonHeight));
// Draw the icon on the window drawlist
var iconPos = new Vector2(cursor.X + ImGui.GetStyle().FramePadding.X, cursor.Y + ImGui.GetStyle().FramePadding.Y);
ImGui.PushFont(UiBuilder.IconFont);
dl.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon);
ImGui.PopFont();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var iconSize = ImGui.CalcTextSize(icon);
var cursor = ImGui.GetCursorScreenPos();
ImGui.PopID();
var width = size is { X: not 0 } ? size.Value.X : iconSize.X + (ImGui.GetStyle().FramePadding.X * 2);
var height = size is { Y: not 0 } ? size.Value.Y : ImGui.GetFrameHeight();
if (numColors > 0)
ImGui.PopStyleColor(numColors);
var buttonSize = new Vector2(width, height);
using (ImRaii.PushId(iconText))
{
button = ImGui.Button(string.Empty, buttonSize);
}
var iconPos = cursor + ((buttonSize - iconSize) / 2f);
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon);
}
return button;
}
/// <summary>
/// IconButton component to use an icon as a button with color options.
/// </summary>
/// <param name="icon">Icon to show.</param>
/// <param name="text">Text to show.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon & text.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector2 size) => IconButtonWithText(icon, text, null, null, null, size);
/// <summary>
/// IconButton component to use an icon as a button with color options.
/// </summary>
@ -150,61 +194,72 @@ public static partial class ImGuiComponents
/// <param name="defaultColor">The default color of the button.</param>
/// <param name="activeColor">The color of the button when active.</param>
/// <param name="hoveredColor">The color of the button when hovered.</param>
/// <param name="size">Sets the size of the button. If either dimension is set to 0, that dimension will conform to the size of the icon & text.</param>
/// <returns>Indicator if button is clicked.</returns>
public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, Vector2? size = null)
{
var numColors = 0;
using var col = new ImRaii.Color();
if (defaultColor.HasValue)
{
ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
numColors++;
col.Push(ImGuiCol.Button, defaultColor.Value);
}
if (activeColor.HasValue)
{
ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value);
numColors++;
col.Push(ImGuiCol.ButtonActive, activeColor.Value);
}
if (hoveredColor.HasValue)
{
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value);
numColors++;
col.Push(ImGuiCol.ButtonHovered, hoveredColor.Value);
}
ImGui.PushID(text);
if (size.HasValue)
{
size *= ImGuiHelpers.GlobalScale;
}
bool button;
Vector2 iconSize;
using (ImRaii.PushFont(UiBuilder.IconFont))
{
iconSize = ImGui.CalcTextSize(icon.ToIconString());
}
var textStr = text;
if (textStr.Contains('#'))
{
textStr = textStr[..textStr.IndexOf('#', StringComparison.Ordinal)];
}
var framePadding = ImGui.GetStyle().FramePadding;
var iconPadding = 3 * ImGuiHelpers.GlobalScale;
ImGui.PushFont(UiBuilder.IconFont);
var iconSize = ImGui.CalcTextSize(icon.ToIconString());
ImGui.PopFont();
var textSize = ImGui.CalcTextSize(text);
var dl = ImGui.GetWindowDrawList();
var cursor = ImGui.GetCursorScreenPos();
var iconPadding = 3 * ImGuiHelpers.GlobalScale;
// Draw an ImGui button with the icon and text
var buttonWidth = iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding;
var buttonHeight = ImGui.GetFrameHeight();
var button = ImGui.Button(string.Empty, new Vector2(buttonWidth, buttonHeight));
// Draw the icon on the window drawlist
var iconPos = new Vector2(cursor.X + ImGui.GetStyle().FramePadding.X, cursor.Y + ImGui.GetStyle().FramePadding.Y);
ImGui.PushFont(UiBuilder.IconFont);
dl.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
ImGui.PopFont();
// Draw the text on the window drawlist
var textPos = new Vector2(iconPos.X + iconSize.X + iconPadding, cursor.Y + ImGui.GetStyle().FramePadding.Y);
dl.AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), text);
using (ImRaii.PushId(text))
{
var textSize = ImGui.CalcTextSize(textStr);
ImGui.PopID();
var width = size is { X: not 0 } ? size.Value.X : iconSize.X + textSize.X + (framePadding.X * 2) + iconPadding;
var height = size is { Y: not 0 } ? size.Value.Y : ImGui.GetFrameHeight();
if (numColors > 0)
ImGui.PopStyleColor(numColors);
button = ImGui.Button(string.Empty, new Vector2(width, height));
}
var iconPos = cursor + framePadding;
var textPos = new Vector2(iconPos.X + iconSize.X + iconPadding, cursor.Y + framePadding.Y);
var dl = ImGui.GetWindowDrawList();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
dl.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
}
dl.AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), textStr);
return button;
}
@ -217,16 +272,15 @@ public static partial class ImGuiComponents
/// <returns>Width.</returns>
internal static float GetIconButtonWithTextWidth(FontAwesomeIcon icon, string text)
{
ImGui.PushFont(UiBuilder.IconFont);
var iconSize = ImGui.CalcTextSize(icon.ToIconString());
ImGui.PopFont();
var textSize = ImGui.CalcTextSize(text);
var dl = ImGui.GetWindowDrawList();
var cursor = ImGui.GetCursorScreenPos();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
var iconSize = ImGui.CalcTextSize(icon.ToIconString());
var iconPadding = 3 * ImGuiHelpers.GlobalScale;
return iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding;
var textSize = ImGui.CalcTextSize(text);
var iconPadding = 3 * ImGuiHelpers.GlobalScale;
return iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding;
}
}
}

View file

@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.Components;
public static partial class ImGuiComponents
{
/// <summary>
/// A radio-like input that uses icon buttons.
/// </summary>
/// <typeparam name="T">The type of the value being set.</typeparam>
/// <param name="label">Text that will be used to generate individual labels for the buttons.</param>
/// <param name="val">The value to set.</param>
/// <param name="optionIcons">The icons that will be displayed on each button.</param>
/// <param name="optionValues">The options that each button will apply.</param>
/// <param name="columns">Arranges the buttons in a grid with the given number of columns. 0 = ignored (all buttons drawn in one row).</param>
/// <param name="buttonSize">Sets the size of all buttons. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <param name="defaultColor">The default color of the button range.</param>
/// <param name="activeColor">The color of the actively-selected button.</param>
/// <param name="hoveredColor">The color of the buttons when hovered.</param>
/// <returns>True if any button is clicked.</returns>
internal static bool IconButtonSelect<T>(string label, ref T val, IEnumerable<FontAwesomeIcon> optionIcons, IEnumerable<T> optionValues, uint columns = 0, Vector2? buttonSize = null, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
{
var options = optionIcons.Zip(optionValues, static (icon, value) => new KeyValuePair<FontAwesomeIcon, T>(icon, value));
return IconButtonSelect(label, ref val, options, columns, buttonSize, defaultColor, activeColor, hoveredColor);
}
/// <summary>
/// A radio-like input that uses icon buttons.
/// </summary>
/// <typeparam name="T">The type of the value being set.</typeparam>
/// <param name="label">Text that will be used to generate individual labels for the buttons.</param>
/// <param name="val">The value to set.</param>
/// <param name="options">A list of all icon/option pairs.</param>
/// <param name="columns">Arranges the buttons in a grid with the given number of columns. 0 = ignored (all buttons drawn in one row).</param>
/// <param name="buttonSize">Sets the size of all buttons. If either dimension is set to 0, that dimension will conform to the size of the icon.</param>
/// <param name="defaultColor">The default color of the button range.</param>
/// <param name="activeColor">The color of the actively-selected button.</param>
/// <param name="hoveredColor">The color of the buttons when hovered.</param>
/// <returns>True if any button is clicked.</returns>
internal static unsafe bool IconButtonSelect<T>(string label, ref T val, IEnumerable<KeyValuePair<FontAwesomeIcon, T>> options, uint columns = 0, Vector2? buttonSize = null, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
{
defaultColor ??= *ImGui.GetStyleColorVec4(ImGuiCol.Button);
activeColor ??= *ImGui.GetStyleColorVec4(ImGuiCol.ButtonActive);
hoveredColor ??= *ImGui.GetStyleColorVec4(ImGuiCol.ButtonHovered);
var result = false;
var innerSpacing = ImGui.GetStyle().ItemInnerSpacing;
var y = ImGui.GetCursorPosY();
var optArr = options.ToArray();
for (var i = 0; i < optArr.Length; i++)
{
if (i > 0)
{
if (columns == 0 || i % columns != 0)
{
ImGui.SameLine(0, innerSpacing.X);
}
else
{
y += (buttonSize is { Y: not 0 } ? buttonSize.Value.Y * ImGuiHelpers.GlobalScale : ImGui.GetFrameHeight()) + innerSpacing.Y;
ImGui.SetCursorPosY(y);
}
}
optArr[i].Deconstruct(out var icon, out var option);
var selected = val is not null && val.Equals(option);
if (IconButton($"{label}{option}{i}", icon, selected ? activeColor : defaultColor, activeColor, hoveredColor, buttonSize))
{
val = option;
result = true;
}
}
return result;
}
}

View file

@ -1,3 +1,5 @@
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Components;
@ -24,7 +26,13 @@ public static partial class ImGuiComponents
else
{
ImGui.Text(value + "*");
if (ImGui.IsItemHovered()) ImGui.SetTooltip(hint);
if (ImGui.IsItemHovered())
{
using (ImRaii.Tooltip())
{
ImGui.TextUnformatted(hint);
}
}
}
}
}

View file

@ -36,9 +36,14 @@ public static partial class ImGuiComponents
}
if (ImGui.IsItemHovered())
{
drawList.AddRectFilled(p, new Vector2(p.X + width, p.Y + height), ImGui.GetColorU32(!v ? colors[(int)ImGuiCol.ButtonActive] : new Vector4(0.78f, 0.78f, 0.78f, 1.0f)), height * 0.5f);
}
else
{
drawList.AddRectFilled(p, new Vector2(p.X + width, p.Y + height), ImGui.GetColorU32(!v ? colors[(int)ImGuiCol.Button] * 0.6f : new Vector4(0.35f, 0.35f, 0.35f, 1.0f)), height * 0.50f);
}
drawList.AddCircleFilled(new Vector2(p.X + radius + ((v ? 1 : 0) * (width - (radius * 2.0f))), p.Y + radius), radius - 1.5f, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 1)));
return changed;
@ -62,7 +67,7 @@ public static partial class ImGuiComponents
// TODO: animate
ImGui.InvisibleButton(id, new Vector2(width, height));
var dimFactor = 0.5f;
const float dimFactor = 0.5f;
drawList.AddRectFilled(p, new Vector2(p.X + width, p.Y + height), ImGui.GetColorU32(v ? colors[(int)ImGuiCol.Button] * dimFactor : new Vector4(0.55f, 0.55f, 0.55f, 1.0f) * dimFactor), height * 0.50f);
drawList.AddCircleFilled(new Vector2(p.X + radius + ((v ? 1 : 0) * (width - (radius * 2.0f))), p.Y + radius), radius - 1.5f, ImGui.ColorConvertFloat4ToU32(new Vector4(1, 1, 1, 1) * dimFactor));

View file

@ -5,7 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets2;
using Lumina.Excel.Sheets;
using Lumina.Text.Expressions;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;

View file

@ -18,7 +18,7 @@ using FFXIVClientStructs.FFXIV.Client.UI;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets2;
using Lumina.Excel.Sheets;
using Lumina.Text.Parse;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;

View file

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Console;
using Dalamud.Data;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
@ -57,6 +58,7 @@ internal class DalamudInterface : IInternalDisposableService
private readonly Dalamud dalamud;
private readonly DalamudConfiguration configuration;
private readonly InterfaceManager interfaceManager;
private readonly DataManager dataManager;
private readonly ChangelogWindow changelogWindow;
private readonly ColorDemoWindow colorDemoWindow;
@ -98,6 +100,7 @@ internal class DalamudInterface : IInternalDisposableService
DalamudConfiguration configuration,
FontAtlasFactory fontAtlasFactory,
InterfaceManager interfaceManager,
DataManager dataManager,
PluginImageCache pluginImageCache,
DalamudAssetManager dalamudAssetManager,
Game.Framework framework,
@ -110,6 +113,7 @@ internal class DalamudInterface : IInternalDisposableService
this.dalamud = dalamud;
this.configuration = configuration;
this.interfaceManager = interfaceManager;
this.dataManager = dataManager;
this.WindowSystem = new WindowSystem("DalamudCore");

View file

@ -0,0 +1,124 @@
using Dalamud.Interface.Internal.UiDebug2.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Memory;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
public unsafe partial class AddonTree
{
/// <summary>
/// Prints a table of AtkValues associated with a given addon.
/// </summary>
/// <param name="addon">The addon to look up.</param>
internal static void PrintAtkValues(AtkUnitBase* addon)
{
var atkValue = addon->AtkValues;
if (addon->AtkValuesCount > 0 && atkValue != null)
{
using var tree = ImRaii.TreeNode($"Atk Values [{addon->AtkValuesCount}]###atkValues_{addon->NameString}");
if (tree)
{
using (ImRaii.Table(
"atkUnitBase_atkValueTable",
3,
ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg))
{
ImGui.TableSetupColumn("Index");
ImGui.TableSetupColumn("Type");
ImGui.TableSetupColumn("Value");
ImGui.TableHeadersRow();
try
{
for (var i = 0; i < addon->AtkValuesCount; i++)
{
ImGui.TableNextColumn();
if (atkValue->Type == 0)
{
ImGui.TextDisabled($"#{i}");
}
else
{
ImGui.Text($"#{i}");
}
ImGui.TableNextColumn();
if (atkValue->Type == 0)
{
ImGui.TextDisabled("Not Set");
}
else
{
ImGui.Text($"{atkValue->Type}");
}
ImGui.TableNextColumn();
switch (atkValue->Type)
{
case 0:
break;
case ValueType.Int:
case ValueType.UInt:
{
ImGui.TextUnformatted($"{atkValue->Int}");
break;
}
case ValueType.ManagedString:
case ValueType.String8:
case ValueType.String:
{
if (atkValue->String == null)
{
ImGui.TextDisabled("null");
}
else
{
var str = MemoryHelper.ReadSeStringNullTerminated(new nint(atkValue->String));
Util.ShowStruct(str, (ulong)atkValue);
}
break;
}
case ValueType.Bool:
{
ImGui.TextUnformatted($"{atkValue->Byte != 0}");
break;
}
case ValueType.Pointer:
ImGui.TextUnformatted($"{(nint)atkValue->Pointer}");
break;
default:
{
ImGui.TextDisabled("Unhandled Type");
ImGui.SameLine();
Util.ShowStruct(atkValue);
break;
}
}
atkValue++;
}
}
catch (Exception ex)
{
ImGui.TextColored(new(1, 0, 0, 1), $"{ex}");
}
}
}
Gui.PaddedSeparator();
}
}
}

View file

@ -16,13 +16,18 @@ public unsafe partial class AddonTree
{
private static readonly Dictionary<string, Type?> AddonTypeDict = [];
private static readonly Assembly? ClientStructsAssembly = typeof(Addon).Assembly;
private static readonly Assembly? ClientStructsAssembly = typeof(AddonAttribute).Assembly;
/// <summary>
/// Gets or sets a collection of names for field offsets that have been documented in FFXIVClientStructs.
/// </summary>
internal Dictionary<nint, List<string>> FieldNames { get; set; } = [];
/// <summary>
/// Gets or sets the size of the addon according to its Attributes in FFXIVClientStructs.
/// </summary>
internal int AddonSize { get; set; }
private object? GetAddonObj(AtkUnitBase* addon)
{
if (addon == null)
@ -36,12 +41,19 @@ public unsafe partial class AddonTree
{
foreach (var t in from t in ClientStructsAssembly.GetTypes()
where t.IsPublic
let xivAddonAttr = (Addon?)t.GetCustomAttribute(typeof(Addon), false)
let xivAddonAttr = (AddonAttribute?)t.GetCustomAttribute(typeof(AddonAttribute), false)
where xivAddonAttr != null
where xivAddonAttr.AddonIdentifiers.Contains(this.AddonName)
select t)
{
AddonTypeDict[this.AddonName] = t;
var size = t.StructLayoutAttribute?.Size;
if (size != null)
{
this.AddonSize = size.Value;
}
break;
}
}

View file

@ -3,6 +3,7 @@ using System.Linq;
using System.Numerics;
using Dalamud.Interface.Components;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
@ -19,14 +20,12 @@ namespace Dalamud.Interface.Internal.UiDebug2.Browsing;
/// </summary>
public unsafe partial class AddonTree : IDisposable
{
private readonly nint initialPtr;
private AddonPopoutWindow? window;
private AddonTree(string name, nint ptr)
{
this.AddonName = name;
this.initialPtr = ptr;
this.InitialPtr = ptr;
this.PopulateFieldNames(ptr);
}
@ -35,6 +34,11 @@ public unsafe partial class AddonTree : IDisposable
/// </summary>
internal string AddonName { get; init; }
/// <summary>
/// Gets the addon's pointer at the time this <see cref="AddonTree"/> was created.
/// </summary>
internal nint InitialPtr { get; init; }
/// <summary>
/// Gets or sets a collection of trees representing nodes within this addon.
/// </summary>
@ -81,7 +85,7 @@ public unsafe partial class AddonTree : IDisposable
{
if (AddonTrees.TryGetValue(name, out var tree))
{
if (tree.initialPtr == ptr)
if (tree.InitialPtr == ptr)
{
return tree;
}
@ -143,34 +147,47 @@ public unsafe partial class AddonTree : IDisposable
ImGui.SetTooltip("Toggle Popout Window");
}
ImGui.Separator();
PrintFieldValuePair("Address", $"{(nint)addon:X}");
PaddedSeparator(1);
var uldManager = addon->UldManager;
PrintFieldValuePair("Address", $"{(nint)addon:X}");
PrintFieldValuePair("Agent", $"{GameGui.FindAgentInterface(addon):X}");
PrintFieldValuePairs(
("X", $"{addon->X}"),
("Y", $"{addon->X}"),
("Y", $"{addon->Y}"),
("Scale", $"{addon->Scale}"),
("Widget Count", $"{uldManager.ObjectCount}"));
ImGui.Separator();
var addonObj = this.GetAddonObj(addon);
if (addonObj != null)
{
PaddedSeparator();
ShowStruct(addonObj, (ulong)addon);
}
ImGui.Dummy(new(25 * ImGui.GetIO().FontGlobalScale));
ImGui.Separator();
PaddedSeparator();
ResNodeTree.PrintNodeList(uldManager.NodeList, uldManager.NodeListCount, this);
PrintAtkValues(addon);
ImGui.Dummy(new(25 * ImGui.GetIO().FontGlobalScale));
ImGui.Separator();
if (addon->RootNode != null)
{
ResNodeTree.GetOrCreate(addon->RootNode, this).Print(0);
PaddedSeparator();
}
ResNodeTree.PrintNodeListAsTree(addon->CollisionNodeList, (int)addon->CollisionNodeListCount, "Collision List", this, new(0.5F, 0.7F, 1F, 1F));
if (uldManager.NodeList != null)
{
var count = uldManager.NodeListCount;
ResNodeTree.PrintNodeListAsTree(uldManager.NodeList, count, $"Node List [{count}]:", this, new(0, 0.85F, 1, 1));
PaddedSeparator();
}
if (addon->CollisionNodeList != null)
{
ResNodeTree.PrintNodeListAsTree(addon->CollisionNodeList, (int)addon->CollisionNodeListCount, "Collision List", this, new(0.5F, 0.7F, 1F, 1F));
}
if (SearchResults.Length > 0 && Countdown <= 0)
{
@ -218,7 +235,7 @@ public unsafe partial class AddonTree : IDisposable
private bool ValidateAddon(out AtkUnitBase* addon)
{
addon = (AtkUnitBase*)GameGui.GetAddonByName(this.AddonName);
if (addon == null || (nint)addon != this.initialPtr)
if (addon == null || (nint)addon != this.InitialPtr)
{
this.Dispose();
return false;
@ -231,7 +248,7 @@ public unsafe partial class AddonTree : IDisposable
{
if (this.window == null)
{
this.window = new AddonPopoutWindow(this, $"{this.AddonName}###addonPopout{this.initialPtr}");
this.window = new AddonPopoutWindow(this, $"{this.AddonName}###addonPopout{this.InitialPtr}");
PopoutWindows.AddWindow(this.window);
}
else

View file

@ -1,4 +1,6 @@
using Dalamud.Interface.Internal.UiDebug2.Utility;
using System.Numerics;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -36,7 +38,7 @@ public static class Events
ImGui.TableSetupColumn("Type", WidthFixed);
ImGui.TableSetupColumn("Param", WidthFixed);
ImGui.TableSetupColumn("Flags", WidthFixed);
ImGui.TableSetupColumn("Unk29", WidthFixed);
ImGui.TableSetupColumn("StateFlags1", WidthFixed);
ImGui.TableSetupColumn("Target", WidthFixed);
ImGui.TableSetupColumn("Listener", WidthFixed);
@ -48,17 +50,17 @@ public static class Events
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{i++}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{evt->Type}");
ImGui.TextUnformatted($"{evt->State.EventType}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{evt->Param}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{evt->Flags}");
ImGui.TextUnformatted($"{evt->State.StateFlags}");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{evt->Unk29}");
ImGui.TextUnformatted($"{evt->State.UnkFlags1}");
ImGui.TableNextColumn();
Gui.ClickToCopyText($"{(nint)evt->Target:X}");
ImGuiHelpers.ClickToCopyText($"{(nint)evt->Target:X}", null, new Vector4(0.6f, 0.6f, 0.6f, 1));
ImGui.TableNextColumn();
Gui.ClickToCopyText($"{(nint)evt->Listener:X}");
ImGuiHelpers.ClickToCopyText($"{(nint)evt->Listener:X}", null, new Vector4(0.6f, 0.6f, 0.6f, 1));
evt = evt->NextEvent;
}
}

View file

@ -34,11 +34,13 @@ internal unsafe class ComponentNodeTree : ResNodeTree
private AtkUldManager* UldManager => &this.Component->UldManager;
private int? ComponentFieldOffset { get; set; }
/// <inheritdoc/>
private protected override string GetHeaderText()
{
var childCount = (int)this.UldManager->NodeListCount;
return $"{this.componentType} Component Node{(childCount > 0 ? $" [+{childCount}]" : string.Empty)} (Node: {(nint)this.Node:X} / Comp: {(nint)this.Component:X})";
return $"{this.componentType} Component Node{(childCount > 0 ? $" [+{childCount}]" : string.Empty)}";
}
/// <inheritdoc/>
@ -62,10 +64,10 @@ internal unsafe class ComponentNodeTree : ResNodeTree
}
/// <inheritdoc/>
private protected override void PrintFieldNames()
private protected override void PrintFieldLabels()
{
this.PrintFieldName((nint)this.Node, new(0, 0.85F, 1, 1));
this.PrintFieldName((nint)this.Component, new(0f, 0.5f, 0.8f, 1f));
this.PrintFieldLabel((nint)this.Node, new(0, 0.85F, 1, 1), this.NodeFieldOffset);
this.PrintFieldLabel((nint)this.Component, new(0f, 0.5f, 0.8f, 1f), this.ComponentFieldOffset);
}
/// <inheritdoc/>
@ -108,6 +110,34 @@ internal unsafe class ComponentNodeTree : ResNodeTree
}
}
/// <inheritdoc/>
private protected override void GetFieldOffset()
{
var nodeFound = false;
var componentFound = false;
for (var i = 0; i < this.AddonTree.AddonSize; i += 0x8)
{
var readPtr = Marshal.ReadIntPtr(this.AddonTree.InitialPtr + i);
if (readPtr == (nint)this.Node)
{
this.NodeFieldOffset = i;
nodeFound = true;
}
if (readPtr == (nint)this.Component)
{
this.ComponentFieldOffset = i;
componentFound = true;
}
if (nodeFound && componentFound)
{
break;
}
}
}
private void PrintComponentObject()
{
PrintFieldValuePair("Component", $"{(nint)this.Component:X}");

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.UiDebug2.Utility;
using Dalamud.Interface.Utility.Raii;
@ -370,8 +371,8 @@ internal unsafe partial class TextNodeTree
var hAlign = (int)alignment % 3;
var vAlign = ((int)alignment - hAlign) / 3;
var hAlignInput = IconButtonSelect($"{label}H", ref hAlign, [0, 1, 2], [AlignLeft, AlignCenter, AlignRight]);
var vAlignInput = IconButtonSelect($"{label}V", ref vAlign, [0, 1, 2], [ArrowsUpToLine, GripLines, ArrowsDownToLine]);
var hAlignInput = ImGuiComponents.IconButtonSelect($"{label}H", ref hAlign, [AlignLeft, AlignCenter, AlignRight], [0, 1, 2], 3u, new(25, 0));
var vAlignInput = ImGuiComponents.IconButtonSelect($"{label}V", ref vAlign, [ArrowsUpToLine, GripLines, ArrowsDownToLine], [0, 1, 2], 3u, new(25, 0));
if (hAlignInput || vAlignInput)
{

View file

@ -1,5 +1,6 @@
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.UiDebug2.Utility;
@ -60,6 +61,11 @@ internal unsafe partial class ResNodeTree : IDisposable
/// </summary>
private protected NodeType NodeType { get; init; }
/// <summary>
/// Gets or sets the offset of this node within its parent Addon.
/// </summary>
private protected int? NodeFieldOffset { get; set; }
/// <summary>
/// Clears this NodeTree's popout window, if it has one.
/// </summary>
@ -164,19 +170,26 @@ internal unsafe partial class ResNodeTree : IDisposable
internal void WriteTreeHeading()
{
ImGui.TextUnformatted(this.GetHeaderText());
this.PrintFieldNames();
this.PrintFieldLabels();
}
/// <summary>
/// If the given pointer has been identified as a field within the addon struct, this method prints that field's name.
/// If the given pointer is referenced with the addon struct, the offset within the addon will be printed. If the given pointer has been identified as a field within the addon struct, this method also prints that field's name.
/// </summary>
/// <param name="ptr">The pointer to check.</param>
/// <param name="color">The text color to use.</param>
private protected void PrintFieldName(nint ptr, Vector4 color)
/// <param name="fieldOffset">The field offset of the pointer, if it was found in the addon.</param>
private protected void PrintFieldLabel(nint ptr, Vector4 color, int? fieldOffset)
{
if (fieldOffset != null)
{
ImGui.SameLine(0, -1);
ImGui.TextColored(color * 0.85f, $"[0x{fieldOffset:X}]");
}
if (this.AddonTree.FieldNames.TryGetValue(ptr, out var result))
{
ImGui.SameLine();
ImGui.SameLine(0, -1);
ImGui.TextColored(color, string.Join(".", result));
}
}
@ -188,7 +201,15 @@ internal unsafe partial class ResNodeTree : IDisposable
private protected virtual string GetHeaderText()
{
var count = this.GetDirectChildCount();
return $"{this.NodeType} Node{(count > 0 ? $" [+{count}]" : string.Empty)} ({(nint)this.Node:X})";
return $"{this.NodeType} Node{(count > 0 ? $" [+{count}]" : string.Empty)}";
}
/// <summary>
/// Prints any field names for the node.
/// </summary>
private protected virtual void PrintFieldLabels()
{
this.PrintFieldLabel((nint)this.Node, new(0, 0.85F, 1, 1), this.NodeFieldOffset);
}
/// <summary>
@ -201,11 +222,6 @@ internal unsafe partial class ResNodeTree : IDisposable
ImGui.NewLine();
}
/// <summary>
/// Prints any field names for the node.
/// </summary>
private protected virtual void PrintFieldNames() => this.PrintFieldName((nint)this.Node, new(0, 0.85F, 1, 1));
/// <summary>
/// Prints all direct children of this node.
/// </summary>
@ -227,6 +243,21 @@ internal unsafe partial class ResNodeTree : IDisposable
{
}
/// <summary>
/// Attempts to retrieve the field offset of the given pointer within the parent addon.
/// </summary>
private protected virtual void GetFieldOffset()
{
for (var i = 0; i < this.AddonTree.AddonSize; i += 0x8)
{
if (Marshal.ReadIntPtr(this.AddonTree.InitialPtr + i) == (nint)this.Node)
{
this.NodeFieldOffset = i;
break;
}
}
}
private int GetDirectChildCount()
{
var count = 0;
@ -273,6 +304,8 @@ internal unsafe partial class ResNodeTree : IDisposable
ImGui.SetNextItemOpen(true, ImGuiCond.Always);
}
this.GetFieldOffset();
using var col = ImRaii.PushColor(Text, displayColor);
using var tree = ImRaii.TreeNode(label, SpanFullWidth);
@ -281,7 +314,7 @@ internal unsafe partial class ResNodeTree : IDisposable
new NodeBounds(this.Node).Draw(visible ? new(0.1f, 1f, 0.1f, 1f) : new(1f, 0f, 0.2f, 1f));
}
ImGui.SameLine();
ImGui.SameLine(0, -1);
this.WriteTreeHeading();
col.Pop();

View file

@ -79,7 +79,7 @@ internal unsafe class ElementSelector : IDisposable
/// </summary>
internal void DrawInterface()
{
using (ImRaii.Child("###sidebar_elementSelector", new(250, 0), true))
using (ImRaii.Child("###sidebar_elementSelector", new(250, -1), true))
{
using (ImRaii.PushFont(IconFont))
{

View file

@ -1,7 +1,6 @@
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Client.Graphics;
@ -17,40 +16,6 @@ namespace Dalamud.Interface.Internal.UiDebug2.Utility;
/// </summary>
internal static class Gui
{
/// <summary>
/// A radio-button-esque input that uses Fontawesome icon buttons.
/// </summary>
/// <typeparam name="T">The type of value being set.</typeparam>
/// <param name="label">The label for the inputs.</param>
/// <param name="val">The value being set.</param>
/// <param name="options">A list of all options.</param>
/// <param name="icons">A list of icons corresponding to the options.</param>
/// <returns>true if a button is clicked.</returns>
internal static unsafe bool IconButtonSelect<T>(string label, ref T val, List<T> options, List<FontAwesomeIcon> icons)
{
var ret = false;
for (var i = 0; i < options.Count; i++)
{
if (i > 0)
{
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
}
var option = options[i];
var icon = icons.Count > i ? icons[i] : FontAwesomeIcon.Question;
var color = *ImGui.GetStyleColorVec4(val is not null && val.Equals(option) ? ButtonActive : Button);
if (ImGuiComponents.IconButton($"{label}{option}{i}", icon, color))
{
val = option;
ret = true;
}
}
return ret;
}
/// <summary>
/// Prints field name and its value.
/// </summary>
@ -61,13 +26,14 @@ internal static class Gui
{
ImGui.TextUnformatted($"{fieldName}:");
ImGui.SameLine();
var grey60 = new Vector4(0.6f, 0.6f, 0.6f, 1);
if (copy)
{
ClickToCopyText(value);
ImGuiHelpers.ClickToCopyText(value, null, grey60);
}
else
{
ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), value);
ImGui.TextColored(grey60, value);
}
}
@ -102,7 +68,10 @@ internal static class Gui
/// <remarks>Colors the text itself either white or black, depending on the luminosity of the background color.</remarks>
internal static void PrintColor(Vector4 color, string fmt)
{
using (new ImRaii.Color().Push(Text, Luminosity(color) < 0.5f ? new Vector4(1) : new(0, 0, 0, 1)).Push(Button, color).Push(ButtonActive, color).Push(ButtonHovered, color))
using (new ImRaii.Color().Push(Text, Luminosity(color) < 0.5f ? new Vector4(1) : new(0, 0, 0, 1))
.Push(Button, color)
.Push(ButtonActive, color)
.Push(ButtonHovered, color))
{
ImGui.SmallButton(fmt);
}
@ -117,39 +86,6 @@ internal static class Gui
0.5f) * vector4.W;
}
/// <summary>
/// Print out text that can be copied when clicked.
/// </summary>
/// <param name="text">The text to show.</param>
/// <param name="textCopy">The text to copy when clicked.</param>
internal static void ClickToCopyText(string text, string? textCopy = null)
{
using (ImRaii.PushColor(Text, new Vector4(0.6f, 0.6f, 0.6f, 1)))
{
textCopy ??= text;
ImGui.TextUnformatted($"{text}");
}
if (ImGui.IsItemHovered())
{
using (ImRaii.Tooltip())
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextUnformatted(FontAwesomeIcon.Copy.ToIconString());
}
ImGui.SameLine();
ImGui.TextUnformatted($"{textCopy}");
}
}
if (ImGui.IsItemClicked())
{
ImGui.SetClipboardText($"{textCopy}");
}
}
/// <summary>
/// Draws a tooltip that changes based on the cursor's x-position within the hovered item.
/// </summary>
@ -176,4 +112,23 @@ internal static class Gui
return true;
}
/// <summary>
/// Draws a separator with some padding above and below.
/// </summary>
/// <param name="mask">Governs whether to pad above, below, or both.</param>
/// <param name="padding">The amount of padding.</param>
internal static void PaddedSeparator(uint mask = 0b11, float padding = 5f)
{
if ((mask & 0b10) > 0)
{
ImGui.Dummy(new(padding * ImGui.GetIO().FontGlobalScale));
}
ImGui.Separator();
if ((mask & 0b01) > 0)
{
ImGui.Dummy(new(padding * ImGui.GetIO().FontGlobalScale));
}
}
}

View file

@ -6,7 +6,7 @@ using Dalamud.Interface.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using static System.Math;
using static System.MathF;
using static Dalamud.Interface.ColorHelpers;
namespace Dalamud.Interface.Internal.UiDebug2.Utility;
@ -133,7 +133,7 @@ public unsafe struct NodeBounds
if (p.Y > Min(p1.Y, p2.Y) &&
p.Y <= Max(p1.Y, p2.Y) &&
p.X <= Max(p1.X, p2.X) &&
(p1.X.Equals(p2.X) || p.X <= ((p.Y - p1.Y) * (p2.X - p1.X) / (p2.Y - p1.Y)) + p1.X))
(p1.X.Equals(p2.X) || p.X <= (((p.Y - p1.Y) * (p2.X - p1.X)) / (p2.Y - p1.Y)) + p1.X))
{
inside = !inside;
}
@ -144,12 +144,12 @@ public unsafe struct NodeBounds
private static Vector2 TransformPoint(Vector2 p, Vector2 o, float r, Vector2 s)
{
var cosR = (float)Cos(r);
var sinR = (float)Sin(r);
var cosR = Cos(r);
var sinR = Sin(r);
var d = (p - o) * s;
return new(
o.X + (d.X * cosR) - (d.Y * sinR),
(o.X + (d.X * cosR)) - (d.Y * sinR),
o.Y + (d.X * sinR) + (d.Y * cosR));
}

View file

@ -1,4 +1,4 @@
using Dalamud.Game.ClientState.Aetherytes;
using Dalamud.Game.ClientState.Aetherytes;
using ImGuiNET;
@ -56,7 +56,7 @@ internal class AetherytesWidget : IDataWindowWidget
ImGui.TextUnformatted($"{i}");
ImGui.TableNextColumn(); // Name
ImGui.TextUnformatted($"{info.AetheryteData.GameData?.PlaceName.Value?.Name}");
ImGui.TextUnformatted($"{info.AetheryteData.ValueNullable?.PlaceName.ValueNullable?.Name}");
ImGui.TableNextColumn(); // ID
ImGui.TextUnformatted($"{info.AetheryteId}");

View file

@ -1,4 +1,4 @@
using Dalamud.Game.ClientState.Buddy;
using Dalamud.Game.ClientState.Buddy;
using Dalamud.Utility;
using ImGuiNET;
@ -32,8 +32,6 @@ internal class BuddyListWidget : IDataWindowWidget
var buddyList = Service<BuddyList>.Get();
ImGui.Checkbox("Resolve GameData", ref this.resolveGameData);
ImGui.Text($"BuddyList: {buddyList.BuddyListAddress.ToInt64():X}");
{
var member = buddyList.CompanionBuddy;
if (member == null)

View file

@ -153,7 +153,7 @@ internal class FateTableWidget : IDataWindowWidget
}
ImGui.TableNextColumn(); // HasExpBonus
ImGui.TextUnformatted(fate.HasExpBonus.ToString());
ImGui.TextUnformatted(fate.HasBonus.ToString());
ImGui.TableNextColumn(); // Position
DrawCopyableText(fate.Position.ToString(), "Click to copy Position");

View file

@ -1,4 +1,4 @@
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.JobGauge;
using Dalamud.Game.ClientState.JobGauge.Types;
using Dalamud.Utility;
@ -39,7 +39,7 @@ internal class GaugeWidget : IDataWindowWidget
return;
}
var jobID = player.ClassJob.Id;
var jobID = player.ClassJob.RowId;
JobGaugeBase? gauge = jobID switch
{
19 => jobGauges.Get<PLDGauge>(),

View file

@ -17,6 +17,8 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary>
public class IconBrowserWidget : IDataWindowWidget
{
private const int MaxIconId = 250_000;
private Vector2 iconSize = new(64.0f, 64.0f);
private Vector2 editIconSize = new(64.0f, 64.0f);
@ -24,7 +26,7 @@ public class IconBrowserWidget : IDataWindowWidget
private Task<List<(int ItemId, string Path)>>? iconIdsTask;
private int startRange;
private int stopRange = 200000;
private int stopRange = MaxIconId;
private bool showTooltipImage;
private Vector2 mouseDragStart;
@ -53,8 +55,8 @@ public class IconBrowserWidget : IDataWindowWidget
{
var texm = Service<TextureManager>.Get();
var result = new List<(int ItemId, string Path)>(200000);
for (var iconId = 0; iconId < 200000; iconId++)
var result = new List<(int ItemId, string Path)>(MaxIconId);
for (var iconId = 0; iconId < MaxIconId; iconId++)
{
// // Remove range 170,000 -> 180,000 by default, this specific range causes exceptions.
// if (iconId is >= 170000 and < 180000)
@ -119,12 +121,19 @@ public class IconBrowserWidget : IDataWindowWidget
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputInt("##StartRange", ref this.startRange, 0, 0))
{
this.startRange = Math.Clamp(this.startRange, 0, MaxIconId);
this.valueRange = null;
}
ImGui.NextColumn();
ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X);
if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0))
{
this.stopRange = Math.Clamp(this.stopRange, 0, MaxIconId);
this.valueRange = null;
}
ImGui.NextColumn();
ImGui.Checkbox("Show Image in Tooltip", ref this.showTooltipImage);

View file

@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
@ -56,8 +56,8 @@ internal class ObjectTableWidget : IDataWindowWidget
{
stateString += $"ObjectTableLen: {objectTable.Length}\n";
stateString += $"LocalPlayerName: {clientState.LocalPlayer.Name}\n";
stateString += $"CurrentWorldName: {(this.resolveGameData ? clientState.LocalPlayer.CurrentWorld.GameData?.Name : clientState.LocalPlayer.CurrentWorld.Id.ToString())}\n";
stateString += $"HomeWorldName: {(this.resolveGameData ? clientState.LocalPlayer.HomeWorld.GameData?.Name : clientState.LocalPlayer.HomeWorld.Id.ToString())}\n";
stateString += $"CurrentWorldName: {(this.resolveGameData ? clientState.LocalPlayer.CurrentWorld.ValueNullable?.Name : clientState.LocalPlayer.CurrentWorld.RowId.ToString())}\n";
stateString += $"HomeWorldName: {(this.resolveGameData ? clientState.LocalPlayer.HomeWorld.ValueNullable?.Name : clientState.LocalPlayer.HomeWorld.RowId.ToString())}\n";
stateString += $"LocalCID: {clientState.LocalContentId:X}\n";
stateString += $"LastLinkedItem: {chatGui.LastLinkedItemId}\n";
stateString += $"TerritoryType: {clientState.TerritoryType}\n\n";

View file

@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Numerics;
using System.Text;
@ -16,7 +16,8 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets2;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
@ -33,7 +34,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
private static readonly string[] ThemeNames = ["Dark", "Light", "Classic FF", "Clear Blue"];
private ImVectorWrapper<byte> testStringBuffer;
private string testString = string.Empty;
private Addon[]? addons;
private ExcelSheet<Addon> addons;
private ReadOnlySeString? logkind;
private SeStringDrawParams style;
private bool interactable;
@ -53,7 +54,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
public void Load()
{
this.style = new() { GetEntity = this.GetEntity };
this.addons = null;
this.addons = Service<DataManager>.Get().GetExcelSheet<Addon>();
this.logkind = null;
this.testString = string.Empty;
this.interactable = this.useEntity = true;
@ -155,9 +156,9 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (this.logkind is null)
{
var tt = new SeStringBuilder();
foreach (var uc in Service<DataManager>.Get().GetExcelSheet<LogKind>()!)
foreach (var uc in Service<DataManager>.Get().GetExcelSheet<LogKind>())
{
var ucsp = uc.Format.AsReadOnly().AsSpan();
var ucsp = uc.Format.AsSpan();
if (ucsp.IsEmpty)
continue;
@ -184,7 +185,6 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (ImGui.CollapsingHeader("Addon Table"))
{
this.addons ??= Service<DataManager>.Get().GetExcelSheet<Addon>()!.ToArray();
if (ImGui.BeginTable("Addon Sheet", 3))
{
ImGui.TableSetupScrollFreeze(0, 1);
@ -197,25 +197,27 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGui.TableHeadersRow();
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
clipper.Begin(this.addons.Length);
clipper.Begin(this.addons.Count);
while (clipper.Step())
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
var row = this.addons.GetRowAt(i);
ImGui.TableNextRow();
ImGui.PushID(i);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted($"{this.addons[i].RowId}");
ImGui.TextUnformatted($"{row.RowId}");
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGuiHelpers.SeStringWrapped(this.addons[i].Text.AsReadOnly(), this.style);
ImGuiHelpers.SeStringWrapped(row.Text, this.style);
ImGui.TableNextColumn();
if (ImGui.Button("Print to Chat"))
Service<ChatGui>.Get().Print(this.addons[i].Text.ToDalamudString());
Service<ChatGui>.Get().Print(row.Text.ToDalamudString());
ImGui.PopID();
}

View file

@ -1,4 +1,4 @@
using System.Buffers.Binary;
using System.Buffers.Binary;
using System.Linq;
using System.Numerics;
using System.Text;
@ -12,7 +12,8 @@ using Dalamud.Storage.Assets;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@ -21,7 +22,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// </summary>
internal class UiColorWidget : IDataWindowWidget
{
private UIColor[]? colors;
private ExcelSheet<UIColor> colors;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = ["uicolor"];
@ -36,15 +37,12 @@ internal class UiColorWidget : IDataWindowWidget
public void Load()
{
this.Ready = true;
this.colors = null;
this.colors = Service<DataManager>.Get().GetExcelSheet<UIColor>();
}
/// <inheritdoc/>
public unsafe void Draw()
{
this.colors ??= Service<DataManager>.Get().GetExcelSheet<UIColor>()?.ToArray();
if (this.colors is null) return;
Service<SeStringRenderer>.Get().CompileAndDrawWrapped(
"· Color notation is #" +
"<edgecolor(0xFFEEEE)><color(0xFF0000)>RR<color(stackcolor)><edgecolor(stackcolor)>" +
@ -73,12 +71,24 @@ internal class UiColorWidget : IDataWindowWidget
ImGui.TableHeadersRow();
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
clipper.Begin(this.colors.Length, ImGui.GetFrameHeightWithSpacing());
clipper.Begin(this.colors.Count, ImGui.GetFrameHeightWithSpacing());
while (clipper.Step())
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
var id = this.colors[i].RowId;
var row = this.colors.GetRowAt(i);
UIColor? adjacentRow = null;
if (i + 1 < this.colors.Count)
{
var adjRow = this.colors.GetRowAt(i + 1);
if (adjRow.RowId == row.RowId + 1)
{
adjacentRow = adjRow;
}
}
var id = row.RowId;
ImGui.TableNextRow();
ImGui.TableNextColumn();
@ -88,33 +98,33 @@ internal class UiColorWidget : IDataWindowWidget
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.PushID($"row{id}_col1");
if (this.DrawColorColumn(this.colors[i].UIForeground) &&
i + 1 < this.colors.Length && this.colors[i + 1].RowId == id + 1)
DrawEdgePreview(id, this.colors[i].UIForeground, this.colors[i + 1].UIForeground);
if (this.DrawColorColumn(row.UIForeground) &&
adjacentRow.HasValue)
DrawEdgePreview(id, row.UIForeground, adjacentRow.Value.UIForeground);
ImGui.PopID();
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.PushID($"row{id}_col2");
if (this.DrawColorColumn(this.colors[i].UIGlow) &&
i + 1 < this.colors.Length && this.colors[i + 1].RowId == id + 1)
DrawEdgePreview(id, this.colors[i].UIGlow, this.colors[i + 1].UIGlow);
if (this.DrawColorColumn(row.UIGlow) &&
adjacentRow.HasValue)
DrawEdgePreview(id, row.UIGlow, adjacentRow.Value.UIGlow);
ImGui.PopID();
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.PushID($"row{id}_col3");
if (this.DrawColorColumn(this.colors[i].Unknown2) &&
i + 1 < this.colors.Length && this.colors[i + 1].RowId == id + 1)
DrawEdgePreview(id, this.colors[i].Unknown2, this.colors[i + 1].Unknown2);
if (this.DrawColorColumn(row.Unknown0) &&
adjacentRow.HasValue)
DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0);
ImGui.PopID();
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.PushID($"row{id}_col4");
if (this.DrawColorColumn(this.colors[i].Unknown3) &&
i + 1 < this.colors.Length && this.colors[i + 1].RowId == id + 1)
DrawEdgePreview(id, this.colors[i].Unknown3, this.colors[i + 1].Unknown3);
if (this.DrawColorColumn(row.Unknown1) &&
adjacentRow.HasValue)
DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1);
ImGui.PopID();
}
}

View file

@ -7,10 +7,9 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility;
using ImGuiNET;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using Serilog;
namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
@ -45,9 +44,9 @@ internal class ContextMenuAgingStep : IAgingStep
{
var contextMenu = Service<ContextMenu>.Get();
var dataMgr = Service<DataManager>.Get();
this.itemSheet = dataMgr.GetExcelSheet<Item>()!;
this.materiaSheet = dataMgr.GetExcelSheet<Materia>()!;
this.stainSheet = dataMgr.GetExcelSheet<Stain>()!;
this.itemSheet = dataMgr.GetExcelSheet<Item>();
this.materiaSheet = dataMgr.GetExcelSheet<Materia>();
this.stainSheet = dataMgr.GetExcelSheet<Stain>();
ImGui.Text(this.currentSubStep.ToString());
@ -83,7 +82,7 @@ internal class ContextMenuAgingStep : IAgingStep
case SubStep.TestDefault:
if (this.targetCharacter is { } character)
{
ImGui.Text($"Did you click \"{character.Name}\" ({character.ClassJob.GameData!.Abbreviation.ToDalamudString()})?");
ImGui.Text($"Did you click \"{character.Name}\" ({character.ClassJob.Value.Abbreviation.ExtractText()})?");
if (ImGui.Button("Yes"))
this.currentSubStep++;
@ -142,11 +141,11 @@ internal class ContextMenuAgingStep : IAgingStep
OnClicked = (IMenuItemClickedArgs a) =>
{
SeString name;
uint count;
int count;
var targetItem = (a.Target as MenuTargetInventory)!.TargetItem;
if (targetItem is { } item)
{
name = (this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty);
name = (this.itemSheet.GetRowOrDefault(item.ItemId)?.Name.ExtractText() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty);
count = item.Quantity;
}
else
@ -194,7 +193,7 @@ internal class ContextMenuAgingStep : IAgingStep
{
var b = new StringBuilder();
b.AppendLine($"Target: {targetDefault.TargetName}");
b.AppendLine($"Home World: {targetDefault.TargetHomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({targetDefault.TargetHomeWorld.Id})");
b.AppendLine($"Home World: {targetDefault.TargetHomeWorld.ValueNullable?.Name.ExtractText() ?? "Unknown"} ({targetDefault.TargetHomeWorld.RowId})");
b.AppendLine($"Content Id: 0x{targetDefault.TargetContentId:X8}");
b.AppendLine($"Object Id: 0x{targetDefault.TargetObjectId:X8}");
Log.Verbose(b.ToString());
@ -209,20 +208,20 @@ internal class ContextMenuAgingStep : IAgingStep
b.AppendLine($"Content Id: 0x{character.ContentId:X8}");
b.AppendLine($"FC Tag: {character.FCTag}");
b.AppendLine($"Job: {character.ClassJob.GameData?.Abbreviation.ToDalamudString() ?? "Unknown"} ({character.ClassJob.Id})");
b.AppendLine($"Statuses: {string.Join(", ", character.Statuses.Select(s => s.GameData?.Name.ToDalamudString() ?? s.Id.ToString()))}");
b.AppendLine($"Home World: {character.HomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.HomeWorld.Id})");
b.AppendLine($"Current World: {character.CurrentWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.CurrentWorld.Id})");
b.AppendLine($"Job: {character.ClassJob.ValueNullable?.Abbreviation.ExtractText() ?? "Unknown"} ({character.ClassJob.RowId})");
b.AppendLine($"Statuses: {string.Join(", ", character.Statuses.Select(s => s.ValueNullable?.Name.ExtractText() ?? s.RowId.ToString()))}");
b.AppendLine($"Home World: {character.HomeWorld.ValueNullable?.Name.ExtractText() ?? "Unknown"} ({character.HomeWorld.RowId})");
b.AppendLine($"Current World: {character.CurrentWorld.ValueNullable?.Name.ExtractText() ?? "Unknown"} ({character.CurrentWorld.RowId})");
b.AppendLine($"Is From Other Server: {character.IsFromOtherServer}");
b.Append("Location: ");
if (character.Location.GameData is { } location)
b.Append($"{location.PlaceNameRegion.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceNameZone.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceName.Value?.Name.ToDalamudString() ?? "Unknown"}");
if (character.Location.ValueNullable is { } location)
b.Append($"{location.PlaceNameRegion.ValueNullable?.Name.ExtractText() ?? "Unknown"}/{location.PlaceNameZone.ValueNullable?.Name.ExtractText() ?? "Unknown"}/{location.PlaceName.ValueNullable?.Name.ExtractText() ?? "Unknown"}");
else
b.Append("Unknown");
b.AppendLine($" ({character.Location.Id})");
b.AppendLine($" ({character.Location.RowId})");
b.AppendLine($"Grand Company: {character.GrandCompany.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.GrandCompany.Id})");
b.AppendLine($"Grand Company: {character.GrandCompany.ValueNullable?.Name.ExtractText() ?? "Unknown"} ({character.GrandCompany.RowId})");
b.AppendLine($"Client Language: {character.ClientLanguage}");
b.AppendLine($"Languages: {string.Join(", ", character.Languages)}");
b.AppendLine($"Gender: {character.Gender}");
@ -241,7 +240,7 @@ internal class ContextMenuAgingStep : IAgingStep
if (targetInventory.TargetItem is { } item)
{
var b = new StringBuilder();
b.AppendLine($"Item: {(item.IsEmpty ? "None" : this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString())} ({item.ItemId})");
b.AppendLine($"Item: {(item.IsEmpty ? "None" : this.itemSheet.GetRowOrDefault(item.ItemId)?.Name.ExtractText())} ({item.ItemId})");
b.AppendLine($"Container: {item.ContainerType}");
b.AppendLine($"Slot: {item.InventorySlot}");
b.AppendLine($"Quantity: {item.Quantity}");
@ -259,7 +258,7 @@ internal class ContextMenuAgingStep : IAgingStep
Log.Verbose($"{materiaId} {materiaGrade}");
if (this.materiaSheet.GetRow(materiaId) is { } materia &&
materia.Item[materiaGrade].Value is { } materiaItem)
materias.Add($"{materiaItem.Name.ToDalamudString()}");
materias.Add($"{materiaItem.Name.ExtractText()}");
else
materias.Add($"Unknown (Id: {materiaId}, Grade: {materiaGrade})");
}
@ -275,7 +274,7 @@ internal class ContextMenuAgingStep : IAgingStep
var stainId = item.Stains[i];
if (stainId != 0)
{
var stainName = this.stainSheet.GetRow(stainId)?.Name.ToDalamudString().ToString() ?? "Unknown";
var stainName = this.stainSheet.GetRowOrDefault(stainId)?.Name.ExtractText() ?? "Unknown";
b.AppendLine($" Stain {i + 1}: {stainName} ({stainId})");
}
else
@ -285,13 +284,13 @@ internal class ContextMenuAgingStep : IAgingStep
}
if (item.Stains[0] != 0)
b.AppendLine($"{this.stainSheet.GetRow(item.Stains[0])?.Name.ToDalamudString() ?? "Unknown"} ({item.Stains[0]})");
b.AppendLine($"{this.stainSheet.GetRowOrDefault(item.Stains[0])?.Name.ExtractText() ?? "Unknown"} ({item.Stains[0]})");
else
b.AppendLine("None");
b.Append("Glamoured Item: ");
if (item.GlamourId != 0)
b.AppendLine($"{this.itemSheet.GetRow(item.GlamourId)?.Name.ToDalamudString() ?? "Unknown"} ({item.GlamourId})");
b.AppendLine($"{this.itemSheet.GetRowOrDefault(item.GlamourId)?.Name.ExtractText() ?? "Unknown"} ({item.GlamourId})");
else
b.AppendLine("None");

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