Files
HSUI/Interface/Party/AllianceManager.cs
T

423 lines
18 KiB
C#

using Dalamud.Game.ClientState.Objects.Types;
using HSUI.Helpers;
using FFXIVClientStructs.FFXIV.Client.Game.Group;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using System;
using System.Collections.Generic;
using System.Linq;
namespace HSUI.Interface.Party
{
public delegate void AllianceMembersChangedEventHandler(AllianceManager sender);
/// <summary>
/// Provides member lists for Alliance A, B, and C when in 24-player raid content.
/// Only populated when CrossRealm has 3 groups (alliance raid).
/// </summary>
public unsafe class AllianceManager : IDisposable
{
public static AllianceManager Instance { get; private set; } = null!;
private readonly List<IPartyFramesMember>[] _allianceMembers = new List<IPartyFramesMember>[3];
private int[] _lastMemberCounts = new int[3];
private bool _wasInAllianceRaid = false;
private AllianceManager()
{
for (int i = 0; i < 3; i++)
_allianceMembers[i] = new List<IPartyFramesMember>();
}
public static void Initialize()
{
Instance = new AllianceManager();
}
public void Dispose()
{
for (int i = 0; i < 3; i++)
_allianceMembers[i].Clear();
Instance = null!;
}
/// <summary>Internal alliance index (0, 1, or 2) containing the local player, or -1 if not in alliance raid / unknown.</summary>
public int PlayerAllianceIndex
{
get
{
var player = Plugin.ObjectTable?.LocalPlayer;
if (player == null) return -1;
uint playerEntityId = player.EntityId;
ulong playerObjectId = (ulong)player.GameObjectId;
// Primary: Check our populated member lists first - most reliable (GroupManager/CrossRealm ordering may differ)
for (int i = 0; i < 3; i++)
{
foreach (var m in _allianceMembers[i])
{
if (m.ObjectId == playerEntityId || (ulong)m.ObjectId == playerObjectId) return i;
if (m.Character != null && m.Character.Address == player.Address) return i;
}
}
// Fallback: InfoProxyCrossRealm.LocalPlayerGroupIndex (when in duty finder / cross-world before instance)
var info = InfoProxyCrossRealm.Instance();
if (info != null && info->IsInAllianceRaid && info->GroupCount >= 3)
{
byte localIdx = info->LocalPlayerGroupIndex;
if (localIdx < 3) return localIdx;
}
// Fallback: CrossRealm iteration
if (info != null && info->IsInAllianceRaid && info->GroupCount >= 3)
{
for (int i = 0; i < 3; i++)
{
var group = info->CrossRealmGroups[i];
for (int j = 0; j < group.GroupMemberCount; j++)
{
var member = group.GroupMembers[j];
if (member.EntityId == playerEntityId) return i;
}
}
}
// Fallback 3: Match any of our party members - we're in the same alliance as them
var partyList = Plugin.PartyList;
if (partyList != null)
{
for (int i = 0; i < 3; i++)
{
foreach (var m in _allianceMembers[i])
{
foreach (var pm in partyList)
{
if (pm != null && pm.EntityId == m.ObjectId)
{
return i;
}
}
}
}
}
// Fallback: GroupManager - player is in MainGroup._partyMembers (their 8-man).
// Group layout: 0,1 = other alliances from _allianceMembers; 2 = main party from _partyMembers.
var gm = GroupManager.Instance();
if (gm != null && gm->MainGroup.IsAlliance)
{
ref var mainGroup = ref gm->MainGroup;
if (mainGroup.GetPartyMemberByEntityId(playerEntityId) != null)
{
return 2; // Player in main party = group 2 (our alliance)
}
}
return -1;
}
}
/// <summary>Slot 0 = first other alliance, slot 1 = second. Excludes the player's alliance.</summary>
public IReadOnlyList<IPartyFramesMember> GetMembersForOtherAlliance(int slotIndex)
{
if (slotIndex < 0 || slotIndex > 1) return Array.Empty<IPartyFramesMember>();
if (!TryGetAllianceIndexForSlot(slotIndex, out int allianceIdx)) return Array.Empty<IPartyFramesMember>();
return _allianceMembers[allianceIdx].AsReadOnly();
}
/// <summary>Gets the alliance index (0=A, 1=B, 2=C) for the given slot. Returns false when not in raid.</summary>
public bool TryGetAllianceIndexForSlot(int slotIndex, out int allianceIndex)
{
allianceIndex = -1;
if (slotIndex < 0 || slotIndex > 1) return false;
int playerIdx = PlayerAllianceIndex;
int idx = 0;
for (int i = 0; i < 3; i++)
{
if (i == playerIdx) continue;
if (idx == slotIndex)
{
allianceIndex = i;
return true;
}
idx++;
}
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>
public bool TryGetAllianceLetter(int allianceIndex, out string letter)
{
letter = "";
if (allianceIndex < 0 || allianceIndex > 2) return false;
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++)
{
var crGroup = info->CrossRealmGroups[crIdx];
for (int j = 0; j < crGroup.GroupMemberCount; j++)
{
if (crGroup.GroupMembers[j].EntityId == matchEntityId)
{
byte displayIdx = crGroup.GroupMembers[j].GroupIndex;
if (displayIdx < 3)
{
letter = ((char)('A' + displayIdx)).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;
}
}
}
// Fallback when no CrossRealm data (PvP, in-instance)
letter = ((char)('A' + allianceIndex)).ToString();
return true;
}
/// <summary>True when in 24-player content with 3 alliances (PvE) or PvP Frontlines/Rival Wings.</summary>
public bool IsInAllianceRaid
{
get
{
var gm = GroupManager.Instance();
if (gm != null)
{
// GroupManager: IsAlliance = 3x8 (alliance raids, Frontlines), IsSmallGroupAlliance = 6x4 (Rival Wings)
if (gm->MainGroup.IsAlliance || gm->MainGroup.IsSmallGroupAlliance)
return true;
}
var info = InfoProxyCrossRealm.Instance();
if (info == null) return false;
if (info->IsInAllianceRaid) return true;
return info->IsCrossRealm && info->GroupCount >= 3;
}
}
public event AllianceMembersChangedEventHandler? MembersChangedEvent;
public void Update()
{
var gm = GroupManager.Instance();
// Use GroupManager for PvE alliance raids and PvP (Frontlines, Rival Wings) - CrossRealm is not populated in PvP
bool useGroupManager = gm != null && (gm->MainGroup.IsAlliance || gm->MainGroup.IsSmallGroupAlliance);
if (useGroupManager)
{
UpdateFromGroupManager(gm);
return;
}
var info = InfoProxyCrossRealm.Instance();
if (info == null || !info->IsInAllianceRaid || info->GroupCount < 3)
{
if (_wasInAllianceRaid)
{
_wasInAllianceRaid = false;
for (int i = 0; i < 3; i++)
_allianceMembers[i].Clear();
MembersChangedEvent?.Invoke(this);
}
return;
}
_wasInAllianceRaid = true;
UpdateFromCrossRealm(info);
}
private void UpdateFromGroupManager(GroupManager* gm)
{
_wasInAllianceRaid = true;
bool anyChanged = false;
ref var mainGroup = ref gm->MainGroup;
for (int allianceIdx = 0; allianceIdx < 3; allianceIdx++)
{
int count = 0;
var list = new List<IPartyFramesMember>();
for (int slot = 0; slot < 8; slot++)
{
var pm = mainGroup.GetAllianceMemberByGroupAndIndex(allianceIdx, 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();
_allianceMembers[allianceIdx].AddRange(list);
_lastMemberCounts[allianceIdx] = count;
anyChanged = true;
}
else
{
int ourIdx = 0;
for (int slot = 0; slot < 8 && ourIdx < _allianceMembers[allianceIdx].Count; slot++)
{
var pm = 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);
ourIdx++;
}
}
}
if (anyChanged)
MembersChangedEvent?.Invoke(this);
}
/// <summary>Dumps alliance detection and letter-mapping debug info to the log. Run /hsui debug alliance while in an alliance raid.</summary>
public static void DumpAllianceDebugToLog()
{
var inst = Instance;
if (inst == null) { Plugin.Logger.Information("[HSUI Alliance DBG] AllianceManager not initialized."); return; }
var player = Plugin.ObjectTable?.LocalPlayer;
uint playerEntityId = player?.EntityId ?? 0;
string playerName = player?.Name.ToString() ?? "(null)";
Plugin.Logger.Information("=== HSUI Alliance Debug ===");
Plugin.Logger.Information($"[Alliance] IsInAllianceRaid={inst.IsInAllianceRaid}");
Plugin.Logger.Information($"[Alliance] Player: EntityId={playerEntityId} Name={playerName}");
var gm = GroupManager.Instance();
bool useGM = gm != null && (gm->MainGroup.IsAlliance || gm->MainGroup.IsSmallGroupAlliance);
Plugin.Logger.Information($"[Alliance] Data source: GroupManager={useGM} (IsAlliance={gm != null && gm->MainGroup.IsAlliance}, IsSmallGroupAlliance={gm != null && gm->MainGroup.IsSmallGroupAlliance})");
var info = InfoProxyCrossRealm.Instance();
if (info != null)
{
Plugin.Logger.Information($"[Alliance] InfoProxyCrossRealm: GroupCount={info->GroupCount} IsInAllianceRaid={info->IsInAllianceRaid} LocalPlayerGroupIndex={info->LocalPlayerGroupIndex}");
if (info->GroupCount >= 3)
{
for (byte d = 0; d < 3; d++)
Plugin.Logger.Information($"[Alliance] GetGroupIndex({d}) => internal index {InfoProxyCrossRealm.GetGroupIndex(d)}");
}
}
else
Plugin.Logger.Information("[Alliance] InfoProxyCrossRealm: null");
int playerIdx = inst.PlayerAllianceIndex;
Plugin.Logger.Information($"[Alliance] PlayerAllianceIndex={playerIdx} (internal index we treat as 'our' alliance, will exclude from display)");
for (int i = 0; i < 3; i++)
{
int count = inst._allianceMembers[i].Count;
string firstEntityId = count > 0 ? inst._allianceMembers[i][0].ObjectId.ToString() : "none";
string firstName = count > 0 ? inst._allianceMembers[i][0].Name : "";
bool hasPlayer = playerEntityId != 0 && inst._allianceMembers[i].Any(m => m.ObjectId == playerEntityId);
string letter = inst.TryGetAllianceLetter(i, out string l) ? l : "?";
Plugin.Logger.Information($"[Alliance] _allianceMembers[{i}]: count={count} firstEntityId={firstEntityId} firstName={firstName} hasPlayer={hasPlayer} resolvedLetter={letter}");
}
if (info != null && info->GroupCount >= 3)
{
for (int crIdx = 0; crIdx < 3; crIdx++)
{
var grp = info->CrossRealmGroups[crIdx];
int gc = grp.GroupMemberCount;
string crFirstEntity = gc > 0 ? grp.GroupMembers[0].EntityId.ToString() : "none";
string crFirstName = gc > 0 ? grp.GroupMembers[0].NameString : "";
byte crGroupIndex = gc > 0 ? grp.GroupMembers[0].GroupIndex : (byte)255;
char crLetter = crGroupIndex < 3 ? (char)('A' + crGroupIndex) : '?';
Plugin.Logger.Information($"[Alliance] CrossRealmGroups[{crIdx}]: count={gc} firstEntityId={crFirstEntity} firstName={crFirstName} GroupIndex={crGroupIndex} => letter '{crLetter}'");
}
}
for (int slot = 0; slot <= 1; slot++)
{
if (inst.TryGetAllianceIndexForSlot(slot, out int aidx) && inst.TryGetAllianceLetter(aidx, out string let))
Plugin.Logger.Information($"[Alliance] Display slot {slot}: allianceIndex={aidx} letter={let}");
else
Plugin.Logger.Information($"[Alliance] Display slot {slot}: no data");
}
Plugin.Logger.Information("=== End Alliance Debug ===");
}
private void UpdateFromCrossRealm(InfoProxyCrossRealm* info)
{
bool anyChanged = false;
for (int allianceIdx = 0; allianceIdx < 3; allianceIdx++)
{
var group = info->CrossRealmGroups[allianceIdx];
int count = group.GroupMemberCount;
if (count != _lastMemberCounts[allianceIdx])
{
_allianceMembers[allianceIdx].Clear();
_lastMemberCounts[allianceIdx] = count;
for (int i = 0; i < count; i++)
{
CrossRealmMember member = group.GroupMembers[i];
var partyMember = new PartyFramesMember(
member,
i,
i,
PartyMemberStatus.None,
ReadyCheckStatus.None,
member.IsPartyLeader,
false
);
_allianceMembers[allianceIdx].Add(partyMember);
}
anyChanged = true;
}
else
{
for (int i = 0; i < _allianceMembers[allianceIdx].Count; i++)
{
if (_allianceMembers[allianceIdx][i] is PartyFramesMember pfMember)
{
pfMember.Update(EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, pfMember.IsPartyLeader, pfMember.JobId);
}
}
}
}
if (anyChanged)
MembersChangedEvent?.Invoke(this);
}
}
}