Add wildcard support for automations

This commit is contained in:
MTVirux 2025-11-22 13:13:17 +00:00
parent bf4673a1d9
commit b384c8614a
3 changed files with 226 additions and 33 deletions

View file

@ -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;
}
}
/// <summary> Try to get a set matching exactly or via wildcard pattern. </summary>
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)

View file

@ -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<AutoDesignSet>, IDispos
};
}
/// <summary>
/// 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.
/// </summary>
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,20 +666,31 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
{
IdentifierType.Player =>
[
identifier.CreatePermanent(),
(IsWildcardName(identifier.PlayerName)
? _actors.CreatePlayerUnchecked(identifier.PlayerName, identifier.HomeWorld).CreatePermanent()
: identifier.CreatePermanent()),
],
IdentifierType.Retainer =>
[
_actors.CreateRetainer(identifier.PlayerName,
(IsWildcardName(identifier.PlayerName)
? _actors.CreateRetainerUnchecked(identifier.PlayerName,
identifier.Retainer == ActorIdentifier.RetainerType.Mannequin
? ActorIdentifier.RetainerType.Mannequin
: ActorIdentifier.RetainerType.Bell).CreatePermanent(),
: 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 bool IsWildcardName(ByteString name)
=> name.ToString().Contains('*');
}
static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier)
{
var name = manager.Data.ToName(identifier.Kind, identifier.DataId);
@ -638,7 +704,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
.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? _)
{

View file

@ -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,19 +65,51 @@ public class IdentifierDrawer
public bool CanSetOwned
=> OwnedIdentifier.IsValid;
private static bool IsWildcardPattern(string? name)
=> !string.IsNullOrEmpty(name) && name.Contains('*');
private static T Create<T>(bool wildcard, Func<T> uncheckedFactory, Func<T> 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);
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 = _actors.CreateOwned(byteName, _worldCombo.CurrentSelection.Key, _humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]);
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])