mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-02-25 22:21:49 +01:00
Merge branch 'goatcorp:master' into Plugin-Installer-Plus-RepoFilter
This commit is contained in:
commit
db94b2cc87
21 changed files with 513 additions and 430 deletions
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = "")
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue