diff --git a/Plugin.cs b/Plugin.cs index 3754e1b..4a55cbd 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -9,11 +9,15 @@ using Dalamud.IoC; using Dalamud.Memory; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Group; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.GeneratedSheets; @@ -63,6 +67,9 @@ public class Plugin : IDalamudPlugin { private bool _wasActive; private bool _manuallyReset = true; + [Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? FE C2")] + internal unsafe delegate* unmanaged PlaySound; + private readonly byte[] _manaUsers = [ 6, // cnj 7, // thm @@ -80,6 +87,9 @@ public class Plugin : IDalamudPlugin { 42, // pct ]; + [PluginService] + private IGameInteropProvider GameInteropProvider { get; init; } + public Plugin() { this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); this.Client = new Client(); @@ -88,6 +98,8 @@ public class Plugin : IDalamudPlugin { this.AddonLifecycle!.RegisterListener(AddonEvent.PostUpdate, "_PartyList", this.UpdateList); this.ContextMenu!.OnMenuOpened += this.MenuOpened; + + this.GameInteropProvider!.InitializeFromAttributes(this); } public void Dispose() { @@ -296,21 +308,64 @@ public class Plugin : IDalamudPlugin { 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; - } + var names = new List(); - // controller soft target not handled by hoveredindex above - if (chara->GetSoftTargetId() == member.EntityId) { - return true; - } + var crossInfo = InfoProxyCrossRealm.Instance(); + if (crossInfo->IsCrossRealm != 0) { + var crossGroup = crossInfo->CrossRealmGroups[crossInfo->LocalPlayerGroupIndex]; - var name = MemoryHelper.ReadStringNullTerminated((nint) member.Name); - names.Add((name, i)); + for (var i = 0; i < crossGroup.GroupMemberCount && i < list->PartyMembers.Length; i++) { + var member = crossGroup.GroupMembers[i]; + var name = member.NameString; + if (string.IsNullOrWhiteSpace(name)) { + continue; + } + + if (chara->GetSoftTargetId() == member.EntityId) { + return true; + } + + var info = new ListInfo { + Name = name, + IsChocobo = false, + Mana = null, + PartyListIndex = i, + ClassJob = member.ClassJobId, + }; + + names.Add(info); + } + } else { + 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); + var isChocobo = member.Object != null + && member.Object->GetObjectKind() == ObjectKind.BattleNpc + && member.Object->CompanionOwnerId == player.GameObjectId; + if (isChocobo) { + name += " (YOU)"; + } + + var info = new ListInfo { + Name = name, + IsChocobo = isChocobo, + Mana = member.Object != null ? member.Object->Mana : null, + PartyListIndex = i, + ClassJob = member.Object != null ? member.Object->ClassJob : null, + }; + + names.Add(info); + } } this._ranLastTick = true; @@ -321,17 +376,16 @@ public class Plugin : IDalamudPlugin { } for (var i = 0; i < names.Count; i++) { - var (name, membersIdx) = names[i]; - var lookupName = name == playerName + var info = names[i]; + var lookupName = info.Name == playerName ? "YOU" - : name; - var member = members[membersIdx]; + : info.Name; if (!data.Combatants.TryGetValue(lookupName, out var combatant)) { continue; } - this.UpdateMember(list, i, member.Object, data.Encounter, combatant, data.Combatants.Values); + this.UpdateMember(list, info, data.Encounter, combatant, data.Combatants.Values); } return false; @@ -339,13 +393,12 @@ public class Plugin : IDalamudPlugin { private unsafe void ResetMember( AddonPartyList* list, - BattleChara* chara, - int listIndex, + ListInfo info, bool includeBar, bool includeTargeted ) { - var unit = list->PartyMembers[listIndex]; - if (includeBar && unit.TargetGlow != null && (includeTargeted || list->TargetedIndex != listIndex)) { + var unit = info.IsChocobo ? list->Chocobo : list->PartyMembers[info.PartyListIndex]; + if (includeBar && unit.TargetGlow != null && (includeTargeted || list->TargetedIndex != info.PartyListIndex)) { unit.TargetGlow->SetAlpha(255); unit.TargetGlow->SetScaleX(0); unit.TargetGlow->AddRed = 0; @@ -355,6 +408,29 @@ public class Plugin : IDalamudPlugin { unit.TargetGlow->MultiplyGreen = 100; unit.TargetGlow->MultiplyBlue = 100; unit.TargetGlow->ToggleVisibility(true); + unit.TargetGlow->SetWidth(320); + } + + if (info.IsChocobo) { + var chocoboTimer = list->GetTextNodeById(5); + var chocoboTimerIcon = list->GetTextNodeById(4); + + var hasTimer = chocoboTimer != null && chocoboTimer->IsVisible(); + var hasTimerIcon = chocoboTimerIcon != null && chocoboTimerIcon->IsVisible(); + + if (hasTimer) { + var companion = UIState.Instance()->Buddy.CompanionInfo; + var timeLeft = TimeSpan.FromSeconds(Math.Ceiling(companion.TimeLeft)); + chocoboTimer->SetText($"{timeLeft.Minutes:00}:{timeLeft.Seconds:00}"); + } + + if (hasTimerIcon) { + chocoboTimerIcon->SetText(SeIconChar.Clock.ToIconString()); + } + } + + if (unit.MPGaugeBar == null) { + return; } var left = (AtkTextNode*) unit.MPGaugeBar->GetTextNodeById(2); @@ -362,7 +438,6 @@ public class Plugin : IDalamudPlugin { var hasLeft = left != null && left->IsVisible(); var hasRight = right != null && right->IsVisible(); - var hasChara = chara != null; if (!hasLeft) { return; @@ -372,9 +447,9 @@ public class Plugin : IDalamudPlugin { RGBA = 0xFFFFFFFF, }; - var manaString = hasChara - ? chara->Mana.ToString(CultureInfo.InvariantCulture) - : "???"; + var manaString = info.Mana == null + ? "???" + : info.Mana.Value.ToString(CultureInfo.InvariantCulture); if (hasRight) { if (manaString.Length <= 1) { left->SetText(""); @@ -430,7 +505,25 @@ public class Plugin : IDalamudPlugin { 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, true, false); + var member = members[i]; + var name = MemoryHelper.ReadStringNullTerminated((nint) member.Name); + var isChocobo = member.Object != null + && this.ClientState.LocalPlayer is { } player + && member.Object->GetObjectKind() == ObjectKind.BattleNpc + && member.Object->CompanionOwnerId == player.GameObjectId; + if (isChocobo) { + name += " (YOU)"; + } + + var info = new ListInfo { + Name = name, + IsChocobo = isChocobo, + Mana = member.Object != null ? member.Object->Mana : null, + PartyListIndex = i, + ClassJob = member.Object != null ? member.Object->ClassJob : null, + }; + + this.ResetMember(list, info, true, false); } } @@ -464,34 +557,35 @@ public class Plugin : IDalamudPlugin { private unsafe void UpdateMember( AddonPartyList* list, - int listIndex, - BattleChara* chara, + ListInfo info, Encounter encounter, Combatant combatant, IEnumerable combatants ) { - var member = list->PartyMembers[listIndex]; + var member = info.IsChocobo ? list->Chocobo : list->PartyMembers[info.PartyListIndex]; if (this.Config.UseDpsBar && member.TargetGlow != null) { float denominator; switch (this.Config.DpsBarMode) { case DpsBarMode.Encounter: { - denominator = combatants.Select(c => c.EncDps).Max(); + denominator = combatants + .Select(c => c.EncDps) + .Aggregate(0f, (acc, val) => val > acc ? val : acc); break; } case DpsBarMode.Party: { var party = this.GetPartyNames(false); denominator = combatants - .Where(c => c.Name == "YOU" || party.Contains(c.Name)) + .Where(c => c.Name == "YOU" || party.Contains(c.Name) || c.Name.EndsWith(')')) .Select(c => c.EncDps) - .Max(); + .Aggregate(0f, (acc, val) => val > acc ? val : acc); break; } case DpsBarMode.Alliance: { var alliance = this.GetPartyNames(true); denominator = combatants - .Where(c => c.Name == "YOU" || alliance.Contains(c.Name)) + .Where(c => c.Name == "YOU" || alliance.Contains(c.Name) || c.Name.EndsWith(')')) .Select(c => c.EncDps) - .Max(); + .Aggregate(0f, (acc, val) => val > acc ? val : acc); break; } default: { @@ -502,11 +596,12 @@ public class Plugin : IDalamudPlugin { member.TargetGlow->ToggleVisibility(true); member.TargetGlow->SetAlpha((byte) Math.Round(this.Config.BarAlpha * 255)); - member.TargetGlow->SetScaleX( - denominator == 0 - ? 0 - : combatant.EncDps / denominator - ); + // member.TargetGlow->SetScaleX( + // denominator == 0 + // ? 0 + // : combatant.EncDps / denominator + // ); + member.TargetGlow->SetScaleX(1); 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); @@ -514,49 +609,70 @@ public class Plugin : IDalamudPlugin { member.TargetGlow->MultiplyGreen = (byte) Math.Clamp(this.Config.BarMulGreen, 0, 100); member.TargetGlow->MultiplyBlue = (byte) Math.Clamp(this.Config.BarMulBlue, 0, 100); member.TargetGlow->PartId = 3; + // member.TargetGlow->SetWidth(320); + member.TargetGlow->SetWidth( + (ushort) Math.Round(320 * (denominator == 0 + ? 1 + : combatant.EncDps / denominator)) + ); } - if (this.Config.Mode == MeterMode.Mana) { + if (this.Config.Mode == MeterMode.Mana && info.IsChocobo) { + var chocoboTimer = list->GetTextNodeById(5); + var chocoboTimerIcon = list->GetTextNodeById(4); + + var hasTimer = chocoboTimer != null && chocoboTimer->IsVisible(); + var hasTimerIcon = chocoboTimerIcon != null && chocoboTimerIcon->IsVisible(); + + if (this.Config.Alternate && !this._showDps) { + this.ResetMember(list, info, false, false); + } else { + if (hasTimer) { + var (leftText, rightText) = GetDpsText(combatant); + chocoboTimer->SetText($"{leftText}{rightText}"); + } + + if (hasTimerIcon) { + chocoboTimerIcon->SetText("\ue05e"); + } + } + } + + if (this.Config.Mode == MeterMode.Mana && member.MPGaugeBar != null) { 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.Alternate && !this._showDps && info.ClassJob is { } classJob) { + var isCaster = Array.IndexOf(this._manaUsers, classJob) != -1; if (!this.Config.ManaModeAlternateOnlyManaUsers || isCaster) { - this.ResetMember(list, chara, listIndex, false, false); + this.ResetMember(list, info, false, false); 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); - + SetTextNodeColor(left); 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; + SetTextNodeColor(right); } - string leftText, rightText; + var (leftText, rightText) = GetDpsText(combatant); + if (hasRight) { + left->SetText(leftText); + right->SetText(rightText); + } else { + left->SetText($"{leftText}{rightText}"); + } + } + return; + + (string, string) GetDpsText(Combatant combatant) { + string leftText, rightText; if (combatant.EncDps == 0 || float.IsInfinity(combatant.EncDps) || float.IsNaN(combatant.EncDps)) { leftText = "0."; rightText = "00"; @@ -578,12 +694,27 @@ public class Plugin : IDalamudPlugin { rightText = $"{dps[^1..]}B"; } - if (hasRight) { - left->SetText(leftText); - right->SetText(rightText); - } else { - left->SetText($"{leftText}{rightText}"); - } + return (leftText, rightText); + } + + void SetTextNodeColor(AtkTextNode* node) { + node->TextColor = new ByteColor { + RGBA = this.Config.TextColour, + }; + node->AddRed = (byte) Math.Clamp(this.Config.TextAddRed, 0, 255); + node->AddGreen = (byte) Math.Clamp(this.Config.TextAddGreen, 0, 255); + node->AddBlue = (byte) Math.Clamp(this.Config.TextAddBlue, 0, 255); + node->MultiplyRed = (byte) Math.Clamp(this.Config.TextMulRed, 0, 100); + node->MultiplyGreen = (byte) Math.Clamp(this.Config.TextMulGreen, 0, 100); + node->MultiplyBlue = (byte) Math.Clamp(this.Config.TextMulBlue, 0, 100); } } } + +public class ListInfo { + public required string Name { get; init; } + public required bool IsChocobo { get; init; } + public required int PartyListIndex { get; init; } + public required uint? Mana { get; init; } + public required byte? ClassJob { get; init; } +}