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!"; } }