diff --git a/Dalamud.sln b/Dalamud.sln
index 93089b9a6..2d22932dd 100644
--- a/Dalamud.sln
+++ b/Dalamud.sln
@@ -40,6 +40,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "Dala
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.LocExporter", "tools\Dalamud.LocExporter\Dalamud.LocExporter.csproj", "{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -102,6 +104,10 @@ Global
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs
index cfe8ba411..63d56fdb6 100644
--- a/Dalamud/Configuration/Internal/DevPluginSettings.cs
+++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
namespace Dalamud.Configuration.Internal;
@@ -21,4 +22,9 @@ internal sealed class DevPluginSettings
/// Gets or sets an ID uniquely identifying this specific instance of a devPlugin.
///
public Guid WorkingPluginId { get; set; } = Guid.Empty;
+
+ ///
+ /// Gets or sets a list of validation problems that have been dismissed by the user.
+ ///
+ public List DismissedValidationProblems { get; set; } = new();
}
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 6b0ca1c0c..f80962a9d 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -8,7 +8,7 @@
- 9.1.0.2
+ 9.1.0.5
XIV Launcher addon framework
$(DalamudVersion)
$(DalamudVersion)
diff --git a/Dalamud/Interface/Fools24.cs b/Dalamud/Interface/Fools24.cs
new file mode 100644
index 000000000..9fa3eb7b9
--- /dev/null
+++ b/Dalamud/Interface/Fools24.cs
@@ -0,0 +1,96 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Interface.Internal.Windows;
+using Dalamud.Networking.Http;
+
+using Serilog;
+
+namespace Dalamud.Interface;
+
+[ServiceManager.EarlyLoadedService]
+[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "One-off")]
+internal class Fools24 : IServiceType
+{
+ private readonly HappyHttpClient httpClient;
+
+ private CancellationTokenSource? cancellation;
+ private Task? horseIconTask = null;
+
+ private string[]? applicableIcons = null;
+
+ [ServiceManager.ServiceConstructor]
+ public Fools24(HappyHttpClient httpClient)
+ {
+ this.httpClient = httpClient;
+ }
+
+ public bool IsWaitingForIconList => this.horseIconTask?.IsCompleted == false;
+
+ public bool Failed { get; private set; }
+
+ public static bool IsDayApplicable()
+ {
+ var utcNow = DateTime.UtcNow;
+
+ var dateAhead = utcNow.AddHours(14);
+ var dateBehind = utcNow.AddHours(-12);
+
+ return dateAhead is { Day: 1, Month: 4 } || dateBehind is { Day: 1, Month: 4 } || DateTime.Now is { Day: 1, Month: 4 };
+ }
+
+ public string? GetHorseIconLink(string internalName)
+ {
+ if (this.applicableIcons == null || this.applicableIcons.All(x => x != $"{internalName}.png"))
+ return null;
+
+ return $"https://raw.githubusercontent.com/goaaats/horse-icons/main/icons/{internalName}.png";
+ }
+
+ public void NotifyInstallerWindowOpened()
+ {
+ if (!IsDayApplicable())
+ return;
+
+ Service.Get().ClearIconCache();
+
+ if (this.horseIconTask?.IsCompleted == false)
+ return;
+
+ this.Failed = false;
+ try
+ {
+ this.cancellation = new CancellationTokenSource();
+ this.horseIconTask = this.httpClient.SharedHttpClient.GetStringAsync("https://raw.githubusercontent.com/goaaats/horse-icons/main/iconlist.txt", this.cancellation.Token)
+ .ContinueWith(
+ f =>
+ {
+ if (!f.IsCompletedSuccessfully)
+ {
+ this.Failed = true;
+ this.applicableIcons = null;
+ return;
+ }
+
+ if (f.Result is not { Length: > 0 })
+ {
+ this.Failed = true;
+ this.applicableIcons = null;
+ return;
+ }
+
+ this.applicableIcons = f.Result.Split(
+ '\n',
+ StringSplitOptions.RemoveEmptyEntries);
+ });
+ this.cancellation.CancelAfter(10000);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Failed to fetch horse icons");
+ this.Failed = true;
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs
index 18bb57118..7811c1aaa 100644
--- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs
+++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs
@@ -56,7 +56,7 @@ internal static class NotificationConstants
public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f;
/// Default duration of the notification.
- public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3);
+ public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(7);
/// Duration of show animation.
public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300);
diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs
index 64040011e..2b0da5aca 100644
--- a/Dalamud/Interface/Internal/DalamudIme.cs
+++ b/Dalamud/Interface/Internal/DalamudIme.cs
@@ -11,6 +11,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Text.Unicode;
+using Dalamud.Game;
using Dalamud.Game.Text;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts;
@@ -110,6 +111,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
/// Undo range for modifying the buffer while composition is in progress.
private (int Start, int End, int Cursor)? temporaryUndoSelection;
+ private bool hadWantTextInput;
private bool updateInputLanguage = true;
private bool updateImeStatusAgain;
@@ -262,20 +264,37 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
if (!ImGuiHelpers.IsImGuiInitialized)
{
this.updateInputLanguage = true;
+ this.temporaryUndoSelection = null;
return;
}
// Are we not the target of text input?
if (!ImGui.GetIO().WantTextInput)
{
+ if (this.hadWantTextInput)
+ {
+ // Force the cancellation of whatever was being input.
+ var hImc2 = ImmGetContext(args.Hwnd);
+ if (hImc2 != 0)
+ {
+ ImmNotifyIME(hImc2, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0);
+ ImmReleaseContext(args.Hwnd, hImc2);
+ }
+ }
+
+ this.hadWantTextInput = false;
this.updateInputLanguage = true;
+ this.temporaryUndoSelection = null;
return;
}
+ this.hadWantTextInput = true;
+
var hImc = ImmGetContext(args.Hwnd);
if (hImc == nint.Zero)
{
this.updateInputLanguage = true;
+ this.temporaryUndoSelection = null;
return;
}
@@ -338,9 +357,11 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
this.updateInputLanguage = false;
}
+ // Microsoft Korean IME and Google Japanese IME drop notifying us of a candidate list change.
+ // As the candidate list update is already there on the next WndProc call, update the candidate list again
+ // here.
if (this.updateImeStatusAgain)
{
- this.ReplaceCompositionString(hImc, false);
this.UpdateCandidates(hImc);
this.updateImeStatusAgain = false;
}
@@ -410,7 +431,8 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
or VK.VK_RIGHT
or VK.VK_DOWN
or VK.VK_RETURN:
- if (this.candidateStrings.Count != 0)
+ // If key inputs that usually result in focus change, cancel the input process.
+ if (!string.IsNullOrEmpty(ImmGetCompositionString(hImc, GCS.GCS_COMPSTR)))
{
this.ClearState(hImc);
args.WParam = VK.VK_PROCESSKEY;
@@ -428,7 +450,15 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
case WM.WM_RBUTTONDOWN:
case WM.WM_MBUTTONDOWN:
case WM.WM_XBUTTONDOWN:
- ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
+ // If mouse click happened while IME composition was in progress, force complete the input process.
+ if (!string.IsNullOrEmpty(ImmGetCompositionString(hImc, GCS.GCS_COMPSTR)))
+ {
+ ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
+
+ // Disable further handling of mouse button down event, or something would lock up the cursor.
+ args.SuppressWithValue(1);
+ }
+
break;
}
}
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index ec18fbb69..834bd9865 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -943,7 +943,7 @@ internal class DalamudInterface : IInternalDisposableService
if (ImGui.MenuItem("Export localizable"))
{
- localization.ExportLocalizable();
+ localization.ExportLocalizable(true);
}
if (ImGui.BeginMenu("Load language..."))
diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
index 634999143..9e0ef4df7 100644
--- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
@@ -191,6 +191,11 @@ internal class PluginImageCache : IInternalDisposableService
{
iconTexture = null;
loadedSince = null;
+
+ // Wait for the horse icon list to be there, if applicable
+ var fools = Service.Get();
+ if (Fools24.IsDayApplicable() && fools.IsWaitingForIconList && !fools.Failed)
+ return false;
if (manifest == null || manifest.InternalName == null)
{
@@ -638,6 +643,14 @@ internal class PluginImageCache : IInternalDisposableService
{
if (isThirdParty)
return manifest.IconUrl;
+
+ var fools = Service.Get();
+ if (Fools24.IsDayApplicable())
+ {
+ var iconLink = fools.GetHorseIconLink(manifest.InternalName);
+ if (iconLink != null)
+ return iconLink;
+ }
return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png");
}
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index ea49ef3ba..88d66eb84 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -268,6 +268,8 @@ internal class PluginInstallerWindow : Window, IDisposable
///
public override void OnOpen()
{
+ Service.Get().NotifyInstallerWindowOpened();
+
var pluginManager = Service.Get();
_ = pluginManager.ReloadPluginMastersAsync();
@@ -596,6 +598,7 @@ internal class PluginInstallerWindow : Window, IDisposable
using (ImRaii.Disabled(isProfileManager))
{
var searchTextChanged = false;
+ var prevSearchText = this.searchText;
ImGui.SetNextItemWidth(searchInputWidth);
searchTextChanged |= ImGui.InputTextWithHint(
"###XlPluginInstaller_Search",
@@ -615,7 +618,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
if (searchTextChanged)
- this.UpdateCategoriesOnSearchChange();
+ this.UpdateCategoriesOnSearchChange(prevSearchText);
}
// Disable sort if changelogs or profile editor
@@ -2458,6 +2461,12 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
+ if (plugin is LocalDevPlugin devPlugin)
+ {
+ this.DrawDevPluginValidationIssues(devPlugin);
+ ImGuiHelpers.ScaledDummy(5);
+ }
+
// Controls
this.DrawPluginControlButton(plugin, availablePluginUpdate);
this.DrawDevPluginButtons(plugin);
@@ -2961,6 +2970,100 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
+ private void DrawDevPluginValidationIssues(LocalDevPlugin devPlugin)
+ {
+ if (!devPlugin.IsLoaded)
+ {
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "You have to load this plugin to see validation issues.");
+ }
+ else
+ {
+ var problems = PluginValidator.CheckForProblems(devPlugin);
+ if (problems.Count == 0)
+ {
+ ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.Text(FontAwesomeIcon.Check.ToIconString());
+ ImGui.PopFont();
+ ImGui.SameLine();
+ ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "No validation issues found in this plugin!");
+ }
+ else
+ {
+ var numValidProblems = problems.Count(
+ problem => devPlugin.DismissedValidationProblems.All(name => name != problem.GetType().Name));
+ var shouldBother = numValidProblems > 0;
+ var validationIssuesText = shouldBother ?
+ $"Found {problems.Count} validation issue{(problems.Count > 1 ? "s" : string.Empty)} in this plugin!" :
+ $"{problems.Count} dismissed validation issue{(problems.Count > 1 ? "s" : string.Empty)} in this plugin.";
+
+ using var col = ImRaii.PushColor(ImGuiCol.Text, shouldBother ? ImGuiColors.DalamudOrange : ImGuiColors.DalamudGrey);
+ using var tree = ImRaii.TreeNode($"{validationIssuesText}###validationIssueCollapsible");
+ if (tree.Success)
+ {
+ foreach (var problem in problems)
+ {
+ var thisProblemIsDismissed = devPlugin.DismissedValidationProblems.Contains(problem.GetType().Name);
+
+ if (!thisProblemIsDismissed)
+ {
+ using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudWhite))
+ {
+ if (ImGuiComponents.IconButton(
+ $"##dismissValidationIssue{problem.GetType().Name}",
+ FontAwesomeIcon.TimesCircle))
+ {
+ devPlugin.DismissedValidationProblems.Add(problem.GetType().Name);
+ Service.Get().QueueSave();
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip("Dismiss this issue");
+ }
+ }
+
+ ImGui.SameLine();
+ }
+
+ var iconColor = problem.Severity switch
+ {
+ PluginValidator.ValidationSeverity.Fatal => ImGuiColors.DalamudRed,
+ PluginValidator.ValidationSeverity.Warning => ImGuiColors.DalamudOrange,
+ PluginValidator.ValidationSeverity.Information => ImGuiColors.TankBlue,
+ _ => ImGuiColors.DalamudGrey,
+ };
+
+ using (ImRaii.PushColor(ImGuiCol.Text, iconColor))
+ using (ImRaii.PushFont(InterfaceManager.IconFont))
+ {
+ switch (problem.Severity)
+ {
+ case PluginValidator.ValidationSeverity.Fatal:
+ ImGui.Text(FontAwesomeIcon.TimesCircle.ToIconString());
+ break;
+ case PluginValidator.ValidationSeverity.Warning:
+ ImGui.Text(FontAwesomeIcon.ExclamationTriangle.ToIconString());
+ break;
+ case PluginValidator.ValidationSeverity.Information:
+ ImGui.Text(FontAwesomeIcon.InfoCircle.ToIconString());
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ ImGui.SameLine();
+
+ using (ImRaii.PushColor(ImGuiCol.Text, thisProblemIsDismissed ? ImGuiColors.DalamudGrey : ImGuiColors.DalamudWhite))
+ {
+ ImGuiHelpers.SafeTextWrapped(problem.GetLocalizedDescription());
+ }
+ }
+ }
+ }
+ }
+ }
+
private void DrawDevPluginButtons(LocalPlugin localPlugin)
{
ImGui.SameLine();
@@ -3341,15 +3444,27 @@ internal class PluginInstallerWindow : Window, IDisposable
return this.updateModalTaskCompletionSource.Task;
}
- private void UpdateCategoriesOnSearchChange()
+ private void UpdateCategoriesOnSearchChange(string? previousSearchText)
{
if (string.IsNullOrEmpty(this.searchText))
{
this.categoryManager.SetCategoryHighlightsForPlugins(null);
+
+ // Reset here for good measure, as we're returning from a search
+ this.openPluginCollapsibles.Clear();
}
else
{
- var pluginsMatchingSearch = this.pluginListAvailable.Where(rm => !this.IsManifestFiltered(rm));
+ var pluginsMatchingSearch = this.pluginListAvailable.Where(rm => !this.IsManifestFiltered(rm)).ToArray();
+
+ // Check if the search results are different, and clear the open collapsibles if they are
+ if (previousSearchText != null)
+ {
+ var previousSearchResults = this.pluginListAvailable.Where(rm => !this.IsManifestFiltered(rm)).ToArray();
+ if (!previousSearchResults.SequenceEqual(pluginsMatchingSearch))
+ this.openPluginCollapsibles.Clear();
+ }
+
this.categoryManager.SetCategoryHighlightsForPlugins(pluginsMatchingSearch);
}
}
@@ -3357,7 +3472,7 @@ internal class PluginInstallerWindow : Window, IDisposable
private void UpdateCategoriesOnPluginsChange()
{
this.categoryManager.BuildCategories(this.pluginListAvailable);
- this.UpdateCategoriesOnSearchChange();
+ this.UpdateCategoriesOnSearchChange(null);
}
private void DrawFontawesomeIconOutlined(FontAwesomeIcon icon, Vector4 outline, Vector4 iconColor)
@@ -3760,7 +3875,7 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string FeedbackModal_ContactInformationRequired => Loc.Localize("InstallerFeedbackContactInfoRequired", "Contact information has not been provided. We require contact information to respond to questions, or to request additional information to troubleshoot problems.");
- public static string FeedbackModal_ContactInformationDiscordButton => Loc.Localize("ContactInformationDiscordButton", "Join Goat Place Discord");
+ public static string FeedbackModal_ContactInformationDiscordButton => Loc.Localize("ContactInformationDiscordButton", "Join XIVLauncher & Dalamud Discord");
public static string FeedbackModal_ContactInformationDiscordUrl => Loc.Localize("ContactInformationDiscordUrl", "https://goat.place/");
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
index 9f3196928..6bee26755 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs
@@ -651,13 +651,13 @@ internal class ProfileManagerWidget
Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:");
public static string TutorialCommandsEnable =>
- Loc.Localize("ProfileManagerTutorialCommandsEnable", "/xlenableprofile \"Collection Name\" - Enable a collection");
+ Loc.Localize("ProfileManagerTutorialCommandsEnable", "{0} \"Collection Name\" - Enable a collection").Format(ProfileCommandHandler.CommandEnable);
public static string TutorialCommandsDisable =>
- Loc.Localize("ProfileManagerTutorialCommandsDisable", "/xldisableprofile \"Collection Name\" - Disable a collection");
+ Loc.Localize("ProfileManagerTutorialCommandsDisable", "{0} \"Collection Name\" - Disable a collection").Format(ProfileCommandHandler.CommandDisable);
public static string TutorialCommandsToggle =>
- Loc.Localize("ProfileManagerTutorialCommandsToggle", "/xltoggleprofile \"Collection Name\" - Toggle a collection's state");
+ Loc.Localize("ProfileManagerTutorialCommandsToggle", "{0} \"Collection Name\" - Toggle a collection's state").Format(ProfileCommandHandler.CommandToggle);
public static string TutorialCommandsEnd =>
Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order.");
diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs
index 47ba2c65f..4c67afce4 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs
@@ -182,7 +182,7 @@ internal class SettingsWindow : Window
{
ImGui.SetTooltip(!ImGui.IsKeyDown(ImGuiKey.ModShift)
? Loc.Localize("DalamudSettingsSaveAndExit", "Save changes and close")
- : Loc.Localize("DalamudSettingsSaveAndExit", "Save changes"));
+ : Loc.Localize("DalamudSettingsSave", "Save changes"));
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
index c7fcdc58d..1c446f26b 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs
@@ -133,7 +133,7 @@ public class SettingsTabLook : SettingsTab
new SettingsEntry(
Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"),
- Loc.Localize("DalamudSettingReducedMotion", "This will suppress certain animations from Dalamud, such as the notification popup."),
+ Loc.Localize("DalamudSettingReducedMotionHint", "This will suppress certain animations from Dalamud, such as the notification popup."),
c => c.ReduceMotions ?? false,
(v, c) => c.ReduceMotions = v),
};
diff --git a/Dalamud/Interface/ManagedFontAtlas/FluentGlyphRangeBuilder.cs b/Dalamud/Interface/ManagedFontAtlas/FluentGlyphRangeBuilder.cs
new file mode 100644
index 000000000..ec395a61f
--- /dev/null
+++ b/Dalamud/Interface/ManagedFontAtlas/FluentGlyphRangeBuilder.cs
@@ -0,0 +1,361 @@
+using System.Buffers;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Unicode;
+
+using Dalamud.Interface.ManagedFontAtlas.Internals;
+
+namespace Dalamud.Interface.ManagedFontAtlas;
+
+/// A fluent ImGui glyph range builder.
+[SuppressMessage(
+ "StyleCop.CSharp.SpacingRules",
+ "SA1010:Opening square brackets should be spaced correctly",
+ Justification = "No")]
+public struct FluentGlyphRangeBuilder
+{
+ private const int ImUnicodeCodepointMax = char.MaxValue;
+
+ private BitArray? characters;
+
+ /// Clears the builder.
+ /// this for method chaining.
+ /// A builder is in cleared state on first use.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public FluentGlyphRangeBuilder Clear()
+ {
+ this.characters?.SetAll(false);
+ return this;
+ }
+
+ /// Adds a single codepoint to the builder.
+ /// The codepoint to add.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public FluentGlyphRangeBuilder With(char codepoint) => this.With((int)codepoint);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public FluentGlyphRangeBuilder With(uint codepoint) =>
+ codepoint <= char.MaxValue ? this.With((int)codepoint) : this;
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public FluentGlyphRangeBuilder With(int codepoint)
+ {
+ if (codepoint <= ImUnicodeCodepointMax)
+ this.EnsureCharacters().Set(codepoint, true);
+ return this;
+ }
+
+ /// Adds a unicode range to the builder.
+ /// The unicode range to add.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(UnicodeRange range) =>
+ this.With(range.FirstCodePoint, (range.FirstCodePoint + range.Length) - 1);
+
+ /// Adds unicode ranges to the builder.
+ /// The 1st unicode range to add.
+ /// The 2st unicode range to add.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(UnicodeRange range1, UnicodeRange range2) =>
+ this.With(range1.FirstCodePoint, (range1.FirstCodePoint + range1.Length) - 1)
+ .With(range2.FirstCodePoint, (range2.FirstCodePoint + range2.Length) - 1);
+
+ /// Adds unicode ranges to the builder.
+ /// The 1st unicode range to add.
+ /// The 2st unicode range to add.
+ /// The 3rd unicode range to add.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(UnicodeRange range1, UnicodeRange range2, UnicodeRange range3) =>
+ this.With(range1.FirstCodePoint, (range1.FirstCodePoint + range1.Length) - 1)
+ .With(range2.FirstCodePoint, (range2.FirstCodePoint + range2.Length) - 1)
+ .With(range3.FirstCodePoint, (range3.FirstCodePoint + range3.Length) - 1);
+
+ /// Adds unicode ranges to the builder.
+ /// The 1st unicode range to add.
+ /// The 2st unicode range to add.
+ /// The 3rd unicode range to add.
+ /// The 4th unicode range to add.
+ /// Even more unicode ranges to add.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(
+ UnicodeRange range1,
+ UnicodeRange range2,
+ UnicodeRange range3,
+ UnicodeRange range4,
+ params UnicodeRange[] evenMoreRanges) =>
+ this.With(range1.FirstCodePoint, (range1.FirstCodePoint + range1.Length) - 1)
+ .With(range2.FirstCodePoint, (range2.FirstCodePoint + range2.Length) - 1)
+ .With(range3.FirstCodePoint, (range3.FirstCodePoint + range3.Length) - 1)
+ .With(range4.FirstCodePoint, (range4.FirstCodePoint + range4.Length) - 1)
+ .With(evenMoreRanges);
+
+ /// Adds unicode ranges to the builder.
+ /// Unicode ranges to add.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(IEnumerable ranges)
+ {
+ foreach (var range in ranges)
+ this.With(range);
+ return this;
+ }
+
+ /// Adds a range of characters to the builder.
+ /// The first codepoint, inclusive.
+ /// The last codepoint, inclusive.
+ /// this for method chaining.
+ ///
+ /// Unsupported codepoints will be ignored.
+ /// If is more than , then they will be swapped.
+ ///
+ public FluentGlyphRangeBuilder With(char from, char to) =>
+ this.With(Math.Clamp(from, int.MinValue, int.MaxValue), Math.Clamp(to, int.MinValue, int.MaxValue));
+
+ ///
+ public FluentGlyphRangeBuilder With(uint from, uint to) =>
+ this.With((int)Math.Min(from, int.MaxValue), (int)Math.Min(to, int.MaxValue));
+
+ ///
+ public FluentGlyphRangeBuilder With(int from, int to)
+ {
+ from = Math.Clamp(from, 1, ImUnicodeCodepointMax);
+ to = Math.Clamp(to, 1, ImUnicodeCodepointMax);
+ if (from > to)
+ (from, to) = (to, from);
+
+ var bits = this.EnsureCharacters();
+ for (; from <= to; from++)
+ bits.Set(from, true);
+ return this;
+ }
+
+ /// Adds characters from a UTF-8 character sequence.
+ /// The sequence.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(ReadOnlySpan utf8Sequence)
+ {
+ var bits = this.EnsureCharacters();
+ while (!utf8Sequence.IsEmpty)
+ {
+ if (Rune.DecodeFromUtf8(utf8Sequence, out var rune, out var len) == OperationStatus.Done
+ && rune.Value < ImUnicodeCodepointMax)
+ bits.Set(rune.Value, true);
+ utf8Sequence = utf8Sequence[len..];
+ }
+
+ return this;
+ }
+
+ /// Adds characters from a UTF-8 character sequence.
+ /// The sequence.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(IEnumerable utf8Sequence)
+ {
+ Span buf = stackalloc byte[4];
+ var bufp = 0;
+ var bits = this.EnsureCharacters();
+ foreach (var b in utf8Sequence)
+ {
+ buf[bufp++] = b;
+
+ while (Rune.DecodeFromUtf8(buf[..bufp], out var rune, out var len) is var state
+ && state != OperationStatus.NeedMoreData)
+ {
+ switch (state)
+ {
+ case OperationStatus.Done when rune.Value <= ImUnicodeCodepointMax:
+ bits.Set(rune.Value, true);
+ goto case OperationStatus.InvalidData;
+
+ case OperationStatus.InvalidData:
+ bufp -= len;
+ break;
+
+ case OperationStatus.NeedMoreData:
+ case OperationStatus.DestinationTooSmall:
+ default:
+ throw new InvalidOperationException($"Unexpected return from {Rune.DecodeFromUtf8}.");
+ }
+ }
+ }
+
+ return this;
+ }
+
+ /// Adds characters from a UTF-16 character sequence.
+ /// The sequence.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(ReadOnlySpan utf16Sequence)
+ {
+ var bits = this.EnsureCharacters();
+ while (!utf16Sequence.IsEmpty)
+ {
+ if (Rune.DecodeFromUtf16(utf16Sequence, out var rune, out var len) == OperationStatus.Done
+ && rune.Value <= ImUnicodeCodepointMax)
+ bits.Set(rune.Value, true);
+ utf16Sequence = utf16Sequence[len..];
+ }
+
+ return this;
+ }
+
+ /// Adds characters from a UTF-16 character sequence.
+ /// The sequence.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(IEnumerable utf16Sequence)
+ {
+ var bits = this.EnsureCharacters();
+ foreach (var c in utf16Sequence)
+ {
+ if (!char.IsSurrogate(c))
+ bits.Set(c, true);
+ }
+
+ return this;
+ }
+
+ /// Adds characters from a string.
+ /// The string.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored.
+ public FluentGlyphRangeBuilder With(string @string) => this.With(@string.AsSpan());
+
+ /// Adds glyphs that are likely to be used in the given culture to the builder.
+ /// A culture info.
+ /// this for method chaining.
+ /// Unsupported codepoints will be ignored. Unsupported culture will do nothing.
+ /// Do make a PR if you need more.
+ public FluentGlyphRangeBuilder WithLanguage(CultureInfo cultureInfo)
+ {
+ // Call in chunks of three to avoid allocating arrays.
+ // Avoid adding ranges that goes over BMP; that is, ranges that goes over ImUnicodeCodepointMax.
+ switch (cultureInfo.TwoLetterISOLanguageName)
+ {
+ case "ja":
+ // http://www.rikai.com/library/kanjitables/kanji_codes.unicode.shtml
+ return
+ this
+ .With(
+ UnicodeRanges.CjkSymbolsandPunctuation,
+ UnicodeRanges.Hiragana,
+ UnicodeRanges.Katakana)
+ .With(
+ UnicodeRanges.HalfwidthandFullwidthForms,
+ UnicodeRanges.CjkUnifiedIdeographs,
+ UnicodeRanges.CjkUnifiedIdeographsExtensionA)
+ // Blame Japanese cell carriers for the below.
+ .With(
+ UnicodeRanges.EnclosedCjkLettersandMonths);
+ case "zh":
+ return
+ this
+ .With(
+ UnicodeRanges.CjkUnifiedIdeographs,
+ UnicodeRanges.CjkUnifiedIdeographsExtensionA);
+ case "ko":
+ return
+ this
+ .With(
+ UnicodeRanges.HangulJamo,
+ UnicodeRanges.HangulCompatibilityJamo,
+ UnicodeRanges.HangulSyllables)
+ .With(
+ UnicodeRanges.HangulJamoExtendedA,
+ UnicodeRanges.HangulJamoExtendedB);
+ default:
+ return this;
+ }
+ }
+
+ /// Adds glyphs that are likely to be used in the given culture to the builder.
+ /// A language tag that will be used to locate the culture info.
+ /// this for method chaining.
+ /// See documentation for supported language tags.
+ ///
+ public FluentGlyphRangeBuilder WithLanguage(string languageTag) =>
+ this.WithLanguage(CultureInfo.GetCultureInfo(languageTag));
+
+ /// Builds the accumulated data into an ImGui glyph range.
+ /// Whether to add the default fallback codepoints to the range.
+ /// Whether to add the default ellipsis codepoints to the range.
+ /// The built ImGui glyph ranges.
+ public ushort[] Build(bool addFallbackCodepoints = true, bool addEllipsisCodepoints = true)
+ {
+ if (addFallbackCodepoints)
+ this.With(FontAtlasFactory.FallbackCodepoints);
+ if (addEllipsisCodepoints)
+ this.With(FontAtlasFactory.EllipsisCodepoints).With('.');
+ return this.BuildExact();
+ }
+
+ /// Builds the accumulated data into an ImGui glyph range, exactly as specified.
+ /// The built ImGui glyph ranges.
+ public ushort[] BuildExact()
+ {
+ if (this.characters is null)
+ return [0];
+ var bits = this.characters;
+
+ // Count the number of ranges first.
+ var numRanges = 0;
+ var lastCodepoint = -1;
+ for (var i = 1; i <= ImUnicodeCodepointMax; i++)
+ {
+ if (bits.Get(i))
+ {
+ if (lastCodepoint == -1)
+ lastCodepoint = i;
+ }
+ else
+ {
+ if (lastCodepoint != -1)
+ {
+ numRanges++;
+ lastCodepoint = -1;
+ }
+ }
+ }
+
+ // Handle the final range that terminates on the ending boundary.
+ if (lastCodepoint != -1)
+ numRanges++;
+
+ // Allocate the array and build the range.
+ var res = GC.AllocateUninitializedArray((numRanges * 2) + 1);
+ var resp = 0;
+ for (var i = 1; i <= ImUnicodeCodepointMax; i++)
+ {
+ if (bits.Get(i) == ((resp & 1) == 0))
+ res[resp++] = unchecked((ushort)i);
+ }
+
+ // Handle the final range that terminates on the ending boundary.
+ if ((resp & 1) == 1)
+ res[resp++] = ImUnicodeCodepointMax;
+
+ // Add the zero terminator.
+ res[resp] = 0;
+
+ return res;
+ }
+
+ /// Ensures that is not null, by creating one as necessary.
+ /// An instance of .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private BitArray EnsureCharacters() => this.characters ??= new(ImUnicodeCodepointMax + 1);
+}
diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs
index 4c3e9023a..3b8bfd965 100644
--- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
+using System.Text.Unicode;
using Dalamud.Interface.Utility;
@@ -12,6 +13,34 @@ namespace Dalamud.Interface.ManagedFontAtlas;
///
public static class FontAtlasBuildToolkitUtilities
{
+ /// Begins building a new array of containing ImGui glyph ranges.
+ /// The chars.
+ /// A new range builder.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static FluentGlyphRangeBuilder BeginGlyphRange(this IEnumerable chars) =>
+ default(FluentGlyphRangeBuilder).With(chars);
+
+ /// Begins building a new array of containing ImGui glyph ranges.
+ /// The chars.
+ /// A new range builder.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static FluentGlyphRangeBuilder BeginGlyphRange(this ReadOnlySpan chars) =>
+ default(FluentGlyphRangeBuilder).With(chars);
+
+ /// Begins building a new array of containing ImGui glyph ranges.
+ /// The chars.
+ /// A new range builder.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static FluentGlyphRangeBuilder BeginGlyphRange(this string chars) =>
+ default(FluentGlyphRangeBuilder).With(chars);
+
+ /// Begins building a new array of containing ImGui glyph ranges.
+ /// The unicode range.
+ /// A new range builder.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static FluentGlyphRangeBuilder BeginGlyphRange(this UnicodeRange range) =>
+ default(FluentGlyphRangeBuilder).With(range);
+
///
/// Compiles given s into an array of containing ImGui glyph ranges.
///
@@ -19,16 +48,12 @@ public static class FontAtlasBuildToolkitUtilities
/// Add fallback codepoints to the range.
/// Add ellipsis codepoints to the range.
/// The compiled range.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort[] ToGlyphRange(
this IEnumerable enumerable,
bool addFallbackCodepoints = true,
- bool addEllipsisCodepoints = true)
- {
- using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
- foreach (var c in enumerable)
- builder.AddChar(c);
- return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
- }
+ bool addEllipsisCodepoints = true) =>
+ enumerable.BeginGlyphRange().Build(addFallbackCodepoints, addEllipsisCodepoints);
///
/// Compiles given s into an array of containing ImGui glyph ranges.
@@ -37,16 +62,12 @@ public static class FontAtlasBuildToolkitUtilities
/// Add fallback codepoints to the range.
/// Add ellipsis codepoints to the range.
/// The compiled range.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort[] ToGlyphRange(
this ReadOnlySpan span,
bool addFallbackCodepoints = true,
- bool addEllipsisCodepoints = true)
- {
- using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
- foreach (var c in span)
- builder.AddChar(c);
- return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
- }
+ bool addEllipsisCodepoints = true) =>
+ span.BeginGlyphRange().Build(addFallbackCodepoints, addEllipsisCodepoints);
///
/// Compiles given string into an array of containing ImGui glyph ranges.
@@ -55,11 +76,12 @@ public static class FontAtlasBuildToolkitUtilities
/// Add fallback codepoints to the range.
/// Add ellipsis codepoints to the range.
/// The compiled range.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort[] ToGlyphRange(
this string @string,
bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true) =>
- @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints);
+ @string.BeginGlyphRange().Build(addFallbackCodepoints, addEllipsisCodepoints);
///
/// Finds the corresponding in
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs
index 9b80d27ff..b32e3db18 100644
--- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System.Globalization;
+using System.IO;
using System.Runtime.InteropServices;
using Dalamud.Interface.FontIdentifier;
@@ -8,6 +9,8 @@ using Dalamud.Utility;
using ImGuiNET;
+using TerraFX.Interop.DirectX;
+
namespace Dalamud.Interface.ManagedFontAtlas;
///
@@ -216,10 +219,35 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// The added font.
ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont);
+ /// Adds glyphs from the Windows default font for the given culture info into the provided font.
+ /// The culture info.
+ /// The font config. If is not set, then
+ /// will be used as the target. If that is empty too, then it will do
+ /// nothing.
+ /// The font weight, in range from 1 to 1000. 400 is regular(normal).
+ ///
+ /// The font stretch, in range from 1 to 9. 5 is medium(normal).
+ ///
+ /// The font style, in range from 0 to 2. 0 is normal.
+ ///
+ /// May do nothing at all if is unsupported by Dalamud font handler.
+ /// See
+ /// Microsoft
+ /// Learn for the fonts.
+ ///
+ void AttachWindowsDefaultFont(
+ CultureInfo cultureInfo,
+ in SafeFontConfig fontConfig,
+ int weight = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL,
+ int stretch = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL,
+ int style = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL);
+
///
/// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
/// will be ignored.
///
- /// The font config.
+ /// The font config. If is not set, then
+ /// will be used as the target. If that is empty too, then it will do
+ /// nothing.
void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig);
}
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
index 55af20329..34d28ccbd 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs
@@ -1,9 +1,9 @@
using System.Buffers;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
-using System.Text.Unicode;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.FontIdentifier;
@@ -17,6 +17,8 @@ using ImGuiNET;
using SharpDX.DXGI;
+using TerraFX.Interop.DirectX;
+
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
///
@@ -433,9 +435,163 @@ internal sealed partial class FontAtlasFactory
public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) =>
this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges);
+ ///
+ public void AttachWindowsDefaultFont(
+ CultureInfo cultureInfo,
+ in SafeFontConfig fontConfig,
+ int weight = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL,
+ int stretch = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL,
+ int style = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL)
+ {
+ var targetFont = fontConfig.MergeFont;
+ if (targetFont.IsNull())
+ targetFont = this.Font;
+ if (targetFont.IsNull())
+ return;
+
+ // https://learn.microsoft.com/en-us/windows/apps/design/globalizing/loc-international-fonts
+ var splitTag = cultureInfo.IetfLanguageTag.Split("-");
+ foreach (var test in new[]
+ {
+ cultureInfo.IetfLanguageTag,
+ $"{splitTag[0]}-{splitTag[^1]}",
+ })
+ {
+ var familyName = test switch
+ {
+ "af-ZA" => "Segoe UI",
+ "am-ET" => "Ebrima",
+ "ar-SA" => "Segoe UI",
+ "as-IN" => "Nirmala UI",
+ "az-Latn-AZ" => "Segoe UI",
+ "be-BY" => "Segoe UI",
+ "bg-BG" => "Segoe UI",
+ "bn-BD" => "Nirmala UI",
+ "bn-IN" => "Nirmala UI",
+ "bs-Latn-BA" => "Segoe UI",
+ "ca-ES" => "Segoe UI",
+ "ca-ES-valencia" => "Segoe UI",
+ "chr-CHER-US" => "Gadugi",
+ "cs-CZ" => "Segoe UI",
+ "cy-GB" => "Segoe UI",
+ "da-DK" => "Segoe UI",
+ "de-DE" => "Segoe UI",
+ "el-GR" => "Segoe UI",
+ "en-GB" => "Segoe UI",
+ "es-ES" => "Segoe UI",
+ "et-EE" => "Segoe UI",
+ "eu-ES" => "Segoe UI",
+ "fa-IR" => "Segoe UI",
+ "fi-FI" => "Segoe UI",
+ "fil-PH" => "Segoe UI",
+ "fr-FR" => "Segoe UI",
+ "ga-IE" => "Segoe UI",
+ "gd-GB" => "Segoe UI",
+ "gl-ES" => "Segoe UI",
+ "gu-IN" => "Nirmala UI",
+ "ha-Latn-NG" => "Segoe UI",
+ "he-IL" => "Segoe UI",
+ "hi-IN" => "Nirmala UI",
+ "hr-HR" => "Segoe UI",
+ "hu-HU" => "Segoe UI",
+ "hy-AM" => "Segoe UI",
+ "id-ID" => "Segoe UI",
+ "ig-NG" => "Segoe UI",
+ "is-IS" => "Segoe UI",
+ "it-IT" => "Segoe UI",
+ "ja-JP" => "Yu Gothic UI",
+ "ka-GE" => "Segoe UI",
+ "kk-KZ" => "Segoe UI",
+ "km-KH" => "Leelawadee UI",
+ "kn-IN" => "Nirmala UI",
+ "ko-KR" => "Malgun Gothic",
+ "kok-IN" => "Nirmala UI",
+ "ku-ARAB-IQ" => "Segoe UI",
+ "ky-KG" => "Segoe UI",
+ "lb-LU" => "Segoe UI",
+ "lt-LT" => "Segoe UI",
+ "lv-LV" => "Segoe UI",
+ "mi-NZ" => "Segoe UI",
+ "mk-MK" => "Segoe UI",
+ "ml-IN" => "Nirmala UI",
+ "mn-MN" => "Segoe UI",
+ "mr-IN" => "Nirmala UI",
+ "ms-MY" => "Segoe UI",
+ "mt-MT" => "Segoe UI",
+ "nb-NO" => "Segoe UI",
+ "ne-NP" => "Nirmala UI",
+ "nl-NL" => "Segoe UI",
+ "nn-NO" => "Segoe UI",
+ "nso-ZA" => "Segoe UI",
+ "or-IN" => "Nirmala UI",
+ "pa-Arab-PK" => "Segoe UI",
+ "pa-IN" => "Nirmala UI",
+ "pl-PL" => "Segoe UI",
+ "prs-AF" => "Segoe UI",
+ "pt-BR" => "Segoe UI",
+ "pt-PT" => "Segoe UI",
+ "qut-GT" => "Segoe UI",
+ "quz-PE" => "Segoe UI",
+ "ro-RO" => "Segoe UI",
+ "ru-RU" => "Segoe UI",
+ "rw-RW" => "Segoe UI",
+ "sd-Arab-PK" => "Segoe UI",
+ "si-LK" => "Nirmala UI",
+ "sk-SK" => "Segoe UI",
+ "sl-SI" => "Segoe UI",
+ "sq-AL" => "Segoe UI",
+ "sr-Cyrl-BA" => "Segoe UI",
+ "sr-Cyrl-CS" => "Segoe UI",
+ "sr-Latn-CS" => "Segoe UI",
+ "sv-SE" => "Segoe UI",
+ "sw-KE" => "Segoe UI",
+ "ta-IN" => "Nirmala UI",
+ "te-IN" => "Nirmala UI",
+ "tg-Cyrl-TJ" => "Segoe UI",
+ "th-TH" => "Leelawadee UI",
+ "ti-ET" => "Ebrima",
+ "tk-TM" => "Segoe UI",
+ "tn-ZA" => "Segoe UI",
+ "tr-TR" => "Segoe UI",
+ "tt-RU" => "Segoe UI",
+ "ug-CN" => "Segoe UI",
+ "uk-UA" => "Segoe UI",
+ "ur-PK" => "Segoe UI",
+ "uz-Latn-UZ" => "Segoe UI",
+ "vi-VN" => "Segoe UI",
+ "wo-SN" => "Segoe UI",
+ "xh-ZA" => "Segoe UI",
+ "yo-NG" => "Segoe UI",
+ "zh-CN" => "Microsoft YaHei UI",
+ "zh-HK" => "Microsoft JhengHei UI",
+ "zh-TW" => "Microsoft JhengHei UI",
+ "zh-Hans" => "Microsoft YaHei UI",
+ "zh-Hant" => "Microsoft YaHei UI",
+ "zu-ZA" => "Segoe UI",
+ _ => null,
+ };
+ if (familyName is null)
+ continue;
+ var family = IFontFamilyId
+ .ListSystemFonts(false)
+ .FirstOrDefault(
+ x => x.EnglishName.Equals(familyName, StringComparison.InvariantCultureIgnoreCase));
+ if (family?.Fonts[family.FindBestMatch(weight, stretch, style)] is not { } font)
+ return;
+ font.AddToBuildToolkit(this, fontConfig with { MergeFont = targetFont });
+ return;
+ }
+ }
+
///
public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig)
{
+ var targetFont = fontConfig.MergeFont;
+ if (targetFont.IsNull())
+ targetFont = this.Font;
+ if (targetFont.IsNull())
+ return;
+
var dalamudConfiguration = Service.Get();
if (dalamudConfiguration.EffectiveLanguage == "ko"
|| Service.GetNullable()?.EncounteredHangul is true)
@@ -444,41 +600,24 @@ internal sealed partial class FontAtlasFactory
DalamudAsset.NotoSansKrRegular,
fontConfig with
{
- GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
- UnicodeRanges.HangulJamo,
- UnicodeRanges.HangulCompatibilityJamo,
- UnicodeRanges.HangulSyllables,
- UnicodeRanges.HangulJamoExtendedA,
- UnicodeRanges.HangulJamoExtendedB),
+ MergeFont = targetFont,
+ GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("ko-kr").BuildExact(),
});
}
- var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
- var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc");
- if (!File.Exists(fontPathChs))
- fontPathChs = null;
-
- var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc");
- if (!File.Exists(fontPathCht))
- fontPathCht = null;
-
- if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw")
+ if (Service.Get().EffectiveLanguage == "tw")
{
- this.AddFontFromFile(fontPathCht, fontConfig with
+ this.AttachWindowsDefaultFont(CultureInfo.GetCultureInfo("zh-hant"), fontConfig with
{
- GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
- UnicodeRanges.CjkUnifiedIdeographs,
- UnicodeRanges.CjkUnifiedIdeographsExtensionA),
+ GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("zh-hant").BuildExact(),
});
}
- else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh"
- || Service.GetNullable()?.EncounteredHan is true))
+ else if (Service.Get().EffectiveLanguage == "zh"
+ || Service.GetNullable()?.EncounteredHan is true)
{
- this.AddFontFromFile(fontPathChs, fontConfig with
+ this.AttachWindowsDefaultFont(CultureInfo.GetCultureInfo("zh-hans"), fontConfig with
{
- GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
- UnicodeRanges.CjkUnifiedIdeographs,
- UnicodeRanges.CjkUnifiedIdeographsExtensionA),
+ GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("zh-hans").BuildExact(),
});
}
}
diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs
index 2c2ca9725..b80fe0b82 100644
--- a/Dalamud/Interface/UiBuilder.cs
+++ b/Dalamud/Interface/UiBuilder.cs
@@ -347,6 +347,12 @@ public sealed class UiBuilder : IDisposable
///
public IFontAtlas FontAtlas { get; }
+ ///
+ /// Gets a value indicating whether or not to use "reduced motion". This usually means that you should use less
+ /// intrusive animations, or disable them entirely.
+ ///
+ public bool ShouldUseReducedMotion => Service.Get().ReduceMotions ?? false;
+
///
/// Gets or sets a value indicating whether statistics about UI draw time should be collected.
///
diff --git a/Dalamud/Plugin/Internal/PluginValidator.cs b/Dalamud/Plugin/Internal/PluginValidator.cs
new file mode 100644
index 000000000..134bb2a4c
--- /dev/null
+++ b/Dalamud/Plugin/Internal/PluginValidator.cs
@@ -0,0 +1,194 @@
+using System.Collections.Generic;
+using System.Linq;
+
+using Dalamud.Game.Command;
+using Dalamud.Plugin.Internal.Types;
+
+namespace Dalamud.Plugin.Internal;
+
+///
+/// Class responsible for validating a dev plugin.
+///
+internal static class PluginValidator
+{
+ private static readonly char[] LineSeparator = new[] { ' ', '\n', '\r' };
+
+ ///
+ /// Represents the severity of a validation problem.
+ ///
+ public enum ValidationSeverity
+ {
+ ///
+ /// The problem is informational.
+ ///
+ Information,
+
+ ///
+ /// The problem is a warning.
+ ///
+ Warning,
+
+ ///
+ /// The problem is fatal.
+ ///
+ Fatal,
+ }
+
+ ///
+ /// Represents a validation problem.
+ ///
+ public interface IValidationProblem
+ {
+ ///
+ /// Gets the severity of the validation.
+ ///
+ public ValidationSeverity Severity { get; }
+
+ ///
+ /// Compute the localized description of the problem.
+ ///
+ /// Localized string to be shown to the developer.
+ public string GetLocalizedDescription();
+ }
+
+ ///
+ /// Check for problems in a plugin.
+ ///
+ /// The plugin to validate.
+ /// An list of problems.
+ /// Thrown when the plugin is not loaded. A plugin must be loaded to validate it.
+ public static IReadOnlyList CheckForProblems(LocalDevPlugin plugin)
+ {
+ var problems = new List();
+
+ if (!plugin.IsLoaded)
+ throw new InvalidOperationException("Plugin must be loaded to validate.");
+
+ if (!plugin.DalamudInterface!.UiBuilder.HasConfigUi)
+ problems.Add(new NoConfigUiProblem());
+
+ if (!plugin.DalamudInterface.UiBuilder.HasMainUi)
+ problems.Add(new NoMainUiProblem());
+
+ var cmdManager = Service.Get();
+ foreach (var cmd in cmdManager.Commands.Where(x => x.Value.LoaderAssemblyName == plugin.InternalName && x.Value.ShowInHelp))
+ {
+ if (string.IsNullOrEmpty(cmd.Value.HelpMessage))
+ problems.Add(new CommandWithoutHelpTextProblem(cmd.Key));
+ }
+
+ if (plugin.Manifest.Tags == null || plugin.Manifest.Tags.Count == 0)
+ problems.Add(new NoTagsProblem());
+
+ if (string.IsNullOrEmpty(plugin.Manifest.Description) || plugin.Manifest.Description.Split(LineSeparator, StringSplitOptions.RemoveEmptyEntries).Length <= 1)
+ problems.Add(new NoDescriptionProblem());
+
+ if (string.IsNullOrEmpty(plugin.Manifest.Punchline))
+ problems.Add(new NoPunchlineProblem());
+
+ if (string.IsNullOrEmpty(plugin.Manifest.Name))
+ problems.Add(new NoNameProblem());
+
+ if (string.IsNullOrEmpty(plugin.Manifest.Author))
+ problems.Add(new NoAuthorProblem());
+
+ return problems;
+ }
+
+ ///
+ /// Representing a problem where the plugin does not have a config UI callback.
+ ///
+ public class NoConfigUiProblem : IValidationProblem
+ {
+ ///
+ public ValidationSeverity Severity => ValidationSeverity.Warning;
+
+ ///
+ public string GetLocalizedDescription() => "The plugin does not register a config UI callback. If you have a settings window or section, please consider registering UiBuilder.OpenConfigUi to open it.";
+ }
+
+ ///
+ /// Representing a problem where the plugin does not have a main UI callback.
+ ///
+ public class NoMainUiProblem : IValidationProblem
+ {
+ ///
+ public ValidationSeverity Severity => ValidationSeverity.Warning;
+
+ ///
+ public string GetLocalizedDescription() => "The plugin does not register a main UI callback. If your plugin has a window that could be considered the main entrypoint to its features, please consider registering UiBuilder.OpenMainUi to open the plugin's main window.";
+ }
+
+ ///
+ /// Representing a problem where a command does not have a help text.
+ ///
+ /// Name of the command.
+ public class CommandWithoutHelpTextProblem(string commandName) : IValidationProblem
+ {
+ ///
+ public ValidationSeverity Severity => ValidationSeverity.Fatal;
+
+ ///
+ public string GetLocalizedDescription() => $"The plugin has a command ({commandName}) without a help message. Please consider adding a help message to the command when registering it.";
+ }
+
+ ///
+ /// Representing a problem where a plugin does not have any tags in its manifest.
+ ///
+ public class NoTagsProblem : IValidationProblem
+ {
+ ///
+ public ValidationSeverity Severity => ValidationSeverity.Information;
+
+ ///
+ public string GetLocalizedDescription() => "Your plugin does not have any tags in its manifest. Please consider adding some to make it easier for users to find your plugin in the installer.";
+ }
+
+ ///
+ /// Representing a problem where a plugin does not have a description in its manifest.
+ ///
+ public class NoDescriptionProblem : IValidationProblem
+ {
+ ///
+ public ValidationSeverity Severity => ValidationSeverity.Information;
+
+ ///
+ public string GetLocalizedDescription() => "Your plugin does not have a description in its manifest, or it is very terse. Please consider adding one to give users more information about your plugin.";
+ }
+
+ ///
+ /// Representing a problem where a plugin has no punchline in its manifest.
+ ///
+ public class NoPunchlineProblem : IValidationProblem
+ {
+ ///
+ public ValidationSeverity Severity => ValidationSeverity.Information;
+
+ ///
+ public string GetLocalizedDescription() => "Your plugin does not have a punchline in its manifest. Please consider adding one to give users a quick overview of what your plugin does.";
+ }
+
+ ///
+ /// Representing a problem where a plugin has no name in its manifest.
+ ///
+ public class NoNameProblem : IValidationProblem
+ {
+ ///
+ public ValidationSeverity Severity => ValidationSeverity.Fatal;
+
+ ///
+ public string GetLocalizedDescription() => "Your plugin does not have a name in its manifest.";
+ }
+
+ ///
+ /// Representing a problem where a plugin has no author in its manifest.
+ ///
+ public class NoAuthorProblem : IValidationProblem
+ {
+ ///
+ public ValidationSeverity Severity => ValidationSeverity.Fatal;
+
+ ///
+ public string GetLocalizedDescription() => "Your plugin does not have an author in its manifest.";
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs
index eebb87aaa..7b7b4cfd0 100644
--- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs
+++ b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs
@@ -18,6 +18,16 @@ namespace Dalamud.Plugin.Internal.Profiles;
[ServiceManager.EarlyLoadedService]
internal class ProfileCommandHandler : IInternalDisposableService
{
+#pragma warning disable SA1600
+ public const string CommandEnable = "/xlenablecollection";
+ public const string CommandDisable = "/xldisablecollection";
+ public const string CommandToggle = "/xltogglecollection";
+#pragma warning restore SA1600
+
+ private static readonly string LegacyCommandEnable = CommandEnable.Replace("collection", "profile");
+ private static readonly string LegacyCommandDisable = CommandDisable.Replace("collection", "profile");
+ private static readonly string LegacyCommandToggle = CommandToggle.Replace("collection", "profile");
+
private readonly CommandManager cmd;
private readonly ProfileManager profileManager;
private readonly ChatGui chat;
@@ -40,23 +50,38 @@ internal class ProfileCommandHandler : IInternalDisposableService
this.chat = chat;
this.framework = framework;
- this.cmd.AddHandler("/xlenableprofile", new CommandInfo(this.OnEnableProfile)
+ this.cmd.AddHandler(CommandEnable, new CommandInfo(this.OnEnableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""),
ShowInHelp = true,
});
- this.cmd.AddHandler("/xldisableprofile", new CommandInfo(this.OnDisableProfile)
+ this.cmd.AddHandler(CommandDisable, new CommandInfo(this.OnDisableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""),
ShowInHelp = true,
});
- this.cmd.AddHandler("/xltoggleprofile", new CommandInfo(this.OnToggleProfile)
+ this.cmd.AddHandler(CommandToggle, new CommandInfo(this.OnToggleProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
ShowInHelp = true,
});
+
+ this.cmd.AddHandler(LegacyCommandEnable, new CommandInfo(this.OnEnableProfile)
+ {
+ ShowInHelp = false,
+ });
+
+ this.cmd.AddHandler(LegacyCommandDisable, new CommandInfo(this.OnDisableProfile)
+ {
+ ShowInHelp = true,
+ });
+
+ this.cmd.AddHandler(LegacyCommandToggle, new CommandInfo(this.OnToggleProfile)
+ {
+ ShowInHelp = true,
+ });
this.framework.Update += this.FrameworkOnUpdate;
}
@@ -71,9 +96,12 @@ internal class ProfileCommandHandler : IInternalDisposableService
///
void IInternalDisposableService.DisposeService()
{
- this.cmd.RemoveHandler("/xlenablecollection");
- this.cmd.RemoveHandler("/xldisablecollection");
- this.cmd.RemoveHandler("/xltogglecollection");
+ this.cmd.RemoveHandler(CommandEnable);
+ this.cmd.RemoveHandler(CommandDisable);
+ this.cmd.RemoveHandler(CommandToggle);
+ this.cmd.RemoveHandler(LegacyCommandEnable);
+ this.cmd.RemoveHandler(LegacyCommandDisable);
+ this.cmd.RemoveHandler(LegacyCommandToggle);
this.framework.Update += this.FrameworkOnUpdate;
}
diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
index 1f9f503e0..9f7c761de 100644
--- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
+++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
@@ -99,6 +100,11 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
/// Gets an ID uniquely identifying this specific instance of a devPlugin.
///
public Guid DevImposedWorkingPluginId => this.devSettings.WorkingPluginId;
+
+ ///
+ /// Gets a list of validation problems that have been dismissed by the user.
+ ///
+ public List DismissedValidationProblems => this.devSettings.DismissedValidationProblems;
///
public new void Dispose()
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index 4bfbbe1c9..76f15950d 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit 4bfbbe1c9eba5e0f70aa7d119ef6816bd9925988
+Subproject commit 76f15950d210d5196d942cc8439c2e48bfffd0ee
diff --git a/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj b/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj
new file mode 100644
index 000000000..5701e706f
--- /dev/null
+++ b/tools/Dalamud.LocExporter/Dalamud.LocExporter.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net8.0-windows
+ x64
+ x64;AnyCPU
+ 12.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/Dalamud.LocExporter/Program.cs b/tools/Dalamud.LocExporter/Program.cs
new file mode 100644
index 000000000..f9fe8f6aa
--- /dev/null
+++ b/tools/Dalamud.LocExporter/Program.cs
@@ -0,0 +1,10 @@
+// See https://aka.ms/new-console-template for more information
+
+using CheapLoc;
+
+Console.WriteLine("=> Starting loc export...");
+
+var dalamud = typeof(Dalamud.Localization).Assembly;
+Loc.ExportLocalizableForAssembly(dalamud, true);
+
+Console.WriteLine("=> Finished loc export!");