From b384c8614ad97b5a826515f10b16a23c873bd9e7 Mon Sep 17 00:00:00 2001 From: MTVirux Date: Sat, 22 Nov 2025 13:13:17 +0000 Subject: [PATCH] Add wildcard support for automations --- Glamourer/Automation/AutoDesignApplier.cs | 106 +++++++++++++++++- Glamourer/Automation/AutoDesignManager.cs | 102 ++++++++++++++--- .../Tabs/AutomationTab/IdentifierDrawer.cs | 51 +++++++-- 3 files changed, 226 insertions(+), 33 deletions(-) diff --git a/Glamourer/Automation/AutoDesignApplier.cs b/Glamourer/Automation/AutoDesignApplier.cs index a61a004..d4d2f82 100644 --- a/Glamourer/Automation/AutoDesignApplier.cs +++ b/Glamourer/Automation/AutoDesignApplier.cs @@ -155,6 +155,49 @@ public sealed class AutoDesignApplier : IDisposable foreach (var id in newSet.Identifiers) { + // If the stored identifier uses a wildcard in the player name, it will not directly + // be present in the ActorObjectManager dictionaries. Scan the live objects and + // apply to any matching actors instead. + if (!id.PlayerName.IsEmpty && id.PlayerName.ToString().Contains('*')) + { + var pattern = id.PlayerName.ToString(); + var regexPattern = System.Text.RegularExpressions.Regex.Escape(pattern).Replace("\\*", ".*"); + foreach (var (key, data2) in _objects) + { + if (key.Type != id.Type) + continue; + + var worldMatches = key.Type switch + { + IdentifierType.Player => key.HomeWorld == id.HomeWorld || key.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld || id.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld, + IdentifierType.Owned => key.HomeWorld == id.HomeWorld || key.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld || id.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld, + _ => true, + }; + + if (!worldMatches) + continue; + + if (!System.Text.RegularExpressions.Regex.IsMatch(key.PlayerName.ToString(), $"^{regexPattern}$", System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + continue; + + // Skip this actor if there's an exact-match automation set already enabled for it. + if (_manager.EnabledSets.ContainsKey(key)) + continue; + + // Apply to all actors represented by this key. + foreach (var actor in data2.Objects) + { + var specificId = actor.GetIdentifier(_actors); + if (_state.GetOrCreate(specificId, actor, out var state)) + { + Reduce(actor, state, newSet, _config.RespectManualOnAutomationUpdate, false, true, out var forcedRedraw); + _state.ReapplyAutomationState(actor, forcedRedraw, false, StateSource.Fixed); + } + } + } + + continue; + } if (_objects.TryGetValue(id, out var data)) { if (_state.GetOrCreate(id, data.Objects[0], out var state)) @@ -318,30 +361,81 @@ public sealed class AutoDesignApplier : IDisposable switch (identifier.Type) { case IdentifierType.Player: - if (_manager.EnabledSets.TryGetValue(identifier, out set)) + if (TryGettingSetExactOrWildcard(identifier, out set)) return true; identifier = _actors.CreatePlayer(identifier.PlayerName, WorldId.AnyWorld); - return _manager.EnabledSets.TryGetValue(identifier, out set); + if (TryGettingSetExactOrWildcard(identifier, out set)) + return true; + + set = null; + return false; case IdentifierType.Retainer: case IdentifierType.Npc: - return _manager.EnabledSets.TryGetValue(identifier, out set); + return TryGettingSetExactOrWildcard(identifier, out set); case IdentifierType.Owned: - if (_manager.EnabledSets.TryGetValue(identifier, out set)) + if (TryGettingSetExactOrWildcard(identifier, out set)) return true; identifier = _actors.CreateOwned(identifier.PlayerName, WorldId.AnyWorld, identifier.Kind, identifier.DataId); - if (_manager.EnabledSets.TryGetValue(identifier, out set)) + if (TryGettingSetExactOrWildcard(identifier, out set)) return true; identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId); - return _manager.EnabledSets.TryGetValue(identifier, out set); + return TryGettingSetExactOrWildcard(identifier, out set); default: set = null; return false; } } + /// Try to get a set matching exactly or via wildcard pattern. + private bool TryGettingSetExactOrWildcard(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set) + { + // First try exact match + if (_manager.EnabledSets.TryGetValue(identifier, out set)) + return true; + + // Then try wildcard matches + foreach (var (key, value) in _manager.EnabledSets) + { + // Use wildcard-aware matching when the stored identifier contains a wildcard pattern in the name. + if (!key.PlayerName.IsEmpty && key.PlayerName.ToString().Contains('*')) + { + var sameType = identifier.Type == key.Type; + if (!sameType) + continue; + + var worldMatches = identifier.Type switch + { + Penumbra.GameData.Enums.IdentifierType.Player => identifier.HomeWorld == key.HomeWorld || identifier.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld || key.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld, + Penumbra.GameData.Enums.IdentifierType.Owned => identifier.HomeWorld == key.HomeWorld || identifier.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld || key.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld, + _ => true, + }; + + if (!worldMatches) + continue; + + var name = identifier.PlayerName.ToString(); + var pattern = key.PlayerName.ToString(); + var regexPattern = System.Text.RegularExpressions.Regex.Escape(pattern).Replace("\\*", ".*"); + if (System.Text.RegularExpressions.Regex.IsMatch(name, $"^{regexPattern}$", System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + { + set = value; + return true; + } + } + else if (identifier.Matches(key)) + { + set = value; + return true; + } + } + + set = null; + return false; + } + internal static int NewGearsetId = -1; private void OnEquippedGearset(string name, int id, int prior, byte _, byte job) diff --git a/Glamourer/Automation/AutoDesignManager.cs b/Glamourer/Automation/AutoDesignManager.cs index 7a4511b..ebf76bf 100644 --- a/Glamourer/Automation/AutoDesignManager.cs +++ b/Glamourer/Automation/AutoDesignManager.cs @@ -15,6 +15,7 @@ using OtterGui.Filesystem; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; +using Penumbra.String; namespace Glamourer.Automation; @@ -406,6 +407,60 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos }; } + /// + /// Try to get an automation set that matches the given identifier, including wildcard patterns. + /// First tries exact match, then tries wildcard patterns from enabled sets. + /// + public bool TryGetSetWithWildcard(ActorIdentifier identifier, [NotNullWhen(true)] out AutoDesignSet? set) + { + // First try exact match + if (_enabled.TryGetValue(identifier, out set)) + return true; + + // Then try wildcard matching against all enabled sets + foreach (var (_, enabledSet) in _enabled) + { + foreach (var setId in enabledSet.Identifiers) + { + // Use wildcard-aware matching when the stored identifier contains a wildcard pattern in the name. + if (!setId.PlayerName.IsEmpty && setId.PlayerName.ToString().Contains('*')) + { + var sameType = identifier.Type == setId.Type; + if (!sameType) + continue; + + var worldMatches = identifier.Type switch + { + Penumbra.GameData.Enums.IdentifierType.Player => identifier.HomeWorld == setId.HomeWorld || identifier.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld || setId.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld, + Penumbra.GameData.Enums.IdentifierType.Owned => identifier.HomeWorld == setId.HomeWorld || identifier.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld || setId.HomeWorld == Penumbra.GameData.Structs.WorldId.AnyWorld, + _ => true, + }; + + if (!worldMatches) + continue; + + // Inline wildcard matching to avoid cross-namespace ambiguity. + var name = identifier.PlayerName.ToString(); + var pattern = setId.PlayerName.ToString(); + var regexPattern = System.Text.RegularExpressions.Regex.Escape(pattern).Replace("\\*", ".*"); + if (System.Text.RegularExpressions.Regex.IsMatch(name, $"^{regexPattern}$", System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + { + set = enabledSet; + return true; + } + } + else if (identifier.Matches(setId)) + { + set = enabledSet; + return true; + } + } + } + + set = null; + return false; + } + private void Load() { var file = _saveService.FileNames.AutomationFile; @@ -611,35 +666,46 @@ public class AutoDesignManager : ISavable, IReadOnlyList, IDispos { IdentifierType.Player => [ - identifier.CreatePermanent(), + (IsWildcardName(identifier.PlayerName) + ? _actors.CreatePlayerUnchecked(identifier.PlayerName, identifier.HomeWorld).CreatePermanent() + : identifier.CreatePermanent()), ], IdentifierType.Retainer => [ - _actors.CreateRetainer(identifier.PlayerName, - identifier.Retainer == ActorIdentifier.RetainerType.Mannequin - ? ActorIdentifier.RetainerType.Mannequin - : ActorIdentifier.RetainerType.Bell).CreatePermanent(), + (IsWildcardName(identifier.PlayerName) + ? _actors.CreateRetainerUnchecked(identifier.PlayerName, + identifier.Retainer == ActorIdentifier.RetainerType.Mannequin + ? ActorIdentifier.RetainerType.Mannequin + : ActorIdentifier.RetainerType.Bell) + : _actors.CreateRetainer(identifier.PlayerName, + identifier.Retainer == ActorIdentifier.RetainerType.Mannequin + ? ActorIdentifier.RetainerType.Mannequin + : ActorIdentifier.RetainerType.Bell)).CreatePermanent(), ], IdentifierType.Npc => CreateNpcs(_actors, identifier), IdentifierType.Owned => CreateNpcs(_actors, identifier), _ => [], }; - static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) - { - var name = manager.Data.ToName(identifier.Kind, identifier.DataId); - var table = identifier.Kind switch - { - ObjectKind.BattleNpc => (IReadOnlyDictionary)manager.Data.BNpcs, - ObjectKind.EventNpc => manager.Data.ENpcs, - _ => new Dictionary(), - }; - return table.Where(kvp => kvp.Value == name) - .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, - identifier.Kind, kvp.Key)).ToArray(); - } + static bool IsWildcardName(ByteString name) + => name.ToString().Contains('*'); } + static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier) + { + var name = manager.Data.ToName(identifier.Kind, identifier.DataId); + var table = identifier.Kind switch + { + ObjectKind.BattleNpc => (IReadOnlyDictionary)manager.Data.BNpcs, + ObjectKind.EventNpc => manager.Data.ENpcs, + _ => new Dictionary(), + }; + return table.Where(kvp => kvp.Value == name) + .Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id, + identifier.Kind, kvp.Key)).ToArray(); + } + + private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _) { if (type is not DesignChanged.Type.Deleted) diff --git a/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs b/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs index ba2e424..2fbebe2 100644 --- a/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs +++ b/Glamourer/Gui/Tabs/AutomationTab/IdentifierDrawer.cs @@ -5,6 +5,7 @@ using Penumbra.GameData.DataContainers; using Penumbra.GameData.Gui; using Penumbra.GameData.Structs; using Penumbra.String; +using Penumbra.GameData.Enums; namespace Glamourer.Gui.Tabs.AutomationTab; @@ -64,20 +65,52 @@ public class IdentifierDrawer public bool CanSetOwned => OwnedIdentifier.IsValid; + private static bool IsWildcardPattern(string? name) + => !string.IsNullOrEmpty(name) && name.Contains('*'); + + private static T Create(bool wildcard, Func uncheckedFactory, Func checkedFactory) + => wildcard ? uncheckedFactory() : checkedFactory(); + private void UpdateIdentifiers() { - if (ByteString.FromString(_characterName, out var byteName)) + var isWildcard = IsWildcardPattern(_characterName); + ByteString byteName = default; + + // For wildcard patterns, use FromStringUnsafe to allow '*' characters + if (isWildcard) + byteName = ByteString.FromStringUnsafe(_characterName ?? string.Empty, false); + else if (!ByteString.FromString(_characterName, out byteName)) { - PlayerIdentifier = _actors.CreatePlayer(byteName, _worldCombo.CurrentSelection.Key); - RetainerIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Bell); - MannequinIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Mannequin); - - if (_humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc) - OwnedIdentifier = _actors.CreateOwned(byteName, _worldCombo.CurrentSelection.Key, _humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]); - else - OwnedIdentifier = ActorIdentifier.Invalid; + PlayerIdentifier = ActorIdentifier.Invalid; + RetainerIdentifier = ActorIdentifier.Invalid; + MannequinIdentifier = ActorIdentifier.Invalid; + OwnedIdentifier = ActorIdentifier.Invalid; + NpcIdentifier = ActorIdentifier.Invalid; + return; } + // Create identifiers using a single helper to handle wildcard vs checked creation + PlayerIdentifier = Create(isWildcard, + () => _actors.CreatePlayerUnchecked(byteName, _worldCombo.CurrentSelection.Key), + () => _actors.CreatePlayer(byteName, _worldCombo.CurrentSelection.Key)); + + RetainerIdentifier = Create(isWildcard, + () => _actors.CreateRetainerUnchecked(byteName, ActorIdentifier.RetainerType.Bell), + () => _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Bell)); + + MannequinIdentifier = Create(isWildcard, + () => _actors.CreateRetainerUnchecked(byteName, ActorIdentifier.RetainerType.Mannequin), + () => _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Mannequin)); + + if (_humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc) + OwnedIdentifier = Create(isWildcard, + () => _actors.CreateIndividualUnchecked(IdentifierType.Owned, byteName, _worldCombo.CurrentSelection.Key.Id, + _humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]), + () => _actors.CreateOwned(byteName, _worldCombo.CurrentSelection.Key, _humanNpcCombo.CurrentSelection.Kind, + _humanNpcCombo.CurrentSelection.Ids[0])); + else + OwnedIdentifier = ActorIdentifier.Invalid; + NpcIdentifier = _humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc ? _actors.CreateNpc(_humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]) : ActorIdentifier.Invalid;