mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2025-12-12 18:27:24 +01:00
Add wildcard support for automations
This commit is contained in:
parent
bf4673a1d9
commit
b384c8614a
3 changed files with 226 additions and 33 deletions
|
|
@ -155,6 +155,49 @@ public sealed class AutoDesignApplier : IDisposable
|
||||||
|
|
||||||
foreach (var id in newSet.Identifiers)
|
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 (_objects.TryGetValue(id, out var data))
|
||||||
{
|
{
|
||||||
if (_state.GetOrCreate(id, data.Objects[0], out var state))
|
if (_state.GetOrCreate(id, data.Objects[0], out var state))
|
||||||
|
|
@ -318,30 +361,81 @@ public sealed class AutoDesignApplier : IDisposable
|
||||||
switch (identifier.Type)
|
switch (identifier.Type)
|
||||||
{
|
{
|
||||||
case IdentifierType.Player:
|
case IdentifierType.Player:
|
||||||
if (_manager.EnabledSets.TryGetValue(identifier, out set))
|
if (TryGettingSetExactOrWildcard(identifier, out set))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
identifier = _actors.CreatePlayer(identifier.PlayerName, WorldId.AnyWorld);
|
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.Retainer:
|
||||||
case IdentifierType.Npc:
|
case IdentifierType.Npc:
|
||||||
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
return TryGettingSetExactOrWildcard(identifier, out set);
|
||||||
case IdentifierType.Owned:
|
case IdentifierType.Owned:
|
||||||
if (_manager.EnabledSets.TryGetValue(identifier, out set))
|
if (TryGettingSetExactOrWildcard(identifier, out set))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
identifier = _actors.CreateOwned(identifier.PlayerName, WorldId.AnyWorld, identifier.Kind, identifier.DataId);
|
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;
|
return true;
|
||||||
|
|
||||||
identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId);
|
identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId);
|
||||||
return _manager.EnabledSets.TryGetValue(identifier, out set);
|
return TryGettingSetExactOrWildcard(identifier, out set);
|
||||||
default:
|
default:
|
||||||
set = null;
|
set = null;
|
||||||
return false;
|
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;
|
internal static int NewGearsetId = -1;
|
||||||
|
|
||||||
private void OnEquippedGearset(string name, int id, int prior, byte _, byte job)
|
private void OnEquippedGearset(string name, int id, int prior, byte _, byte job)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ using OtterGui.Filesystem;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.String;
|
||||||
|
|
||||||
namespace Glamourer.Automation;
|
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()
|
private void Load()
|
||||||
{
|
{
|
||||||
var file = _saveService.FileNames.AutomationFile;
|
var file = _saveService.FileNames.AutomationFile;
|
||||||
|
|
@ -611,35 +666,46 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
|
||||||
{
|
{
|
||||||
IdentifierType.Player =>
|
IdentifierType.Player =>
|
||||||
[
|
[
|
||||||
identifier.CreatePermanent(),
|
(IsWildcardName(identifier.PlayerName)
|
||||||
|
? _actors.CreatePlayerUnchecked(identifier.PlayerName, identifier.HomeWorld).CreatePermanent()
|
||||||
|
: identifier.CreatePermanent()),
|
||||||
],
|
],
|
||||||
IdentifierType.Retainer =>
|
IdentifierType.Retainer =>
|
||||||
[
|
[
|
||||||
_actors.CreateRetainer(identifier.PlayerName,
|
(IsWildcardName(identifier.PlayerName)
|
||||||
identifier.Retainer == ActorIdentifier.RetainerType.Mannequin
|
? _actors.CreateRetainerUnchecked(identifier.PlayerName,
|
||||||
? ActorIdentifier.RetainerType.Mannequin
|
identifier.Retainer == ActorIdentifier.RetainerType.Mannequin
|
||||||
: ActorIdentifier.RetainerType.Bell).CreatePermanent(),
|
? 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.Npc => CreateNpcs(_actors, identifier),
|
||||||
IdentifierType.Owned => CreateNpcs(_actors, identifier),
|
IdentifierType.Owned => CreateNpcs(_actors, identifier),
|
||||||
_ => [],
|
_ => [],
|
||||||
};
|
};
|
||||||
|
|
||||||
static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier)
|
static bool IsWildcardName(ByteString name)
|
||||||
{
|
=> name.ToString().Contains('*');
|
||||||
var name = manager.Data.ToName(identifier.Kind, identifier.DataId);
|
|
||||||
var table = identifier.Kind switch
|
|
||||||
{
|
|
||||||
ObjectKind.BattleNpc => (IReadOnlyDictionary<NpcId, string>)manager.Data.BNpcs,
|
|
||||||
ObjectKind.EventNpc => manager.Data.ENpcs,
|
|
||||||
_ => new Dictionary<NpcId, string>(),
|
|
||||||
};
|
|
||||||
return table.Where(kvp => kvp.Value == name)
|
|
||||||
.Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id,
|
|
||||||
identifier.Kind, kvp.Key)).ToArray();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ActorIdentifier[] CreateNpcs(ActorManager manager, ActorIdentifier identifier)
|
||||||
|
{
|
||||||
|
var name = manager.Data.ToName(identifier.Kind, identifier.DataId);
|
||||||
|
var table = identifier.Kind switch
|
||||||
|
{
|
||||||
|
ObjectKind.BattleNpc => (IReadOnlyDictionary<NpcId, string>)manager.Data.BNpcs,
|
||||||
|
ObjectKind.EventNpc => manager.Data.ENpcs,
|
||||||
|
_ => new Dictionary<NpcId, string>(),
|
||||||
|
};
|
||||||
|
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? _)
|
private void OnDesignChange(DesignChanged.Type type, Design design, ITransaction? _)
|
||||||
{
|
{
|
||||||
if (type is not DesignChanged.Type.Deleted)
|
if (type is not DesignChanged.Type.Deleted)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using Penumbra.GameData.DataContainers;
|
||||||
using Penumbra.GameData.Gui;
|
using Penumbra.GameData.Gui;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
|
||||||
namespace Glamourer.Gui.Tabs.AutomationTab;
|
namespace Glamourer.Gui.Tabs.AutomationTab;
|
||||||
|
|
||||||
|
|
@ -64,20 +65,52 @@ public class IdentifierDrawer
|
||||||
public bool CanSetOwned
|
public bool CanSetOwned
|
||||||
=> OwnedIdentifier.IsValid;
|
=> 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()
|
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);
|
PlayerIdentifier = ActorIdentifier.Invalid;
|
||||||
RetainerIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Bell);
|
RetainerIdentifier = ActorIdentifier.Invalid;
|
||||||
MannequinIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Mannequin);
|
MannequinIdentifier = ActorIdentifier.Invalid;
|
||||||
|
OwnedIdentifier = ActorIdentifier.Invalid;
|
||||||
if (_humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc)
|
NpcIdentifier = ActorIdentifier.Invalid;
|
||||||
OwnedIdentifier = _actors.CreateOwned(byteName, _worldCombo.CurrentSelection.Key, _humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0]);
|
return;
|
||||||
else
|
|
||||||
OwnedIdentifier = ActorIdentifier.Invalid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
NpcIdentifier = _humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc
|
||||||
? _actors.CreateNpc(_humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0])
|
? _actors.CreateNpc(_humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0])
|
||||||
: ActorIdentifier.Invalid;
|
: ActorIdentifier.Invalid;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue