diff --git a/CustomizePlus/Api/CustomizePlusIpc.Profile.cs b/CustomizePlus/Api/CustomizePlusIpc.Profile.cs index ac04b93..80fc5b1 100644 --- a/CustomizePlus/Api/CustomizePlusIpc.Profile.cs +++ b/CustomizePlus/Api/CustomizePlusIpc.Profile.cs @@ -46,7 +46,7 @@ public partial class CustomizePlusIpc .Select(x => { string path = _profileFileSystem.FindLeaf(x, out var leaf) ? leaf.FullName() : x.Name.Text; - return (x.UniqueId, x.Name.Text, path, x.Characters[0].ToNameWithoutOwnerName(), x.Enabled); //todo: proper update to v5 + return (x.UniqueId, x.Name.Text, path, x.Characters.Count > 0 ? x.Characters[0].ToNameWithoutOwnerName() : "", x.Enabled); //todo: proper update to v5 }) .ToList(); } diff --git a/CustomizePlus/Armatures/Services/ArmatureManager.cs b/CustomizePlus/Armatures/Services/ArmatureManager.cs index 43c1498..19e000e 100644 --- a/CustomizePlus/Armatures/Services/ArmatureManager.cs +++ b/CustomizePlus/Armatures/Services/ArmatureManager.cs @@ -413,6 +413,7 @@ public unsafe sealed class ArmatureManager : IDisposable type is not ProfileChanged.Type.TemporaryProfileDeleted && type is not ProfileChanged.Type.AddedCharacter && type is not ProfileChanged.Type.RemovedCharacter && + type is not ProfileChanged.Type.PriorityChanged && type is not ProfileChanged.Type.ChangedDefaultProfile && type is not ProfileChanged.Type.ChangedDefaultLocalPlayerProfile) return; @@ -438,6 +439,26 @@ public unsafe sealed class ArmatureManager : IDisposable return; } + if(type == ProfileChanged.Type.PriorityChanged) + { + if (!profile.Enabled) + return; + + foreach (var character in profile.Characters) + { + if (!character.IsValid) + continue; + + foreach (var armature in GetArmaturesForCharacter(character)) + { + armature.IsPendingProfileRebind = true; + _logger.Debug($"ArmatureManager.OnProfileChange profile {profile} priority changed, planning rebind for armature {armature}"); + } + } + + return; + } + if (type == ProfileChanged.Type.Toggled) { if (!profile.Enabled && profile.Armatures.Count == 0) diff --git a/CustomizePlus/Core/Data/Constants.cs b/CustomizePlus/Core/Data/Constants.cs index 64feeab..73307e9 100644 --- a/CustomizePlus/Core/Data/Constants.cs +++ b/CustomizePlus/Core/Data/Constants.cs @@ -87,6 +87,7 @@ internal static class Constants internal static class Colors { public static Vector4 Normal = new Vector4(1, 1, 1, 1); + public static Vector4 Info = new Vector4(0.3f, 0.5f, 1f, 1); public static Vector4 Warning = new Vector4(1, 0.5f, 0, 1); public static Vector4 Error = new Vector4(1, 0, 0, 1); } diff --git a/CustomizePlus/Core/Services/CommandService.cs b/CustomizePlus/Core/Services/CommandService.cs index 8d74fb8..5a1194d 100644 --- a/CustomizePlus/Core/Services/CommandService.cs +++ b/CustomizePlus/Core/Services/CommandService.cs @@ -168,6 +168,7 @@ public class CommandService : IDisposable break; } + //todo: support for multiple profiles Profile? targetProfile = null; characterName = subArgumentList[0].Trim(); diff --git a/CustomizePlus/Profiles/Data/Profile.cs b/CustomizePlus/Profiles/Data/Profile.cs index 0d1677f..27b9413 100644 --- a/CustomizePlus/Profiles/Data/Profile.cs +++ b/CustomizePlus/Profiles/Data/Profile.cs @@ -55,6 +55,11 @@ public sealed class Profile : ISavable public ProfileType ProfileType { get; set; } + /// + /// Profile priority when there are several profiles affecting same character + /// + public int Priority { get; set; } + /// /// Tells us if this profile is not persistent (ex. was made via IPC calls) and should have specific treatement like not being shown in UI, etc. /// WARNING, TEMPLATES FOR TEMPORARY PROFILES *ARE NOT* STORED IN TemplateManager @@ -107,6 +112,7 @@ public sealed class Profile : ISavable ["Name"] = Name.Text, ["Enabled"] = Enabled, ["IsWriteProtected"] = IsWriteProtected, + ["Priority"] = Priority, ["Templates"] = SerializeTemplates() }; diff --git a/CustomizePlus/Profiles/Events/ProfileChanged.cs b/CustomizePlus/Profiles/Events/ProfileChanged.cs index 2c74b74..4eb08c8 100644 --- a/CustomizePlus/Profiles/Events/ProfileChanged.cs +++ b/CustomizePlus/Profiles/Events/ProfileChanged.cs @@ -15,6 +15,7 @@ public sealed class ProfileChanged() : EventWrapper 0) - { Profiles.AddRange(temporaryProfiles); - Profiles.Sort((x, y) => y.IsTemporary.CompareTo(x.IsTemporary)); - } var failed = MoveInvalidNames(invalidNames); if (invalidNames.Count > 0) @@ -135,6 +132,8 @@ public partial class ProfileManager : IDisposable { var profile = LoadProfileV4V5(obj); + profile.Priority = obj["Priority"]?.ToObject() ?? throw new ArgumentNullException("Priority"); + if (obj["Characters"] is not JArray characterArray) return profile; diff --git a/CustomizePlus/Profiles/ProfileManager.cs b/CustomizePlus/Profiles/ProfileManager.cs index b2d1d92..4db4f9e 100644 --- a/CustomizePlus/Profiles/ProfileManager.cs +++ b/CustomizePlus/Profiles/ProfileManager.cs @@ -248,51 +248,11 @@ public partial class ProfileManager : IDisposable if (profile.Enabled == value && !force) return; - var oldValue = profile.Enabled; + profile.Enabled = value; - if (value) - { - _logger.Debug($"Setting {profile} as enabled..."); + SaveProfile(profile); - foreach (var otherProfile in Profiles) - { - if (otherProfile == profile || !otherProfile.Enabled || otherProfile.IsTemporary) - continue; - - bool shouldDisable = false; - - //my god this is ugly - foreach(var otherCharacter in otherProfile.Characters) - { - foreach(var currentCharacter in profile.Characters) - { - if(otherCharacter.MatchesIgnoringOwnership(currentCharacter)) - { - shouldDisable = true; - break; - } - } - - if (shouldDisable) - break; - } - - if(shouldDisable) - { - _logger.Debug($"\t-> {otherProfile} disabled"); - SetEnabled(otherProfile, false); - } - } - } - - if (oldValue != value) - { - profile.Enabled = value; - - SaveProfile(profile); - - _event.Invoke(ProfileChanged.Type.Toggled, profile, value); - } + _event.Invoke(ProfileChanged.Type.Toggled, profile, value); } public void SetEnabled(Guid guid, bool value) @@ -306,6 +266,21 @@ public partial class ProfileManager : IDisposable throw new ProfileNotFoundException(); } + public void SetPriority(Profile profile, int value) + { + if (profile.Priority == value) + return; + + if (value > int.MaxValue || value < int.MinValue) + return; + + profile.Priority = value; + + SaveProfile(profile); + + _event.Invoke(ProfileChanged.Type.PriorityChanged, profile, value); + } + public void DeleteTemplate(Profile profile, int templateIndex) { _logger.Debug($"Deleting template #{templateIndex} from {profile}..."); @@ -408,6 +383,7 @@ public partial class ProfileManager : IDisposable profile.Enabled = true; profile.ProfileType = ProfileType.Temporary; + profile.Priority = int.MaxValue; //Make sure temporary profile is always at max priority var permanentIdentifier = identifier.CreatePermanent(); profile.Characters.Clear(); @@ -423,9 +399,6 @@ public partial class ProfileManager : IDisposable Profiles.Add(profile); - //Make sure temporary profiles come first, so they are returned by all other methods first - Profiles.Sort((x, y) => y.IsTemporary.CompareTo(x.IsTemporary)); - _logger.Debug($"Added temporary profile for {permanentIdentifier}"); _event.Invoke(ProfileChanged.Type.TemporaryProfileAdded, profile, null); } @@ -457,7 +430,7 @@ public partial class ProfileManager : IDisposable if (!actor.Identifier(_actorManager, out var identifier)) throw new ActorNotFoundException(); - var profile = Profiles.FirstOrDefault(x => x.Characters[0] == identifier && x.IsTemporary); + var profile = Profiles.FirstOrDefault(x => x.Characters.Count == 1 && x.Characters[0] == identifier && x.IsTemporary); if (profile == null) throw new ProfileNotFoundException(); @@ -478,7 +451,7 @@ public partial class ProfileManager : IDisposable if (enabledOnly) query = query.Where(x => x.Enabled); - var profile = query.FirstOrDefault(); + var profile = query.OrderByDescending(x => x.Priority).FirstOrDefault(); if (profile == null) return null; @@ -529,7 +502,7 @@ public partial class ProfileManager : IDisposable if (_templateEditorManager.IsEditorActive && _templateEditorManager.EditorProfile.Enabled && IsProfileAppliesToCurrentActor(_templateEditorManager.EditorProfile)) yield return _templateEditorManager.EditorProfile; - foreach (var profile in Profiles) + foreach (var profile in Profiles.OrderByDescending(x => x.Priority)) { if(profile.Enabled && IsProfileAppliesToCurrentActor(profile)) yield return profile; @@ -553,7 +526,7 @@ public partial class ProfileManager : IDisposable if (template == null) yield break; - foreach (var profile in Profiles) + foreach (var profile in Profiles.OrderByDescending(x => x.Priority)) if (profile.Templates.Contains(template)) yield return profile; diff --git a/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs b/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs index 2366768..39c8c75 100644 --- a/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs +++ b/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs @@ -63,7 +63,7 @@ public class StateMonitoringTab private void DrawProfiles() { - foreach (var profile in _profileManager.Profiles.OrderByDescending(x => x.Enabled)) + foreach (var profile in _profileManager.Profiles.OrderByDescending(x => x.Enabled).ThenByDescending(x => x.Priority)) { DrawSingleProfile("root", profile); ImGui.Spacing(); @@ -141,7 +141,7 @@ public class StateMonitoringTab //characterName = characterName.Incognify(); #endif - var show = ImGui.CollapsingHeader($"[{(profile.Enabled ? "E" : "D")}] {name} on {characterName} [{profile.ProfileType}] [{profile.UniqueId}]###{prefix}-profile-{profile.UniqueId}"); + var show = ImGui.CollapsingHeader($"[{(profile.Enabled ? "E" : "D")}] [P:{profile.Priority}] {name} on {characterName} [{profile.ProfileType}] [{profile.UniqueId}]###{prefix}-profile-{profile.UniqueId}"); if (!show) return; diff --git a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs index 1cda1e7..2313b8d 100644 --- a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs +++ b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs @@ -241,6 +241,7 @@ public class ProfileFileSystemSelector : FileSystemSelector x.MatchesIgnoringOwnership(identifier)) ? ColorId.LocalCharacterEnabledProfile : ColorId.EnabledProfile; diff --git a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfilePanel.cs b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfilePanel.cs index d8a8ae5..6fd204e 100644 --- a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfilePanel.cs +++ b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfilePanel.cs @@ -18,6 +18,7 @@ using Penumbra.String; using static FFXIVClientStructs.FFXIV.Client.LayoutEngine.ILayoutInstance; using CustomizePlus.GameData.Extensions; using CustomizePlus.Core.Extensions; +using Dalamud.Interface.Components; namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles; @@ -33,6 +34,7 @@ public class ProfilePanel private readonly TemplateEditorEvent _templateEditorEvent; private string? _newName; + private int? _newPriority; private Profile? _changedProfile; private Action? _endAction; @@ -212,6 +214,30 @@ public class ProfilePanel } else ImGui.TextUnformatted(_selector.Selected!.Incognito); + + ImGui.TableNextRow(); + + ImGuiUtil.DrawFrameColumn("Priority"); + ImGui.TableNextColumn(); + + var priority = _newPriority ?? _selector.Selected!.Priority; + + ImGui.SetNextItemWidth(50); + if (ImGui.InputInt("##Priority", ref priority, 0, 0)) + { + _newPriority = priority; + _changedProfile = _selector.Selected; + } + + if (ImGui.IsItemDeactivatedAfterEdit() && _changedProfile != null) + { + _manager.SetPriority(_changedProfile, priority); + _newPriority = null; + _changedProfile = null; + } + + ImGuiComponents.HelpMarker("Profiles with a higher number here take precedence before profiles with a lower number.\n" + + "That means if two or more profiles affect same character, profile with higher priority will be applied to that character."); } } } @@ -345,6 +371,36 @@ public class ProfilePanel ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(!_selector.IncognitoMode ? $"{character.ToNameWithoutOwnerName()}{character.TypeToString()}" : "Incognito"); + + var profiles = _manager.GetEnabledProfilesByActor(character).ToList(); + if (profiles.Count > 1) + { + //todo: make helper + ImGui.SameLine(); + if(profiles.Any(x => x.IsTemporary)) + { + ImGui.PushStyleColor(ImGuiCol.Text, Constants.Colors.Error); + ImGuiUtil.PrintIcon(FontAwesomeIcon.Lock); + } + else if (profiles[0] != _selector.Selected!) + { + ImGui.PushStyleColor(ImGuiCol.Text, Constants.Colors.Warning); + ImGuiUtil.PrintIcon(FontAwesomeIcon.ExclamationTriangle); + } + else + { + ImGui.PushStyleColor(ImGuiCol.Text, Constants.Colors.Info); + ImGuiUtil.PrintIcon(FontAwesomeIcon.Star); + } + + ImGui.PopStyleColor(); + + if (profiles.Any(x => x.IsTemporary)) + ImGuiUtil.HoverTooltip("This character is being affected by temporary profile set by external plugin. This profile will not be applied!"); + else + ImGuiUtil.HoverTooltip(profiles[0] != _selector.Selected! ? "Several profiles are trying to affect this character. This profile will not be applied!" : + "Several profiles are trying to affect this character. This profile is being applied."); + } } }