using System.Diagnostics; using System.Globalization; using System.Text; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.IoC; using Dalamud.Memory; using Dalamud.Plugin; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; 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; } internal Configuration Config { get; } private Client Client { get; } internal PluginUi Ui { get; } private Commands Commands { get; } private Stopwatch Watch { get; } = Stopwatch.StartNew(); private bool _ranLastTick; private bool _showDps = 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); } public void Dispose() { 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 UpdateList(AddonEvent type, AddonArgs args) { var ranLast = this._ranLastTick; this._ranLastTick = false; var list = (AddonPartyList*) AtkStage.Instance()->RaptureAtkUnitManager->GetAddonByName("_PartyList"); if (!this.UpdateListInner(list) && ranLast) { this.ResetMembers(list); } } private unsafe bool UpdateListInner(AddonPartyList* list) { if (this.Client.Data is not { } data) { return false; } if (!data.IsActive) { 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 false; } 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 false; } var name = MemoryHelper.ReadStringNullTerminated((nint) member.Name); names.Add((name, i)); } this._ranLastTick = true; if (this.Watch.Elapsed.TotalMilliseconds > this.Config.AlternateSeconds * 1_000) { this.Watch.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 true; } 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->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); left->TextColor = new ByteColor { RGBA = 0xFFFFFFFF, }; right->TextColor = left->TextColor; var manaString = member.Object->Mana.ToString(CultureInfo.InvariantCulture); left->SetText(manaString[..^2]); right->SetText(manaString[^2..]); left->AddRed = 0; left->AddGreen = 0; left->AddBlue = 0; left->MultiplyRed = 100; left->MultiplyGreen = 100; left->MultiplyBlue = 100; 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->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); if (this.Config.Alternate && !this._showDps) { var isCaster = Array.IndexOf(this._manaUsers, chara->ClassJob) != -1; if (!this.Config.ManaModeAlternateOnlyManaUsers || isCaster) { left->TextColor = new ByteColor { RGBA = 0xFFFFFFFF, }; right->TextColor = left->TextColor; left->AddRed = 0; left->AddGreen = 0; left->AddBlue = 0; left->MultiplyRed = 100; left->MultiplyGreen = 100; left->MultiplyBlue = 100; 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 = chara->Mana.ToString(CultureInfo.InvariantCulture); left->SetText(manaString[..^2]); right->SetText(manaString[^2..]); return; } } left->TextColor = new ByteColor { RGBA = this.Config.TextColour, }; right->TextColor = left->TextColor; 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); right->AddRed = left->AddRed; right->AddGreen = left->AddGreen; right->AddBlue = left->AddBlue; right->MultiplyRed = left->MultiplyRed; right->MultiplyGreen = left->MultiplyGreen; right->MultiplyBlue = left->MultiplyBlue; if (combatant.EncDps == 0 || float.IsInfinity(combatant.EncDps) || float.IsNaN(combatant.EncDps)) { left->SetText("0."); right->SetText("00"); } else if (combatant.EncDps < 1_000) { var dps = Math.Round(combatant.EncDps * 100).ToString(CultureInfo.InvariantCulture); left->SetText($"{dps[..^2]}."); right->SetText(dps[^2..]); } else if (combatant.EncDps < 1_000_000) { var dps = Math.Round(combatant.EncDps / 100).ToString(CultureInfo.InvariantCulture); left->SetText($"{dps[..^1]}."); right->SetText($"{dps[^1..]}K"); } else if (combatant.EncDps < 1_000_000_000) { var dps = Math.Round(combatant.EncDps / 100_000).ToString(CultureInfo.InvariantCulture); left->SetText($"{dps[..^1]}."); right->SetText($"{dps[^1..]}M"); } else { var dps = Math.Round(combatant.EncDps / 100_000_000).ToString(CultureInfo.InvariantCulture); left->SetText($"{dps[..^1]}."); right->SetText($"{dps[^1..]}B"); } } 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); } } }