using Dalamud.Plugin.Services;
namespace PlayerNotes;
///
/// Watches the party/alliance list on every framework update and prints a chat notification
/// whenever a player from the friend list or blacklist joins a party we are in (including
/// when we join a party that already contains one of them). Notifications fire once per
/// membership transition, so they do not repeat while the player stays in the party.
///
public class PartyWatcher
{
// FFXIV uiColors palette keys (the game's role colors), used for the colored
// "[Blacklist]"/"[Friends]" message tag via IChatGui.Print(message, tag, color).
private static readonly ushort BlacklistColor = 0x221; // DPS red (uiColors key 545)
private static readonly ushort FriendColor = 0x1F8; // Healer green (uiColors key 504)
private readonly IPartyList partyList;
private readonly IPlayerState playerState;
private readonly Storage storage;
private readonly IChatGui chatGui;
/// Content IDs that were in the party on the previous frame.
private HashSet previousMembers = new();
public PartyWatcher(IPartyList partyList, IPlayerState playerState, Storage storage, IChatGui chatGui)
{
this.partyList = partyList;
this.playerState = playerState;
this.storage = storage;
this.chatGui = chatGui;
}
public void OnFrameworkUpdate(IFramework _)
{
if (playerState.ContentId == 0)
return; // not logged in / transitioning
var currentMembers = new HashSet();
foreach (var member in partyList)
{
if (member is not null && member.ContentId != 0)
currentMembers.Add(member.ContentId);
}
// Solo (or no readable party data): treat as "not in a party" so that the next
// time we group up, joins are detected again from scratch.
if (currentMembers.Count <= 1)
{
previousMembers.Clear();
return;
}
foreach (var member in partyList)
{
if (member is null || member.ContentId == 0)
continue;
if (previousMembers.Contains(member.ContentId))
continue; // already notified / already present last frame
if (member.ContentId == playerState.ContentId)
continue; // ignore our own join
NotifyIfListed(member);
}
previousMembers = currentMembers;
}
private void NotifyIfListed(Dalamud.Game.ClientState.Party.IPartyMember member)
{
var data = storage.Data;
var name = member.Name.TextValue;
var world = MemberWorld(member);
// Blacklist takes priority: it is the more important warning.
if (Match(data.Blacklist, member.ContentId, name, world) is { } blacklisted)
{
chatGui.Print(BuildAlert(blacklisted, world), "Blacklist", BlacklistColor);
return;
}
if (Match(data.Friends, member.ContentId, name, world) is { } friend)
chatGui.Print(BuildAlert(friend, world), "Friends", FriendColor);
}
/// The party member's home world, or "" when it cannot be determined.
private static string MemberWorld(Dalamud.Game.ClientState.Party.IPartyMember member)
{
var world = member.World;
return world.IsValid ? world.Value.Name.ExtractText() : ""; // Lumina sheet rows are structs
}
///
/// Matches by content ID first, then by name. When several entries share a name,
/// the party member's world disambiguates between them (entries with an unknown
/// world still match by name alone).
///
private static PlayerEntry? Match(List pool, ulong contentId, string name, string world)
{
if (contentId != 0)
{
var byId = pool.FirstOrDefault(e => e.ContentId == contentId);
if (byId is not null)
return byId;
}
var candidates = pool.Where(e => string.Equals(e.Name, name, StringComparison.OrdinalIgnoreCase)).ToList();
if (candidates.Count > 1 && world.Length > 0)
candidates = candidates.Where(e => e.World.Length == 0 ||
string.Equals(e.World, world, StringComparison.OrdinalIgnoreCase)).ToList();
return candidates.FirstOrDefault();
}
///
/// Builds the alert text. The colored "[Blacklist]"/"[Friends]" tag comes from
/// IChatGui.Print's messageTag + tagColor parameters (this Dalamud build has no
/// ARGB text-color SeString payload, only palette-keyed UI colors).
///
private static string BuildAlert(PlayerEntry entry, string world)
{
var tags = entry.Tags.Count > 0 ? $" [{string.Join(", ", entry.Tags)}]" : "";
var worldSuffix = world.Length > 0 ? $" ({world})" : "";
return $"\"{entry.Name}\"{worldSuffix}{tags} just joined your party!";
}
}