From eaf6dec8e420fddf2c545db7e5a94a0aca5adfe3 Mon Sep 17 00:00:00 2001 From: Anna Clemens Date: Tue, 6 Apr 2021 18:36:09 -0400 Subject: [PATCH] feat: add quest and error toasts --- Dalamud/Game/Internal/Gui/ToastGui.cs | 435 +++++++++++++++--- .../Internal/Gui/ToastGuiAddressResolver.cs | 10 +- Dalamud/Interface/DalamudDataWindow.cs | 2 +- 3 files changed, 370 insertions(+), 77 deletions(-) diff --git a/Dalamud/Game/Internal/Gui/ToastGui.cs b/Dalamud/Game/Internal/Gui/ToastGui.cs index d92fe23bb..9ed50d7e1 100755 --- a/Dalamud/Game/Internal/Gui/ToastGui.cs +++ b/Dalamud/Game/Internal/Gui/ToastGui.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; - using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; @@ -10,31 +9,64 @@ namespace Dalamud.Game.Internal.Gui { public sealed class ToastGui : IDisposable { + internal const uint QuestToastCheckmarkMagic = 60081; + #region Events - public delegate void OnToastDelegate(ref SeString message, ref ToastOptions options, ref bool isHandled); + public delegate void OnNormalToastDelegate(ref SeString message, ref ToastOptions options, ref bool isHandled); + + public delegate void OnQuestToastDelegate(ref SeString message, ref QuestToastOptions options, ref bool isHandled); + + public delegate void OnErrorToastDelegate(ref SeString message, ref bool isHandled); /// /// Event that will be fired when a toast is sent by the game or a plugin. /// - public event OnToastDelegate OnToast; + public event OnNormalToastDelegate OnToast; + + /// + /// Event that will be fired when a quest toast is sent by the game or a plugin. + /// + public event OnQuestToastDelegate OnQuestToast; + + /// + /// Event that will be fired when an error toast is sent by the game or a plugin. + /// + public event OnErrorToastDelegate OnErrorToast; #endregion #region Hooks - private readonly Hook showToastHook; + private readonly Hook showNormalToastHook; + + private readonly Hook showQuestToastHook; + + private readonly Hook showErrorToastHook; #endregion - private delegate IntPtr ShowToastDelegate(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId); + #region Delegates + + private delegate IntPtr ShowNormalToastDelegate(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId); + + private delegate byte ShowQuestToastDelegate(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound); + + private delegate byte ShowErrorToastDelegate(IntPtr manager, IntPtr text, int layer, byte respectsHidingMaybe); + + private delegate IntPtr GetAtkModuleDelegate(IntPtr uiModule); + + #endregion private Dalamud Dalamud { get; } private ToastGuiAddressResolver Address { get; } - private Queue<(byte[] message, ToastOptions options)> ToastQueue { get; } = new Queue<(byte[] message, ToastOptions options)>(); + private Queue<(byte[], ToastOptions)> NormalQueue { get; } = new Queue<(byte[], ToastOptions)>(); + private Queue<(byte[], QuestToastOptions)> QuestQueue { get; } = new Queue<(byte[], QuestToastOptions)>(); + + private Queue ErrorQueue { get; } = new Queue(); public ToastGui(SigScanner scanner, Dalamud dalamud) { @@ -43,86 +75,40 @@ namespace Dalamud.Game.Internal.Gui this.Address = new ToastGuiAddressResolver(); this.Address.Setup(scanner); - Marshal.GetDelegateForFunctionPointer(this.Address.ShowToast); - this.showToastHook = new Hook(this.Address.ShowToast, new ShowToastDelegate(this.HandleToastDetour)); + this.showNormalToastHook = new Hook(this.Address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour)); + this.showQuestToastHook = new Hook(this.Address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour)); + this.showErrorToastHook = new Hook(this.Address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour)); } public void Enable() { - this.showToastHook.Enable(); + this.showNormalToastHook.Enable(); + this.showQuestToastHook.Enable(); + this.showErrorToastHook.Enable(); } public void Dispose() { - this.showToastHook.Dispose(); + this.showNormalToastHook.Dispose(); + this.showQuestToastHook.Dispose(); + this.showErrorToastHook.Dispose(); } - /// - /// Show a toast message with the given content. - /// - /// The message to be shown - /// Options for the toast - public void Show(string message, ToastOptions options = null) + private static byte[] Terminate(byte[] source) { - options ??= new ToastOptions(); - this.ToastQueue.Enqueue((Encoding.UTF8.GetBytes(message), options)); - } - - /// - /// Show a toast message with the given content. - /// - /// The message to be shown - /// Options for the toast - public void Show(SeString message, ToastOptions options = null) - { - options ??= new ToastOptions(); - this.ToastQueue.Enqueue((message.Encode(), options)); - } - - /// - /// Process the toast queue. - /// - internal void UpdateQueue() - { - while (this.ToastQueue.Count > 0) - { - var (message, options) = this.ToastQueue.Dequeue(); - this.Show(message, options); - } - } - - private void Show(byte[] bytes, ToastOptions options = null) - { - options ??= new ToastOptions(); - - var manager = this.Dalamud.Framework.Gui.GetUIModule(); - - // terminate the string - var terminated = new byte[bytes.Length + 1]; - Array.Copy(bytes, 0, terminated, 0, bytes.Length); + var terminated = new byte[source.Length + 1]; + Array.Copy(source, 0, terminated, 0, source.Length); terminated[^1] = 0; - unsafe - { - fixed (byte* ptr = terminated) - { - this.HandleToastDetour(manager, (IntPtr)ptr, 5, (byte)options.Position, (byte)options.Speed, 0); - } - } + return terminated; } - private IntPtr HandleToastDetour(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId) + private SeString ParseString(IntPtr text) { - if (text == IntPtr.Zero) - { - return IntPtr.Zero; - } - - // get the message as an sestring var bytes = new List(); unsafe { - var ptr = (byte*)text; + var ptr = (byte*) text; while (*ptr != 0) { bytes.Add(*ptr); @@ -130,13 +116,187 @@ namespace Dalamud.Game.Internal.Gui } } + // call events + return this.Dalamud.SeStringManager.Parse(bytes.ToArray()); + } + + /// + /// Process the toast queue. + /// + internal void UpdateQueue() + { + while (this.NormalQueue.Count > 0) + { + var (message, options) = this.NormalQueue.Dequeue(); + this.ShowNormal(message, options); + } + + while (this.QuestQueue.Count > 0) + { + var (message, options) = this.QuestQueue.Dequeue(); + this.ShowQuest(message, options); + } + + while (this.ErrorQueue.Count > 0) + { + var message = this.ErrorQueue.Dequeue(); + this.ShowError(message); + } + } + + #region Normal API + + /// + /// Show a toast message with the given content. + /// + /// The message to be shown + /// Options for the toast + public void ShowNormal(string message, ToastOptions options = null) + { + options ??= new ToastOptions(); + this.NormalQueue.Enqueue((Encoding.UTF8.GetBytes(message), options)); + } + + /// + /// Show a toast message with the given content. + /// + /// The message to be shown + /// Options for the toast + public void ShowNormal(SeString message, ToastOptions options = null) + { + options ??= new ToastOptions(); + this.NormalQueue.Enqueue((message.Encode(), options)); + } + + private void ShowNormal(byte[] bytes, ToastOptions options = null) + { + options ??= new ToastOptions(); + + var manager = this.Dalamud.Framework.Gui.GetUIModule(); + + // terminate the string + var terminated = Terminate(bytes); + + unsafe + { + fixed (byte* ptr = terminated) + { + this.HandleNormalToastDetour(manager, (IntPtr) ptr, 5, (byte) options.Position, (byte) options.Speed, 0); + } + } + } + + #endregion + + #region Quest API + + /// + /// Show a quest toast message with the given content. + /// + /// The message to be shown + /// Options for the toast + public void ShowQuest(string message, QuestToastOptions options = null) + { + options ??= new QuestToastOptions(); + this.QuestQueue.Enqueue((Encoding.UTF8.GetBytes(message), options)); + } + + /// + /// Show a quest toast message with the given content. + /// + /// The message to be shown + /// Options for the toast + public void ShowQuest(SeString message, QuestToastOptions options = null) + { + options ??= new QuestToastOptions(); + this.QuestQueue.Enqueue((message.Encode(), options)); + } + + private void ShowQuest(byte[] bytes, QuestToastOptions options = null) + { + options ??= new QuestToastOptions(); + + var manager = this.Dalamud.Framework.Gui.GetUIModule(); + + // terminate the string + var terminated = Terminate(bytes); + + var (ioc1, ioc2) = options.DetermineParameterOrder(); + + unsafe + { + fixed (byte* ptr = terminated) + { + this.HandleQuestToastDetour( + manager, + (int) options.Position, + (IntPtr) ptr, + ioc1, + options.PlaySound ? (byte) 1 : (byte) 0, + ioc2, + 0); + } + } + } + + #endregion + + #region Error API + + /// + /// Show an error toast message with the given content. + /// + /// The message to be shown + public void ShowError(string message) + { + this.ErrorQueue.Enqueue(Encoding.UTF8.GetBytes(message)); + } + + /// + /// Show an error toast message with the given content. + /// + /// The message to be shown + public void ShowError(SeString message) + { + this.ErrorQueue.Enqueue(message.Encode()); + } + + private void ShowError(byte[] bytes) + { + var uiModule = this.Dalamud.Framework.Gui.GetUIModule(); + var vtbl = Marshal.ReadIntPtr(uiModule); + var atkModulePtr = Marshal.ReadIntPtr(vtbl + (7 * 8)); + var getAtkModule = Marshal.GetDelegateForFunctionPointer(atkModulePtr); + var manager = getAtkModule(uiModule); + + // terminate the string + var terminated = Terminate(bytes); + + unsafe + { + fixed (byte* ptr = terminated) + { + this.HandleErrorToastDetour(manager, (IntPtr) ptr, 10, 0); + } + } + } + + #endregion + + private IntPtr HandleNormalToastDetour(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId) + { + if (text == IntPtr.Zero) + { + return IntPtr.Zero; + } + // call events var isHandled = false; - var str = this.Dalamud.SeStringManager.Parse(bytes.ToArray()); + var str = this.ParseString(text); var options = new ToastOptions { Position = (ToastPosition) isTop, - Speed = (ToastSpeed)isFast, + Speed = (ToastSpeed) isFast, }; this.OnToast?.Invoke(ref str, ref options, ref isHandled); @@ -147,16 +307,89 @@ namespace Dalamud.Game.Internal.Gui return IntPtr.Zero; } - var encoded = str.Encode(); - var terminated = new byte[encoded.Length + 1]; - Array.Copy(encoded, 0, terminated, 0, encoded.Length); - terminated[^1] = 0; + var terminated = Terminate(str.Encode()); unsafe { fixed (byte* message = terminated) { - return this.showToastHook.Original(manager, (IntPtr)message, layer, (byte)options.Position, (byte)options.Speed, logMessageId); + return this.showNormalToastHook.Original(manager, (IntPtr) message, layer, (byte) options.Position, (byte) options.Speed, logMessageId); + } + } + } + + private byte HandleQuestToastDetour(IntPtr manager, int position, IntPtr text, uint iconOrCheck1, byte playSound, uint iconOrCheck2, byte alsoPlaySound) + { + if (text == IntPtr.Zero) + { + return 0; + } + + // call events + var isHandled = false; + var str = this.ParseString(text); + var options = new QuestToastOptions + { + Position = (QuestToastPosition) position, + DisplayCheckmark = iconOrCheck1 == QuestToastCheckmarkMagic, + IconId = iconOrCheck1 == QuestToastCheckmarkMagic ? iconOrCheck2 : iconOrCheck1, + PlaySound = playSound == 1, + }; + + this.OnQuestToast?.Invoke(ref str, ref options, ref isHandled); + + // do nothing if handled + if (isHandled) + { + return 0; + } + + var terminated = Terminate(str.Encode()); + + var (ioc1, ioc2) = options.DetermineParameterOrder(); + + unsafe + { + fixed (byte* message = terminated) + { + return this.showQuestToastHook.Original( + manager, + (int) options.Position, + (IntPtr) message, + ioc1, + options.PlaySound ? (byte) 1 : (byte) 0, + ioc2, + 0); + } + } + } + + private byte HandleErrorToastDetour(IntPtr manager, IntPtr text, int layer, byte respectsHidingMaybe) + { + if (text == IntPtr.Zero) + { + return 0; + } + + // call events + var isHandled = false; + var str = this.ParseString(text); + + this.OnErrorToast?.Invoke(ref str, ref isHandled); + + // do nothing if handled + if (isHandled) + { + return 0; + } + + var terminated = Terminate(str.Encode()); + + unsafe + { + fixed (byte* message = terminated) + { + return this.showErrorToastHook.Original(manager, (IntPtr) message, layer, respectsHidingMaybe); } } } @@ -164,8 +397,14 @@ namespace Dalamud.Game.Internal.Gui public sealed class ToastOptions { + /// + /// Gets or sets the position of the toast on the screen. + /// public ToastPosition Position { get; set; } = ToastPosition.Bottom; + /// + /// Gets or sets the speed of the toast. + /// public ToastSpeed Speed { get; set; } = ToastSpeed.Slow; } @@ -177,7 +416,55 @@ namespace Dalamud.Game.Internal.Gui public enum ToastSpeed : byte { + /// + /// The toast will take longer to disappear (around four seconds). + /// Slow = 0, + + /// + /// The toast will disappear more quickly (around two seconds). + /// Fast = 1, } + + public sealed class QuestToastOptions + { + /// + /// Gets or sets the position of the toast on the screen. + /// + public QuestToastPosition Position { get; set; } = QuestToastPosition.Centre; + + /// + /// Gets or sets the ID of the icon that will appear in the toast. + /// + /// This may be 0 for no icon. + /// + public uint IconId { get; set; } = 0; + + /// + /// Gets or sets a value indicating whether the toast will show a checkmark after appearing. + /// + public bool DisplayCheckmark { get; set; } = false; + + /// + /// Gets or sets a value indicating whether the toast will play a completion sound. + /// + /// This only works if is non-zero or is true. + /// + public bool PlaySound { get; set; } = false; + + internal (uint, uint) DetermineParameterOrder() + { + return this.DisplayCheckmark + ? (ToastGui.QuestToastCheckmarkMagic, this.IconId) + : (this.IconId, 0); + } + } + + public enum QuestToastPosition + { + Centre = 0, + Right = 1, + Left = 2, + } } diff --git a/Dalamud/Game/Internal/Gui/ToastGuiAddressResolver.cs b/Dalamud/Game/Internal/Gui/ToastGuiAddressResolver.cs index e5ee511e1..969b15ecb 100755 --- a/Dalamud/Game/Internal/Gui/ToastGuiAddressResolver.cs +++ b/Dalamud/Game/Internal/Gui/ToastGuiAddressResolver.cs @@ -4,11 +4,17 @@ namespace Dalamud.Game.Internal.Gui { public class ToastGuiAddressResolver : BaseAddressResolver { - public IntPtr ShowToast { get; private set; } + public IntPtr ShowNormalToast { get; private set; } + + public IntPtr ShowQuestToast { get; private set; } + + public IntPtr ShowErrorToast { get; private set; } protected override void Setup64Bit(SigScanner sig) { - ShowToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 83 3D ?? ?? ?? ?? ??"); + this.ShowNormalToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 83 3D ?? ?? ?? ?? ??"); + this.ShowQuestToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 40 83 3D ?? ?? ?? ?? ??"); + this.ShowErrorToast = sig.ScanText("40 56 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 41 8B F0"); } } } diff --git a/Dalamud/Interface/DalamudDataWindow.cs b/Dalamud/Interface/DalamudDataWindow.cs index 38f462ff0..5335c30fb 100644 --- a/Dalamud/Interface/DalamudDataWindow.cs +++ b/Dalamud/Interface/DalamudDataWindow.cs @@ -277,7 +277,7 @@ namespace Dalamud.Interface ImGui.InputText("Toast text", ref this.inputTextToast, 200); if (ImGui.Button("Show toast")) - this.dalamud.Framework.Gui.Toast.Show(this.inputTextToast); + this.dalamud.Framework.Gui.Toast.ShowNormal(this.inputTextToast); break; } }