diff --git a/Brio/Brio.cs b/Brio/Brio.cs index fc132dcc..a2db7460 100644 --- a/Brio/Brio.cs +++ b/Brio/Brio.cs @@ -226,6 +226,7 @@ private static ServiceCollection SetupServices(DalamudPluginService dalamudServi serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); diff --git a/Brio/IPC/PenumbraModActionService.cs b/Brio/IPC/PenumbraModActionService.cs new file mode 100644 index 00000000..ed9c8eff --- /dev/null +++ b/Brio/IPC/PenumbraModActionService.cs @@ -0,0 +1,323 @@ +using Brio.Resources; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Plugin; +using Lumina.Excel.Sheets; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Brio.IPC; + +public sealed class PenumbraModActionService +{ + private static readonly TimeSpan RefreshInterval = TimeSpan.FromSeconds(2); + + private readonly PenumbraService _penumbraService; + private readonly GetEnabledState _getEnabledState; + private readonly GetCollectionForObject _getCollectionForObject; + private readonly GetChangedItemsForCollection _getChangedItemsForCollection; + private readonly CheckCurrentChangedItemFunc _checkCurrentChangedItem; + private readonly GetGameObjectResourcePaths _getGameObjectResourcePaths; + private readonly GetModPath _getModPath; + private readonly ResolveGameObjectPath _resolveGameObjectPath; + + private IReadOnlyList _cachedActions = []; + private string _cacheSignature = string.Empty; + private string _statusMessage = "Penumbra mod actions have not been scanned yet."; + private DateTime _nextRefreshAtUtc = DateTime.MinValue; + private ushort _cachedObjectIndex = ushort.MaxValue; + + public int Version { get; private set; } + public string StatusMessage => _statusMessage; + + public PenumbraModActionService(IDalamudPluginInterface pluginInterface, PenumbraService penumbraService) + { + _penumbraService = penumbraService; + _getEnabledState = new GetEnabledState(pluginInterface); + _getCollectionForObject = new GetCollectionForObject(pluginInterface); + _getChangedItemsForCollection = new GetChangedItemsForCollection(pluginInterface); + _checkCurrentChangedItem = new CheckCurrentChangedItemFunc(pluginInterface); + _getGameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface); + _getModPath = new GetModPath(pluginInterface); + _resolveGameObjectPath = new ResolveGameObjectPath(pluginInterface); + } + + public IReadOnlyList GetActiveActions(IGameObject actor) + { + var objectChanged = _cachedObjectIndex != actor.ObjectIndex; + if(!objectChanged && DateTime.UtcNow < _nextRefreshAtUtc) + return _cachedActions; + + _cachedObjectIndex = actor.ObjectIndex; + _nextRefreshAtUtc = DateTime.UtcNow + RefreshInterval; + + Refresh(actor); + return _cachedActions; + } + + private void Refresh(IGameObject actor) + { + try + { + if(!_penumbraService.AllowIntegration || !_penumbraService.IsAvailable) + { + SetCache([], "Penumbra integration is unavailable or disabled."); + return; + } + + if(!_getEnabledState.Invoke()) + { + SetCache([], "Penumbra is currently disabled."); + return; + } + + var (objectValid, _, collection) = _getCollectionForObject.Invoke(actor.ObjectIndex); + if(!objectValid) + { + SetCache([], "The actor's Penumbra collection could not be resolved."); + return; + } + + var changedItems = _getChangedItemsForCollection.Invoke(collection.Id); + var modLookup = _checkCurrentChangedItem.Invoke(); + var actions = BuildActions(changedItems, modLookup, actor.ObjectIndex); + var status = actions.Count == 0 + ? string.Format("No active mod actions were found in {0}.", collection.Name) + : string.Format("{0} active mod actions from {1}.", actions.Count, collection.Name); + + SetCache(actions, status); + } + catch(Exception ex) + { + Brio.Log.Warning(ex, "Failed to scan Penumbra mod actions"); + SetCache([], "Failed to scan Penumbra mod actions."); + } + } + + private IReadOnlyList BuildActions( + IReadOnlyDictionary changedItems, + Func modLookup, + ushort objectIndex) + { + var modsByChangedItem = new Dictionary>(StringComparer.OrdinalIgnoreCase); + foreach(var changedItemName in changedItems.Keys) + { + var resolved = ResolveMods(modLookup(changedItemName)); + if(resolved.Count > 0) + modsByChangedItem[changedItemName] = resolved; + } + + var allMods = modsByChangedItem.Values + .SelectMany(mods => mods) + .GroupBy(mod => mod.ModDirectory, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .ToList(); + + var variantResources = GetVariantResources(objectIndex); + var (variantActions, detectedModEmotes) = BuildVariantActions(allMods, variantResources, changedItems.Values.OfType()); + var actions = new List(variantActions); + + foreach(var (changedItemName, payload) in changedItems.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase)) + { + if(payload is not Emote emote || emote.RowId is 0 or > ushort.MaxValue) + continue; + + if(!modsByChangedItem.TryGetValue(changedItemName, out var resolvedMods)) + continue; + + var mods = resolvedMods.ToList(); + + var emoteName = emote.Name.ToString().Trim(); + if(string.IsNullOrWhiteSpace(emoteName)) + emoteName = $"Emote {emote.RowId}"; + + if(CommonPoseCatalog.All.Any(variant => variant.BaseEmoteId == emote.RowId)) + mods = mods.Where(mod => !detectedModEmotes.Contains(VariantDetectionKey(mod.ModDirectory, emote.RowId))).ToList(); + + if(mods.Count > 0) + actions.Add(new PenumbraModAction(emote, emoteName, BuildModLabel(mods.Select(mod => mod.DisplayName).ToArray()), null)); + } + + return actions; + } + + private IReadOnlyList ResolveMods((string ModDirectory, string ModName)[] modPairs) + { + var mods = new List(modPairs.Length); + foreach(var (modDirectory, modName) in modPairs) + { + var displayName = string.IsNullOrWhiteSpace(modName) ? modDirectory : modName; + if(string.IsNullOrWhiteSpace(displayName) + || mods.Any(mod => string.Equals(mod.ModDirectory, modDirectory, StringComparison.OrdinalIgnoreCase))) + continue; + + string? pathPrefix = null; + try + { + var (result, fullPath, _, _) = _getModPath.Invoke(modDirectory, modName); + if(result == PenumbraApiEc.Success && !string.IsNullOrWhiteSpace(fullPath)) + pathPrefix = NormalizeDirectoryPrefix(fullPath); + } + catch(Exception ex) + { + Brio.Log.Debug(ex, "Failed to resolve the Penumbra path for {ModDirectory}", modDirectory); + } + + mods.Add(new ResolvedModReference(modDirectory, displayName, pathPrefix)); + } + + return mods.OrderBy(mod => mod.DisplayName, StringComparer.OrdinalIgnoreCase).ToList(); + } + + private IReadOnlyList GetVariantResources(ushort objectIndex) + { + try + { + var resources = _getGameObjectResourcePaths.Invoke(objectIndex); + if(resources.Length == 0 || resources[0] is not { } pathMap) + return []; + + var hits = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var variantsByFileName = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach(var definition in CommonPoseCatalog.All) + { + if(GameDataProvider.Instance.ActionTimelines.TryGetRow(definition.TimelineId, out var timeline)) + variantsByFileName[$"{timeline.Key}.pap"] = definition; + } + + foreach(var actualPath in pathMap.Keys) + { + if(string.IsNullOrWhiteSpace(actualPath)) + continue; + + var fileName = Path.GetFileName(actualPath); + if(!string.IsNullOrWhiteSpace(fileName) && variantsByFileName.TryGetValue(fileName, out var definition) + && seen.Add($"{actualPath}|{definition.TimelineId}")) + hits.Add(new VariantResourceHit(actualPath, definition)); + } + + var residentDirectories = pathMap.Values + .SelectMany(paths => paths) + .Select(NormalizeGamePath) + .Where(path => path.Contains("/bt_common/resident/", StringComparison.OrdinalIgnoreCase)) + .Select(path => path[..path.LastIndexOf('/')]) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach(var residentDirectory in residentDirectories) + { + foreach(var (fileName, definition) in variantsByFileName) + { + var gamePath = $"{residentDirectory}/{fileName}"; + var resolvedPath = _resolveGameObjectPath.Invoke(gamePath, objectIndex); + if(string.IsNullOrWhiteSpace(resolvedPath) + || string.Equals(NormalizeGamePath(resolvedPath), gamePath, StringComparison.OrdinalIgnoreCase) + || !seen.Add($"{resolvedPath}|{definition.TimelineId}")) + continue; + + hits.Add(new VariantResourceHit(resolvedPath, definition)); + } + } + + return hits; + } + catch(Exception ex) + { + Brio.Log.Debug(ex, "Failed to inspect Penumbra pose variant resources"); + return []; + } + } + + private static (IReadOnlyList Actions, IReadOnlySet DetectedModEmotes) BuildVariantActions( + IReadOnlyList mods, + IReadOnlyList variantResources, + IEnumerable emotes) + { + var actions = new List(); + var detectedModEmotes = new HashSet(StringComparer.OrdinalIgnoreCase); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var emotesById = emotes + .GroupBy(emote => emote.RowId) + .ToDictionary(group => group.Key, group => group.First()); + + foreach(var mod in mods) + { + if(string.IsNullOrWhiteSpace(mod.PathPrefix)) + continue; + + foreach(var hit in variantResources + .Where(hit => IsPathInDirectory(hit.ActualPath, mod.PathPrefix)) + .OrderBy(hit => hit.Definition.Kind) + .ThenBy(hit => hit.Definition.VariantIndex)) + { + if(!seen.Add($"{mod.ModDirectory}|{hit.Definition.TimelineId}")) + continue; + + Emote? emote = null; + if(hit.Definition.BaseEmoteId != 0 && emotesById.TryGetValue(hit.Definition.BaseEmoteId, out var baseEmote)) + emote = baseEmote; + + actions.Add(new PenumbraModAction( + emote, + hit.Definition.DisplayName, + mod.DisplayName, + hit.Definition.TimelineId)); + + if(hit.Definition.BaseEmoteId != 0) + detectedModEmotes.Add(VariantDetectionKey(mod.ModDirectory, hit.Definition.BaseEmoteId)); + } + } + + return (actions, detectedModEmotes); + } + + private static string NormalizeGamePath(string path) + => path.Replace('\\', '/'); + + private static string VariantDetectionKey(string modDirectory, uint baseEmoteId) + => $"{modDirectory}|{baseEmoteId}"; + + private static string BuildModLabel(IReadOnlyList names) + => names.Count switch + { + 0 => string.Empty, + 1 => names[0], + 2 => $"{names[0]} / {names[1]}", + _ => $"{names[0]} +{names.Count - 1}", + }; + + private static string NormalizeDirectoryPrefix(string path) + { + var normalized = path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) + .TrimEnd(Path.DirectorySeparatorChar); + return $"{normalized}{Path.DirectorySeparatorChar}"; + } + + private static bool IsPathInDirectory(string actualPath, string pathPrefix) + { + var normalizedPath = actualPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + return normalizedPath.StartsWith(pathPrefix, StringComparison.OrdinalIgnoreCase); + } + + private void SetCache(IReadOnlyList actions, string status) + { + var signature = string.Join('\n', actions.Select(action => $"{action.Emote?.RowId ?? 0}|{action.ModName}|{action.TimelineId}")); + if(!string.Equals(_cacheSignature, signature, StringComparison.Ordinal)) + { + _cachedActions = actions; + _cacheSignature = signature; + Version++; + } + + _statusMessage = status; + } + + private sealed record ResolvedModReference(string ModDirectory, string DisplayName, string? PathPrefix); + private sealed record VariantResourceHit(string ActualPath, CommonPoseDefinition Definition); +} + +public sealed record PenumbraModAction(Emote? Emote, string EmoteName, string ModName, uint? TimelineId); diff --git a/Brio/Resources/CommonPoseCatalog.cs b/Brio/Resources/CommonPoseCatalog.cs new file mode 100644 index 00000000..ac3faf3f --- /dev/null +++ b/Brio/Resources/CommonPoseCatalog.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; + +namespace Brio.Resources; + +public enum CommonPoseKind +{ + ChairSit, + Standing, + GroundSit, + Sleep, +} + +public readonly record struct CommonPoseDefinition( + uint TimelineId, + CommonPoseKind Kind, + int VariantIndex, + uint BaseEmoteId) +{ + public string DisplayName => (Kind, VariantIndex) switch + { + (CommonPoseKind.ChairSit, 0) => "Base Chair Sit Pose", + (CommonPoseKind.ChairSit, _) => string.Format("Chair Sit Pose {0:D2}", VariantIndex), + (CommonPoseKind.Standing, 0) => "Base Standing Pose", + (CommonPoseKind.Standing, _) => string.Format("Standing Pose {0:D2}", VariantIndex), + (CommonPoseKind.GroundSit, 0) => "Base Ground Sit Pose", + (CommonPoseKind.GroundSit, _) => string.Format("Ground Sit Pose {0:D2}", VariantIndex), + (CommonPoseKind.Sleep, _) => string.Format("Sleep Pose {0:D2}", VariantIndex), + _ => string.Format("Timeline {0}", TimelineId), + }; +} + +public static class CommonPoseCatalog +{ + public static IReadOnlyList All { get; } = + [ + new(643, CommonPoseKind.ChairSit, 0, 50), + new(3132, CommonPoseKind.ChairSit, 1, 50), + new(3134, CommonPoseKind.ChairSit, 2, 50), + new(8002, CommonPoseKind.ChairSit, 3, 50), + new(8004, CommonPoseKind.ChairSit, 4, 50), + + new(3, CommonPoseKind.Standing, 0, 0), + new(3124, CommonPoseKind.Standing, 1, 0), + new(3126, CommonPoseKind.Standing, 2, 0), + new(3182, CommonPoseKind.Standing, 3, 0), + new(3184, CommonPoseKind.Standing, 4, 0), + new(7405, CommonPoseKind.Standing, 5, 0), + new(7407, CommonPoseKind.Standing, 6, 0), + + new(654, CommonPoseKind.GroundSit, 0, 52), + new(3136, CommonPoseKind.GroundSit, 1, 52), + new(3138, CommonPoseKind.GroundSit, 2, 52), + new(3771, CommonPoseKind.GroundSit, 3, 52), + + new(3140, CommonPoseKind.Sleep, 1, 13), + new(3142, CommonPoseKind.Sleep, 2, 13), + new(585, CommonPoseKind.Sleep, 3, 13), + ]; +} diff --git a/Brio/UI/Controls/Editors/ActionTimelineEditor.cs b/Brio/UI/Controls/Editors/ActionTimelineEditor.cs index a20100bd..d08cea1a 100644 --- a/Brio/UI/Controls/Editors/ActionTimelineEditor.cs +++ b/Brio/UI/Controls/Editors/ActionTimelineEditor.cs @@ -78,6 +78,7 @@ private void HandleSelectorChanges() public void Draw(bool drawAdvanced, ActionTimelineCapability capability) { _capability = capability; + _globalTimelineSelector.ModActionActor = capability.GameObject; _globalTimelineSelector.DrawAsWindow(); diff --git a/Brio/UI/Controls/Selectors/ActionTimelineSelector.cs b/Brio/UI/Controls/Selectors/ActionTimelineSelector.cs index 07cd4562..9fdd240f 100644 --- a/Brio/UI/Controls/Selectors/ActionTimelineSelector.cs +++ b/Brio/UI/Controls/Selectors/ActionTimelineSelector.cs @@ -1,12 +1,15 @@ +using Brio.IPC; using Brio.Resources; using Brio.Resources.Sheets; using Brio.UI.Controls.Core; using Brio.UI.Controls.Stateless; using Brio.UI.Theming; using Dalamud.Bindings.ImGui; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Lumina.Excel.Sheets; using System; +using System.Collections.Generic; using System.Numerics; using static Brio.Game.Actor.ActionTimelineService; using ActionSheet = Lumina.Excel.Sheets.Action; @@ -22,10 +25,10 @@ public class ActionTimelineSelector(string id) : Selector _modActions = []; + private int _modActionVersion = -1; + public bool IsPinned => _isPinned; + public IGameObject? ModActionActor + { + get => _modActionActor; + set + { + var changed = _modActionActor?.ObjectIndex != value?.ObjectIndex; + _modActionActor = value; + if(changed) + _modActionVersion = -1; + } + } + //TODO(KEN) at some point make all of them use `field` public bool AllowBlending @@ -73,12 +92,14 @@ public void TogglePin() public void DrawAsWindow() { + RefreshModActions(); + if(!_isPinned) return; ImGui.SetNextWindowSize(new Vector2(400, 500), ImGuiCond.FirstUseEver); - if(ImGui.Begin($"Animation Search Selector ###{_id}_window2", ref _isWindowOpen, ImGuiWindowFlags.NoCollapse)) + if(ImGui.Begin($"Animation Search Selector ###{_id}_window2", ref _isWindowOpen, ImGuiWindowFlags.NoCollapse)) { if(!_isWindowOpen) { @@ -104,7 +125,7 @@ private void DrawPinButton() var pinIcon = _isPinned ? FontAwesomeIcon.Thumbtack : FontAwesomeIcon.Thumbtack; var pinColor = _isPinned ? UIConstants.GizmoRed : ThemeManager.CurrentTheme.Text.Text; - var tooltip = _isPinned ? "Unpin (close window)" : "Pin to keep open"; + var tooltip = _isPinned ? "Unpin (close window)" : "Pin to keep open"; if(ImBrio.FontIconButton($"pin_toggle_{_id}", pinIcon, tooltip, true, true, pinColor)) { @@ -116,9 +137,11 @@ private void DrawPinButton() public new void Draw() { + RefreshModActions(); + if(_isPinned) { - ImGui.TextDisabled("(Selector is pinned as separate window)"); + ImGui.TextDisabled("(Selector is pinned as separate window)"); return; } @@ -127,6 +150,20 @@ private void DrawPinButton() base.Draw(); } + private void RefreshModActions() + { + if(_modActionActor is null || !Brio.TryGetService(out var service)) + return; + + var actions = service.GetActiveActions(_modActionActor); + if(_modActionVersion == service.Version) + return; + + _modActions = actions; + _modActionVersion = service.Version; + ReloadList(); + } + protected override void PopulateList() { foreach(var timeline in GameDataProvider.Instance.ActionTimelines) @@ -232,7 +269,7 @@ protected override void PopulateList() } } - foreach(var action in GameDataProvider.Instance.GetExcelSheet()) + foreach(var action in GameDataProvider.Instance.GetExcelSheet()) { if(action.AnimationEnd.RowId != 0 && GameDataProvider.Instance.ActionTimelines.TryGetRow(action.AnimationEnd.RowId, out BrioActionTimeline timeline)) AddItem(new ActionTimelineSelectorEntry( @@ -245,13 +282,77 @@ protected override void PopulateList() (ActionTimelineSlots)timeline.Slot, action.Icon, false, - 0)); - } + 0)); + } + + foreach(var pose in CommonPoseCatalog.All) + { + if(pose.TimelineId > ushort.MaxValue + || !GameDataProvider.Instance.ActionTimelines.TryGetRow(pose.TimelineId, out BrioActionTimeline timeline)) + continue; + + AddItem(new ActionTimelineSelectorEntry( + pose.DisplayName, + (ushort)pose.TimelineId, + pose.TimelineId, + timeline.Key.ToString(), + ActionTimelineSelectorEntry.OriginalType.Pose, + ActionTimelineSelectorEntry.AnimationPurpose.Standard, + (ActionTimelineSlots)timeline.Slot, + 0, + false, + 0)); + } + + foreach(var modAction in _modActions) + { + if(modAction.TimelineId is uint timelineId) + { + AddModTimeline(modAction, timelineId, ActionTimelineSelectorEntry.AnimationPurpose.Standard); + } + else + { + AddModTimeline(modAction, 0, ActionTimelineSelectorEntry.AnimationPurpose.Standard); + AddModTimeline(modAction, 1, ActionTimelineSelectorEntry.AnimationPurpose.Intro); + AddModTimeline(modAction, 2, ActionTimelineSelectorEntry.AnimationPurpose.Ground); + AddModTimeline(modAction, 3, ActionTimelineSelectorEntry.AnimationPurpose.Chair); + AddModTimeline(modAction, 4, ActionTimelineSelectorEntry.AnimationPurpose.Blend); + } + } + } + + private void AddModTimeline(PenumbraModAction modAction, int timelineIndex, ActionTimelineSelectorEntry.AnimationPurpose purpose) + { + if(modAction.Emote is not Emote emote) + return; + + var timelineId = emote.ActionTimeline[timelineIndex].RowId; + AddModTimeline(modAction, timelineId, purpose); + } + + private void AddModTimeline(PenumbraModAction modAction, uint timelineId, ActionTimelineSelectorEntry.AnimationPurpose purpose) + { + var emote = modAction.Emote; + if(timelineId == 0 || timelineId > ushort.MaxValue + || !GameDataProvider.Instance.ActionTimelines.TryGetRow(timelineId, out BrioActionTimeline timeline)) + return; + + AddItem(new ActionTimelineSelectorEntry( + $"{modAction.ModName} - {modAction.EmoteName}", + (ushort)timelineId, + emote?.RowId ?? timelineId, + timeline.Key.ToString(), + ActionTimelineSelectorEntry.OriginalType.Mod, + purpose, + (ActionTimelineSlots)timeline.Slot, + emote?.Icon ?? 0, + emote?.DrawsWeapon ?? false, + emote is Emote value ? (byte)value.EmoteCategory.RowId : (byte)0)); } protected override void DrawItem(ActionTimelineSelectorEntry item, bool isSoftSelected) { - var description = $"{item.Name}\n{item.SecondaryId} {item.TimelineType} {item.Slot} {item.Purpose}\n{item.TimelineId} {item.Key}"; + var description = $"{item.Name}\n{item.SecondaryId} {item.TimelineType} {item.Slot} {item.Purpose}\n{item.TimelineId} {item.Key}"; ImBrio.BorderedGameIcon("icon", item.Icon, "Images.ActionTimeline.png", description, flags: ImGuiButtonFlags.None, size: IconSize); } @@ -266,24 +367,29 @@ protected override void DrawOptions() if(ExpressionsOnly) return; - bool[] items = [_showEmotes, _showActions, _showRaw]; - - var changed = ImBrio.ToggleSelecterStrip("actiontimeline_filters_selector", Vector2.Zero, ref items, ["Emotes", "Actions", "Timelines"]); + bool[] items = [_showEmotes, _showActions, _showRaw]; + + var changed = ImBrio.ToggleSelecterStrip("actiontimeline_filters_selector", Vector2.Zero, ref items, + ["Emotes", "Actions", "Timelines"]); if(changed) { - _showEmotes = items[0]; - _showActions = items[1]; - _showRaw = items[2]; - - UpdateList(); - } + _showEmotes = items[0]; + _showActions = items[1]; + _showRaw = items[2]; + + UpdateList(); + } + + if(_emoteCategoryValue == 4 && _modActions.Count == 0 + && Brio.TryGetService(out var modActionService)) + ImGui.TextDisabled(modActionService.StatusMessage); ImBrio.VerticalPadding(4); if(_showBlendable) { - if(ImGui.Checkbox("Show Non-Blend Animations", ref _showNonBlendInBlendMode)) + if(ImGui.Checkbox("Show Non-Blend Animations", ref _showNonBlendInBlendMode)) UpdateList(); ImBrio.VerticalPadding(2); @@ -291,12 +397,13 @@ protected override void DrawOptions() if(!_showBlendable) { - ImGui.Text("Draws Weapon"); + ImGui.Text("Draws Weapon"); ImBrio.VerticalPadding(1); int drawsWeaponSelection = !_filterByDrawsWeapon ? 0 : (_drawsWeaponValue ? 2 : 1); - if(ImBrio.ButtonSelectorStrip("draws_weapon_filter", Vector2.Zero, ref drawsWeaponSelection, ["All", "Sheathed", "Drawn"])) + if(ImBrio.ButtonSelectorStrip("draws_weapon_filter", Vector2.Zero, ref drawsWeaponSelection, + ["All", "Sheathed", "Drawn"])) { switch(drawsWeaponSelection) { @@ -320,36 +427,27 @@ protected override void DrawOptions() ImBrio.VerticalPadding(4); } - ImGui.Text("Emote Category"); + ImGui.Text("Emote Category"); ImBrio.VerticalPadding(1); int emoteCategorySelection = _emoteCategoryValue; - if(ImBrio.ButtonSelectorStrip("emote_category_filter", Vector2.Zero, ref emoteCategorySelection, ["All", "General", "Special", "Expression"])) - { - _emoteCategoryValue = emoteCategorySelection; - _filterByEmoteCategory = _emoteCategoryValue != 0; - UpdateList(); - } + if(ImBrio.ButtonSelectorStrip("emote_category_filter", Vector2.Zero, ref emoteCategorySelection, + ["All", "General", "Special", "Expression", "Mod", "Poses"])) + { + _emoteCategoryValue = emoteCategorySelection; + _filterByEmoteCategory = _emoteCategoryValue is >= 1 and <= 3; + UpdateList(); + } ImBrio.VerticalPadding(3); } - protected override int Compare(ActionTimelineSelectorEntry itemA, ActionTimelineSelectorEntry itemB) - { - // Emotes first - if(itemA.TimelineType == ActionTimelineSelectorEntry.OriginalType.Emote && itemB.TimelineType != ActionTimelineSelectorEntry.OriginalType.Emote) - return -1; - - if(itemA.TimelineType != ActionTimelineSelectorEntry.OriginalType.Emote && itemB.TimelineType == ActionTimelineSelectorEntry.OriginalType.Emote) - return 1; - - // Then Actions - if(itemA.TimelineType == ActionTimelineSelectorEntry.OriginalType.Action && itemB.TimelineType != ActionTimelineSelectorEntry.OriginalType.Action) - return -1; - - if(itemA.TimelineType != ActionTimelineSelectorEntry.OriginalType.Action && itemB.TimelineType == ActionTimelineSelectorEntry.OriginalType.Action) - return 1; + protected override int Compare(ActionTimelineSelectorEntry itemA, ActionTimelineSelectorEntry itemB) + { + var typeCompare = TypePriority(itemA.TimelineType).CompareTo(TypePriority(itemB.TimelineType)); + if(typeCompare != 0) + return typeCompare; // Blank to last if(string.IsNullOrEmpty(itemA.Name) && !string.IsNullOrEmpty(itemB.Name)) @@ -370,8 +468,18 @@ protected override int Compare(ActionTimelineSelectorEntry itemA, ActionTimeline if(itemA.Slot != ActionTimelineSlots.Base && itemB.Slot == ActionTimelineSlots.Base) return 1; - return 0; - } + return 0; + } + + private static int TypePriority(ActionTimelineSelectorEntry.OriginalType type) + => type switch + { + ActionTimelineSelectorEntry.OriginalType.Emote => 0, + ActionTimelineSelectorEntry.OriginalType.Pose => 1, + ActionTimelineSelectorEntry.OriginalType.Mod => 2, + ActionTimelineSelectorEntry.OriginalType.Action => 3, + _ => 4, + }; protected override bool Filter(ActionTimelineSelectorEntry item, string search) { @@ -380,11 +488,15 @@ protected override bool Filter(ActionTimelineSelectorEntry item, string search) if(!searchText.Contains(search, StringComparison.InvariantCultureIgnoreCase)) return false; + var isEmote = item.TimelineType is ActionTimelineSelectorEntry.OriginalType.Emote + or ActionTimelineSelectorEntry.OriginalType.Mod + or ActionTimelineSelectorEntry.OriginalType.Pose; + if(ExpressionsOnly) - return item.TimelineType == ActionTimelineSelectorEntry.OriginalType.Emote && item.EmoteCategory == 3 && item.Purpose == ActionTimelineSelectorEntry.AnimationPurpose.Blend; + return isEmote && item.EmoteCategory == 3 && item.Purpose == ActionTimelineSelectorEntry.AnimationPurpose.Blend; - if(item.TimelineType == ActionTimelineSelectorEntry.OriginalType.Emote && !_showEmotes) - return false; + if(isEmote && !_showEmotes) + return false; if(item.TimelineType == ActionTimelineSelectorEntry.OriginalType.Action && !_showActions) return false; @@ -392,13 +504,13 @@ protected override bool Filter(ActionTimelineSelectorEntry item, string search) if(item.TimelineType == ActionTimelineSelectorEntry.OriginalType.Raw && !_showRaw) return false; - if(item.Slot != ActionTimelineSlots.Base && !_showBlendable) + if(item.Slot != ActionTimelineSlots.Base && !_showBlendable) return false; // When in blend mode, filter out non-blend animations unless option is enabled if(_showBlendable && !_showNonBlendInBlendMode) { - if(item.TimelineType == ActionTimelineSelectorEntry.OriginalType.Emote) + if(isEmote) { if(item.Purpose != ActionTimelineSelectorEntry.AnimationPurpose.Blend) return false; @@ -407,21 +519,30 @@ protected override bool Filter(ActionTimelineSelectorEntry item, string search) if(_filterByDrawsWeapon) { - if(item.TimelineType == ActionTimelineSelectorEntry.OriginalType.Emote) + if(isEmote) { if(item.DrawsWeapon != _drawsWeaponValue) return false; } } - if(_filterByEmoteCategory) - { - if(item.TimelineType == ActionTimelineSelectorEntry.OriginalType.Emote) - { - if(item.EmoteCategory != _emoteCategoryValue) - return false; - } - } + if(_filterByEmoteCategory) + { + if(item.TimelineType is ActionTimelineSelectorEntry.OriginalType.Mod or ActionTimelineSelectorEntry.OriginalType.Pose) + return false; + + if(item.TimelineType == ActionTimelineSelectorEntry.OriginalType.Emote) + { + if(item.EmoteCategory != _emoteCategoryValue) + return false; + } + } + + if(_emoteCategoryValue == 4 && item.TimelineType != ActionTimelineSelectorEntry.OriginalType.Mod) + return false; + + if(_emoteCategoryValue == 5 && item.TimelineType != ActionTimelineSelectorEntry.OriginalType.Pose) + return false; return true; } @@ -455,6 +576,8 @@ public enum OriginalType { Raw, Emote, - Action, - } -} + Action, + Mod, + Pose, + } +} diff --git a/Brio/UI/Controls/Selectors/Selector.cs b/Brio/UI/Controls/Selectors/Selector.cs index 1d50ca51..667e5a0e 100644 --- a/Brio/UI/Controls/Selectors/Selector.cs +++ b/Brio/UI/Controls/Selectors/Selector.cs @@ -260,6 +260,17 @@ protected void UpdateList(bool shouldClear = false) }, TaskScheduler.Default); } + protected void ReloadList() + { + _taskQueue = _taskQueue.ContinueWith(_ => + { + _items.Clear(); + PopulateList(); + }, TaskScheduler.Default); + + UpdateList(shouldClear: true); + } + protected virtual bool Filter(T item, string search) { return true;