From 689984ee3e0fa67b1ab8747b7a7a1d65e5dbc9cd Mon Sep 17 00:00:00 2001 From: Anna Date: Sun, 7 Jul 2024 14:49:45 -0400 Subject: [PATCH] refactor: use a combined queue --- client/Messages.cs | 35 +++---- client/PluginUi.cs | 29 ++++++ client/Ui/MainWindowTabs/MessageList.cs | 2 +- client/Ui/MainWindowTabs/Settings.cs | 5 + client/Util/SemaphoreExt.cs | 13 +++ client/Vfx.cs | 123 +++++++++++++++++------- 6 files changed, 149 insertions(+), 58 deletions(-) create mode 100644 client/Util/SemaphoreExt.cs diff --git a/client/Messages.cs b/client/Messages.cs index c519a4d..d385f58 100644 --- a/client/Messages.cs +++ b/client/Messages.cs @@ -39,8 +39,13 @@ internal class Messages : IDisposable { private Plugin Plugin { get; } private SemaphoreSlim CurrentMutex { get; } = new(1, 1); - private Dictionary Current { get; } = new(); - private Queue SpawnQueue { get; } = new(); + private Dictionary Current { get; } = []; + internal IReadOnlyDictionary CurrentCloned { + get { + using var guard = this.CurrentMutex.With(); + return this.Current.ToDictionary(e => e.Key, e => e.Value); + } + } private HashSet Trials { get; } = []; private HashSet DeepDungeons { get; } = []; @@ -83,7 +88,6 @@ internal class Messages : IDisposable { this.Plugin.Framework.Update += this.DetermineIfSpawn; this.Plugin.Framework.Update += this.RemoveConditionally; - this.Plugin.Framework.Update += this.HandleSpawnQueue; this.Plugin.ClientState.TerritoryChanged += this.TerritoryChanged; this.Plugin.ClientState.Login += this.SpawnVfx; this.Plugin.ClientState.Logout += this.RemoveVfx; @@ -93,7 +97,6 @@ internal class Messages : IDisposable { this.Plugin.ClientState.Logout -= this.RemoveVfx; this.Plugin.ClientState.Login -= this.SpawnVfx; this.Plugin.ClientState.TerritoryChanged -= this.TerritoryChanged; - this.Plugin.Framework.Update -= this.HandleSpawnQueue; this.Plugin.Framework.Update -= this.RemoveConditionally; this.Plugin.Framework.Update -= this.DetermineIfSpawn; @@ -154,20 +157,6 @@ internal class Messages : IDisposable { this._inGpose = nowGpose; } - private unsafe void HandleSpawnQueue(IFramework framework) { - if (!this.SpawnQueue.TryDequeue(out var message)) { - return; - } - - Plugin.Log.Debug($"spawning vfx for {message.Id}"); - var rotation = Quaternion.CreateFromYawPitchRoll(message.Yaw, 0, 0); - var path = GetPath(this.Plugin.DataManager, message); - if (this.Plugin.Vfx.SpawnStatic(message.Id, path, message.Position, rotation) == null) { - Plugin.Log.Debug("trying again"); - this.SpawnQueue.Enqueue(message); - } - } - internal void SpawnVfx() { var territory = this.Plugin.ClientState.TerritoryType; if (territory == 0 || this.Plugin.Config.BannedTerritories.Contains(territory)) { @@ -236,7 +225,9 @@ internal class Messages : IDisposable { foreach (var message in messages) { this.Current[message.Id] = message; - this.SpawnQueue.Enqueue(message); + var path = GetPath(this.Plugin.DataManager, message); + var rotation = Quaternion.CreateFromYawPitchRoll(message.Yaw, 0, 0); + this.Plugin.Vfx.QueueSpawn(message.Id, path, message.Position, rotation); } } finally { this.CurrentMutex.Release(); @@ -244,7 +235,7 @@ internal class Messages : IDisposable { } internal void RemoveVfx() { - this.Plugin.Vfx.RemoveAll(); + this.Plugin.Vfx.QueueRemoveAll(); } internal void Clear() { @@ -287,7 +278,9 @@ internal class Messages : IDisposable { this.CurrentMutex.Release(); } - this.SpawnQueue.Enqueue(message); + var path = GetPath(this.Plugin.DataManager, message); + var rotation = Quaternion.CreateFromYawPitchRoll(message.Yaw, 0, 0); + this.Plugin.Vfx.QueueSpawn(message.Id, path, message.Position, rotation); } internal void Remove(Guid id) { diff --git a/client/PluginUi.cs b/client/PluginUi.cs index c90d232..86267a7 100644 --- a/client/PluginUi.cs +++ b/client/PluginUi.cs @@ -1,3 +1,4 @@ +using Dalamud.Interface.Utility; using ImGuiNET; using OrangeGuidanceTomestone.Ui; @@ -9,6 +10,11 @@ public class PluginUi : IDisposable { internal MainWindow MainWindow { get; } internal Viewer Viewer { get; } internal ViewerButton ViewerButton { get; } + #if DEBUG + internal bool Debug = true; + #else + internal bool Debug; + #endif private List<(string, string)> Modals { get; } = []; private Queue ToShow { get; } = new(); @@ -33,6 +39,10 @@ public class PluginUi : IDisposable { } private void Draw() { + if (this.Debug) { + this.DrawDebug(); + } + this.MainWindow.Draw(); this.ViewerButton.Draw(); this.Viewer.Draw(); @@ -80,4 +90,23 @@ public class PluginUi : IDisposable { this.Modals.Add((id, text)); this.ToShow.Enqueue(id); } + + private void DrawDebug() { + foreach (var msg in this.Plugin.Messages.CurrentCloned.Values) { + if (!this.Plugin.GameGui.WorldToScreen(msg.Position, out var screen)) { + continue; + } + + ImGui.GetBackgroundDrawList().AddCircleFilled(screen, 6f * ImGuiHelpers.GlobalScale, 0xff0000ff); + + var label = msg.Id.ToString("N"); + var size = ImGui.CalcTextSize(label); + ImGui.GetBackgroundDrawList().AddRectFilled( + screen, + screen + size, + 0xff000000 + ); + ImGui.GetForegroundDrawList().AddText(screen, 0xffffffff, label); + } + } } diff --git a/client/Ui/MainWindowTabs/MessageList.cs b/client/Ui/MainWindowTabs/MessageList.cs index 5273f11..f4e8316 100644 --- a/client/Ui/MainWindowTabs/MessageList.cs +++ b/client/Ui/MainWindowTabs/MessageList.cs @@ -187,7 +187,7 @@ internal class MessageList : ITab { if (resp.IsSuccessStatusCode) { this.Refresh(); - this.Plugin.Vfx.RemoveStatic(id); + this.Plugin.Vfx.QueueRemove(id); this.Plugin.Messages.Remove(id); } }); diff --git a/client/Ui/MainWindowTabs/Settings.cs b/client/Ui/MainWindowTabs/Settings.cs index fee701f..96be8ff 100644 --- a/client/Ui/MainWindowTabs/Settings.cs +++ b/client/Ui/MainWindowTabs/Settings.cs @@ -38,6 +38,7 @@ internal class Settings : ITab { ("Viewer", this.DrawViewer), ("Unlocks", this.DrawUnlocks), ("Account", this.DrawAccount), + ("Debug", this.DrawDebug), }; } @@ -227,6 +228,10 @@ internal class Settings : ITab { this.DeleteAccountButton(); } + private void DrawDebug(ref bool anyChanged, ref bool vfx) { + ImGui.Checkbox("Show debug information", ref this.Plugin.Ui.Debug); + } + private void ExtraCodeInput() { ImGui.InputText("Extra code", ref this._extraCode, 128); if (!ImGui.Button("Claim")) { diff --git a/client/Util/SemaphoreExt.cs b/client/Util/SemaphoreExt.cs new file mode 100644 index 0000000..87c7b24 --- /dev/null +++ b/client/Util/SemaphoreExt.cs @@ -0,0 +1,13 @@ +namespace OrangeGuidanceTomestone.Util; + +internal static class SemaphoreExt { + internal static OnDispose With(this SemaphoreSlim semaphore) { + semaphore.Wait(); + return new OnDispose(() => semaphore.Release()); + } + + internal static async Task WithAsync(this SemaphoreSlim semaphore, CancellationToken token = default) { + await semaphore.WaitAsync(token); + return new OnDispose(() => semaphore.Release()); + } +} diff --git a/client/Vfx.cs b/client/Vfx.cs index 8850b4b..7139b0f 100644 --- a/client/Vfx.cs +++ b/client/Vfx.cs @@ -4,6 +4,7 @@ using System.Text; using Dalamud.Memory; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using OrangeGuidanceTomestone.Util; namespace OrangeGuidanceTomestone; @@ -11,24 +12,30 @@ internal unsafe class Vfx : IDisposable { private static readonly byte[] Pool = "Client.System.Scheduler.Instance.VfxObject\0"u8.ToArray(); [Signature("E8 ?? ?? ?? ?? F3 0F 10 35 ?? ?? ?? ?? 48 89 43 08")] - private delegate* unmanaged _staticVfxCreate; + private readonly delegate* unmanaged _staticVfxCreate; [Signature("E8 ?? ?? ?? ?? 8B 4B 7C 85 C9")] - private delegate* unmanaged _staticVfxRun; + private readonly delegate* unmanaged _staticVfxRun; [Signature("40 53 48 83 EC 20 48 8B D9 48 8B 89 ?? ?? ?? ?? 48 85 C9 74 28 33 D2 E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9")] - private delegate* unmanaged _staticVfxRemove; + private readonly delegate* unmanaged _staticVfxRemove; private Plugin Plugin { get; } + private SemaphoreSlim Mutex { get; } = new(1, 1); private Dictionary Spawned { get; } = []; - private Queue RemoveQueue { get; } = []; + private Queue Queue { get; } = []; private bool _disposed; + private enum Mode { + Add, + Remove, + } + internal Vfx(Plugin plugin) { this.Plugin = plugin; this.Plugin.GameInteropProvider.InitializeFromAttributes(this); - this.Plugin.Framework.Update += this.OnFrameworkUpdate; + this.Plugin.Framework.Update += this.HandleQueues; } public void Dispose() { @@ -37,30 +44,77 @@ internal unsafe class Vfx : IDisposable { } this._disposed = true; - this.RemoveAll(); + this.Plugin.Framework.Update -= this.HandleQueues; + this.RemoveAllSync(); } - private void OnFrameworkUpdate(IFramework framework) { - if (this._disposed && this.RemoveQueue.Count == 0) { - this.Plugin.Framework.Update -= this.OnFrameworkUpdate; - } - - if (!this.RemoveQueue.TryDequeue(out var vfx)) { + private void HandleQueues(IFramework framework) { + if (!this.Queue.TryDequeue(out var action)) { return; } - if (!this.RemoveStatic((VfxStruct*) vfx)) { - this.RemoveQueue.Enqueue(vfx); + switch (action) { + case AddQueueAction add: { + using var guard = this.Mutex.With(); + Plugin.Log.Debug($"adding vfx for {add.Id}"); + if (this.Spawned.Remove(add.Id, out var existing)) { + Plugin.Log.Warning($"vfx for {add.Id} already exists, queuing remove"); + this.Queue.Enqueue(new RemoveRawQueueAction(existing)); + } + + var vfx = this.SpawnStatic(add.Id, add.Path, add.Position, add.Rotation); + this.Spawned[add.Id] = (nint) vfx; + break; + } + + case RemoveQueueAction remove: { + using var guard = this.Mutex.With(); + Plugin.Log.Debug($"removing vfx for {remove.Id}"); + if (!this.Spawned.Remove(remove.Id, out var ptr)) { + break; + } + + this.RemoveStatic((VfxStruct*) ptr); + break; + }; + + case RemoveRawQueueAction remove: { + Plugin.Log.Debug($"removing raw vfx at {remove.Pointer:X}"); + this.RemoveStatic((VfxStruct*) remove.Pointer); + break; + } } } - internal void RemoveAll() { - foreach (var spawned in this.Spawned.Keys.ToArray()) { - this.RemoveStatic(spawned); + internal void RemoveAllSync() { + using var guard = this.Mutex.With(); + + foreach (var spawned in this.Spawned.Values.ToArray()) { + this.RemoveStatic((VfxStruct*) spawned); + } + + this.Spawned.Clear(); + } + + internal void QueueSpawn(Guid id, string path, Vector3 pos, Quaternion rotation) { + using var guard = this.Mutex.With(); + this.Queue.Enqueue(new AddQueueAction(id, path, pos, rotation)); + } + + internal void QueueRemove(Guid id) { + using var guard = this.Mutex.With(); + this.Queue.Enqueue(new RemoveQueueAction(id)); + } + + internal void QueueRemoveAll() { + using var guard = this.Mutex.With(); + + foreach (var id in this.Spawned.Keys) { + this.Queue.Enqueue(new RemoveQueueAction(id)); } } - internal VfxStruct* SpawnStatic(Guid id, string path, Vector3 pos, Quaternion rotation) { + private VfxStruct* SpawnStatic(Guid id, string path, Vector3 pos, Quaternion rotation) { VfxStruct* vfx; fixed (byte* p = Encoding.UTF8.GetBytes(path).NullTerminate()) { fixed (byte* pool = Pool) { @@ -82,27 +136,11 @@ internal unsafe class Vfx : IDisposable { this._staticVfxRun(vfx, 0.0f, -1); - this.Spawned[id] = (nint) vfx; - return vfx; } - internal bool RemoveStatic(VfxStruct* vfx) { - var result = this._staticVfxRemove(vfx); - var success = result != 0; - if (!success) { - this.RemoveQueue.Enqueue((nint) vfx); - } - - return success; - } - - internal void RemoveStatic(Guid id) { - if (!this.Spawned.Remove(id, out var vfx)) { - return; - } - - this.RemoveStatic((VfxStruct*) vfx); + private void RemoveStatic(VfxStruct* vfx) { + this._staticVfxRemove(vfx); } [StructLayout(LayoutKind.Explicit)] @@ -132,3 +170,16 @@ internal unsafe class Vfx : IDisposable { public int StaticTarget; } } + +internal interface IQueueAction; + +internal sealed record AddQueueAction( + Guid Id, + string Path, + Vector3 Position, + Quaternion Rotation +) : IQueueAction; + +internal sealed record RemoveQueueAction(Guid Id) : IQueueAction; + +internal sealed record RemoveRawQueueAction(nint Pointer) : IQueueAction;