using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; using System.Text; using Dalamud; using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.Plugin; using FFXIVClientStructs.FFXIV.Component.GUI; namespace TheGreatSeparator { // ReSharper disable once ClassNeverInstantiated.Global public class TheGreatSeparator : IDalamudPlugin { public string Name => "The Great Separator"; private static class Signatures { internal const string ShowFlyText = "E8 ?? ?? ?? ?? FF C7 41 D1 C7"; internal const string SprintfNumber = "48 83 EC 28 44 8B C9"; internal const string FlyTextStringify = "45 33 C0 C6 44 24 ?? ?? 8B D3 E8 ?? ?? ?? ?? 41 8B CF E8"; internal const string HotbarManaStringify = "45 33 C0 44 88 64 24 ?? 44 88 64 24 ?? 8B 14 08 48 8B CE E8"; internal const string PartyListStringify = "45 33 C0 C6 44 24 ?? ?? 8B D5 E8 ?? ?? ?? ?? EB"; internal const string Separator = "44 0F B6 05 ?? ?? ?? ?? 45 84 C0 74 36 F6 87"; } private static readonly byte[] ThirdArgOne = { 0x41, 0xB0, 0x01, }; private delegate void ShowFlyTextDelegate(IntPtr addon, uint actorIndex, uint messageMax, IntPtr numbers, int offsetNum, int offsetNumMax, IntPtr strings, int offsetStr, int offsetStrMax, int a10); private Hook? ShowFlyTextHook { get; } private delegate IntPtr SprintfNumberDelegate(uint number); private Hook? SprintfNumberHook { get; } [PluginService] internal DalamudPluginInterface Interface { get; init; } = null!; [PluginService] internal CommandManager CommandManager { get; init; } = null!; [PluginService] internal SigScanner SigScanner { get; init; } = null!; internal Configuration Config { get; } internal PluginUi Ui { get; } private Commands Commands { get; } private Dictionary OldBytes { get; } = new(); private byte OriginalSeparator { get; } private IntPtr SeparatorPtr { get; } = IntPtr.Zero; public TheGreatSeparator() { this.Config = this.Interface.GetPluginConfig() as Configuration ?? new Configuration(); this.Config.Initialise(this.Interface); this.ConfigureInstructions(); this.Ui = new PluginUi(this); this.Commands = new Commands(this); if (this.SigScanner.TryScanText(Signatures.ShowFlyText, out var showFlyPtr)) { this.ShowFlyTextHook = new Hook(showFlyPtr + 9, this.ShowFlyTextDetour); this.ShowFlyTextHook.Enable(); } if (this.SigScanner.TryScanText(Signatures.SprintfNumber, out var sprintfPtr)) { this.SprintfNumberHook = new Hook(sprintfPtr, this.SprintfNumberDetour); this.SprintfNumberHook.Enable(); } if (this.SigScanner.TryGetStaticAddressFromSig(Signatures.Separator, out var separatorPtr)) { this.SeparatorPtr = separatorPtr; this.OriginalSeparator = Marshal.ReadByte(this.SeparatorPtr); } this.SetSeparator(this.Config.CustomSeparator); } public void Dispose() { this.SprintfNumberHook?.Dispose(); this.ShowFlyTextHook?.Dispose(); this.RestoreAllBytes(); this.Commands.Dispose(); this.Ui.Dispose(); if (this.SeparatorPtr != IntPtr.Zero && this.OriginalSeparator != 0) { Marshal.WriteByte(this.SeparatorPtr, this.OriginalSeparator); } } internal void SetSeparator(char? sep) { if (this.SeparatorPtr == IntPtr.Zero) { return; } var separator = (byte?) sep ?? this.OriginalSeparator; if (separator == 0) { separator = (byte) ','; } Marshal.WriteByte(this.SeparatorPtr, separator); } internal void ConfigureInstructions() { this.ConfigureInstruction(Signatures.FlyTextStringify, this.Config.FlyText); this.ConfigureInstruction(Signatures.HotbarManaStringify, this.Config.AbilityCost); this.ConfigureInstruction(Signatures.PartyListStringify, this.Config.PartyList); } private void ConfigureInstruction(string sig, bool enabled) { if (!this.SigScanner.TryScanText(sig, out var ptr)) { return; } if (enabled) { this.ReplaceBytes(ptr); } else { this.RestoreBytes(ptr); } } private void ReplaceBytes(IntPtr ptr) { if (this.OldBytes.ContainsKey(ptr)) { return; } SafeMemory.ReadBytes(ptr, ThirdArgOne.Length, out var oldBytes); SafeMemory.WriteBytes(ptr, ThirdArgOne); this.OldBytes[ptr] = oldBytes; } private void RestoreBytes(IntPtr ptr) { if (!this.OldBytes.TryGetValue(ptr, out var oldBytes)) { return; } SafeMemory.WriteBytes(ptr, oldBytes); this.OldBytes.Remove(ptr); } private void RestoreAllBytes() { foreach (var ptr in this.OldBytes.Keys.ToList()) { this.RestoreBytes(ptr); } } private unsafe IntPtr SprintfNumberDetour(uint number) { var ret = (byte*) this.SprintfNumberHook!.Original(number); if (!this.Config.AbilityTooltip) { goto Return; } var nfi = (NumberFormatInfo) NumberFormatInfo.CurrentInfo.Clone(); if (this.Config.CustomSeparator != null) { nfi.NumberGroupSeparator = this.Config.CustomSeparator.ToString(); } var str = number.ToString("N0", nfi); var strBytes = Encoding.UTF8.GetBytes(str); fixed (byte* bytesPtr = strBytes) { Buffer.MemoryCopy(bytesPtr, ret, 0x40, strBytes.Length); } *(ret + strBytes.Length) = 0; Return: return (IntPtr) ret; } private unsafe void ShowFlyTextDetour(IntPtr addon, uint actorIndex, uint messageMax, IntPtr numbers, int offsetNum, int offsetNumMax, IntPtr strings, int offsetStr, int offsetStrMax, int a10) { this.ShowFlyTextHook!.Original(addon, actorIndex, messageMax, numbers, offsetNum, offsetNumMax, strings, offsetStr, offsetStrMax, a10); if (!this.Config.FlyText) { return; } static void Action(IntPtr ptr) { // only check text nodes var node = (AtkResNode*) ptr; if (node->Type != NodeType.Text) { return; } var text = (AtkTextNode*) node; var font = (text->AlignmentFontType & 0xF0) >> 4; // only touch text nodes with a font above four and less than eight if (font is not (> 4 and < 8)) { return; } // only touch text nodes with a string starting with a digit var stringPtr = text->NodeText.StringPtr; if (stringPtr == null || !char.IsDigit((char) *stringPtr)) { return; } // set the font type of the node to 4 for non-number support text->AlignmentFontType = (byte) ((text->AlignmentFontType & 0xF) | (4 << 4)); } var unit = (AtkUnitBase*) addon; if (unit->RootNode != null) { this.TraverseNodes(unit->RootNode, Action); } for (var i = 0; i < unit->UldManager.NodeListCount; i++) { var node = unit->UldManager.NodeList[i]; this.TraverseNodes(node, Action); } } private unsafe void TraverseNodes(AtkResNode* node, Action action, bool siblings = true) { if (node == null) { return; } action((IntPtr) node); if ((int) node->Type < 1000) { this.TraverseNodes(node->ChildNode, action); } else { var comp = (AtkComponentNode*) node; for (var i = 0; i < comp->Component->UldManager.NodeListCount; i++) { this.TraverseNodes(comp->Component->UldManager.NodeList[i], action); } } if (!siblings) { return; } var prev = node; while ((prev = prev->PrevSiblingNode) != null) { this.TraverseNodes(prev, action, false); } var next = node; while ((next = next->NextSiblingNode) != null) { this.TraverseNodes(next, action, false); } } } }