From 127e1c36b289ae3f61f470e2699859f1536ab7cc Mon Sep 17 00:00:00 2001 From: Fabricio W Date: Wed, 22 Oct 2025 15:47:27 -0300 Subject: [PATCH] Implement rudimentary regex voidlist support --- .../Configuration/VisibilityConfiguration.cs | 15 +- Visibility/Localization.cs | 6 + Visibility/Utils/FrameworkHandler.cs | 11 +- Visibility/Utils/ImGuiElements.cs | 22 +- Visibility/Utils/VoidListManager.cs | 59 +++++ Visibility/Void/VoidPattern.cs | 24 ++ Visibility/Windows/Configuration.cs | 19 +- Visibility/Windows/VoidPatternList.cs | 213 ++++++++++++++++++ 8 files changed, 353 insertions(+), 16 deletions(-) create mode 100644 Visibility/Void/VoidPattern.cs create mode 100644 Visibility/Windows/VoidPatternList.cs diff --git a/Visibility/Configuration/VisibilityConfiguration.cs b/Visibility/Configuration/VisibilityConfiguration.cs index 06be7f1..82e9868 100644 --- a/Visibility/Configuration/VisibilityConfiguration.cs +++ b/Visibility/Configuration/VisibilityConfiguration.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Dalamud.Configuration; @@ -8,7 +9,6 @@ using Lumina.Text.ReadOnly; using Visibility.Handlers; -using Visibility.Utils; using Visibility.Void; namespace Visibility.Configuration; @@ -29,6 +29,8 @@ public partial class VisibilityConfiguration: IPluginConfiguration public List Whitelist { get; } = []; + public List VoidPatterns { get; } = []; + [NonSerialized] public Dictionary VoidDictionary = null!; [NonSerialized] public Dictionary WhitelistDictionary = null!; @@ -49,7 +51,8 @@ public partial class VisibilityConfiguration: IPluginConfiguration public void Init(ushort territoryType) { this.VoidDictionary = this.VoidList.Where(x => x.Id != 0).DistinctBy(x => x.Id).ToDictionary(x => x.Id, x => x); - this.WhitelistDictionary = this.Whitelist.Where(x => x.Id != 0).DistinctBy(x => x.Id).ToDictionary(x => x.Id, x => x); + this.WhitelistDictionary = + this.Whitelist.Where(x => x.Id != 0).DistinctBy(x => x.Id).ToDictionary(x => x.Id, x => x); this.SettingsHandler = new SettingsHandler(this); IEnumerable<(ushort, ReadOnlySeString)> valueTuples = Service.DataManager.GetExcelSheet() @@ -68,7 +71,8 @@ public void Init(ushort territoryType) } // Allowed territory intended use IDs - private static readonly HashSet allowedTerritoryIntendedUses = [ + private static readonly HashSet allowedTerritoryIntendedUses = + [ 0, // Hub Cities 1, // Overworld 13, // Residential Area @@ -140,6 +144,7 @@ private void HandleVersionChanges() } public void Save() => Service.PluginInterface.SavePluginConfig(this); - [System.Text.RegularExpressions.GeneratedRegex("")] - private static partial System.Text.RegularExpressions.Regex ItalicRegex(); + + [GeneratedRegex("")] + private static partial Regex ItalicRegex(); } diff --git a/Visibility/Localization.cs b/Visibility/Localization.cs index 12d1dea..c2b18de 100644 --- a/Visibility/Localization.cs +++ b/Visibility/Localization.cs @@ -189,6 +189,12 @@ public string ContextMenuRemove(string name) => public string AdvancedOption => this.GetString("AdvancedOption", this.CurrentLanguage); public string AdvancedOptionTooltip => this.GetString("AdvancedOptionTooltip", this.CurrentLanguage); public string ResetToCurrentArea => this.GetString("ResetToCurrentArea", this.CurrentLanguage); + + public string PatternListName => this.GetString("PatternListName", this.CurrentLanguage); + public string PatternName => this.GetString("PatternName", this.CurrentLanguage); + public string PatternDescription => this.GetString("PatternDescription", this.CurrentLanguage); + public string ActionAddPattern => this.GetString("ActionAddPattern", this.CurrentLanguage); + public string PatternOffworld => this.GetString("PatternOffworld", this.CurrentLanguage); public SeString EntryAdded(string name, SeString entryName) => FormatSeString(this.GetString("EntryAdded", this.CurrentLanguage), name, entryName); diff --git a/Visibility/Utils/FrameworkHandler.cs b/Visibility/Utils/FrameworkHandler.cs index f77615a..a3224bf 100644 --- a/Visibility/Utils/FrameworkHandler.cs +++ b/Visibility/Utils/FrameworkHandler.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects.Enums; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; @@ -14,6 +13,7 @@ using Visibility.Utils.EntityHandlers; +using BattleNpcSubKind = Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind; using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; namespace Visibility.Utils; @@ -287,6 +287,15 @@ public void ShowPlayer(uint id) this.visibilityManager.MarkObjectToShow(id); } + /// + /// Flushes the regex visibility cache and allows the visibility manager to recalculate all characters. + /// + public void ClearRegexCache() + { + this.voidListManager.ClearRegexCache(); + this.visibilityManager.ClearAll(); + } + /// /// Dispose the framework handler and show all hidden entities /// diff --git a/Visibility/Utils/ImGuiElements.cs b/Visibility/Utils/ImGuiElements.cs index 63e7137..3545ac4 100644 --- a/Visibility/Utils/ImGuiElements.cs +++ b/Visibility/Utils/ImGuiElements.cs @@ -9,7 +9,7 @@ namespace Visibility.Utils; public static class ImGuiElements { - public static bool Checkbox(bool value, string name) + public static bool Checkbox(bool value, string name, Action? callback = null) { if (!ImGui.Checkbox($"###{name}", ref value)) { @@ -19,24 +19,30 @@ public static bool Checkbox(bool value, string name) Action? onValueChanged = VisibilityPlugin.Instance.Configuration.SettingsHandler.GetAction(name); - if (onValueChanged == null) + if (onValueChanged != null) { - return false; + onValueChanged(value, false, true); + VisibilityPlugin.Instance.Configuration.Save(); + return true; } - onValueChanged(value, false, true); - VisibilityPlugin.Instance.Configuration.Save(); - return true; + if (callback != null) + { + callback(value, false, true); + return true; + } + + return false; } - public static bool CenteredCheckbox(bool value, string name) + public static bool CenteredCheckbox(bool value, string name, Action? callback = null) { ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ((ImGui.GetColumnWidth() + (2 * ImGui.GetStyle().FramePadding.X)) / 2) - (2 * ImGui.GetStyle().ItemSpacing.X) - (2 * ImGui.GetStyle().CellPadding.X)); - return Checkbox(value, name); + return Checkbox(value, name, callback); } /// diff --git a/Visibility/Utils/VoidListManager.cs b/Visibility/Utils/VoidListManager.cs index 1539a30..62d0256 100644 --- a/Visibility/Utils/VoidListManager.cs +++ b/Visibility/Utils/VoidListManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using FFXIVClientStructs.FFXIV.Client.Game.Character; @@ -16,6 +17,7 @@ public class VoidListManager private readonly Dictionary checkedWhitelistedObjectIds = new(capacity: 1000); private readonly Dictionary voidedObjectIds = new(capacity: 1000); private readonly Dictionary whitelistedObjectIds = new(capacity: 1000); + private readonly HashSet regexObjectIds = new(capacity: 1000); /// /// Check if an object is in the void list @@ -25,12 +27,25 @@ public unsafe bool CheckAndProcessVoidList(Character* characterPtr) if (this.checkedVoidedObjectIds.ContainsKey(characterPtr->GameObject.EntityId)) return this.voidedObjectIds.ContainsKey(characterPtr->GameObject.EntityId); + if (this.regexObjectIds.Contains(characterPtr->GameObject.EntityId)) + return true; + if (!VisibilityPlugin.Instance.Configuration.VoidDictionary.TryGetValue(characterPtr->ContentId, out VoidItem? voidedPlayer)) { voidedPlayer = VisibilityPlugin.Instance.Configuration.VoidList.Find(x => characterPtr->GameObject.Name.StartsWith(x.NameBytes) && x.HomeworldId == characterPtr->HomeWorld); + + if (voidedPlayer == null) + { + bool offworld = characterPtr->HomeWorld != Service.ClientState.LocalPlayer?.CurrentWorld.RowId; + if (this.IsRegexVoided(characterPtr->GameObject.NameString, offworld)) + { + this.regexObjectIds.Add(characterPtr->GameObject.EntityId); + return true; + } + } } if (voidedPlayer != null) @@ -86,6 +101,39 @@ public unsafe bool CheckAndProcessWhitelist(Character* characterPtr) return this.whitelistedObjectIds.ContainsKey(characterPtr->GameObject.EntityId); } + /// + /// Checks if a character should be voided by a registered regex pattern. + /// + /// Character name + /// Is character from a world different from the current world + private bool IsRegexVoided(string name, bool offworld) + { + return VisibilityPlugin.Instance.Configuration.VoidPatterns.Any(item => + { + if (!item.Enabled) + return false; + + if (item.Offworld && !offworld) + { + Service.PluginLog.Debug($"Regex Ignored: {item.Pattern} | {name} is offworld={offworld}"); + return false; + } + + try + { + bool match = item.Regex.IsMatch(name); + Service.PluginLog.Debug($"IsRegexVoided: {name} | {item.Pattern} | {match}"); + return match; + } + catch (Exception e) + { + Service.PluginLog.Error($"Error during regex creation {item.Pattern}:\n{e}"); + } + + return false; + }); + } + /// /// Check if an object ID is in the void list /// @@ -111,6 +159,7 @@ public void RemoveChecked(uint id) this.whitelistedObjectIds.Remove(id); this.checkedVoidedObjectIds.Remove(id); this.checkedWhitelistedObjectIds.Remove(id); + this.regexObjectIds.Remove(id); } /// @@ -122,5 +171,15 @@ public void ClearAll() this.checkedWhitelistedObjectIds.Clear(); this.voidedObjectIds.Clear(); this.whitelistedObjectIds.Clear(); + this.regexObjectIds.Clear(); + } + + /// + /// Clears the regex object list and checked list to allow for redrawing of models + /// + public void ClearRegexCache() + { + this.checkedVoidedObjectIds.Clear(); + this.regexObjectIds.Clear(); } } diff --git a/Visibility/Void/VoidPattern.cs b/Visibility/Void/VoidPattern.cs new file mode 100644 index 0000000..8386522 --- /dev/null +++ b/Visibility/Void/VoidPattern.cs @@ -0,0 +1,24 @@ +using System.Text.RegularExpressions; + +using Newtonsoft.Json; + +namespace Visibility.Void; + +[method: JsonConstructor] +public class VoidPattern( + string id, + int version, + string pattern, + string description, + bool offworld = false, + bool enabled = true) +{ + public string Id { get; } = id; + public int Version { get; set; } = version; + public string Pattern { get; } = pattern; + public string Description { get; } = description; + public bool Offworld { get; set; } = offworld; + public bool Enabled { get; set; } = enabled; + + [JsonIgnore] public readonly Regex Regex = new(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); +} diff --git a/Visibility/Windows/Configuration.cs b/Visibility/Windows/Configuration.cs index c1db6df..91ccd75 100644 --- a/Visibility/Windows/Configuration.cs +++ b/Visibility/Windows/Configuration.cs @@ -17,9 +17,11 @@ public Configuration(WindowSystem windowSystem): base($"{VisibilityPlugin.Instan { this.whitelistWindow = new VoidItemList(isWhitelist: true); this.voidItemListWindow = new VoidItemList(isWhitelist: false); + this.voidPatternWindow = new VoidPatternList(); windowSystem.AddWindow(this.whitelistWindow); windowSystem.AddWindow(this.voidItemListWindow); + windowSystem.AddWindow(this.voidPatternWindow); this.Size = new Vector2(700 * ImGui.GetIO().FontGlobalScale, 0); this.SizeCondition = ImGuiCond.Always; @@ -35,6 +37,7 @@ public Configuration(WindowSystem windowSystem): base($"{VisibilityPlugin.Instan private readonly VoidItemList whitelistWindow; private readonly VoidItemList voidItemListWindow; + private readonly VoidPatternList voidPatternWindow; public override void Draw() { @@ -290,7 +293,8 @@ public override void Draw() ImGui.GetContentRegionMax().X - ImGui.CalcTextSize(VisibilityPlugin.Instance.PluginLocalization.WhitelistName).X - ImGui.CalcTextSize(VisibilityPlugin.Instance.PluginLocalization.VoidListName).X - - (4 * ImGui.GetStyle().FramePadding.X) - + ImGui.CalcTextSize(VisibilityPlugin.Instance.PluginLocalization.PatternListName).X - + (8 * ImGui.GetStyle().FramePadding.X) - (ImGui.GetStyle().ItemSpacing.X * ImGui.GetIO().FontGlobalScale)); if (ImGui.Button(VisibilityPlugin.Instance.PluginLocalization.WhitelistName)) @@ -301,11 +305,22 @@ public override void Draw() ImGui.SameLine( ImGui.GetContentRegionMax().X - ImGui.CalcTextSize(VisibilityPlugin.Instance.PluginLocalization.VoidListName).X - - (2 * ImGui.GetStyle().FramePadding.X)); + ImGui.CalcTextSize(VisibilityPlugin.Instance.PluginLocalization.PatternListName).X - + (6 * ImGui.GetStyle().FramePadding.X)); if (ImGui.Button(VisibilityPlugin.Instance.PluginLocalization.VoidListName)) { this.voidItemListWindow.Toggle(); } + + ImGui.SameLine( + ImGui.GetContentRegionMax().X - + ImGui.CalcTextSize(VisibilityPlugin.Instance.PluginLocalization.PatternListName).X - + (2 * ImGui.GetStyle().FramePadding.X)); + + if (ImGui.Button(VisibilityPlugin.Instance.PluginLocalization.PatternListName)) + { + this.voidPatternWindow.Toggle(); + } } } diff --git a/Visibility/Windows/VoidPatternList.cs b/Visibility/Windows/VoidPatternList.cs new file mode 100644 index 0000000..b4692f0 --- /dev/null +++ b/Visibility/Windows/VoidPatternList.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Windowing; + +using Visibility.Configuration; +using Visibility.Utils; +using Visibility.Void; + +namespace Visibility.Windows; + +public class VoidPatternList: Window +{ + public VoidPatternList(): base( + $"{VisibilityPlugin.Instance.Name}: ", 0, true) + { + this.WindowName += VisibilityPlugin.Instance.PluginLocalization.PatternListName; + this.Size = new Vector2(700, 500); + this.SizeCondition = ImGuiCond.FirstUseEver; + } + + private bool sortAscending; + private IEnumerable? sortedContainer; + private Func? sortKeySelector; + + private readonly byte[][] buffer = { new byte[128], new byte[128] }; + + public override void Draw() + { + if (!ImGui.BeginTable( + "PatternVoidListTable", + 5, + ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.Sortable)) + { + return; + } + + ImGui.TableSetupColumn(VisibilityPlugin.Instance.PluginLocalization.PatternName); + ImGui.TableSetupColumn(VisibilityPlugin.Instance.PluginLocalization.PatternDescription); + ImGui.TableSetupColumn(VisibilityPlugin.Instance.PluginLocalization.PatternOffworld); + ImGui.TableSetupColumn(VisibilityPlugin.Instance.PluginLocalization.OptionEnable); + ImGui.TableSetupColumn(VisibilityPlugin.Instance.PluginLocalization.ColumnAction, ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + VoidPattern? itemToRemove = null; + + VisibilityConfiguration configuration = VisibilityPlugin.Instance.Configuration; + + List container = configuration.VoidPatterns; + this.sortedContainer ??= container; + + ImGuiTableSortSpecsPtr sortSpecs = ImGui.TableGetSortSpecs(); + + if (sortSpecs.SpecsDirty) + { + this.sortAscending = sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending; + + this.sortedContainer = sortSpecs.Specs.ColumnIndex switch + { + 0 => SortContainer( + container, + x => x.Pattern, + this.sortAscending, + out this.sortKeySelector), + 1 => SortContainer( + container, + x => x.Description, + this.sortAscending, + out this.sortKeySelector), + 2 => SortContainer( + container, + x => x.Enabled, + this.sortAscending, + out this.sortKeySelector), + _ => this.sortedContainer + }; + + sortSpecs.SpecsDirty = false; + } + + foreach (VoidPattern item in this.sortedContainer) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.Pattern); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(item.Description); + ImGui.TableNextColumn(); + ImGuiElements.CenteredCheckbox( + item.Offworld, + $"VoidPattern##{item.Id}##Offworld", + ((value, _, _) => + { + item.Offworld = value; + configuration.Save(); + // Refresh and recalculate visibility + VisibilityPlugin.Instance.FrameworkHandler.ClearRegexCache(); + })); + ImGui.TableNextColumn(); + ImGuiElements.CenteredCheckbox( + item.Enabled, + $"VoidPattern##{item.Id}##Enabled", + ((value, _, _) => + { + item.Enabled = value; + configuration.Save(); + // Refresh and recalculate visibility + VisibilityPlugin.Instance.FrameworkHandler.ClearRegexCache(); + })); + ImGui.TableNextColumn(); + + if (ImGui.Button( + $"{VisibilityPlugin.Instance.PluginLocalization.OptionRemovePlayer}##{item.Id}")) + { + itemToRemove = item; + } + + ImGui.TableNextRow(); + } + + if (itemToRemove != null) + { + container.Remove(itemToRemove); + configuration.Save(); + + if (this.sortKeySelector != null) + { + this.sortedContainer = SortContainer( + container, + this.sortKeySelector, + this.sortAscending, + out this.sortKeySelector); + } + + // Refresh and recalculate visibility + VisibilityPlugin.Instance.FrameworkHandler.ClearRegexCache(); + } + + bool enabled = true; + bool offworld = true; + + ImGui.TableNextColumn(); + ImGui.InputText( + "###voidPattern", + this.buffer[0]); + ImGui.TableNextColumn(); + ImGui.InputText( + "###voidPatternReason", + this.buffer[1]); + ImGui.TableNextColumn(); + ImGuiElements.CenteredCheckbox( + offworld, + "###voidPatternOffworld"); + ImGui.TableNextColumn(); + ImGuiElements.CenteredCheckbox( + enabled, + "###voidPatternEnabled"); + ImGui.TableNextColumn(); + + if (ImGui.Button(VisibilityPlugin.Instance.PluginLocalization.ActionAddPattern)) + { + VoidPattern pattern; + try + { + pattern = new( + Guid.NewGuid().ToString(), + 0, + this.buffer[0].ByteToString(), + this.buffer[1].ByteToString(), + enabled); + } + catch (Exception ex) + { + Service.PluginLog.Error($"Error registering pattern {this.buffer[0].ByteToString()}:\n{ex}"); + return; + } + + configuration.VoidPatterns.Add(pattern); + configuration.Save(); + + VisibilityPlugin.Instance.FrameworkHandler.ClearRegexCache(); + + foreach (byte[] item in this.buffer) + { + Array.Clear(item, 0, item.Length); + } + + if (this.sortKeySelector != null) + { + this.sortedContainer = SortContainer( + container, + this.sortKeySelector, + this.sortAscending, + out this.sortKeySelector); + } + } + + ImGui.EndTable(); + } + + private static IEnumerable SortContainer( + IEnumerable container, + Func keySelector, + bool isAscending, + out Func keySelectorOut) + { + keySelectorOut = keySelector; + return isAscending ? container.OrderBy(keySelector) : container.OrderByDescending(keySelector); + } +}