using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Game.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.StyleEditor; using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; using ImGuiNET; using PInvoke; using Serilog.Events; namespace Dalamud.Interface.Internal { /// /// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping. /// internal class DalamudInterface : IDisposable { private static readonly ModuleLog Log = new("DUI"); private readonly ChangelogWindow changelogWindow; private readonly ColorDemoWindow colorDemoWindow; private readonly ComponentDemoWindow componentDemoWindow; private readonly CreditsWindow creditsWindow; private readonly DataWindow dataWindow; private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow; private readonly IMEWindow imeWindow; private readonly ConsoleWindow consoleWindow; private readonly PluginStatWindow pluginStatWindow; private readonly PluginInstallerWindow pluginWindow; private readonly SettingsWindow settingsWindow; private readonly SelfTestWindow selfTestWindow; private readonly StyleEditorWindow styleEditorWindow; private ulong frameCount = 0; #if DEBUG private bool isImGuiDrawDevMenu = true; #else private bool isImGuiDrawDevMenu = false; #endif private bool isImGuiDrawDemoWindow = false; /// /// Initializes a new instance of the class. /// public DalamudInterface() { var configuration = Service.Get(); this.WindowSystem = new WindowSystem("DalamudCore"); this.changelogWindow = new ChangelogWindow() { IsOpen = false }; this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; this.creditsWindow = new CreditsWindow() { IsOpen = false }; this.dataWindow = new DataWindow() { IsOpen = false }; this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow() { IsOpen = false }; this.imeWindow = new IMEWindow() { IsOpen = false }; this.consoleWindow = new ConsoleWindow() { IsOpen = configuration.LogOpenAtStartup }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; this.pluginWindow = new PluginInstallerWindow() { IsOpen = false }; this.settingsWindow = new SettingsWindow() { IsOpen = false }; this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; this.WindowSystem.AddWindow(this.changelogWindow); this.WindowSystem.AddWindow(this.colorDemoWindow); this.WindowSystem.AddWindow(this.componentDemoWindow); this.WindowSystem.AddWindow(this.creditsWindow); this.WindowSystem.AddWindow(this.dataWindow); this.WindowSystem.AddWindow(this.gamepadModeNotifierWindow); this.WindowSystem.AddWindow(this.imeWindow); this.WindowSystem.AddWindow(this.consoleWindow); this.WindowSystem.AddWindow(this.pluginStatWindow); this.WindowSystem.AddWindow(this.pluginWindow); this.WindowSystem.AddWindow(this.settingsWindow); this.WindowSystem.AddWindow(this.selfTestWindow); this.WindowSystem.AddWindow(this.styleEditorWindow); ImGuiManagedAsserts.AssertsEnabled = true; Service.Get().Draw += this.OnDraw; } /// /// Gets the controlling all Dalamud-internal windows. /// public WindowSystem WindowSystem { get; init; } /// /// Gets or sets a value indicating whether the /xldev menu is open. /// public bool IsDevMenuOpen { get => this.isImGuiDrawDevMenu; set => this.isImGuiDrawDevMenu = value; } /// public void Dispose() { Service.Get().Draw -= this.OnDraw; this.WindowSystem.RemoveAllWindows(); this.creditsWindow.Dispose(); this.consoleWindow.Dispose(); } #region Open /// /// Opens the . /// public void OpenChangelogWindow() => this.changelogWindow.IsOpen = true; /// /// Opens the . /// public void OpenColorsDemoWindow() => this.colorDemoWindow.IsOpen = true; /// /// Opens the . /// public void OpenComponentDemoWindow() => this.componentDemoWindow.IsOpen = true; /// /// Opens the . /// public void OpenCreditsWindow() => this.creditsWindow.IsOpen = true; /// /// Opens the . /// /// The data kind to switch to after opening. public void OpenDataWindow(string dataKind = null) { this.dataWindow.IsOpen = true; if (dataKind != null && this.dataWindow.IsOpen) { this.dataWindow.SetDataKind(dataKind); } } /// /// Opens the dev menu bar. /// public void OpenDevMenu() => this.isImGuiDrawDevMenu = true; /// /// Opens the . /// public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true; /// /// Opens the . /// public void OpenIMEWindow() => this.imeWindow.IsOpen = true; /// /// Opens the . /// public void OpenLogWindow() => this.consoleWindow.IsOpen = true; /// /// Opens the . /// public void OpenPluginStats() => this.pluginStatWindow.IsOpen = true; /// /// Opens the . /// public void OpenPluginInstaller() => this.pluginWindow.IsOpen = true; /// /// Opens the . /// public void OpenSettings() => this.settingsWindow.IsOpen = true; /// /// Opens the . /// public void OpenSelfTest() => this.selfTestWindow.IsOpen = true; /// /// Opens the . /// public void OpenStyleEditor() => this.styleEditorWindow.IsOpen = true; #endregion #region Close /// /// Closes the . /// public void CloseIMEWindow() => this.imeWindow.IsOpen = false; #endregion #region Toggle /// /// Toggles the . /// public void ToggleChangelogWindow() => this.changelogWindow.Toggle(); /// /// Toggles the . /// public void ToggleColorsDemoWindow() => this.colorDemoWindow.Toggle(); /// /// Toggles the . /// public void ToggleComponentDemoWindow() => this.componentDemoWindow.Toggle(); /// /// Toggles the . /// public void ToggleCreditsWindow() => this.creditsWindow.Toggle(); /// /// Toggles the . /// /// The data kind to switch to after opening. public void ToggleDataWindow(string dataKind = null) { this.dataWindow.Toggle(); if (dataKind != null && this.dataWindow.IsOpen) { this.dataWindow.SetDataKind(dataKind); } } /// /// Toggles the dev menu bar. /// public void ToggleDevMenu() => this.isImGuiDrawDevMenu ^= true; /// /// Toggles the . /// public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle(); /// /// Toggles the . /// public void ToggleIMEWindow() => this.imeWindow.Toggle(); /// /// Toggles the . /// public void ToggleLogWindow() => this.consoleWindow.Toggle(); /// /// Toggles the . /// public void TogglePluginStatsWindow() => this.pluginStatWindow.Toggle(); /// /// Toggles the . /// public void TogglePluginInstallerWindow() => this.pluginWindow.Toggle(); /// /// Toggles the . /// public void ToggleSettingsWindow() => this.settingsWindow.Toggle(); /// /// Toggles the . /// public void ToggleSelfTestWindow() => this.selfTestWindow.Toggle(); /// /// Toggles the . /// public void ToggleStyleEditorWindow() => this.selfTestWindow.Toggle(); #endregion private void OnDraw() { this.frameCount++; try { this.DrawHiddenDevMenuOpener(); this.DrawDevMenu(); if (Service.Get().GameUiHidden) return; this.WindowSystem.Draw(); if (this.isImGuiDrawDemoWindow) ImGui.ShowDemoWindow(); // Release focus of any ImGui window if we click into the game. var io = ImGui.GetIO(); if (!io.WantCaptureMouse && (User32.GetKeyState((int)User32.VirtualKey.VK_LBUTTON) & 0x8000) != 0) { ImGui.SetWindowFocus(null); } } catch (Exception ex) { PluginLog.Error(ex, "Error during OnDraw"); } } private void DrawHiddenDevMenuOpener() { var condition = Service.Get(); if (!this.isImGuiDrawDevMenu && !condition.Any()) { ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1)); ImGui.PushStyleColor(ImGuiCol.TextSelectedBg, new Vector4(0, 0, 0, 1)); ImGui.PushStyleColor(ImGuiCol.Border, new Vector4(0, 0, 0, 1)); ImGui.PushStyleColor(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 1)); ImGui.PushStyleColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 1)); var mainViewportPos = ImGui.GetMainViewport().Pos; ImGui.SetNextWindowPos(new Vector2(mainViewportPos.X, mainViewportPos.Y), ImGuiCond.Always); ImGui.SetNextWindowBgAlpha(1); if (ImGui.Begin("DevMenu Opener", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoSavedSettings)) { if (ImGui.Button("###devMenuOpener", new Vector2(40, 25))) this.isImGuiDrawDevMenu = true; ImGui.End(); } ImGui.PopStyleColor(8); } } private void DrawDevMenu() { if (this.isImGuiDrawDevMenu) { if (ImGui.BeginMainMenuBar()) { var dalamud = Service.Get(); var configuration = Service.Get(); var pluginManager = Service.Get(); if (ImGui.BeginMenu("Dalamud")) { ImGui.MenuItem("Draw Dalamud dev menu", string.Empty, ref this.isImGuiDrawDevMenu); ImGui.Separator(); if (ImGui.MenuItem("Open Log window")) { this.OpenLogWindow(); } if (ImGui.BeginMenu("Set log level...")) { foreach (var logLevel in Enum.GetValues(typeof(LogEventLevel)).Cast()) { if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, dalamud.LogLevelSwitch.MinimumLevel == logLevel)) { dalamud.LogLevelSwitch.MinimumLevel = logLevel; configuration.LogLevel = logLevel; configuration.Save(); } } ImGui.EndMenu(); } var antiDebug = Service.Get(); if (ImGui.MenuItem("Enable AntiDebug", null, antiDebug.IsEnabled)) { var newEnabled = !antiDebug.IsEnabled; if (newEnabled) antiDebug.Enable(); else antiDebug.Disable(); configuration.IsAntiAntiDebugEnabled = newEnabled; configuration.Save(); } ImGui.Separator(); if (ImGui.MenuItem("Open Data window")) { this.OpenDataWindow(); } if (ImGui.MenuItem("Open Credits window")) { this.OpenCreditsWindow(); } if (ImGui.MenuItem("Open Settings window")) { this.OpenSettings(); } if (ImGui.MenuItem("Open Changelog window")) { this.OpenChangelogWindow(); } if (ImGui.MenuItem("Open Components Demo")) { this.OpenComponentDemoWindow(); } if (ImGui.MenuItem("Open Colors Demo")) { this.OpenColorsDemoWindow(); } if (ImGui.MenuItem("Open Self-Test")) { this.OpenSelfTest(); } if (ImGui.MenuItem("Open Style Editor")) { this.OpenStyleEditor(); } ImGui.Separator(); if (ImGui.MenuItem("Unload Dalamud")) { Service.Get().Unload(); } if (ImGui.MenuItem("Kill game")) { Process.GetCurrentProcess().Kill(); } if (ImGui.MenuItem("Cause AccessViolation")) { Marshal.ReadByte(IntPtr.Zero); } ImGui.Separator(); if (ImGui.MenuItem("Enable Dalamud testing", string.Empty, configuration.DoDalamudTest)) { configuration.DoDalamudTest ^= true; configuration.Save(); } var startInfo = Service.Get(); ImGui.MenuItem(Util.AssemblyVersion, false); ImGui.MenuItem(startInfo.GameVersion.ToString(), false); ImGui.EndMenu(); } if (ImGui.BeginMenu("GUI")) { ImGui.MenuItem("Draw ImGui demo", string.Empty, ref this.isImGuiDrawDemoWindow); ImGui.Separator(); var val = ImGuiManagedAsserts.AssertsEnabled; if (ImGui.MenuItem("Enable Asserts", string.Empty, ref val)) { ImGuiManagedAsserts.AssertsEnabled = val; } if (ImGui.MenuItem("Clear focus")) { ImGui.SetWindowFocus(null); } if (ImGui.MenuItem("Dump style")) { var info = string.Empty; var style = StyleModel.Get(); var enCulture = new CultureInfo("en-US"); foreach (var propertyInfo in typeof(StyleModel).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (propertyInfo.PropertyType == typeof(Vector2)) { var vec2 = (Vector2)propertyInfo.GetValue(style); info += $"{propertyInfo.Name} = new Vector2({vec2.X.ToString(enCulture)}f, {vec2.Y.ToString(enCulture)}f),\n"; } else { info += $"{propertyInfo.Name} = {propertyInfo.GetValue(style)},\n"; } } info += "Colors = new Dictionary()\n"; info += "{\n"; foreach (var color in style.Colors) { info += $"{{\"{color.Key}\", new Vector4({color.Value.X.ToString(enCulture)}f, {color.Value.Y.ToString(enCulture)}f, {color.Value.Z.ToString(enCulture)}f, {color.Value.W.ToString(enCulture)}f)}},\n"; } info += "},"; Log.Information(info); } ImGui.EndMenu(); } if (ImGui.BeginMenu("Game")) { if (ImGui.MenuItem("Replace ExceptionHandler")) { dalamud.ReplaceExceptionHandler(); } ImGui.EndMenu(); } if (ImGui.BeginMenu("Plugins")) { if (ImGui.MenuItem("Open Plugin installer")) { this.OpenPluginInstaller(); } if (ImGui.MenuItem("Clear cached images/icons")) { this.pluginWindow?.ClearIconCache(); } ImGui.Separator(); if (ImGui.MenuItem("Open Plugin Stats")) { this.OpenPluginStats(); } if (ImGui.MenuItem("Print plugin info")) { foreach (var plugin in pluginManager.InstalledPlugins) { // TODO: some more here, state maybe? PluginLog.Information($"{plugin.Name}"); } } if (ImGui.MenuItem("Reload plugins")) { try { pluginManager.ReloadAllPlugins(); } catch (Exception ex) { Service.Get().PrintError("Reload failed."); PluginLog.Error(ex, "Plugin reload failed."); } } if (ImGui.MenuItem("Scan dev plugins")) { pluginManager.ScanDevPlugins(); } ImGui.Separator(); if (ImGui.MenuItem("Load all API levels", null, configuration.LoadAllApiLevels)) { configuration.LoadAllApiLevels = !configuration.LoadAllApiLevels; configuration.Save(); } ImGui.Separator(); ImGui.MenuItem("API Level:" + PluginManager.DalamudApiLevel, false); ImGui.MenuItem("Loaded plugins:" + pluginManager.InstalledPlugins.Count, false); ImGui.EndMenu(); } if (ImGui.BeginMenu("Localization")) { var localization = Service.Get(); if (ImGui.MenuItem("Export localizable")) { localization.ExportLocalizable(); } if (ImGui.BeginMenu("Load language...")) { if (ImGui.MenuItem("From Fallbacks")) { localization.SetupWithFallbacks(); } if (ImGui.MenuItem("From UICulture")) { localization.SetupWithUiCulture(); } foreach (var applicableLangCode in Localization.ApplicableLangCodes) { if (ImGui.MenuItem($"Applicable: {applicableLangCode}")) { localization.SetupWithLangCode(applicableLangCode); } } ImGui.EndMenu(); } ImGui.EndMenu(); } if (Service.Get().GameUiHidden) ImGui.BeginMenu("UI is hidden...", false); ImGui.BeginMenu(Util.GetGitHash(), false); ImGui.BeginMenu(this.frameCount.ToString(), false); ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("F2"), false); ImGui.EndMainMenuBar(); } } } } }