using System.Diagnostics; using System.Globalization; 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; 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.UseEvaluatorNpc && this.Client.Data != null) { var combatants = this.Client.Data.Combatants.Values.ToList(); combatants.Sort((a, b) => b.EncDps.CompareTo(a.EncDps)); var rank = combatants.FindIndex(combatant => combatant.Name == "YOU"); // 001 Venat // 002 Zenos // 003 Evil? Zenos // 004 Fandaniel // 005 ? some garlean // 006 ? // 007 Estinien // 008 Alphinaud // 009 radiant host dude // 010 Forchenault // 011 Alisaie // 012 G'raha Tia // 013 Thancred // 014 oh god I should know his name // 015 one of the padjali, the boy // 016 Emmanelaine or however you spell it // 017 ? // 018 Lucia // 019 Pipin? // 020 Lyse // 021 Magnai for sure // 022 Sadu // 023 Cirina // 024 Thancred again // 025 Y'shtola // 026 Urianger // 027 Nero with helmet on // 028 different alphinaud's dad // 029 Kan-E-Senna // 030 Aymeric // 031 ? // 032 oh god I should know his name... stormblood main boy // 033 ? (monster) // 034 Alphinaud again // 035 Estinien casual // 036 Alisaie cold attire // 037 some kobold // 038 bearded roe // 039 ? // 040 ardbert with ascian mask? // 041 ardbert but creepy smile // 042 ? // 043 oh god some fviera // 044 another fviera // 045 yet another fviera // 046 ? soranus // 047 gaius? // 048 imperial buckethead // 049 imperial helmet // 050 ? soranus // 051 imperial helmet but less aggro // 052 Lyna // 053 Alphinaud in a red shawl // 054 mustachioed-man with horn helmet // 055 different horn helmet // 056 Alisaie in a red a shawl // 057 horn helmet // 058 horn helmet // 059 fu manchu man // 060 Minfilia (Ryne) // 061 Alisaie cold attire // 062 Alphi cold attire // 063 Ranjit? // 064 Ryne // 065 Female Jester red // 066 Female Jester blue // 067 a dwarf // 068 Fandaniel hood covering eyes // 069 Merlwyb // 070 Vrtra boy avatar // 071 Krile // 072 Tataru // 073 Hydaelyn? // 074 Faurchenault // 075 Vrtra // 076 some boss // 077 Zero // 078 Vrtra man avatar // 079 Zenos Helmet? // 080 Lyse in red dress // 081 Yugiri // 082 Zenos? helmet revealing eyes // 083 Sadu // 084 some blue au ra male // 085 Y'shtola // 086 garlean guy // 087 garlean pot helmet // 088 garlean bucket helmet // 089 garlean pot helmet // 090 purple haired chin strap guy // 091 garlean bucket helmet // 092 evil oni? // 093 Asahi // 094 Lakshmi // 095 cripple man // 096 criminal woman // 097 ? // 098 some machine? // 099 some guy with radiation hair // 100 Krile but happy // 101 lalafell in horn helmet // 102 some ala mhigan resistance guy // 103 criminal lady again // 104 Lyse in red jacket // 105 green Kojin main guy // 106 Yotsuyu // 107 magnai // 108 Gosetsu // 109 old guy // 110 imperial bucket head // 111 some guy with a scar maybe ala mhigan resistance // 112 Thancred with blindfold? // 113 face mask ala mhigan resistance // 114 imperial pot helmet // 115 lupin guy // 116 another lupin // 117 imperial pot helmet but roe // 118 straw hat // 119 some boy // 120 Raubahn // 121 Sultana? // 122 elezen man // 123 angry bird thing // 124 Haurchefant // 125 Tataru // 126 no idea elezen // 127 eyepatch elezen // 128 Alphinaud old outfit // 129 Yugiri with dagger // 130 Raubahn maybe with two arms? // 131 some uldahn lalafell // 132 Ilberd // 133 a crystal brave // 134 diff crystal brave // 135 woman crystal brave // 136 Estinien full armour // 137 some dude in chain mail elezen face plate // 138 someone other person in same outfit not elezen maybe hyur // 139 Hilda // 140 elezen guy // 141 ishgardian soldier face plate chain mail // 142 a vanu vanu or gundu or whatever // 143 goblin brayflox // 144 the machine boss from brayflox hard // 145 imperial in cool helmet (not gaius?) // 146 red dragoon but doesn't look like estinien // 147 some child // 148 some old elezen // 149 emmanelaine's brother // 150 old elezen woman // 151 someone in a hat // 152 other person in a hat // 153 Raubahn in big helmet // 154 Ardbert? // 155 old outfit Urianger no mask // 156 Yda // 157 imperial pot helmet long hair // 158 ??? // 159 ??? // 160 mammet with green face // 161 Hancock // 162 pink haired fem elezen // 163 not sure dragoon // 164 Papalymo // 165 old Thancred // 166 some elezen girl // 167 Yugiri in full face mask // 168 roe in big plate armour // 169 child f (doman?) // 170 another child m // 171 m elezen caster in witch's hat // 172 another child m/f // 173 another child m // 174 Thancred ascian mask // 175 full armour person // 176 red imperial buckethead // 177 Minfilia (original) // 178 Urianger (old outfit with mask thing) // 179 imperial pot helmet // 180 imperial pot helmet // 181 biggs or wedge (the lala) // 182 biggs or wedge (the roe) // 183 imperial buckethead // 184 Cid (old outfit) // 185 imperial pot helmet // 186 imperial pot helmet (roe) // 187 Y'shtola with normal sight and aetherometer or whatever // 188 crazy hair girl // 189 an elezen with a beard whoa // 190 some kind of monster? maybe old mamool ja // 191 Haurchefant in chain mail // 192 Haurchefant in chain mail // 193 elezen with a soul patch // 194 young elezen (emman?) // 195 elezen with awful facial hair // 196 roe with straw hat (not the ronin kind) // 197 generic man // 198 generic anime man // 199 Ranjit // 200 Amalj'aa // 201 shirtless roe man // 202 different shirtless roe man // 203 mutton chops guy // 204 elezen with a sick red beard whoa // 205 imperial pot helmet roe beard // 206 creepy mask miqo // 207 sylph purple // 208 sylph green // 209 weird monster guy // 210 Erenville // 211 Wuk Lamat (with hat) // 212 giant Toucan // 213 Arkasodara // 214 some guy // 215 imperial pot helmet // 216 gridanian guy? // 217 lady with cap // 218 man with cap // 219 roe with hat // 220 lalafell with pot helm // 221 miqo with pot helm // 222 dude with mask // 223 girl with pot helm // 224 lala with pot helm // 225 guy with cool hair // 226 dude with pot helm // 227 lala pot helm // 228 hyur pot helm // 229 pot helm // 230 roe with bandana mask // 231 yet another pot helm // 232 poooot helm // 233 thief rogue? // 234 full face mask ascian with red floaty mask and hood // 235 POT. HELM. // 236 ph // 237 roe man // 238 sailor roe man // 239 maelstrom roe lady // 240 Mamool Ja // 241 pot helm // 242 creepy mask guy // 243 wood wailer creepy mask // 244 ix...al? // 245 cfm // 246 cfm ww // 247 elezen with face paint // 248 Moogle // 249 lala with tiara and mustache // 250 boy // 251 hooded m roe // 252 hyur m // 253 hyur m // 254 Papa...lymo? // 255 dude // 256 Emet Selch elpis version // 257 Krile // 258 Gulool Ja Ja with cover for dead head // 259 some mamool ja // 260 Bakool Ja Ja mystic // 261 Bakool Ja Ja both heads // 262 a mamool ja // 263 another mamool ja // 264 another mamool ja // 265 Wuk Lamat without hat // 266 Koana // 267 old Gulool Ja Ja with both heads alive // 268 head of resolve // 269 head of reason // 270 Sphene // 271 evil Otis in machine form // 272 good Otis in machine form // 273 Sphene final boss form // 274 looks like Zero kind of // "73" + str(number) for index var msg = rank switch { < 0 => null, 1 => "Well done! I never doubted for a second that you'd do so well.", _ => "Keep trying, friend! You'll get there in the end.", }; UIModule.Instance()->ShowBattleTalkImage("G'raha Tia", msg, 5f, 73012, 0); } } 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, i, member.Object, data.Encounter, combatant); } return false; } private unsafe void ResetMember( AddonPartyList* list, BattleChara* chara, int listIndex, bool includeTargeted ) { var unit = list->PartyMembers[listIndex]; if (unit.TargetGlow != null && (includeTargeted || list->TargetedIndex != listIndex)) { 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 hasLeft = left != null && left->IsVisible(); var hasRight = right != null && right->IsVisible(); 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 ResetMembers(AddonPartyList* list) { var members = AgentHUD.Instance()->PartyMembers; for (var i = 0; i < members.Length && i < list->PartyMembers.Length; i++) { this.ResetMember(list, members[i].Object, i, false); } } private unsafe void UpdateMember( AddonPartyList* list, int listIndex, BattleChara* chara, Encounter encounter, Combatant combatant ) { var member = list->PartyMembers[listIndex]; 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 && left->IsVisible(); var hasRight = right != null && right->IsVisible(); 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) { this.ResetMember(list, chara, listIndex, true); 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}"); } } } }