From 80f45f5a31f8df00c8f624e4de20fb0ab92cdaa9 Mon Sep 17 00:00:00 2001 From: Knack117 Date: Sat, 31 Jan 2026 22:40:28 -0500 Subject: [PATCH] PvP nameplates: correct GC icons (62601/62602/62603), configurable GC IDs, Role/Job icon for enemy players Co-authored-by: Cursor --- Helpers/Utils.cs | 21 +++ Interface/GeneralElements/ActionBarsHud.cs | 28 ++++ Interface/GeneralElements/IconConfig.cs | 23 ++++ Interface/Nameplates/Nameplate.cs | 47 +++++++ Interface/Nameplates/NameplateConfig.cs | 22 +++ Interface/Nameplates/NameplatesHud.cs | 25 +++- Interface/Party/AllianceManager.cs | 151 +++++++++++++++------ 7 files changed, 275 insertions(+), 42 deletions(-) diff --git a/Helpers/Utils.cs b/Helpers/Utils.cs index 0280a26..0c1a548 100644 --- a/Helpers/Utils.cs +++ b/Helpers/Utils.cs @@ -94,6 +94,27 @@ namespace HSUI.Helpers return plateType >= 4 && plateType <= 11; } + /// Returns Grand Company icon ID (Maelstrom, Flames, or Adders) for enemy players in PvP Frontline. + /// Nameplate color types 4, 5, 6 map to the three teams. Returns null when not a PvP enemy team (4–6). + /// Use iconOverrides for custom IDs: [0]=team1(plate4), [1]=team2(plate5), [2]=team3(plate6). Non-zero overrides default. + public static unsafe uint? GrandCompanyIconIdForPvPEnemy(IGameObject? obj, (int t1, int t2, int t3)? iconOverrides = null) + { + if (obj == null || !Plugin.ClientState.IsPvP) return null; + StructsGameObject* gameObject = (StructsGameObject*)obj.Address; + byte plateType = gameObject->GetNamePlateColorType(); + if (plateType < 4 || plateType > 6) return null; + + // 62601=Maelstrom, 62602=Twin Adder, 62603=Immortal Flames + uint iconId = plateType switch + { + 4 => iconOverrides.HasValue && iconOverrides.Value.t1 > 0 ? (uint)iconOverrides.Value.t1 : 62601u, + 5 => iconOverrides.HasValue && iconOverrides.Value.t2 > 0 ? (uint)iconOverrides.Value.t2 : 62602u, + 6 => iconOverrides.HasValue && iconOverrides.Value.t3 > 0 ? (uint)iconOverrides.Value.t3 : 62603u, + _ => 0 + }; + return iconId > 0 ? iconId : null; + } + public static unsafe float ActorShieldValue(IGameObject? actor) { if (actor == null || actor is not ICharacter) diff --git a/Interface/GeneralElements/ActionBarsHud.cs b/Interface/GeneralElements/ActionBarsHud.cs index 4d5df23..cec7804 100644 --- a/Interface/GeneralElements/ActionBarsHud.cs +++ b/Interface/GeneralElements/ActionBarsHud.cs @@ -1327,6 +1327,34 @@ namespace HSUI.Interface.GeneralElements } } + if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.MainCommand || + slot.SlotType == RaptureHotbarModule.HotbarSlotType.ExtraCommand) + { + if (Plugin.DataManager.GetExcelSheet()?.TryGetRow(slot.ActionId, out var mcRow) == true) + { + string name = mcRow.Name.ToString(); + string desc = ""; + try + { + string descRaw = mcRow.Description.ToDalamudString().ToString(); + if (!string.IsNullOrEmpty(descRaw)) + { + try + { + var evaluated = Plugin.SeStringEvaluator.Evaluate(mcRow.Description.AsSpan()); + desc = evaluated.ExtractText(); + if (string.IsNullOrEmpty(desc)) desc = descRaw; + } + catch { desc = descRaw; } + if (!string.IsNullOrEmpty(desc)) + desc = EncryptedStringsHelper.GetString(desc); + } + } + catch { /* ignore */ } + return (name, desc ?? ""); + } + } + return (slot.SlotType.ToString(), ""); } diff --git a/Interface/GeneralElements/IconConfig.cs b/Interface/GeneralElements/IconConfig.cs index 6cb6632..179ecab 100644 --- a/Interface/GeneralElements/IconConfig.cs +++ b/Interface/GeneralElements/IconConfig.cs @@ -129,6 +129,29 @@ namespace HSUI.Interface.GeneralElements } } + /// Shows Grand Company icon (Maelstrom, Flames, or Adders) on enemy player nameplates in PvP Frontline. + public class NameplateCompanyIconConfig : NameplateIconConfig + { + [DragInt("Icon ID Team 1 (plateType 4)", min = 0, max = 999999)] + [Order(19)] + public int IconIdTeam1; + + [DragInt("Icon ID Team 2 (plateType 5)", min = 0, max = 999999)] + [Order(20)] + public int IconIdTeam2; + + [DragInt("Icon ID Team 3 (plateType 6)", min = 0, max = 999999)] + [Order(21)] + public int IconIdTeam3; + + public NameplateCompanyIconConfig() : base() { } + + public NameplateCompanyIconConfig(Vector2 position, Vector2 size, DrawAnchor anchor, DrawAnchor frameAnchor) + : base(position, size, anchor, frameAnchor) + { + } + } + public class NameplateRoleJobIconConfig : RoleJobIconConfig { public NameplateRoleJobIconConfig() : base() { } diff --git a/Interface/Nameplates/Nameplate.cs b/Interface/Nameplates/Nameplate.cs index 93237b7..2f53e9c 100644 --- a/Interface/Nameplates/Nameplate.cs +++ b/Interface/Nameplates/Nameplate.cs @@ -550,6 +550,53 @@ namespace HSUI.Interface.Nameplates } + // company icon (PvP Frontline: Maelstrom, Flames, Adders) + var gcIconOverrides = (Config.CompanyIconConfig.IconIdTeam1, Config.CompanyIconConfig.IconIdTeam2, Config.CompanyIconConfig.IconIdTeam3); + if (Config.CompanyIconConfig.Enabled && Utils.GrandCompanyIconIdForPvPEnemy(data.GameObject, gcIconOverrides) is uint gcIconId) + { + anchor = anchors.GetAnchor(Config.CompanyIconConfig.NameplateLabelAnchor, Config.CompanyIconConfig.PrioritizeHealthBarAnchor); + anchor = anchor ?? new NameplateAnchor(data.ScreenPosition, Vector2.Zero); + + var pos = Utils.GetAnchoredPosition(_config.Position + anchor.Value.Position, -anchor.Value.Size, Config.CompanyIconConfig.FrameAnchor); + var iconPos = Utils.GetAnchoredPosition(pos + Config.CompanyIconConfig.Position, Config.CompanyIconConfig.Size, Config.CompanyIconConfig.Anchor); + + drawActions.Add((Config.CompanyIconConfig.StrataLevel, () => + { + DrawHelper.DrawInWindow(_config.ID + "_enemyCompanyIcon", iconPos, Config.CompanyIconConfig.Size, false, (drawList) => + { + DrawHelper.DrawIcon(gcIconId, iconPos, Config.CompanyIconConfig.Size, false, alpha, drawList); + }); + } + )); + } + + // role/job icon (enemy players only; has ClassJob) + if (Config.RoleIconConfig.Enabled && data.GameObject is IPlayerCharacter playerCharacter) + { + uint jobId = playerCharacter.ClassJob.RowId; + uint iconId = Config.RoleIconConfig.UseRoleIcons + ? JobsHelper.RoleIconIDForJob(jobId, Config.RoleIconConfig.UseSpecificDPSRoleIcons) + : JobsHelper.IconIDForJob(jobId, (uint)Config.RoleIconConfig.Style); + + if (iconId > 0) + { + anchor = anchors.GetAnchor(Config.RoleIconConfig.NameplateLabelAnchor, Config.RoleIconConfig.PrioritizeHealthBarAnchor); + anchor = anchor ?? new NameplateAnchor(data.ScreenPosition, Vector2.Zero); + + var pos = Utils.GetAnchoredPosition(_config.Position + anchor.Value.Position, -anchor.Value.Size, Config.RoleIconConfig.FrameAnchor); + var iconPos = Utils.GetAnchoredPosition(pos + Config.RoleIconConfig.Position, Config.RoleIconConfig.Size, Config.RoleIconConfig.Anchor); + + drawActions.Add((Config.RoleIconConfig.StrataLevel, () => + { + DrawHelper.DrawInWindow(_config.ID + "_enemyRoleJobIcon", iconPos, Config.RoleIconConfig.Size, false, (drawList) => + { + DrawHelper.DrawIcon(iconId, iconPos, Config.RoleIconConfig.Size, false, alpha, drawList); + }); + } + )); + } + } + return drawActions; } diff --git a/Interface/Nameplates/NameplateConfig.cs b/Interface/Nameplates/NameplateConfig.cs index 9fb472b..2470c65 100644 --- a/Interface/Nameplates/NameplateConfig.cs +++ b/Interface/Nameplates/NameplateConfig.cs @@ -45,6 +45,10 @@ namespace HSUI.Interface.GeneralElements [Order(21)] public bool AlwaysShowTargetNameplate = true; + [Checkbox("In PvP, show only enemy player nameplates", spacing = true, help = "When in Frontlines, Rival Wings, or Crystal Conflict, hide ally player nameplates (party, alliance, teammates) and show only enemy players. Uses Enemy nameplate styling for PvP enemies.")] + [Order(22)] + public bool PvPShowOnlyEnemyPlayers = false; + public int RaycastFlag() => OcclusionType == NameplatesOcclusionType.WallsAndObjects ? 0x2000 : 0x4000; } @@ -490,6 +494,24 @@ namespace HSUI.Interface.GeneralElements ) { PrioritizeHealthBarAnchor = true, Strata = StrataLevel.LOWEST }; + [NestedConfig("Company Icon (PvP)", 46, collapsingHeader = false)] + public NameplateCompanyIconConfig CompanyIconConfig = new NameplateCompanyIconConfig( + new Vector2(-5, 0), + new Vector2(24, 24), + DrawAnchor.Right, + DrawAnchor.Left + ) + { PrioritizeHealthBarAnchor = true, Strata = StrataLevel.LOWEST }; + + [NestedConfig("Role/Job Icon (enemy players)", 47)] + public NameplateRoleJobIconConfig RoleIconConfig = new NameplateRoleJobIconConfig( + new Vector2(-35, 0), + new Vector2(24, 24), + DrawAnchor.Right, + DrawAnchor.Left + ) + { PrioritizeHealthBarAnchor = true, Strata = StrataLevel.LOWEST }; + [NestedConfig("Debuffs", 50)] public EnemyNameplateStatusEffectsListConfig DebuffsConfig = null!; diff --git a/Interface/Nameplates/NameplatesHud.cs b/Interface/Nameplates/NameplatesHud.cs index 5db44fd..6bcf4b4 100644 --- a/Interface/Nameplates/NameplatesHud.cs +++ b/Interface/Nameplates/NameplatesHud.cs @@ -128,17 +128,34 @@ namespace HSUI.Interface.Nameplates return _playerHud; } - if (data.GameObject is ICharacter character) + // In PvP, optionally show only enemy player nameplates (hide allies) + if (Config.PvPShowOnlyEnemyPlayers && Plugin.ClientState.IsPvP) { - if ((character.StatusFlags & StatusFlags.PartyMember) != 0) // PartyMember + if (data.GameObject is ICharacter character) + { + if ((character.StatusFlags & StatusFlags.PartyMember) != 0 || + (character.StatusFlags & StatusFlags.AllianceMember) != 0 || + (character.StatusFlags & StatusFlags.Friend) != 0) + { + return null; // Hide party, alliance, and friend nameplates + } + } + // Other players: show only hostile (enemy team), hide allies + if (data.GameObject == null) { return null; } + return Utils.IsHostile(data.GameObject) ? _enemyHud : null; + } + + if (data.GameObject is ICharacter character2) + { + if ((character2.StatusFlags & StatusFlags.PartyMember) != 0) // PartyMember { return _partyMemberHud; } - else if ((character.StatusFlags & StatusFlags.AllianceMember) != 0) // AllianceMember + else if ((character2.StatusFlags & StatusFlags.AllianceMember) != 0) // AllianceMember { return _allianceMemberHud; } - else if ((character.StatusFlags & StatusFlags.Friend) != 0) // Friend + else if ((character2.StatusFlags & StatusFlags.Friend) != 0) // Friend { return _friendsHud; } diff --git a/Interface/Party/AllianceManager.cs b/Interface/Party/AllianceManager.cs index 4665d69..b80b968 100644 --- a/Interface/Party/AllianceManager.cs +++ b/Interface/Party/AllianceManager.cs @@ -145,7 +145,7 @@ namespace HSUI.Interface.Party return false; } - /// Gets the alliance letter (A/B/C) for an internal group index. Uses member EntityId matching to CrossRealm when GroupManager/CrossRealm use different orderings. + /// Gets the alliance letter (A/B/C) for an internal group index. GroupManager ordering can differ from game display; use LocalPlayerGroupIndex to map when CrossRealm member data is unavailable. public bool TryGetAllianceLetter(int allianceIndex, out string letter) { letter = ""; @@ -154,53 +154,72 @@ namespace HSUI.Interface.Party var info = InfoProxyCrossRealm.Instance(); if (info != null && info->GroupCount >= 3) { - // Get first member EntityId from our group (works for both GroupManager and CrossRealm data) - uint matchEntityId = 0; - if (_allianceMembers[allianceIndex].Count > 0) - matchEntityId = _allianceMembers[allianceIndex][0].ObjectId; - - // Find which CrossRealm group contains this member - they may use different ordering than GroupManager - for (int crIdx = 0; crIdx < 3 && matchEntityId != 0; crIdx++) + // Check if CrossRealm has member data - when empty, we're in-instance using GroupManager (indices differ from InfoProxy) + bool crossRealmHasData = false; + for (int i = 0; i < 3; i++) { - var crGroup = info->CrossRealmGroups[crIdx]; - for (int j = 0; j < crGroup.GroupMemberCount; j++) + if (info->CrossRealmGroups[i].GroupMemberCount > 0) { - if (crGroup.GroupMembers[j].EntityId == matchEntityId) + crossRealmHasData = true; + break; + } + } + + if (crossRealmHasData) + { + // CrossRealm populated: use EntityId lookup or direct lookup + uint matchEntityId = 0; + if (_allianceMembers[allianceIndex].Count > 0) + matchEntityId = _allianceMembers[allianceIndex][0].ObjectId; + + if (matchEntityId != 0) + { + for (int crIdx = 0; crIdx < 3; crIdx++) { - byte displayIdx = crGroup.GroupMembers[j].GroupIndex; - if (displayIdx < 3) + var crGroup = info->CrossRealmGroups[crIdx]; + for (int j = 0; j < crGroup.GroupMemberCount; j++) { - letter = ((char)('A' + displayIdx)).ToString(); - return true; + if (crGroup.GroupMembers[j].EntityId == matchEntityId) + { + byte displayIdx = crGroup.GroupMembers[j].GroupIndex; + if (displayIdx < 3) + { + letter = ((char)('A' + displayIdx)).ToString(); + return true; + } + } } } } + + var group = info->CrossRealmGroups[allianceIndex]; + if (group.GroupMemberCount > 0) + { + byte displayIdx = group.GroupMembers[0].GroupIndex; + if (displayIdx < 3) + { + letter = ((char)('A' + displayIdx)).ToString(); + return true; + } + } + + for (byte d = 0; d < 3; d++) + { + if (InfoProxyCrossRealm.GetGroupIndex(d) == allianceIndex) + { + letter = ((char)('A' + d)).ToString(); + return true; + } + } } - // Direct lookup when CrossRealm index matches ours (e.g. both use same ordering) - var group = info->CrossRealmGroups[allianceIndex]; - if (group.GroupMemberCount > 0) - { - byte displayIdx = group.GroupMembers[0].GroupIndex; - if (displayIdx < 3) - { - letter = ((char)('A' + displayIdx)).ToString(); - return true; - } - } - - // GetGroupIndex(displayLetter) returns internal index - find which letter maps to our index - for (byte d = 0; d < 3; d++) - { - if (InfoProxyCrossRealm.GetGroupIndex(d) == allianceIndex) - { - letter = ((char)('A' + d)).ToString(); - return true; - } - } + // CrossRealm empty (in-instance with GroupManager): use direct GM-index mapping. + // GroupManager alliance index 0=A, 1=B, 2=C in the game's party list display order. + letter = ((char)('A' + allianceIndex)).ToString(); + return true; } - // Fallback when no CrossRealm data (PvP, in-instance) + // Fallback when no CrossRealm/InfoProxy data (PvP edge cases) letter = ((char)('A' + allianceIndex)).ToString(); return true; } @@ -265,6 +284,7 @@ namespace HSUI.Interface.Party { int count = 0; var list = new List(); + for (int slot = 0; slot < 8; slot++) { var pm = mainGroup.GetAllianceMemberByGroupAndIndex(allianceIdx, slot); @@ -283,6 +303,31 @@ namespace HSUI.Interface.Party list.Add(pfMember); count++; } + + // GroupManager stores our party in _partyMembers, not GetAllianceMemberByGroupAndIndex. + // When group 2 is empty, it's our alliance—populate from GetPartyMemberByIndex. + if (count == 0 && allianceIdx == 2) + { + for (int slot = 0; slot < 8; slot++) + { + var pm = mainGroup.GetPartyMemberByIndex(slot); + if (pm == null || pm->EntityId == 0) continue; + var pfMember = new PartyFramesMember( + pm->EntityId, + count, + count, + EnmityLevel.Last, + PartyMemberStatus.None, + ReadyCheckStatus.None, + false, + false + ); + pfMember.Update(EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false, pm->ClassJob); + list.Add(pfMember); + count++; + } + } + if (count != _lastMemberCounts[allianceIdx]) { _allianceMembers[allianceIdx].Clear(); @@ -295,7 +340,9 @@ namespace HSUI.Interface.Party int ourIdx = 0; for (int slot = 0; slot < 8 && ourIdx < _allianceMembers[allianceIdx].Count; slot++) { - var pm = mainGroup.GetAllianceMemberByGroupAndIndex(allianceIdx, slot); + PartyMember* pm = allianceIdx == 2 + ? mainGroup.GetPartyMemberByIndex(slot) + : mainGroup.GetAllianceMemberByGroupAndIndex(allianceIdx, slot); if (pm == null || pm->EntityId == 0) continue; if (_allianceMembers[allianceIdx][ourIdx] is PartyFramesMember pfMember) pfMember.Update(EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false, pm->ClassJob); @@ -372,6 +419,34 @@ namespace HSUI.Interface.Party else Plugin.Logger.Information($"[Alliance] Display slot {slot}: no data"); } + + // Letter-resolution path debug: which code path is used and why + bool crossRealmHasData = false; + if (info != null && info->GroupCount >= 3) + { + for (int i = 0; i < 3; i++) + { + if (info->CrossRealmGroups[i].GroupMemberCount > 0) + { + crossRealmHasData = true; + break; + } + } + } + Plugin.Logger.Information($"[Alliance] crossRealmHasData={crossRealmHasData} → {(crossRealmHasData ? "EntityId/GetGroupIndex path" : "LocalPlayerGroupIndex path")}"); + + int dbgPlayerIdx = inst.PlayerAllianceIndex; + byte dbgLocalDisplayIdx = info != null ? info->LocalPlayerGroupIndex : (byte)255; + Plugin.Logger.Information($"[Alliance] LocalPlayerGroupIndex mapping: playerIdx={dbgPlayerIdx} localDisplayIdx={dbgLocalDisplayIdx} (0=A,1=B,2=C for our alliance)"); + Plugin.Logger.Information($"[Alliance] Formula: displayIndex = (gmIndex - playerIdx + localDisplayIdx + 3) % 3"); + for (int i = 0; i < 3; i++) + { + int displayIndex = (i - dbgPlayerIdx + dbgLocalDisplayIdx + 3) % 3; + char computedLetter = displayIndex >= 0 && displayIndex < 3 ? (char)('A' + displayIndex) : '?'; + string ours = i == dbgPlayerIdx ? " (OURS)" : ""; + Plugin.Logger.Information($"[Alliance] GM[{i}] → displayIndex=({i}-{dbgPlayerIdx}+{dbgLocalDisplayIdx}+3)%3={displayIndex} → letter '{computedLetter}'{ours}"); + } + Plugin.Logger.Information("=== End Alliance Debug ==="); }