From a3ae2629ef0c24dafb5a87b763005a075a8d6bac Mon Sep 17 00:00:00 2001 From: Anna Date: Mon, 9 May 2022 21:57:31 -0400 Subject: [PATCH] feat: start adding appearance obscurer --- NominaOcculta/AppearanceRepository.cs | 66 ++++ NominaOcculta/Commands.cs | 170 ++++---- NominaOcculta/Configuration.cs | 24 +- NominaOcculta/EquipData.cs | 10 + NominaOcculta/GameFunctions.cs | 227 +++++++---- NominaOcculta/NameRepository.cs | 268 ++++++------- NominaOcculta/NominaOcculta.csproj | 9 +- NominaOcculta/Obscurer.cs | 537 +++++++++++++++----------- NominaOcculta/Plugin.cs | 110 +++--- NominaOcculta/PluginUi.cs | 244 ++++++------ NominaOcculta/Util.cs | 126 +++--- 11 files changed, 1018 insertions(+), 773 deletions(-) create mode 100755 NominaOcculta/AppearanceRepository.cs create mode 100755 NominaOcculta/EquipData.cs diff --git a/NominaOcculta/AppearanceRepository.cs b/NominaOcculta/AppearanceRepository.cs new file mode 100755 index 0000000..2e04911 --- /dev/null +++ b/NominaOcculta/AppearanceRepository.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Logging; +using Lumina.Excel.GeneratedSheets; + +namespace NominaOcculta; + +internal class AppearanceRepository { + private Plugin Plugin { get; } + private List Npcs { get; } + private int Salt { get; } = new Random().Next(); + + private static readonly string[] Exclude = { + "Thancred", + "Y'shtola", + "Alphinaud", + "Alisaie", + "Urianger", + "Tataru", + "Minfilia", + "Lyse", + "Yda", + "Papalymo", + "Krile", + "Ryne", + "Estinien", + "Nanamo Ul Namo", + "G'raha Tia", + "Raubahn", + "Cid", + "Biggs", + "Wedge", + "Haurchefant", + "Merlwyb", + "Kan-E-Senna", + "Yugiri", + "Aymeric", + "Lahabrea", + "Igeyorhm", + "Hildibrand", + "Godbert", + }; + + internal AppearanceRepository(Plugin plugin) { + this.Plugin = plugin; + + var names = this.Plugin.DataManager.GetExcelSheet()!; + this.Npcs = this.Plugin.DataManager.GetExcelSheet()! + .Where(row => row.BodyType == 1) + .Where(row => row.ModelChara.Row == 0) + .Where(row => row.ModelBody != 0) + .Where(row => row.ModelLegs != 0) + .Where(row => !Exclude.Contains(names.GetRow(row.RowId)?.Singular.RawString)) + .ToList(); + PluginLog.Log($"npcs: {this.Npcs.Count}"); + } + + private int GetNpcIndex(uint objectId) { + return new Random((int) (objectId + this.Salt)).Next(0, this.Npcs.Count); + } + + internal ENpcBase GetNpc(uint objectId) { + return this.Npcs[this.GetNpcIndex(objectId)]; + } +} diff --git a/NominaOcculta/Commands.cs b/NominaOcculta/Commands.cs index c05dd28..e5bf62a 100755 --- a/NominaOcculta/Commands.cs +++ b/NominaOcculta/Commands.cs @@ -1,96 +1,96 @@ using System; using Dalamud.Game.Command; -namespace NominaOcculta { - internal class Commands : IDisposable { - private Plugin Plugin { get; } +namespace NominaOcculta; - internal Commands(Plugin plugin) { - this.Plugin = plugin; +internal class Commands : IDisposable { + private Plugin Plugin { get; } - this.Plugin.CommandManager.AddHandler("/occulta", new CommandInfo(this.OnCommand) { - HelpMessage = "Toggle the Nomina Occulta interface", - }); + internal Commands(Plugin plugin) { + this.Plugin = plugin; + + this.Plugin.CommandManager.AddHandler("/occulta", new CommandInfo(this.OnCommand) { + HelpMessage = "Toggle the Nomina Occulta interface", + }); + } + + public void Dispose() { + this.Plugin.CommandManager.RemoveHandler("/occulta"); + } + + private void OnCommand(string command, string arguments) { + arguments = arguments.Trim(); + + if (arguments.Length == 0) { + this.Plugin.Ui.Visible ^= true; + return; } - public void Dispose() { - this.Plugin.CommandManager.RemoveHandler("/occulta"); - } - - private void OnCommand(string command, string arguments) { - arguments = arguments.Trim(); - - if (arguments.Length == 0) { - this.Plugin.Ui.Visible ^= true; + var first = arguments.Split(' ', 2); + bool? enable; + switch (first[0]) { + case "enable": + enable = true; + break; + case "disable": + enable = false; + break; + case "toggle": + enable = null; + break; + case "reset": + this.Plugin.NameRepository.Reset(); + return; + default: + this.Plugin.ChatGui.PrintError($"Invalid operation \"{first[0]}\", was expecting enable, disable, toggle, or reset."); return; - } - - var first = arguments.Split(' ', 2); - bool? enable; - switch (first[0]) { - case "enable": - enable = true; - break; - case "disable": - enable = false; - break; - case "toggle": - enable = null; - break; - case "reset": - this.Plugin.NameRepository.Reset(); - return; - default: - this.Plugin.ChatGui.PrintError($"Invalid operation \"{first[0]}\", was expecting enable, disable, toggle, or reset."); - return; - } - - string? rest = null; - if (first.Length > 1) { - rest = first[1]; - } - - void Set(ref bool setting) { - if (enable == null) { - setting ^= true; - } else { - setting = enable.Value; - } - } - - switch (rest) { - case null: - Set(ref this.Plugin.Config.Enabled); - break; - case "self": - Set(ref this.Plugin.Config.SelfFull); - Set(ref this.Plugin.Config.SelfFirst); - Set(ref this.Plugin.Config.SelfLast); - break; - case "self full": - Set(ref this.Plugin.Config.SelfFull); - break; - case "self first": - Set(ref this.Plugin.Config.SelfFirst); - break; - case "self last": - Set(ref this.Plugin.Config.SelfLast); - break; - case "party": - Set(ref this.Plugin.Config.Party); - break; - case "others": - Set(ref this.Plugin.Config.Others); - break; - case "exclude friends": - Set(ref this.Plugin.Config.ExcludeFriends); - break; - default: - this.Plugin.ChatGui.PrintError($"Invalid option \"{rest}\"."); - return; - } - - this.Plugin.SaveConfig(); } + + string? rest = null; + if (first.Length > 1) { + rest = first[1]; + } + + void Set(ref bool setting) { + if (enable == null) { + setting ^= true; + } else { + setting = enable.Value; + } + } + + switch (rest) { + case null: + Set(ref this.Plugin.Config.Enabled); + break; + case "self": + Set(ref this.Plugin.Config.SelfFull); + Set(ref this.Plugin.Config.SelfFirst); + Set(ref this.Plugin.Config.SelfLast); + break; + case "self full": + Set(ref this.Plugin.Config.SelfFull); + break; + case "self first": + Set(ref this.Plugin.Config.SelfFirst); + break; + case "self last": + Set(ref this.Plugin.Config.SelfLast); + break; + case "party": + Set(ref this.Plugin.Config.Party); + break; + case "others": + Set(ref this.Plugin.Config.Others); + break; + case "exclude friends": + Set(ref this.Plugin.Config.ExcludeFriends); + break; + default: + this.Plugin.ChatGui.PrintError($"Invalid option \"{rest}\"."); + return; + } + + this.Plugin.SaveConfig(); } } diff --git a/NominaOcculta/Configuration.cs b/NominaOcculta/Configuration.cs index e882e8c..887f698 100755 --- a/NominaOcculta/Configuration.cs +++ b/NominaOcculta/Configuration.cs @@ -1,17 +1,17 @@ using System; using Dalamud.Configuration; -namespace NominaOcculta { - [Serializable] - internal class Configuration : IPluginConfiguration { - public int Version { get; set; } = 1; +namespace NominaOcculta; - public bool Enabled; - public bool SelfFull; - public bool SelfFirst; - public bool SelfLast; - public bool Party; - public bool Others; - public bool ExcludeFriends; - } +[Serializable] +internal class Configuration : IPluginConfiguration { + public int Version { get; set; } = 1; + + public bool Enabled; + public bool SelfFull; + public bool SelfFirst; + public bool SelfLast; + public bool Party; + public bool Others; + public bool ExcludeFriends; } diff --git a/NominaOcculta/EquipData.cs b/NominaOcculta/EquipData.cs new file mode 100755 index 0000000..ba42f7d --- /dev/null +++ b/NominaOcculta/EquipData.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace NominaOcculta; + +[StructLayout(LayoutKind.Sequential)] +internal struct EquipData { + internal ushort Model; + internal byte Variant; + internal byte Dye; +} diff --git a/NominaOcculta/GameFunctions.cs b/NominaOcculta/GameFunctions.cs index 7e1b367..dc45116 100755 --- a/NominaOcculta/GameFunctions.cs +++ b/NominaOcculta/GameFunctions.cs @@ -2,124 +2,199 @@ using System.Runtime.InteropServices; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Framework; -using Siggingway; -namespace NominaOcculta { - internal class GameFunctions : IDisposable { - private static class Signatures { - internal const string GenerateName = "E8 ?? ?? ?? ?? 48 8D 8B ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 74 1B 48 8D 8B ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 8B D0 E8 ?? ?? ?? ?? 48 8B CB 48 8B 7C 24"; - internal const string Utf8StringCtor = "E8 ?? ?? ?? ?? 44 2B F7"; - internal const string Utf8StringDtor = "80 79 21 00 75 12"; - internal const string AtkTextNodeSetText = "E8 ?? ?? ?? ?? 8D 4E 32"; - internal const string LoadExd = "40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 41 0F B6 D9"; - } +namespace NominaOcculta; - #region Delegates +internal class GameFunctions : IDisposable { + private static class Signatures { + internal const string GenerateName = "E8 ?? ?? ?? ?? 48 8D 8B ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 74 1B 48 8D 8B ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 8B D0 E8 ?? ?? ?? ?? 48 8B CB 48 8B 7C 24"; + internal const string Utf8StringCtor = "E8 ?? ?? ?? ?? 44 2B F7"; + internal const string Utf8StringDtor = "80 79 21 00 75 12"; + internal const string AtkTextNodeSetText = "E8 ?? ?? ?? ?? 8D 4E 32"; + internal const string LoadExd = "40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 41 0F B6 D9"; + internal const string CharacterInitialise = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 48 8B F9 48 8B EA 48 81 C1 ?? ?? ?? ?? E8"; + internal const string CharacterIsMount = "40 53 48 83 EC 20 48 8B 01 48 8B D9 FF 50 10 83 F8 08 75 08"; + internal const string FlagSlotUpdate = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A"; + } - private delegate IntPtr GenerateNameDelegate(int race, int clan, int gender, IntPtr first, IntPtr last); + #region Delegates - private delegate IntPtr Utf8StringCtorDelegate(IntPtr memory); + private delegate IntPtr GenerateNameDelegate(int race, int clan, int gender, IntPtr first, IntPtr last); - private delegate void Utf8StringDtorDelegate(IntPtr memory); + private delegate IntPtr Utf8StringCtorDelegate(IntPtr memory); - private delegate void AtkTextNodeSetTextDelegate(IntPtr node, IntPtr text); + private delegate void Utf8StringDtorDelegate(IntPtr memory); - private delegate byte LoadExdDelegate(IntPtr a1, string sheetName, byte a3, byte a4); + private delegate void AtkTextNodeSetTextDelegate(IntPtr node, IntPtr text); - private delegate IntPtr GetExcelModuleDelegate(IntPtr uiModule); + private delegate byte LoadExdDelegate(IntPtr a1, string sheetName, byte a3, byte a4); - #endregion + private delegate IntPtr GetExcelModuleDelegate(IntPtr uiModule); - #region Functions + private delegate IntPtr CharacterIsMountDelegate(IntPtr actor); - [Signature(Signatures.Utf8StringCtor)] - private Utf8StringCtorDelegate Utf8StringCtor { get; init; } = null!; + private delegate char CharacterInitialiseDelegate(IntPtr actorPtr, IntPtr customizeDataPtr); - [Signature(Signatures.Utf8StringDtor)] - private Utf8StringDtorDelegate Utf8StringDtor { get; init; } = null!; + private delegate IntPtr FlagSlotUpdateDelegate(IntPtr actorPtr, uint slot, IntPtr equipData); - [Signature(Signatures.GenerateName)] - private GenerateNameDelegate InternalGenerateName { get; init; } = null!; + #endregion - [Signature(Signatures.LoadExd)] - private LoadExdDelegate LoadExd { get; init; } = null!; + #region Functions - #endregion + [Signature(Signatures.Utf8StringCtor)] + private Utf8StringCtorDelegate Utf8StringCtor { get; init; } = null!; - [Signature(Signatures.AtkTextNodeSetText, DetourName = nameof(AtkTextNodeSetTextDetour))] - private Hook AtkTextNodeSetTextHook { get; init; } = null!; + [Signature(Signatures.Utf8StringDtor)] + private Utf8StringDtorDelegate Utf8StringDtor { get; init; } = null!; - internal delegate void AtkTextNodeSetTextEventDelegate(IntPtr node, IntPtr text, ref SeString? overwrite); + [Signature(Signatures.GenerateName)] + private GenerateNameDelegate InternalGenerateName { get; init; } = null!; - internal event AtkTextNodeSetTextEventDelegate? AtkTextNodeSetText; + [Signature(Signatures.LoadExd)] + private LoadExdDelegate LoadExd { get; init; } = null!; - private Plugin Plugin { get; } + #endregion - private IntPtr First { get; } - private IntPtr Last { get; } + [Signature(Signatures.AtkTextNodeSetText, DetourName = nameof(AtkTextNodeSetTextDetour))] + private Hook AtkTextNodeSetTextHook { get; init; } = null!; + + [Signature(Signatures.CharacterIsMount, DetourName = nameof(CharacterIsMountDetour))] + private Hook CharacterIsMountHook { get; init; } = null!; - internal GameFunctions(Plugin plugin) { - this.Plugin = plugin; + [Signature(Signatures.CharacterInitialise, DetourName = nameof(CharacterInitialiseDetour))] + private Hook CharacterInitializeHook { get; init; } = null!; + + [Signature(Signatures.FlagSlotUpdate, DetourName = nameof(FlagSlotUpdateDetour))] + private Hook FlagSlotUpdateHook { get; init; } = null!; - Siggingway.Siggingway.Initialise(this.Plugin.SigScanner, this); + #region Events - this.AtkTextNodeSetTextHook.Enable(); + internal delegate void AtkTextNodeSetTextEventDelegate(IntPtr node, IntPtr text, ref SeString? overwrite); - this.First = Marshal.AllocHGlobal(128); - this.Last = Marshal.AllocHGlobal(128); + internal event AtkTextNodeSetTextEventDelegate? AtkTextNodeSetText; + + internal unsafe delegate void CharacterInitialiseEventDelegate(GameObject* gameObj, IntPtr humanPtr, IntPtr customiseDataPtr); + + internal event CharacterInitialiseEventDelegate? CharacterInitialise; + + internal unsafe delegate void FlagSlotUpdateEventDelegate(GameObject* gameObj, uint slot, EquipData* equipData); - this.Utf8StringCtor(this.First); - this.Utf8StringCtor(this.Last); - } + internal event FlagSlotUpdateEventDelegate? FlagSlotUpdate; - public void Dispose() { - this.Utf8StringDtor(this.Last); - this.Utf8StringDtor(this.First); - Marshal.FreeHGlobal(this.Last); - Marshal.FreeHGlobal(this.First); - this.AtkTextNodeSetTextHook.Dispose(); - } + #endregion - private unsafe void AtkTextNodeSetTextDetour(IntPtr node, IntPtr text) { - SeString? overwrite = null; - this.AtkTextNodeSetText?.Invoke(node, text, ref overwrite); + private Plugin Plugin { get; } - if (overwrite != null) { - fixed (byte* newText = overwrite.Encode().Terminate()) { - this.AtkTextNodeSetTextHook.Original(node, (IntPtr) newText); - } + private IntPtr First { get; } + private IntPtr Last { get; } - return; + internal GameFunctions(Plugin plugin) { + this.Plugin = plugin; + + SignatureHelper.Initialise(this); + + this.AtkTextNodeSetTextHook.Enable(); + this.CharacterInitializeHook.Enable(); + this.CharacterIsMountHook.Enable(); + this.FlagSlotUpdateHook.Enable(); + + this.First = Marshal.AllocHGlobal(128); + this.Last = Marshal.AllocHGlobal(128); + + this.Utf8StringCtor(this.First); + this.Utf8StringCtor(this.Last); + } + + public void Dispose() { + this.Utf8StringDtor(this.Last); + this.Utf8StringDtor(this.First); + Marshal.FreeHGlobal(this.Last); + Marshal.FreeHGlobal(this.First); + this.AtkTextNodeSetTextHook.Dispose(); + this.CharacterInitializeHook.Dispose(); + this.CharacterIsMountHook.Dispose(); + this.FlagSlotUpdateHook.Dispose(); + } + + private unsafe void AtkTextNodeSetTextDetour(IntPtr node, IntPtr text) { + SeString? overwrite = null; + this.AtkTextNodeSetText?.Invoke(node, text, ref overwrite); + + if (overwrite != null) { + fixed (byte* newText = overwrite.Encode().Terminate()) { + this.AtkTextNodeSetTextHook.Original(node, (IntPtr) newText); } - this.AtkTextNodeSetTextHook.Original(node, text); + return; } - public string? GenerateName(int race, int clan, int gender) { - if (this.InternalGenerateName(race, clan, gender, this.First, this.Last) == IntPtr.Zero) { - return null; + this.AtkTextNodeSetTextHook.Original(node, text); + } + + private IntPtr _lastActor = IntPtr.Zero; + + private unsafe IntPtr CharacterIsMountDetour(IntPtr characterPtr) { + var chara = (GameObject*) characterPtr; + if (chara != null && chara->ObjectKind == (byte) ObjectKind.Pc) { + this._lastActor = characterPtr; + } else { + this._lastActor = IntPtr.Zero; + } + + return this.CharacterIsMountHook.Original(characterPtr); + } + + private unsafe char CharacterInitialiseDetour(IntPtr actorPtr, IntPtr customizeDataPtr) { + if (this._lastActor != IntPtr.Zero) { + try { + this.CharacterInitialise?.Invoke((GameObject*) this._lastActor, actorPtr, customizeDataPtr); + } catch (Exception e) { + PluginLog.LogError(e, "yeet"); } + } - var first = Marshal.PtrToStringUTF8(Marshal.ReadIntPtr(this.First)); - var last = Marshal.PtrToStringUTF8(Marshal.ReadIntPtr(this.Last)); + return this.CharacterInitializeHook.Original(actorPtr, customizeDataPtr); + } - if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(last)) { - return null; + private unsafe IntPtr FlagSlotUpdateDetour(IntPtr actorPtr, uint slot, IntPtr equipDataPtr) { + if (this._lastActor != IntPtr.Zero) { + try { + this.FlagSlotUpdate?.Invoke((GameObject*) this._lastActor, slot, (EquipData*) equipDataPtr); + } catch (Exception e) { + PluginLog.LogError(e, "yeet2"); } + } + + return this.FlagSlotUpdateHook.Original(actorPtr, slot, equipDataPtr); + } - return $"{first} {last}"; + public string? GenerateName(int race, int clan, int gender) { + if (this.InternalGenerateName(race, clan, gender, this.First, this.Last) == IntPtr.Zero) { + return null; } - public unsafe void LoadSheet(string name) { - var ui = (IntPtr) Framework.Instance()->GetUiModule(); - var getExcelModulePtr = *(*(IntPtr**) ui + 5); - var getExcelModule = Marshal.GetDelegateForFunctionPointer(getExcelModulePtr); - var excelModule = getExcelModule(ui); - var exdModule = *(IntPtr*) (excelModule + 8); - var excel = *(IntPtr*) (exdModule + 0x20); + var first = Marshal.PtrToStringUTF8(Marshal.ReadIntPtr(this.First)); + var last = Marshal.PtrToStringUTF8(Marshal.ReadIntPtr(this.Last)); - this.LoadExd(excel, name, 0, 1); + if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(last)) { + return null; } + + return $"{first} {last}"; + } + + public unsafe void LoadSheet(string name) { + var ui = (IntPtr) Framework.Instance()->GetUiModule(); + var getExcelModulePtr = *(*(IntPtr**) ui + 5); + var getExcelModule = Marshal.GetDelegateForFunctionPointer(getExcelModulePtr); + var excelModule = getExcelModule(ui); + var exdModule = *(IntPtr*) (excelModule + 8); + var excel = *(IntPtr*) (exdModule + 0x20); + + this.LoadExd(excel, name, 0, 1); } } diff --git a/NominaOcculta/NameRepository.cs b/NominaOcculta/NameRepository.cs index 0ee8162..48fc01c 100755 --- a/NominaOcculta/NameRepository.cs +++ b/NominaOcculta/NameRepository.cs @@ -5,156 +5,156 @@ using System.Linq; using Dalamud.Game; using Lumina.Excel.GeneratedSheets; -namespace NominaOcculta { - internal class NameRepository : IDisposable { - private Plugin Plugin { get; } +namespace NominaOcculta; - private Random Rng { get; } = new(); +internal class NameRepository : IDisposable { + private Plugin Plugin { get; } - private Dictionary<(byte, byte, byte), Queue> Names { get; } = new(); - internal IReadOnlyDictionary<(byte, byte, byte), Queue> ReadOnlyNames => this.Names; + private Random Rng { get; } = new(); - private Dictionary Replacements { get; } = new(); - internal IReadOnlyDictionary ReadonlyReplacements => this.Replacements; + private Dictionary<(byte, byte, byte), Queue> Names { get; } = new(); + internal IReadOnlyDictionary<(byte, byte, byte), Queue> ReadOnlyNames => this.Names; - private Dictionary LastSeenInfo { get; } = new(); - internal IReadOnlyDictionary ReadOnlyLastSeenInfo => this.LastSeenInfo; + private Dictionary Replacements { get; } = new(); + internal IReadOnlyDictionary ReadonlyReplacements => this.Replacements; - private readonly int _numRaces; - private readonly Stopwatch _loadSheetWatch = new(); + private Dictionary LastSeenInfo { get; } = new(); + internal IReadOnlyDictionary ReadOnlyLastSeenInfo => this.LastSeenInfo; - internal bool Initialised; + private readonly int _numRaces; + private readonly Stopwatch _loadSheetWatch = new(); - internal NameRepository(Plugin plugin) { - this.Plugin = plugin; + internal bool Initialised; - this._numRaces = this.Plugin.DataManager.GetExcelSheet()!.Count(row => row.RowId != 0); + internal NameRepository(Plugin plugin) { + this.Plugin = plugin; + this._numRaces = this.Plugin.DataManager.GetExcelSheet()!.Count(row => row.RowId != 0); + + this.Plugin.Functions.LoadSheet(Util.SheetName); + this.Plugin.ClientState.Login += this.OnLogin; + + for (var race = (byte) 1; race <= this._numRaces; race++) { + for (var clan = (byte) 0; clan <= 1; clan++) { + for (var sex = (byte) 0; sex <= 1; sex++) { + this.Names[(race, clan, sex)] = new Queue(); + } + } + } + + this.Plugin.Framework.Update += this.OnFrameworkUpdate; + } + + public void Dispose() { + this.Plugin.Framework.Update -= this.OnFrameworkUpdate; + this.Plugin.ClientState.Login -= this.OnLogin; + } + + private void OnFrameworkUpdate(Framework framework) { + // The game unloads the CharaMakeName sheet after logging in. + // We need this sheet to generate names, so we load it again. + if (this._loadSheetWatch.IsRunning && this._loadSheetWatch.Elapsed > TimeSpan.FromSeconds(3)) { this.Plugin.Functions.LoadSheet(Util.SheetName); - this.Plugin.ClientState.Login += this.OnLogin; + this._loadSheetWatch.Reset(); + } - for (var race = (byte) 1; race <= this._numRaces; race++) { - for (var clan = (byte) 0; clan <= 1; clan++) { - for (var sex = (byte) 0; sex <= 1; sex++) { - this.Names[(race, clan, sex)] = new Queue(); + // The in-game name generator will generate duplicate names if it is given + // identical parameters on the same frame. Instead, we will fill up a queue + // with 100 names (the maximum amount of players in the object table) for + // each combination of parameters, generating one name per combination per + // frame. + + for (var race = (byte) 1; race <= this._numRaces; race++) { + for (var clan = (byte) 0; clan <= 1; clan++) { + for (var sex = (byte) 0; sex <= 1; sex++) { + var queue = this.Names[(race, clan, sex)]; + if (queue.Count >= 100) { + continue; + } + + var name = this.Plugin.Functions.GenerateName(race, clan, sex); + if (name != null && (!queue.TryPeek(out var peek) || peek != name)) { + queue.Enqueue(name); } } } - - this.Plugin.Framework.Update += this.OnFrameworkUpdate; } - public void Dispose() { - this.Plugin.Framework.Update -= this.OnFrameworkUpdate; - this.Plugin.ClientState.Login -= this.OnLogin; - } - - private void OnFrameworkUpdate(Framework framework) { - // The game unloads the CharaMakeName sheet after logging in. - // We need this sheet to generate names, so we load it again. - if (this._loadSheetWatch.IsRunning && this._loadSheetWatch.Elapsed > TimeSpan.FromSeconds(3)) { - this.Plugin.Functions.LoadSheet(Util.SheetName); - this._loadSheetWatch.Reset(); - } - - // The in-game name generator will generate duplicate names if it is given - // identical parameters on the same frame. Instead, we will fill up a queue - // with 100 names (the maximum amount of players in the object table) for - // each combination of parameters, generating one name per combination per - // frame. - - for (var race = (byte) 1; race <= this._numRaces; race++) { - for (var clan = (byte) 0; clan <= 1; clan++) { - for (var sex = (byte) 0; sex <= 1; sex++) { - var queue = this.Names[(race, clan, sex)]; - if (queue.Count >= 100) { - continue; - } - - var name = this.Plugin.Functions.GenerateName(race, clan, sex); - if (name != null && (!queue.TryPeek(out var peek) || peek != name)) { - queue.Enqueue(name); - } - } - } - } - - if (!this.Initialised) { - this.Initialised = this.Names.Values.All(queue => queue.Count >= 100); - } - } - - private void OnLogin(object? sender, EventArgs e) { - this._loadSheetWatch.Restart(); - } - - /// - /// - /// Get a consistent replacement name for a real name. - /// - /// - /// This will generate a new name if the given info changes. - /// - /// - /// - /// (race, clan, sex) if known. Any unknowns should be 0xFF to be replaced with random, valid values. - /// A replacement name. Returns null if name is null/empty or no name could be generated. - internal string? GetReplacement(string name, (byte race, byte clan, byte sex) info) { - if (string.IsNullOrEmpty(name)) { - return null; - } - - if (this.LastSeenInfo.TryGetValue(name, out var lastInfo) && lastInfo != info) { - this.Replacements.Remove(name); - } - - this.LastSeenInfo[name] = info; - - if (this.Replacements.TryGetValue(name, out var replacement)) { - return replacement; - } - - // need to generate a name after this point - - // use random parameters for info if none was specified - if (info.race == 0xFF) { - info.race = (byte) this.Rng.Next(1, this._numRaces + 1); - } - - if (info.clan == 0xFF) { - info.clan = (byte) this.Rng.Next(0, 2); - } - - if (info.sex == 0xFF) { - info.sex = (byte) this.Rng.Next(0, 2); - } - - // get a name for the given info if possible - if (this.Names.TryGetValue(info, out var names)) { - // make sure the new name is not the same as the old name - names.TryDequeue(out var newName); - while (newName == name) { - names.TryDequeue(out newName); - } - - if (newName != null) { - this.Replacements[name] = newName; - return newName; - } - } - - // otherwise, get a random name - // can't really do anything about conflicts here, but this should be a very rare/impossible case - var random = this.Plugin.Functions.GenerateName(info.race, info.clan, info.sex); - if (random != null) { - this.Replacements[name] = random; - } - - return random; - } - - internal void Reset() { - this.Replacements.Clear(); + if (!this.Initialised) { + this.Initialised = this.Names.Values.All(queue => queue.Count >= 100); } } + + private void OnLogin(object? sender, EventArgs e) { + this._loadSheetWatch.Restart(); + } + + /// + /// + /// Get a consistent replacement name for a real name. + /// + /// + /// This will generate a new name if the given info changes. + /// + /// + /// + /// (race, clan, sex) if known. Any unknowns should be 0xFF to be replaced with random, valid values. + /// A replacement name. Returns null if name is null/empty or no name could be generated. + internal string? GetReplacement(string name, (byte race, byte clan, byte sex) info) { + if (string.IsNullOrEmpty(name)) { + return null; + } + + if (this.LastSeenInfo.TryGetValue(name, out var lastInfo) && lastInfo != info) { + this.Replacements.Remove(name); + } + + this.LastSeenInfo[name] = info; + + if (this.Replacements.TryGetValue(name, out var replacement)) { + return replacement; + } + + // need to generate a name after this point + + // use random parameters for info if none was specified + if (info.race == 0xFF) { + info.race = (byte) this.Rng.Next(1, this._numRaces + 1); + } + + if (info.clan == 0xFF) { + info.clan = (byte) this.Rng.Next(0, 2); + } + + if (info.sex == 0xFF) { + info.sex = (byte) this.Rng.Next(0, 2); + } + + // get a name for the given info if possible + if (this.Names.TryGetValue(info, out var names)) { + // make sure the new name is not the same as the old name + names.TryDequeue(out var newName); + while (newName == name) { + names.TryDequeue(out newName); + } + + if (newName != null) { + this.Replacements[name] = newName; + return newName; + } + } + + // otherwise, get a random name + // can't really do anything about conflicts here, but this should be a very rare/impossible case + var random = this.Plugin.Functions.GenerateName(info.race, info.clan, info.sex); + if (random != null) { + this.Replacements[name] = random; + } + + return random; + } + + internal void Reset() { + this.Replacements.Clear(); + } } diff --git a/NominaOcculta/NominaOcculta.csproj b/NominaOcculta/NominaOcculta.csproj index 65173b1..9abb137 100755 --- a/NominaOcculta/NominaOcculta.csproj +++ b/NominaOcculta/NominaOcculta.csproj @@ -8,6 +8,8 @@ false true true + true + full @@ -42,10 +44,9 @@ - - - - + + + diff --git a/NominaOcculta/Obscurer.cs b/NominaOcculta/Obscurer.cs index a4c46d0..3f776f7 100755 --- a/NominaOcculta/Obscurer.cs +++ b/NominaOcculta/Obscurer.cs @@ -8,245 +8,328 @@ 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; } +namespace NominaOcculta; - private Stopwatch UpdateTimer { get; } = new(); - private IList Friends { get; set; } +internal class Obscurer : IDisposable { + private Plugin Plugin { get; } - internal Obscurer(Plugin plugin) { - this.Plugin = plugin; + private Stopwatch UpdateTimer { get; } = new(); + private IList Friends { get; set; } - this.UpdateTimer.Start(); + internal unsafe Obscurer(Plugin plugin) { + this.Plugin = plugin; - this.Friends = this.Plugin.Common.Functions.FriendList.List - .Select(friend => friend.Name.TextValue) - .ToList(); + this.UpdateTimer.Start(); - this.Plugin.Framework.Update += this.OnFrameworkUpdate; - this.Plugin.Functions.AtkTextNodeSetText += this.OnAtkTextNodeSetText; - this.Plugin.Common.Functions.NamePlates.OnUpdate += this.OnNamePlateUpdate; - this.Plugin.ChatGui.ChatMessage += this.OnChatMessage; + 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; } - public void Dispose() { - this.Plugin.ChatGui.ChatMessage -= this.OnChatMessage; - this.Plugin.Common.Functions.NamePlates.OnUpdate -= this.OnNamePlateUpdate; - this.Plugin.Functions.AtkTextNodeSetText -= this.OnAtkTextNodeSetText; - this.Plugin.Framework.Update -= this.OnFrameworkUpdate; - } + this.Friends = this.Plugin.Common.Functions.FriendList.List + .Select(friend => friend.Name.TextValue) + .ToList(); + this.UpdateTimer.Restart(); + } - private static readonly ConditionFlag[] DutyFlags = { - ConditionFlag.BoundByDuty, - ConditionFlag.BoundByDuty56, - ConditionFlag.BoundByDuty95, - ConditionFlag.BoundToDuty97, - }; + 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. - private bool IsInDuty() { - return DutyFlags.Any(flag => this.Plugin.Condition[flag]); - } + var text = Util.ReadRawSeString(textPtr); - 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 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] - ); + 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] + ); + } } diff --git a/NominaOcculta/Plugin.cs b/NominaOcculta/Plugin.cs index 89dd6d6..82ddd62 100755 --- a/NominaOcculta/Plugin.cs +++ b/NominaOcculta/Plugin.cs @@ -11,77 +11,79 @@ using Dalamud.IoC; using Dalamud.Plugin; using XivCommon; -namespace NominaOcculta { - public class Plugin : IDalamudPlugin { - public string Name => "Nomina Occulta"; +namespace NominaOcculta; - [PluginService] - internal DalamudPluginInterface Interface { get; private init; } +public class Plugin : IDalamudPlugin { + public string Name => "Nomina Occulta"; - [PluginService] - internal ChatGui ChatGui { get; private init; } + [PluginService] + internal DalamudPluginInterface Interface { get; private init; } - [PluginService] - internal ClientState ClientState { get; private init; } + [PluginService] + internal ChatGui ChatGui { get; private init; } - [PluginService] - internal CommandManager CommandManager { get; private init; } + [PluginService] + internal ClientState ClientState { get; private init; } - [PluginService] - internal Condition Condition { get; private set; } + [PluginService] + internal CommandManager CommandManager { get; private init; } - [PluginService] - internal DataManager DataManager { get; private init; } + [PluginService] + internal Condition Condition { get; private set; } - [PluginService] - internal Framework Framework { get; private init; } + [PluginService] + internal DataManager DataManager { get; private init; } - [PluginService] - internal KeyState KeyState { get; private init; } + [PluginService] + internal Framework Framework { get; private init; } - [PluginService] - internal PartyList PartyList { get; private init; } + [PluginService] + internal KeyState KeyState { get; private init; } - [PluginService] - internal ObjectTable ObjectTable { get; private init; } + [PluginService] + internal PartyList PartyList { get; private init; } + + [PluginService] + internal TargetManager TargetManager { get; private init; } - [PluginService] - internal SigScanner SigScanner { get; private init; } + [PluginService] + internal ObjectTable ObjectTable { get; private init; } - internal XivCommonBase Common { get; } - internal GameFunctions Functions { get; } + internal XivCommonBase Common { get; } + internal GameFunctions Functions { get; } - internal Configuration Config { get; } - private Commands Commands { get; } - internal NameRepository NameRepository { get; } - private Obscurer Obscurer { get; } - internal PluginUi Ui { get; } + internal Configuration Config { get; } + private Commands Commands { get; } + internal NameRepository NameRepository { get; } + internal AppearanceRepository AppearanceRepository { get; } + private Obscurer Obscurer { get; } + internal PluginUi Ui { get; } - #pragma warning disable 8618 - public Plugin() { - this.Common = new XivCommonBase(Hooks.NamePlates); - this.Functions = new GameFunctions(this); + #pragma warning disable 8618 + public Plugin() { + this.Common = new XivCommonBase(Hooks.NamePlates); + this.Functions = new GameFunctions(this); - this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); - this.Ui = new PluginUi(this); - this.NameRepository = new NameRepository(this); - this.Obscurer = new Obscurer(this); - this.Commands = new Commands(this); - } - #pragma warning restore 8618 + this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); + this.Ui = new PluginUi(this); + this.NameRepository = new NameRepository(this); + this.AppearanceRepository = new AppearanceRepository(this); + this.Obscurer = new Obscurer(this); + this.Commands = new Commands(this); + } + #pragma warning restore 8618 - public void Dispose() { - this.Commands.Dispose(); - this.Obscurer.Dispose(); - this.NameRepository.Dispose(); - this.Ui.Dispose(); + public void Dispose() { + this.Commands.Dispose(); + this.Obscurer.Dispose(); + this.NameRepository.Dispose(); + this.Ui.Dispose(); - this.Functions.Dispose(); - this.Common.Dispose(); - } + this.Functions.Dispose(); + this.Common.Dispose(); + } - internal void SaveConfig() { - this.Interface.SavePluginConfig(this.Config); - } + internal void SaveConfig() { + this.Interface.SavePluginConfig(this.Config); } } diff --git a/NominaOcculta/PluginUi.cs b/NominaOcculta/PluginUi.cs index 70a4d8a..202576a 100755 --- a/NominaOcculta/PluginUi.cs +++ b/NominaOcculta/PluginUi.cs @@ -1,145 +1,153 @@ using System; using Dalamud.Game.ClientState.Keys; +using Dalamud.Logging; using ImGuiNET; +using Lumina.Excel.GeneratedSheets; -namespace NominaOcculta { - internal class PluginUi : IDisposable { - private Plugin Plugin { get; } +namespace NominaOcculta; - internal bool Visible; - private bool _debug; - private int _queueColumns = 2; +internal class PluginUi : IDisposable { + private Plugin Plugin { get; } - internal PluginUi(Plugin plugin) { - this.Plugin = plugin; + internal bool Visible; + private bool _debug; + private int _queueColumns = 2; - this.Plugin.Interface.UiBuilder.Draw += this.Draw; - this.Plugin.Interface.UiBuilder.OpenConfigUi += this.OpenConfig; + internal PluginUi(Plugin plugin) { + this.Plugin = plugin; + + this.Plugin.Interface.UiBuilder.Draw += this.Draw; + this.Plugin.Interface.UiBuilder.OpenConfigUi += this.OpenConfig; + } + + public void Dispose() { + this.Plugin.Interface.UiBuilder.OpenConfigUi -= this.OpenConfig; + this.Plugin.Interface.UiBuilder.Draw -= this.Draw; + } + + private void OpenConfig() { + this.Visible = true; + } + + private void Draw() { + if (!this.Visible) { + return; } - public void Dispose() { - this.Plugin.Interface.UiBuilder.OpenConfigUi -= this.OpenConfig; - this.Plugin.Interface.UiBuilder.Draw -= this.Draw; - } - - private void OpenConfig() { - this.Visible = true; - } - - private void Draw() { - if (!this.Visible) { - return; - } - - if (!ImGui.Begin(this.Plugin.Name, ref this.Visible)) { - ImGui.End(); - return; - } - - var anyChanged = ImGui.Checkbox("Enabled", ref this.Plugin.Config.Enabled); - ImGui.Separator(); - anyChanged |= ImGui.Checkbox("Obscure self (full name)", ref this.Plugin.Config.SelfFull); - ImGui.TreePush(); - anyChanged |= ImGui.Checkbox("First name", ref this.Plugin.Config.SelfFirst); - anyChanged |= ImGui.Checkbox("Last name", ref this.Plugin.Config.SelfLast); - ImGui.TreePop(); - anyChanged |= ImGui.Checkbox("Obscure party members", ref this.Plugin.Config.Party); - anyChanged |= ImGui.Checkbox("Obscure others", ref this.Plugin.Config.Others); - anyChanged |= ImGui.Checkbox("Exclude friends", ref this.Plugin.Config.ExcludeFriends); - - if (anyChanged) { - this.Plugin.SaveConfig(); - } - - ImGui.Separator(); - - if (ImGui.Button("Reset names")) { - if (this.Plugin.KeyState[VirtualKey.CONTROL] && this.Plugin.KeyState[VirtualKey.SHIFT]) { - this._debug ^= true; - } else { - this.Plugin.NameRepository.Reset(); - } - } - - if (this._debug) { - if (ImGui.CollapsingHeader("Debug")) { - ImGui.PushID("debug"); - try { - this.DrawDebug(); - } finally { - ImGui.PopID(); - } - } - } - + if (!ImGui.Begin(this.Plugin.Name, ref this.Visible)) { ImGui.End(); + return; } - private void DrawDebug() { - ImGui.TextUnformatted($"Initialised: {this.Plugin.NameRepository.Initialised}"); + var anyChanged = ImGui.Checkbox("Enabled", ref this.Plugin.Config.Enabled); + ImGui.Separator(); + anyChanged |= ImGui.Checkbox("Obscure self (full name)", ref this.Plugin.Config.SelfFull); + ImGui.TreePush(); + anyChanged |= ImGui.Checkbox("First name", ref this.Plugin.Config.SelfFirst); + anyChanged |= ImGui.Checkbox("Last name", ref this.Plugin.Config.SelfLast); + ImGui.TreePop(); + anyChanged |= ImGui.Checkbox("Obscure party members", ref this.Plugin.Config.Party); + anyChanged |= ImGui.Checkbox("Obscure others", ref this.Plugin.Config.Others); + anyChanged |= ImGui.Checkbox("Exclude friends", ref this.Plugin.Config.ExcludeFriends); - if (ImGui.Button("Load sheet")) { - this.Plugin.Functions.LoadSheet(Util.SheetName); + if (anyChanged) { + this.Plugin.SaveConfig(); + } + + ImGui.Separator(); + + if (ImGui.Button("Reset names")) { + if (this.Plugin.KeyState[VirtualKey.CONTROL] && this.Plugin.KeyState[VirtualKey.SHIFT]) { + this._debug ^= true; + } else { + this.Plugin.NameRepository.Reset(); + } + } + + if (this._debug) { + if (ImGui.CollapsingHeader("Debug")) { + ImGui.PushID("debug"); + try { + this.DrawDebug(); + } finally { + ImGui.PopID(); + } + } + } + + ImGui.End(); + } + + private void DrawDebug() { + ImGui.TextUnformatted($"Initialised: {this.Plugin.NameRepository.Initialised}"); + + if (ImGui.Button("Load sheet")) { + this.Plugin.Functions.LoadSheet(Util.SheetName); + } + + if (this.Plugin.TargetManager.Target is {} target) { + var npc = this.Plugin.AppearanceRepository.GetNpc(target.ObjectId); + ImGui.TextUnformatted(npc.ToString()); + ImGui.TextUnformatted(this.Plugin.DataManager.GetExcelSheet()!.GetRow(npc.RowId)!.Singular); + } + + ImGui.Separator(); + + if (ImGui.TreeNode("Name queue")) { + if (ImGui.InputInt("Columns", ref this._queueColumns)) { + this._queueColumns = Math.Max(1, this._queueColumns); } - ImGui.Separator(); - - if (ImGui.TreeNode("Name queue")) { - if (ImGui.InputInt("Columns", ref this._queueColumns)) { - this._queueColumns = Math.Max(1, this._queueColumns); + foreach (var (info, queue) in this.Plugin.NameRepository.ReadOnlyNames) { + if (!ImGui.CollapsingHeader($"{info}")) { + continue; } - foreach (var (info, queue) in this.Plugin.NameRepository.ReadOnlyNames) { - if (!ImGui.CollapsingHeader($"{info}")) { - continue; - } - - if (!ImGui.BeginTable($"{info} table", this._queueColumns)) { - continue; - } - - foreach (var name in queue) { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(name); - } - - ImGui.EndTable(); + if (!ImGui.BeginTable($"{info} table", this._queueColumns)) { + continue; } - ImGui.TreePop(); - } - - if (ImGui.TreeNode("Replacements")) { - if (ImGui.BeginTable("replacements", 2)) { - foreach (var (name, replacement) in this.Plugin.NameRepository.ReadonlyReplacements) { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(name); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted(replacement); - } - - ImGui.EndTable(); + foreach (var name in queue) { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(name); } - ImGui.TreePop(); - } - - if (ImGui.TreeNode("Last seen info")) { - if (ImGui.BeginTable("last seen info", 2)) { - foreach (var (name, info) in this.Plugin.NameRepository.ReadOnlyLastSeenInfo) { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(name); - - ImGui.TableNextColumn(); - ImGui.TextUnformatted($"{info}"); - } - - ImGui.EndTable(); - } + ImGui.EndTable(); } ImGui.TreePop(); } + + if (ImGui.TreeNode("Replacements")) { + if (ImGui.BeginTable("replacements", 2)) { + foreach (var (name, replacement) in this.Plugin.NameRepository.ReadonlyReplacements) { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(name); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(replacement); + } + + ImGui.EndTable(); + } + + ImGui.TreePop(); + } + + if (ImGui.TreeNode("Last seen info")) { + if (ImGui.BeginTable("last seen info", 2)) { + foreach (var (name, info) in this.Plugin.NameRepository.ReadOnlyLastSeenInfo) { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(name); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{info}"); + } + + ImGui.EndTable(); + } + } + + ImGui.TreePop(); } } diff --git a/NominaOcculta/Util.cs b/NominaOcculta/Util.cs index 2e6e26a..efa6a0f 100755 --- a/NominaOcculta/Util.cs +++ b/NominaOcculta/Util.cs @@ -3,80 +3,80 @@ using System.Collections.Generic; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; -namespace NominaOcculta { - internal static class Util { - internal const string SheetName = "CharaMakeName"; +namespace NominaOcculta; - internal static bool ContainsPlayerName(this SeString text, string name) { - foreach (var payload in text.Payloads) { - switch (payload) { - case PlayerPayload pp: - if (pp.PlayerName.Contains(name)) { - return true; - } +internal static class Util { + internal const string SheetName = "CharaMakeName"; - break; - case ITextProvider prov: - if (prov.Text.Contains(name)) { - return true; - } + internal static bool ContainsPlayerName(this SeString text, string name) { + foreach (var payload in text.Payloads) { + switch (payload) { + case PlayerPayload pp: + if (pp.PlayerName.Contains(name)) { + return true; + } - break; - } - } + break; + case ITextProvider prov: + if (prov.Text.Contains(name)) { + return true; + } - return false; - } - - internal static void ReplacePlayerName(this SeString text, string name, string replacement) { - if (string.IsNullOrEmpty(name)) { - return; - } - - foreach (var payload in text.Payloads) { - switch (payload) { - // case PlayerPayload pp: - // if (pp.PlayerName.Contains(name)) { - // pp.PlayerName = pp.PlayerName.Replace(name, replacement); - // } - // - // break; - case TextPayload txt: - if (txt.Text.Contains(name)) { - txt.Text = txt.Text.Replace(name, replacement); - } - - break; - } + break; } } - internal static byte[] Terminate(this byte[] bs) { - var terminated = new byte[bs.Length + 1]; - Array.Copy(bs, terminated, bs.Length); - terminated[^1] = 0; - return terminated; + return false; + } + + internal static void ReplacePlayerName(this SeString text, string name, string replacement) { + if (string.IsNullOrEmpty(name)) { + return; } - internal static SeString ReadRawSeString(IntPtr ptr) { - var bytes = ReadRawBytes(ptr); - return SeString.Parse(bytes); - } + foreach (var payload in text.Payloads) { + switch (payload) { + // case PlayerPayload pp: + // if (pp.PlayerName.Contains(name)) { + // pp.PlayerName = pp.PlayerName.Replace(name, replacement); + // } + // + // break; + case TextPayload txt: + if (txt.Text.Contains(name)) { + txt.Text = txt.Text.Replace(name, replacement); + } - private static unsafe byte[] ReadRawBytes(IntPtr ptr) { - if (ptr == IntPtr.Zero) { - return Array.Empty(); + break; } - - var bytes = new List(); - - var bytePtr = (byte*) ptr; - while (*bytePtr != 0) { - bytes.Add(*bytePtr); - bytePtr += 1; - } - - return bytes.ToArray(); } } + + internal static byte[] Terminate(this byte[] bs) { + var terminated = new byte[bs.Length + 1]; + Array.Copy(bs, terminated, bs.Length); + terminated[^1] = 0; + return terminated; + } + + internal static SeString ReadRawSeString(IntPtr ptr) { + var bytes = ReadRawBytes(ptr); + return SeString.Parse(bytes); + } + + private static unsafe byte[] ReadRawBytes(IntPtr ptr) { + if (ptr == IntPtr.Zero) { + return Array.Empty(); + } + + var bytes = new List(); + + var bytePtr = (byte*) ptr; + while (*bytePtr != 0) { + bytes.Add(*bytePtr); + bytePtr += 1; + } + + return bytes.ToArray(); + } }