using System; using System.Collections.Generic; using System.Diagnostics; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using ImGuiNET; using ImGuiScene; using Serilog; using SharpDX.Direct3D11; namespace Dalamud.Interface { /// /// This class represents the Dalamud UI that is drawn on top of the game. /// It can be used to draw custom windows and overlays. /// public sealed class UiBuilder : IDisposable { private readonly Stopwatch stopwatch; private readonly string namespaceName; private bool hasErrorWindow; /// /// Initializes a new instance of the class and registers it. /// You do not have to call this manually. /// /// The plugin namespace. internal UiBuilder(string namespaceName) { this.stopwatch = new Stopwatch(); this.namespaceName = namespaceName; var interfaceManager = Service.Get(); interfaceManager.Draw += this.OnDraw; interfaceManager.BuildFonts += this.OnBuildFonts; interfaceManager.ResizeBuffers += this.OnResizeBuffers; } /// /// The event that gets called when Dalamud is ready to draw your windows or overlays. /// When it is called, you can use static ImGui calls. /// public event Action Draw; /// /// The event that is called when the game's DirectX device is requesting you to resize your buffers. /// public event Action ResizeBuffers; /// /// Event that is fired when the plugin should open its configuration interface. /// public event Action OpenConfigUi; /// /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler.
/// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! ///
public event Action BuildFonts; /// /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. /// public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; /// /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt. /// public static ImFontPtr IconFont => InterfaceManager.IconFont; /// /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. /// public static ImFontPtr MonoFont => InterfaceManager.MonoFont; /// /// Gets the game's active Direct3D device. /// public Device Device => Service.Get().Device; /// /// Gets the game's main window handle. /// public IntPtr WindowHandlePtr => Service.Get().WindowHandlePtr; /// /// Gets or sets a value indicating whether this plugin should hide its UI automatically when the game's UI is hidden. /// public bool DisableAutomaticUiHide { get; set; } = false; /// /// Gets or sets a value indicating whether this plugin should hide its UI automatically when the user toggles the UI. /// public bool DisableUserUiHide { get; set; } = false; /// /// Gets or sets a value indicating whether this plugin should hide its UI automatically during cutscenes. /// public bool DisableCutsceneUiHide { get; set; } = false; /// /// Gets or sets a value indicating whether this plugin should hide its UI automatically while gpose is active. /// public bool DisableGposeUiHide { get; set; } = false; /// /// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. /// public bool OverrideGameCursor { get => Service.Get().OverrideGameCursor; set => Service.Get().OverrideGameCursor = value; } /// /// Gets the count of Draw calls made since plugin creation. /// public ulong FrameCount { get; private set; } = 0; /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// #if DEBUG internal static bool DoStats { get; set; } = true; #else internal static bool DoStats { get; set; } = false; #endif /// /// Gets a value indicating whether this UiBuilder has a configuration UI registered. /// internal bool HasConfigUi => this.OpenConfigUi != null; /// /// Gets or sets the time this plugin took to draw on the last frame. /// internal long LastDrawTime { get; set; } = -1; /// /// Gets or sets the longest amount of time this plugin ever took to draw. /// internal long MaxDrawTime { get; set; } = -1; /// /// Gets or sets a history of the last draw times, used to calculate an average. /// internal List DrawTimeHistory { get; set; } = new List(); private bool CutsceneActive { get { var condition = Service.Get(); return condition[ConditionFlag.OccupiedInCutSceneEvent] || condition[ConditionFlag.WatchingCutscene78]; } } private bool GposeActive { get { var condition = Service.Get(); return condition[ConditionFlag.WatchingCutscene]; } } /// /// Loads an image from the specified file. /// /// The full filepath to the image. /// A object wrapping the created image. Use inside ImGui.Image(). public TextureWrap LoadImage(string filePath) => Service.Get().LoadImage(filePath); /// /// Loads an image from a byte stream, such as a png downloaded into memory. /// /// A byte array containing the raw image data. /// A object wrapping the created image. Use inside ImGui.Image(). public TextureWrap LoadImage(byte[] imageData) => Service.Get().LoadImage(imageData); /// /// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use . /// /// A byte array containing the raw pixel data. /// The width of the image contained in . /// The height of the image contained in . /// The number of channels (bytes per pixel) of the image contained in . This should usually be 4. /// A object wrapping the created image. Use inside ImGui.Image(). public TextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => Service.Get().LoadImageRaw(imageData, width, height, numChannels); /// /// Call this to queue a rebuild of the font atlas.
/// This will invoke any handlers and ensure that any loaded fonts are /// ready to be used on the next UI frame. ///
public void RebuildFonts() { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); Service.Get().RebuildFonts(); } /// /// Add a notification to the notification queue. /// /// The content of the notification. /// The title of the notification. /// The type of the notification. /// The time the notification should be displayed for. public void AddNotification( string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000) => Service.Get().AddNotification(content, title, type, msDelay); /// /// Unregister the UiBuilder. Do not call this in plugin code. /// public void Dispose() { var interfaceManager = Service.Get(); interfaceManager.Draw -= this.OnDraw; interfaceManager.BuildFonts -= this.OnBuildFonts; interfaceManager.ResizeBuffers -= this.OnResizeBuffers; } /// /// Open the registered configuration UI, if it exists. /// internal void OpenConfig() { this.OpenConfigUi?.Invoke(); } private void OnDraw() { var configuration = Service.Get(); var gameGui = Service.Get(); var interfaceManager = Service.Get(); if ((gameGui.GameUiHidden && configuration.ToggleUiHide && !(this.DisableUserUiHide || this.DisableAutomaticUiHide)) || (this.CutsceneActive && configuration.ToggleUiHideDuringCutscenes && !(this.DisableCutsceneUiHide || this.DisableAutomaticUiHide)) || (this.GposeActive && configuration.ToggleUiHideDuringGpose && !(this.DisableGposeUiHide || this.DisableAutomaticUiHide))) return; if (!interfaceManager.FontsReady) return; ImGui.PushID(this.namespaceName); if (DoStats) { this.stopwatch.Restart(); } if (this.hasErrorWindow && ImGui.Begin($"{this.namespaceName} Error", ref this.hasErrorWindow, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize)) { ImGui.Text($"The plugin {this.namespaceName} ran into an error.\nContact the plugin developer for support.\n\nPlease try restarting your game."); ImGui.Spacing(); if (ImGui.Button("OK")) { this.hasErrorWindow = false; } ImGui.End(); } ImGuiManagedAsserts.ImGuiContextSnapshot snapshot = null; if (this.Draw != null) { snapshot = ImGuiManagedAsserts.GetSnapshot(); } try { this.Draw?.Invoke(); } catch (Exception ex) { Log.Error(ex, "[{0}] UiBuilder OnBuildUi caught exception", this.namespaceName); this.Draw = null; this.OpenConfigUi = null; this.hasErrorWindow = true; } // Only if Draw was successful if (this.Draw != null) { ImGuiManagedAsserts.ReportProblems(this.namespaceName, snapshot); } this.FrameCount++; if (DoStats) { this.stopwatch.Stop(); this.LastDrawTime = this.stopwatch.ElapsedTicks; this.MaxDrawTime = Math.Max(this.LastDrawTime, this.MaxDrawTime); this.DrawTimeHistory.Add(this.LastDrawTime); while (this.DrawTimeHistory.Count > 100) this.DrawTimeHistory.RemoveAt(0); } ImGui.PopID(); } private void OnBuildFonts() { this.BuildFonts?.Invoke(); } private void OnResizeBuffers() { this.ResizeBuffers?.Invoke(); } } }