From 2d131fa1ec0a1e9ecb32b5bfdc86cb66858b2929 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 31 Jan 2022 18:24:45 +0100 Subject: [PATCH] feat: Dalamud TitleScreenMenu service --- .../Internal/DalamudConfiguration.cs | 11 + Dalamud/Dalamud.cs | 3 + .../Interface/Internal/DalamudInterface.cs | 35 +- .../Internal/Windows/SettingsWindow.cs | 6 + .../Internal/Windows/TitleScreenMenuWindow.cs | 322 ++++++++++++++++++ Dalamud/Interface/TitleScreenMenu.cs | 99 ++++++ 6 files changed, 471 insertions(+), 5 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs create mode 100644 Dalamud/Interface/TitleScreenMenu.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index b96226982..cd5d90ba3 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -42,6 +42,12 @@ namespace Dalamud.Configuration.Internal /// public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved; + /// + /// Gets a value indicating whether or not Dalamud staging is enabled. + /// + [JsonIgnore] + public bool IsConventionalStaging => this.DalamudBetaKey == DalamudCurrentBetaKey; + /// /// Gets or sets a list of muted works. /// @@ -250,6 +256,11 @@ namespace Dalamud.Configuration.Internal /// public List? DtrIgnore { get; set; } + /// + /// Gets or sets a value indicating whether the title screen menu is shown. + /// + public bool ShowTsm { get; set; } = true; + /// /// Load a configuration from the provided path. /// diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 6bbaa08bc..62e860a5c 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -17,6 +17,7 @@ using Dalamud.Game.Network; using Dalamud.Game.Network.Internal; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking.Internal; +using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -234,6 +235,8 @@ namespace Dalamud { Log.Information("[T3] START!"); + Service.Set(); + var pluginManager = Service.Set(); Service.Set(); diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index ae191680c..004f3348e 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -7,11 +7,11 @@ using System.Numerics; using System.Reflection; using System.Runtime.InteropServices; +using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Game.Internal; -using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.SelfTest; @@ -51,8 +51,10 @@ namespace Dalamud.Interface.Internal private readonly SettingsWindow settingsWindow; private readonly SelfTestWindow selfTestWindow; private readonly StyleEditorWindow styleEditorWindow; + private readonly TitleScreenMenuWindow titleScreenMenuWindow; private readonly TextureWrap logoTexture; + private readonly TextureWrap tsmLogoTexture; private ulong frameCount = 0; @@ -87,6 +89,7 @@ namespace Dalamud.Interface.Internal this.settingsWindow = new SettingsWindow() { IsOpen = false }; this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; + this.titleScreenMenuWindow = new TitleScreenMenuWindow() { IsOpen = false }; this.WindowSystem.AddWindow(this.changelogWindow); this.WindowSystem.AddWindow(this.colorDemoWindow); @@ -101,6 +104,7 @@ namespace Dalamud.Interface.Internal this.WindowSystem.AddWindow(this.settingsWindow); this.WindowSystem.AddWindow(this.selfTestWindow); this.WindowSystem.AddWindow(this.styleEditorWindow); + this.WindowSystem.AddWindow(this.titleScreenMenuWindow); ImGuiManagedAsserts.AssertsEnabled = configuration.AssertsEnabledAtStartup; @@ -108,8 +112,27 @@ namespace Dalamud.Interface.Internal interfaceManager.Draw += this.OnDraw; var dalamud = Service.Get(); - this.logoTexture = - interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "logo.png"))!; + var logoTex = + interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "logo.png")); + var tsmLogoTex = + interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png")); + + if (logoTex == null || tsmLogoTex == null) + { + throw new Exception("Failed to load logo textures"); + } + + this.logoTexture = logoTex; + this.tsmLogoTexture = tsmLogoTex; + + var tsm = Service.Get(); + tsm.AddEntry(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), this.tsmLogoTexture, () => this.pluginWindow.IsOpen = true); + tsm.AddEntry(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), this.tsmLogoTexture, () => this.settingsWindow.IsOpen = true); + + if (configuration.IsConventionalStaging) + { + tsm.AddEntry(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), this.tsmLogoTexture, () => this.isImGuiDrawDevMenu = true); + } } /// @@ -131,14 +154,16 @@ namespace Dalamud.Interface.Internal { Service.Get().Draw -= this.OnDraw; - this.logoTexture.Dispose(); - this.WindowSystem.RemoveAllWindows(); this.changelogWindow.Dispose(); this.creditsWindow.Dispose(); this.consoleWindow.Dispose(); this.pluginWindow.Dispose(); + this.titleScreenMenuWindow.Dispose(); + + this.logoTexture.Dispose(); + this.tsmLogoTexture.Dispose(); } #region Open diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index 45ee557b8..312415dc7 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -44,6 +44,7 @@ namespace Dalamud.Interface.Internal.Windows private bool doViewport; private bool doGamepad; private bool doFocus; + private bool doTsm; private List? dtrOrder; private List? dtrIgnore; @@ -94,6 +95,7 @@ namespace Dalamud.Interface.Internal.Windows this.doViewport = !configuration.IsDisableViewport; this.doGamepad = configuration.IsGamepadNavigationEnabled; this.doFocus = configuration.IsFocusManagementEnabled; + this.doTsm = configuration.ShowTsm; this.doPluginTest = configuration.DoPluginTest; this.thirdRepoList = configuration.ThirdRepoList.Select(x => x.Clone()).ToList(); @@ -312,6 +314,9 @@ namespace Dalamud.Interface.Internal.Windows ImGui.Checkbox(Loc.Localize("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"), ref this.doGamepad); ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleGamepadNavigationHint", "This will allow you to toggle between game and plugin navigation via L1+L3.\nToggle the PluginInstaller window via R3 if ImGui navigation is enabled.")); + + ImGui.Checkbox(Loc.Localize("DalamudSettingToggleTsm", "Show title screen menu"), ref this.doTsm); + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen.")); } private void DrawServerInfoBarTab() @@ -778,6 +783,7 @@ namespace Dalamud.Interface.Internal.Windows configuration.IsDocking = this.doDocking; configuration.IsGamepadNavigationEnabled = this.doGamepad; configuration.IsFocusManagementEnabled = this.doFocus; + configuration.ShowTsm = this.doTsm; // This is applied every frame in InterfaceManager::CheckViewportState() configuration.IsDisableViewport = !this.doViewport; diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs new file mode 100644 index 000000000..429986c64 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -0,0 +1,322 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; + +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using ImGuiScene; + +namespace Dalamud.Interface.Internal.Windows +{ + /// + /// Class responsible for drawing the main plugin window. + /// + internal class TitleScreenMenuWindow : Window, IDisposable + { + private readonly TextureWrap shadeTexture; + + private readonly Dictionary shadeEasings = new(); + private readonly Dictionary moveEasings = new(); + private readonly Dictionary logoEasings = new(); + + private InOutCubic? fadeOutEasing; + + private State state = State.Hide; + + /// + /// Initializes a new instance of the class. + /// + public TitleScreenMenuWindow() + : base( + "TitleScreenMenuOverlay", + ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar | + ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) + { + this.IsOpen = true; + + this.ForceMainWindow = true; + + this.Position = new Vector2(0, 200); + this.PositionCondition = ImGuiCond.Always; + this.RespectCloseHotkey = false; + + var dalamud = Service.Get(); + var interfaceManager = Service.Get(); + + var shadeTex = + interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmShade.png")); + this.shadeTexture = shadeTex ?? throw new Exception("Could not load TSM background texture."); + + var framework = Service.Get(); + framework.Update += this.FrameworkOnUpdate; + } + + private enum State + { + Hide, + Show, + FadeOut, + } + + /// + public override void PreDraw() + { + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0)); + base.PreDraw(); + } + + /// + public override void PostDraw() + { + ImGui.PopStyleVar(2); + base.PostDraw(); + } + + /// + public void Dispose() + { + this.shadeTexture.Dispose(); + var framework = Service.Get(); + framework.Update -= this.FrameworkOnUpdate; + } + + /// + public override void Draw() + { + ImGui.SetWindowFontScale(1.3f); + + var tsm = Service.Get(); + + switch (this.state) + { + case State.Show: + { + for (var i = 0; i < tsm.Entries.Count; i++) + { + var entry = tsm.Entries[i]; + + if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing)) + { + moveEasing = new InOutQuint(TimeSpan.FromMilliseconds(400)); + this.moveEasings.Add(entry.Id, moveEasing); + } + + if (!moveEasing.IsRunning && !moveEasing.IsDone) + { + moveEasing.Restart(); + } + + if (moveEasing.IsDone) + { + moveEasing.Stop(); + } + + moveEasing.Update(); + + var finalPos = (i + 1) * this.shadeTexture.Height; + var pos = moveEasing.Value * finalPos; + + // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment. + if (moveEasing.IsDone) + { + pos = finalPos; + } + + this.DrawEntry(entry, moveEasing.IsRunning && i != 0, true, i == 0, true); + + var cursor = ImGui.GetCursorPos(); + cursor.Y = (float)pos; + ImGui.SetCursorPos(cursor); + } + + if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | + ImGuiHoveredFlags.AllowWhenOverlapped | + ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)) + { + this.state = State.FadeOut; + } + + break; + } + + case State.FadeOut: + { + this.fadeOutEasing ??= new InOutCubic(TimeSpan.FromMilliseconds(400)) + { + IsInverse = true, + }; + + if (!this.fadeOutEasing.IsRunning && !this.fadeOutEasing.IsDone) + { + this.fadeOutEasing.Restart(); + } + + if (this.fadeOutEasing.IsDone) + { + this.fadeOutEasing.Stop(); + } + + this.fadeOutEasing.Update(); + + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value); + + for (var i = 0; i < tsm.Entries.Count; i++) + { + var entry = tsm.Entries[i]; + + var finalPos = (i + 1) * this.shadeTexture.Height; + + this.DrawEntry(entry, i != 0, true, i == 0, false); + + var cursor = ImGui.GetCursorPos(); + cursor.Y = finalPos; + ImGui.SetCursorPos(cursor); + } + + ImGui.PopStyleVar(); + + var isHover = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | + ImGuiHoveredFlags.AllowWhenOverlapped | + ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + + if (!isHover && this.fadeOutEasing!.IsDone) + { + this.state = State.Hide; + this.fadeOutEasing = null; + } + else if (isHover) + { + this.state = State.Show; + this.fadeOutEasing = null; + } + + break; + } + + case State.Hide: + { + if (this.DrawEntry(tsm.Entries[0], true, false, true, true)) + { + this.state = State.Show; + } + + this.moveEasings.Clear(); + this.logoEasings.Clear(); + this.shadeEasings.Clear(); + break; + } + } + } + + private bool DrawEntry( + TitleScreenMenu.TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha) + { + if (!this.shadeEasings.TryGetValue(entry.Id, out var shadeEasing)) + { + shadeEasing = new InOutCubic(TimeSpan.FromMilliseconds(350)); + this.shadeEasings.Add(entry.Id, shadeEasing); + } + + var initialCursor = ImGui.GetCursorPos(); + + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)shadeEasing.Value); + ImGui.Image(this.shadeTexture.ImGuiHandle, new Vector2(this.shadeTexture.Width, this.shadeTexture.Height)); + ImGui.PopStyleVar(); + + var isHover = ImGui.IsItemHovered(); + if (isHover && (!shadeEasing.IsRunning || (shadeEasing.IsDone && shadeEasing.IsInverse)) && !inhibitFadeout) + { + shadeEasing.IsInverse = false; + shadeEasing.Restart(); + } + else if (!isHover && !shadeEasing.IsInverse && shadeEasing.IsRunning && !inhibitFadeout) + { + shadeEasing.IsInverse = true; + shadeEasing.Restart(); + } + + var isClick = ImGui.IsItemClicked(); + if (isClick) + { + entry.Trigger(); + } + + shadeEasing.Update(); + + if (!this.logoEasings.TryGetValue(entry.Id, out var logoEasing)) + { + logoEasing = new InOutCubic(TimeSpan.FromMilliseconds(350)); + this.logoEasings.Add(entry.Id, logoEasing); + } + + if (!logoEasing.IsRunning && !logoEasing.IsDone) + { + logoEasing.Restart(); + } + + if (logoEasing.IsDone) + { + logoEasing.Stop(); + } + + logoEasing.Update(); + + ImGui.SetCursorPos(initialCursor); + ImGuiHelpers.ScaledDummy(5); + ImGui.SameLine(); + + if (overrideAlpha) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, isFirst ? 1f : (float)logoEasing.Value); + } + else if (isFirst) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); + } + + ImGui.Image(entry.Texture.ImGuiHandle, new Vector2(TitleScreenMenu.TextureSize)); + if (overrideAlpha || isFirst) + { + ImGui.PopStyleVar(); + } + + ImGui.SameLine(); + + ImGuiHelpers.ScaledDummy(10); + ImGui.SameLine(); + + var textHeight = ImGui.GetTextLineHeightWithSpacing(); + var cursor = ImGui.GetCursorPos(); + + cursor.Y += (entry.Texture.Height / 2) - (textHeight / 2); + ImGui.SetCursorPos(cursor); + + if (overrideAlpha) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, showText ? (float)logoEasing.Value : 0f); + } + + ImGui.Text(entry.Name); + if (overrideAlpha) + { + ImGui.PopStyleVar(); + } + + initialCursor.Y += entry.Texture.Height; + ImGui.SetCursorPos(initialCursor); + + return isHover; + } + + private void FrameworkOnUpdate(Framework framework) + { + var clientState = Service.Get(); + this.IsOpen = !clientState.IsLoggedIn; + } + } +} diff --git a/Dalamud/Interface/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu.cs new file mode 100644 index 000000000..03418d2de --- /dev/null +++ b/Dalamud/Interface/TitleScreenMenu.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using ImGuiScene; + +namespace Dalamud.Interface +{ + /// + /// Class responsible for managing elements in the title screen menu. + /// + [PluginInterface] + [InterfaceVersion("1.0")] + public class TitleScreenMenu + { + /// + /// Gets the texture size needed for title screen menu logos. + /// + internal const uint TextureSize = 64; + + private readonly List entries = new(); + + /// + /// Gets the list of entries in the title screen menu. + /// + public IReadOnlyList Entries => this.entries; + + /// + /// Adds a new entry to the title screen menu. + /// + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + /// A object that can be used to manage the entry. + /// Thrown when the texture provided does not match the required resolution(64x64). + public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered) + { + if (texture.Height != TextureSize || texture.Width != TextureSize) + { + throw new ArgumentException("Texture must be 64x64"); + } + + var entry = new TitleScreenMenuEntry(text, texture, onTriggered); + this.entries.Add(entry); + return entry; + } + + /// + /// Remove an entry from the title screen menu. + /// + /// The entry to remove. + public void RemoveEntry(TitleScreenMenuEntry entry) => this.entries.Remove(entry); + + /// + /// Class representing an entry in the title screen menu. + /// + public class TitleScreenMenuEntry + { + private readonly Action onTriggered; + + /// + /// Initializes a new instance of the class. + /// + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + internal TitleScreenMenuEntry(string text, TextureWrap texture, Action onTriggered) + { + this.Name = text; + this.Texture = texture; + this.onTriggered = onTriggered; + } + + /// + /// Gets or sets the name of this entry. + /// + public string Name { get; set; } + + /// + /// Gets or sets the texture of this entry. + /// + public TextureWrap Texture { get; set; } + + /// + /// Gets the internal ID of this entry. + /// + internal Guid Id { get; init; } = Guid.NewGuid(); + + /// + /// Trigger the action associated with this entry. + /// + internal void Trigger() + { + this.onTriggered(); + } + } + } +}