From efed9ca20b74452bf332c9cd4f68f5588bb81156 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Dec 2025 20:55:30 +0100 Subject: [PATCH] Add badges --- .../Internal/DalamudConfiguration.cs | 10 ++ Dalamud/DalamudAsset.cs | 9 +- Dalamud/Interface/DalamudWindowOpenKinds.cs | 5 + Dalamud/Interface/Internal/Badge/BadgeInfo.cs | 48 +++++++ .../Interface/Internal/Badge/BadgeManager.cs | 85 ++++++++++++ .../Internal/Badge/BadgeUnlockMethod.cs | 22 +++ .../Interface/Internal/DalamudInterface.cs | 85 ++++++++++++ .../Windows/Settings/SettingsWindow.cs | 1 + .../Windows/Settings/Tabs/SettingsTabBadge.cs | 128 ++++++++++++++++++ 9 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Interface/Internal/Badge/BadgeInfo.cs create mode 100644 Dalamud/Interface/Internal/Badge/BadgeManager.cs create mode 100644 Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs create mode 100644 Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index d546dc517..ddcb26914 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -495,6 +495,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService #pragma warning restore SA1516 #pragma warning restore SA1600 + /// + /// Gets or sets a list of badge passwords used to unlock badges. + /// + public List UsedBadgePasswords { get; set; } = []; + + /// + /// Gets or sets a value indicating whether badges should be shown on the title screen. + /// + public bool ShowBadgesOnTitleScreen { get; set; } = true; + /// /// Load a configuration from the provided path. /// diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index e234fbb4c..9c0f247ee 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -73,7 +73,7 @@ public enum DalamudAsset [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "troubleIcon.png")] TroubleIcon = 1006, - + /// /// : The plugin trouble icon overlay. /// @@ -124,6 +124,13 @@ public enum DalamudAsset [DalamudAssetPath("UIRes", "tsmShade.png")] TitleScreenMenuShade = 1013, + /// + /// : Atlas containing badges. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "badgeAtlas.png")] + BadgeAtlas = 1015, + /// /// : Noto Sans CJK JP Medium. /// diff --git a/Dalamud/Interface/DalamudWindowOpenKinds.cs b/Dalamud/Interface/DalamudWindowOpenKinds.cs index 35d2825f7..891f9281a 100644 --- a/Dalamud/Interface/DalamudWindowOpenKinds.cs +++ b/Dalamud/Interface/DalamudWindowOpenKinds.cs @@ -56,6 +56,11 @@ public enum SettingsOpenKind /// ServerInfoBar, + /// + /// Open to the "Badges" page. + /// + Badge, + /// /// Open to the "Experimental" page. /// diff --git a/Dalamud/Interface/Internal/Badge/BadgeInfo.cs b/Dalamud/Interface/Internal/Badge/BadgeInfo.cs new file mode 100644 index 000000000..0787f0658 --- /dev/null +++ b/Dalamud/Interface/Internal/Badge/BadgeInfo.cs @@ -0,0 +1,48 @@ +using System.Numerics; + +namespace Dalamud.Interface.Internal.Badge; + +/// +/// Represents information about a badge. +/// +/// Name of the badge. +/// Description of the badge. +/// Icon index. +/// Sha256 hash of the unlock password. +/// How the badge is unlocked. +internal record BadgeInfo( + Func Name, + Func Description, + int IconIndex, + string UnlockSha256, + BadgeUnlockMethod UnlockMethod) +{ + private const float BadgeWidth = 256; + private const float BadgeHeight = 256; + private const float BadgesPerRow = 2; + + /// + /// Gets the UV coordinates for the badge icon in the atlas. + /// + /// Width of the atlas. + /// Height of the atlas. + /// UV coordinates. + public (Vector2 Uv0, Vector2 Uv1) GetIconUv(float atlasWidthPx, float atlasHeightPx) + { + // Calculate row and column from icon index + var col = this.IconIndex % (int)BadgesPerRow; + var row = this.IconIndex / (int)BadgesPerRow; + + // Calculate pixel positions + var x0 = col * BadgeWidth; + var y0 = row * BadgeHeight; + var x1 = x0 + BadgeWidth; + var y1 = y0 + BadgeHeight; + + // Convert to UV coordinates (0.0 to 1.0) + var uv0 = new Vector2(x0 / atlasWidthPx, y0 / atlasHeightPx); + var uv1 = new Vector2(x1 / atlasWidthPx, y1 / atlasHeightPx); + + return (uv0, uv1); + } +} diff --git a/Dalamud/Interface/Internal/Badge/BadgeManager.cs b/Dalamud/Interface/Internal/Badge/BadgeManager.cs new file mode 100644 index 000000000..9290d6cc8 --- /dev/null +++ b/Dalamud/Interface/Internal/Badge/BadgeManager.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Configuration.Internal; + +namespace Dalamud.Interface.Internal.Badge; + +/// +/// Service responsible for managing user badges. +/// +[ServiceManager.EarlyLoadedService] +internal class BadgeManager : IServiceType +{ + private readonly DalamudConfiguration configuration; + + private readonly List badges = + [ + new(() => "Test Badge", + () => "Awarded for testing badges.", + 0, + "937e8d5fbb48bd4949536cd65b8d35c426b80d2f830c5c308e2cdec422ae2244", + BadgeUnlockMethod.User), + + new(() => "Fundraiser #1 Donor", + () => "Awarded for participating in the first patch fundraiser.", + 1, + "56e752257bd0cbb2944f95cc7b3cb3d0db15091dd043f7a195ed37028d079322", + BadgeUnlockMethod.User) + ]; + + private readonly List unlockedBadgeIndices = []; + + /// + /// Initializes a new instance of the class. + /// + /// Configuration to use. + [ServiceManager.ServiceConstructor] + public BadgeManager(DalamudConfiguration configuration) + { + this.configuration = configuration; + + foreach (var usedBadge in this.configuration.UsedBadgePasswords) + { + this.TryUnlockBadge(usedBadge, BadgeUnlockMethod.Startup, out _); + } + } + + /// + /// Gets the badges the user has unlocked. + /// + public IEnumerable UnlockedBadges + => this.badges.Where((_, index) => this.unlockedBadgeIndices.Contains(index)); + + /// + /// Unlock a badge with the given password and method. + /// + /// The password to unlock the badge with. + /// How we are unlocking this badge. + /// The badge that was unlocked, if the function returns true, null otherwise. + /// The unlocked badge, if one was unlocked by this call. + public bool TryUnlockBadge(string password, BadgeUnlockMethod method, out BadgeInfo unlockedBadge) + { + var sha256 = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(password)); + var hashString = Convert.ToHexString(sha256); + + foreach (var (idx, badge) in this.badges.Where(x => x.UnlockMethod == method || method == BadgeUnlockMethod.Startup).Index()) + { + if (!this.unlockedBadgeIndices.Contains(idx) && badge.UnlockSha256.Equals(hashString, StringComparison.OrdinalIgnoreCase)) + { + if (method != BadgeUnlockMethod.Startup) + { + this.configuration.UsedBadgePasswords.Add(password); + this.configuration.QueueSave(); + } + + this.unlockedBadgeIndices.Add(idx); + unlockedBadge = badge; + return true; + } + } + + unlockedBadge = null!; + return false; + } +} diff --git a/Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs b/Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs new file mode 100644 index 000000000..45828c097 --- /dev/null +++ b/Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.Internal.Badge; + +/// +/// Method by which a badge can be unlocked. +/// +internal enum BadgeUnlockMethod +{ + /// + /// Badge can be unlocked by the user by entering a password. + /// + User, + + /// + /// Badge can be unlocked from Dalamud internal features. + /// + Internal, + + /// + /// Badge is no longer obtainable and can only be unlocked from the configuration file. + /// + Startup, +} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index be4228a81..13e7ff3f7 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -19,6 +19,9 @@ using Dalamud.Game.Gui; using Dalamud.Hooking; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal.Badge; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.Data; using Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -540,6 +543,25 @@ internal class DalamudInterface : IInternalDisposableService /// Widget to set current. public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget; + /// + /// Play an animation when a badge has been unlocked. + /// + /// The badge that has been unlocked. + public void StartBadgeUnlockAnimation(BadgeInfo badge) + { + var badgeTexture = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); + var uvs = badge.GetIconUv(badgeTexture.Width, badgeTexture.Height); + + // TODO: Make it more fancy? + Service.Get().AddNotification( + new Notification + { + Title = "Badge unlocked!", + Content = $"You unlocked the badge '{badge.Name()}'", + Type = NotificationType.Success, + }); + } + private void OnDraw() { this.FrameCount++; @@ -561,6 +583,7 @@ internal class DalamudInterface : IInternalDisposableService { this.DrawHiddenDevMenuOpener(); this.DrawDevMenu(); + this.DrawTitleScreenBadges(); if (Service.Get().GameUiHidden) return; @@ -591,6 +614,68 @@ internal class DalamudInterface : IInternalDisposableService } } + private void DrawTitleScreenBadges() + { + if (!this.titleScreenMenuWindow.IsOpen) + return; + + var badgeManager = Service.Get(); + if (!this.configuration.ShowBadgesOnTitleScreen || !badgeManager.UnlockedBadges.Any()) + return; + + var vp = ImGui.GetMainViewport(); + ImGui.SetNextWindowPos(vp.Pos); + ImGui.SetNextWindowSize(vp.Size); + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowBgAlpha(0f); + + ImGui.Begin( + "###TitleScreenBadgeWindow"u8, + ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.NoNav); + + var badgeAtlas = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); + var badgeSize = ImGuiHelpers.GlobalScale * 80; + var spacing = ImGuiHelpers.GlobalScale * 10; + const float margin = 60f; + var startPos = vp.Pos + new Vector2(vp.Size.X - margin, margin); + + // Use the mouse position in screen space for hover detection because the usual ImGui hover checks + // don't work with this full-viewport overlay window setup. + var mouse = ImGui.GetMousePos(); + + foreach (var badge in badgeManager.UnlockedBadges) + { + var uvs = badge.GetIconUv(badgeAtlas.Width, badgeAtlas.Height); + + startPos.X -= badgeSize; + ImGui.SetCursorPos(startPos); + ImGui.Image(badgeAtlas.Handle, new Vector2(badgeSize), uvs.Uv0, uvs.Uv1); + + // Get the actual screen-space bounds of the image we just drew + var badgeMin = ImGui.GetItemRectMin(); + var badgeMax = ImGui.GetItemRectMax(); + + // add spacing to the left for the next badge + startPos.X -= spacing; + + // Manual hit test using mouse position + if (mouse.X >= badgeMin.X && mouse.X <= badgeMax.X && mouse.Y >= badgeMin.Y && mouse.Y <= badgeMax.Y) + { + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(300 * ImGuiHelpers.GlobalScale); + ImGui.TextWrapped(badge.Name()); + ImGui.Separator(); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, badge.Description()); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + } + + ImGui.End(); + } + private void DrawCreditsDarkeningAnimation() { using var style1 = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f); diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 581ef3746..62c931b20 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -46,6 +46,7 @@ internal sealed class SettingsWindow : Window new SettingsTabLook(), new SettingsTabAutoUpdates(), new SettingsTabDtr(), + new SettingsTabBadge(), new SettingsTabExperimental(), new SettingsTabAbout() ]; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs new file mode 100644 index 000000000..8e44ef7ea --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs @@ -0,0 +1,128 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using CheapLoc; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.Badge; +using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.Utility; +using Dalamud.Storage.Assets; +using Dalamud.Utility.Internal; + +namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; + +[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] +internal sealed class SettingsTabBadge : SettingsTab +{ + private string badgePassword = string.Empty; + private bool badgeWasError = false; + + public override string Title => Loc.Localize("DalamudSettingsBadge", "Badges"); + + public override SettingsOpenKind Kind => SettingsOpenKind.ServerInfoBar; + + public override SettingsEntry[] Entries { get; } = + [ + new SettingsEntry( + LazyLoc.Localize("DalamudSettingsShowBadgesOnTitleScreen", "Show Badges on Title Screen"), + LazyLoc.Localize("DalamudSettingsShowBadgesOnTitleScreenHint", "If enabled, your unlocked badges will also be shown on the title screen."), + c => c.ShowBadgesOnTitleScreen, + (v, c) => c.ShowBadgesOnTitleScreen = v), + ]; + + public override void Draw() + { + var badgeManager = Service.Get(); + var dalamudInterface = Service.Get(); + + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarHint", "Plugins can put additional information into your server information bar(where world & time can be seen).\nYou can reorder and disable these here.")); + + ImGuiHelpers.ScaledDummy(5); + + ImGui.Text(Loc.Localize("DalamudSettingsBadgesUnlock", "Unlock a badge")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsBadgesUnlockHint", "If you have received a code for a badge, enter it here to unlock the badge.\nCodes are usually given out during community events or contests.")); + ImGui.InputTextWithHint( + "##BadgePassword", + Loc.Localize("DalamudSettingsBadgesUnlockHintInput", "Enter badge code here"), + ref this.badgePassword, + 100); + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("DalamudSettingsBadgesUnlockButton", "Unlock Badge"))) + { + if (badgeManager.TryUnlockBadge(this.badgePassword.Trim(), BadgeUnlockMethod.User, out var unlockedBadge)) + { + dalamudInterface.StartBadgeUnlockAnimation(unlockedBadge); + this.badgeWasError = false; + } + else + { + this.badgeWasError = true; + } + } + + if (this.badgeWasError) + { + ImGuiHelpers.ScaledDummy(5); + ImGui.TextColored(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingsBadgesUnlockError", "Failed to unlock badge. The code may be invalid or you may have already unlocked this badge.")); + } + + ImGuiHelpers.ScaledDummy(5); + + base.Draw(); + + ImGui.Separator(); + + ImGuiHelpers.ScaledDummy(5); + + var haveBadges = badgeManager.UnlockedBadges.ToArray(); + + if (haveBadges.Length == 0) + { + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarDidNone", "You did not unlock any badges yet.\nBadges can be unlocked by participating in community events or contests.")); + } + + var badgeTexture = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); + foreach (var badge in haveBadges) + { + var uvs = badge.GetIconUv(badgeTexture.Width, badgeTexture.Height); + var sectionSize = ImGuiHelpers.GlobalScale * 66; + + var startCursor = ImGui.GetCursorPos(); + + ImGui.SetCursorPos(startCursor); + + var iconSize = ImGuiHelpers.ScaledVector2(64, 64); + var cursorBeforeImage = ImGui.GetCursorPos(); + var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos(); + + if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize)) + { + ImGui.Image(badgeTexture.Handle, iconSize, uvs.Uv0, uvs.Uv1); + ImGui.SameLine(); + ImGui.SetCursorPos(cursorBeforeImage); + } + + ImGui.SameLine(); + + ImGuiHelpers.ScaledDummy(5); + ImGui.SameLine(); + + var cursor = ImGui.GetCursorPos(); + + // Name + ImGui.Text(badge.Name()); + + cursor.Y += ImGui.GetTextLineHeightWithSpacing(); + ImGui.SetCursorPos(cursor); + + // Description + ImGui.TextWrapped(badge.Description()); + + startCursor.Y += sectionSize; + ImGui.SetCursorPos(startCursor); + + ImGuiHelpers.ScaledDummy(5); + } + } +}