Merge remote-tracking branch 'origin/master' into new_im_hooks-rollup

This commit is contained in:
github-actions[bot] 2024-04-02 16:27:26 +00:00
commit 6764cac648
23 changed files with 1148 additions and 67 deletions

View file

@ -40,6 +40,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "Dala
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.LocExporter", "tools\Dalamud.LocExporter\Dalamud.LocExporter.csproj", "{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace Dalamud.Configuration.Internal; 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. /// Gets or sets an ID uniquely identifying this specific instance of a devPlugin.
/// </summary> /// </summary>
public Guid WorkingPluginId { get; set; } = Guid.Empty; public Guid WorkingPluginId { get; set; } = Guid.Empty;
/// <summary>
/// Gets or sets a list of validation problems that have been dismissed by the user.
/// </summary>
public List<string> DismissedValidationProblems { get; set; } = new();
} }

View file

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<DalamudVersion>9.1.0.2</DalamudVersion> <DalamudVersion>9.1.0.5</DalamudVersion>
<Description>XIV Launcher addon framework</Description> <Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version> <Version>$(DalamudVersion)</Version>

View file

@ -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<PluginImageCache>.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;
}
}
}

View file

@ -56,7 +56,7 @@ internal static class NotificationConstants
public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f;
/// <summary>Default duration of the notification.</summary> /// <summary>Default duration of the notification.</summary>
public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3); public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(7);
/// <summary>Duration of show animation.</summary> /// <summary>Duration of show animation.</summary>
public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300);

View file

@ -11,6 +11,7 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.Unicode; using System.Text.Unicode;
using Dalamud.Game;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Hooking.WndProcHook; using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.GameFonts;
@ -110,6 +111,7 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
/// <summary>Undo range for modifying the buffer while composition is in progress.</summary> /// <summary>Undo range for modifying the buffer while composition is in progress.</summary>
private (int Start, int End, int Cursor)? temporaryUndoSelection; private (int Start, int End, int Cursor)? temporaryUndoSelection;
private bool hadWantTextInput;
private bool updateInputLanguage = true; private bool updateInputLanguage = true;
private bool updateImeStatusAgain; private bool updateImeStatusAgain;
@ -262,20 +264,37 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
if (!ImGuiHelpers.IsImGuiInitialized) if (!ImGuiHelpers.IsImGuiInitialized)
{ {
this.updateInputLanguage = true; this.updateInputLanguage = true;
this.temporaryUndoSelection = null;
return; return;
} }
// Are we not the target of text input? // Are we not the target of text input?
if (!ImGui.GetIO().WantTextInput) 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.updateInputLanguage = true;
this.temporaryUndoSelection = null;
return; return;
} }
this.hadWantTextInput = true;
var hImc = ImmGetContext(args.Hwnd); var hImc = ImmGetContext(args.Hwnd);
if (hImc == nint.Zero) if (hImc == nint.Zero)
{ {
this.updateInputLanguage = true; this.updateInputLanguage = true;
this.temporaryUndoSelection = null;
return; return;
} }
@ -338,9 +357,11 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
this.updateInputLanguage = false; 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) if (this.updateImeStatusAgain)
{ {
this.ReplaceCompositionString(hImc, false);
this.UpdateCandidates(hImc); this.UpdateCandidates(hImc);
this.updateImeStatusAgain = false; this.updateImeStatusAgain = false;
} }
@ -410,7 +431,8 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
or VK.VK_RIGHT or VK.VK_RIGHT
or VK.VK_DOWN or VK.VK_DOWN
or VK.VK_RETURN: 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); this.ClearState(hImc);
args.WParam = VK.VK_PROCESSKEY; args.WParam = VK.VK_PROCESSKEY;
@ -428,7 +450,15 @@ internal sealed unsafe class DalamudIme : IInternalDisposableService
case WM.WM_RBUTTONDOWN: case WM.WM_RBUTTONDOWN:
case WM.WM_MBUTTONDOWN: case WM.WM_MBUTTONDOWN:
case WM.WM_XBUTTONDOWN: 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; break;
} }
} }

View file

@ -943,7 +943,7 @@ internal class DalamudInterface : IInternalDisposableService
if (ImGui.MenuItem("Export localizable")) if (ImGui.MenuItem("Export localizable"))
{ {
localization.ExportLocalizable(); localization.ExportLocalizable(true);
} }
if (ImGui.BeginMenu("Load language...")) if (ImGui.BeginMenu("Load language..."))

View file

@ -191,6 +191,11 @@ internal class PluginImageCache : IInternalDisposableService
{ {
iconTexture = null; iconTexture = null;
loadedSince = null; loadedSince = null;
// Wait for the horse icon list to be there, if applicable
var fools = Service<Fools24>.Get();
if (Fools24.IsDayApplicable() && fools.IsWaitingForIconList && !fools.Failed)
return false;
if (manifest == null || manifest.InternalName == null) if (manifest == null || manifest.InternalName == null)
{ {
@ -638,6 +643,14 @@ internal class PluginImageCache : IInternalDisposableService
{ {
if (isThirdParty) if (isThirdParty)
return manifest.IconUrl; return manifest.IconUrl;
var fools = Service<Fools24>.Get();
if (Fools24.IsDayApplicable())
{
var iconLink = fools.GetHorseIconLink(manifest.InternalName);
if (iconLink != null)
return iconLink;
}
return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png"); return MainRepoDip17ImageUrl.Format(manifest.Dip17Channel!, manifest.InternalName, "icon.png");
} }

View file

@ -268,6 +268,8 @@ internal class PluginInstallerWindow : Window, IDisposable
/// <inheritdoc/> /// <inheritdoc/>
public override void OnOpen() public override void OnOpen()
{ {
Service<Fools24>.Get().NotifyInstallerWindowOpened();
var pluginManager = Service<PluginManager>.Get(); var pluginManager = Service<PluginManager>.Get();
_ = pluginManager.ReloadPluginMastersAsync(); _ = pluginManager.ReloadPluginMastersAsync();
@ -596,6 +598,7 @@ internal class PluginInstallerWindow : Window, IDisposable
using (ImRaii.Disabled(isProfileManager)) using (ImRaii.Disabled(isProfileManager))
{ {
var searchTextChanged = false; var searchTextChanged = false;
var prevSearchText = this.searchText;
ImGui.SetNextItemWidth(searchInputWidth); ImGui.SetNextItemWidth(searchInputWidth);
searchTextChanged |= ImGui.InputTextWithHint( searchTextChanged |= ImGui.InputTextWithHint(
"###XlPluginInstaller_Search", "###XlPluginInstaller_Search",
@ -615,7 +618,7 @@ internal class PluginInstallerWindow : Window, IDisposable
} }
if (searchTextChanged) if (searchTextChanged)
this.UpdateCategoriesOnSearchChange(); this.UpdateCategoriesOnSearchChange(prevSearchText);
} }
// Disable sort if changelogs or profile editor // 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 // Controls
this.DrawPluginControlButton(plugin, availablePluginUpdate); this.DrawPluginControlButton(plugin, availablePluginUpdate);
this.DrawDevPluginButtons(plugin); 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<DalamudConfiguration>.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) private void DrawDevPluginButtons(LocalPlugin localPlugin)
{ {
ImGui.SameLine(); ImGui.SameLine();
@ -3341,15 +3444,27 @@ internal class PluginInstallerWindow : Window, IDisposable
return this.updateModalTaskCompletionSource.Task; return this.updateModalTaskCompletionSource.Task;
} }
private void UpdateCategoriesOnSearchChange() private void UpdateCategoriesOnSearchChange(string? previousSearchText)
{ {
if (string.IsNullOrEmpty(this.searchText)) if (string.IsNullOrEmpty(this.searchText))
{ {
this.categoryManager.SetCategoryHighlightsForPlugins(null); this.categoryManager.SetCategoryHighlightsForPlugins(null);
// Reset here for good measure, as we're returning from a search
this.openPluginCollapsibles.Clear();
} }
else 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); this.categoryManager.SetCategoryHighlightsForPlugins(pluginsMatchingSearch);
} }
} }
@ -3357,7 +3472,7 @@ internal class PluginInstallerWindow : Window, IDisposable
private void UpdateCategoriesOnPluginsChange() private void UpdateCategoriesOnPluginsChange()
{ {
this.categoryManager.BuildCategories(this.pluginListAvailable); this.categoryManager.BuildCategories(this.pluginListAvailable);
this.UpdateCategoriesOnSearchChange(); this.UpdateCategoriesOnSearchChange(null);
} }
private void DrawFontawesomeIconOutlined(FontAwesomeIcon icon, Vector4 outline, Vector4 iconColor) 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_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/"); public static string FeedbackModal_ContactInformationDiscordUrl => Loc.Localize("ContactInformationDiscordUrl", "https://goat.place/");

View file

@ -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:"); Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:");
public static string TutorialCommandsEnable => 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 => 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 => 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 => public static string TutorialCommandsEnd =>
Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order."); Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order.");

View file

@ -182,7 +182,7 @@ internal class SettingsWindow : Window
{ {
ImGui.SetTooltip(!ImGui.IsKeyDown(ImGuiKey.ModShift) ImGui.SetTooltip(!ImGui.IsKeyDown(ImGuiKey.ModShift)
? Loc.Localize("DalamudSettingsSaveAndExit", "Save changes and close") ? Loc.Localize("DalamudSettingsSaveAndExit", "Save changes and close")
: Loc.Localize("DalamudSettingsSaveAndExit", "Save changes")); : Loc.Localize("DalamudSettingsSave", "Save changes"));
} }
} }

View file

@ -133,7 +133,7 @@ public class SettingsTabLook : SettingsTab
new SettingsEntry<bool>( new SettingsEntry<bool>(
Loc.Localize("DalamudSettingReducedMotion", "Reduce motions"), 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, c => c.ReduceMotions ?? false,
(v, c) => c.ReduceMotions = v), (v, c) => c.ReduceMotions = v),
}; };

View file

@ -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;
/// <summary>A fluent ImGui glyph range builder.</summary>
[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;
/// <summary>Clears the builder.</summary>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>A builder is in cleared state on first use.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FluentGlyphRangeBuilder Clear()
{
this.characters?.SetAll(false);
return this;
}
/// <summary>Adds a single codepoint to the builder.</summary>
/// <param name="codepoint">The codepoint to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FluentGlyphRangeBuilder With(char codepoint) => this.With((int)codepoint);
/// <inheritdoc cref="With(char)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FluentGlyphRangeBuilder With(uint codepoint) =>
codepoint <= char.MaxValue ? this.With((int)codepoint) : this;
/// <inheritdoc cref="With(char)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public FluentGlyphRangeBuilder With(int codepoint)
{
if (codepoint <= ImUnicodeCodepointMax)
this.EnsureCharacters().Set(codepoint, true);
return this;
}
/// <summary>Adds a unicode range to the builder.</summary>
/// <param name="range">The unicode range to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(UnicodeRange range) =>
this.With(range.FirstCodePoint, (range.FirstCodePoint + range.Length) - 1);
/// <summary>Adds unicode ranges to the builder.</summary>
/// <param name="range1">The 1st unicode range to add.</param>
/// <param name="range2">The 2st unicode range to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(UnicodeRange range1, UnicodeRange range2) =>
this.With(range1.FirstCodePoint, (range1.FirstCodePoint + range1.Length) - 1)
.With(range2.FirstCodePoint, (range2.FirstCodePoint + range2.Length) - 1);
/// <summary>Adds unicode ranges to the builder.</summary>
/// <param name="range1">The 1st unicode range to add.</param>
/// <param name="range2">The 2st unicode range to add.</param>
/// <param name="range3">The 3rd unicode range to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
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);
/// <summary>Adds unicode ranges to the builder.</summary>
/// <param name="range1">The 1st unicode range to add.</param>
/// <param name="range2">The 2st unicode range to add.</param>
/// <param name="range3">The 3rd unicode range to add.</param>
/// <param name="range4">The 4th unicode range to add.</param>
/// <param name="evenMoreRanges">Even more unicode ranges to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
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);
/// <summary>Adds unicode ranges to the builder.</summary>
/// <param name="ranges">Unicode ranges to add.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(IEnumerable<UnicodeRange> ranges)
{
foreach (var range in ranges)
this.With(range);
return this;
}
/// <summary>Adds a range of characters to the builder.</summary>
/// <param name="from">The first codepoint, inclusive.</param>
/// <param name="to">The last codepoint, inclusive.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>
/// <para>Unsupported codepoints will be ignored.</para>
/// <para>If <paramref name="from"/> is more than <paramref name="to"/>, then they will be swapped.</para>
/// </remarks>
public FluentGlyphRangeBuilder With(char from, char to) =>
this.With(Math.Clamp(from, int.MinValue, int.MaxValue), Math.Clamp(to, int.MinValue, int.MaxValue));
/// <inheritdoc cref="With(char,char)"/>
public FluentGlyphRangeBuilder With(uint from, uint to) =>
this.With((int)Math.Min(from, int.MaxValue), (int)Math.Min(to, int.MaxValue));
/// <inheritdoc cref="With(char,char)"/>
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;
}
/// <summary>Adds characters from a UTF-8 character sequence.</summary>
/// <param name="utf8Sequence">The sequence.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(ReadOnlySpan<byte> 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;
}
/// <summary>Adds characters from a UTF-8 character sequence.</summary>
/// <param name="utf8Sequence">The sequence.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(IEnumerable<byte> utf8Sequence)
{
Span<byte> 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;
}
/// <summary>Adds characters from a UTF-16 character sequence.</summary>
/// <param name="utf16Sequence">The sequence.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(ReadOnlySpan<char> 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;
}
/// <summary>Adds characters from a UTF-16 character sequence.</summary>
/// <param name="utf16Sequence">The sequence.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(IEnumerable<char> utf16Sequence)
{
var bits = this.EnsureCharacters();
foreach (var c in utf16Sequence)
{
if (!char.IsSurrogate(c))
bits.Set(c, true);
}
return this;
}
/// <summary>Adds characters from a string.</summary>
/// <param name="string">The string.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored.</remarks>
public FluentGlyphRangeBuilder With(string @string) => this.With(@string.AsSpan());
/// <summary>Adds glyphs that are likely to be used in the given culture to the builder.</summary>
/// <param name="cultureInfo">A culture info.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Unsupported codepoints will be ignored. Unsupported culture will do nothing.
/// Do make a PR if you need more.</remarks>
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;
}
}
/// <summary>Adds glyphs that are likely to be used in the given culture to the builder.</summary>
/// <param name="languageTag">A language tag that will be used to locate the culture info.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>See <see cref="CultureInfo.GetCultureInfo(string)"/> documentation for supported language tags.
/// </remarks>
public FluentGlyphRangeBuilder WithLanguage(string languageTag) =>
this.WithLanguage(CultureInfo.GetCultureInfo(languageTag));
/// <summary>Builds the accumulated data into an ImGui glyph range.</summary>
/// <param name="addFallbackCodepoints">Whether to add the default fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Whether to add the default ellipsis codepoints to the range.</param>
/// <returns>The built ImGui glyph ranges.</returns>
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();
}
/// <summary>Builds the accumulated data into an ImGui glyph range, exactly as specified.</summary>
/// <returns>The built ImGui glyph ranges.</returns>
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<ushort>((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;
}
/// <summary>Ensures that <see cref="characters"/> is not null, by creating one as necessary.</summary>
/// <returns>An instance of <see cref="BitArray"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private BitArray EnsureCharacters() => this.characters ??= new(ImUnicodeCodepointMax + 1);
}

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Unicode;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
@ -12,6 +13,34 @@ namespace Dalamud.Interface.ManagedFontAtlas;
/// </summary> /// </summary>
public static class FontAtlasBuildToolkitUtilities public static class FontAtlasBuildToolkitUtilities
{ {
/// <summary>Begins building a new array of <see cref="ushort"/> containing ImGui glyph ranges.</summary>
/// <param name="chars">The chars.</param>
/// <returns>A new range builder.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FluentGlyphRangeBuilder BeginGlyphRange(this IEnumerable<char> chars) =>
default(FluentGlyphRangeBuilder).With(chars);
/// <summary>Begins building a new array of <see cref="ushort"/> containing ImGui glyph ranges.</summary>
/// <param name="chars">The chars.</param>
/// <returns>A new range builder.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FluentGlyphRangeBuilder BeginGlyphRange(this ReadOnlySpan<char> chars) =>
default(FluentGlyphRangeBuilder).With(chars);
/// <summary>Begins building a new array of <see cref="ushort"/> containing ImGui glyph ranges.</summary>
/// <param name="chars">The chars.</param>
/// <returns>A new range builder.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FluentGlyphRangeBuilder BeginGlyphRange(this string chars) =>
default(FluentGlyphRangeBuilder).With(chars);
/// <summary>Begins building a new array of <see cref="ushort"/> containing ImGui glyph ranges.</summary>
/// <param name="range">The unicode range.</param>
/// <returns>A new range builder.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static FluentGlyphRangeBuilder BeginGlyphRange(this UnicodeRange range) =>
default(FluentGlyphRangeBuilder).With(range);
/// <summary> /// <summary>
/// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges. /// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges.
/// </summary> /// </summary>
@ -19,16 +48,12 @@ public static class FontAtlasBuildToolkitUtilities
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param> /// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param> /// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>The compiled range.</returns> /// <returns>The compiled range.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort[] ToGlyphRange( public static ushort[] ToGlyphRange(
this IEnumerable<char> enumerable, this IEnumerable<char> enumerable,
bool addFallbackCodepoints = true, bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true) bool addEllipsisCodepoints = true) =>
{ enumerable.BeginGlyphRange().Build(addFallbackCodepoints, addEllipsisCodepoints);
using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
foreach (var c in enumerable)
builder.AddChar(c);
return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
}
/// <summary> /// <summary>
/// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges. /// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges.
@ -37,16 +62,12 @@ public static class FontAtlasBuildToolkitUtilities
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param> /// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param> /// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>The compiled range.</returns> /// <returns>The compiled range.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort[] ToGlyphRange( public static ushort[] ToGlyphRange(
this ReadOnlySpan<char> span, this ReadOnlySpan<char> span,
bool addFallbackCodepoints = true, bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true) bool addEllipsisCodepoints = true) =>
{ span.BeginGlyphRange().Build(addFallbackCodepoints, addEllipsisCodepoints);
using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
foreach (var c in span)
builder.AddChar(c);
return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
}
/// <summary> /// <summary>
/// Compiles given string into an array of <see cref="ushort"/> containing ImGui glyph ranges. /// Compiles given string into an array of <see cref="ushort"/> containing ImGui glyph ranges.
@ -55,11 +76,12 @@ public static class FontAtlasBuildToolkitUtilities
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param> /// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param> /// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
/// <returns>The compiled range.</returns> /// <returns>The compiled range.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort[] ToGlyphRange( public static ushort[] ToGlyphRange(
this string @string, this string @string,
bool addFallbackCodepoints = true, bool addFallbackCodepoints = true,
bool addEllipsisCodepoints = true) => bool addEllipsisCodepoints = true) =>
@string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); @string.BeginGlyphRange().Build(addFallbackCodepoints, addEllipsisCodepoints);
/// <summary> /// <summary>
/// Finds the corresponding <see cref="ImFontConfigPtr"/> in /// Finds the corresponding <see cref="ImFontConfigPtr"/> in

View file

@ -1,4 +1,5 @@
using System.IO; using System.Globalization;
using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.FontIdentifier;
@ -8,6 +9,8 @@ using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.ManagedFontAtlas; namespace Dalamud.Interface.ManagedFontAtlas;
/// <summary> /// <summary>
@ -216,10 +219,35 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
/// <returns>The added font.</returns> /// <returns>The added font.</returns>
ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont); ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont);
/// <summary>Adds glyphs from the Windows default font for the given culture info into the provided font.</summary>
/// <param name="cultureInfo">The culture info.</param>
/// <param name="fontConfig">The font config. If <see cref="SafeFontConfig.MergeFont"/> is not set, then
/// <see cref="IFontAtlasBuildToolkit.Font"/> will be used as the target. If that is empty too, then it will do
/// nothing.</param>
/// <param name="weight">The font weight, in range from <c>1</c> to <c>1000</c>. <c>400</c> is regular(normal).
/// </param>
/// <param name="stretch">The font stretch, in range from <c>1</c> to <c>9</c>. <c>5</c> is medium(normal).
/// </param>
/// <param name="style">The font style, in range from <c>0</c> to <c>2</c>. <c>0</c> is normal.</param>
/// <remarks>
/// <para>May do nothing at all if <paramref name="cultureInfo"/> is unsupported by Dalamud font handler.</para>
/// <para>See
/// <a href="https://learn.microsoft.com/en-us/windows/apps/design/globalizing/loc-international-fonts">Microsoft
/// Learn</a> for the fonts.</para>
/// </remarks>
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);
/// <summary> /// <summary>
/// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.<br /> /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.<br />
/// <see cref="SafeFontConfig.GlyphRanges"/> will be ignored. /// <see cref="SafeFontConfig.GlyphRanges"/> will be ignored.
/// </summary> /// </summary>
/// <param name="fontConfig">The font config.</param> /// <param name="fontConfig">The font config. If <see cref="SafeFontConfig.MergeFont"/> is not set, then
/// <see cref="IFontAtlasBuildToolkit.Font"/> will be used as the target. If that is empty too, then it will do
/// nothing.</param>
void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig);
} }

View file

@ -1,9 +1,9 @@
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Unicode;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.FontIdentifier;
@ -17,6 +17,8 @@ using ImGuiNET;
using SharpDX.DXGI; using SharpDX.DXGI;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.ManagedFontAtlas.Internals; namespace Dalamud.Interface.ManagedFontAtlas.Internals;
/// <summary> /// <summary>
@ -433,9 +435,163 @@ internal sealed partial class FontAtlasFactory
public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) => public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) =>
this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges); this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges);
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig)
{ {
var targetFont = fontConfig.MergeFont;
if (targetFont.IsNull())
targetFont = this.Font;
if (targetFont.IsNull())
return;
var dalamudConfiguration = Service<DalamudConfiguration>.Get(); var dalamudConfiguration = Service<DalamudConfiguration>.Get();
if (dalamudConfiguration.EffectiveLanguage == "ko" if (dalamudConfiguration.EffectiveLanguage == "ko"
|| Service<DalamudIme>.GetNullable()?.EncounteredHangul is true) || Service<DalamudIme>.GetNullable()?.EncounteredHangul is true)
@ -444,41 +600,24 @@ internal sealed partial class FontAtlasFactory
DalamudAsset.NotoSansKrRegular, DalamudAsset.NotoSansKrRegular,
fontConfig with fontConfig with
{ {
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( MergeFont = targetFont,
UnicodeRanges.HangulJamo, GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("ko-kr").BuildExact(),
UnicodeRanges.HangulCompatibilityJamo,
UnicodeRanges.HangulSyllables,
UnicodeRanges.HangulJamoExtendedA,
UnicodeRanges.HangulJamoExtendedB),
}); });
} }
var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); if (Service<DalamudConfiguration>.Get().EffectiveLanguage == "tw")
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<DalamudConfiguration>.Get().EffectiveLanguage == "tw")
{ {
this.AddFontFromFile(fontPathCht, fontConfig with this.AttachWindowsDefaultFont(CultureInfo.GetCultureInfo("zh-hant"), fontConfig with
{ {
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("zh-hant").BuildExact(),
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkUnifiedIdeographsExtensionA),
}); });
} }
else if (fontPathChs != null && (Service<DalamudConfiguration>.Get().EffectiveLanguage == "zh" else if (Service<DalamudConfiguration>.Get().EffectiveLanguage == "zh"
|| Service<DalamudIme>.GetNullable()?.EncounteredHan is true)) || Service<DalamudIme>.GetNullable()?.EncounteredHan is true)
{ {
this.AddFontFromFile(fontPathChs, fontConfig with this.AttachWindowsDefaultFont(CultureInfo.GetCultureInfo("zh-hans"), fontConfig with
{ {
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( GlyphRanges = default(FluentGlyphRangeBuilder).WithLanguage("zh-hans").BuildExact(),
UnicodeRanges.CjkUnifiedIdeographs,
UnicodeRanges.CjkUnifiedIdeographsExtensionA),
}); });
} }
} }

View file

@ -347,6 +347,12 @@ public sealed class UiBuilder : IDisposable
/// </summary> /// </summary>
public IFontAtlas FontAtlas { get; } public IFontAtlas FontAtlas { get; }
/// <summary>
/// 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.
/// </summary>
public bool ShouldUseReducedMotion => Service<DalamudConfiguration>.Get().ReduceMotions ?? false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// Gets or sets a value indicating whether statistics about UI draw time should be collected.
/// </summary> /// </summary>

View file

@ -0,0 +1,194 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.Command;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Plugin.Internal;
/// <summary>
/// Class responsible for validating a dev plugin.
/// </summary>
internal static class PluginValidator
{
private static readonly char[] LineSeparator = new[] { ' ', '\n', '\r' };
/// <summary>
/// Represents the severity of a validation problem.
/// </summary>
public enum ValidationSeverity
{
/// <summary>
/// The problem is informational.
/// </summary>
Information,
/// <summary>
/// The problem is a warning.
/// </summary>
Warning,
/// <summary>
/// The problem is fatal.
/// </summary>
Fatal,
}
/// <summary>
/// Represents a validation problem.
/// </summary>
public interface IValidationProblem
{
/// <summary>
/// Gets the severity of the validation.
/// </summary>
public ValidationSeverity Severity { get; }
/// <summary>
/// Compute the localized description of the problem.
/// </summary>
/// <returns>Localized string to be shown to the developer.</returns>
public string GetLocalizedDescription();
}
/// <summary>
/// Check for problems in a plugin.
/// </summary>
/// <param name="plugin">The plugin to validate.</param>
/// <returns>An list of problems.</returns>
/// <exception cref="InvalidOperationException">Thrown when the plugin is not loaded. A plugin must be loaded to validate it.</exception>
public static IReadOnlyList<IValidationProblem> CheckForProblems(LocalDevPlugin plugin)
{
var problems = new List<IValidationProblem>();
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<CommandManager>.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;
}
/// <summary>
/// Representing a problem where the plugin does not have a config UI callback.
/// </summary>
public class NoConfigUiProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Warning;
/// <inheritdoc/>
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.";
}
/// <summary>
/// Representing a problem where the plugin does not have a main UI callback.
/// </summary>
public class NoMainUiProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Warning;
/// <inheritdoc/>
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.";
}
/// <summary>
/// Representing a problem where a command does not have a help text.
/// </summary>
/// <param name="commandName">Name of the command.</param>
public class CommandWithoutHelpTextProblem(string commandName) : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Fatal;
/// <inheritdoc/>
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.";
}
/// <summary>
/// Representing a problem where a plugin does not have any tags in its manifest.
/// </summary>
public class NoTagsProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Information;
/// <inheritdoc/>
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.";
}
/// <summary>
/// Representing a problem where a plugin does not have a description in its manifest.
/// </summary>
public class NoDescriptionProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Information;
/// <inheritdoc/>
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.";
}
/// <summary>
/// Representing a problem where a plugin has no punchline in its manifest.
/// </summary>
public class NoPunchlineProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Information;
/// <inheritdoc/>
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.";
}
/// <summary>
/// Representing a problem where a plugin has no name in its manifest.
/// </summary>
public class NoNameProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Fatal;
/// <inheritdoc/>
public string GetLocalizedDescription() => "Your plugin does not have a name in its manifest.";
}
/// <summary>
/// Representing a problem where a plugin has no author in its manifest.
/// </summary>
public class NoAuthorProblem : IValidationProblem
{
/// <inheritdoc/>
public ValidationSeverity Severity => ValidationSeverity.Fatal;
/// <inheritdoc/>
public string GetLocalizedDescription() => "Your plugin does not have an author in its manifest.";
}
}

View file

@ -18,6 +18,16 @@ namespace Dalamud.Plugin.Internal.Profiles;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal class ProfileCommandHandler : IInternalDisposableService 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 CommandManager cmd;
private readonly ProfileManager profileManager; private readonly ProfileManager profileManager;
private readonly ChatGui chat; private readonly ChatGui chat;
@ -40,23 +50,38 @@ internal class ProfileCommandHandler : IInternalDisposableService
this.chat = chat; this.chat = chat;
this.framework = framework; 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\""), HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""),
ShowInHelp = true, 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\""), HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""),
ShowInHelp = true, 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\""), HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
ShowInHelp = true, 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; this.framework.Update += this.FrameworkOnUpdate;
} }
@ -71,9 +96,12 @@ internal class ProfileCommandHandler : IInternalDisposableService
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.cmd.RemoveHandler("/xlenablecollection"); this.cmd.RemoveHandler(CommandEnable);
this.cmd.RemoveHandler("/xldisablecollection"); this.cmd.RemoveHandler(CommandDisable);
this.cmd.RemoveHandler("/xltogglecollection"); this.cmd.RemoveHandler(CommandToggle);
this.cmd.RemoveHandler(LegacyCommandEnable);
this.cmd.RemoveHandler(LegacyCommandDisable);
this.cmd.RemoveHandler(LegacyCommandToggle);
this.framework.Update += this.FrameworkOnUpdate; this.framework.Update += this.FrameworkOnUpdate;
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
@ -99,6 +100,11 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
/// Gets an ID uniquely identifying this specific instance of a devPlugin. /// Gets an ID uniquely identifying this specific instance of a devPlugin.
/// </summary> /// </summary>
public Guid DevImposedWorkingPluginId => this.devSettings.WorkingPluginId; public Guid DevImposedWorkingPluginId => this.devSettings.WorkingPluginId;
/// <summary>
/// Gets a list of validation problems that have been dismissed by the user.
/// </summary>
public List<string> DismissedValidationProblems => this.devSettings.DismissedValidationProblems;
/// <inheritdoc/> /// <inheritdoc/>
public new void Dispose() public new void Dispose()

@ -1 +1 @@
Subproject commit 4bfbbe1c9eba5e0f70aa7d119ef6816bd9925988 Subproject commit 76f15950d210d5196d942cc8439c2e48bfffd0ee

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<LangVersion>12.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Dalamud\Dalamud.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CheapLoc" Version="1.1.8" />
</ItemGroup>
</Project>

View file

@ -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!");