using System.Collections; using ChatTwo.Code; using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; using ChatTwo.Util; using Dalamud; using Dalamud.Configuration; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.FontIdentifier; using Dalamud.Bindings.ImGui; namespace ChatTwo; [Serializable] public class ConfigKeyBind { public ModifierFlag Modifier; public VirtualKey Key; public override string ToString() { var modString = ""; if (Modifier.HasFlag(ModifierFlag.Ctrl)) modString += Language.Keybind_Modifier_Ctrl + " + "; if (Modifier.HasFlag(ModifierFlag.Shift)) modString += Language.Keybind_Modifier_Shift + " + "; if (Modifier.HasFlag(ModifierFlag.Alt)) modString += Language.Keybind_Modifier_Alt + " + "; return modString+Key.GetFancyName(); } } [Serializable] public class Configuration : IPluginConfiguration { private const int LatestVersion = 10; public int Version { get; set; } = LatestVersion; // Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default). // Master-switch defaults to true; set false to restore upstream behavior. public bool PrivacyFilterEnabled = true; // Empty set means the migration has not run yet — see Plugin.cs v6→v7. public HashSet PrivacyPersistChannels = []; // Failsafe for ChatTypes added by future FFXIV patches we don't know about. public bool PrivacyPersistUnknownChannels; public bool IsAllowedForStorage(ChatType type) { if (!PrivacyFilterEnabled) return true; if (PrivacyPersistChannels.Contains(type)) return true; return PrivacyPersistUnknownChannels; } // Hellion Chat — Message retention (GDPR data minimization, time axis). // Master switch defaults to false; the plugin will not delete history // until the user explicitly opts in. public bool RetentionEnabled; public int RetentionDefaultDays = 30; public Dictionary RetentionPerChannelDays = []; public DateTimeOffset RetentionLastRunAt = DateTimeOffset.MinValue; // Hellion Chat first-run wizard — opens once on a fresh install. Existing // ChatTwo users skip it because the v6→v7 migration sets the flag. public bool FirstRunCompleted; // Hellion Chat global ImGui theme — applied to every plugin window in // Plugin.Draw. Default ON; users who prefer the upstream Dalamud look // can flip this off in the Privacy tab. public bool HellionThemeEnabled = true; // Window background opacity, 0.5–1.0. Lower values make the plugin // panes more glass-like so the game shines through. Default 0.5 // matches the maintainer's daily-driver preference; users who want // a less translucent look bump it up in Aussehen → Theme. public float HellionThemeWindowOpacity = 0.5f; // Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font // instead of whatever GlobalFontV2.FontId points at. Default ON so a // fresh install gets the Hellion typography out-of-the-box; flip OFF // to fall back to the user's chosen system or Dalamud font. public bool UseHellionFont = true; // Hellion Chat — Auto-Tell-Tabs. When enabled, an incoming or outgoing // /tell spawns a session-only tab dedicated to that conversation // partner. See spec: Hellion Chat Auto-Tell-Tabs Spec (Obsidian). public bool EnableAutoTellTabs = true; // Hard cap on simultaneously open auto tell tabs. Range enforced by the // settings slider (1–50). LRU drop favors greeted tabs first. public int AutoTellTabsLimit = 15; // When true the sidebar shows only a thin separator before the temp // tabs; when false a section header "Active Tells (n)" is rendered. public bool AutoTellTabsCompactDisplay; // Number of prior tells to preload from the message store when an // auto tell tab is spawned. Range 0–100; 0 disables preload. public int AutoTellTabsHistoryPreload = 20; // Show the greeter "marked-as-greeted" toggle button next to each // temp tab and dim the tab name when set. Off by default because the // workflow is specific to club-greeter use cases — most users just // want the auto tabs themselves without the extra UI affordance. public bool AutoTellTabsShowGreetedToggle; public int GetRetentionDays(ChatType type) { if (RetentionPerChannelDays.TryGetValue(type, out var userOverride)) return userOverride; if (Privacy.PrivacyDefaults.DefaultRetentionDays.TryGetValue(type, out var specDefault)) return specDefault; return RetentionDefaultDays; } public bool HideChat = true; public bool HideDuringCutscenes = true; public bool HideWhenNotLoggedIn = true; public bool HideWhenUiHidden = true; public bool HideInLoadingScreens; public bool HideInBattle; public bool HideWhenInactive; public int InactivityHideTimeout = 10; public bool InactivityHideActiveDuringBattle = true; [Obsolete("Use InactivityHideChannelsV2 instead")] public Dictionary InactivityHideChannels = []; public Dictionary InactivityHideChannelsV2 = []; public bool InactivityHideExtraChatAll = true; public HashSet InactivityHideExtraChatChannels = []; public bool ShowHideButton = true; public bool NativeItemTooltips = true; public bool PrettierTimestamps = true; public bool MoreCompactPretty; public bool HideSameTimestamps; public bool ShowNoviceNetwork; // Hellion Chat — vertical sidebar tab layout reads better than the // horizontal tab strip in the company of Auto-Tell-Tabs (a club // greeter typically tracks 5–15 simultaneous conversations). Bestand // users keep their saved value untouched — only fresh installs pick // up the new default. public bool SidebarTabView = true; public bool PrintChangelog = true; public bool OnlyPreviewIf; public int PreviewMinimum = 1; public PreviewPosition PreviewPosition = PreviewPosition.Inside; public CommandHelpSide CommandHelpSide = CommandHelpSide.None; public KeybindMode KeybindMode = KeybindMode.Strict; public LanguageOverride LanguageOverride = LanguageOverride.None; public bool CanMove = true; public bool CanResize = true; public bool ShowTitleBar = true; public bool ShowPopOutTitleBar = true; public bool DatabaseBattleMessages; public bool LoadPreviousSession; public bool FilterIncludePreviousSessions; public bool SortAutoTranslate; public bool CollapseDuplicateMessages; public bool CollapseKeepUniqueLinks; public bool PlaySounds = true; public bool KeepInputFocus = true; public int MaxLinesToRender = 5_000; // 1-10000 // Default ON to match a German / European 24h locale. The // ChatLogWindow.cs format-flip in v0.5.1 honours this strictly via // CultureInfo.InvariantCulture so the result is consistent across // host locales. public bool Use24HourClock = true; public bool ShowEmotes = true; public HashSet BlockedEmotes = []; public bool FontsEnabled = true; public ExtraGlyphRanges ExtraGlyphRanges = 0; public float FontSizeV2 = 12.75f; public float SymbolsFontSizeV2 = 12.75f; public SingleFontSpec GlobalFontV2 = new() { // dalamud only ships KR as regular, which chat2 used previously for global fonts FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f, }; public SingleFontSpec JapaneseFontV2 = new() { FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkMedium), SizePt = 12.75f, }; public bool ItalicEnabled; public SingleFontSpec ItalicFontV2 = new() { FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f, }; public float TooltipOffset; public float WindowAlpha = 100f; public Dictionary ChatColours = new(); public List Tabs = []; public bool OverrideStyle; public string? ChosenStyle; public ConfigKeyBind? ChatTabForward; public ConfigKeyBind? ChatTabBackward; public void UpdateFrom(Configuration other, bool backToOriginal) { if (backToOriginal) foreach (var tab in Tabs.Where(t => t.PopOut)) tab.PopOut = false; HideChat = other.HideChat; HideDuringCutscenes = other.HideDuringCutscenes; HideWhenNotLoggedIn = other.HideWhenNotLoggedIn; HideWhenUiHidden = other.HideWhenUiHidden; HideInLoadingScreens = other.HideInLoadingScreens; HideInBattle = other.HideInBattle; HideWhenInactive = other.HideWhenInactive; InactivityHideTimeout = other.InactivityHideTimeout; InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle; InactivityHideChannelsV2 = other.InactivityHideChannelsV2.ToDictionary(pair => pair.Key, pair => pair.Value); InactivityHideExtraChatAll = other.InactivityHideExtraChatAll; InactivityHideExtraChatChannels = other.InactivityHideExtraChatChannels.ToHashSet(); ShowHideButton = other.ShowHideButton; NativeItemTooltips = other.NativeItemTooltips; PrettierTimestamps = other.PrettierTimestamps; MoreCompactPretty = other.MoreCompactPretty; HideSameTimestamps = other.HideSameTimestamps; ShowNoviceNetwork = other.ShowNoviceNetwork; SidebarTabView = other.SidebarTabView; PrintChangelog = other.PrintChangelog; OnlyPreviewIf = other.OnlyPreviewIf; PreviewMinimum = other.PreviewMinimum; PreviewPosition = other.PreviewPosition; CommandHelpSide = other.CommandHelpSide; KeybindMode = other.KeybindMode; LanguageOverride = other.LanguageOverride; CanMove = other.CanMove; CanResize = other.CanResize; ShowTitleBar = other.ShowTitleBar; ShowPopOutTitleBar = other.ShowPopOutTitleBar; DatabaseBattleMessages = other.DatabaseBattleMessages; LoadPreviousSession = other.LoadPreviousSession; FilterIncludePreviousSessions = other.FilterIncludePreviousSessions; SortAutoTranslate = other.SortAutoTranslate; CollapseDuplicateMessages = other.CollapseDuplicateMessages; CollapseKeepUniqueLinks = other.CollapseKeepUniqueLinks; PlaySounds = other.PlaySounds; KeepInputFocus = other.KeepInputFocus; MaxLinesToRender = other.MaxLinesToRender; Use24HourClock = other.Use24HourClock; ShowEmotes = other.ShowEmotes; BlockedEmotes = other.BlockedEmotes; FontsEnabled = other.FontsEnabled; ItalicEnabled = other.ItalicEnabled; ExtraGlyphRanges = other.ExtraGlyphRanges; FontSizeV2 = other.FontSizeV2; GlobalFontV2 = other.GlobalFontV2; JapaneseFontV2 = other.JapaneseFontV2; ItalicFontV2 = other.ItalicFontV2; SymbolsFontSizeV2 = other.SymbolsFontSizeV2; TooltipOffset = other.TooltipOffset; WindowAlpha = other.WindowAlpha; ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); // Hellion Chat — Auto-Tell-Tabs are session-only and therefore // never present in a disk-loaded copy. Keep the live temp tabs of // *this* configuration alive across an UpdateFrom so a settings // save (or sidebar-mode toggle) does not silently destroy the // user's open tell conversations. Persistent tabs from `other` // still get the regular clone-replace treatment. var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList(); Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t => t.Clone()).ToList(); Tabs.AddRange(liveTempTabs); OverrideStyle = other.OverrideStyle; ChosenStyle = other.ChosenStyle; ChatTabForward = other.ChatTabForward; ChatTabBackward = other.ChatTabBackward; PrivacyFilterEnabled = other.PrivacyFilterEnabled; PrivacyPersistChannels = [..other.PrivacyPersistChannels]; PrivacyPersistUnknownChannels = other.PrivacyPersistUnknownChannels; RetentionEnabled = other.RetentionEnabled; RetentionDefaultDays = other.RetentionDefaultDays; RetentionPerChannelDays = other.RetentionPerChannelDays.ToDictionary(p => p.Key, p => p.Value); RetentionLastRunAt = other.RetentionLastRunAt; FirstRunCompleted = other.FirstRunCompleted; HellionThemeEnabled = other.HellionThemeEnabled; HellionThemeWindowOpacity = other.HellionThemeWindowOpacity; UseHellionFont = other.UseHellionFont; EnableAutoTellTabs = other.EnableAutoTellTabs; AutoTellTabsLimit = other.AutoTellTabsLimit; AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay; AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload; AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle; } } [Serializable] public enum UnreadMode { All, Unseen, None, } public static class UnreadModeExt { internal static string Name(this UnreadMode mode) => mode switch { UnreadMode.All => Language.UnreadMode_All, UnreadMode.Unseen => Language.UnreadMode_Unseen, UnreadMode.None => Language.UnreadMode_None, _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), }; internal static string? Tooltip(this UnreadMode mode) => mode switch { UnreadMode.All => Language.UnreadMode_All_Tooltip, UnreadMode.Unseen => Language.UnreadMode_Unseen_Tooltip, UnreadMode.None => Language.UnreadMode_None_Tooltip, _ => null, }; } [Serializable] public class Tab { public string Name = Language.Tab_DefaultName; [Obsolete("Removed in favor of SelectedChannels")] public Dictionary ChatCodes = new(); public Dictionary SelectedChannels = new(); public bool ExtraChatAll; public HashSet ExtraChatChannels = []; public UnreadMode UnreadMode = UnreadMode.Unseen; public bool UnhideOnActivity; public bool DisplayTimestamp = true; public InputChannel? Channel; public bool PopOut; public bool IndependentOpacity; public float Opacity = 100f; public bool InputDisabled; public bool CanMove = true; public bool CanResize = true; public bool IndependentHide; public bool HideDuringCutscenes = true; public bool HideWhenNotLoggedIn = true; public bool HideWhenUiHidden = true; public bool HideInLoadingScreens; public bool HideInBattle; public bool HideWhenInactive; public bool IsTempTab; public bool AllSenderMessages; public TellTarget TellTarget = TellTarget.Empty(); [NonSerialized] public uint Unread; [NonSerialized] public uint LastSendUnread; [NonSerialized] public long LastActivity; [NonSerialized] public MessageList Messages = new(); [NonSerialized] public UsedChannel CurrentChannel = new(); [NonSerialized] public Guid Identifier = Guid.NewGuid(); // Hellion Chat — Auto-Tell-Tabs greeted flag. Toggled manually from the // sidebar to mark a tell partner as already greeted in the current // session. NonSerialized because the temp tab itself is session-only. [NonSerialized] public bool IsGreeted; public bool Matches(Message message) { if (!message.Matches(SelectedChannels, ExtraChatAll, ExtraChatChannels)) { return false; } // Auto-tell temp tabs are bound to a single conversation partner; // every other tell that matches the channel filter must NOT land // here, otherwise all temp tabs would mirror "Tell Exclusive". if (IsTempTab && TellTarget?.IsSet() == true) { return ChunkUtil.MatchesSender(message, TellTarget.Name, TellTarget.World); } return true; } public void AddMessage(Message message, bool unread = true) { Messages.AddPrune(message, MessageManager.MessageDisplayLimit); if (!unread) return; Unread += 1; if (message.Matches(Plugin.Config.InactivityHideChannelsV2, Plugin.Config.InactivityHideExtraChatAll, Plugin.Config.InactivityHideExtraChatChannels)) LastActivity = Environment.TickCount64; } public void Clear() => Messages.Clear(); public Tab Clone() { return new Tab { Name = Name, SelectedChannels = SelectedChannels.ToDictionary(pair => pair.Key, pair => pair.Value), ExtraChatAll = ExtraChatAll, ExtraChatChannels = ExtraChatChannels.ToHashSet(), UnreadMode = UnreadMode, UnhideOnActivity = UnhideOnActivity, Unread = Unread, LastActivity = LastActivity, DisplayTimestamp = DisplayTimestamp, Channel = Channel, PopOut = PopOut, IndependentOpacity = IndependentOpacity, Opacity = Opacity, Identifier = Identifier, InputDisabled = InputDisabled, CurrentChannel = CurrentChannel, CanMove = CanMove, CanResize = CanResize, IndependentHide = IndependentHide, HideDuringCutscenes = HideDuringCutscenes, HideWhenNotLoggedIn = HideWhenNotLoggedIn, HideWhenUiHidden = HideWhenUiHidden, HideInLoadingScreens = HideInLoadingScreens, HideInBattle = HideInBattle, HideWhenInactive = HideWhenInactive, IsTempTab = IsTempTab, AllSenderMessages = AllSenderMessages, TellTarget = TellTarget.From(TellTarget), IsGreeted = IsGreeted, }; } /// /// MessageList provides an ordered list of messages with duplicate ID /// tracking, sorting and mutex protection. /// public class MessageList { private readonly SemaphoreSlim LockSlim = new(1, 1); private readonly List Messages; private readonly HashSet TrackedMessageIds; public MessageList() { Messages = []; TrackedMessageIds = []; } public MessageList(int initialCapacity) { Messages = new List(initialCapacity); TrackedMessageIds = new HashSet(initialCapacity); } public void AddPrune(Message message, int max) { LockSlim.Wait(-1); try { AddLocked(message); PruneMaxLocked(max); } finally { LockSlim.Release(); } } public void AddSortPrune(IEnumerable messages, int max) { LockSlim.Wait(-1); try { foreach (var message in messages) AddLocked(message); SortLocked(); PruneMaxLocked(max); } finally { LockSlim.Release(); } } private void AddLocked(Message message) { if (TrackedMessageIds.Contains(message.Id)) return; Messages.Add(message); TrackedMessageIds.Add(message.Id); } public void Clear() { LockSlim.Wait(-1); try { Messages.Clear(); TrackedMessageIds.Clear(); } finally { LockSlim.Release(); } } private void SortLocked() { Messages.Sort((a, b) => a.Date.CompareTo(b.Date)); } private void PruneMaxLocked(int max) { while (Messages.Count > max) { TrackedMessageIds.Remove(Messages[0].Id); Messages.RemoveAt(0); } } /// /// Returns an array copy of the message list for usage outside of main thread /// public async Task GetCopy(int millisecondsTimeout = -1) { await LockSlim.WaitAsync(millisecondsTimeout); try { return Messages.ToArray(); } finally { LockSlim.Release(); } } /// /// GetReadOnly returns a read-only list of messages while holding a /// reader lock. The list should be used with a using statement. /// public RLockedMessageList GetReadOnly(int millisecondsTimeout = -1) { LockSlim.Wait(millisecondsTimeout); return new RLockedMessageList(LockSlim, Messages); } public class RLockedMessageList(SemaphoreSlim lockSlim, List messages) : IReadOnlyList, IDisposable { public IEnumerator GetEnumerator() { return messages.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public int Count => messages.Count; public Message this[int index] => messages[index]; public void Dispose() { lockSlim.Release(); } } } } public class UsedChannel { public InputChannel Channel = InputChannel.Invalid; public List Name = []; public TellTarget? TellTarget; public bool UseTempChannel; public InputChannel TempChannel = InputChannel.Invalid; public TellTarget? TempTellTarget; public void ResetTempChannel() { UseTempChannel = false; TempTellTarget = null; TempChannel = InputChannel.Invalid; } public void SetChannel(InputChannel channel) { Channel = channel; } } [Serializable] public enum PreviewPosition { None, Inside, Top, Bottom, Tooltip, } public static class PreviewPositionExt { public static string Name(this PreviewPosition position) => position switch { PreviewPosition.None => Language.Options_Preview_None, PreviewPosition.Inside => Language.Options_Preview_Inside, PreviewPosition.Top => Language.Options_Preview_Top, PreviewPosition.Bottom => Language.Options_Preview_Bottom, PreviewPosition.Tooltip => Language.Options_Preview_Tooltip, _ => throw new ArgumentOutOfRangeException(nameof(position), position, null), }; } [Serializable] public enum CommandHelpSide { None, Left, Right, } public static class CommandHelpSideExt { public static string Name(this CommandHelpSide side) => side switch { CommandHelpSide.None => Language.CommandHelpSide_None, CommandHelpSide.Left => Language.CommandHelpSide_Left, CommandHelpSide.Right => Language.CommandHelpSide_Right, _ => throw new ArgumentOutOfRangeException(nameof(side), side, null), }; } [Serializable] public enum KeybindMode { Flexible, Strict, } public static class KeybindModeExt { public static string Name(this KeybindMode mode) => mode switch { KeybindMode.Flexible => Language.KeybindMode_Flexible_Name, KeybindMode.Strict => Language.KeybindMode_Strict_Name, _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), }; public static string? Tooltip(this KeybindMode mode) => mode switch { KeybindMode.Flexible => Language.KeybindMode_Flexible_Tooltip, KeybindMode.Strict => Language.KeybindMode_Strict_Tooltip, _ => null, }; } [Serializable] public enum LanguageOverride { None, ChineseSimplified, ChineseTraditional, Dutch, English, French, German, Greek, // Italian, Japanese, // Korean, // Norwegian, PortugueseBrazil, Romanian, Russian, Spanish, Swedish, } public static class LanguageOverrideExt { public static string Name(this LanguageOverride mode) => mode switch { LanguageOverride.None => Language.LanguageOverride_None, LanguageOverride.ChineseSimplified => "简体中文", LanguageOverride.ChineseTraditional => "繁體中文", LanguageOverride.Dutch => "Nederlands", LanguageOverride.English => "English", LanguageOverride.French => "Français", LanguageOverride.German => "Deutsch", LanguageOverride.Greek => "Ελληνικά", // LanguageOverride.Italian => "Italiano", LanguageOverride.Japanese => "日本語", // LanguageOverride.Korean => "한국어 (Korean)", // LanguageOverride.Norwegian => "Norsk", LanguageOverride.PortugueseBrazil => "Português do Brasil", LanguageOverride.Romanian => "Română", LanguageOverride.Russian => "Русский", LanguageOverride.Spanish => "Español", LanguageOverride.Swedish => "Svenska", _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), }; public static string Code(this LanguageOverride mode) => mode switch { LanguageOverride.None => "", LanguageOverride.ChineseSimplified => "zh-hans", LanguageOverride.ChineseTraditional => "zh-hant", LanguageOverride.Dutch => "nl", LanguageOverride.English => "en", LanguageOverride.French => "fr", LanguageOverride.German => "de", LanguageOverride.Greek => "el", // LanguageOverride.Italian => "it", LanguageOverride.Japanese => "ja", // LanguageOverride.Korean => "ko", // LanguageOverride.Norwegian => "no", LanguageOverride.PortugueseBrazil => "pt-br", LanguageOverride.Romanian => "ro", LanguageOverride.Russian => "ru", LanguageOverride.Spanish => "es", LanguageOverride.Swedish => "sv", _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), }; } [Serializable] [Flags] public enum ExtraGlyphRanges { ChineseFull = 1 << 0, ChineseSimplifiedCommon = 1 << 1, Cyrillic = 1 << 2, Japanese = 1 << 3, Korean = 1 << 4, Thai = 1 << 5, Vietnamese = 1 << 6, } public static class ExtraGlyphRangesExt { public static string Name(this ExtraGlyphRanges ranges) => ranges switch { ExtraGlyphRanges.ChineseFull => Language.ExtraGlyphRanges_ChineseFull_Name, ExtraGlyphRanges.ChineseSimplifiedCommon => Language.ExtraGlyphRanges_ChineseSimplifiedCommon_Name, ExtraGlyphRanges.Cyrillic => Language.ExtraGlyphRanges_Cyrillic_Name, ExtraGlyphRanges.Japanese => Language.ExtraGlyphRanges_Japanese_Name, ExtraGlyphRanges.Korean => Language.ExtraGlyphRanges_Korean_Name, ExtraGlyphRanges.Thai => Language.ExtraGlyphRanges_Thai_Name, ExtraGlyphRanges.Vietnamese => Language.ExtraGlyphRanges_Vietnamese_Name, _ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null), }; public static unsafe nint Range(this ExtraGlyphRanges ranges) => ranges switch { ExtraGlyphRanges.ChineseFull => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseFull(), ExtraGlyphRanges.ChineseSimplifiedCommon => (nint)ImGui.GetIO().Fonts.GetGlyphRangesChineseSimplifiedCommon(), ExtraGlyphRanges.Cyrillic => (nint)ImGui.GetIO().Fonts.GetGlyphRangesCyrillic(), ExtraGlyphRanges.Japanese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesJapanese(), ExtraGlyphRanges.Korean => (nint)ImGui.GetIO().Fonts.GetGlyphRangesKorean(), ExtraGlyphRanges.Thai => (nint)ImGui.GetIO().Fonts.GetGlyphRangesThai(), ExtraGlyphRanges.Vietnamese => (nint)ImGui.GetIO().Fonts.GetGlyphRangesVietnamese(), _ => throw new ArgumentOutOfRangeException(nameof(ranges), ranges, null), }; }