using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Command; using Dalamud.Interface; using Dalamud.Interface.Textures; using Dalamud.Plugin; using Dalamud.Plugin.Services; using HSUI.Config; using HSUI.Config.Profiles; using HSUI.Helpers; using HSUI.Interface; using HSUI.Interface.GeneralElements; using HSUI.Interface.Nameplates; using HSUI.Interface.Party; using HSUI.Interface.PartyCooldowns; using Dalamud.Bindings.ImGui; using KamiToolKit; using System; using System.IO; using System.Reflection; namespace HSUI { public class Plugin : IDalamudPlugin { public static IBuddyList BuddyList { get; private set; } = null!; public static IClientState ClientState { get; private set; } = null!; public static ICommandManager CommandManager { get; private set; } = null!; public static ICondition Condition { get; private set; } = null!; public static IDalamudPluginInterface PluginInterface { get; private set; } = null!; public static IDataManager DataManager { get; private set; } = null!; public static IFramework Framework { get; private set; } = null!; public static IGameGui GameGui { get; private set; } = null!; public static IJobGauges JobGauges { get; private set; } = null!; public static IObjectTable ObjectTable { get; private set; } = null!; public static ISigScanner SigScanner { get; private set; } = null!; public static IGameInteropProvider GameInteropProvider { get; private set; } = null!; public static ITargetManager TargetManager { get; private set; } = null!; public static IUiBuilder UiBuilder { get; private set; } = null!; public static IPartyList PartyList { get; private set; } = null!; public static IPluginLog Logger { get; private set; } = null!; public static ITextureProvider TextureProvider { get; private set; } = null!; public static IAddonLifecycle AddonLifecycle { get; private set; } = null!; public static IChatGui Chat { get; private set; } = null!; public static ISeStringEvaluator SeStringEvaluator { get; private set; } = null!; public static IKeyState? KeyState { get; private set; } public static IGamepadState? GamepadState { get; private set; } public static ISharedImmediateTexture? BannerTexture; public static string AssemblyLocation { get; private set; } = ""; public string Name => "HSUI"; public static string Version { get; private set; } = ""; private HudManager _hudManager = null!; public delegate void JobChangedEventHandler(uint jobId); public static event JobChangedEventHandler? JobChangedEvent; private uint _jobId = 0; public static double LoadTime = -1; public Plugin( IBuddyList buddyList, IClientState clientState, ICommandManager commandManager, ICondition condition, IDalamudPluginInterface pluginInterface, IDataManager dataManager, IFramework framework, IGameGui gameGui, IJobGauges jobGauges, IObjectTable objectTable, IPartyList partyList, ISigScanner sigScanner, IGameInteropProvider gameInteropProvider, ITargetManager targetManager, IPluginLog logger, ITextureProvider textureProvider, IAddonLifecycle addonLifecycle, IChatGui chat, ISeStringEvaluator seStringEvaluator, IGamepadState? gamepadState = null, IKeyState? keyState = null) { BuddyList = buddyList; ClientState = clientState; CommandManager = commandManager; Condition = condition; PluginInterface = pluginInterface; DataManager = dataManager; Framework = framework; GameGui = gameGui; JobGauges = jobGauges; ObjectTable = objectTable; PartyList = partyList; SigScanner = sigScanner; GameInteropProvider = gameInteropProvider; TargetManager = targetManager; UiBuilder = PluginInterface.UiBuilder; Logger = logger; TextureProvider = textureProvider; AddonLifecycle = addonLifecycle; Chat = chat; SeStringEvaluator = seStringEvaluator; KeyState = keyState; GamepadState = gamepadState; if (pluginInterface.AssemblyLocation.DirectoryName != null) { AssemblyLocation = pluginInterface.AssemblyLocation.DirectoryName + "\\"; } else { AssemblyLocation = Assembly.GetExecutingAssembly().Location; } Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "1.0.0.0"; KamiToolKitLibrary.Initialize(pluginInterface); FontsManager.Initialize(AssemblyLocation); BarTexturesManager.Initialize(AssemblyLocation); LoadBanner(); ConfigurationManager.Initialize(); ProfilesManager.Initialize(); ConfigurationManager.Instance.LoadOrInitializeFiles(); FontsManager.Instance.LoadConfig(); BarTexturesManager.Instance.LoadConfig(); ClipRectsHelper.Initialize(); GlobalColors.Initialize(); InputsHelper.Initialize(); NameplatesManager.Initialize(); PartyManager.Initialize(); AllianceManager.Initialize(); PartyCooldownsManager.Initialize(); PullTimerHelper.Initialize(); ActionBarsManager.Initialize(); ControllerHotbarHelper.SetGamepadState(GamepadState); TextTagsHelper.Initialize(); TooltipsHelper.Initialize(); ActionChatLinkHelper.Initialize(); PetRenamerHelper.Initialize(); HonorificHelper.Initialize(); WotsitHelper.Initialize(); WhosTalkingHelper.Initialize(); _hudManager = new HudManager(); UiBuilder.Draw += Draw; UiBuilder.OpenConfigUi += OpenConfigUi; FontsManager.Instance.BuildFonts(); CommandManager.AddHandler( "/hsui", new CommandInfo(PluginCommand) { HelpMessage = "Opens the HSUI configuration window.\n" + "/hsui toggle → Toggles HUD visibility.\n" + "/hsui show → Shows HUD.\n" + "/hsui hide → Hides HUD.\n" + "/hsui toggledefaulthud → Toggles the game's Job Gauges visibility.\n" + "/hsui forcejob → Forces HSUI to show the HUD for the given Job short name.\n" + "/hsui profile → Switch to the given profile.\n" + "/hsui mouse → Toggles special input handling for extra mouse buttons when hovering HSUI elements.\n" + "/hsui debug dragdrop → Toggles debug logging for action bar drag & drop (all hotbars). Logs SwapSlots before/after, release-outside, item payload resolution.\n" + "/hsui debug tooltips → Toggles debug logging for tooltips.\n" + "/hsui debug hud → Dumps HudLayout addon names and hashes to the log (for HUD hiding).\n" + "/hsui debug hotbarslots → Dumps all hotbar slot CommandType/CommandId to the log (for SwapSlots diagnosis).\n" + "/hsui debug macro → Dumps HotbarSlot + RaptureMacroModule.Macro memory for the slot (macro persistence).\n" + "/hsui_alliance_debug → Dumps alliance frame detection and letter-mapping debug info (run while in alliance raid).", ShowInHelp = true } ); CommandManager.AddHandler( "/hsui_alliance_debug", new CommandInfo((_, _) => { HSUI.Interface.Party.AllianceManager.DumpAllianceDebugToLog(); Chat.Print("HSUI: Alliance debug info dumped to the log (Dalamud log window or dev plugin)."); }) { HelpMessage = "Dumps alliance frame detection and letter-mapping debug info. Run while in an alliance raid.", ShowInHelp = true } ); CommandManager.AddHandler( "/hui", new CommandInfo(PluginCommand) { HelpMessage = "Opens the HSUI configuration window.\n" + "/hui toggle → Toggles HUD visibility.\n" + "/hui show → Shows HUD.\n" + "/hui hide → Hides HUD.\n" + "/hui toggledefaulthud → Toggles the game's Job Gauges visibility.\n" + "/hui forcejob → Forces HSUI to show the HUD for the given Job short name.\n" + "/hui profile → Switch to the given profile.\n" + "/hui mouse → Toggles special input handling for extra mouse buttons when hovering HSUI elements.", ShowInHelp = true } ); WotsitHelper.Instance?.Update(); if (ConfigurationManager.Instance?.IsChangelogWindowOpened == false) { LoadTime = ImGui.GetTime(); } } public void Dispose() { Logger.Info("Starting HSUI Dispose v" + Version); Dispose(true); GC.SuppressFinalize(this); } private void LoadBanner() { string bannerImage = Path.Combine(Path.GetDirectoryName(AssemblyLocation.TrimEnd('\\')) ?? "", "Media", "Images", "banner_short_x150.png"); if (File.Exists(bannerImage)) { try { BannerTexture = TextureProvider.GetFromFile(bannerImage); } catch (Exception ex) { Logger.Error($"Image failed to load. {bannerImage}\n\n{ex}"); } } else { Logger.Debug($"Image doesn't exist. {bannerImage}"); } } private void PluginCommand(string command, string arguments) { var configManager = ConfigurationManager.Instance; if (configManager.IsConfigWindowOpened && !configManager.LockHUD) { configManager.LockHUD = true; } else { bool printHUDStatus = false; switch (arguments) { case "toggle": ConfigurationManager.Instance.ShowHUD = !ConfigurationManager.Instance.ShowHUD; printHUDStatus = true; break; case "toggledefaulthud": HUDOptionsConfig config = ConfigurationManager.Instance.GetConfigObject(); config.HideDefaultJobGauges = !config.HideDefaultJobGauges; string defaultJobGaugeStr = config.HideDefaultJobGauges ? "hidden" : "visible"; Chat.Print($"Default Job Gauges are {defaultJobGaugeStr}."); break; case "show": ConfigurationManager.Instance.ShowHUD = true; printHUDStatus = true; break; case "hide": ConfigurationManager.Instance.ShowHUD = false; printHUDStatus = true; break; case { } argument when argument.StartsWith("mouse"): string[] mouseArgs = argument.Split(" "); if (mouseArgs.Length > 1) { if (mouseArgs[1] == "on") InputsHelper.Instance?.ToggleProxy(true); else if (mouseArgs[1] == "off") InputsHelper.Instance?.ToggleProxy(false); } string mouseStr = InputsHelper.Instance?.IsProxyEnabled == true ? "enabled" : "disabled"; Chat.Print($"HSUI special mouse handling is currently {mouseStr}."); break; case { } argument when argument.StartsWith("forcejob"): string[] args = argument.Split(" "); if (args.Length > 1) { if (args[1].Equals("off", StringComparison.OrdinalIgnoreCase)) { ForcedJob.Enabled = false; return; } var job = typeof(JobIDs).GetField(args[1].ToUpperInvariant()); if (job != null) { ForcedJob.Enabled = true; ForcedJob.ForcedJobId = (uint)(job.GetValue(null) ?? JobIDs.ACN); } } break; case { } argument when argument.StartsWith("profile"): string[] profile = argument.Split(" ", 2); if (profile.Length > 1) { ProfilesManager.Instance?.CheckUpdateSwitchCurrentProfile(profile[1]); } break; case "debug dragdrop": case "debug drag": var hotbarConfigs = configManager.GetObjects(); bool newState = hotbarConfigs.Count == 0 || !hotbarConfigs.Exists(c => c.DebugDragDrop); foreach (var bar in hotbarConfigs) bar.DebugDragDrop = newState; configManager.SaveConfigurations(); Chat.Print($"HSUI drag-drop debug logging is {(newState ? "ON" : "OFF")} for all hotbars."); break; case "debug tooltips": var tooltipsConfig = configManager.GetConfigObject(); tooltipsConfig.DebugTooltips = !tooltipsConfig.DebugTooltips; configManager.SaveConfigurations(); Chat.Print($"HSUI tooltip debug logging is {(tooltipsConfig.DebugTooltips ? "ON" : "OFF")}."); break; case "debug hud": HSUI.Helpers.HudLayoutHashHelper.DumpHudLayoutAddonsToLog(); Chat.Print("HSUI: HudLayout addon names and hashes dumped to the log (Dalamud log window or dev plugin)."); break; case "debug hotbarslots": HSUI.Helpers.ActionBarsManager.Instance?.DumpSlotStateToLog(); Chat.Print("HSUI: Hotbar slot state dumped to the log."); break; case "debug macromenu": HSUI.Helpers.ActionBarsManager.Instance?.DumpMacroMenuToLog(); Chat.Print("HSUI: Macro menu state dumped to the log (open Macro menu first for best data)."); break; case { } arg when arg.StartsWith("debug macro"): var macroParts = arg.Split(" ", StringSplitOptions.RemoveEmptyEntries); if (macroParts.Length >= 4 && int.TryParse(macroParts[2], out int macroBar) && int.TryParse(macroParts[3], out int macroSlot)) { HSUI.Helpers.ActionBarsManager.Instance?.DumpMacroSlotMemoryToLog(macroBar, macroSlot); Chat.Print($"HSUI: Macro slot memory for bar {macroBar} slot {macroSlot} dumped to the log."); } else Chat.Print("Usage: /hsui debug macro (e.g. /hsui debug macro 1 2)"); break; case { } when string.Equals(arguments.Trim(), "debug alliance", StringComparison.OrdinalIgnoreCase) || string.Equals(arguments.Trim(), "debugalliance", StringComparison.OrdinalIgnoreCase): HSUI.Interface.Party.AllianceManager.DumpAllianceDebugToLog(); Chat.Print("HSUI: Alliance debug info dumped to the log (Dalamud log window or dev plugin)."); break; default: configManager.ToggleConfigWindow(); break; } if (printHUDStatus) { string hudStr = ConfigurationManager.Instance.ShowHUD ? "visible" : "hidden"; Chat.Print($"HSUI HUD is {hudStr}."); } } } private void UpdateJob() { var player = ObjectTable.LocalPlayer; if (player is null) return; uint newJobId = player.ClassJob.RowId; if (ForcedJob.Enabled) newJobId = ForcedJob.ForcedJobId; if (_jobId != newJobId) { _jobId = newJobId; JobChangedEvent?.Invoke(_jobId); } } private static bool _drawInProgress; private void Draw() { if (_drawInProgress) return; _drawInProgress = true; try { UpdateJob(); UiBuilder.OverrideGameCursor = false; ConfigurationManager.Instance.Draw(); try { NameplatesManager.Instance?.Update(); } catch (Exception ex) { Logger.Warning($"NameplatesManager.Update: {ex.Message}"); } try { PartyManager.Instance?.Update(); } catch (Exception ex) { Logger.Warning($"PartyManager.Update: {ex.Message}"); } try { AllianceManager.Instance?.Update(); } catch (Exception ex) { Logger.Warning($"AllianceManager.Update: {ex.Message}"); } try { using (FontsManager.Instance.PushDefaultFont()) { _hudManager?.Draw(_jobId); } } catch (Exception e) { Logger.Error("Something went wrong!:\n" + e.StackTrace); } InputsHelper.Instance.OnFrameEnd(); } finally { _drawInProgress = false; } } private void OpenConfigUi() { ConfigurationManager.Instance.ToggleConfigWindow(); } protected virtual void Dispose(bool disposing) { if (!disposing) return; // Restore game window WndProc first so left-click works again immediately, even if teardown throws. InputsHelper.RestoreWndProcIfNeeded(); // Stop UI callbacks first so no Draw/OpenConfigUi runs during teardown try { Logger.Info("\tRemoving commands..."); CommandManager.RemoveHandler("/hsui"); CommandManager.RemoveHandler("/hsui debugalliance"); CommandManager.RemoveHandler("/hui"); } catch (Exception e) { Logger.Error("Error removing commands: " + e.Message); } try { Logger.Info("\tUnsubscribing from UIBuilder events..."); UiBuilder.Draw -= Draw; UiBuilder.OpenConfigUi -= OpenConfigUi; } catch (Exception e) { Logger.Error("Error unsubscribing UIBuilder: " + e.Message); } try { Logger.Info("\tSaving configurations..."); ConfigurationManager.Instance?.SaveConfigurations(true); ConfigurationManager.Instance?.CloseConfigWindow(); } catch (Exception e) { Logger.Error("Error saving/closing config: " + e.Message); } TryDispose("InputsHelper", () => InputsHelper.Instance?.Dispose()); TryDispose("HudManager", () => _hudManager?.Dispose()); TryDispose("BarTexturesManager", () => BarTexturesManager.Instance?.Dispose()); TryDispose("ClipRectsHelper", () => ClipRectsHelper.Instance?.Dispose()); TryDispose("ExperienceHelper", () => ExperienceHelper.Instance?.Dispose()); TryDispose("FontsManager", () => FontsManager.Instance?.Dispose()); TryDispose("GlobalColors", () => GlobalColors.Instance?.Dispose()); TryDispose("NameplatesManager", () => NameplatesManager.Instance?.Dispose()); TryDispose("PartyCooldownsManager", () => PartyCooldownsManager.Instance?.Dispose()); TryDispose("AllianceManager", () => AllianceManager.Instance?.Dispose()); TryDispose("PartyManager", () => PartyManager.Instance?.Dispose()); TryDispose("PullTimerHelper", () => PullTimerHelper.Instance?.Dispose()); TryDispose("ActionBarsManager", () => ActionBarsManager.Instance?.Dispose()); TryDispose("ProfilesManager", () => ProfilesManager.Instance?.Dispose()); TryDispose("SpellHelper", () => SpellHelper.Instance?.Dispose()); TryDispose("TooltipsHelper", () => TooltipsHelper.Instance?.Dispose()); TryDispose("ActionChatLinkHelper", () => ActionChatLinkHelper.Instance?.Dispose()); TryDispose("HonorificHelper", () => HonorificHelper.Instance?.Dispose()); TryDispose("PetRenamerHelper", () => PetRenamerHelper.Instance?.Dispose()); TryDispose("WotsitHelper", () => WotsitHelper.Instance?.Dispose()); TryDispose("WhosTalkingHelper", () => WhosTalkingHelper.Instance?.Dispose()); try { Logger.Info("\tRebuilding fonts..."); UiBuilder.FontAtlas.BuildFontsAsync(); } catch (Exception e) { Logger.Error("Error rebuilding fonts: " + e.Message); } try { KamiToolKitLibrary.Dispose(); } catch (Exception e) { Logger.Error("Error disposing KamiToolKit: " + e.Message); } TryDispose("ConfigurationManager", () => ConfigurationManager.Instance?.Dispose()); } private static void TryDispose(string name, Action dispose) { try { Logger.Info("\tDisposing " + name + "..."); dispose(); } catch (Exception e) { Logger.Error("Error disposing " + name + ": " + e.Message); } } } }