From 7085cf616cdb62a690e3929a84332ebde8d812d8 Mon Sep 17 00:00:00 2001
From: RisaDev <151885272+RisaDev@users.noreply.github.com>
Date: Sat, 19 Oct 2024 02:55:38 +0300
Subject: [PATCH] Added ability to apply profile to several characters
---
.../Extensions/ActorIdentifierExtensions.cs | 17 ++
CustomizePlus/Api/CustomizePlusIpc.Profile.cs | 2 +-
CustomizePlus/Api/Data/IPCCharacterProfile.cs | 2 +-
.../Armatures/Services/ArmatureManager.cs | 42 +--
CustomizePlus/Core/Services/CommandService.cs | 6 +-
.../Core/Services/SupportLogBuilderService.cs | 2 +-
CustomizePlus/Profiles/Data/Profile.cs | 20 +-
.../Profiles/Events/ProfileChanged.cs | 4 +-
.../Profiles/ProfileManager.ProfileLoading.cs | 38 ++-
CustomizePlus/Profiles/ProfileManager.cs | 88 +++++--
.../Templates/TemplateEditorManager.cs | 10 +-
.../Tabs/Debug/StateMonitoringTab.cs | 5 +-
.../Profiles/ProfileFileSystemSelector.cs | 11 +-
.../MainWindow/Tabs/Profiles/ProfilePanel.cs | 241 +++++++++++-------
14 files changed, 324 insertions(+), 164 deletions(-)
diff --git a/CustomizePlus.GameData/Extensions/ActorIdentifierExtensions.cs b/CustomizePlus.GameData/Extensions/ActorIdentifierExtensions.cs
index 42d7eb1..dd5a713 100644
--- a/CustomizePlus.GameData/Extensions/ActorIdentifierExtensions.cs
+++ b/CustomizePlus.GameData/Extensions/ActorIdentifierExtensions.cs
@@ -85,6 +85,23 @@ public static class ActorIdentifierExtensions
}
}
+ public static string TypeToString(this ActorIdentifier identifier)
+ {
+ return identifier.Type switch
+ {
+ IdentifierType.Player => $" ({PenumbraExtensions.Manager?.Data.ToWorldName(identifier.HomeWorld) ?? "Unknown"})",
+ IdentifierType.Retainer => $"{identifier.Retainer switch
+ {
+ ActorIdentifier.RetainerType.Bell => " (Bell)",
+ ActorIdentifier.RetainerType.Mannequin => " (Mannequin)",
+ _ => " (Retainer)",
+ }}",
+ IdentifierType.Owned => " (Companion/Mount)",
+ IdentifierType.Npc => " (NPC)",
+ _ => "",
+ };
+ }
+
///
/// For now used to determine if root scaling should be allowed or not
///
diff --git a/CustomizePlus/Api/CustomizePlusIpc.Profile.cs b/CustomizePlus/Api/CustomizePlusIpc.Profile.cs
index 6dca450..ac04b93 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.Character.ToNameWithoutOwnerName(), x.Enabled); //todo: proper update to v5
+ return (x.UniqueId, x.Name.Text, path, x.Characters[0].ToNameWithoutOwnerName(), x.Enabled); //todo: proper update to v5
})
.ToList();
}
diff --git a/CustomizePlus/Api/Data/IPCCharacterProfile.cs b/CustomizePlus/Api/Data/IPCCharacterProfile.cs
index b9a1988..f3066ae 100644
--- a/CustomizePlus/Api/Data/IPCCharacterProfile.cs
+++ b/CustomizePlus/Api/Data/IPCCharacterProfile.cs
@@ -29,7 +29,7 @@ public class IPCCharacterProfile
{
var ipcProfile = new IPCCharacterProfile
{
- CharacterName = profile.Character.ToNameWithoutOwnerName(),
+ CharacterName = profile.Characters.FirstOrDefault().ToNameWithoutOwnerName(),
Bones = new Dictionary()
};
diff --git a/CustomizePlus/Armatures/Services/ArmatureManager.cs b/CustomizePlus/Armatures/Services/ArmatureManager.cs
index 1242e00..43c1498 100644
--- a/CustomizePlus/Armatures/Services/ArmatureManager.cs
+++ b/CustomizePlus/Armatures/Services/ArmatureManager.cs
@@ -411,7 +411,8 @@ public unsafe sealed class ArmatureManager : IDisposable
type is not ProfileChanged.Type.Deleted &&
type is not ProfileChanged.Type.TemporaryProfileAdded &&
type is not ProfileChanged.Type.TemporaryProfileDeleted &&
- type is not ProfileChanged.Type.ChangedCharacter &&
+ type is not ProfileChanged.Type.AddedCharacter &&
+ type is not ProfileChanged.Type.RemovedCharacter &&
type is not ProfileChanged.Type.ChangedDefaultProfile &&
type is not ProfileChanged.Type.ChangedDefaultLocalPlayerProfile)
return;
@@ -456,13 +457,16 @@ public unsafe sealed class ArmatureManager : IDisposable
return;
}
- if (!profile.Character.IsValid)
- return;
-
- foreach (var armature in GetArmaturesForCharacter(profile.Character))
+ foreach(var character in profile.Characters)
{
- armature.IsPendingProfileRebind = true;
- _logger.Debug($"ArmatureManager.OnProfileChange profile {profile} toggled, planning rebind for armature {armature}");
+ if (!character.IsValid)
+ continue;
+
+ foreach (var armature in GetArmaturesForCharacter(character))
+ {
+ armature.IsPendingProfileRebind = true;
+ _logger.Debug($"ArmatureManager.OnProfileChange profile {profile} toggled, planning rebind for armature {armature}");
+ }
}
return;
@@ -470,9 +474,6 @@ public unsafe sealed class ArmatureManager : IDisposable
if (type == ProfileChanged.Type.TemporaryProfileAdded)
{
- if (!profile.Character.IsValid || !Armatures.ContainsKey(profile.Character)) //temporary profiles are never using AnyWorld identifiers so we should be fine here
- return;
-
//todo: remove this later
/*Armature? armature = null;
foreach(var kvPair in Armatures)
@@ -489,21 +490,28 @@ public unsafe sealed class ArmatureManager : IDisposable
if (armature == null)
return;*/
- var armature = Armatures[profile.Character];
+ foreach(var character in profile.Characters)
+ {
+ if (!character.IsValid || !Armatures.ContainsKey(character))
+ continue;
- if (armature.Profile == profile)
- return;
+ var armature = Armatures[character];
- armature.UpdateLastSeen();
+ if (armature.Profile == profile)
+ return;
- armature.IsPendingProfileRebind = true;
+ armature.UpdateLastSeen();
+
+ armature.IsPendingProfileRebind = true;
+ }
_logger.Debug($"ArmatureManager.OnProfileChange TemporaryProfileAdded, calling rebind for existing armature: {type}, data payload: {arg3?.ToString()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
return;
}
- if (type == ProfileChanged.Type.ChangedCharacter ||
+ if (type == ProfileChanged.Type.AddedCharacter ||
+ type == ProfileChanged.Type.RemovedCharacter ||
type == ProfileChanged.Type.Deleted ||
type == ProfileChanged.Type.TemporaryProfileDeleted)
{
@@ -518,7 +526,7 @@ public unsafe sealed class ArmatureManager : IDisposable
armature.IsPendingProfileRebind = true;
}
- _logger.Debug($"ArmatureManager.OnProfileChange CC/DEL/TPD/ATCACC, armature rebind scheduled: {type}, data payload: {arg3?.ToString()?.Incognify()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
+ _logger.Debug($"ArmatureManager.OnProfileChange AC/RC/DEL/TPD/ATCACC, armature rebind scheduled: {type}, data payload: {arg3?.ToString()?.Incognify()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
return;
}
diff --git a/CustomizePlus/Core/Services/CommandService.cs b/CustomizePlus/Core/Services/CommandService.cs
index 1002393..8d74fb8 100644
--- a/CustomizePlus/Core/Services/CommandService.cs
+++ b/CustomizePlus/Core/Services/CommandService.cs
@@ -183,10 +183,10 @@ public class CommandService : IDisposable
if (!isTurningOffAllProfiles)
{
profileName = subArgumentList[1].Trim();
- targetProfile = _profileManager.Profiles.FirstOrDefault(x => x.Name == profileName && x.Character.ToNameWithoutOwnerName() == characterName);
+ targetProfile = _profileManager.Profiles.FirstOrDefault(x => x.Name == profileName && x.Characters.Any(x => x.ToNameWithoutOwnerName() == characterName));
}
else
- targetProfile = _profileManager.Profiles.FirstOrDefault(x => x.Character.ToNameWithoutOwnerName() == characterName && x.Enabled);
+ targetProfile = _profileManager.Profiles.FirstOrDefault(x => x.Characters.Any(x => x.ToNameWithoutOwnerName() == characterName) && x.Enabled);
if (targetProfile == null)
{
@@ -225,7 +225,7 @@ public class CommandService : IDisposable
.AddText(" was successfully ")
.AddBlue(state != null ? ((bool)state ? "enabled" : "disabled") : "toggled")
.AddText(" for ")
- .AddRed(targetProfile.Character.ToNameWithoutOwnerName()).BuiltString);
+ .AddRed(string.Join(',', targetProfile.Characters.Select(x => x.ToNameWithoutOwnerName()))).BuiltString);
}
catch (Exception e)
{
diff --git a/CustomizePlus/Core/Services/SupportLogBuilderService.cs b/CustomizePlus/Core/Services/SupportLogBuilderService.cs
index 89ce76a..0114fff 100644
--- a/CustomizePlus/Core/Services/SupportLogBuilderService.cs
+++ b/CustomizePlus/Core/Services/SupportLogBuilderService.cs
@@ -68,7 +68,7 @@ public class SupportLogBuilderService
sb.Append($"> > **`{profile.ToString(),-32}`*\n");
sb.Append($"> > **`Name: `** {profile.Name.Text.Incognify()}\n");
sb.Append($"> > **`Type: `** {profile.ProfileType} \n");
- sb.Append($"> > **`Character name: `** {profile.Character.Incognito(null)}\n");
+ sb.Append($"> > **`Characters: `** {string.Join(',', profile.Characters.Select(x => x.Incognito(null)))}\n");
sb.Append($"> > **`Templates:`**\n");
sb.Append($"> > > **`Count: `** {profile.Templates.Count}\n");
foreach (var template in profile.Templates)
diff --git a/CustomizePlus/Profiles/Data/Profile.cs b/CustomizePlus/Profiles/Data/Profile.cs
index ed6154b..0d1677f 100644
--- a/CustomizePlus/Profiles/Data/Profile.cs
+++ b/CustomizePlus/Profiles/Data/Profile.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using CustomizePlus.Armatures.Data;
using CustomizePlus.Core.Data;
using CustomizePlus.Core.Extensions;
@@ -32,7 +33,8 @@ public sealed class Profile : ISavable
/* [Obsolete("To be removed in the future versions")]
public LowerString CharacterName { get; set; } = LowerString.Empty;*/
- public ActorIdentifier Character { get; set; } = ActorIdentifier.Invalid;
+ //public ActorIdentifier Character { get; set; } = ActorIdentifier.Invalid;
+ public List Characters { get; set; } = new();
public LowerString Name { get; set; } = LowerString.Empty;
@@ -78,7 +80,7 @@ public sealed class Profile : ISavable
///
public Profile(Profile original) : this()
{
- Character = original.Character;
+ Characters = original.Characters.ToList();
foreach (var template in original.Templates)
{
@@ -88,7 +90,7 @@ public sealed class Profile : ISavable
public override string ToString()
{
- return $"Profile '{Name.Text.Incognify()}' on {Character.Incognito(null)} [{UniqueId}]";
+ return $"Profile '{Name.Text.Incognify()}' on {string.Join(',', Characters.Select(x => x.Incognito(null)))} [{UniqueId}]";
}
#region Serialization
@@ -101,7 +103,7 @@ public sealed class Profile : ISavable
["UniqueId"] = UniqueId,
["CreationDate"] = CreationDate,
["ModifiedDate"] = ModifiedDate,
- ["Character"] = Character.ToJson(),
+ ["Characters"] = SerializeCharacters(),
["Name"] = Name.Text,
["Enabled"] = Enabled,
["IsWriteProtected"] = IsWriteProtected,
@@ -124,6 +126,16 @@ public sealed class Profile : ISavable
return ret;
}
+ private JArray SerializeCharacters()
+ {
+ var ret = new JArray();
+ foreach (var character in Characters)
+ {
+ ret.Add(character.ToJson());
+ }
+ return ret;
+ }
+
#endregion
//Loading is in ProfileManager
diff --git a/CustomizePlus/Profiles/Events/ProfileChanged.cs b/CustomizePlus/Profiles/Events/ProfileChanged.cs
index 1df3200..2c74b74 100644
--- a/CustomizePlus/Profiles/Events/ProfileChanged.cs
+++ b/CustomizePlus/Profiles/Events/ProfileChanged.cs
@@ -15,7 +15,9 @@ public sealed class ProfileChanged() : EventWrapper 0)
{
- _logger.Debug($"Upgraded profile \"{profile.Name}\" to V5: {characterName} -> {profile.Character}. Save queued.");
+ _logger.Debug($"Upgraded profile \"{profile.Name}\" to V5: {characterName} -> {profile.Characters[0]}. Save queued.");
_saveService.QueueSave(profile);
}
@@ -135,14 +135,32 @@ public partial class ProfileManager : IDisposable
{
var profile = LoadProfileV4V5(obj);
- var character = _actorManager.FromJson(obj["Character"] as JObject);
+ if (obj["Characters"] is not JArray characterArray)
+ return profile;
- profile.Character = character;
+ foreach(var characterObj in characterArray)
+ {
+ if (characterObj is not JObject characterObjCast)
+ {
+ //todo: warning
+ continue;
+ }
+
+ var character = _actorManager.FromJson(characterObjCast);
+
+ if(!character.IsValid)
+ {
+ //todo: warning
+ continue;
+ }
+
+ profile.Characters.Add(character);
+ }
return profile;
}
- //V4 and V5 are mostly not different, so common loading logic is here
+ //V4 and V5 are mostly the same, so common loading logic is here
private Profile LoadProfileV4V5(JObject obj)
{
var creationDate = obj["CreationDate"]?.ToObject() ?? throw new ArgumentNullException("CreationDate");
diff --git a/CustomizePlus/Profiles/ProfileManager.cs b/CustomizePlus/Profiles/ProfileManager.cs
index 802731c..b2d1d92 100644
--- a/CustomizePlus/Profiles/ProfileManager.cs
+++ b/CustomizePlus/Profiles/ProfileManager.cs
@@ -177,15 +177,14 @@ public partial class ProfileManager : IDisposable
}
///
- /// Change character associated with profile
+ /// Add character to profile
///
- public void ChangeCharacter(Profile profile, ActorIdentifier actorIdentifier)
+ public void AddCharacter(Profile profile, ActorIdentifier actorIdentifier)
{
- if (!actorIdentifier.IsValid || actorIdentifier.MatchesIgnoringOwnership(profile.Character))
+ if (!actorIdentifier.IsValid || profile.Characters.Any(x => actorIdentifier.MatchesIgnoringOwnership(x)) || profile.IsTemporary)
return;
- var oldCharacter = profile.Character;
- profile.Character = actorIdentifier;
+ profile.Characters.Add(actorIdentifier);
//Called so all other active profiles for new character name get disabled
//saving is performed there
@@ -193,8 +192,28 @@ public partial class ProfileManager : IDisposable
SaveProfile(profile);
- _logger.Debug($"Changed character for profile {profile.UniqueId}.");
- _event.Invoke(ProfileChanged.Type.ChangedCharacter, profile, oldCharacter);
+ _logger.Debug($"Add character for profile {profile.UniqueId}.");
+ _event.Invoke(ProfileChanged.Type.AddedCharacter, profile, actorIdentifier);
+ }
+
+ ///
+ /// Delete character from profile
+ ///
+ public void DeleteCharacter(Profile profile, ActorIdentifier actorIdentifier)
+ {
+ if (!actorIdentifier.IsValid || !profile.Characters.Any(x => actorIdentifier.MatchesIgnoringOwnership(x)) || profile.IsTemporary)
+ return;
+
+ profile.Characters.Remove(actorIdentifier);
+
+ //Called so all other active profiles for new character name get disabled
+ //saving is performed there
+ //SetEnabled(profile, profile.Enabled, true); //todo
+
+ SaveProfile(profile);
+
+ _logger.Debug($"Removed character from profile {profile.UniqueId}.");
+ _event.Invoke(ProfileChanged.Type.RemovedCharacter, profile, actorIdentifier);
}
///
@@ -235,11 +254,34 @@ public partial class ProfileManager : IDisposable
{
_logger.Debug($"Setting {profile} as enabled...");
- foreach (var otherProfile in Profiles
- .Where(x => x.Character.MatchesIgnoringOwnership(profile.Character) && x != profile && x.Enabled && !x.IsTemporary))
+ foreach (var otherProfile in Profiles)
{
- _logger.Debug($"\t-> {otherProfile} disabled");
- SetEnabled(otherProfile, false);
+ 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);
+ }
}
}
@@ -366,12 +408,15 @@ public partial class ProfileManager : IDisposable
profile.Enabled = true;
profile.ProfileType = ProfileType.Temporary;
- profile.Character = identifier.CreatePermanent(); //warn: identifier must not be AnyWorld or stuff will break!
- var existingProfile = Profiles.FirstOrDefault(x => x.Character.MatchesIgnoringOwnership(profile.Character) && x.IsTemporary);
+ var permanentIdentifier = identifier.CreatePermanent();
+ profile.Characters.Clear();
+ profile.Characters.Add(permanentIdentifier); //warn: identifier must not be AnyWorld or stuff will break!
+
+ var existingProfile = Profiles.FirstOrDefault(p => p.Characters.Count == 1 && p.Characters[0].MatchesIgnoringOwnership(permanentIdentifier) && p.IsTemporary);
if (existingProfile != null)
{
- _logger.Debug($"Temporary profile for {existingProfile.Character.Incognito(null)} already exists, removing...");
+ _logger.Debug($"Temporary profile for {permanentIdentifier.Incognito(null)} already exists, removing...");
Profiles.Remove(existingProfile);
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, existingProfile, null);
}
@@ -381,16 +426,19 @@ public partial class ProfileManager : IDisposable
//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 {profile.Character}");
+ _logger.Debug($"Added temporary profile for {permanentIdentifier}");
_event.Invoke(ProfileChanged.Type.TemporaryProfileAdded, profile, null);
}
public void RemoveTemporaryProfile(Profile profile)
{
+ if (!profile.IsTemporary)
+ return;
+
if (!Profiles.Remove(profile))
throw new ProfileNotFoundException();
- _logger.Debug($"Removed temporary profile for {profile.Character.Incognito(null)}");
+ _logger.Debug($"Removed temporary profile for {profile.Characters[0].Incognito(null)}");
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, profile, null);
}
@@ -409,7 +457,7 @@ public partial class ProfileManager : IDisposable
if (!actor.Identifier(_actorManager, out var identifier))
throw new ActorNotFoundException();
- var profile = Profiles.FirstOrDefault(x => x.Character == identifier && x.IsTemporary);
+ var profile = Profiles.FirstOrDefault(x => x.Characters[0] == identifier && x.IsTemporary);
if (profile == null)
throw new ProfileNotFoundException();
@@ -426,7 +474,7 @@ public partial class ProfileManager : IDisposable
if (!actorIdentifier.IsValid)
return null;
- var query = Profiles.Where(x => x.Character.MatchesIgnoringOwnership(actorIdentifier) && !x.IsTemporary);
+ var query = Profiles.Where(p => p.Characters.Any(x => x.MatchesIgnoringOwnership(actorIdentifier)) && !p.IsTemporary);
if (enabledOnly)
query = query.Where(x => x.Enabled);
@@ -475,7 +523,7 @@ public partial class ProfileManager : IDisposable
if (actorIdentifier.Type == IdentifierType.Owned && !actorIdentifier.IsOwnedByLocalPlayer())
return false;
- return profile.Character.MatchesIgnoringOwnership(actorIdentifier);
+ return profile.Characters.Any(x => x.MatchesIgnoringOwnership(actorIdentifier));
}
if (_templateEditorManager.IsEditorActive && _templateEditorManager.EditorProfile.Enabled && IsProfileAppliesToCurrentActor(_templateEditorManager.EditorProfile))
@@ -585,7 +633,7 @@ public partial class ProfileManager : IDisposable
if (!Profiles.Remove(profile))
return;
- _logger.Debug($"ProfileManager.OnArmatureChange: Removed unused temporary profile for {profile.Character.Incognito(null)}");
+ _logger.Debug($"ProfileManager.OnArmatureChange: Removed unused temporary profile for {profile.Characters[0].Incognito(null)}");
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, profile, null);
}
diff --git a/CustomizePlus/Templates/TemplateEditorManager.cs b/CustomizePlus/Templates/TemplateEditorManager.cs
index c16aab5..953ad5c 100644
--- a/CustomizePlus/Templates/TemplateEditorManager.cs
+++ b/CustomizePlus/Templates/TemplateEditorManager.cs
@@ -65,7 +65,7 @@ public class TemplateEditorManager : IDisposable
///
/// Name of the preview character for the editor
///
- public ActorIdentifier Character => EditorProfile.Character;
+ public ActorIdentifier Character => EditorProfile.Characters[0];
///
/// Checks if preview character exists at the time of call
@@ -107,8 +107,9 @@ public class TemplateEditorManager : IDisposable
Enabled = false,
Name = "Template editor profile",
ProfileType = ProfileType.Editor,
- Character = configuration.EditorConfiguration.PreviewCharacter
};
+
+ EditorProfile.Characters.Add(configuration.EditorConfiguration.PreviewCharacter);
}
public void Dispose()
@@ -194,9 +195,10 @@ public class TemplateEditorManager : IDisposable
private bool ChangeEditorCharacterInternal(ActorIdentifier character)
{
- _logger.Debug($"Changing character name for editor profile from {EditorProfile.Character.Incognito(null)} to {character.Incognito(null)}");
+ _logger.Debug($"Changing character name for editor profile from {EditorProfile.Characters.FirstOrDefault().Incognito(null)} to {character.Incognito(null)}");
- EditorProfile.Character = character;
+ EditorProfile.Characters.Clear();
+ EditorProfile.Characters.Add(character);
_configuration.EditorConfiguration.PreviewCharacter = character;
_configuration.Save();
diff --git a/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs b/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs
index bdfc28c..2366768 100644
--- a/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs
+++ b/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs
@@ -134,12 +134,11 @@ public class StateMonitoringTab
private void DrawSingleProfile(string prefix, Profile profile)
{
string name = profile.Name;
- string characterName = profile.Character.Type == Penumbra.GameData.Enums.IdentifierType.Owned ?
- profile.Character.ToNameWithoutOwnerName() : profile.Character.ToString();
+ string characterName = string.Join(',', profile.Characters.Select(x => x.ToNameWithoutOwnerName().Incognify()));
#if INCOGNIFY_STRINGS
name = name.Incognify();
- characterName = characterName.Incognify();
+ //characterName = characterName.Incognify();
#endif
var show = ImGui.CollapsingHeader($"[{(profile.Enabled ? "E" : "D")}] {name} on {characterName} [{profile.ProfileType}] [{profile.UniqueId}]###{prefix}-profile-{profile.UniqueId}");
diff --git a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs
index 03019d8..1cda1e7 100644
--- a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs
+++ b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs
@@ -17,6 +17,7 @@ using CustomizePlus.Profiles.Data;
using CustomizePlus.Game.Services;
using CustomizePlus.Profiles.Events;
using CustomizePlus.GameData.Extensions;
+using System.Linq;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
@@ -133,7 +134,8 @@ public class ProfileFileSystemSelector : FileSystemSelector x.MatchesIgnoringOwnership(identifier)) ? ColorId.LocalCharacterEnabledProfile : ColorId.EnabledProfile;
else
- state.Color = leaf.Value.Character.MatchesIgnoringOwnership(_gameObjectService.GetCurrentPlayerActorIdentifier()) ? ColorId.LocalCharacterDisabledProfile : ColorId.DisabledProfile;
+ state.Color = leaf.Value.Characters.Any(x => x.MatchesIgnoringOwnership(identifier)) ? ColorId.LocalCharacterDisabledProfile : ColorId.DisabledProfile;
+
+ //todo: missing actor color
return ApplyStringFilters(leaf, leaf.Value);
}
diff --git a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfilePanel.cs b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfilePanel.cs
index 656f538..d8a8ae5 100644
--- a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfilePanel.cs
+++ b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfilePanel.cs
@@ -17,6 +17,7 @@ using Penumbra.GameData.Actors;
using Penumbra.String;
using static FFXIVClientStructs.FFXIV.Client.LayoutEngine.ILayoutInstance;
using CustomizePlus.GameData.Extensions;
+using CustomizePlus.Core.Extensions;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
@@ -151,6 +152,13 @@ public class ProfilePanel
ImGui.Separator();
+ var isShouldDraw = ImGui.CollapsingHeader("Character settings");
+
+ if (isShouldDraw)
+ DrawCharacterArea();
+
+ ImGui.Separator();
+
DrawTemplateArea();
}
}
@@ -204,108 +212,149 @@ public class ProfilePanel
}
else
ImGui.TextUnformatted(_selector.Selected!.Incognito);
-
- ImGui.TableNextRow();
-
- ImGuiUtil.DrawFrameColumn("Character");
- ImGui.TableNextColumn();
- width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0);
-
- ImGui.SetNextItemWidth(width.X);
-
- if (!_selector.IncognitoMode)
- {
- bool showMultipleMessage = false;
- if (_manager.DefaultProfile != _selector.Selected && _manager.DefaultLocalPlayerProfile != _selector.Selected)
- {
- ImGui.Text(_selector.Selected!.Character.IsValid ? $"Applies to {(_selector.Selected?.Character.Type == Penumbra.GameData.Enums.IdentifierType.Owned ?
- _selector.Selected?.Character.ToNameWithoutOwnerName() : _selector.Selected?.Character.ToString())}" : "No valid character selected for the profile");
-
- ImGui.Separator();
-
- _actorAssignmentUi.DrawWorldCombo(width.X / 2);
- ImGui.SameLine();
- _actorAssignmentUi.DrawPlayerInput(width.X / 2);
-
- var buttonWidth = new Vector2(165 * ImGuiHelpers.GlobalScale - ImGui.GetStyle().ItemSpacing.X / 2, 0);
-
- if (ImGuiUtil.DrawDisabledButton("Apply to player character", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetPlayer))
- _manager.ChangeCharacter(_selector.Selected!, _actorAssignmentUi.PlayerIdentifier);
-
- ImGui.SameLine();
-
- if (ImGuiUtil.DrawDisabledButton("Apply to retainer", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetRetainer))
- _manager.ChangeCharacter(_selector.Selected!, _actorAssignmentUi.RetainerIdentifier);
-
- ImGui.SameLine();
-
- if (ImGuiUtil.DrawDisabledButton("Apply to mannequin", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetMannequin))
- _manager.ChangeCharacter(_selector.Selected!, _actorAssignmentUi.MannequinIdentifier);
-
- var currentPlayer = _actorManager.GetCurrentPlayer();
- if (ImGuiUtil.DrawDisabledButton("Apply to current character", buttonWidth, string.Empty, !currentPlayer.IsValid))
- _manager.ChangeCharacter(_selector.Selected!, currentPlayer);
-
- ImGui.Separator();
-
- _actorAssignmentUi.DrawObjectKindCombo(width.X / 2);
- ImGui.SameLine();
- _actorAssignmentUi.DrawNpcInput(width.X / 2);
-
- if (ImGuiUtil.DrawDisabledButton("Apply to selected NPC", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetNpc))
- _manager.ChangeCharacter(_selector.Selected!, _actorAssignmentUi.NpcIdentifier);
-
- }
- else
- ImGui.TextUnformatted("Applies to multiple targets");
- }
- else
- ImGui.TextUnformatted("Incognito active");
-
- ImGui.Separator();
-
- var isDefaultLP = _manager.DefaultLocalPlayerProfile == _selector.Selected;
- var isDefaultLPOrCurrentProfilesEnabled = (_manager.DefaultLocalPlayerProfile?.Enabled ?? false) || (_selector.Selected?.Enabled ?? false);
- using (ImRaii.Disabled(isDefaultLPOrCurrentProfilesEnabled))
- {
- if (ImGui.Checkbox("##DefaultLocalPlayerProfile", ref isDefaultLP))
- _manager.SetDefaultLocalPlayerProfile(isDefaultLP ? _selector.Selected! : null);
- ImGuiUtil.LabeledHelpMarker("Apply to any character you are logged in with",
- "Whether the templates in this profile should be applied to any character you are currently logged in with.\r\nTakes priority over the next option for said character.\r\nThis setting cannot be applied to multiple profiles.");
- }
- if (isDefaultLPOrCurrentProfilesEnabled)
- {
- ImGui.SameLine();
- ImGui.PushStyleColor(ImGuiCol.Text, Constants.Colors.Warning);
- ImGuiUtil.PrintIcon(FontAwesomeIcon.ExclamationTriangle);
- ImGui.PopStyleColor();
- ImGuiUtil.HoverTooltip("Can only be changed when both currently selected and profile where this checkbox is checked are disabled.");
- }
-
- var isDefault = _manager.DefaultProfile == _selector.Selected;
- var isDefaultOrCurrentProfilesEnabled = (_manager.DefaultProfile?.Enabled ?? false) || (_selector.Selected?.Enabled ?? false);
- using (ImRaii.Disabled(isDefaultOrCurrentProfilesEnabled))
- {
- if (ImGui.Checkbox("##DefaultProfile", ref isDefault))
- _manager.SetDefaultProfile(isDefault ? _selector.Selected! : null);
- ImGuiUtil.LabeledHelpMarker("Apply to all players and retainers",
- "Whether the templates in this profile are applied to all players and retainers without a specific profile.\r\nThis setting cannot be applied to multiple profiles.");
- }
- if (isDefaultOrCurrentProfilesEnabled)
- {
- ImGui.SameLine();
- ImGui.PushStyleColor(ImGuiCol.Text, Constants.Colors.Warning);
- ImGuiUtil.PrintIcon(FontAwesomeIcon.ExclamationTriangle);
- ImGui.PopStyleColor();
- ImGuiUtil.HoverTooltip("Can only be changed when both currently selected and profile where this checkbox is checked are disabled.");
- }
}
}
}
+ private void DrawCharacterArea()
+ {
+ using (var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)))
+ {
+ var width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0);
+
+ ImGui.SetNextItemWidth(width.X);
+
+ bool appliesToMultiple = _manager.DefaultProfile == _selector.Selected || _manager.DefaultLocalPlayerProfile == _selector.Selected;
+ using (ImRaii.Disabled(appliesToMultiple))
+ {
+ _actorAssignmentUi.DrawWorldCombo(width.X / 2);
+ ImGui.SameLine();
+ _actorAssignmentUi.DrawPlayerInput(width.X / 2);
+
+ var buttonWidth = new Vector2(165 * ImGuiHelpers.GlobalScale - ImGui.GetStyle().ItemSpacing.X / 2, 0);
+
+ if (ImGuiUtil.DrawDisabledButton("Apply to player character", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetPlayer))
+ _manager.AddCharacter(_selector.Selected!, _actorAssignmentUi.PlayerIdentifier);
+
+ ImGui.SameLine();
+
+ if (ImGuiUtil.DrawDisabledButton("Apply to retainer", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetRetainer))
+ _manager.AddCharacter(_selector.Selected!, _actorAssignmentUi.RetainerIdentifier);
+
+ ImGui.SameLine();
+
+ if (ImGuiUtil.DrawDisabledButton("Apply to mannequin", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetMannequin))
+ _manager.AddCharacter(_selector.Selected!, _actorAssignmentUi.MannequinIdentifier);
+
+ var currentPlayer = _actorManager.GetCurrentPlayer();
+ if (ImGuiUtil.DrawDisabledButton("Apply to current character", buttonWidth, string.Empty, !currentPlayer.IsValid))
+ _manager.AddCharacter(_selector.Selected!, currentPlayer);
+
+ ImGui.Separator();
+
+ _actorAssignmentUi.DrawObjectKindCombo(width.X / 2);
+ ImGui.SameLine();
+ _actorAssignmentUi.DrawNpcInput(width.X / 2);
+
+ if (ImGuiUtil.DrawDisabledButton("Apply to selected NPC", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetNpc))
+ _manager.AddCharacter(_selector.Selected!, _actorAssignmentUi.NpcIdentifier);
+ }
+
+ ImGui.Separator();
+
+ var isDefaultLP = _manager.DefaultLocalPlayerProfile == _selector.Selected;
+ var isDefaultLPOrCurrentProfilesEnabled = (_manager.DefaultLocalPlayerProfile?.Enabled ?? false) || (_selector.Selected?.Enabled ?? false);
+ using (ImRaii.Disabled(isDefaultLPOrCurrentProfilesEnabled))
+ {
+ if (ImGui.Checkbox("##DefaultLocalPlayerProfile", ref isDefaultLP))
+ _manager.SetDefaultLocalPlayerProfile(isDefaultLP ? _selector.Selected! : null);
+ ImGuiUtil.LabeledHelpMarker("Apply to any character you are logged in with",
+ "Whether the templates in this profile should be applied to any character you are currently logged in with.\r\nTakes priority over the next option for said character.\r\nThis setting cannot be applied to multiple profiles.");
+ }
+ if (isDefaultLPOrCurrentProfilesEnabled)
+ {
+ ImGui.SameLine();
+ ImGui.PushStyleColor(ImGuiCol.Text, Constants.Colors.Warning);
+ ImGuiUtil.PrintIcon(FontAwesomeIcon.ExclamationTriangle);
+ ImGui.PopStyleColor();
+ ImGuiUtil.HoverTooltip("Can only be changed when both currently selected and profile where this checkbox is checked are disabled.");
+ }
+
+ var isDefault = _manager.DefaultProfile == _selector.Selected;
+ var isDefaultOrCurrentProfilesEnabled = (_manager.DefaultProfile?.Enabled ?? false) || (_selector.Selected?.Enabled ?? false);
+ using (ImRaii.Disabled(isDefaultOrCurrentProfilesEnabled))
+ {
+ if (ImGui.Checkbox("##DefaultProfile", ref isDefault))
+ _manager.SetDefaultProfile(isDefault ? _selector.Selected! : null);
+ ImGuiUtil.LabeledHelpMarker("Apply to all players and retainers",
+ "Whether the templates in this profile are applied to all players and retainers without a specific profile.\r\nThis setting cannot be applied to multiple profiles.");
+ }
+ if (isDefaultOrCurrentProfilesEnabled)
+ {
+ ImGui.SameLine();
+ ImGui.PushStyleColor(ImGuiCol.Text, Constants.Colors.Warning);
+ ImGuiUtil.PrintIcon(FontAwesomeIcon.ExclamationTriangle);
+ ImGui.PopStyleColor();
+ ImGuiUtil.HoverTooltip("Can only be changed when both currently selected and profile where this checkbox is checked are disabled.");
+ }
+
+ ImGui.Separator();
+
+ using var dis = ImRaii.Disabled(appliesToMultiple);
+ using var table = ImRaii.Table("CharacterTable", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY, new Vector2(ImGui.GetContentRegionAvail().X, 150));
+ if (!table)
+ return;
+
+ ImGui.TableSetupColumn("##charaDel", ImGuiTableColumnFlags.WidthFixed, ImGui.GetFrameHeight());
+ ImGui.TableSetupColumn("Character", ImGuiTableColumnFlags.WidthFixed, 320 * ImGuiHelpers.GlobalScale);
+ ImGui.TableHeadersRow();
+
+ if(appliesToMultiple)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted("Applies to multiple targets");
+ return;
+ }
+
+
+ //warn: .ToList() might be performance critical at some point
+ //the copying via ToList is done because manipulations with .Templates list result in "Collection was modified" exception here
+ var charas = _selector.Selected!.Characters.WithIndex().ToList();
+
+ if (charas.Count == 0)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted("No characters are associated with this profile");
+ }
+
+ foreach (var (character, idx) in charas)
+ {
+ using var id = ImRaii.PushId(idx);
+ ImGui.TableNextColumn();
+ var keyValid = _configuration.UISettings.DeleteTemplateModifier.IsActive();
+ var tt = keyValid
+ ? "Remove this character from the profile."
+ : $"Remove this character from the profile.\nHold {_configuration.UISettings.DeleteTemplateModifier} to remove.";
+
+ if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), new Vector2(ImGui.GetFrameHeight()), tt, !keyValid, true))
+ _endAction = () => _manager.DeleteCharacter(_selector.Selected!, character);
+ ImGui.TableNextColumn();
+ ImGui.AlignTextToFramePadding();
+ ImGui.TextUnformatted(!_selector.IncognitoMode ? $"{character.ToNameWithoutOwnerName()}{character.TypeToString()}" : "Incognito");
+ }
+ }
+
+ _endAction?.Invoke();
+ _endAction = null;
+ }
+
private void DrawTemplateArea()
{
- using var table = ImRaii.Table("SetTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY);
+ using var table = ImRaii.Table("TemplateTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY);
if (!table)
return;