PvP nameplates: correct GC icons (62601/62602/62603), configurable GC IDs, Role/Job icon for enemy players
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1327,6 +1327,34 @@ namespace HSUI.Interface.GeneralElements
|
||||
}
|
||||
}
|
||||
|
||||
if (slot.SlotType == RaptureHotbarModule.HotbarSlotType.MainCommand ||
|
||||
slot.SlotType == RaptureHotbarModule.HotbarSlotType.ExtraCommand)
|
||||
{
|
||||
if (Plugin.DataManager.GetExcelSheet<MainCommand>()?.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(), "");
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,29 @@ namespace HSUI.Interface.GeneralElements
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shows Grand Company icon (Maelstrom, Flames, or Adders) on enemy player nameplates in PvP Frontline.</summary>
|
||||
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() { }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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!;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ namespace HSUI.Interface.Party
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Gets the alliance letter (A/B/C) for an internal group index. Uses member EntityId matching to CrossRealm when GroupManager/CrossRealm use different orderings.</summary>
|
||||
/// <summary>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.</summary>
|
||||
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<IPartyFramesMember>();
|
||||
|
||||
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 ===");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user