using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using ChatTwo.Http; using ChatTwo.Ipc; using ChatTwo.Resources; using ChatTwo.Ui; using ChatTwo.Util; using Dalamud.Game.ClientState.Conditions; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Bindings.ImGui; using Dalamud.Interface.ImGuiFileDialog; namespace ChatTwo; // ReSharper disable once ClassNeverInstantiated.Global public sealed class Plugin : IDalamudPlugin { public const string PluginName = "Chat 2"; [PluginService] public static IPluginLog Log { get; private set; } = null!; [PluginService] public static IDalamudPluginInterface Interface { get; private set; } = null!; [PluginService] public static IChatGui ChatGui { get; private set; } = null!; [PluginService] public static IClientState ClientState { get; private set; } = null!; [PluginService] public static ICommandManager CommandManager { get; private set; } = null!; [PluginService] public static ICondition Condition { get; private set; } = null!; [PluginService] public static IDataManager DataManager { get; private set; } = null!; [PluginService] public static IFramework Framework { get; private set; } = null!; [PluginService] public static IGameGui GameGui { get; private set; } = null!; [PluginService] public static IKeyState KeyState { get; private set; } = null!; [PluginService] public static IObjectTable ObjectTable { get; private set; } = null!; [PluginService] public static IPartyList PartyList { get; private set; } = null!; [PluginService] public static ITargetManager TargetManager { get; private set; } = null!; [PluginService] public static ITextureProvider TextureProvider { get; private set; } = null!; [PluginService] public static IGameInteropProvider GameInteropProvider { get; private set; } = null!; [PluginService] public static IGameConfig GameConfig { get; private set; } = null!; [PluginService] public static INotificationManager Notification { get; private set; } = null!; [PluginService] public static IAddonLifecycle AddonLifecycle { get; private set; } = null!; [PluginService] public static IPlayerState PlayerState { get; private set; } = null!; [PluginService] public static ISeStringEvaluator Evaluator { get; private set; } = null!; public static Configuration Config = null!; public static FileDialogManager FileDialogManager { get; private set; } = null!; public readonly WindowSystem WindowSystem = new(PluginName); public SettingsWindow SettingsWindow { get; } public ChatLogWindow ChatLogWindow { get; } public DbViewer DbViewer { get; } public InputPreview InputPreview { get; } public CommandHelpWindow CommandHelpWindow { get; } public SeStringDebugger SeStringDebugger { get; } public DebuggerWindow DebuggerWindow { get; } internal Commands Commands { get; } internal GameFunctions.GameFunctions Functions { get; } internal MessageManager MessageManager { get; } internal IpcManager Ipc { get; } internal ExtraChat ExtraChat { get; } internal TypingIpc TypingIpc { get; } internal FontManager FontManager { get; } public readonly ServerCore ServerCore; internal int DeferredSaveFrames = -1; internal DateTime GameStarted { get; } // Tab management needs to happen outside the chatlog window class for access reasons internal int LastTab { get; set; } internal int? WantedTab { get; set; } internal Tab CurrentTab { get { var i = LastTab; return i > -1 && i < Config.Tabs.Count ? Config.Tabs[i] : new Tab(); } } public Plugin() { try { GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime(); Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); #pragma warning disable CS0618 // Type or member is obsolete // TODO Remove after 01.07.2026 // Migrate old channel values if (Config.Version <= 5) { foreach (var tab in Config.Tabs) { if (tab.ChatCodes.Count > 0) { tab.SelectedChannels = tab.ChatCodes.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value)); tab.ChatCodes.Clear(); } if (Config.InactivityHideChannels.Count > 0) { Config.InactivityHideChannelsV2 = Config.InactivityHideChannels.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value)); Config.InactivityHideChannels.Clear(); } Config.Version = 6; SaveConfig(); } } #pragma warning restore CS0618 // Type or member is obsolete // Hellion Chat v6→v7: seed Privacy-First defaults. if (Config.Version <= 6) { Config.PrivacyFilterEnabled = true; Config.PrivacyPersistChannels = [..Privacy.PrivacyDefaults.PrivacyFirstWhitelist]; Config.PrivacyPersistUnknownChannels = false; Config.Version = 7; SaveConfig(); Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification { Title = "Hellion Chat", Content = "Privacy filter activated by default. Settings → Privacy to adjust.", Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info, InitialDuration = TimeSpan.FromSeconds(15), }); } if (Config.Tabs.Count == 0) Config.Tabs.Add(TabsUtil.VanillaGeneral); LanguageChanged(Interface.UiLanguage); ImGuiUtil.Initialize(this); FileDialogManager = new FileDialogManager(); // Function call this in its ctor if the player is already logged in ServerCore = new ServerCore(this); Commands = new Commands(); Functions = new GameFunctions.GameFunctions(this); Ipc = new IpcManager(); TypingIpc = new TypingIpc(this); ExtraChat = new ExtraChat(); FontManager = new FontManager(); MessageManager = new MessageManager(this); // Does it require UI? ChatLogWindow = new ChatLogWindow(this); SettingsWindow = new SettingsWindow(this); DbViewer = new DbViewer(this); InputPreview = new InputPreview(ChatLogWindow); CommandHelpWindow = new CommandHelpWindow(ChatLogWindow); SeStringDebugger = new SeStringDebugger(this); DebuggerWindow = new DebuggerWindow(this); WindowSystem.AddWindow(ChatLogWindow); WindowSystem.AddWindow(SettingsWindow); WindowSystem.AddWindow(DbViewer); WindowSystem.AddWindow(InputPreview); WindowSystem.AddWindow(CommandHelpWindow); WindowSystem.AddWindow(SeStringDebugger); WindowSystem.AddWindow(DebuggerWindow); FontManager.BuildFonts(); Interface.UiBuilder.DisableCutsceneUiHide = true; Interface.UiBuilder.DisableGposeUiHide = true; // let all the other components register, then initialize commands Commands.Initialise(); if (Interface.Reason is not PluginLoadReason.Boot) MessageManager.FilterAllTabsAsync(); Framework.Update += FrameworkUpdate; Interface.UiBuilder.Draw += Draw; Interface.LanguageChanged += LanguageChanged; if (Config.ShowEmotes) Task.Run(EmoteCache.LoadData); #if !DEBUG // Avoid 300ms hitch when sending first message by preloading the // auto-translate cache. Don't do this in debug because it makes // profiling difficult. AutoTranslate.PreloadCache(); #endif // Automatically start the webserver if requested if (Config.WebinterfaceAutoStart) { Task.Run(() => { ServerCore.Start(); ServerCore.Run(); }); } } catch (Exception ex) { Log.Error(ex, "Plugin load threw an error, turning off plugin"); Dispose(); // Re-throw the exception to fail the plugin load. throw; } } // Suppressing this warning because Dispose() is called in Plugin() if the // load fails, so some values may not be initialized. [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract")] public void Dispose() { Interface.LanguageChanged -= LanguageChanged; Interface.UiBuilder.Draw -= Draw; Framework.Update -= FrameworkUpdate; GameFunctions.GameFunctions.SetChatInteractable(true); WindowSystem?.RemoveAllWindows(); ChatLogWindow?.Dispose(); DbViewer?.Dispose(); InputPreview?.Dispose(); SettingsWindow?.Dispose(); DebuggerWindow?.Dispose(); SeStringDebugger?.Dispose(); TypingIpc?.Dispose(); ExtraChat?.Dispose(); Ipc?.Dispose(); MessageManager?.DisposeAsync().AsTask().Wait(); Functions?.Dispose(); Commands?.Dispose(); EmoteCache.Dispose(); ServerCore?.DisposeAsync().AsTask().Wait(); } private void Draw() { ChatLogWindow.BeginFrame(); if (Config.HideInLoadingScreens && Condition[ConditionFlag.BetweenAreas]) { ChatLogWindow.FinalizeFrame(); TypingIpc.Update(); return; } ChatLogWindow.HideStateCheck(); Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden; ChatLogWindow.DefaultText = ImGui.GetStyle().Colors[(int) ImGuiCol.Text]; using ((Config.FontsEnabled ? FontManager.RegularFont : FontManager.Axis).Push()) WindowSystem.Draw(); ChatLogWindow.FinalizeFrame(); TypingIpc.Update(); FileDialogManager.Draw(); } internal void SaveConfig() { Interface.SavePluginConfig(Config); } internal void LanguageChanged(string langCode) { var info = Config.LanguageOverride is LanguageOverride.None ? new CultureInfo(langCode) : new CultureInfo(Config.LanguageOverride.Code()); Language.Culture = info; } private static readonly string[] ChatAddonNames = [ "ChatLog", "ChatLogPanel_0", "ChatLogPanel_1", "ChatLogPanel_2", "ChatLogPanel_3" ]; private void FrameworkUpdate(IFramework framework) { if (DeferredSaveFrames >= 0 && DeferredSaveFrames-- == 0) SaveConfig(); if (!Config.HideChat) return; foreach (var name in ChatAddonNames) if (GameFunctions.GameFunctions.IsAddonInteractable(name)) GameFunctions.GameFunctions.SetAddonInteractable(name, false); } public static bool InBattle => Condition[ConditionFlag.InCombat]; public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene]; public static bool CutsceneActive => Condition[ConditionFlag.OccupiedInCutSceneEvent] || Condition[ConditionFlag.WatchingCutscene78]; }