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