f37369cdda
Co-authored-by: Cursor <cursoragent@cursor.com>
806 lines
29 KiB
C#
806 lines
29 KiB
C#
using Dalamud.Game.ClientState.Objects.Enums;
|
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
using Dalamud.Game.ClientState.Objects.Types;
|
|
using Dalamud.Memory;
|
|
using HSUI.Config;
|
|
using HSUI.Helpers;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Group;
|
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
|
using Lumina.Excel.Sheets;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
|
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
using static FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
|
|
using DalamudPartyMember = Dalamud.Game.ClientState.Party.IPartyMember;
|
|
using StructsPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
|
|
|
|
namespace HSUI.Interface.Party
|
|
{
|
|
public delegate void PartyMembersChangedEventHandler(PartyManager sender);
|
|
|
|
public unsafe class PartyManager : IDisposable
|
|
{
|
|
#region Singleton
|
|
public static PartyManager Instance { get; private set; } = null!;
|
|
private PartyFramesConfig _config = null!;
|
|
private PartyFramesIconsConfig _iconsConfig = null!;
|
|
|
|
private PartyManager()
|
|
{
|
|
_readyCheckHelper = new PartyReadyCheckHelper();
|
|
_raiseTracker = new PartyFramesRaiseTracker();
|
|
_invulnTracker = new PartyFramesInvulnTracker();
|
|
_cleanseTracker = new PartyFramesCleanseTracker();
|
|
|
|
ConfigurationManager.Instance.ResetEvent += OnConfigReset;
|
|
|
|
OnConfigReset(ConfigurationManager.Instance);
|
|
UpdatePreview();
|
|
|
|
// find offline string for active language
|
|
if (Plugin.DataManager.GetExcelSheet<Addon>().TryGetRow(9836, out Addon row))
|
|
{
|
|
_offlineString = row.Text.ToString();
|
|
}
|
|
}
|
|
|
|
public static void Initialize()
|
|
{
|
|
Instance = new PartyManager();
|
|
}
|
|
|
|
~PartyManager()
|
|
{
|
|
Dispose(false);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
protected void Dispose(bool disposing)
|
|
{
|
|
if (!disposing)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_readyCheckHelper.Dispose();
|
|
_raiseTracker.Dispose();
|
|
_invulnTracker.Dispose();
|
|
_cleanseTracker.Dispose();
|
|
|
|
_config.ValueChangeEvent -= OnConfigPropertyChanged;
|
|
|
|
Instance = null!;
|
|
}
|
|
|
|
private void OnConfigReset(ConfigurationManager sender)
|
|
{
|
|
if (_config != null)
|
|
{
|
|
_config.ValueChangeEvent -= OnConfigPropertyChanged;
|
|
}
|
|
|
|
_config = sender.GetConfigObject<PartyFramesConfig>();
|
|
_config.ValueChangeEvent += OnConfigPropertyChanged;
|
|
|
|
_iconsConfig = ConfigurationManager.Instance.GetConfigObject<PartyFramesIconsConfig>();
|
|
}
|
|
|
|
#endregion Singleton
|
|
|
|
public AddonPartyList* PartyListAddon { get; private set; } = null;
|
|
public IntPtr HudAgent { get; private set; } = IntPtr.Zero;
|
|
|
|
private RaptureAtkModule* _raptureAtkModule = null;
|
|
|
|
private const int PartyListInfoOffset = 0x0D40;
|
|
private const int PartyListMemberRawInfoSize = 0x28;
|
|
|
|
private const int PartyMembersInfoIndex = 12; // TODO: Should be reworked to use PartyMemberListStringArray.Instance()
|
|
|
|
private List<IPartyFramesMember> _groupMembers = new List<IPartyFramesMember>();
|
|
public IReadOnlyCollection<IPartyFramesMember> GroupMembers => _groupMembers.AsReadOnly();
|
|
|
|
private List<IPartyFramesMember> _sortedGroupMembers = new List<IPartyFramesMember>();
|
|
public IReadOnlyCollection<IPartyFramesMember> SortedGroupMembers => _sortedGroupMembers.AsReadOnly();
|
|
|
|
public uint MemberCount => (uint)_groupMembers.Count;
|
|
|
|
private string? _partyTitle = null;
|
|
public string PartyTitle => _partyTitle ?? "";
|
|
|
|
private int _groupMemberCount => GroupManager.Instance()->MainGroup.MemberCount;
|
|
private int _realMemberCount => PartyListAddon != null ? PartyListAddon->MemberCount : Plugin.PartyList.Length;
|
|
private int _realMemberAndChocoboCount => PartyListAddon != null ? PartyListAddon->MemberCount + Math.Max(1, (int)PartyListAddon->ChocoboCount) : Plugin.PartyList.Length;
|
|
|
|
private Dictionary<string, InternalMemberData> _prevDataMap = new();
|
|
|
|
private bool _wasRealGroup = false;
|
|
private bool _wasCrossWorld = false;
|
|
|
|
private InfoProxyCrossRealm* _crossRealmInfo => InfoProxyCrossRealm.Instance();
|
|
private Group _mainGroup => GroupManager.Instance()->MainGroup;
|
|
|
|
private string _offlineString = "offline";
|
|
|
|
private PartyReadyCheckHelper _readyCheckHelper;
|
|
private PartyFramesRaiseTracker _raiseTracker;
|
|
private PartyFramesInvulnTracker _invulnTracker;
|
|
private PartyFramesCleanseTracker _cleanseTracker;
|
|
|
|
public event PartyMembersChangedEventHandler? MembersChangedEvent;
|
|
|
|
public bool Previewing => _config.Preview;
|
|
|
|
public bool IsSoloParty()
|
|
{
|
|
if (!_config.ShowWhenSolo) { return false; }
|
|
|
|
return _groupMembers.Count <= 1 ||
|
|
(_groupMembers.Count == 2 && _config.ShowChocobo &&
|
|
_groupMembers[1].Character is IBattleNpc npc && npc.BattleNpcKind == BattleNpcSubKind.Chocobo);
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
// find party list hud agent
|
|
PartyListAddon = (AddonPartyList*)Plugin.GameGui.GetAddonByName("_PartyList", 1).Address;
|
|
HudAgent = Plugin.GameGui.FindAgentInterface(PartyListAddon);
|
|
|
|
if (PartyListAddon == null || HudAgent == IntPtr.Zero)
|
|
{
|
|
if (_groupMembers.Count > 0)
|
|
{
|
|
_groupMembers.Clear();
|
|
MembersChangedEvent?.Invoke(this);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
_raptureAtkModule = RaptureAtkModule.Instance();
|
|
|
|
// no need to update on preview mode
|
|
if (_config.Preview)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InternalUpdate();
|
|
}
|
|
|
|
private void InternalUpdate()
|
|
{
|
|
IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
|
|
if (player is null || player is not IPlayerCharacter)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool isCrossWorld = IsCrossWorldParty();
|
|
|
|
// ready check update
|
|
if (_iconsConfig.ReadyCheckStatus.Enabled)
|
|
{
|
|
_readyCheckHelper.Update(_iconsConfig.ReadyCheckStatus.Duration);
|
|
}
|
|
|
|
try
|
|
{
|
|
// title
|
|
_partyTitle = GetPartyListTitle();
|
|
|
|
// solo
|
|
if (_realMemberCount <= 1 && PartyListAddon->TrustCount == 0)
|
|
{
|
|
if (_config.ShowWhenSolo)
|
|
{
|
|
UpdateSoloParty(player);
|
|
}
|
|
else if (_groupMembers.Count > 0)
|
|
{
|
|
_groupMembers.Clear();
|
|
MembersChangedEvent?.Invoke(this);
|
|
}
|
|
|
|
_wasRealGroup = false;
|
|
}
|
|
else
|
|
{
|
|
// player maps
|
|
Dictionary<string, InternalMemberData> dataMap = GetMembersDataMap(player, isCrossWorld);
|
|
bool partyChanged = _prevDataMap.Count != dataMap.Count;
|
|
|
|
if (!partyChanged)
|
|
{
|
|
foreach (string key in dataMap.Keys)
|
|
{
|
|
InternalMemberData newData = dataMap[key];
|
|
if (!_prevDataMap.TryGetValue(key, out InternalMemberData? oldData) ||
|
|
oldData == null ||
|
|
newData.Order != oldData.Order)
|
|
{
|
|
partyChanged = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
_prevDataMap = dataMap;
|
|
|
|
// trust
|
|
if (PartyListAddon->TrustCount > 0)
|
|
{
|
|
UpdateTrustParty(player, dataMap, partyChanged);
|
|
}
|
|
// cross world party/alliance
|
|
else if (isCrossWorld)
|
|
{
|
|
UpdateCrossWorldParty(player, dataMap, partyChanged);
|
|
}
|
|
// regular party
|
|
else
|
|
{
|
|
UpdateRegularParty(player, dataMap, partyChanged);
|
|
}
|
|
|
|
_wasRealGroup = true;
|
|
}
|
|
|
|
UpdateTrackers();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Plugin.Logger.Warning(e.Message);
|
|
}
|
|
|
|
_wasCrossWorld = isCrossWorld;
|
|
}
|
|
|
|
private Dictionary<string, InternalMemberData> GetMembersDataMap(IPlayerCharacter player, bool isCrossWorld)
|
|
{
|
|
Dictionary<string, InternalMemberData> dataMap = new Dictionary<string, InternalMemberData>();
|
|
|
|
if (_raptureAtkModule == null || _raptureAtkModule->AtkModule.AtkArrayDataHolder.StringArrayCount <= PartyMembersInfoIndex)
|
|
{
|
|
return dataMap;
|
|
}
|
|
|
|
// raw info
|
|
int allianceNum = FindAlliance(player);
|
|
int count = isCrossWorld ? _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMemberCount : _realMemberCount + PartyListAddon->TrustCount;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
InternalMemberData data = new InternalMemberData();
|
|
data.Index = i;
|
|
|
|
if (!isCrossWorld)
|
|
{
|
|
PartyListMemberRawInfo* info = (PartyListMemberRawInfo*)(HudAgent + (PartyListInfoOffset + PartyListMemberRawInfoSize * i));
|
|
data.ObjectId = info->ObjectId;
|
|
data.ContentId = info->ContentId;
|
|
data.Name = info->Name;
|
|
data.Order = info->Order;
|
|
}
|
|
else
|
|
{
|
|
CrossRealmMember member = _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMembers[i];
|
|
data.ObjectId = member.EntityId;
|
|
data.ContentId = (long)member.ContentId;
|
|
data.Name = member.NameString;
|
|
data.Order = i;
|
|
}
|
|
|
|
if (!dataMap.ContainsKey(data.Name))
|
|
{
|
|
dataMap.Add(data.Name, data);
|
|
}
|
|
}
|
|
|
|
// status string
|
|
var stringArrayData = _raptureAtkModule->AtkModule.AtkArrayDataHolder.StringArrays[PartyMembersInfoIndex];
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int index = i * 5;
|
|
if (stringArrayData->AtkArrayData.Size <= index + 3 ||
|
|
stringArrayData->StringArray[index] == null ||
|
|
stringArrayData->StringArray[index + 3] == null) { break; }
|
|
|
|
IntPtr ptr = new IntPtr(stringArrayData->StringArray[index]);
|
|
string name = MemoryHelper.ReadSeStringNullTerminated(ptr).ToString();
|
|
|
|
ptr = new IntPtr(stringArrayData->StringArray[index + 3]);
|
|
|
|
string a = MemoryHelper.ReadSeStringNullTerminated(ptr).ToString();
|
|
|
|
|
|
if (dataMap.TryGetValue(name, out InternalMemberData? data) && data != null)
|
|
{
|
|
data.Status = MemoryHelper.ReadSeStringNullTerminated(ptr).ToString();
|
|
}
|
|
}
|
|
|
|
return dataMap;
|
|
}
|
|
|
|
private bool IsCrossWorldParty()
|
|
{
|
|
return _crossRealmInfo->IsCrossRealm && _crossRealmInfo->GroupCount > 0 && _mainGroup.MemberCount == 0;
|
|
}
|
|
|
|
private ReadyCheckStatus GetReadyCheckStatus(ulong contentId)
|
|
{
|
|
return _readyCheckHelper.GetStatusForContentId(contentId);
|
|
}
|
|
|
|
private void UpdateTrustParty(IPlayerCharacter player, Dictionary<string, InternalMemberData> dataMap, bool forced)
|
|
{
|
|
bool softUpdate = true;
|
|
|
|
if (_groupMembers.Count != dataMap.Count || forced)
|
|
{
|
|
_groupMembers.Clear();
|
|
softUpdate = false;
|
|
}
|
|
|
|
if (softUpdate)
|
|
{
|
|
foreach (IPartyFramesMember member in _groupMembers)
|
|
{
|
|
if (member.ObjectId == player.GameObjectId)
|
|
{
|
|
member.Update(EnmityForIndex(member.Index), PartyMemberStatus.None, ReadyCheckStatus.None, true, player.ClassJob.RowId);
|
|
}
|
|
else
|
|
{
|
|
member.Update(EnmityForTrustMemberIndex(member.Index - 1), PartyMemberStatus.None, ReadyCheckStatus.None, false, 0);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string[] keys = dataMap.Keys.ToArray();
|
|
for (int i = 0; i < keys.Length; i++)
|
|
{
|
|
InternalMemberData data = dataMap[keys[i]];
|
|
if (keys[i] == player.Name.ToString())
|
|
{
|
|
PartyFramesMember playerMember = new PartyFramesMember(player, data.Index, data.Order, EnmityForIndex(i), PartyMemberStatus.None, ReadyCheckStatus.None, true);
|
|
_groupMembers.Add(playerMember);
|
|
}
|
|
else
|
|
{
|
|
ICharacter? trustChara = Utils.GetGameObjectByName(keys[i]) as ICharacter;
|
|
if (trustChara != null)
|
|
{
|
|
_groupMembers.Add(new PartyFramesMember(trustChara, data.Index, data.Order, EnmityForTrustMemberIndex(i), PartyMemberStatus.None, ReadyCheckStatus.None, false));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!softUpdate)
|
|
{
|
|
SortGroupMembers(player);
|
|
MembersChangedEvent?.Invoke(this);
|
|
}
|
|
}
|
|
|
|
private void UpdateSoloParty(IPlayerCharacter player)
|
|
{
|
|
ICharacter? chocobo = null;
|
|
if (_config.ShowChocobo)
|
|
{
|
|
var gameObject = Utils.GetBattleChocobo(player);
|
|
if (gameObject != null && gameObject is ICharacter)
|
|
{
|
|
chocobo = (ICharacter)gameObject;
|
|
}
|
|
}
|
|
|
|
bool needsUpdate =
|
|
_groupMembers.Count == 0 ||
|
|
(_groupMembers.Count != 2 && _config.ShowChocobo && chocobo != null) ||
|
|
(_groupMembers.Count > 1 && !_config.ShowChocobo) ||
|
|
(_groupMembers.Count > 1 && chocobo == null) ||
|
|
(_groupMembers.Count == 2 && _config.ShowChocobo && _groupMembers[1].ObjectId != chocobo?.EntityId);
|
|
|
|
EnmityLevel playerEnmity = PartyListAddon->EnmityLeaderIndex == 0 ? EnmityLevel.Leader : EnmityLevel.Last;
|
|
|
|
// for some reason chocobos never get a proper enmity value even though they have aggro
|
|
// if the player enmity is set to first, but the "leader index" is invalid
|
|
// we can pretty much deduce that the chocobo is the one with aggro
|
|
// this might fail on some cases when there are other players not in party hitting the same thing
|
|
// but the edge case is so minor we should be fine
|
|
EnmityLevel chocoboEnmity = PartyListAddon->EnmityLeaderIndex == -1 && PartyListAddon->PartyMembers[0].EmnityByte == 1 ? EnmityLevel.Leader : EnmityLevel.Last;
|
|
|
|
if (needsUpdate)
|
|
{
|
|
_groupMembers.Clear();
|
|
|
|
_groupMembers.Add(new PartyFramesMember(player, 0, 0, playerEnmity, PartyMemberStatus.None, ReadyCheckStatus.None, true));
|
|
|
|
if (chocobo != null)
|
|
{
|
|
_groupMembers.Add(new PartyFramesMember(chocobo, 1, 1, chocoboEnmity, PartyMemberStatus.None, ReadyCheckStatus.None, false));
|
|
}
|
|
|
|
SortGroupMembers(player);
|
|
MembersChangedEvent?.Invoke(this);
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < _groupMembers.Count; i++)
|
|
{
|
|
_groupMembers[i].Update(i == 0 ? playerEnmity : chocoboEnmity, PartyMemberStatus.None, ReadyCheckStatus.None, i == 0, i == 0 ? player.ClassJob.RowId : 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateCrossWorldParty(IPlayerCharacter player, Dictionary<string, InternalMemberData> dataMap, bool forced)
|
|
{
|
|
bool softUpdate = true;
|
|
int allianceNum = FindAlliance(player);
|
|
|
|
int count = _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMemberCount;
|
|
if (!_wasCrossWorld || count != _groupMembers.Count || forced)
|
|
{
|
|
_groupMembers.Clear();
|
|
softUpdate = false;
|
|
}
|
|
|
|
// create new members array with cross world data
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
CrossRealmMember member = _crossRealmInfo->CrossRealmGroups[allianceNum].GroupMembers[i];
|
|
string memberName = member.NameString;
|
|
|
|
if (!dataMap.TryGetValue(memberName, out InternalMemberData? data) || data == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool isPlayer = member.EntityId == player.EntityId;
|
|
bool isLeader = member.IsPartyLeader;
|
|
PartyMemberStatus status = data.Status != null ? StatusForCrossWorldMember(data.Status) : PartyMemberStatus.None;
|
|
ReadyCheckStatus readyCheckStatus = GetReadyCheckStatus(member.ContentId);
|
|
|
|
if (softUpdate)
|
|
{
|
|
IPartyFramesMember groupMember = _groupMembers.ElementAt(i);
|
|
groupMember.Update(EnmityLevel.Last, status, readyCheckStatus, isLeader, member.ClassJobId);
|
|
}
|
|
else
|
|
{
|
|
PartyFramesMember partyMember = isPlayer ?
|
|
new PartyFramesMember(player, i, data.Order, EnmityLevel.Last, status, readyCheckStatus, isLeader) :
|
|
new PartyFramesMember(member, i, data.Order, status, readyCheckStatus, isLeader);
|
|
_groupMembers.Add(partyMember);
|
|
}
|
|
}
|
|
|
|
if (!softUpdate)
|
|
{
|
|
SortGroupMembers(player);
|
|
MembersChangedEvent?.Invoke(this);
|
|
}
|
|
}
|
|
|
|
private int FindAlliance(IPlayerCharacter player)
|
|
{
|
|
for (int i = 0; i < _crossRealmInfo->CrossRealmGroups.Length; i++)
|
|
{
|
|
for (int j = 0; j < _crossRealmInfo->CrossRealmGroups[i].GroupMemberCount; j++)
|
|
{
|
|
CrossRealmMember member = _crossRealmInfo->CrossRealmGroups[i].GroupMembers[j];
|
|
if (member.EntityId == player.EntityId)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
private void UpdateRegularParty(IPlayerCharacter player, Dictionary<string, InternalMemberData> dataMap, bool forced)
|
|
{
|
|
bool softUpdate = true;
|
|
|
|
if (!_wasRealGroup || _realMemberCount != _groupMembers.Count || forced)
|
|
{
|
|
_groupMembers.Clear();
|
|
softUpdate = false;
|
|
}
|
|
|
|
string[] keys = dataMap.Keys.ToArray();
|
|
for (int i = 0; i < keys.Length; i++)
|
|
{
|
|
if (!dataMap.TryGetValue(keys[i], out InternalMemberData? data) || data == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool isPlayer = data.ObjectId == player.GameObjectId;
|
|
bool isLeader = IsPartyLeader(data.Order);
|
|
EnmityLevel enmity = EnmityForIndex(data.Index);
|
|
PartyMemberStatus status = data.Status != null ? StatusForMember(data.Status, data.Name) : PartyMemberStatus.None;
|
|
ReadyCheckStatus readyCheckStatus = GetReadyCheckStatus((ulong)data.ContentId);
|
|
|
|
if (softUpdate)
|
|
{
|
|
IPartyFramesMember groupMember = _groupMembers.ElementAt(i);
|
|
groupMember.Update(enmity, status, readyCheckStatus, isLeader);
|
|
}
|
|
else
|
|
{
|
|
PartyFramesMember partyMember;
|
|
var member = GetDalamudPartyMember(data.Name);
|
|
if (member.HasValue && member.Value.Item1 is DalamudPartyMember dalamudPartyMember)
|
|
{
|
|
partyMember = new PartyFramesMember(dalamudPartyMember, i, data.Order, enmity, status, readyCheckStatus, isLeader);
|
|
}
|
|
else
|
|
{
|
|
partyMember = new PartyFramesMember(data.ObjectId, i, data.Order, enmity, status, readyCheckStatus, isLeader);
|
|
}
|
|
_groupMembers.Add(partyMember);
|
|
}
|
|
}
|
|
|
|
// player's chocobo (always last)
|
|
if (_config.ShowChocobo)
|
|
{
|
|
IGameObject? companion = Utils.GetBattleChocobo(player);
|
|
|
|
if (softUpdate && _groupMembers.FirstOrDefault(o => o.IsChocobo) is PartyFramesMember chocoboMember)
|
|
{
|
|
if (companion is ICharacter)
|
|
{
|
|
chocoboMember.Update(EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false);
|
|
}
|
|
else
|
|
{
|
|
_groupMembers.Remove(chocoboMember);
|
|
}
|
|
}
|
|
else if (companion is ICharacter companionCharacter)
|
|
{
|
|
_groupMembers.Add(new PartyFramesMember(companionCharacter, _groupMemberCount, 10, EnmityLevel.Last, PartyMemberStatus.None, ReadyCheckStatus.None, false, true));
|
|
}
|
|
}
|
|
|
|
if (!softUpdate)
|
|
{
|
|
SortGroupMembers(player);
|
|
MembersChangedEvent?.Invoke(this);
|
|
}
|
|
}
|
|
|
|
|
|
private void SortGroupMembers(IPlayerCharacter? player = null)
|
|
{
|
|
_sortedGroupMembers.Clear();
|
|
_sortedGroupMembers.AddRange(_groupMembers);
|
|
|
|
_sortedGroupMembers.Sort((a, b) =>
|
|
{
|
|
if (a.Order == b.Order)
|
|
{
|
|
if (a.ObjectId == player?.GameObjectId)
|
|
{
|
|
return 1;
|
|
}
|
|
else if (b.ObjectId == player?.GameObjectId)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
return a.Name.CompareTo(b.Name);
|
|
}
|
|
|
|
if (a.Order < b.Order)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
return 1;
|
|
});
|
|
}
|
|
|
|
private (DalamudPartyMember?, int)? GetDalamudPartyMember(string name)
|
|
{
|
|
for (int i = 0; i < Plugin.PartyList.Length; i++)
|
|
{
|
|
DalamudPartyMember? member = Plugin.PartyList[i];
|
|
if (member != null && member.Name.ToString() == name)
|
|
{
|
|
return (member, i);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void UpdateTrackers()
|
|
{
|
|
_raiseTracker.Update(_groupMembers);
|
|
_invulnTracker.Update(_groupMembers);
|
|
_cleanseTracker.Update(_groupMembers);
|
|
}
|
|
|
|
#region utils
|
|
private bool IsPartyLeader(int index)
|
|
{
|
|
if (PartyListAddon == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// we use the icon Y coordinate in the party list to know the index (lmao)
|
|
uint partyLeadIndex = (uint)PartyListAddon->LeaderMarkResNode->ChildNode->Y / 40;
|
|
return index == partyLeadIndex;
|
|
}
|
|
|
|
private PartyMemberStatus StatusForCrossWorldMember(string statusStr)
|
|
{
|
|
// offline status
|
|
if (statusStr.Contains(_offlineString, StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
return PartyMemberStatus.Offline;
|
|
}
|
|
|
|
return PartyMemberStatus.None;
|
|
}
|
|
|
|
|
|
private PartyMemberStatus StatusForMember(string statusStr, string name)
|
|
{
|
|
// offline status
|
|
if (statusStr.Contains(_offlineString, StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
return PartyMemberStatus.Offline;
|
|
}
|
|
|
|
// viewing cutscene status
|
|
for (int i = 0; i < _mainGroup.MemberCount; i++)
|
|
{
|
|
if (_mainGroup.PartyMembers[i].NameString == name)
|
|
{
|
|
if ((_mainGroup.PartyMembers[i].Flags & 0x10) != 0)
|
|
{
|
|
return PartyMemberStatus.ViewingCutscene;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return PartyMemberStatus.None;
|
|
}
|
|
|
|
private EnmityLevel EnmityForIndex(int index)
|
|
{
|
|
if (PartyListAddon == null || index < 0 || index > 7)
|
|
{
|
|
return EnmityLevel.Last;
|
|
}
|
|
|
|
EnmityLevel enmityLevel = (EnmityLevel)PartyListAddon->PartyMembers[index].EmnityByte;
|
|
if (enmityLevel == EnmityLevel.Leader && PartyListAddon->EnmityLeaderIndex != index)
|
|
{
|
|
enmityLevel = EnmityLevel.Last;
|
|
}
|
|
|
|
return enmityLevel;
|
|
}
|
|
|
|
private EnmityLevel EnmityForTrustMemberIndex(int index)
|
|
{
|
|
if (PartyListAddon == null || index < 0 || index > 6)
|
|
{
|
|
return EnmityLevel.Last;
|
|
}
|
|
|
|
return (EnmityLevel)PartyListAddon->TrustMembers[index].EmnityByte;
|
|
}
|
|
|
|
private static unsafe string? GetPartyListTitle()
|
|
{
|
|
AgentModule* agentModule = AgentModule.Instance();
|
|
if (agentModule == null) { return ""; }
|
|
|
|
AgentHUD* agentHUD = agentModule->GetAgentHUD();
|
|
if (agentHUD == null) { return ""; }
|
|
|
|
Lumina.Excel.ExcelSheet<Addon> sheet = Plugin.DataManager.GetExcelSheet<Addon>();
|
|
if (sheet.TryGetRow(agentHUD->PartyTitleAddonId, out Addon row))
|
|
{
|
|
return row.Text.ToString();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
#endregion
|
|
|
|
#region events
|
|
private void OnConfigPropertyChanged(object sender, OnChangeBaseArgs args)
|
|
{
|
|
if (args.PropertyName == "Preview")
|
|
{
|
|
UpdatePreview();
|
|
}
|
|
}
|
|
|
|
public void UpdatePreview()
|
|
{
|
|
_iconsConfig.Sign.Preview = _config.Preview;
|
|
|
|
if (!_config.Preview)
|
|
{
|
|
_groupMembers.Clear();
|
|
MembersChangedEvent?.Invoke(this);
|
|
return;
|
|
}
|
|
|
|
// fill list with fake members for UI testing
|
|
_groupMembers.Clear();
|
|
|
|
if (_config.Preview)
|
|
{
|
|
int count = FakePartyFramesMember.RNG.Next(4, 9);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
_groupMembers.Add(new FakePartyFramesMember(i));
|
|
}
|
|
}
|
|
|
|
SortGroupMembers();
|
|
MembersChangedEvent?.Invoke(this);
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
internal class InternalMemberData
|
|
{
|
|
internal uint ObjectId = 0;
|
|
internal long ContentId = 0;
|
|
internal string Name = "";
|
|
internal uint JobId = 0;
|
|
internal int Order = 0;
|
|
internal int Index = 0;
|
|
internal string? Status = null;
|
|
|
|
public InternalMemberData()
|
|
{
|
|
}
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Explicit, Size = 28)]
|
|
public unsafe struct PartyListMemberRawInfo
|
|
{
|
|
[FieldOffset(0x00)] public byte* NamePtr;
|
|
[FieldOffset(0x08)] public long ContentId;
|
|
[FieldOffset(0x10)] public uint ObjectId;
|
|
|
|
// some kind of type
|
|
// 1 = player
|
|
// 2 = party member?
|
|
// 3 = unknown
|
|
// 4 = chocobo
|
|
// 5 = summon?
|
|
[FieldOffset(0x14)] public byte Type;
|
|
|
|
[FieldOffset(0x18)] public byte Order;
|
|
|
|
public string Name => Marshal.PtrToStringUTF8(new IntPtr(NamePtr)) ?? "";
|
|
}
|
|
}
|