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();
+ }
+ }
+ }
+}