Merge branch 'goatcorp:master' into Plugin-Installer-Plus-RepoFilter

This commit is contained in:
Jerric 2026-01-31 08:29:38 -05:00 committed by GitHub
commit db94b2cc87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 513 additions and 430 deletions

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description> <Description>XIV Launcher addon framework</Description>
<DalamudVersion>14.0.1.0</DalamudVersion> <DalamudVersion>14.0.2.0</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version> <Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion> <FileVersion>$(DalamudVersion)</FileVersion>
@ -65,7 +65,6 @@
<PackageReference Include="CheapLoc" /> <PackageReference Include="CheapLoc" />
<PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" /> <PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" />
<PackageReference Include="goatcorp.Reloaded.Hooks" /> <PackageReference Include="goatcorp.Reloaded.Hooks" />
<PackageReference Include="goatcorp.Reloaded.Assembler" />
<PackageReference Include="JetBrains.Annotations" /> <PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Lumina" /> <PackageReference Include="Lumina" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" /> <PackageReference Include="Microsoft.Extensions.ObjectPool" />

View file

@ -0,0 +1,22 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for OnFocusChanged events.
/// </summary>
public class AddonFocusChangedArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonFocusChangedArgs"/> class.
/// </summary>
internal AddonFocusChangedArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.FocusChanged;
/// <summary>
/// Gets or sets a value indicating whether the window is being focused or unfocused.
/// </summary>
public bool ShouldFocus { get; set; }
}

View file

@ -44,4 +44,9 @@ public enum AddonArgsType
/// Contains argument data for Close. /// Contains argument data for Close.
/// </summary> /// </summary>
Close, Close,
/// <summary>
/// Contains argument data for OnFocusChanged.
/// </summary>
FocusChanged,
} }

View file

@ -203,4 +203,14 @@ public enum AddonEvent
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows. /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
/// </remarks> /// </remarks>
PostFocus, PostFocus,
/// <summary>
/// An event that is fired before an addon processes its FocusChanged method.
/// </summary>
PreFocusChanged,
/// <summary>
/// An event that is fired after a addon processes its FocusChanged method.
/// </summary>
PostFocusChanged,
} }

View file

@ -31,7 +31,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
private readonly Framework framework = Service<Framework>.Get(); private readonly Framework framework = Service<Framework>.Get();
private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook; private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
private bool isInvokingListeners = false; private bool isInvokingListeners;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private AddonLifecycle() private AddonLifecycle()
@ -56,29 +56,36 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
AllocatedTables.Clear(); AllocatedTables.Clear();
} }
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal static AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null)
{
return null;
}
return matchedTable.OriginalVirtualTable;
}
/// <summary> /// <summary>
/// Register a listener for the target event and addon. /// Register a listener for the target event and addon.
/// </summary> /// </summary>
/// <param name="listener">The listener to register.</param> /// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener) internal void RegisterListener(AddonLifecycleEventListener listener)
{ {
this.framework.RunOnTick(() => if (this.isInvokingListeners)
{ {
if (!this.EventListeners.ContainsKey(listener.EventType)) this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
{ }
if (!this.EventListeners.TryAdd(listener.EventType, [])) else
return; {
} this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
}
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
return;
}
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
}, delayTicks: this.isInvokingListeners ? 1 : 0);
} }
/// <summary> /// <summary>
@ -87,16 +94,14 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param> /// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener) internal void UnregisterListener(AddonLifecycleEventListener listener)
{ {
this.framework.RunOnTick(() => if (this.isInvokingListeners)
{ {
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
{ }
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) else
{ {
addonListener.Remove(listener); this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener));
} }
}
}, delayTicks: this.isInvokingListeners ? 1 : 0);
} }
/// <summary> /// <summary>
@ -147,17 +152,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
this.isInvokingListeners = false; this.isInvokingListeners = false;
} }
/// <summary> private void RegisterListenerMethod(AddonLifecycleEventListener listener)
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress)
{ {
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); if (!this.EventListeners.ContainsKey(listener.EventType))
if (matchedTable == null) return null; {
if (!this.EventListeners.TryAdd(listener.EventType, []))
{
return;
}
}
return matchedTable.OriginalVirtualTable; // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
{
return;
}
}
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
}
private void UnregisterListenerMethod(AddonLifecycleEventListener listener)
{
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
{
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
{
addonListener.Remove(listener);
}
}
} }
private void OnAddonInitialize(AtkUnitBase* addon) private void OnAddonInitialize(AtkUnitBase* addon)
@ -277,5 +302,5 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)this.addonLifecycleService.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress); => (nint)AddonLifecycle.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress);
} }

View file

@ -42,6 +42,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AddonArgs onMouseOverArgs = new(); private readonly AddonArgs onMouseOverArgs = new();
private readonly AddonArgs onMouseOutArgs = new(); private readonly AddonArgs onMouseOutArgs = new();
private readonly AddonArgs focusArgs = new(); private readonly AddonArgs focusArgs = new();
private readonly AddonFocusChangedArgs focusChangedArgs = new();
private readonly AtkUnitBase* atkUnitBase; private readonly AtkUnitBase* atkUnitBase;
@ -63,6 +64,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction; private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction;
private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction; private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction;
private readonly AtkUnitBase.Delegates.Focus focusFunction; private readonly AtkUnitBase.Delegates.Focus focusFunction;
private readonly AtkUnitBase.Delegates.OnFocusChange onFocusChangeFunction;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonVirtualTable"/> class. /// Initializes a new instance of the <see cref="AddonVirtualTable"/> class.
@ -103,6 +105,7 @@ internal unsafe class AddonVirtualTable : IDisposable
this.onMouseOverFunction = this.OnAddonMouseOver; this.onMouseOverFunction = this.OnAddonMouseOver;
this.onMouseOutFunction = this.OnAddonMouseOut; this.onMouseOutFunction = this.OnAddonMouseOut;
this.focusFunction = this.OnAddonFocus; this.focusFunction = this.OnAddonFocus;
this.onFocusChangeFunction = this.OnAddonFocusChange;
// Overwrite specific virtual table entries // Overwrite specific virtual table entries
this.ModifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); this.ModifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
@ -121,6 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable
this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
this.ModifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction); this.ModifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
this.ModifiedVirtualTable->OnFocusChange = (delegate* unmanaged<AtkUnitBase*, bool, void>)Marshal.GetFunctionPointerForDelegate(this.onFocusChangeFunction);
} }
/// <summary> /// <summary>
@ -630,6 +634,36 @@ internal unsafe class AddonVirtualTable : IDisposable
} }
} }
private void OnAddonFocusChange(AtkUnitBase* thisPtr, bool isFocused)
{
try
{
this.LogEvent(EnableLogging);
this.focusChangedArgs.Addon = thisPtr;
this.focusChangedArgs.ShouldFocus = isFocused;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocusChanged, this.focusChangedArgs);
isFocused = this.focusChangedArgs.ShouldFocus;
try
{
this.OriginalVirtualTable->OnFocusChange(thisPtr, isFocused);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnFocusChanged. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocusChanged, this.focusChangedArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocusChange.");
}
}
[Conditional("DEBUG")] [Conditional("DEBUG")]
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
{ {

View file

@ -18,12 +18,12 @@ public enum AgentEvent
/// <summary> /// <summary>
/// An event that is fired before the agent processes its Filtered Receive Event Function. /// An event that is fired before the agent processes its Filtered Receive Event Function.
/// </summary> /// </summary>
PreReceiveFilteredEvent, PreReceiveEventWithResult,
/// <summary> /// <summary>
/// An event that is fired after the agent has processed its Filtered Receive Event Function. /// An event that is fired after the agent has processed its Filtered Receive Event Function.
/// </summary> /// </summary>
PostReceiveFilteredEvent, PostReceiveEventWithResult,
/// <summary> /// <summary>
/// An event that is fired before the agent processes its Show Function. /// An event that is fired before the agent processes its Show Function.

View file

@ -69,30 +69,36 @@ internal unsafe class AgentLifecycle : IInternalDisposableService
AllocatedTables.Clear(); AllocatedTables.Clear();
} }
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal static AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null)
{
return null;
}
return matchedTable.OriginalVirtualTable;
}
/// <summary> /// <summary>
/// Register a listener for the target event and agent. /// Register a listener for the target event and agent.
/// </summary> /// </summary>
/// <param name="listener">The listener to register.</param> /// <param name="listener">The listener to register.</param>
internal void RegisterListener(AgentLifecycleEventListener listener) internal void RegisterListener(AgentLifecycleEventListener listener)
{ {
this.framework.RunOnTick(() => if (this.isInvokingListeners)
{ {
if (!this.EventListeners.ContainsKey(listener.EventType)) this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
{ }
if (!this.EventListeners.TryAdd(listener.EventType, [])) else
return; {
} this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
}
// Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, []))
return;
}
this.EventListeners[listener.EventType][listener.AgentId].Add(listener);
},
delayTicks: this.isInvokingListeners ? 1 : 0);
} }
/// <summary> /// <summary>
@ -101,17 +107,14 @@ internal unsafe class AgentLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param> /// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AgentLifecycleEventListener listener) internal void UnregisterListener(AgentLifecycleEventListener listener)
{ {
this.framework.RunOnTick(() => if (this.isInvokingListeners)
{ {
if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners)) this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
{ }
if (agentListeners.TryGetValue(listener.AgentId, out var agentListener)) else
{ {
agentListener.Remove(listener); this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener));
} }
}
},
delayTicks: this.isInvokingListeners ? 1 : 0);
} }
/// <summary> /// <summary>
@ -162,19 +165,6 @@ internal unsafe class AgentLifecycle : IInternalDisposableService
this.isInvokingListeners = false; this.isInvokingListeners = false;
} }
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null) return null;
return matchedTable.OriginalVirtualTable;
}
private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule) private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule)
{ {
this.onInitializeAgentsHook!.Original(thisPtr, uiModule); this.onInitializeAgentsHook!.Original(thisPtr, uiModule);
@ -193,6 +183,39 @@ internal unsafe class AgentLifecycle : IInternalDisposableService
} }
} }
private void RegisterListenerMethod(AgentLifecycleEventListener listener)
{
if (!this.EventListeners.ContainsKey(listener.EventType))
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
{
return;
}
}
// Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, []))
{
return;
}
}
this.EventListeners[listener.EventType][listener.AgentId].Add(listener);
}
private void UnregisterListenerMethod(AgentLifecycleEventListener listener)
{
if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners))
{
if (agentListeners.TryGetValue(listener.AgentId, out var agentListener))
{
agentListener.Remove(listener);
}
}
}
private void ReplaceVirtualTables(AgentModule* agentModule) private void ReplaceVirtualTables(AgentModule* agentModule)
{ {
foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length)) foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length))
@ -311,5 +334,5 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)this.agentLifecycleService.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress); => (nint)AgentLifecycle.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress);
} }

View file

@ -21,7 +21,7 @@ internal unsafe class AgentVirtualTable : IDisposable
// Copying extra entries is not problematic, and is considered safe. // Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 60; private const int VirtualTableEntryCount = 60;
private const bool EnableLogging = true; private const bool EnableLogging = false;
private static readonly ModuleLog Log = new("AgentVT"); private static readonly ModuleLog Log = new("AgentVT");
@ -44,7 +44,7 @@ internal unsafe class AgentVirtualTable : IDisposable
// Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table, // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
// the CLR needs to know they are in use, or it will invalidate them causing random crashing. // the CLR needs to know they are in use, or it will invalidate them causing random crashing.
private readonly AgentInterface.Delegates.ReceiveEvent receiveEventFunction; private readonly AgentInterface.Delegates.ReceiveEvent receiveEventFunction;
private readonly AgentInterface.Delegates.ReceiveEvent2 filteredReceiveEventFunction; private readonly AgentInterface.Delegates.ReceiveEventWithResult receiveEventWithResultFunction;
private readonly AgentInterface.Delegates.Show showFunction; private readonly AgentInterface.Delegates.Show showFunction;
private readonly AgentInterface.Delegates.Hide hideFunction; private readonly AgentInterface.Delegates.Hide hideFunction;
private readonly AgentInterface.Delegates.Update updateFunction; private readonly AgentInterface.Delegates.Update updateFunction;
@ -60,8 +60,6 @@ internal unsafe class AgentVirtualTable : IDisposable
/// <param name="lifecycleService">Reference to AgentLifecycle service to callback and invoke listeners.</param> /// <param name="lifecycleService">Reference to AgentLifecycle service to callback and invoke listeners.</param>
internal AgentVirtualTable(AgentInterface* agent, AgentId agentId, AgentLifecycle lifecycleService) internal AgentVirtualTable(AgentInterface* agent, AgentId agentId, AgentLifecycle lifecycleService)
{ {
Log.Debug($"Initializing AgentVirtualTable for {agentId}, Address: {(nint)agent:X}");
this.agentInterface = agent; this.agentInterface = agent;
this.agentId = agentId; this.agentId = agentId;
this.lifecycleService = lifecycleService; this.lifecycleService = lifecycleService;
@ -80,7 +78,7 @@ internal unsafe class AgentVirtualTable : IDisposable
// Pin each of our listener functions // Pin each of our listener functions
this.receiveEventFunction = this.OnAgentReceiveEvent; this.receiveEventFunction = this.OnAgentReceiveEvent;
this.filteredReceiveEventFunction = this.OnAgentFilteredReceiveEvent; this.receiveEventWithResultFunction = this.OnAgentReceiveEventWithResult;
this.showFunction = this.OnAgentShow; this.showFunction = this.OnAgentShow;
this.hideFunction = this.OnAgentHide; this.hideFunction = this.OnAgentHide;
this.updateFunction = this.OnAgentUpdate; this.updateFunction = this.OnAgentUpdate;
@ -90,7 +88,7 @@ internal unsafe class AgentVirtualTable : IDisposable
// Overwrite specific virtual table entries // Overwrite specific virtual table entries
this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction); this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction);
this.ModifiedVirtualTable->ReceiveEvent2 = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.filteredReceiveEventFunction); this.ModifiedVirtualTable->ReceiveEventWithResult = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventWithResultFunction);
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction); this.ModifiedVirtualTable->Show = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction); this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AgentInterface*, uint, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction); this.ModifiedVirtualTable->Update = (delegate* unmanaged<AgentInterface*, uint, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
@ -158,7 +156,7 @@ internal unsafe class AgentVirtualTable : IDisposable
return result; return result;
} }
private AtkValue* OnAgentFilteredReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind) private AtkValue* OnAgentReceiveEventWithResult(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
{ {
AtkValue* result = null; AtkValue* result = null;
@ -173,7 +171,7 @@ internal unsafe class AgentVirtualTable : IDisposable
this.filteredReceiveEventArgs.ValueCount = valueCount; this.filteredReceiveEventArgs.ValueCount = valueCount;
this.filteredReceiveEventArgs.EventKind = eventKind; this.filteredReceiveEventArgs.EventKind = eventKind;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveFilteredEvent, this.filteredReceiveEventArgs); this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEventWithResult, this.filteredReceiveEventArgs);
returnValue = (AtkValue*)this.filteredReceiveEventArgs.ReturnValue; returnValue = (AtkValue*)this.filteredReceiveEventArgs.ReturnValue;
values = (AtkValue*)this.filteredReceiveEventArgs.AtkValues; values = (AtkValue*)this.filteredReceiveEventArgs.AtkValues;
@ -182,18 +180,18 @@ internal unsafe class AgentVirtualTable : IDisposable
try try
{ {
result = this.OriginalVirtualTable->ReceiveEvent2(thisPtr, returnValue, values, valueCount, eventKind); result = this.OriginalVirtualTable->ReceiveEventWithResult(thisPtr, returnValue, values, valueCount, eventKind);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e, "Caught exception when calling original Agent FilteredReceiveEvent. This may be a bug in the game or another plugin hooking this method."); Log.Error(e, "Caught exception when calling original Agent FilteredReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
} }
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveFilteredEvent, this.filteredReceiveEventArgs); this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEventWithResult, this.filteredReceiveEventArgs);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentFilteredReceiveEvent."); Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEventWithResult.");
} }
return result; return result;

View file

@ -121,9 +121,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework
/// <inheritdoc/> /// <inheritdoc/>
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default)
{ {
if (this.frameworkDestroy.IsCancellationRequested) if (this.frameworkDestroy.IsCancellationRequested) // Going away
return Task.FromCanceled(this.frameworkDestroy.Token); return Task.FromCanceled(this.frameworkDestroy.Token);
if (numTicks <= 0) if (numTicks <= 0 || this.frameworkThreadTaskScheduler.BoundThread == null) // Nonsense or before first tick
return Task.CompletedTask; return Task.CompletedTask;
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

View file

@ -397,7 +397,15 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
ushort w = 0, h = 0; ushort w = 0, h = 0;
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
node->SetWidth(w);
if (data.MinimumWidth > 0)
{
node->SetWidth(Math.Max(data.MinimumWidth, w));
}
else
{
node->SetWidth(w);
}
} }
var elementWidth = data.TextNode->Width + this.configuration.DtrSpacing; var elementWidth = data.TextNode->Width + this.configuration.DtrSpacing;

View file

@ -40,6 +40,11 @@ public interface IReadOnlyDtrBarEntry
/// </summary> /// </summary>
public bool Shown { get; } public bool Shown { get; }
/// <summary>
/// Gets a value indicating this entry's minimum width.
/// </summary>
public ushort MinimumWidth { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings. /// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings.
/// </summary> /// </summary>
@ -76,6 +81,11 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
/// </summary> /// </summary>
public new bool Shown { get; set; } public new bool Shown { get; set; }
/// <summary>
/// Gets or sets a value specifying the requested minimum width to make this entry.
/// </summary>
public new ushort MinimumWidth { get; set; }
/// <summary> /// <summary>
/// Gets or sets an action to be invoked when the user clicks on the dtr entry. /// Gets or sets an action to be invoked when the user clicks on the dtr entry.
/// </summary> /// </summary>
@ -128,6 +138,25 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
/// <inheritdoc cref="IDtrBarEntry.Tooltip" /> /// <inheritdoc cref="IDtrBarEntry.Tooltip" />
public SeString? Tooltip { get; set; } public SeString? Tooltip { get; set; }
/// <inheritdoc cref="MinimumWidth" />
public ushort MinimumWidth
{
get;
set
{
field = value;
if (this.TextNode is not null)
{
if (this.TextNode->GetWidth() < value)
{
this.TextNode->SetWidth(value);
}
}
this.Dirty = true;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public Action<DtrInteractionEvent>? OnClick { get; set; } public Action<DtrInteractionEvent>? OnClick { get; set; }

View file

@ -1,147 +0,0 @@
using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal;
using Dalamud.Hooking;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Network;
using Serilog;
namespace Dalamud.Game.Network;
/// <summary>
/// This class handles interacting with game network events.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class GameNetwork : IInternalDisposableService
{
private readonly GameNetworkAddressResolver address;
private readonly Hook<PacketDispatcher.Delegates.OnReceivePacket> processZonePacketDownHook;
private readonly Hook<ProcessZonePacketUpDelegate> processZonePacketUpHook;
private readonly HitchDetector hitchDetectorUp;
private readonly HitchDetector hitchDetectorDown;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceConstructor]
private unsafe GameNetwork(TargetSigScanner sigScanner)
{
this.hitchDetectorUp = new HitchDetector("GameNetworkUp", this.configuration.GameNetworkUpHitch);
this.hitchDetectorDown = new HitchDetector("GameNetworkDown", this.configuration.GameNetworkDownHitch);
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($"OnReceivePacket address {Util.DescribeAddress(onReceivePacketAddress)}");
Log.Verbose($"ProcessZonePacketUp address {Util.DescribeAddress(this.address.ProcessZonePacketUp)}");
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();
}
/// <summary>
/// The delegate type of a network message event.
/// </summary>
/// <param name="dataPtr">The pointer to the raw data.</param>
/// <param name="opCode">The operation ID code.</param>
/// <param name="sourceActorId">The source actor ID.</param>
/// <param name="targetActorId">The taret actor ID.</param>
/// <param name="direction">The direction of the packed.</param>
public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate byte ProcessZonePacketUpDelegate(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4);
/// <summary>
/// Event that is called when a network message is sent/received.
/// </summary>
public event OnNetworkMessageDelegate? NetworkMessage;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.processZonePacketDownHook.Dispose();
this.processZonePacketUpHook.Dispose();
}
private void ProcessZonePacketDownDetour(PacketDispatcher* dispatcher, uint targetId, IntPtr dataPtr)
{
this.hitchDetectorDown.Start();
// Go back 0x10 to get back to the start of the packet header
dataPtr -= 0x10;
foreach (var d in Delegate.EnumerateInvocationList(this.NetworkMessage))
{
try
{
d.Invoke(
dataPtr + 0x20,
(ushort)Marshal.ReadInt16(dataPtr, 0x12),
0,
targetId,
NetworkMessageDirection.ZoneDown);
}
catch (Exception ex)
{
string header;
try
{
var data = new byte[32];
Marshal.Copy(dataPtr, data, 0, 32);
header = BitConverter.ToString(data);
}
catch (Exception)
{
header = "failed";
}
Log.Error(ex, "Exception on ProcessZonePacketDown hook. Header: " + header);
}
}
this.processZonePacketDownHook.Original(dispatcher, targetId, dataPtr + 0x10);
this.hitchDetectorDown.Stop();
}
private byte ProcessZonePacketUpDetour(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4)
{
this.hitchDetectorUp.Start();
try
{
// Call events
// TODO: Implement actor IDs
this.NetworkMessage?.Invoke(dataPtr + 0x20, (ushort)Marshal.ReadInt16(dataPtr), 0x0, 0x0, NetworkMessageDirection.ZoneUp);
}
catch (Exception ex)
{
string header;
try
{
var data = new byte[32];
Marshal.Copy(dataPtr, data, 0, 32);
header = BitConverter.ToString(data);
}
catch (Exception)
{
header = "failed";
}
Log.Error(ex, "Exception on ProcessZonePacketUp hook. Header: " + header);
}
this.hitchDetectorUp.Stop();
return this.processZonePacketUpHook.Original(a1, dataPtr, a3, a4);
}
}

View file

@ -1,20 +0,0 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Network;
/// <summary>
/// The address resolver for the <see cref="GameNetwork"/> class.
/// </summary>
internal sealed class GameNetworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the ProcessZonePacketUp method.
/// </summary>
public IntPtr ProcessZonePacketUp { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
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

@ -55,10 +55,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private bool disposing; private bool disposing;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private NetworkHandlers( private NetworkHandlers(TargetSigScanner sigScanner, HappyHttpClient happyHttpClient)
GameNetwork gameNetwork,
TargetSigScanner sigScanner,
HappyHttpClient happyHttpClient)
{ {
this.uploader = new UniversalisMarketBoardUploader(happyHttpClient); this.uploader = new UniversalisMarketBoardUploader(happyHttpClient);

View file

@ -3,6 +3,7 @@ namespace Dalamud.Game.Network;
/// <summary> /// <summary>
/// This represents the direction of a network message. /// This represents the direction of a network message.
/// </summary> /// </summary>
[Obsolete("No longer part of public API", true)]
public enum NetworkMessageDirection public enum NetworkMessageDirection
{ {
/// <summary> /// <summary>

View file

@ -1,8 +1,13 @@
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using InteropGenerator.Runtime;
namespace Dalamud.Hooking.Internal.Verification; namespace Dalamud.Hooking.Internal.Verification;
/// <summary> /// <summary>
@ -19,11 +24,13 @@ internal static class HookVerifier
new( new(
"ActorControlSelf", "ActorControlSelf",
"E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64",
typeof(ActorControlSelfDelegate), typeof(ActorControlSelfDelegate), // TODO: change this to CS delegate
"Signature changed in Patch 7.4") // 7.4 (new parameters) "Signature changed in Patch 7.4") // 7.4 (new parameters)
]; ];
private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); private static readonly string ClientStructsInteropNamespacePrefix = string.Join(".", nameof(FFXIVClientStructs), nameof(FFXIVClientStructs.Interop));
private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); // TODO: change this to CS delegate
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HookVerifier"/> class. /// Initializes a new instance of the <see cref="HookVerifier"/> class.
@ -71,7 +78,7 @@ internal static class HookVerifier
var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!;
// Compare Return Type // Compare Return Type
var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType; var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType);
// Compare Parameter Count // Compare Parameter Count
var passedParams = passedInvoke.GetParameters(); var passedParams = passedInvoke.GetParameters();
@ -86,7 +93,7 @@ internal static class HookVerifier
// Compare Parameter Types // Compare Parameter Types
for (var i = 0; i < passedParams.Length; i++) for (var i = 0; i < passedParams.Length; i++)
{ {
if (passedParams[i].ParameterType != enforcedParams[i].ParameterType) if (!CheckParam(passedParams[i].ParameterType, enforcedParams[i].ParameterType))
{ {
mismatch = true; mismatch = true;
break; break;
@ -100,6 +107,45 @@ internal static class HookVerifier
} }
} }
private static bool CheckParam(Type paramLeft, Type paramRight)
{
var sameType = paramLeft == paramRight;
return sameType || SizeOf(paramLeft) == SizeOf(paramRight);
}
private static int SizeOf(Type type)
{
return type switch {
_ when type == typeof(sbyte) || type == typeof(byte) || type == typeof(bool) => 1,
_ when type == typeof(char) || type == typeof(short) || type == typeof(ushort) || type == typeof(Half) => 2,
_ when type == typeof(int) || type == typeof(uint) || type == typeof(float) => 4,
_ when type == typeof(long) || type == typeof(ulong) || type == typeof(double) || type.IsPointer || type.IsFunctionPointer || type.IsUnmanagedFunctionPointer || (type.Name == "Pointer`1" && type.Namespace.AsSpan().SequenceEqual(ClientStructsInteropNamespacePrefix)) || type == typeof(CStringPointer) => 8,
_ when type.Name.StartsWith("FixedSizeArray") => SizeOf(type.GetGenericArguments()[0]) * int.Parse(type.Name[14..type.Name.IndexOf('`')]),
_ when type.GetCustomAttribute<InlineArrayAttribute>() is { Length: var length } => SizeOf(type.GetGenericArguments()[0]) * length,
_ when IsStruct(type) && !type.IsGenericType && (type.StructLayoutAttribute?.Value ?? LayoutKind.Sequential) != LayoutKind.Sequential => type.StructLayoutAttribute?.Size ?? (int?)typeof(Unsafe).GetMethod("SizeOf")?.MakeGenericMethod(type).Invoke(null, null) ?? 0,
_ when type.IsEnum => SizeOf(Enum.GetUnderlyingType(type)),
_ when type.IsGenericType => Marshal.SizeOf(Activator.CreateInstance(type)!),
_ => GetSizeOf(type),
};
}
private static int GetSizeOf(Type type)
{
try
{
return Marshal.SizeOf(Activator.CreateInstance(type)!);
}
catch
{
return 0;
}
}
private static bool IsStruct(Type type)
{
return type != typeof(decimal) && type is { IsValueType: true, IsPrimitive: false, IsEnum: false };
}
private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message) private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message)
{ {
public nint Address { get; set; } public nint Address { get; set; }

View file

@ -1,44 +1,51 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Threading;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Game.Network; using Dalamud.Game;
using Dalamud.Interface.Utility; using Dalamud.Hooking;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Memory;
using ImGuiTable = Dalamud.Interface.Utility.ImGuiTable; using FFXIVClientStructs.FFXIV.Application.Network;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Network;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets; namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary> /// <summary>
/// Widget to display the current packets. /// Widget to display the current packets.
/// </summary> /// </summary>
internal class NetworkMonitorWidget : IDataWindowWidget internal unsafe class NetworkMonitorWidget : IDataWindowWidget
{ {
private readonly ConcurrentQueue<NetworkPacketData> packets = new(); private readonly ConcurrentQueue<NetworkPacketData> packets = new();
private Hook<PacketDispatcher.Delegates.OnReceivePacket>? hookDown;
private Hook<ZoneClientSendPacketDelegate>? hookUp;
private bool trackNetwork; private bool trackNetwork;
private int trackedPackets; private int trackedPackets = 20;
private Regex? trackedOpCodes; private ulong nextPacketIndex;
private string filterString = string.Empty; private string filterString = string.Empty;
private Regex? untrackedOpCodes; private bool filterRecording = true;
private string negativeFilterString = string.Empty; private bool autoScroll = true;
private bool autoScrollPending;
/// <summary> Finalizes an instance of the <see cref="NetworkMonitorWidget"/> class. </summary> /// <summary> Finalizes an instance of the <see cref="NetworkMonitorWidget"/> class. </summary>
~NetworkMonitorWidget() ~NetworkMonitorWidget()
{ {
if (this.trackNetwork) this.hookDown?.Dispose();
{ this.hookUp?.Dispose();
this.trackNetwork = false; }
var network = Service<GameNetwork>.GetNullable();
if (network != null) private delegate byte ZoneClientSendPacketDelegate(ZoneClient* thisPtr, nint packet, uint a3, uint a4, byte a5);
{
network.NetworkMessage -= this.OnNetworkMessage; private enum NetworkMessageDirection
} {
} ZoneDown,
ZoneUp,
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -53,31 +60,36 @@ internal class NetworkMonitorWidget : IDataWindowWidget
/// <inheritdoc/> /// <inheritdoc/>
public void Load() public void Load()
{ {
this.trackNetwork = false; this.hookDown = Hook<PacketDispatcher.Delegates.OnReceivePacket>.FromAddress(
this.trackedPackets = 20; (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket,
this.trackedOpCodes = null; this.OnReceivePacketDetour);
this.filterString = string.Empty;
this.packets.Clear(); // TODO: switch to ZoneClient.SendPacket from CS
if (Service<TargetSigScanner>.Get().TryScanText("E8 ?? ?? ?? ?? 4C 8B 44 24 ?? E9", out var address))
this.hookUp = Hook<ZoneClientSendPacketDelegate>.FromAddress(address, this.SendPacketDetour);
this.Ready = true; this.Ready = true;
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Draw() public void Draw()
{ {
var network = Service<GameNetwork>.Get();
if (ImGui.Checkbox("Track Network Packets"u8, ref this.trackNetwork)) if (ImGui.Checkbox("Track Network Packets"u8, ref this.trackNetwork))
{ {
if (this.trackNetwork) if (this.trackNetwork)
{ {
network.NetworkMessage += this.OnNetworkMessage; this.nextPacketIndex = 0;
this.hookDown?.Enable();
this.hookUp?.Enable();
} }
else else
{ {
network.NetworkMessage -= this.OnNetworkMessage; this.hookDown?.Disable();
this.hookUp?.Disable();
} }
} }
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2); ImGui.SetNextItemWidth(-1);
if (ImGui.DragInt("Stored Number of Packets"u8, ref this.trackedPackets, 0.1f, 1, 512)) if (ImGui.DragInt("Stored Number of Packets"u8, ref this.trackedPackets, 0.1f, 1, 512))
{ {
this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512); this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512);
@ -88,131 +100,200 @@ internal class NetworkMonitorWidget : IDataWindowWidget
this.packets.Clear(); this.packets.Clear();
} }
this.DrawFilterInput(); ImGui.SameLine();
this.DrawNegativeFilterInput(); ImGui.Checkbox("Auto-Scroll"u8, ref this.autoScroll);
ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "OpCode", "Hex", "Target", "Source", "Data"); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight()) * 2);
} ImGui.InputTextWithHint("##Filter"u8, "Filter OpCodes..."u8, ref this.filterString, 1024, ImGuiInputTextFlags.AutoSelectAll);
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.Checkbox("##FilterRecording"u8, ref this.filterRecording);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Apply filter to incoming packets.\nUncheck to record all packets and filter the table instead."u8);
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGuiComponents.HelpMarker("Enter OpCodes in a comma-separated list.\nRanges are supported. Exclude OpCodes with exclamation mark.\nExample: -400,!50-100,650,700-980,!941");
private void DrawNetworkPacket(NetworkPacketData data) using var table = ImRaii.Table("NetworkMonitorTableV2"u8, 6, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.NoSavedSettings);
{ if (!table) return;
ImGui.TableNextColumn();
ImGui.Text(data.Direction.ToString());
ImGui.TableNextColumn(); ImGui.TableSetupColumn("Index"u8, ImGuiTableColumnFlags.WidthFixed, 50);
ImGui.Text(data.OpCode.ToString()); ImGui.TableSetupColumn("Time"u8, ImGuiTableColumnFlags.WidthFixed, 100);
ImGui.TableSetupColumn("Direction"u8, ImGuiTableColumnFlags.WidthFixed, 100);
ImGui.TableSetupColumn("OpCode"u8, ImGuiTableColumnFlags.WidthFixed, 100);
ImGui.TableSetupColumn("OpCode (Hex)"u8, ImGuiTableColumnFlags.WidthFixed, 100);
ImGui.TableSetupColumn("Target EntityId"u8, ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
ImGui.TableNextColumn(); var autoScrollDisabled = false;
ImGui.Text($"0x{data.OpCode:X4}");
ImGui.TableNextColumn(); foreach (var packet in this.packets)
ImGui.Text(data.TargetActorId > 0 ? $"0x{data.TargetActorId:X}" : string.Empty);
ImGui.TableNextColumn();
ImGui.Text(data.SourceActorId > 0 ? $"0x{data.SourceActorId:X}" : string.Empty);
ImGui.TableNextColumn();
if (data.Data.Count > 0)
{ {
ImGui.Text(string.Join(" ", data.Data.Select(b => b.ToString("X2")))); if (!this.filterRecording && !this.IsFiltered(packet.OpCode))
continue;
ImGui.TableNextColumn();
ImGui.Text(packet.Index.ToString());
ImGui.TableNextColumn();
ImGui.Text(packet.Time.ToLongTimeString());
ImGui.TableNextColumn();
ImGui.Text(packet.Direction.ToString());
ImGui.TableNextColumn();
using (ImRaii.PushId(packet.Index.ToString()))
{
if (ImGui.SmallButton("X"))
{
if (!string.IsNullOrEmpty(this.filterString))
this.filterString += ",";
this.filterString += $"!{packet.OpCode}";
}
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Filter OpCode"u8);
autoScrollDisabled |= ImGui.IsItemHovered();
ImGui.SameLine();
WidgetUtil.DrawCopyableText(packet.OpCode.ToString());
autoScrollDisabled |= ImGui.IsItemHovered();
ImGui.TableNextColumn();
WidgetUtil.DrawCopyableText($"0x{packet.OpCode:X3}");
autoScrollDisabled |= ImGui.IsItemHovered();
ImGui.TableNextColumn();
if (packet.TargetEntityId > 0)
{
WidgetUtil.DrawCopyableText($"{packet.TargetEntityId:X}");
var name = !string.IsNullOrEmpty(packet.TargetName)
? packet.TargetName
: GetTargetName(packet.TargetEntityId);
if (!string.IsNullOrEmpty(name))
{
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.Text($"({name})");
}
}
} }
else
if (this.autoScroll && this.autoScrollPending && !autoScrollDisabled)
{ {
ImGui.Dummy(ImGui.GetContentRegionAvail() with { Y = 0 }); ImGui.SetScrollHereY();
this.autoScrollPending = false;
} }
} }
private void DrawFilterInput() private static string GetTargetName(uint targetId)
{ {
var invalidRegEx = this.filterString.Length > 0 && this.trackedOpCodes == null; if (targetId == PlayerState.Instance()->EntityId)
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx); return "Local Player";
using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); var cachedName = NameCache.Instance()->GetNameByEntityId(targetId);
if (!ImGui.InputTextWithHint("##Filter"u8, "Regex Filter OpCodes..."u8, ref this.filterString, 1024)) if (cachedName.HasValue)
{ return cachedName.ToString();
var obj = GameObjectManager.Instance()->Objects.GetObjectByEntityId(targetId);
if (obj != null)
return obj->NameString;
return string.Empty;
}
private void OnReceivePacketDetour(PacketDispatcher* thisPtr, uint targetId, nint packet)
{
var opCode = *(ushort*)(packet + 2);
var targetName = GetTargetName(targetId);
this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId, targetName));
this.hookDown.OriginalDisposeSafe(thisPtr, targetId, packet);
}
private byte SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, byte a5)
{
var opCode = *(ushort*)packet;
this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty));
return this.hookUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5);
}
private void RecordPacket(NetworkPacketData packet)
{
if (this.filterRecording && !this.IsFiltered(packet.OpCode))
return; return;
this.packets.Enqueue(packet);
while (this.packets.Count > this.trackedPackets)
{
this.packets.TryDequeue(out _);
} }
if (this.filterString.Length == 0) this.autoScrollPending = true;
{
this.trackedOpCodes = null;
}
else
{
try
{
this.trackedOpCodes = new Regex(this.filterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
}
catch
{
this.trackedOpCodes = null;
}
}
} }
private void DrawNegativeFilterInput() private bool IsFiltered(ushort opcode)
{ {
var invalidRegEx = this.negativeFilterString.Length > 0 && this.untrackedOpCodes == null; var filterString = this.filterString.Replace(" ", string.Empty);
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, invalidRegEx);
using var color = ImRaii.PushColor(ImGuiCol.Border, 0xFF0000FF, invalidRegEx);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
if (!ImGui.InputTextWithHint("##NegativeFilter"u8, "Regex Filter Against OpCodes..."u8, ref this.negativeFilterString, 1024))
{
return;
}
if (this.negativeFilterString.Length == 0) if (filterString.Length == 0)
return true;
try
{ {
this.untrackedOpCodes = null; var offset = 0;
var included = false;
var hasInclude = false;
while (filterString.Length - offset > 0)
{
var remaining = filterString[offset..];
// find the end of the current entry
var entryEnd = remaining.IndexOf(',');
if (entryEnd == -1)
entryEnd = remaining.Length;
var entry = filterString[offset..(offset + entryEnd)];
var dash = entry.IndexOf('-');
var isExcluded = entry.StartsWith('!');
var startOffset = isExcluded ? 1 : 0;
var entryMatch = dash == -1
? ushort.Parse(entry[startOffset..]) == opcode
: ((dash - startOffset == 0 || opcode >= ushort.Parse(entry[startOffset..dash]))
&& (entry[(dash + 1)..].Length == 0 || opcode <= ushort.Parse(entry[(dash + 1)..])));
if (isExcluded)
{
if (entryMatch)
return false;
}
else
{
hasInclude = true;
included |= entryMatch;
}
if (entryEnd == filterString.Length)
break;
offset += entryEnd + 1;
}
return !hasInclude || included;
} }
else catch (Exception ex)
{ {
try Serilog.Log.Error(ex, "Invalid filter string");
{ return false;
this.untrackedOpCodes = new Regex(this.negativeFilterString, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
}
catch
{
this.untrackedOpCodes = null;
}
} }
} }
private void OnNetworkMessage(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction)
{
if ((this.trackedOpCodes == null || this.trackedOpCodes.IsMatch(this.OpCodeToString(opCode)))
&& (this.untrackedOpCodes == null || !this.untrackedOpCodes.IsMatch(this.OpCodeToString(opCode))))
{
this.packets.Enqueue(new NetworkPacketData(this, opCode, direction, sourceActorId, targetActorId, dataPtr));
while (this.packets.Count > this.trackedPackets)
{
this.packets.TryDequeue(out _);
}
}
}
private int GetSizeFromOpCode(ushort opCode)
=> 0;
/// <remarks> Add known packet-name -> packet struct size associations here to copy the byte data for such packets. </remarks>>
private int GetSizeFromName(string name)
=> name switch
{
_ => 0,
};
/// <remarks> The filter should find opCodes by number (decimal and hex) and name, if existing. </remarks>
private string OpCodeToString(ushort opCode)
=> $"{opCode}\0{opCode:X}";
#pragma warning disable SA1313 #pragma warning disable SA1313
private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId) private readonly record struct NetworkPacketData(ulong Index, DateTime Time, ushort OpCode, NetworkMessageDirection Direction, uint TargetEntityId, string TargetName);
#pragma warning restore SA1313 #pragma warning restore SA1313
{
public readonly IReadOnlyList<byte> Data = [];
public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr)
: this(opCode, direction, sourceActorId, targetActorId)
=> this.Data = MemoryHelper.Read<byte>(dataPtr, widget.GetSizeFromOpCode(opCode), false);
}
} }

View file

@ -1,27 +0,0 @@
using Dalamud.Game.Network;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This class handles interacting with game network events.
/// </summary>
[Obsolete("Will be removed in a future release. Use packet handler hooks instead.", true)]
public interface IGameNetwork : IDalamudService
{
// TODO(v9): we shouldn't be passing pointers to the actual data here
/// <summary>
/// The delegate type of a network message event.
/// </summary>
/// <param name="dataPtr">The pointer to the raw data.</param>
/// <param name="opCode">The operation ID code.</param>
/// <param name="sourceActorId">The source actor ID.</param>
/// <param name="targetActorId">The taret actor ID.</param>
/// <param name="direction">The direction of the packed.</param>
public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction);
/// <summary>
/// Event that is called when a network message is sent/received.
/// </summary>
public event OnNetworkMessageDelegate NetworkMessage;
}

View file

@ -41,8 +41,7 @@
<PackageVersion Include="HexaGen.Runtime" Version="1.1.20" /> <PackageVersion Include="HexaGen.Runtime" Version="1.1.20" />
<!-- Reloaded --> <!-- Reloaded -->
<PackageVersion Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp7" /> <PackageVersion Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp8" />
<PackageVersion Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp5" />
<PackageVersion Include="Reloaded.Memory" Version="7.0.0" /> <PackageVersion Include="Reloaded.Memory" Version="7.0.0" />
<PackageVersion Include="Reloaded.Memory.Buffers" Version="2.0.0" /> <PackageVersion Include="Reloaded.Memory.Buffers" Version="2.0.0" />

@ -1 +1 @@
Subproject commit 1270470855d6ac2d2f726b07019e21644c5658ec Subproject commit a02536a4bf6862036403c03945a02fcd6689e445