using System.Diagnostics; using System.Numerics; using System.Runtime.InteropServices; using System.Text; using Dalamud.Memory; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using OrangeGuidanceTomestone.Util; namespace OrangeGuidanceTomestone; 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 readonly delegate* unmanaged _staticVfxCreate; [Signature("E8 ?? ?? ?? ?? 8B 4B 7C 85 C9")] 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 readonly delegate* unmanaged _staticVfxRemove; private Plugin Plugin { get; } internal SemaphoreSlim Mutex { get; } = new(1, 1); internal Dictionary Spawned { get; } = []; private Queue Queue { get; } = []; private bool _disposed; private readonly Stopwatch _queueTimer = Stopwatch.StartNew(); internal Vfx(Plugin plugin) { this.Plugin = plugin; this.Plugin.GameInteropProvider.InitializeFromAttributes(this); this.Plugin.Framework.Update += this.HandleQueues; } public void Dispose() { if (this._disposed) { return; } this._disposed = true; this.Plugin.Framework.Update -= this.HandleQueues; this.RemoveAllSync(); } private void HandleQueues(IFramework framework) { this._queueTimer.Restart(); while (this._queueTimer.Elapsed < TimeSpan.FromMilliseconds(1)) { if (!this.Queue.TryDequeue(out var action)) { return; } 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.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 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)); } } private VfxStruct* SpawnStatic(string path, Vector3 pos, Quaternion rotation) { VfxStruct* vfx; fixed (byte* p = Encoding.UTF8.GetBytes(path).NullTerminate()) { fixed (byte* pool = Pool) { vfx = this._staticVfxCreate(p, pool); } } if (vfx == null) { return null; } // update position vfx->Position = new Vector3(pos.X, pos.Y, pos.Z); // update rotation vfx->Rotation = new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W); // remove flag that sometimes causes vfx to not appear? vfx->SomeFlags &= 0xF7; // update vfx->Flags |= 2; this._staticVfxRun(vfx, 0.0f, -1); return vfx; } private void RemoveStatic(VfxStruct* vfx) { this._staticVfxRemove(vfx); } [StructLayout(LayoutKind.Explicit)] internal struct VfxStruct { [FieldOffset(0x38)] public byte Flags; [FieldOffset(0x50)] public Vector3 Position; [FieldOffset(0x60)] public Quaternion Rotation; [FieldOffset(0x70)] public Vector3 Scale; [FieldOffset(0x128)] public int ActorCaster; [FieldOffset(0x130)] public int ActorTarget; [FieldOffset(0x1B8)] public int StaticCaster; [FieldOffset(0x1C0)] public int StaticTarget; [FieldOffset(0x248)] public byte SomeFlags; } } 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;