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.");
+ }
}
}