f37369cdda
Co-authored-by: Cursor <cursoragent@cursor.com>
413 lines
14 KiB
C#
413 lines
14 KiB
C#
using Dalamud.Game.ClientState.Conditions;
|
|
using Dalamud.Game.ClientState.Objects.Types;
|
|
using Dalamud.Hooking;
|
|
using HSUI.Config;
|
|
using HSUI.Helpers;
|
|
using HSUI.Interface.Party;
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
using Dalamud.Bindings.ImGui;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;
|
|
|
|
namespace HSUI.Interface.PartyCooldowns
|
|
{
|
|
public unsafe class PartyCooldownsManager
|
|
{
|
|
#region Singleton
|
|
public static PartyCooldownsManager Instance { get; private set; } = null!;
|
|
private PartyCooldownsConfig _config = null!;
|
|
private PartyCooldownsDataConfig _dataConfig = null!;
|
|
|
|
private PartyCooldownsManager()
|
|
{
|
|
try
|
|
{
|
|
_onActionUsedHook = Plugin.GameInteropProvider.HookFromAddress<ActionEffectHandler.Delegates.Receive>(
|
|
ActionEffectHandler.MemberFunctionPointers.Receive,
|
|
OnActionUsed
|
|
);
|
|
_onActionUsedHook?.Enable();
|
|
}
|
|
catch
|
|
{
|
|
Plugin.Logger.Error("PartyCooldowns OnActionUsed Hook failed!!!");
|
|
}
|
|
|
|
try
|
|
{
|
|
_actorControlHook = Plugin.GameInteropProvider.HookFromSignature<ActorControlDelegate>(
|
|
"E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64",
|
|
OnActorControl
|
|
);
|
|
_actorControlHook?.Enable();
|
|
}
|
|
catch
|
|
{
|
|
Plugin.Logger.Error("PartyCooldowns OnActorControl Hook failed!!!");
|
|
}
|
|
|
|
PartyManager.Instance.MembersChangedEvent += OnMembersChanged;
|
|
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
|
|
Plugin.JobChangedEvent += OnJobChanged;
|
|
Plugin.ClientState.TerritoryChanged += OnTerritoryChanged;
|
|
|
|
ConfigReset(ConfigurationManager.Instance, false);
|
|
UpdatePreview();
|
|
}
|
|
|
|
public static void Initialize()
|
|
{
|
|
Instance = new PartyCooldownsManager();
|
|
}
|
|
|
|
~PartyCooldownsManager()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected void Dispose(bool disposing)
|
|
{
|
|
if (!disposing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_onActionUsedHook?.Disable();
|
|
_onActionUsedHook?.Dispose();
|
|
|
|
_actorControlHook?.Disable();
|
|
_actorControlHook?.Dispose();
|
|
|
|
PartyManager.Instance.MembersChangedEvent -= OnMembersChanged;
|
|
Plugin.JobChangedEvent -= OnJobChanged;
|
|
_config.ValueChangeEvent -= OnConfigPropertyChanged;
|
|
_dataConfig.CooldownsDataEnabledChangedEvent -= OnCooldownEnabledChanged;
|
|
Plugin.ClientState.TerritoryChanged -= OnTerritoryChanged;
|
|
|
|
Instance = null!;
|
|
}
|
|
|
|
private void OnConfigReset(ConfigurationManager sender)
|
|
{
|
|
ConfigReset(sender);
|
|
}
|
|
|
|
private void ConfigReset(ConfigurationManager sender, bool forceUpdate = true)
|
|
{
|
|
if (_config != null)
|
|
{
|
|
_config.ValueChangeEvent -= OnConfigPropertyChanged;
|
|
}
|
|
|
|
_config = sender.GetConfigObject<PartyCooldownsConfig>();
|
|
_config.ValueChangeEvent += OnConfigPropertyChanged;
|
|
|
|
if (_dataConfig != null)
|
|
{
|
|
_dataConfig.CooldownsDataEnabledChangedEvent -= OnCooldownEnabledChanged;
|
|
}
|
|
|
|
_dataConfig = sender.GetConfigObject<PartyCooldownsDataConfig>();
|
|
_dataConfig.CooldownsDataEnabledChangedEvent += OnCooldownEnabledChanged;
|
|
_dataConfig.UpdateDataIfNeeded();
|
|
|
|
if (forceUpdate)
|
|
{
|
|
ForcedUpdate();
|
|
}
|
|
}
|
|
|
|
#endregion Singleton
|
|
|
|
private Hook<ActionEffectHandler.Delegates.Receive>? _onActionUsedHook;
|
|
|
|
private delegate void ActorControlDelegate(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12);
|
|
private Hook<ActorControlDelegate>? _actorControlHook;
|
|
|
|
private Dictionary<uint, Dictionary<uint, PartyCooldown>>? _oldMap;
|
|
private Dictionary<uint, Dictionary<uint, PartyCooldown>> _cooldownsMap = new Dictionary<uint, Dictionary<uint, PartyCooldown>>();
|
|
public IReadOnlyDictionary<uint, Dictionary<uint, PartyCooldown>> CooldownsMap => _cooldownsMap;
|
|
|
|
private Dictionary<uint, double> _technicalStepMap = new Dictionary<uint, double>();
|
|
|
|
public delegate void PartyCooldownsChangedEventHandler(PartyCooldownsManager sender);
|
|
public event PartyCooldownsChangedEventHandler? CooldownsChangedEvent;
|
|
|
|
private bool _wasInDuty = false;
|
|
|
|
private void OnActorControl(uint entityId, uint type, uint buffID, uint direct, uint actionId, uint sourceId, uint arg7, uint arg8, uint arg9, uint arg10, ulong targetId, byte arg12)
|
|
{
|
|
_actorControlHook?.Original(entityId, type, buffID, direct, actionId, sourceId, arg7, arg8, arg9, arg10, targetId, arg12);
|
|
|
|
// detect wipe fadeouts (not 100% reliable but good enough)
|
|
if (type == 0x4000000F)
|
|
{
|
|
ResetCooldowns();
|
|
}
|
|
}
|
|
|
|
private void ResetCooldowns()
|
|
{
|
|
foreach (uint actorId in _cooldownsMap.Keys)
|
|
{
|
|
foreach (PartyCooldown cooldown in _cooldownsMap[actorId].Values)
|
|
{
|
|
cooldown.LastTimeUsed = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
private unsafe void OnActionUsed(uint actorId, Character* casterPtr, Vector3* targetPos, Header* header, TargetEffects* effects, GameObjectId* targetEntityIds)
|
|
{
|
|
_onActionUsedHook?.Original(actorId, casterPtr, targetPos, header, effects, targetEntityIds);
|
|
|
|
// check if its an action
|
|
if ((ActionType)header->ActionType != ActionType.Action ) { return; }
|
|
|
|
// check if its a member in the party
|
|
if (!_cooldownsMap.ContainsKey(actorId))
|
|
{
|
|
// check if its a party member's pet
|
|
IGameObject? actor = Plugin.ObjectTable.SearchById(actorId);
|
|
|
|
if (actor is IBattleNpc battleNpc && _cooldownsMap.ContainsKey(battleNpc.OwnerId))
|
|
{
|
|
actorId = battleNpc.OwnerId;
|
|
}
|
|
else
|
|
{
|
|
actorId = 0;
|
|
}
|
|
}
|
|
|
|
if (actorId <= 0) { return; }
|
|
|
|
uint actionID = header->ActionId;
|
|
|
|
// special case for starry muse > set id to scenic muse
|
|
if (actionID == 34675)
|
|
{
|
|
actionID = 35349;
|
|
}
|
|
|
|
// special case for technical step / finish
|
|
// we detect when technical step is pressed and save the time
|
|
// so we can properly calculate the cooldown once finish is pressed
|
|
if (actionID == 16193 || actionID == 16194 || actionID == 16195 || actionID == 16196)
|
|
{
|
|
actionID = 16004;
|
|
}
|
|
|
|
if (actionID == 15998)
|
|
{
|
|
_technicalStepMap[actorId] = ImGui.GetTime();
|
|
}
|
|
else
|
|
{
|
|
// check if its an action we track
|
|
if (_cooldownsMap[actorId].TryGetValue(actionID, out PartyCooldown? cooldown) && cooldown != null)
|
|
{
|
|
// if its technical finish, we set the cooldown start time to
|
|
// the time when step was pressed
|
|
if (_technicalStepMap.TryGetValue(actorId, out double stepStartTime) && actionID == 16004)
|
|
{
|
|
cooldown.OverridenCooldownStartTime = stepStartTime;
|
|
_technicalStepMap.Remove(actorId);
|
|
}
|
|
|
|
double now = ImGui.GetTime();
|
|
cooldown.LastTimeUsed = now;
|
|
cooldown.IgnoreNextUse = false;
|
|
|
|
foreach (uint id in cooldown.Data.SharedActionIds)
|
|
{
|
|
if (_cooldownsMap[actorId].TryGetValue(id, out PartyCooldown? sharedCooldown) && sharedCooldown != null)
|
|
{
|
|
sharedCooldown.LastTimeUsed = now;
|
|
sharedCooldown.IgnoreNextUse = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ForcedUpdate()
|
|
{
|
|
OnMembersChanged(PartyManager.Instance);
|
|
}
|
|
|
|
private void OnMembersChanged(PartyManager sender)
|
|
{
|
|
Plugin.Framework.RunOnFrameworkThread(() =>
|
|
{
|
|
if (sender.Previewing || _config.Preview) { return; }
|
|
|
|
_cooldownsMap.Clear();
|
|
|
|
if (_config.ShowOnlyInDuties && !Plugin.Condition[ConditionFlag.BoundByDuty])
|
|
{
|
|
CooldownsChangedEvent?.Invoke(this);
|
|
return;
|
|
}
|
|
|
|
// show when solo
|
|
if (sender.IsSoloParty() || sender.MemberCount == 0)
|
|
{
|
|
var player = Plugin.ObjectTable.LocalPlayer;
|
|
if (_config.ShowWhenSolo && player != null)
|
|
{
|
|
_cooldownsMap.Add((uint)player.GameObjectId, CooldownsForMember((uint)player.GameObjectId, player.ClassJob.RowId, player.Level, null));
|
|
}
|
|
}
|
|
else if (!_config.ShowOnlyInDuties || Plugin.Condition[ConditionFlag.BoundByDuty])
|
|
{
|
|
// add new members
|
|
foreach (IPartyFramesMember member in sender.GroupMembers)
|
|
{
|
|
if (member.ObjectId > 0)
|
|
{
|
|
_cooldownsMap.Add(member.ObjectId, CooldownsForMember(member));
|
|
}
|
|
}
|
|
}
|
|
|
|
CooldownsChangedEvent?.Invoke(this);
|
|
});
|
|
}
|
|
|
|
private Dictionary<uint, PartyCooldown> CooldownsForMember(IPartyFramesMember member)
|
|
{
|
|
return CooldownsForMember(member.ObjectId, member.JobId, member.Level, member);
|
|
}
|
|
|
|
private Dictionary<uint, PartyCooldown> CooldownsForMember(uint objectId, uint jobId, uint level, IPartyFramesMember? member)
|
|
{
|
|
Dictionary<uint, PartyCooldown> cooldowns = new Dictionary<uint, PartyCooldown>();
|
|
|
|
foreach (PartyCooldownData data in _dataConfig.Cooldowns)
|
|
{
|
|
if (data.EnabledV2 != PartyCooldownEnabled.Disabled &&
|
|
level >= data.RequiredLevel &&
|
|
(data.DisabledAfterLevel == 0 || level < data.DisabledAfterLevel) &&
|
|
data.IsUsableBy(jobId) &&
|
|
!data.ExcludedJobIds.Contains(jobId))
|
|
{
|
|
cooldowns.Add(data.ActionId, new PartyCooldown(data, objectId, level, member));
|
|
}
|
|
}
|
|
|
|
return cooldowns;
|
|
}
|
|
|
|
#region events
|
|
private void OnConfigPropertyChanged(object sender, OnChangeBaseArgs args)
|
|
{
|
|
if (args.PropertyName == "Preview")
|
|
{
|
|
UpdatePreview();
|
|
}
|
|
else if (args.PropertyName == "ShowWhenSolo" && PartyManager.Instance?.MemberCount == 0)
|
|
{
|
|
OnMembersChanged(PartyManager.Instance);
|
|
}
|
|
else if (args.PropertyName == "ShowOnlyInDuties" && PartyManager.Instance != null)
|
|
{
|
|
OnMembersChanged(PartyManager.Instance);
|
|
}
|
|
}
|
|
|
|
private void OnJobChanged(uint jobId)
|
|
{
|
|
ForcedUpdate();
|
|
}
|
|
|
|
private void OnCooldownEnabledChanged(PartyCooldownsDataConfig config)
|
|
{
|
|
ForcedUpdate();
|
|
}
|
|
|
|
private void OnTerritoryChanged(ushort territoryId)
|
|
{
|
|
bool isInDuty = Plugin.Condition[ConditionFlag.BoundByDuty];
|
|
if (_config.ShowOnlyInDuties && _wasInDuty != isInDuty)
|
|
{
|
|
ForcedUpdate();
|
|
}
|
|
|
|
_wasInDuty = isInDuty;
|
|
}
|
|
|
|
public void UpdatePreview()
|
|
{
|
|
if (!_config.Preview)
|
|
{
|
|
if (_oldMap != null)
|
|
{
|
|
_cooldownsMap = _oldMap;
|
|
}
|
|
else
|
|
{
|
|
_cooldownsMap.Clear();
|
|
}
|
|
|
|
if (PartyManager.Instance.Previewing)
|
|
{
|
|
CooldownsChangedEvent?.Invoke(this);
|
|
}
|
|
else
|
|
{
|
|
OnMembersChanged(PartyManager.Instance);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (PartyManager.Instance?.Previewing == false)
|
|
{
|
|
_oldMap = _cooldownsMap;
|
|
}
|
|
|
|
_cooldownsMap.Clear();
|
|
Random RNG = new Random((int)ImGui.GetTime());
|
|
|
|
for (uint i = 1; i < 9; i++)
|
|
{
|
|
Dictionary<uint, PartyCooldown> cooldowns = new Dictionary<uint, PartyCooldown>();
|
|
|
|
JobRoles role = i < 3 ? JobRoles.Tank : (i < 5 ? JobRoles.Healer : JobRoles.Unknown);
|
|
role = role == JobRoles.Unknown ? JobRoles.DPSMelee + RNG.Next(3) : role;
|
|
int jobCount = JobsHelper.JobsByRole[role].Count;
|
|
int jobIndex = RNG.Next(jobCount);
|
|
uint jobId = JobsHelper.JobsByRole[role][jobIndex];
|
|
|
|
_cooldownsMap.Add(i, CooldownsForMember(i, jobId, 90, null));
|
|
|
|
foreach (PartyCooldown cooldown in _cooldownsMap[i].Values)
|
|
{
|
|
int rng = RNG.Next(100);
|
|
if (rng > 80)
|
|
{
|
|
cooldown.LastTimeUsed = ImGui.GetTime() - 30;
|
|
}
|
|
else if (rng > 50)
|
|
{
|
|
cooldown.LastTimeUsed = ImGui.GetTime() + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
CooldownsChangedEvent?.Invoke(this);
|
|
}
|
|
#endregion
|
|
}
|
|
}
|