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 ===");
}