using System.Diagnostics; using System.Globalization; using System.Text; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Gui.ContextMenu; using Dalamud.Game.Text; using Dalamud.IoC; using Dalamud.Memory; using Dalamud.Plugin; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.GeneratedSheets2; namespace PartyDamage; public class Plugin : IDalamudPlugin { [PluginService] internal static IPluginLog Log { get; private set; } [PluginService] private IAddonLifecycle AddonLifecycle { get; init; } [PluginService] private IClientState ClientState { get; init; } [PluginService] private IFramework Framework { get; init; } [PluginService] private IPartyList PartyList { get; init; } [PluginService] internal IDalamudPluginInterface Interface { get; init; } [PluginService] internal ICommandManager CommandManager { get; init; } [PluginService] internal IContextMenu ContextMenu { get; init; } internal Configuration Config { get; } private Client Client { get; } internal PluginUi Ui { get; } private Commands Commands { get; } private Stopwatch AlternateWatch { get; } = Stopwatch.StartNew(); private Stopwatch DelayWatch { get; } = new(); private bool _ranLastTick; private bool _showDps = true; private bool _reset; private bool _wasActive; private bool _manuallyReset = true; private readonly byte[] _manaUsers = [ 6, // cnj 7, // thm 19, // pld 24, // whm 25, // blm 26, // acn 27, // smn 28, // sch 32, // drk 33, // ast 35, // rdm 36, // blu 40, // sge 42, // pct ]; public Plugin() { this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); this.Client = new Client(); this.Ui = new PluginUi(this); this.Commands = new Commands(this); this.AddonLifecycle!.RegisterListener(AddonEvent.PostUpdate, "_PartyList", this.UpdateList); this.ContextMenu!.OnMenuOpened += this.MenuOpened; } public void Dispose() { this.ContextMenu.OnMenuOpened -= this.MenuOpened; this.AddonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "_PartyList", this.UpdateList); this.Commands.Dispose(); this.Ui.Dispose(); this.Client.Dispose(); } internal void SaveConfig() { this.Interface.SavePluginConfig(this.Config); } private unsafe void MenuOpened(IMenuOpenedArgs args) { var add = args.AddonName == "_PartyList"; if (!add) { var ctx = AgentContext.Instance(); if (args.AgentPtr != (nint) ctx) { return; } var targetId = ctx->TargetObjectId.ObjectId; if (targetId == 0xE000_0000) { return; } if (this.ClientState.LocalPlayer is { } player && player.GameObjectId == targetId) { add = true; } else { var group = GroupManager.Instance()->GetGroup(); if (group != null && group->GetPartyMemberByEntityId(targetId) != null) { add = true; } } } if (!add) { return; } args.AddMenuItem(new MenuItem { Name = "Clear parse results", Prefix = SeIconChar.BoxedLetterP, PrefixColor = 37, IsEnabled = this.Client.Data?.IsActive == false, OnClicked = _ => { this._reset = true; this._manuallyReset = true; }, }); } private unsafe void UpdateList(AddonEvent type, AddonArgs args) { var ranLast = this._ranLastTick; this._ranLastTick = false; var list = (AddonPartyList*) AtkStage.Instance()->RaptureAtkUnitManager->GetAddonByName("_PartyList"); if (list == null) { return; } var shouldUpdate = false; var wasActive = this._wasActive; var becameInactive = false; if (this.Client.Data is { } data) { if (wasActive && !data.IsActive) { becameInactive = true; } this._wasActive = data.IsActive; } if (this.Client.Data is { IsActive: true }) { shouldUpdate = true; this._manuallyReset = false; } else if (becameInactive) { if (this.Config.ClearResultsOnInactive) { this.DelayWatch.Restart(); } } if (this.Config.ClearResultsOnInactive && this.DelayWatch.IsRunning && this.DelayWatch.Elapsed >= TimeSpan.FromSeconds(this.Config.ClearDelaySeconds)) { this.DelayWatch.Reset(); this._reset = true; } if (!this._reset && !this._manuallyReset) { // only do these checks if we shouldn't reset and if we're waiting for active data // keep running if we're in the delay period if (this.DelayWatch.IsRunning) { shouldUpdate = true; } // keep running if the user wants to manually clear results if (!this.Config.ClearResultsOnInactive) { shouldUpdate = true; } } if (this._reset) { this.DelayWatch.Reset(); this._reset = false; this._manuallyReset = true; this.ResetMembers(list); } if (shouldUpdate && this.UpdateListInner(list) && ranLast) { this.ResetMembers(list); } } /// /// Update the party list. /// /// true if the list should be reset immediately private unsafe bool UpdateListInner(AddonPartyList* list) { if (this.Client.Data is not { } data) { return false; } if (this.ClientState.LocalPlayer is not { } player) { return false; } var chara = (Character*) player.Address; var playerName = player.Name.TextValue; if (list->HoveredIndex >= 0 || list->TargetedIndex >= 0) { return true; } var names = new List<(string, int)>(); var members = AgentHUD.Instance()->PartyMembers; for (var i = 0; i < members.Length && i < list->PartyMembers.Length; i++) { var member = members[i]; if (member.Name == null) { continue; } // controller soft target not handled by hoveredindex above if (chara->GetSoftTargetId() == member.EntityId) { return true; } var name = MemoryHelper.ReadStringNullTerminated((nint) member.Name); names.Add((name, i)); } this._ranLastTick = true; if (this.AlternateWatch.Elapsed.TotalMilliseconds > this.Config.AlternateSeconds * 1_000) { this.AlternateWatch.Restart(); this._showDps ^= true; } for (var i = 0; i < names.Count; i++) { var (name, membersIdx) = names[i]; var lookupName = name == playerName ? "YOU" : name; var member = members[membersIdx]; if (!data.Combatants.TryGetValue(lookupName, out var combatant)) { continue; } this.UpdateMember(list->PartyMembers[i], member.Object, data.Encounter, combatant); } return false; } private unsafe void ResetMembers(AddonPartyList* list) { var members = AgentHUD.Instance()->PartyMembers; for (var i = 0; i < members.Length && i < list->PartyMembers.Length; i++) { var member = members[i]; if (member.Name == null) { continue; } var unit = list->PartyMembers[i]; if (list->TargetedIndex != i && unit.TargetGlow != null) { unit.TargetGlow->SetAlpha(255); unit.TargetGlow->SetScaleX(0); unit.TargetGlow->AddRed = 0; unit.TargetGlow->AddGreen = 0; unit.TargetGlow->AddBlue = 0; unit.TargetGlow->MultiplyRed = 100; unit.TargetGlow->MultiplyGreen = 100; unit.TargetGlow->MultiplyBlue = 100; unit.TargetGlow->ToggleVisibility(true); } var left = (AtkTextNode*) unit.MPGaugeBar->GetTextNodeById(2); var right = (AtkTextNode*) unit.MPGaugeBar->GetTextNodeById(3); var chara = member.Object; var hasLeft = left != null; var hasRight = right != null; var hasChara = chara != null; if (!hasLeft) { return; } left->TextColor = new ByteColor { RGBA = 0xFFFFFFFF, }; var manaString = hasChara ? chara->Mana.ToString(CultureInfo.InvariantCulture) : "???"; if (hasRight) { if (manaString.Length <= 1) { left->SetText(""); right->SetText(manaString); } else { left->SetText(manaString[..^2]); right->SetText(manaString[^2..]); } } else { left->SetText(manaString); } left->AddRed = 0; left->AddGreen = 0; left->AddBlue = 0; left->MultiplyRed = 100; left->MultiplyGreen = 100; left->MultiplyBlue = 100; if (hasRight) { right->TextColor = left->TextColor; right->AddRed = left->AddRed; right->AddGreen = left->AddGreen; right->AddBlue = left->AddBlue; right->MultiplyRed = left->MultiplyRed; right->MultiplyGreen = left->MultiplyGreen; right->MultiplyBlue = left->MultiplyBlue; } // fixme: need to figure out a way to get the game to repop // var defaultName = new StringBuilder("\ue06a"); // foreach (var digit in member.Object->Level.ToString(CultureInfo.InvariantCulture)) { // var offset = digit - '0'; // defaultName.Append((char) ('\ue060' + offset)); // } // defaultName.Append(' '); // defaultName.Append(member.Object->NameString); // unit.Name->SetText(defaultName.ToString()); // unit.Name->TextColor = new ByteColor { // RGBA = 0xFFFFFFFF, // }; // unit.Name->AddRed = 0; // unit.Name->AddGreen = 0; // unit.Name->AddBlue = 0; // unit.Name->MultiplyRed = 100; // unit.Name->MultiplyGreen = 100; // unit.Name->MultiplyBlue = 100; } } private unsafe void UpdateMember( AddonPartyList.PartyListMemberStruct member, BattleChara* chara, Encounter encounter, Combatant combatant ) { if (this.Config.UseDpsBar && member.TargetGlow != null) { member.TargetGlow->ToggleVisibility(true); member.TargetGlow->SetAlpha((byte) Math.Round(this.Config.BarAlpha * 255)); member.TargetGlow->SetScaleX( encounter.EncDps == 0 ? 0 : combatant.EncDps / encounter.EncDps ); member.TargetGlow->AddRed = (byte) Math.Clamp(this.Config.BarAddRed, 0, 255); member.TargetGlow->AddGreen = (byte) Math.Clamp(this.Config.BarAddGreen, 0, 255); member.TargetGlow->AddBlue = (byte) Math.Clamp(this.Config.BarAddBlue, 0, 255); member.TargetGlow->MultiplyRed = (byte) Math.Clamp(this.Config.BarMulRed, 0, 100); member.TargetGlow->MultiplyGreen = (byte) Math.Clamp(this.Config.BarMulGreen, 0, 100); member.TargetGlow->MultiplyBlue = (byte) Math.Clamp(this.Config.BarMulBlue, 0, 100); } if (this.Config.Mode == MeterMode.Mana) { var left = (AtkTextNode*) member.MPGaugeBar->GetTextNodeById(2); var right = (AtkTextNode*) member.MPGaugeBar->GetTextNodeById(3); var hasLeft = left != null; var hasRight = right != null; var hasChara = chara != null; if (!hasLeft) { return; } if (this.Config.Alternate && !this._showDps) { var isCaster = hasChara && Array.IndexOf(this._manaUsers, chara->ClassJob) != -1; if (!this.Config.ManaModeAlternateOnlyManaUsers || isCaster) { left->TextColor = new ByteColor { RGBA = 0xFFFFFFFF, }; left->AddRed = 0; left->AddGreen = 0; left->AddBlue = 0; left->MultiplyRed = 100; left->MultiplyGreen = 100; left->MultiplyBlue = 100; if (hasRight) { right->TextColor = left->TextColor; right->AddRed = left->AddRed; right->AddGreen = left->AddGreen; right->AddBlue = left->AddBlue; right->MultiplyRed = left->MultiplyRed; right->MultiplyGreen = left->MultiplyGreen; right->MultiplyBlue = left->MultiplyBlue; } var manaString = hasChara ? chara->Mana.ToString(CultureInfo.InvariantCulture) : "???"; if (hasRight) { left->SetText(manaString[..^2]); right->SetText(manaString[^2..]); } else { left->SetText(manaString); } return; } } left->TextColor = new ByteColor { RGBA = this.Config.TextColour, }; left->AddRed = (byte) Math.Clamp(this.Config.TextAddRed, 0, 255); left->AddGreen = (byte) Math.Clamp(this.Config.TextAddGreen, 0, 255); left->AddBlue = (byte) Math.Clamp(this.Config.TextAddBlue, 0, 255); left->MultiplyRed = (byte) Math.Clamp(this.Config.TextMulRed, 0, 100); left->MultiplyGreen = (byte) Math.Clamp(this.Config.TextMulGreen, 0, 100); left->MultiplyBlue = (byte) Math.Clamp(this.Config.TextMulBlue, 0, 100); if (hasRight) { right->TextColor = left->TextColor; right->AddRed = left->AddRed; right->AddGreen = left->AddGreen; right->AddBlue = left->AddBlue; right->MultiplyRed = left->MultiplyRed; right->MultiplyGreen = left->MultiplyGreen; right->MultiplyBlue = left->MultiplyBlue; } string leftText, rightText; if (combatant.EncDps == 0 || float.IsInfinity(combatant.EncDps) || float.IsNaN(combatant.EncDps)) { leftText = "0."; rightText = "00"; } else if (combatant.EncDps < 1_000) { var dps = Math.Round(combatant.EncDps * 100).ToString(CultureInfo.InvariantCulture); leftText = $"{dps[..^2]}."; rightText = dps[^2..]; } else if (combatant.EncDps < 1_000_000) { var dps = Math.Round(combatant.EncDps / 100).ToString(CultureInfo.InvariantCulture); leftText = $"{dps[..^1]}."; rightText = $"{dps[^1..]}K"; } else if (combatant.EncDps < 1_000_000_000) { var dps = Math.Round(combatant.EncDps / 100_000).ToString(CultureInfo.InvariantCulture); leftText = $"{dps[..^1]}."; rightText = $"{dps[^1..]}M"; } else { var dps = Math.Round(combatant.EncDps / 100_000_000).ToString(CultureInfo.InvariantCulture); leftText = $"{dps[..^1]}."; rightText = $"{dps[^1..]}B"; } if (hasRight) { left->SetText(leftText); right->SetText(rightText); } else { left->SetText($"{leftText}{rightText}"); } } // else if (this.Config.Mode == MeterMode.Name) { // var dpsText = combatant.EncDps switch { // float.NaN => "0", // float.PositiveInfinity => "0", // float.NegativeInfinity => "0", // < 1_000 => $"{combatant.EncDps:N2}", // < 1_000_000 => $"{combatant.EncDps / 1_000:N2}K", // < 1_000_000_000 => $"{combatant.EncDps / 1_000_000:N2}M", // _ => $"{combatant.EncDps / 1_000_000_000:N2}B", // }; // if (this.Config.Alternate && !this._showDps) { // member.Name->TextColor = new ByteColor { // RGBA = 0xFFFFFFFF, // }; // member.Name->SetText(chara->Name); // return; // } // member.Name->SetText(dpsText); // member.Name->TextColor = new ByteColor { // RGBA = this.Config.TextColour, // }; // member.Name->AddRed = (byte) Math.Clamp(this.Config.TextAddRed, 0, 255); // member.Name->AddGreen = (byte) Math.Clamp(this.Config.TextAddGreen, 0, 255); // member.Name->AddBlue = (byte) Math.Clamp(this.Config.TextAddBlue, 0, 255); // member.Name->MultiplyRed = (byte) Math.Clamp(this.Config.TextMulRed, 0, 100); // member.Name->MultiplyGreen = (byte) Math.Clamp(this.Config.TextMulGreen, 0, 100); // member.Name->MultiplyBlue = (byte) Math.Clamp(this.Config.TextMulBlue, 0, 100); // } } }