Add badges
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s

This commit is contained in:
goaaats 2025-12-19 20:55:30 +01:00
parent 5e4ad4a694
commit efed9ca20b
9 changed files with 392 additions and 1 deletions

View file

@ -495,6 +495,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
#pragma warning restore SA1516
#pragma warning restore SA1600
/// <summary>
/// Gets or sets a list of badge passwords used to unlock badges.
/// </summary>
public List<string> UsedBadgePasswords { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether badges should be shown on the title screen.
/// </summary>
public bool ShowBadgesOnTitleScreen { get; set; } = true;
/// <summary>
/// Load a configuration from the provided path.
/// </summary>

View file

@ -73,7 +73,7 @@ public enum DalamudAsset
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "troubleIcon.png")]
TroubleIcon = 1006,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin trouble icon overlay.
/// </summary>
@ -124,6 +124,13 @@ public enum DalamudAsset
[DalamudAssetPath("UIRes", "tsmShade.png")]
TitleScreenMenuShade = 1013,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: Atlas containing badges.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "badgeAtlas.png")]
BadgeAtlas = 1015,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK JP Medium.
/// </summary>

View file

@ -56,6 +56,11 @@ public enum SettingsOpenKind
/// </summary>
ServerInfoBar,
/// <summary>
/// Open to the "Badges" page.
/// </summary>
Badge,
/// <summary>
/// Open to the "Experimental" page.
/// </summary>

View file

@ -0,0 +1,48 @@
using System.Numerics;
namespace Dalamud.Interface.Internal.Badge;
/// <summary>
/// Represents information about a badge.
/// </summary>
/// <param name="Name">Name of the badge.</param>
/// <param name="Description">Description of the badge.</param>
/// <param name="IconIndex">Icon index.</param>
/// <param name="UnlockSha256">Sha256 hash of the unlock password.</param>
/// <param name="UnlockMethod">How the badge is unlocked.</param>
internal record BadgeInfo(
Func<string> Name,
Func<string> Description,
int IconIndex,
string UnlockSha256,
BadgeUnlockMethod UnlockMethod)
{
private const float BadgeWidth = 256;
private const float BadgeHeight = 256;
private const float BadgesPerRow = 2;
/// <summary>
/// Gets the UV coordinates for the badge icon in the atlas.
/// </summary>
/// <param name="atlasWidthPx">Width of the atlas.</param>
/// <param name="atlasHeightPx">Height of the atlas.</param>
/// <returns>UV coordinates.</returns>
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);
}
}

View file

@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Configuration.Internal;
namespace Dalamud.Interface.Internal.Badge;
/// <summary>
/// Service responsible for managing user badges.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class BadgeManager : IServiceType
{
private readonly DalamudConfiguration configuration;
private readonly List<BadgeInfo> 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<int> unlockedBadgeIndices = [];
/// <summary>
/// Initializes a new instance of the <see cref="BadgeManager"/> class.
/// </summary>
/// <param name="configuration">Configuration to use.</param>
[ServiceManager.ServiceConstructor]
public BadgeManager(DalamudConfiguration configuration)
{
this.configuration = configuration;
foreach (var usedBadge in this.configuration.UsedBadgePasswords)
{
this.TryUnlockBadge(usedBadge, BadgeUnlockMethod.Startup, out _);
}
}
/// <summary>
/// Gets the badges the user has unlocked.
/// </summary>
public IEnumerable<BadgeInfo> UnlockedBadges
=> this.badges.Where((_, index) => this.unlockedBadgeIndices.Contains(index));
/// <summary>
/// Unlock a badge with the given password and method.
/// </summary>
/// <param name="password">The password to unlock the badge with.</param>
/// <param name="method">How we are unlocking this badge.</param>
/// <param name="unlockedBadge">The badge that was unlocked, if the function returns true, null otherwise.</param>
/// <returns>The unlocked badge, if one was unlocked by this call.</returns>
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;
}
}

View file

@ -0,0 +1,22 @@
namespace Dalamud.Interface.Internal.Badge;
/// <summary>
/// Method by which a badge can be unlocked.
/// </summary>
internal enum BadgeUnlockMethod
{
/// <summary>
/// Badge can be unlocked by the user by entering a password.
/// </summary>
User,
/// <summary>
/// Badge can be unlocked from Dalamud internal features.
/// </summary>
Internal,
/// <summary>
/// Badge is no longer obtainable and can only be unlocked from the configuration file.
/// </summary>
Startup,
}

View file

@ -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
/// <param name="widget">Widget to set current.</param>
public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget;
/// <summary>
/// Play an animation when a badge has been unlocked.
/// </summary>
/// <param name="badge">The badge that has been unlocked.</param>
public void StartBadgeUnlockAnimation(BadgeInfo badge)
{
var badgeTexture = Service<DalamudAssetManager>.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas);
var uvs = badge.GetIconUv(badgeTexture.Width, badgeTexture.Height);
// TODO: Make it more fancy?
Service<NotificationManager>.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<GameGui>.Get().GameUiHidden)
return;
@ -591,6 +614,68 @@ internal class DalamudInterface : IInternalDisposableService
}
}
private void DrawTitleScreenBadges()
{
if (!this.titleScreenMenuWindow.IsOpen)
return;
var badgeManager = Service<BadgeManager>.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<DalamudAssetManager>.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);

View file

@ -46,6 +46,7 @@ internal sealed class SettingsWindow : Window
new SettingsTabLook(),
new SettingsTabAutoUpdates(),
new SettingsTabDtr(),
new SettingsTabBadge(),
new SettingsTabExperimental(),
new SettingsTabAbout()
];

View file

@ -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<bool>(
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<BadgeManager>.Get();
var dalamudInterface = Service<DalamudInterface>.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<DalamudAssetManager>.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);
}
}
}