using System; using System.Collections.Generic; using System.Linq; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; using KamiToolKit.Classes; using KamiToolKit.Enums; namespace KamiToolKit.Overlay; public unsafe class OverlayController : IDisposable { private readonly Dictionary> overlayNodes = []; private readonly Dictionary addonState = []; private ControllerState controllerState = ControllerState.WaitForNameplate; public OverlayController() { ClearState(); DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, "NamePlate", OnNamePlatePreFinalize); foreach (var overlayLayer in Enum.GetValues()) { var addonName = overlayLayer.Description; DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PreUpdate, addonName, OnOverlayAddonUpdate); DalamudInterface.Instance.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, addonName, OnOverlayAddonFinalize); } BeginStateCheck(); } public void Dispose() { DalamudInterface.Instance.AddonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "NamePlate"); DalamudInterface.Instance.AddonLifecycle.UnregisterListener(OnOverlayAddonFinalize, OnOverlayAddonUpdate); foreach (var node in overlayNodes.SelectMany(nodeList => nodeList.Value)) { node.Dispose(); } overlayNodes.Clear(); } // // State management (framework thread) // private void ClearState() { controllerState = ControllerState.WaitForNameplate; foreach (var overlayLayer in Enum.GetValues()) { addonState[overlayLayer] = OverlayAddonState.None; } } private void BeginStateCheck() { DalamudInterface.Instance.Framework.Update -= CheckOverlayState; DalamudInterface.Instance.Framework.Update += CheckOverlayState; } private void CheckOverlayState(IFramework framework) { switch (controllerState) { case ControllerState.WaitForNameplate: CheckNameplateReady(); break; case ControllerState.WaitForReady: CheckOverlayAddonsReady(); break; case ControllerState.Ready: DalamudInterface.Instance.Framework.Update -= CheckOverlayState; break; } } private void CheckNameplateReady() { var nameplate = RaptureAtkUnitManager.Instance()->GetAddonByName("NamePlate"); if (nameplate is null) return; if (!nameplate->IsReady) return; foreach (var overlayLayer in Enum.GetValues()) { var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(overlayLayer.Description); if (addon is null) { if (addonState[overlayLayer] == OverlayAddonState.None) { addonState[overlayLayer] = OverlayAddonState.WaitForReady; CreateOverlayAddon(overlayLayer).Open(); } } else { addonState[overlayLayer] = OverlayAddonState.WaitForReady; } } controllerState = ControllerState.WaitForReady; } private void CheckOverlayAddonsReady() { var totalAddons = Enum.GetValues().Length; var totalAddonsReady = 0; foreach (var overlayLayer in Enum.GetValues()) { var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(overlayLayer.Description); if (addon is null) continue; if (!addon->IsReady) continue; if (addonState[overlayLayer] is OverlayAddonState.WaitForReady) { AttachAllNodes(overlayLayer); addonState[overlayLayer] = OverlayAddonState.Ready; } totalAddonsReady++; } if (totalAddonsReady == totalAddons) { controllerState = ControllerState.Ready; } } private void AttachAllNodes(OverlayLayer layer) { if (!overlayNodes.TryGetValue(layer, out var list)) return; var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(layer.Description); if (addon is null) return; foreach (var node in list) { AttachNode(addon, node); } } // // Public node access // public void CreateNode(Func creationFunction) => DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => { AddNode(creationFunction()); }); public void AddNode(OverlayNode node) => DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => { overlayNodes.TryAdd(node.OverlayLayer, []); if (overlayNodes[node.OverlayLayer].Contains(node)) return; overlayNodes[node.OverlayLayer].Add(node); if (addonState[node.OverlayLayer] is not OverlayAddonState.Ready) return; var addon = RaptureAtkUnitManager.Instance()->GetAddonByName(node.OverlayLayer.Description); if (addon is null) return; AttachNode(addon, node); }); public void RemoveNode(OverlayNode node) => DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => { if (!overlayNodes.TryGetValue(node.OverlayLayer, out var list)) return; if (list.Remove(node)) { node.Dispose(); } }); public void RemoveAllNodes() => DalamudInterface.Instance.Framework.RunOnFrameworkThread(() => { foreach (var node in overlayNodes.SelectMany(set => set.Value).ToList()) { RemoveNode(node); } }); // // Events // private void OnNamePlatePreFinalize(AddonEvent type, AddonArgs args) { ClearState(); foreach (var overlayLayer in Enum.GetValues()) { if (!overlayNodes.TryGetValue(overlayLayer, out var list)) continue; foreach (var node in list) { node.DetachNode(); } } BeginStateCheck(); } private void OnOverlayAddonFinalize(AddonEvent type, AddonArgs args) { var addon = (AtkUnitBase*)args.Addon.Address; var overlayLayer = addon->DepthLayer.GetOverlayLayer(); if (overlayNodes.TryGetValue(overlayLayer, out var list)) { foreach (var node in list) { node.DetachNode(); } } addonState[overlayLayer] = OverlayAddonState.None; } private void OnOverlayAddonUpdate(AddonEvent type, AddonArgs args) { var addon = (AtkUnitBase*)args.Addon.Address; var overlayLayer = addon->DepthLayer.GetOverlayLayer(); if (addonState[overlayLayer] is not OverlayAddonState.Ready) return; if (!overlayNodes.TryGetValue(overlayLayer, out var list)) return; foreach (var node in list) { node.Update(); } } // // Helpers // private static OverlayAddon CreateOverlayAddon(OverlayLayer layer) => new() { Title = layer.Description, InternalName = layer.Description, DepthLayer = layer.DepthLayer, IsOverlayAddon = true, }; private static void AttachNode(AtkUnitBase* addon, OverlayNode node) { node.NodeId = (uint)addon->UldManager.NodeListCount + 1; node.AttachNode(addon); } }