using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Logging; using XivCommon.Functions.NamePlates; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; namespace NominaOcculta; internal class Obscurer : IDisposable { private Plugin Plugin { get; } private Stopwatch UpdateTimer { get; } = new(); private IList Friends { get; set; } internal unsafe Obscurer(Plugin plugin) { this.Plugin = plugin; this.UpdateTimer.Start(); this.Friends = this.Plugin.Common.Functions.FriendList.List .Select(friend => friend.Name.TextValue) .ToList(); this.Plugin.Framework.Update += this.OnFrameworkUpdate; this.Plugin.Functions.AtkTextNodeSetText += this.OnAtkTextNodeSetText; this.Plugin.Functions.CharacterInitialise += this.OnCharacterInitialise; this.Plugin.Functions.FlagSlotUpdate += this.OnFlagSlotUpdate; this.Plugin.Common.Functions.NamePlates.OnUpdate += this.OnNamePlateUpdate; this.Plugin.ChatGui.ChatMessage += this.OnChatMessage; } public unsafe void Dispose() { this.Plugin.ChatGui.ChatMessage -= this.OnChatMessage; this.Plugin.Common.Functions.NamePlates.OnUpdate -= this.OnNamePlateUpdate; this.Plugin.Functions.AtkTextNodeSetText -= this.OnAtkTextNodeSetText; this.Plugin.Functions.CharacterInitialise -= this.OnCharacterInitialise; this.Plugin.Functions.FlagSlotUpdate -= this.OnFlagSlotUpdate; this.Plugin.Framework.Update -= this.OnFrameworkUpdate; } private static readonly ConditionFlag[] DutyFlags = { ConditionFlag.BoundByDuty, ConditionFlag.BoundByDuty56, ConditionFlag.BoundByDuty95, ConditionFlag.BoundToDuty97, }; private bool IsInDuty() { return DutyFlags.Any(flag => this.Plugin.Condition[flag]); } private void OnFrameworkUpdate(Framework framework) { if (this.UpdateTimer.Elapsed < TimeSpan.FromSeconds(5) || this.IsInDuty()) { return; } this.Friends = this.Plugin.Common.Functions.FriendList.List .Select(friend => friend.Name.TextValue) .ToList(); this.UpdateTimer.Restart(); } private void OnAtkTextNodeSetText(IntPtr node, IntPtr textPtr, ref SeString? overwrite) { // A catch-all for UI text. This is slow, so specialised methods should be preferred. var text = Util.ReadRawSeString(textPtr); var changed = this.ChangeNames(text); if (changed) { overwrite = text; } } private unsafe void OnCharacterInitialise(GameObject* gameObj, IntPtr humanPtr, IntPtr customiseDataPtr) { var npc = this.Plugin.AppearanceRepository.GetNpc(gameObj->ObjectID); var customise = (byte*) customiseDataPtr; customise[(int) CustomizeIndex.Race] = (byte) npc.Race.Row; customise[(int) CustomizeIndex.Gender] = npc.Gender; customise[(int) CustomizeIndex.ModelType] = npc.BodyType; customise[(int) CustomizeIndex.Height] = npc.Height; customise[(int) CustomizeIndex.Tribe] = (byte) npc.Tribe.Row; customise[(int) CustomizeIndex.FaceType] = npc.Face; customise[(int) CustomizeIndex.HairStyle] = npc.HairStyle; customise[(int) CustomizeIndex.HasHighlights] = npc.HairHighlight; customise[(int) CustomizeIndex.SkinColor] = npc.SkinColor; customise[(int) CustomizeIndex.EyeColor] = npc.EyeColor; customise[(int) CustomizeIndex.HairColor] = npc.HairColor; customise[(int) CustomizeIndex.HairColor2] = npc.HairHighlightColor; customise[(int) CustomizeIndex.FaceFeatures] = npc.FacialFeature; customise[(int) CustomizeIndex.FaceFeaturesColor] = npc.FacialFeatureColor; customise[(int) CustomizeIndex.Eyebrows] = npc.Eyebrows; customise[(int) CustomizeIndex.EyeColor2] = npc.EyeHeterochromia; customise[(int) CustomizeIndex.EyeShape] = npc.EyeShape; customise[(int) CustomizeIndex.NoseShape] = npc.Nose; customise[(int) CustomizeIndex.JawShape] = npc.Jaw; customise[(int) CustomizeIndex.LipStyle] = npc.Mouth; customise[(int) CustomizeIndex.LipColor] = npc.LipColor; customise[(int) CustomizeIndex.RaceFeatureSize] = npc.BustOrTone1; customise[(int) CustomizeIndex.RaceFeatureType] = npc.ExtraFeature1; customise[(int) CustomizeIndex.BustSize] = npc.ExtraFeature2OrBust; customise[(int) CustomizeIndex.Facepaint] = npc.FacePaint; customise[(int) CustomizeIndex.FacepaintColor] = npc.FacePaintColor; } private enum PlateSlot : uint { Head = 0, Body = 1, Hands = 2, Legs = 3, Feet = 4, Ears = 5, Neck = 6, Wrists = 7, RightRing = 8, LeftRing = 9, MainHand = 10, OffHand = 11, } private unsafe void OnFlagSlotUpdate(GameObject* gameObj, uint slot, EquipData* equipData) { if (equipData == null) { return; } var npc = this.Plugin.AppearanceRepository.GetNpc(gameObj->ObjectID); var itemSlot = (PlateSlot) slot; var info = itemSlot switch { PlateSlot.Head => (npc.ModelHead, npc.DyeHead.Row), PlateSlot.Body => (npc.ModelBody, npc.DyeBody.Row), PlateSlot.Hands => (npc.ModelHands, npc.DyeHands.Row), PlateSlot.Legs => (npc.ModelLegs, npc.DyeLegs.Row), PlateSlot.Feet => (npc.ModelFeet, npc.DyeFeet.Row), PlateSlot.Ears => (npc.ModelEars, npc.DyeEars.Row), PlateSlot.Neck => (npc.ModelNeck, npc.DyeNeck.Row), PlateSlot.Wrists => (npc.ModelWrists, npc.DyeWrists.Row), PlateSlot.RightRing => (npc.ModelRightRing, npc.DyeRightRing.Row), PlateSlot.LeftRing => (npc.ModelLeftRing, npc.DyeLeftRing.Row), _ => (uint.MaxValue, uint.MaxValue), }; if (info.Item1 == uint.MaxValue) { return; } equipData->Model = (ushort) (info.Item1 & 0xFFFF); equipData->Variant = (byte) ((info.Item1 >> 16) & 0xFF); equipData->Dye = (byte) info.Item2; } private void OnNamePlateUpdate(NamePlateUpdateEventArgs args) { // only replace nameplates that have objects in the table if (!this.Plugin.Config.Enabled || !this.Plugin.NameRepository.Initialised || args.ObjectId == 0xE0000000) { return; } // find the object this nameplate references var obj = this.Plugin.ObjectTable.FirstOrDefault(o => o.ObjectId == args.ObjectId); if (obj == null) { return; } // handle owners if (obj.OwnerId != 0xE0000000) { if (this.Plugin.ObjectTable.FirstOrDefault(o => o.ObjectId == obj.OwnerId) is not { } owner) { return; } obj = owner; } // only work for characters if (obj.ObjectKind != ObjectKind.Player || obj is not Character chara) { return; } var info = GetInfo(chara); void Change(string name) { this.ChangeName(args.Name, name, info); this.ChangeName(args.Title, name, info); } var name = chara.Name.TextValue; var playerId = this.Plugin.ClientState.LocalPlayer?.ObjectId; var party = this.Plugin.PartyList.Select(member => member.ObjectId).ToArray(); if ((this.Plugin.Config.SelfFull || this.Plugin.Config.SelfFirst || this.Plugin.Config.SelfLast) && chara.ObjectId == playerId) { if (this.Plugin.Config.SelfFull) { Change(name); } if ((this.Plugin.Config.SelfFirst || this.Plugin.Config.SelfLast) && this.Plugin.NameRepository.GetReplacement(name, info) is { } replacement) { var parts = name.Split(' ', 2); var replacementParts = replacement.Split(' ', 2); if (this.Plugin.Config.SelfFirst) { args.Name.ReplacePlayerName(parts[0], replacementParts[0]); args.Title.ReplacePlayerName(parts[0], replacementParts[0]); } if (this.Plugin.Config.SelfLast) { args.Name.ReplacePlayerName(parts[1], replacementParts[1]); args.Title.ReplacePlayerName(parts[1], replacementParts[1]); } } } else if (this.Plugin.Config.Party && party.Contains(chara.ObjectId) && (!this.Plugin.Config.ExcludeFriends || !this.Friends.Contains(name))) { Change(name); } else if (this.Plugin.Config.Others && chara.ObjectId != playerId && !party.Contains(chara.ObjectId) && (!this.Plugin.Config.ExcludeFriends || !this.Friends.Contains(name))) { Change(chara.Name.TextValue); } } private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) { this.ChangeNames(sender); this.ChangeNames(message); } private void ChangeName(SeString text, string name, (byte, byte, byte) info) { if (this.Plugin.NameRepository.GetReplacement(name, info) is not { } replacement) { return; } if (!text.ContainsPlayerName(name)) { return; } text.ReplacePlayerName(name, replacement); } // PERFORMANCE NOTE: This potentially loops over the party list twice and the object // table once entirely. Should be avoided if being used in a // position where the player to replace is known. private bool ChangeNames(SeString text) { if (!this.Plugin.Config.Enabled || !this.Plugin.NameRepository.Initialised) { return false; } var changed = false; var player = this.Plugin.ClientState.LocalPlayer; if (this.Plugin.Config.SelfFull) { var playerName = player?.Name.TextValue; if (playerName != null && text.ContainsPlayerName(playerName) && this.Plugin.NameRepository.GetReplacement(playerName, GetInfo(player!)) is { } replacement) { text.ReplacePlayerName(playerName, replacement); changed = true; } } if (this.Plugin.Config.SelfFirst || this.Plugin.Config.SelfLast) { var playerName = player?.Name.TextValue; if (playerName != null && this.Plugin.NameRepository.GetReplacement(playerName, GetInfo(player!)) is { } replacement) { var parts = playerName.Split(' ', 2); var replacementParts = replacement.Split(' ', 2); if (this.Plugin.Config.SelfFirst && text.ContainsPlayerName(parts[0])) { text.ReplacePlayerName(parts[0], replacementParts[0]); changed = true; } if (this.Plugin.Config.SelfLast && text.ContainsPlayerName(parts[1])) { text.ReplacePlayerName(parts[1], replacementParts[1]); changed = true; } } } if (this.Plugin.Config.Party) { foreach (var member in this.Plugin.PartyList) { var name = member.Name.TextValue; var info = ((byte) 0xFF, (byte) 0xFF, member.Sex); if (member.GameObject is Character chara) { info = GetInfo(chara); } if (member.ObjectId == player?.ObjectId || !text.ContainsPlayerName(name) || this.Plugin.NameRepository.GetReplacement(name, info) is not { } replacement) { continue; } if (this.Plugin.Config.ExcludeFriends && this.Friends.Contains(name)) { continue; } text.ReplacePlayerName(name, replacement); changed = true; } } if (this.Plugin.Config.Others) { var party = this.Plugin.PartyList.Select(member => member.ObjectId).ToList(); foreach (var obj in this.Plugin.ObjectTable) { if (obj.ObjectKind != ObjectKind.Player || obj is not Character chara || obj.ObjectId == player?.ObjectId || party.Contains(obj.ObjectId)) { continue; } var name = chara.Name.TextValue; if (this.Plugin.Config.ExcludeFriends && this.Friends.Contains(name)) { continue; } var info = GetInfo(chara); if (info.race == 0) { continue; } if (this.Plugin.NameRepository.GetReplacement(name, GetInfo(chara)) is not { } replacement) { continue; } text.ReplacePlayerName(name, replacement); changed = true; } } return changed; } private static (byte race, byte clan, byte gender) GetInfo(Character chara) { return ( chara.Customize[(byte) CustomizeIndex.Race], (byte) ((chara.Customize[(byte) CustomizeIndex.Tribe] - 1) % 2), chara.Customize[(byte) CustomizeIndex.Gender] ); } }