Files
CBT/CBT/FlyText/FlyTextReceiver.cs
T
KnackAtNite 5b3d8c87d1
Debug Build and Test / Build against Latest Dalamud (push) Has been cancelled
Debug Build and Test / Build against Staging Dalamud (push) Has been cancelled
Release Build and Publish / Release Build against Staging Dalamud and deploy to MyDalamudPlugins (release) Has been cancelled
CBT API 14 compatibility: Dalamud SDK 14, net10, fix ISigScanner and LocalPlayer
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 13:27:41 -05:00

258 lines
9.1 KiB
C#

namespace CBT.FlyText;
using System;
using System.Linq;
using System.Numerics;
using CBT.Attributes;
using CBT.FlyText.Configuration;
using CBT.Types;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;
using DalamudFlyText = Dalamud.Game.Gui.FlyText;
/// <summary>
/// FlyTextReceiver receives FlyText from the game client.
/// </summary>
public unsafe partial class FlyTextReceiver : IDisposable
{
private readonly Hook<AddScreenLogWithKindDelegate> addScreenLogWithKindHook;
private readonly Hook<AddScreenLogDelegate> addScreenLogHook;
private readonly Hook<ReceiveActionEffectDelegate> receiveActionEffectHook;
/// <summary>
/// Initializes a new instance of the <see cref="FlyTextReceiver"/> class.
/// </summary>
/// <param name="gameInteropProvider">Dalamud game interop provider.</param>
public FlyTextReceiver(IGameInteropProvider gameInteropProvider)
{
Service.FlyTextGui.FlyTextCreated += this.FlyTextCreated;
this.addScreenLogWithKindHook = gameInteropProvider.HookFromAddress<AddScreenLogWithKindDelegate>(Service.Address.AddScreenLogWithKind, this.AddScreenLogWithKindDetour);
this.addScreenLogWithKindHook.Enable();
this.addScreenLogHook = gameInteropProvider.HookFromAddress<AddScreenLogDelegate>(Service.Address.AddScreenLog, this.AddScreenLogDetour);
// this.addScreenLogHook.Enable();
this.receiveActionEffectHook = gameInteropProvider.HookFromAddress<ReceiveActionEffectDelegate>(Service.Address.ReceiveActionEffect, this.ReceiveActionEffectDetour);
// this.receiveActionEffectHook.Enable();
}
private delegate void AddScreenLogWithKindDelegate(
Character* target,
Character* source,
FlyTextKind kind,
int option,
int actionKind,
int actionID,
int val1,
int val2,
int val3,
int val4);
private delegate void AddScreenLogDelegate(long screenLogManager, FlyTextCreation* flyTextCreation);
private delegate void ReceiveActionEffectDelegate(
uint casterEntityId,
Character* casterPtr,
Vector3* targetPos,
Header* header,
TargetEffects* effects,
GameObjectId* targetEntityIds);
/// <inheritdoc/>
public void Dispose()
{
this.addScreenLogWithKindHook.Disable();
this.addScreenLogWithKindHook.Dispose();
// this.addScreenLogHook.Disable();
this.addScreenLogHook.Dispose();
// this.receiveActionEffectHook.Disable();
this.receiveActionEffectHook.Dispose();
Service.FlyTextGui.FlyTextCreated -= this.FlyTextCreated;
GC.SuppressFinalize(this);
}
private void AddScreenLogWithKindDetour(
Character* target,
Character* source,
FlyTextKind kind,
int option,
int actionKind,
int actionID,
int val1,
int val2,
int val3,
int val4)
{
try
{
var kindConfig = PluginManager.GetConfigForKind(kind);
var effects = GetEffects(target->GetActionEffectHandler());
var sourceObjectID = GetGameObjectId(target->GetActionEffectHandler());
if (ShouldManageEvent(kind, source, target, sourceObjectID.ObjectId, kindConfig))
{
var flyTextEvent = Service.Pool.Get();
flyTextEvent.Hydrate(kind, effects, sourceObjectID.ObjectId, target, source, option, actionKind, actionID, val1, val2, val3, val4);
Service.Manager.Add(flyTextEvent);
}
}
catch (Exception ex)
{
Service.PluginLog.Error($"CBT Wizard used Testicular Torsion: {ex.Message}");
}
this.addScreenLogWithKindHook.Original(target, source, kind, option, actionKind, actionID, val1, val2, val3, val4);
}
private void AddScreenLogDetour(long screenLogManager, FlyTextCreation* flyTextCreation)
{
this.addScreenLogHook.Original(screenLogManager, flyTextCreation);
}
private void ReceiveActionEffectDetour(
uint casterEntityId,
Character* casterPtr,
Vector3* targetPos,
Header* header,
TargetEffects* effects,
GameObjectId* targetEntityIds)
{
this.receiveActionEffectHook.Original(casterEntityId, casterPtr, targetPos, header, effects, targetEntityIds);
}
private void FlyTextCreated(
ref DalamudFlyText.FlyTextKind kind,
ref int val1,
ref int val2,
ref SeString text1,
ref SeString text2,
ref uint color,
ref uint icon,
ref uint damageTypeIcon,
ref float yOffset,
ref bool handled)
{
if (Service.Configuration.Options.TryGetValue(GlobalOption.NativeUnhandled.ToString(), out var allowNativeEvents))
{
var kindConfig = PluginManager.GetConfigForKind((FlyTextKind)kind);
if (kindConfig == null)
{
return;
}
if (allowNativeEvents)
{
handled = kindConfig.Enabled;
}
else
{
handled = true;
}
if (!handled)
{
Service.PluginLog.Info($"FlyTextKind: {kind} was not handled by CBT.");
}
}
}
}
/// <summary>
/// Bad guy stuff.
/// </summary>
public unsafe partial class FlyTextReceiver
{
/// <summary>
/// There's a pointer to the action effect handler on the target that allows me to get at the action effects.
/// I can get the damage type from this, otherwise I need to share state between two delegates, and pointer arithmetic
/// seemed like the lesser of two evils.
/// </summary>
/// <param name="handler">Action Effect Handler associated with the target.</param>
/// <param name="effectEntryIndex">EffectEntry index. Maybe this changes someday.</param>
/// <returns>A copy of of all effects that an action had.</returns>
private static Effect[] GetEffects(ActionEffectHandler* handler, int effectEntryIndex = 0)
{
var effectEntryPtr = (byte*)handler + (effectEntryIndex * 0x78);
var targetEffectsPtr = (TargetEffects*)(effectEntryPtr + 0x38);
var effectsSpan = targetEffectsPtr->Effects;
return effectsSpan.ToArray();
}
private static GameObjectId GetGameObjectId(ActionEffectHandler* handler, int effectEntryIndex = 0)
{
var effectEntryPtr = (byte*)handler + (effectEntryIndex * 0x78);
var gameObjectID = (GameObjectId*)(effectEntryPtr + 0x18);
return *gameObjectID;
}
/// <summary>
/// Determines if an event should be managed by the plugin or ignored.
/// </summary>
/// <param name="kind">Kind of the event. Unused.</param>
/// <param name="source">Source of the event.</param>
/// <param name="target">Target of the event.</param>
/// <param name="sourceObjectID">Caster of the event.</param>
/// <param name="kindConfig">Configuration for the current kind.</param>
/// <returns>A bool indicating whether the plugin should manage the flytext event.</returns>
private static bool ShouldManageEvent(FlyTextKind kind, Character* source, Character* target, GameObjectId sourceObjectID, FlyTextConfiguration? kindConfig)
{
if (kindConfig == null)
{
return false;
}
if (!kindConfig.Enabled)
{
return false;
}
if (target == null || source == null)
{
return false;
}
if (PluginManager.GetDistance(target) > 60)
{
return false;
}
var isOurAbility = PluginManager.IsPlayerCharacter(source);
var isAgainstUs = PluginManager.IsPlayerCharacter(target);
var isPartyAbility = PluginManager.IsPartyMember(source);
var isAgainstParty = PluginManager.IsPartyMember(target);
var isEnemyAbility = PluginManager.IsEnemy(source);
var isAgainstEnemy = PluginManager.IsEnemy(target);
var specialCaseForHpRegen =
kind == FlyTextKind.Healing
&& PluginManager.IsPartyMember(source)
&& PluginManager.LocalPlayer?.GameObjectId == sourceObjectID.ObjectId;
var conditionTable = new (bool Condition, FlyTextFilter Filter)[]
{
(isOurAbility && isAgainstEnemy && kindConfig.Filter.Enemy, FlyTextFilter.Enemy),
(isOurAbility && isAgainstUs && kindConfig.Filter.Self, FlyTextFilter.Self),
(isOurAbility && isAgainstParty && kindConfig.Filter.Party, FlyTextFilter.Party),
(isPartyAbility && isAgainstUs && kindConfig.Filter.Self, FlyTextFilter.Self),
(isEnemyAbility && isAgainstUs && kindConfig.Filter.Self, FlyTextFilter.Self),
(specialCaseForHpRegen && kindConfig.Filter.Party, FlyTextFilter.Party),
};
return conditionTable.Any(c => c.Condition && kind.ShouldAllow(c.Filter));
}
}