Merge pull request #44 from Aether-Tools/new_actor_assignment_ui

2.0.7.0 for testing
This commit is contained in:
RisaDev
2024-10-19 23:09:25 +03:00
committed by GitHub
41 changed files with 1556 additions and 592 deletions

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>

View File

@@ -0,0 +1,65 @@
using CustomizePlus.GameData.ReverseSearchDictionaries;
using Dalamud.Game.ClientState.Objects.Enums;
using OtterGui.Services;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Structs;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace CustomizePlus.GameData.Data;
/// <summary> A collection service for all the name dictionaries required for reverse name search. </summary>
/// note: this is mvp for profile upgrading purposes, not intended to be used for anything else.
public sealed class ReverseNameDicts(
ReverseSearchDictMount _mounts,
ReverseSearchDictCompanion _companions,
ReverseSearchDictBNpc _bNpcs,
ReverseSearchDictENpc _eNpcs)
: IAsyncService
{
/// <summary> Valid Mount ids by name in title case. </summary>
public readonly ReverseSearchDictMount Mounts = _mounts;
/// <summary> Valid Companion ids by name in title case. </summary>
public readonly ReverseSearchDictCompanion Companions = _companions;
/// <summary> Valid BNPC ids by name in title case. </summary>
public readonly ReverseSearchDictBNpc BNpcs = _bNpcs;
/// <summary> Valid ENPC ids by name in title case. </summary>
public readonly ReverseSearchDictENpc ENpcs = _eNpcs;
/// <summary> Finished when all name dictionaries are finished. </summary>
public Task Awaiter { get; } =
Task.WhenAll(_mounts.Awaiter, _companions.Awaiter, _bNpcs.Awaiter, _eNpcs.Awaiter);
/// <inheritdoc/>
public bool Finished
=> Awaiter.IsCompletedSuccessfully;
/// <summary> Convert a given name for a certain ObjectKind to an ID. </summary>
/// <returns> default or a valid id. </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public uint ToID(ObjectKind kind, string name)
=> TryGetID(kind, name, out var ret) ? ret : default;
/// <summary> Convert a given ID for a certain ObjectKind to a name. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool TryGetID(ObjectKind kind, string name, [NotNullWhen(true)] out uint npcId)
{
npcId = default;
return kind switch
{
ObjectKind.MountType => Mounts.TryGetValue(name, out npcId),
ObjectKind.Companion => Companions.TryGetValue(name, out npcId),
ObjectKind.BattleNpc => BNpcs.TryGetValue(name, out npcId),
ObjectKind.EventNpc => ENpcs.TryGetValue(name, out npcId),
_ => false,
};
}
}

View File

@@ -28,6 +28,35 @@ public static class ActorIdentifierExtensions
return PenumbraExtensions.Manager.Data.ToName(identifier.Kind, identifier.DataId);
}
/// <summary>
/// Matches() method but ignoring ownership for owned objects.
/// </summary>
public static bool MatchesIgnoringOwnership(this ActorIdentifier identifier, ActorIdentifier other)
{
if (identifier.Type != other.Type)
return false;
return identifier.Type switch
{
IdentifierType.Owned => PenumbraExtensions.Manager.DataIdEquals(identifier, other),
_ => identifier.Matches(other)
};
}
/// <summary>
/// Check if owned actor is owned by local player. Will return false if Type is not Owned.
/// </summary>
public static bool IsOwnedByLocalPlayer(this ActorIdentifier identifier)
{
if (identifier.Type != IdentifierType.Owned)
return false;
if (PenumbraExtensions.Manager == null)
return false;
return identifier.PlayerName == PenumbraExtensions.Manager.GetCurrentPlayer().PlayerName;
}
/// <summary>
/// Wrapper around Incognito which returns non-incognito name in debug builds
/// </summary>
@@ -56,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)",
_ => "",
};
}
/// <summary>
/// For now used to determine if root scaling should be allowed or not
/// </summary>

View File

@@ -0,0 +1,83 @@
using Dalamud.Plugin.Services;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Common.Lua;
using OtterGui.Log;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers.Bases;
using Penumbra.GameData.Structs;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CustomizePlus.GameData.ReverseSearchDictionaries.Bases;
/// <summary> A base class for dictionaries from NPC names to their IDs. </summary>
/// <param name="pluginInterface"> The plugin interface. </param>
/// <param name="log"> A logger. </param>
/// <param name="gameData"> The data manger to fetch the data from. </param>
/// <param name="name"> The name of the data share. </param>
/// <param name="version"> The version of the data share. </param>
/// <param name="factory"> The factory function to create the data from. </param>
public abstract class ReverseNameDictionary(
IDalamudPluginInterface pluginInterface,
Logger log,
IDataManager gameData,
string name,
int version,
Func<IReadOnlyDictionary<string, uint>> factory)
: DataSharer<IReadOnlyDictionary<string, uint>>(pluginInterface, log, name, gameData.Language, version, factory),
IReadOnlyDictionary<string, NpcId>
{
/// <inheritdoc/>
public IEnumerator<KeyValuePair<string, NpcId>> GetEnumerator()
=> Value.Select(kvp => new KeyValuePair<string, NpcId>(kvp.Key, new NpcId(kvp.Value))).GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public int Count
=> Value.Count;
/// <inheritdoc/>
public bool ContainsKey(string key)
=> Value.ContainsKey(key);
/// <inheritdoc/>
public bool TryGetValue(string key, [NotNullWhen(true)] out NpcId value)
{
if (!Value.TryGetValue(key, out var uintVal))
{
value = default;
return false;
}
value = new NpcId(uintVal);
return true;
}
/// <inheritdoc/>
public NpcId this[string key]
=> new NpcId(Value[key]);
/// <inheritdoc/>
public IEnumerable<string> Keys
=> Value.Keys;
/// <inheritdoc/>
public IEnumerable<NpcId> Values
=> Value.Values.Select(k => new NpcId(k));
/// <inheritdoc/>
protected override long ComputeMemory()
=> DataUtility.DictionaryMemory(16, Count) + Keys.Sum(v => v.Length * 2); //this seems to be only used by diagnostics stuff so I don't particularly care for this to be correct.
/// <inheritdoc/>
protected override int ComputeTotalCount()
=> Count;
}

View File

@@ -0,0 +1,33 @@
using Dalamud.Plugin.Services;
using Dalamud.Plugin;
using OtterGui.Log;
using Penumbra.GameData.Data;
using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
using Lumina.Excel.GeneratedSheets;
using CustomizePlus.GameData.ReverseSearchDictionaries.Bases;
namespace CustomizePlus.GameData.ReverseSearchDictionaries;
/// <summary> A dictionary that matches names to battle npc ids. </summary>
public sealed class ReverseSearchDictBNpc(IDalamudPluginInterface pluginInterface, Logger log, IDataManager gameData)
: ReverseNameDictionary(pluginInterface, log, gameData, "ReverseSearchBNpcs", 7, () => CreateBNpcData(gameData))
{
/// <summary> Create the data. </summary>
private static IReadOnlyDictionary<string, uint> CreateBNpcData(IDataManager gameData)
{
var sheet = gameData.GetExcelSheet<BNpcName>(gameData.Language)!;
var dict = new Dictionary<string, uint>((int)sheet.RowCount);
foreach (var n in sheet.Where(n => n.Singular.RawData.Length > 0))
dict.TryAdd(DataUtility.ToTitleCaseExtended(n.Singular, n.Article), n.RowId);
return dict.ToFrozenDictionary();
}
/// <inheritdoc cref="ReverseNameDictionary.TryGetValue"/>
public bool TryGetValue(string key, [NotNullWhen(true)] out uint value)
=> Value.TryGetValue(key, out value);
/// <inheritdoc cref="ReverseNameDictionary.this"/>
public uint this[string key]
=> Value[key];
}

View File

@@ -0,0 +1,33 @@
using Dalamud.Plugin.Services;
using Dalamud.Plugin;
using OtterGui.Log;
using Penumbra.GameData.Data;
using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
using Lumina.Excel.GeneratedSheets;
using CustomizePlus.GameData.ReverseSearchDictionaries.Bases;
namespace CustomizePlus.GameData.ReverseSearchDictionaries;
/// <summary> A dictionary that matches companion names to their ids. </summary>
public sealed class ReverseSearchDictCompanion(IDalamudPluginInterface pluginInterface, Logger log, IDataManager gameData)
: ReverseNameDictionary(pluginInterface, log, gameData, "ReverseSearchCompanions", 7, () => CreateCompanionData(gameData))
{
/// <summary> Create the data. </summary>
private static IReadOnlyDictionary<string, uint> CreateCompanionData(IDataManager gameData)
{
var sheet = gameData.GetExcelSheet<Companion>(gameData.Language)!;
var dict = new Dictionary<string, uint>((int)sheet.RowCount);
foreach (var c in sheet.Where(c => c.Singular.RawData.Length > 0 && c.Order < ushort.MaxValue))
dict.TryAdd(DataUtility.ToTitleCaseExtended(c.Singular, c.Article), c.RowId);
return dict.ToFrozenDictionary();
}
/// <inheritdoc cref="ReverseNameDictionary.TryGetValue"/>
public bool TryGetValue(string key, [NotNullWhen(true)] out uint value)
=> Value.TryGetValue(key, out value);
/// <inheritdoc cref="ReverseNameDictionary.this"/>
public uint this[string key]
=> Value[key];
}

View File

@@ -0,0 +1,33 @@
using Dalamud.Plugin.Services;
using Dalamud.Plugin;
using OtterGui.Log;
using Penumbra.GameData.Data;
using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
using Lumina.Excel.GeneratedSheets;
using CustomizePlus.GameData.ReverseSearchDictionaries.Bases;
namespace CustomizePlus.GameData.ReverseSearchDictionaries;
/// <summary> A dictionary that matches names to event npc ids. </summary>
public sealed class ReverseSearchDictENpc(IDalamudPluginInterface pluginInterface, Logger log, IDataManager gameData)
: ReverseNameDictionary(pluginInterface, log, gameData, "ReverseSearchENpcs", 7, () => CreateENpcData(gameData))
{
/// <summary> Create the data. </summary>
private static IReadOnlyDictionary<string, uint> CreateENpcData(IDataManager gameData)
{
var sheet = gameData.GetExcelSheet<ENpcResident>(gameData.Language)!;
var dict = new Dictionary<string, uint>((int)sheet.RowCount);
foreach (var n in sheet.Where(e => e.Singular.RawData.Length > 0))
dict.TryAdd(DataUtility.ToTitleCaseExtended(n.Singular, n.Article), n.RowId);
return dict.ToFrozenDictionary();
}
/// <inheritdoc cref="ReverseNameDictionary.TryGetValue"/>
public bool TryGetValue(string key, [NotNullWhen(true)] out uint value)
=> Value.TryGetValue(key, out value);
/// <inheritdoc cref="ReverseNameDictionary.this"/>
public uint this[string key]
=> Value[key];
}

View File

@@ -0,0 +1,56 @@
using Dalamud.Plugin.Services;
using Dalamud.Plugin;
using OtterGui.Log;
using Penumbra.GameData.Data;
using System.Collections.Frozen;
using System.Diagnostics.CodeAnalysis;
using Lumina.Excel.GeneratedSheets;
using CustomizePlus.GameData.ReverseSearchDictionaries.Bases;
using Dalamud.Utility;
namespace CustomizePlus.GameData.ReverseSearchDictionaries;
/// <summary> A dictionary that matches names to mount ids. </summary>
public sealed class ReverseSearchDictMount(IDalamudPluginInterface pluginInterface, Logger log, IDataManager gameData)
: ReverseNameDictionary(pluginInterface, log, gameData, "ReverseSearchMounts", 7, () => CreateMountData(gameData))
{
/// <summary> Create the data. </summary>
private static IReadOnlyDictionary<string, uint> CreateMountData(IDataManager gameData)
{
var sheet = gameData.GetExcelSheet<Mount>(gameData.Language)!;
var dict = new Dictionary<string, uint>((int)sheet.RowCount);
// Add some custom data.
dict.TryAdd("Falcon (Porter)", 119);
dict.TryAdd("Hippo Cart (Quest)", 295);
dict.TryAdd("Hippo Cart (Quest)", 296);
dict.TryAdd("Miw Miisv (Quest)", 298);
dict.TryAdd("Moon-hopper (Quest)", 309);
foreach (var m in sheet)
{
if (m.Singular.RawData.Length > 0 && m.Order >= 0)
{
dict.TryAdd(DataUtility.ToTitleCaseExtended(m.Singular, m.Article), m.RowId);
}
else if (m.Unknown18.RawData.Length > 0)
{
// Try to transform some file names into category names.
var whistle = m.Unknown18.ToDalamudString().ToString();
whistle = whistle.Replace("SE_Bt_Etc_", string.Empty)
.Replace("Mount_", string.Empty)
.Replace("_call", string.Empty)
.Replace("Whistle", string.Empty);
dict.TryAdd($"? {whistle} #{m.RowId}", m.RowId);
}
}
return dict.ToFrozenDictionary();
}
/// <inheritdoc cref="ReverseNameDictionary.TryGetValue"/>
public bool TryGetValue(string key, [NotNullWhen(true)] out uint value)
=> Value.TryGetValue(key, out value);
/// <inheritdoc cref="ReverseNameDictionary.this"/>
public uint this[string key]
=> Value[key];
}

View File

@@ -4,7 +4,7 @@ namespace CustomizePlus.Api;
public partial class CustomizePlusIpc
{
private readonly (int Breaking, int Feature) _apiVersion = (5, 1);
private readonly (int Breaking, int Feature) _apiVersion = (5, 2);
/// <summary>
/// When there are breaking changes the first number is bumped up and second one is reset.

View File

@@ -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.CharacterName.Text, x.Enabled);
return (x.UniqueId, x.Name.Text, path, x.Characters.Count > 0 ? x.Characters[0].ToNameWithoutOwnerName() : "", x.Enabled); //todo: proper update to v5
})
.ToList();
}
@@ -134,7 +134,7 @@ public partial class CustomizePlusIpc
if (actor == null || !actor.Value.Valid || !actor.Value.IsCharacter)
return ((int)ErrorCode.InvalidCharacter, null);
var profile = _profileManager.GetProfileByCharacterName(actor.Value.Utf8Name.ToString(), true);
var profile = _profileManager.GetProfileByActor(actor.Value, true);
if (profile == null)
return ((int)ErrorCode.ProfileNotFound, null);
@@ -261,12 +261,10 @@ public partial class CustomizePlusIpc
//warn: intended limitation - ignores default profiles because why you would use default profile on your own character
private void OnArmatureChanged(ArmatureChanged.Type type, Armature armature, object? arg3)
{
string currentPlayerName = _gameObjectService.GetCurrentPlayerName();
if (armature.ActorIdentifier.ToNameWithoutOwnerName() != currentPlayerName)
if (armature.ActorIdentifier != _gameObjectService.GetCurrentPlayerActorIdentifier())
return;
if (armature.ActorIdentifier.HomeWorld == WorldId.AnyWorld) //Cutscene/GPose actors
if (armature.ActorIdentifier.HomeWorld == WorldId.AnyWorld) //Only Cutscene/GPose actors have world set to AnyWorld
return;
ICharacter? localPlayerCharacter = (ICharacter?)_gameObjectService.GetDalamudGameObjectFromActor(_gameObjectService.GetLocalPlayerActor());
@@ -285,24 +283,12 @@ public partial class CustomizePlusIpc
else
(activeProfile, oldProfile) = ((Profile?, Profile?))arg3;
if (activeProfile != null)
{
if (activeProfile == _profileManager.DefaultProfile || activeProfile.ProfileType == ProfileType.Editor)
{
//ignore any changes while player is in editor or if player changes between default profiles
//also do not send event if there were no active profile before
if (activeProfile == oldProfile || oldProfile == null)
//do not send event if we are entering editor
if (activeProfile != null && activeProfile.ProfileType == ProfileType.Editor)
return;
OnProfileUpdateInternal(localPlayerCharacter, null); //send empty profile when player enters editor or turns on default profile
return;
}
}
//do not send event if we are exiting editor or disabling default profile and don't have any active profile
if (oldProfile != null &&
(oldProfile == _profileManager.DefaultProfile || oldProfile.ProfileType == ProfileType.Editor) &&
activeProfile == null)
//do not send event if we are exiting editor
if (oldProfile != null && oldProfile.ProfileType == ProfileType.Editor)
return;
OnProfileUpdateInternal(localPlayerCharacter, activeProfile);
@@ -311,8 +297,9 @@ public partial class CustomizePlusIpc
if (type == ArmatureChanged.Type.Deleted)
{
//Do not send event if default or editor profile was used
if (armature.Profile == _profileManager.DefaultProfile || armature.Profile.ProfileType == ProfileType.Editor) //todo: never send if ProfileType != normal?
//Do not send event if editor profile was used
//todo: never send if ProfileType != normal?
if (armature.Profile.ProfileType == ProfileType.Editor)
return;
OnProfileUpdateInternal(localPlayerCharacter, null);

View File

@@ -1,5 +1,7 @@
using CustomizePlus.Configuration.Data.Version3;
using CustomizePlus.Core.Data;
using CustomizePlus.Game.Services;
using CustomizePlus.GameData.Extensions;
using CustomizePlus.Profiles.Data;
using CustomizePlus.Templates.Data;
using System;
@@ -16,14 +18,18 @@ namespace CustomizePlus.Api.Data;
/// </summary>
public class IPCCharacterProfile
{
/// <summary>
/// Used only for display purposes
/// </summary>
public string CharacterName { get; set; } = "Invalid";
public Dictionary<string, IPCBoneTransform> Bones { get; init; } = new();
public static IPCCharacterProfile FromFullProfile(Profile profile)
{
var ipcProfile = new IPCCharacterProfile
{
CharacterName = profile.CharacterName,
CharacterName = profile.Characters.FirstOrDefault().ToNameWithoutOwnerName(),
Bones = new Dictionary<string, IPCBoneTransform>()
};
@@ -47,12 +53,11 @@ public class IPCCharacterProfile
{
var fullProfile = new Profile
{
Name = $"{profile.CharacterName}'s IPC profile",
CharacterName = profile.CharacterName,
Name = $"IPC profile for {profile.CharacterName}",
//Character should be set manually
CreationDate = DateTimeOffset.UtcNow,
ModifiedDate = DateTimeOffset.UtcNow,
Enabled = true,
LimitLookupToOwnedObjects = false,
UniqueId = Guid.NewGuid(),
Templates = new List<Template>(1),
ProfileType = isTemporary ? Profiles.Enums.ProfileType.Temporary : Profiles.Enums.ProfileType.Normal

View File

@@ -343,7 +343,6 @@ public unsafe sealed class ArmatureManager : IDisposable
if (type is not TemplateChanged.Type.NewBone &&
type is not TemplateChanged.Type.DeletedBone &&
type is not TemplateChanged.Type.EditorCharacterChanged &&
type is not TemplateChanged.Type.EditorLimitLookupToOwnedChanged &&
type is not TemplateChanged.Type.EditorEnabled &&
type is not TemplateChanged.Type.EditorDisabled)
return;
@@ -369,9 +368,9 @@ public unsafe sealed class ArmatureManager : IDisposable
if (type == TemplateChanged.Type.EditorCharacterChanged)
{
(var characterName, var profile) = ((string, Profile))arg3;
(var character, var profile) = ((ActorIdentifier, Profile))arg3;
foreach (var armature in GetArmaturesForCharacterName(characterName))
foreach (var armature in GetArmaturesForCharacter(character))
{
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile character name changed, armature rebind scheduled: {type}, {armature}");
@@ -384,22 +383,7 @@ public unsafe sealed class ArmatureManager : IDisposable
foreach (var armature in profile.Armatures)
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile character name changed, armature rebind scheduled: {type}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}, new name: {characterName.Incognify()}");
return;
}
if(type == TemplateChanged.Type.EditorLimitLookupToOwnedChanged)
{
var profile = (Profile)arg3!;
if (profile.Armatures.Count == 0)
return;
foreach (var armature in profile.Armatures)
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile limit lookup setting changed, armature rebind scheduled: {type}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile character name changed, armature rebind scheduled: {type}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}, new name: {character.Incognito(null)}");
return;
}
@@ -407,7 +391,7 @@ public unsafe sealed class ArmatureManager : IDisposable
if (type == TemplateChanged.Type.EditorEnabled ||
type == TemplateChanged.Type.EditorDisabled)
{
foreach (var armature in GetArmaturesForCharacterName((string)arg3!))
foreach (var armature in GetArmaturesForCharacter((ActorIdentifier)arg3!))
{
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnTemplateChange template editor enabled/disabled: {type}, pending profile set for {armature}");
@@ -427,12 +411,14 @@ 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.ChangedCharacterName &&
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.LimitLookupToOwnedChanged)
type is not ProfileChanged.Type.ChangedDefaultLocalPlayerProfile)
return;
if (type == ProfileChanged.Type.ChangedDefaultProfile)
if (type == ProfileChanged.Type.ChangedDefaultProfile || type == ProfileChanged.Type.ChangedDefaultLocalPlayerProfile)
{
var oldProfile = (Profile?)arg3;
@@ -442,7 +428,7 @@ public unsafe sealed class ArmatureManager : IDisposable
foreach (var armature in oldProfile.Armatures)
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnProfileChange Profile no longer default, armatures rebind scheduled: {type}, old profile: {oldProfile.Name.Text.Incognify()}->{oldProfile.Enabled}");
_logger.Debug($"ArmatureManager.OnProfileChange Profile no longer default/default for local player, armatures rebind scheduled: {type}, old profile: {oldProfile.Name.Text.Incognify()}->{oldProfile.Enabled}");
return;
}
@@ -453,59 +439,104 @@ 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)
return;
if (profile == _profileManager.DefaultProfile)
if (profile == _profileManager.DefaultProfile ||
profile == _profileManager.DefaultLocalPlayerProfile)
{
foreach (var kvPair in Armatures)
{
var armature = kvPair.Value;
if (armature.Profile == profile)
if (armature.Profile == _profileManager.DefaultProfile || //not the best solution but w/e
armature.Profile == _profileManager.DefaultLocalPlayerProfile)
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnProfileChange default profile toggled, planning rebind for armature {armature}");
_logger.Debug($"ArmatureManager.OnProfileChange default/default local player profile toggled, planning rebind for armature {armature}");
}
return;
}
if (string.IsNullOrWhiteSpace(profile.CharacterName))
return;
foreach(var character in profile.Characters)
{
if (!character.IsValid)
continue;
foreach (var armature in GetArmaturesForCharacterName(profile.CharacterName))
foreach (var armature in GetArmaturesForCharacter(character))
{
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnProfileChange profile {profile} toggled, planning rebind for armature {armature}");
}
}
return;
}
if (type == ProfileChanged.Type.TemporaryProfileAdded)
{
if (!profile.TemporaryActor.IsValid || !Armatures.ContainsKey(profile.TemporaryActor))
return;
//todo: remove this later
/*Armature? armature = null;
foreach(var kvPair in Armatures)
{
//todo: check mount/companion
if(kvPair.Key.CompareIgnoringOwnership(profile.Character) &&
(kvPair.Key.Type != IdentifierType.Owned || kvPair.Key.IsOwnedByLocalPlayer()))
{
armature = kvPair.Value;
break;
}
}
if (armature == null)
return;*/
foreach(var character in profile.Characters)
{
if (!character.IsValid || !Armatures.ContainsKey(character))
continue;
var armature = Armatures[character];
var armature = Armatures[profile.TemporaryActor];
if (armature.Profile == profile)
return;
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.ChangedCharacterName ||
if (type == ProfileChanged.Type.AddedCharacter ||
type == ProfileChanged.Type.RemovedCharacter ||
type == ProfileChanged.Type.Deleted ||
type == ProfileChanged.Type.TemporaryProfileDeleted ||
type == ProfileChanged.Type.LimitLookupToOwnedChanged)
type == ProfileChanged.Type.TemporaryProfileDeleted)
{
if (profile.Armatures.Count == 0)
return;
@@ -518,7 +549,7 @@ public unsafe sealed class ArmatureManager : IDisposable
armature.IsPendingProfileRebind = true;
}
_logger.Debug($"ArmatureManager.OnProfileChange CCN/DEL/TPD/LLTOC, 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;
}
@@ -532,13 +563,14 @@ public unsafe sealed class ArmatureManager : IDisposable
profile!.Armatures.ForEach(x => x.IsPendingProfileRebind = true);
}
private IEnumerable<Armature> GetArmaturesForCharacterName(string characterName)
private IEnumerable<Armature> GetArmaturesForCharacter(ActorIdentifier actorIdentifier)
{
foreach (var kvPair in Armatures)
{
(var actorIdentifier, _) = _gameObjectService.GetTrueActorForSpecialTypeActor(kvPair.Key);
(var armatureActorIdentifier, _) = _gameObjectService.GetTrueActorForSpecialTypeActor(kvPair.Key);
if(actorIdentifier.ToNameWithoutOwnerName() == characterName)
if (actorIdentifier.IsValid && armatureActorIdentifier.MatchesIgnoringOwnership(actorIdentifier) &&
(armatureActorIdentifier.Type != IdentifierType.Owned || armatureActorIdentifier.IsOwnedByLocalPlayer()))
yield return kvPair.Value;
}
}

View File

@@ -11,6 +11,8 @@ using CustomizePlus.Core.Data;
using CustomizePlus.Configuration.Services;
using CustomizePlus.UI.Windows;
using Dalamud.Interface.ImGuiNotification;
using Penumbra.GameData.Actors;
using CustomizePlus.Core.Helpers;
namespace CustomizePlus.Configuration.Data;
@@ -30,6 +32,11 @@ public class PluginConfiguration : IPluginConfiguration, ISavable
/// </summary>
public Guid DefaultProfile { get; set; } = Guid.Empty;
/// <summary>
/// Id of the profile applied to any character user logins with. Can be set to Empty to disable this feature.
/// </summary>
public Guid DefaultLocalPlayerProfile { get; set; } = Guid.Empty;
[Serializable]
public class ChangelogSettingsEntries
{
@@ -46,8 +53,14 @@ public class PluginConfiguration : IPluginConfiguration, ISavable
public bool FoldersDefaultOpen { get; set; } = true;
public bool OpenWindowAtStart { get; set; } = false;
public bool HideWindowInCutscene { get; set; } = true;
public bool HideWindowWhenUiHidden { get; set; } = true;
public bool HideWindowInGPose { get; set; } = false;
public bool IncognitoMode { get; set; } = false;
public List<string> ViewedMessageWindows { get; set; } = new();
@@ -66,9 +79,8 @@ public class PluginConfiguration : IPluginConfiguration, ISavable
public bool ShowLiveBones { get; set; } = true;
public bool BoneMirroringEnabled { get; set; } = false;
public bool LimitLookupToOwnedObjects { get; set; } = false;
public string? PreviewCharacterName { get; set; } = null;
public ActorIdentifier PreviewCharacter { get; set; } = ActorIdentifier.Invalid;
public int EditorValuesPrecision { get; set; } = 3;
@@ -134,6 +146,7 @@ public class PluginConfiguration : IPluginConfiguration, ISavable
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
{
Error = HandleDeserializationError,
Converters = new List<JsonConverter> { new ActorIdentifierJsonConverter() }
});
}
catch (Exception ex)
@@ -153,6 +166,7 @@ public class PluginConfiguration : IPluginConfiguration, ISavable
{
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Converters.Add(new ActorIdentifierJsonConverter());
serializer.Serialize(jWriter, this);
}

View File

@@ -14,11 +14,10 @@ internal static class V3ProfileToV4Converter
var profile = new Profile
{
Name = $"{v3Profile.ProfileName} - {v3Profile.CharacterName}",
CharacterName = v3Profile.CharacterName,
//CharacterName = v3Profile.CharacterName, //no longer supported
CreationDate = v3Profile.CreationDate,
ModifiedDate = DateTimeOffset.UtcNow,
Enabled = v3Profile.Enabled,
LimitLookupToOwnedObjects = v3Profile.OwnedOnly,
UniqueId = Guid.NewGuid(),
Templates = new List<Template>(1)
};

View File

@@ -44,74 +44,22 @@ public class ConfigurationMigrator
if (configVersion >= Constants.ConfigurationVersion)
return;
//V3 migration code
//We no longer support migrations of any versions < 4
if (configVersion < 3)
{
_messageService.NotificationMessage($"Unable to migrate your Customize+ configuration because it is too old. Manually install latest version of Customize+ 1.x to migrate your configuration to supported version first.", NotificationType.Error);
return;
}
MigrateV3ToV4();
// /V3 migration code
if (configVersion < 4)
{
_messageService.NotificationMessage($"Unable to migrate your Customize+ configuration because it is too old. Manually install Customize+ 2.0.6.5 to migrate your configuration to supported version first.", NotificationType.Error);
return;
}
throw new NotImplementedException();
config.Version = Constants.ConfigurationVersion;
_saveService.ImmediateSave(config);
}
private void MigrateV3ToV4()
{
_backupService.CreateV3Backup();
//I'm sorry, I'm too lazy so v3's enable root position setting is not getting migrated
bool anyMigrationFailures = false;
var usedGuids = new HashSet<Guid>();
foreach (var file in Directory.EnumerateFiles(_saveService.FileNames.ConfigDirectory, "*.profile", SearchOption.TopDirectoryOnly))
{
try
{
_logger.Debug($"Migrating v3 profile {file}");
var legacyProfile = JsonConvert.DeserializeObject<Version3Profile>(File.ReadAllText(file));
if (legacyProfile == null)
continue;
_logger.Debug($"v3 profile {file} loaded as {legacyProfile.ProfileName}");
(var profile, var template) = V3ProfileToV4Converter.Convert(legacyProfile);
//regenerate guids just to be safe
do
{
profile.UniqueId = Guid.NewGuid();
}
while (profile.UniqueId == Guid.Empty || usedGuids.Contains(profile.UniqueId));
usedGuids.Add(profile.UniqueId);
do
{
template.UniqueId = Guid.NewGuid();
}
while (template.UniqueId == Guid.Empty || usedGuids.Contains(template.UniqueId));
usedGuids.Add(template.UniqueId);
_saveService.ImmediateSaveSync(template);
_saveService.ImmediateSaveSync(profile);
_logger.Debug($"Migrated v3 profile {legacyProfile.ProfileName} to profile {profile.UniqueId} and template {template.UniqueId}");
File.Delete(file);
}
catch(Exception ex)
{
anyMigrationFailures = true;
_logger.Error($"Error while migrating {file}: {ex}");
}
}
if (anyMigrationFailures)
_messageService.NotificationMessage($"Some of your Customize+ profiles failed to migrate correctly.\nDetails have been printed to Dalamud log (/xllog in chat).", NotificationType.Error);
_reloadEvent.Invoke(ReloadEvent.Type.ReloadAll);
}
}

View File

@@ -1,35 +0,0 @@
using CustomizePlus.Core.Services;
using OtterGui.Log;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CustomizePlus.Configuration.Services.Temporary;
//V3 has bug when it doesn't create config file. We need to have one to migrate stuff properly.
internal class Version3ConfigFixer
{
private readonly Logger _logger;
private readonly FilenameService _filenameService;
public Version3ConfigFixer(
Logger logger,
FilenameService filenameService)
{
_logger = logger;
_filenameService = filenameService;
}
public void FixV3ConfigIfNeeded()
{
var oldVersionProfiles = Directory.EnumerateFiles(_filenameService.ConfigDirectory, "*.profile", SearchOption.TopDirectoryOnly);
if (oldVersionProfiles.Count() > 0 && !File.Exists(_filenameService.ConfigFile))
{
_logger.Warning("V3 config not found while profiles are available, creating dummy V3 config");
File.WriteAllText(_filenameService.ConfigFile, "{\r\n \"ViewedMessageWindows\": [],\r\n \"Version\": 3,\r\n \"PluginEnabled\": true,\r\n \"DebuggingModeEnabled\": false,\r\n \"RootPositionEditingEnabled\": false\r\n}");
}
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.Json;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Penumbra.GameData.Actors;
using Newtonsoft.Json.Linq;
namespace CustomizePlus.Core.Helpers;
internal sealed class ActorIdentifierJsonConverter : JsonConverter<ActorIdentifier>
{
public override ActorIdentifier ReadJson(JsonReader reader, Type objectType, ActorIdentifier existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer)
{
JObject obj = JObject.Load(reader);
if (Penumbra.GameData.Actors.ActorIdentifierExtensions.Manager == null)
throw new Exception("Penumbra.GameData.Actors.ActorIdentifierExtensions.Manager is not ready");
return Penumbra.GameData.Actors.ActorIdentifierExtensions.Manager.FromJson(obj);
}
public override void WriteJson(JsonWriter writer, ActorIdentifier value, Newtonsoft.Json.JsonSerializer serializer)
{
value.ToJson().WriteTo(writer);
}
}

View File

@@ -1,37 +1,36 @@
using Dalamud.Plugin;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
using CustomizePlus.Profiles;
using CustomizePlus.Core.Services;
using CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
using CustomizePlus.Game.Services;
using CustomizePlus.Configuration.Services;
using CustomizePlus.Templates;
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
using CustomizePlus.Anamnesis;
using CustomizePlus.Api;
using CustomizePlus.Armatures.Events;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Core.Events;
using CustomizePlus.UI;
using CustomizePlus.UI.Windows.Controls;
using CustomizePlus.Anamnesis;
using CustomizePlus.Armatures.Services;
using CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
using CustomizePlus.UI.Windows.MainWindow;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Configuration.Services;
using CustomizePlus.Core.Events;
using CustomizePlus.Core.Services;
using CustomizePlus.Game.Events;
using CustomizePlus.UI.Windows;
using CustomizePlus.UI.Windows.MainWindow.Tabs;
using CustomizePlus.Templates.Events;
using CustomizePlus.Profiles.Events;
using CustomizePlus.Game.Services;
using CustomizePlus.Game.Services.GPose;
using CustomizePlus.Game.Services.GPose.ExternalTools;
using CustomizePlus.GameData.Services;
using CustomizePlus.Configuration.Services.Temporary;
using CustomizePlus.Profiles;
using CustomizePlus.Profiles.Events;
using CustomizePlus.Templates;
using CustomizePlus.Templates.Events;
using CustomizePlus.UI;
using CustomizePlus.UI.Windows;
using CustomizePlus.UI.Windows.Controls;
using CustomizePlus.UI.Windows.MainWindow;
using CustomizePlus.UI.Windows.MainWindow.Tabs;
using CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
using CustomizePlus.UI.Windows.MainWindow.Tabs.Profiles;
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
using Dalamud.Plugin;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Structs;
using OtterGui.Raii;
using CustomizePlus.Api;
namespace CustomizePlus.Core;
@@ -89,6 +88,7 @@ public static class ServiceManagerBuilder
services
.AddSingleton<TemplateCombo>()
.AddSingleton<PluginStateBlock>()
.AddSingleton<ActorAssignmentUi>()
.AddSingleton<SettingsTab>()
// template
.AddSingleton<TemplatesTab>()
@@ -163,8 +163,7 @@ public static class ServiceManagerBuilder
{
services
.AddSingleton<PluginConfiguration>()
.AddSingleton<ConfigurationMigrator>()
.AddSingleton<Version3ConfigFixer>();
.AddSingleton<ConfigurationMigrator>();
return services;
}

View File

@@ -13,6 +13,9 @@ using static System.Windows.Forms.AxHost;
using CustomizePlus.Profiles.Data;
using CustomizePlus.Configuration.Data;
using Dalamud.Interface.ImGuiNotification;
using CustomizePlus.GameData.Extensions;
using System.Collections.Generic;
using ECommons;
namespace CustomizePlus.Core.Services;
@@ -168,6 +171,7 @@ public class CommandService : IDisposable
}
Profile? targetProfile = null;
List<Profile> profilesToDisable = new List<Profile>(_profileManager.Profiles.Count);
characterName = subArgumentList[0].Trim();
characterName = characterName switch
@@ -182,12 +186,24 @@ public class CommandService : IDisposable
if (!isTurningOffAllProfiles)
{
profileName = subArgumentList[1].Trim();
targetProfile = _profileManager.Profiles.FirstOrDefault(x => x.Name == profileName && x.CharacterName == characterName);
foreach(var profile in _profileManager.Profiles)
{
if (!profile.Characters.Any(x => x.ToNameWithoutOwnerName() == characterName))
continue;
if (profile.Name != profileName)
{
profilesToDisable.Add(profile);
continue;
}
targetProfile = profile;
}
}
else
targetProfile = _profileManager.Profiles.FirstOrDefault(x => x.CharacterName == characterName && x.Enabled);
profilesToDisable = _profileManager.Profiles.Where(x => x.Characters.Any(x => x.ToNameWithoutOwnerName() == characterName) && x.Enabled).ToList();
if (targetProfile == null)
if (targetProfile == null || (isTurningOffAllProfiles && profilesToDisable.Count == 0))
{
_chatService.PrintInChat(new SeStringBuilder()
.AddText("Cannot execute command because profile ")
@@ -217,6 +233,12 @@ public class CommandService : IDisposable
else
_profileManager.SetEnabled(targetProfile, !targetProfile.Enabled);
if(targetProfile.Enabled)
{
foreach (var profile in profilesToDisable)
_profileManager.SetEnabled(profile, false);
}
if (_pluginConfiguration.CommandSettings.PrintSuccessMessages)
_chatService.PrintInChat(new SeStringBuilder()
.AddText("Profile ")
@@ -224,7 +246,7 @@ public class CommandService : IDisposable
.AddText(" was successfully ")
.AddBlue(state != null ? ((bool)state ? "enabled" : "disabled") : "toggled")
.AddText(" for ")
.AddRed(targetProfile.CharacterName).BuiltString);
.AddRed(string.Join(',', targetProfile.Characters.Select(x => x.ToNameWithoutOwnerName()))).BuiltString);
}
catch (Exception e)
{

View File

@@ -1,17 +1,13 @@
using CustomizePlus.Armatures.Services;
using System;
using System.Linq;
using System.Text;
using CustomizePlus.Armatures.Services;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Core.Data;
using CustomizePlus.Core.Extensions;
using CustomizePlus.Profiles;
using CustomizePlus.Templates;
using Dalamud.Plugin;
using OtterGui.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace CustomizePlus.Core.Services;
@@ -46,8 +42,7 @@ public class SupportLogBuilderService
sb.Append($"> **`Commit Hash: `** {ThisAssembly.Git.Commit}+{ThisAssembly.Git.Sha}\n");
sb.Append($"> **`Plugin enabled: `** {_configuration.PluginEnabled}\n");
sb.AppendLine("**Settings -> Editor Settings**");
sb.Append($"> **`Limit to my creatures (editor): `** {_configuration.EditorConfiguration.LimitLookupToOwnedObjects}\n");
sb.Append($"> **`Preview character (editor): `** {_configuration.EditorConfiguration.PreviewCharacterName?.Incognify() ?? "Not set"}\n");
sb.Append($"> **`Preview character (editor): `** {_configuration.EditorConfiguration.PreviewCharacter.Incognito(null)}\n");
sb.Append($"> **`Set preview character on login: `** {_configuration.EditorConfiguration.SetPreviewToCurrentCharacterOnLogin}\n");
sb.Append($"> **`Root editing: `** {_configuration.EditorConfiguration.RootPositionEditingEnabled}\n");
sb.AppendLine("**Settings -> Profile application**");
@@ -66,6 +61,7 @@ public class SupportLogBuilderService
}
sb.AppendLine("**Profiles**");
sb.Append($"> **`Default profile: `** {_profileManager.DefaultProfile?.ToString() ?? "None"}\n");
sb.Append($"> **`Default local player profile: `** {_profileManager.DefaultLocalPlayerProfile?.ToString() ?? "None"}\n");
sb.Append($"> **`Count: `** {_profileManager.Profiles.Count}\n");
foreach (var profile in _profileManager.Profiles)
{
@@ -73,8 +69,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.CharacterName.Text.Incognify()}\n");
sb.Append($"> > **`Limit to my creatures: `** {profile.LimitLookupToOwnedObjects}\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)

View File

@@ -9,6 +9,7 @@ using ObjectManager = CustomizePlus.GameData.Services.ObjectManager;
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject;
using CustomizePlus.Configuration.Data;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.GameData.Files.ShaderStructs;
namespace CustomizePlus.Game.Services;
@@ -31,6 +32,11 @@ public class GameObjectService
_configuration = configuration;
}
public ActorIdentifier GetCurrentPlayerActorIdentifier()
{
return _objectManager.PlayerData.Identifier;
}
public string GetCurrentPlayerName()
{
return _objectManager.PlayerData.Identifier.ToName();
@@ -54,8 +60,6 @@ public class GameObjectService
/// <summary>
/// Case sensitive
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public IEnumerable<(ActorIdentifier, Actor)> FindActorsByName(string name)
{
_objectManager.Update();
@@ -80,6 +84,36 @@ public class GameObjectService
}
}
/// <summary>
/// Searches using MatchesIgnoringOwnership
/// </summary>
public IEnumerable<(ActorIdentifier, Actor)> FindActorsByIdentifierIgnoringOwnership(ActorIdentifier identifier)
{
if (!identifier.IsValid)
yield break;
_objectManager.Update();
foreach (var kvPair in _objectManager.Identifiers)
{
var objectIdentifier = kvPair.Key;
(objectIdentifier, _) = GetTrueActorForSpecialTypeActor(objectIdentifier);
if (!objectIdentifier.IsValid)
continue;
if (identifier.MatchesIgnoringOwnership(objectIdentifier))
{
if (kvPair.Value.Objects.Count > 1) //in gpose we can have more than a single object for one actor
foreach (var obj in kvPair.Value.Objects)
yield return (kvPair.Key.CreatePermanent(), obj);
else
yield return (kvPair.Key.CreatePermanent(), kvPair.Value.Objects[0]);
}
}
}
public Actor GetLocalPlayerActor()
{
_objectManager.Update();

View File

@@ -1,26 +1,13 @@
using System;
using System.Reflection;
using Dalamud.Plugin;
using OtterGui.Log;
using CustomizePlus.Api;
using CustomizePlus.Core;
using CustomizePlus.Core.Services;
using CustomizePlus.UI;
using CustomizePlus.Core;
using CustomizePlus.Configuration.Services.Temporary;
using OtterGui.Services;
using CustomizePlus.Api;
using Dalamud.Plugin;
using ECommons;
using ECommons.Commands;
using ECommons.Configuration;
using OtterGui;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Linq;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Core.Extensions;
using CustomizePlus.Templates;
using CustomizePlus.Profiles;
using CustomizePlus.Armatures.Services;
using OtterGui.Log;
using OtterGui.Services;
using Penumbra.GameData.Actors;
namespace CustomizePlus;
@@ -44,9 +31,7 @@ public sealed class Plugin : IDalamudPlugin
_services = ServiceManagerBuilder.CreateProvider(pluginInterface, Logger);
//temporary
var v3ConfigFixer = _services.GetService<Version3ConfigFixer>();
v3ConfigFixer.FixV3ConfigIfNeeded();
_services.GetService<ActorManager>(); //needs to be initialized early for config to be read properly
_services.GetService<CustomizePlusIpc>();
_services.GetService<CPlusWindowSystem>();

View File

@@ -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;
@@ -21,21 +22,26 @@ namespace CustomizePlus.Profiles.Data;
/// </summary>
public sealed class Profile : ISavable
{
public const int Version = 5;
private static int _nextGlobalId;
private readonly int _localId;
public List<Armature> Armatures = new();
public LowerString CharacterName { get; set; } = LowerString.Empty;
/* [Obsolete("To be removed in the future versions")]
public LowerString CharacterName { get; set; } = LowerString.Empty;*/
//public ActorIdentifier Character { get; set; } = ActorIdentifier.Invalid;
public List<ActorIdentifier> Characters { get; set; } = new();
public LowerString Name { get; set; } = LowerString.Empty;
/// <summary>
/// Whether to search only through local player owned characters or all characters when searching for game object by name
/// </summary>
public bool LimitLookupToOwnedObjects { get; set; } = false;
public int Version { get; set; } = Constants.ConfigurationVersion;
//public bool LimitLookupToOwnedObjects { get; set; } = false;
public bool Enabled { get; set; }
public DateTimeOffset CreationDate { get; set; } = DateTime.UtcNow;
@@ -49,16 +55,21 @@ public sealed class Profile : ISavable
public ProfileType ProfileType { get; set; }
/// <summary>
/// Profile priority when there are several profiles affecting same character
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 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
/// </summary>
public bool IsTemporary => ProfileType == ProfileType.Temporary;
/// <summary>
/* /// <summary>
/// Identificator specifying specific actor this profile applies to, only works for temporary profiles
/// </summary>
public ActorIdentifier TemporaryActor { get; set; } = ActorIdentifier.Invalid;
public ActorIdentifier TemporaryActor { get; set; } = ActorIdentifier.Invalid;*/
public string Incognito
=> UniqueId.ToString()[..8];
@@ -74,8 +85,7 @@ public sealed class Profile : ISavable
/// <param name="original"></param>
public Profile(Profile original) : this()
{
CharacterName = original.CharacterName;
LimitLookupToOwnedObjects = original.LimitLookupToOwnedObjects;
Characters = original.Characters.ToList();
foreach (var template in original.Templates)
{
@@ -85,7 +95,7 @@ public sealed class Profile : ISavable
public override string ToString()
{
return $"Profile '{Name.Text.Incognify()}' on {CharacterName.Text.Incognify()} [{UniqueId}]";
return $"Profile '{Name.Text.Incognify()}' on {string.Join(',', Characters.Select(x => x.Incognito(null)))} [{UniqueId}]";
}
#region Serialization
@@ -98,11 +108,11 @@ public sealed class Profile : ISavable
["UniqueId"] = UniqueId,
["CreationDate"] = CreationDate,
["ModifiedDate"] = ModifiedDate,
["CharacterName"] = CharacterName.Text,
["Characters"] = SerializeCharacters(),
["Name"] = Name.Text,
["LimitLookupToOwnedObjects"] = LimitLookupToOwnedObjects,
["Enabled"] = Enabled,
["IsWriteProtected"] = IsWriteProtected,
["Priority"] = Priority,
["Templates"] = SerializeTemplates()
};
@@ -122,65 +132,20 @@ public sealed class Profile : ISavable
return ret;
}
#endregion
#region Deserialization
public static Profile Load(TemplateManager templateManager, JObject obj)
private JArray SerializeCharacters()
{
var version = obj["Version"]?.ToObject<int>() ?? 0;
return version switch
var ret = new JArray();
foreach (var character in Characters)
{
//Ignore everything below v4 for now
4 => LoadV4(templateManager, obj),
_ => throw new Exception("The design to be loaded has no valid Version."),
};
ret.Add(character.ToJson());
}
private static Profile LoadV4(TemplateManager templateManager, JObject obj)
{
var creationDate = obj["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
var profile = new Profile()
{
CreationDate = creationDate,
UniqueId = obj["UniqueId"]?.ToObject<Guid>() ?? throw new ArgumentNullException("UniqueId"),
Name = new LowerString(obj["Name"]?.ToObject<string>()?.Trim() ?? throw new ArgumentNullException("Name")),
CharacterName = new LowerString(obj["CharacterName"]?.ToObject<string>()?.Trim() ?? throw new ArgumentNullException("CharacterName")),
LimitLookupToOwnedObjects = obj["LimitLookupToOwnedObjects"]?.ToObject<bool>() ?? throw new ArgumentNullException("LimitLookupToOwnedObjects"),
Enabled = obj["Enabled"]?.ToObject<bool>() ?? throw new ArgumentNullException("Enabled"),
ModifiedDate = obj["ModifiedDate"]?.ToObject<DateTimeOffset>() ?? creationDate,
IsWriteProtected = obj["IsWriteProtected"]?.ToObject<bool>() ?? false,
Templates = new List<Template>()
};
if (profile.ModifiedDate < creationDate)
profile.ModifiedDate = creationDate;
if (obj["Templates"] is not JArray templateArray)
return profile;
foreach (var templateObj in templateArray)
{
if (templateObj is not JObject templateObjCast)
{
//todo: warning
continue;
}
var templateId = templateObjCast["TemplateId"]?.ToObject<Guid>();
if (templateId == null)
continue; //todo: error
var template = templateManager.GetTemplate((Guid)templateId);
if (template != null)
profile.Templates.Add(template);
}
return profile;
return ret;
}
#endregion
//Loading is in ProfileManager
#region ISavable
public string ToFilename(FilenameService fileNames)

View File

@@ -15,15 +15,18 @@ public sealed class ProfileChanged() : EventWrapper<ProfileChanged.Type, Profile
Deleted,
Renamed,
Toggled,
ChangedCharacterName,
PriorityChanged,
AddedCharacter,
RemovedCharacter,
//ChangedCharacter,
AddedTemplate,
RemovedTemplate,
MovedTemplate,
ChangedTemplate,
ReloadedAll,
WriteProtection,
LimitLookupToOwnedChanged,
ChangedDefaultProfile,
ChangedDefaultLocalPlayerProfile,
TemporaryProfileAdded,
TemporaryProfileDeleted,
/*

View File

@@ -0,0 +1,202 @@
using CustomizePlus.Profiles.Data;
using CustomizePlus.Profiles.Events;
using CustomizePlus.Templates.Data;
using CustomizePlus.Templates;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.String;
using Penumbra.GameData.Structs;
using Dalamud.Game.ClientState.Objects.Enums;
using Penumbra.GameData.Gui;
using System.Xml;
namespace CustomizePlus.Profiles;
public partial class ProfileManager : IDisposable
{
public void LoadProfiles()
{
_logger.Information("Loading profiles...");
//todo: hot reload was not tested
//save temp profiles
var temporaryProfiles = Profiles.Where(x => x.IsTemporary).ToList();
Profiles.Clear();
List<(Profile, string)> invalidNames = new();
foreach (var file in _saveService.FileNames.Profiles())
{
_logger.Debug($"Reading profile {file.FullName}");
try
{
var text = File.ReadAllText(file.FullName);
var data = JObject.Parse(text);
var profile = LoadIndividualProfile(data);
if (profile.UniqueId.ToString() != Path.GetFileNameWithoutExtension(file.Name))
invalidNames.Add((profile, file.FullName));
if (Profiles.Any(f => f.UniqueId == profile.UniqueId))
throw new Exception($"ID {profile.UniqueId} was not unique.");
Profiles.Add(profile);
}
catch (Exception ex)
{
_logger.Error($"Could not load profile, skipped:\n{ex}");
//++skipped;
}
}
foreach (var profile in Profiles)
{
if (_configuration.DefaultProfile == profile.UniqueId)
DefaultProfile = profile;
if (_configuration.DefaultLocalPlayerProfile == profile.UniqueId)
DefaultLocalPlayerProfile = profile;
}
//insert temp profiles back into profile list
if (temporaryProfiles.Count > 0)
Profiles.AddRange(temporaryProfiles);
var failed = MoveInvalidNames(invalidNames);
if (invalidNames.Count > 0)
_logger.Information(
$"Moved {invalidNames.Count - failed} profiles to correct names.{(failed > 0 ? $" Failed to move {failed} profiles to correct names." : string.Empty)}");
_logger.Information("Profiles load complete");
_event.Invoke(ProfileChanged.Type.ReloadedAll, null, null);
}
private Profile LoadIndividualProfile(JObject obj)
{
var version = obj["Version"]?.ToObject<int>() ?? 0;
return version switch
{
//Ignore everything below v4
4 => LoadV4(obj),
5 => LoadV5(obj),
_ => throw new Exception("The profile to be loaded has no valid Version."),
};
}
private Profile LoadV4(JObject obj)
{
var profile = LoadProfileV4V5(obj);
var characterName = obj["CharacterName"]?.ToObject<string>()?.Trim() ?? throw new ArgumentNullException("CharacterName");
if (string.IsNullOrWhiteSpace(characterName))
return profile;
var nameWordsCnt = characterName.Split(' ').Length;
if (_reverseNameDicts.TryGetID(ObjectKind.EventNpc, characterName, out var id))
profile.Characters.Add(_actorManager.CreateNpc(ObjectKind.EventNpc, new NpcId(id)));
else if (_reverseNameDicts.TryGetID(ObjectKind.BattleNpc, characterName, out id))
profile.Characters.Add(_actorManager.CreateNpc(ObjectKind.BattleNpc, new NpcId(id)));
else if (_reverseNameDicts.TryGetID(ObjectKind.MountType, characterName, out id))
{
var currentPlayer = _actorManager.GetCurrentPlayer();
profile.Characters.Add(_actorManager.CreateOwned(currentPlayer.PlayerName, currentPlayer.HomeWorld, ObjectKind.MountType, new NpcId(id)));
}
else if (_reverseNameDicts.TryGetID(ObjectKind.Companion, characterName, out id))
{
var currentPlayer = _actorManager.GetCurrentPlayer();
profile.Characters.Add(_actorManager.CreateOwned(currentPlayer.PlayerName, currentPlayer.HomeWorld, ObjectKind.Companion, new NpcId(id)));
}
else if (nameWordsCnt == 2)
profile.Characters.Add(_actorManager.CreatePlayer(ByteString.FromStringUnsafe(characterName, false), WorldId.AnyWorld));
else
{
_logger.Warning($"Unable to automatically migrate \"{profile.Name}\" to V5, unknown character name: {characterName}");
_messageService.NotificationMessage($"Unable to detect character type for profile \"{profile.Name}\", please set character for this profile manually.", Dalamud.Interface.ImGuiNotification.NotificationType.Error);
}
if (profile.Characters.Count > 0)
{
_logger.Debug($"Upgraded profile \"{profile.Name}\" to V5: {characterName} -> {profile.Characters[0]}. Save queued.");
_saveService.QueueSave(profile);
}
return profile;
}
private Profile LoadV5(JObject obj)
{
var profile = LoadProfileV4V5(obj);
profile.Priority = obj["Priority"]?.ToObject<int>() ?? throw new ArgumentNullException("Priority");
if (obj["Characters"] is not JArray characterArray)
return profile;
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 the same, so common loading logic is here
private Profile LoadProfileV4V5(JObject obj)
{
var creationDate = obj["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
var profile = new Profile()
{
CreationDate = creationDate,
UniqueId = obj["UniqueId"]?.ToObject<Guid>() ?? throw new ArgumentNullException("UniqueId"),
Name = new LowerString(obj["Name"]?.ToObject<string>()?.Trim() ?? throw new ArgumentNullException("Name")),
Enabled = obj["Enabled"]?.ToObject<bool>() ?? throw new ArgumentNullException("Enabled"),
ModifiedDate = obj["ModifiedDate"]?.ToObject<DateTimeOffset>() ?? creationDate,
IsWriteProtected = obj["IsWriteProtected"]?.ToObject<bool>() ?? false,
Templates = new List<Template>()
};
if (profile.ModifiedDate < creationDate)
profile.ModifiedDate = creationDate;
if (obj["Templates"] is not JArray templateArray)
return profile;
foreach (var templateObj in templateArray)
{
if (templateObj is not JObject templateObjCast)
{
//todo: warning
continue;
}
var templateId = templateObjCast["TemplateId"]?.ToObject<Guid>();
if (templateId == null)
continue; //todo: error
var template = _templateManager.GetTemplate((Guid)templateId);
if (template != null)
profile.Templates.Add(template);
}
return profile;
}
}

View File

@@ -28,13 +28,15 @@ using Penumbra.GameData.Interop;
using System.Runtime.Serialization;
using CustomizePlus.Game.Services;
using ObjectManager = CustomizePlus.GameData.Services.ObjectManager;
using System.Threading.Tasks;
using OtterGui.Classes;
namespace CustomizePlus.Profiles;
/// <summary>
/// Container class for administrating <see cref="Profile" />s during runtime.
/// </summary>
public class ProfileManager : IDisposable
public partial class ProfileManager : IDisposable
{
private readonly TemplateManager _templateManager;
private readonly TemplateEditorManager _templateEditorManager;
@@ -44,6 +46,8 @@ public class ProfileManager : IDisposable
private readonly ActorManager _actorManager;
private readonly GameObjectService _gameObjectService;
private readonly ObjectManager _objectManager;
private readonly ReverseNameDicts _reverseNameDicts;
private readonly MessageService _messageService;
private readonly ProfileChanged _event;
private readonly TemplateChanged _templateChangedEvent;
private readonly ReloadEvent _reloadEvent;
@@ -52,6 +56,7 @@ public class ProfileManager : IDisposable
public readonly List<Profile> Profiles = new();
public Profile? DefaultProfile { get; private set; }
public Profile? DefaultLocalPlayerProfile { get; private set; }
public ProfileManager(
TemplateManager templateManager,
@@ -62,6 +67,8 @@ public class ProfileManager : IDisposable
ActorManager actorManager,
GameObjectService gameObjectService,
ObjectManager objectManager,
ReverseNameDicts reverseNameDicts,
MessageService messageService,
ProfileChanged @event,
TemplateChanged templateChangedEvent,
ReloadEvent reloadEvent,
@@ -75,6 +82,8 @@ public class ProfileManager : IDisposable
_actorManager = actorManager;
_gameObjectService = gameObjectService;
_objectManager = objectManager;
_reverseNameDicts = reverseNameDicts;
_messageService = messageService;
_event = @event;
_templateChangedEvent = templateChangedEvent;
_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.ProfileManager);
@@ -85,7 +94,7 @@ public class ProfileManager : IDisposable
CreateProfileFolder(saveService);
LoadProfiles();
_reverseNameDicts.Awaiter.ContinueWith(_ => LoadProfiles(), TaskScheduler.Default);
}
public void Dispose()
@@ -93,65 +102,6 @@ public class ProfileManager : IDisposable
_templateChangedEvent.Unsubscribe(OnTemplateChange);
}
public void LoadProfiles()
{
_logger.Information("Loading profiles...");
//todo: hot reload was not tested
//save temp profiles
var temporaryProfiles = Profiles.Where(x => x.IsTemporary).ToList();
Profiles.Clear();
List<(Profile, string)> invalidNames = new();
foreach (var file in _saveService.FileNames.Profiles())
{
_logger.Debug($"Reading profile {file.FullName}");
try
{
var text = File.ReadAllText(file.FullName);
var data = JObject.Parse(text);
var profile = Profile.Load(_templateManager, data);
if (profile.UniqueId.ToString() != Path.GetFileNameWithoutExtension(file.Name))
invalidNames.Add((profile, file.FullName));
if (Profiles.Any(f => f.UniqueId == profile.UniqueId))
throw new Exception($"ID {profile.UniqueId} was not unique.");
Profiles.Add(profile);
}
catch (Exception ex)
{
_logger.Error($"Could not load profile, skipped:\n{ex}");
//++skipped;
}
}
foreach (var profile in Profiles)
{
//This will solve any issues if file on disk was manually edited and we have more than a single active profile
if (profile.Enabled)
SetEnabled(profile, true, true);
if (_configuration.DefaultProfile == profile.UniqueId)
DefaultProfile = profile;
}
//insert temp profiles back into profile list
if (temporaryProfiles.Count > 0)
{
Profiles.AddRange(temporaryProfiles);
Profiles.Sort((x, y) => y.IsTemporary.CompareTo(x.IsTemporary));
}
var failed = MoveInvalidNames(invalidNames);
if (invalidNames.Count > 0)
_logger.Information(
$"Moved {invalidNames.Count - failed} profiles to correct names.{(failed > 0 ? $" Failed to move {failed} profiles to correct names." : string.Empty)}");
_logger.Information("Profiles load complete");
_event.Invoke(ProfileChanged.Type.ReloadedAll, null, null);
}
/// <summary>
/// Main rendering function, called from rendering hook after calling ArmatureManager.OnRender
/// </summary>
@@ -227,26 +177,35 @@ public class ProfileManager : IDisposable
}
/// <summary>
/// Change character name for profile
/// Add character to profile
/// </summary>
public void ChangeCharacterName(Profile profile, string newName)
public void AddCharacter(Profile profile, ActorIdentifier actorIdentifier)
{
newName = newName.Trim();
var oldName = profile.CharacterName.Text;
if (oldName == newName)
if (!actorIdentifier.IsValid || profile.Characters.Any(x => actorIdentifier.MatchesIgnoringOwnership(x)) || profile.IsTemporary)
return;
profile.CharacterName = newName;
//Called so all other active profiles for new character name get disabled
//saving is performed there
SetEnabled(profile, profile.Enabled, true);
profile.Characters.Add(actorIdentifier);
SaveProfile(profile);
_logger.Debug($"Changed character name for profile {profile.UniqueId}.");
_event.Invoke(ProfileChanged.Type.ChangedCharacterName, profile, oldName);
_logger.Debug($"Add character for profile {profile.UniqueId}.");
_event.Invoke(ProfileChanged.Type.AddedCharacter, profile, actorIdentifier);
}
/// <summary>
/// Delete character from profile
/// </summary>
public void DeleteCharacter(Profile profile, ActorIdentifier actorIdentifier)
{
if (!actorIdentifier.IsValid || !profile.Characters.Any(x => actorIdentifier.MatchesIgnoringOwnership(x)) || profile.IsTemporary)
return;
profile.Characters.Remove(actorIdentifier);
SaveProfile(profile);
_logger.Debug($"Removed character from profile {profile.UniqueId}.");
_event.Invoke(ProfileChanged.Type.RemovedCharacter, profile, actorIdentifier);
}
/// <summary>
@@ -281,29 +240,12 @@ public class ProfileManager : IDisposable
if (profile.Enabled == value && !force)
return;
var oldValue = profile.Enabled;
if (value)
{
_logger.Debug($"Setting {profile} as enabled...");
foreach (var otherProfile in Profiles
.Where(x => x.CharacterName == profile.CharacterName && x != profile && x.Enabled && !x.IsTemporary))
{
_logger.Debug($"\t-> {otherProfile} disabled");
SetEnabled(otherProfile, false);
}
}
if (oldValue != value)
{
profile.Enabled = value;
SaveProfile(profile);
_event.Invoke(ProfileChanged.Type.Toggled, profile, value);
}
}
public void SetEnabled(Guid guid, bool value)
{
@@ -315,16 +257,20 @@ public class ProfileManager : IDisposable
else
throw new ProfileNotFoundException();
}
public void SetLimitLookupToOwned(Profile profile, bool value)
public void SetPriority(Profile profile, int value)
{
if (profile.LimitLookupToOwnedObjects != value)
{
profile.LimitLookupToOwnedObjects = value;
if (profile.Priority == value)
return;
if (value > int.MaxValue || value < int.MinValue)
return;
profile.Priority = value;
SaveProfile(profile);
_event.Invoke(ProfileChanged.Type.LimitLookupToOwnedChanged, profile, value);
}
_event.Invoke(ProfileChanged.Type.PriorityChanged, profile, value);
}
public void DeleteTemplate(Profile profile, int templateIndex)
@@ -401,40 +347,63 @@ public class ProfileManager : IDisposable
_event.Invoke(ProfileChanged.Type.ChangedDefaultProfile, profile, previousProfile);
}
public void AddTemporaryProfile(Profile profile, Actor actor/*, Template template*/)
public void SetDefaultLocalPlayerProfile(Profile? profile)
{
if (profile == null)
{
if (DefaultLocalPlayerProfile == null)
return;
}
else if (!Profiles.Contains(profile))
return;
var previousProfile = DefaultLocalPlayerProfile;
DefaultLocalPlayerProfile = profile;
_configuration.DefaultLocalPlayerProfile = profile?.UniqueId ?? Guid.Empty;
_configuration.Save();
_logger.Debug($"Set profile {profile?.Incognito ?? "no profile"} as default local player profile");
_event.Invoke(ProfileChanged.Type.ChangedDefaultLocalPlayerProfile, profile, previousProfile);
}
//warn: temporary profile system does not support any world identifiers
public void AddTemporaryProfile(Profile profile, Actor actor)
{
if (!actor.Identifier(_actorManager, out var identifier))
throw new ActorNotFoundException();
profile.Enabled = true;
profile.ProfileType = ProfileType.Temporary;
profile.TemporaryActor = identifier;
profile.CharacterName = identifier.ToNameWithoutOwnerName();
profile.LimitLookupToOwnedObjects = false;
profile.Priority = int.MaxValue; //Make sure temporary profile is always at max priority
var existingProfile = Profiles.FirstOrDefault(x => x.CharacterName.Lower == profile.CharacterName.Lower && 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.CharacterName} already exists, removing...");
_logger.Debug($"Temporary profile for {permanentIdentifier.Incognito(null)} already exists, removing...");
Profiles.Remove(existingProfile);
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, existingProfile, null);
}
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 {identifier}");
_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.CharacterName}");
_logger.Debug($"Removed temporary profile for {profile.Characters[0].Incognito(null)}");
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, profile, null);
}
@@ -453,7 +422,7 @@ public class ProfileManager : IDisposable
if (!actor.Identifier(_actorManager, out var identifier))
throw new ActorNotFoundException();
var profile = Profiles.FirstOrDefault(x => x.TemporaryActor == identifier && x.IsTemporary);
var profile = Profiles.FirstOrDefault(x => x.Characters.Count == 1 && x.Characters[0] == identifier && x.IsTemporary);
if (profile == null)
throw new ProfileNotFoundException();
@@ -461,21 +430,28 @@ public class ProfileManager : IDisposable
}
/// <summary>
/// Return profile by character name, does not return temporary profiles
/// Return profile by actor identifier, does not return temporary profiles.
/// </summary>
/// <param name="name"></param>
/// <param name="enabledOnly"></param>
/// <returns></returns>
public Profile? GetProfileByCharacterName(string name, bool enabledOnly = false)
public Profile? GetProfileByActor(Actor actor, bool enabledOnly = false)
{
if (string.IsNullOrWhiteSpace(name))
var actorIdentifier = actor.GetIdentifier(_actorManager);
if (!actorIdentifier.IsValid)
return null;
var query = Profiles.Where(x => x.CharacterName == name);
var query = Profiles.Where(p => p.Characters.Any(x => x.MatchesIgnoringOwnership(actorIdentifier)) && !p.IsTemporary);
if (enabledOnly)
query = query.Where(x => x.Enabled);
return query.FirstOrDefault();
var profile = query.OrderByDescending(x => x.Priority).FirstOrDefault();
if (profile == null)
return null;
if (actorIdentifier.Type == IdentifierType.Owned && !actorIdentifier.IsOwnedByLocalPlayer())
return null;
return profile;
}
//todo: replace with dictionary
@@ -495,32 +471,37 @@ public class ProfileManager : IDisposable
if (!actorIdentifier.IsValid)
yield break;
var name = actorIdentifier.ToNameWithoutOwnerName();
if (name.IsNullOrWhitespace())
yield break;
bool IsProfileAppliesToCurrentActor(Profile profile)
{
//default profile check is done later
if (profile == DefaultProfile)
return false;
return profile.CharacterName.Text == name &&
(!profile.LimitLookupToOwnedObjects ||
(actorIdentifier.Type == IdentifierType.Owned &&
actorIdentifier.PlayerName == _actorManager.GetCurrentPlayer().PlayerName));
if (profile == DefaultLocalPlayerProfile)
return false;
if (actorIdentifier.Type == IdentifierType.Owned && !actorIdentifier.IsOwnedByLocalPlayer())
return false;
return profile.Characters.Any(x => x.MatchesIgnoringOwnership(actorIdentifier));
}
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 (IsProfileAppliesToCurrentActor(profile) && profile.Enabled)
if(profile.Enabled && IsProfileAppliesToCurrentActor(profile))
yield return profile;
}
if (DefaultLocalPlayerProfile != null && DefaultLocalPlayerProfile.Enabled)
{
var currentPlayer = _actorManager.GetCurrentPlayer();
if(_objectManager.IsInLobby || (currentPlayer.IsValid && currentPlayer.Matches(actorIdentifier)))
yield return DefaultLocalPlayerProfile;
}
if (DefaultProfile != null &&
DefaultProfile.Enabled &&
(actorIdentifier.Type == IdentifierType.Player || actorIdentifier.Type == IdentifierType.Retainer))
@@ -532,7 +513,7 @@ public 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;
@@ -612,7 +593,7 @@ public class ProfileManager : IDisposable
if (!Profiles.Remove(profile))
return;
_logger.Debug($"ProfileManager.OnArmatureChange: Removed unused temporary profile for {profile.CharacterName}");
_logger.Debug($"ProfileManager.OnArmatureChange: Removed unused temporary profile for {profile.Characters[0].Incognito(null)}");
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, profile, null);
}

View File

@@ -16,9 +16,10 @@ namespace CustomizePlus.Templates.Data;
/// </summary>
public sealed class Template : ISavable
{
public const int Version = Constants.ConfigurationVersion;
public LowerString Name { get; internal set; } = "Template";
public int Version { get; internal set; } = Constants.ConfigurationVersion;
public DateTimeOffset CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTimeOffset ModifiedDate { get; internal set; } = DateTime.UtcNow;

View File

@@ -20,7 +20,6 @@ public class TemplateChanged() : EventWrapper<TemplateChanged.Type, Template?, o
EditorEnabled,
EditorDisabled,
EditorCharacterChanged,
EditorLimitLookupToOwnedChanged,
ReloadedAll,
WriteProtection
}

View File

@@ -2,6 +2,7 @@
using CustomizePlus.Core.Data;
using CustomizePlus.Game.Events;
using CustomizePlus.Game.Services;
using CustomizePlus.GameData.Extensions;
using CustomizePlus.Profiles;
using CustomizePlus.Profiles.Data;
using CustomizePlus.Profiles.Enums;
@@ -11,6 +12,7 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes;
using OtterGui.Log;
using Penumbra.GameData.Actors;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -63,12 +65,22 @@ public class TemplateEditorManager : IDisposable
/// <summary>
/// Name of the preview character for the editor
/// </summary>
public string CharacterName => EditorProfile.CharacterName;
public ActorIdentifier Character => EditorProfile.Characters[0];
/// <summary>
/// Checks if preview character exists at the time of call
/// </summary>
public bool IsCharacterFound => _gameObjectService.FindActorsByName(CharacterName).Count() > 0;
public bool IsCharacterFound
{
get
{
//todo: check with mounts/companions
var playerName = _gameObjectService.GetCurrentPlayerName();
return _gameObjectService.FindActorsByIdentifierIgnoringOwnership(Character)
.Where(x => x.Item1.Type != Penumbra.GameData.Enums.IdentifierType.Owned || x.Item1.IsOwnedByLocalPlayer())
.Any();
}
}
public bool IsKeepOnlyEditorProfileActive { get; set; } //todo
@@ -95,8 +107,9 @@ public class TemplateEditorManager : IDisposable
Enabled = false,
Name = "Template editor profile",
ProfileType = ProfileType.Editor,
CharacterName = configuration.EditorConfiguration.PreviewCharacterName ?? LowerString.Empty
};
EditorProfile.Characters.Add(configuration.EditorConfiguration.PreviewCharacter);
}
public void Dispose()
@@ -112,7 +125,7 @@ public class TemplateEditorManager : IDisposable
if (IsEditorActive || IsEditorPaused)
return false;
_logger.Debug($"Enabling editor profile for {template.Name} via character {CharacterName}");
_logger.Debug($"Enabling editor profile for {template.Name} via character {Character.Incognito(null)}");
CurrentlyEditedTemplateId = template.UniqueId;
_currentlyEditedTemplateOriginal = template;
@@ -124,8 +137,8 @@ public class TemplateEditorManager : IDisposable
Name = "Template editor temporary template"
};
if (string.IsNullOrWhiteSpace(CharacterName)) //safeguard
ChangeEditorCharacterInternal(_gameObjectService.GetCurrentPlayerName()); //will set EditorProfile.CharacterName
if (!Character.IsValid) //safeguard
ChangeEditorCharacterInternal(_gameObjectService.GetCurrentPlayerActorIdentifier()); //will set EditorProfile.Character
EditorProfile.Templates.Clear(); //safeguard
EditorProfile.Templates.Add(CurrentlyEditedTemplate);
@@ -133,7 +146,7 @@ public class TemplateEditorManager : IDisposable
HasChanges = false;
IsEditorActive = true;
_event.Invoke(TemplateChanged.Type.EditorEnabled, template, CharacterName);
_event.Invoke(TemplateChanged.Type.EditorEnabled, template, Character);
return true;
}
@@ -155,7 +168,7 @@ public class TemplateEditorManager : IDisposable
IsEditorActive = false;
HasChanges = false;
_event.Invoke(TemplateChanged.Type.EditorDisabled, null, CharacterName);
_event.Invoke(TemplateChanged.Type.EditorDisabled, null, Character);
return true;
}
@@ -172,36 +185,25 @@ public class TemplateEditorManager : IDisposable
_templateManager.ApplyBoneChangesAndSave(targetTemplate, CurrentlyEditedTemplate!);
}
public bool ChangeEditorCharacter(string characterName)
public bool ChangeEditorCharacter(ActorIdentifier character)
{
if (!IsEditorActive || CharacterName == characterName || IsEditorPaused || string.IsNullOrWhiteSpace(characterName))
if (!IsEditorActive || Character == character || IsEditorPaused || !character.IsValid)
return false;
return ChangeEditorCharacterInternal(characterName);
return ChangeEditorCharacterInternal(character);
}
private bool ChangeEditorCharacterInternal(string characterName)
private bool ChangeEditorCharacterInternal(ActorIdentifier character)
{
_logger.Debug($"Changing character name for editor profile from {EditorProfile.CharacterName} to {characterName}");
_logger.Debug($"Changing character name for editor profile from {EditorProfile.Characters.FirstOrDefault().Incognito(null)} to {character.Incognito(null)}");
EditorProfile.CharacterName = characterName;
EditorProfile.Characters.Clear();
EditorProfile.Characters.Add(character);
_configuration.EditorConfiguration.PreviewCharacterName = CharacterName;
_configuration.EditorConfiguration.PreviewCharacter = character;
_configuration.Save();
_event.Invoke(TemplateChanged.Type.EditorCharacterChanged, CurrentlyEditedTemplate, (characterName, EditorProfile));
return true;
}
public bool SetLimitLookupToOwned(bool value)
{
if (!IsEditorActive || IsEditorPaused || value == EditorProfile.LimitLookupToOwnedObjects)
return false;
//_profileManager.SetLimitLookupToOwned(EditorProfile, value);
EditorProfile.LimitLookupToOwnedObjects = value;
_event.Invoke(TemplateChanged.Type.EditorLimitLookupToOwnedChanged, CurrentlyEditedTemplate, EditorProfile);
_event.Invoke(TemplateChanged.Type.EditorCharacterChanged, CurrentlyEditedTemplate, (character, EditorProfile));
return true;
}
@@ -306,19 +308,19 @@ public class TemplateEditorManager : IDisposable
private void OnLogin()
{
if (_configuration.EditorConfiguration.SetPreviewToCurrentCharacterOnLogin ||
string.IsNullOrWhiteSpace(_configuration.EditorConfiguration.PreviewCharacterName))
!_configuration.EditorConfiguration.PreviewCharacter.IsValid)
{
var localPlayerName = _gameObjectService.GetCurrentPlayerName();
if(string.IsNullOrWhiteSpace(localPlayerName))
var localPlayer = _gameObjectService.GetCurrentPlayerActorIdentifier();
if(!localPlayer.IsValid)
{
_logger.Warning("Can't retrieve local player name on login");
_logger.Warning("Can't retrieve local player on login");
return;
}
if (_configuration.EditorConfiguration.PreviewCharacterName != localPlayerName)
if (_configuration.EditorConfiguration.PreviewCharacter != localPlayer)
{
_logger.Debug("Resetting editor character because automatic condition triggered in OnLogin");
ChangeEditorCharacterInternal(localPlayerName);
ChangeEditorCharacterInternal(localPlayer);
}
}
}

View File

@@ -30,8 +30,9 @@ public class CPlusWindowSystem : IDisposable
_uiBuilder.Draw += OnDraw;
_uiBuilder.OpenConfigUi += _mainWindow.Toggle;
_uiBuilder.DisableGposeUiHide = true;
_uiBuilder.DisableGposeUiHide = !configuration.UISettings.HideWindowInGPose; //seems to be broken as of 2024/10/18
_uiBuilder.DisableCutsceneUiHide = !configuration.UISettings.HideWindowInCutscene;
_uiBuilder.DisableUserUiHide = !configuration.UISettings.HideWindowWhenUiHidden;
}
private void OnDraw()

View File

@@ -0,0 +1,31 @@
using Dalamud.Interface.Utility;
using ImGuiNET;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CustomizePlus.UI;
internal class UiHelpers
{
/// <summary> Vertical spacing between groups. </summary>
public static Vector2 DefaultSpace;
/// <summary> Multiples of the current Global Scale </summary>
public static float Scale;
/// <summary> Draw default vertical space. </summary>
public static void DefaultLineSpace()
=> ImGui.Dummy(DefaultSpace);
public static void SetupCommonSizes()
{
if (ImGuiHelpers.GlobalScale != Scale)
{
Scale = ImGuiHelpers.GlobalScale;
DefaultSpace = new Vector2(0, 10 * Scale);
}
}
}

View File

@@ -24,6 +24,7 @@ public class CPlusChangeLog
Add2_0_5_0(Changelog);
Add2_0_6_0(Changelog);
Add2_0_6_3(Changelog);
Add2_0_7_0(Changelog);
}
private (int, ChangeLogDisplayType) ConfigData()
@@ -36,6 +37,38 @@ public class CPlusChangeLog
_config.Save();
}
private static void Add2_0_7_0(Changelog log)
=> log.NextVersion("Version 2.0.7.0")
.RegisterImportant("THIS IS A PRELIMINARY CHANGELOG FOR TESTING BUILD OF VERSION 2.0.7.0.")
.RegisterImportant("Some parts of Customize+ have been considerably rewritten in this update. If you encounter any issues please report them.")
.RegisterHighlight("Character management has been rewritten.")
.RegisterImportant("Customize+ will do its best to automatically migrate your profiles to new system but in some rare cases it is possible that you will have to add characters again for some of your profiles.", 1)
.RegisterEntry("New character selection user interface has been added.", 1)
.RegisterEntry("It is now possible to assign several characters to a single profile.", 2)
.RegisterEntry("The way console commands work has not changed. This means that the commands will affect profiles the same way as before, even if profile affects multiple characters.", 3)
.RegisterEntry("\"Limit to my creatures\" option has been removed as it is now obsolete.", 2)
.RegisterEntry("It is now possible to choose profile which will be applied to any character you login with.", 2)
.RegisterHighlight("Added profile priority system.")
.RegisterEntry("When several active profiles affect the same character, profile priority will be used to determine which profile will be applied to said character.", 1)
.RegisterEntry("Added additional options to configure how Customize+ window behaves.")
.RegisterEntry("Added option to configure if Customize+ windows will be hidden when you hide game UI or not.", 1)
.RegisterEntry("Added option to configure if Customize+ windows will be hidden when you enter GPose or not.", 1)
.RegisterEntry("Added option to configure if Customize+ main window will be automatically opened when you launch the game or not.", 1)
.RegisterImportant("IPC notes, developers only.")
.RegisterEntry("IPC version is now 5.2.", 1)
.RegisterEntry("I did not want to bump major version for IPC and break other plugins, so all IPC endpoints are acting as if there is still only one character per profile. This is open for discussion over at support discord.", 1)
.RegisterEntry("Profile.OnUpdate event is now being triggered for profiles with \"Apply to all players and retainers\" and \"Apply to any character you are logged in with\" options enabled.", 1)
.RegisterHighlight("Fixed issue when Customize+ did not detect changes in character skeleton. This mostly happened when altering character appearance via Glamourer and other plugins/tools.")
.RegisterEntry("Dropped support for upgrading from Customize+ 1.0. Clipboard copies are not affected by this change.")
.RegisterEntry("Source code maintenance - external libraries update.");
private static void Add2_0_6_3(Changelog log)
=> log.NextVersion("Version 2.0.6.3")
.RegisterEntry("Added new IPC methods: GameState.GetCutsceneParentIndex, GameState.SetCutsceneParentIndex.")

View File

@@ -0,0 +1,157 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Custom;
using OtterGui.Log;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.DataContainers.Bases;
using Penumbra.GameData.Gui;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.String;
namespace CustomizePlus.UI.Windows.Controls;
public class ActorAssignmentUi
{
private readonly ActorManager _actorManager;
private WorldCombo _worldCombo = null!;
private NpcCombo _mountCombo = null!;
private NpcCombo _companionCombo = null!;
private NpcCombo _bnpcCombo = null!;
private NpcCombo _enpcCombo = null!;
private bool _ready;
private string _newCharacterName = string.Empty;
private ObjectKind _newKind = ObjectKind.BattleNpc;
public ActorIdentifier NpcIdentifier { get; private set; } = ActorIdentifier.Invalid;
public ActorIdentifier PlayerIdentifier { get; private set; } = ActorIdentifier.Invalid;
public ActorIdentifier RetainerIdentifier { get; private set; } = ActorIdentifier.Invalid;
public ActorIdentifier MannequinIdentifier { get; private set; } = ActorIdentifier.Invalid;
public bool CanSetPlayer
=> PlayerIdentifier.IsValid;
public bool CanSetRetainer
=> RetainerIdentifier.IsValid;
public bool CanSetMannequin
=> MannequinIdentifier.IsValid;
public bool CanSetNpc
=> NpcIdentifier.IsValid;
public ActorAssignmentUi(ActorManager actorManager, DictModelChara dictModelChara, DictBNpcNames bNpcNames, DictBNpc bNpc)
{
_actorManager = actorManager;
_actorManager.Awaiter.ContinueWith(_ => SetupCombos(), TaskScheduler.Default);
}
public void DrawWorldCombo(float width)
{
if (_ready && _worldCombo.Draw(width))
UpdateIdentifiersInternal();
}
public void DrawPlayerInput(float width)
{
if (!_ready)
return;
ImGui.SetNextItemWidth(width);
if (ImGui.InputTextWithHint("##NewCharacter", "Character Name...", ref _newCharacterName, 32))
UpdateIdentifiersInternal();
}
public void DrawObjectKindCombo(float width)
{
if (_ready && IndividualHelpers.DrawObjectKindCombo(width, _newKind, out _newKind, ObjectKinds))
UpdateIdentifiersInternal();
}
public void DrawNpcInput(float width)
{
if (!_ready)
return;
var combo = GetNpcCombo(_newKind);
if (combo.Draw(width))
UpdateIdentifiersInternal();
}
private static readonly IReadOnlyList<ObjectKind> ObjectKinds = new[]
{
ObjectKind.BattleNpc,
ObjectKind.EventNpc,
ObjectKind.Companion,
ObjectKind.MountType,
};
private Penumbra.GameData.Gui.NpcCombo GetNpcCombo(ObjectKind kind)
=> kind switch
{
ObjectKind.BattleNpc => _bnpcCombo,
ObjectKind.EventNpc => _enpcCombo,
ObjectKind.MountType => _mountCombo,
ObjectKind.Companion => _companionCombo,
_ => throw new NotImplementedException(),
};
private void SetupCombos()
{
_worldCombo = new WorldCombo(_actorManager.Data.Worlds, Plugin.Logger);
_mountCombo = new Penumbra.GameData.Gui.NpcCombo("##mountCombo", _actorManager.Data.Mounts, Plugin.Logger);
_companionCombo = new Penumbra.GameData.Gui.NpcCombo("##companionCombo", _actorManager.Data.Companions, Plugin.Logger);
_bnpcCombo = new Penumbra.GameData.Gui.NpcCombo("##bnpcCombo", _actorManager.Data.BNpcs, Plugin.Logger);
_enpcCombo = new Penumbra.GameData.Gui.NpcCombo("##enpcCombo", _actorManager.Data.ENpcs, Plugin.Logger);
_ready = true;
}
private void UpdateIdentifiersInternal()
{
if (ByteString.FromString(_newCharacterName, out var byteName))
{
PlayerIdentifier = _actorManager.CreatePlayer(byteName, _worldCombo.CurrentSelection.Key);
RetainerIdentifier = _actorManager.CreateRetainer(byteName, ActorIdentifier.RetainerType.Bell);
MannequinIdentifier = _actorManager.CreateRetainer(byteName, ActorIdentifier.RetainerType.Mannequin);
}
var npcCombo = GetNpcCombo(_newKind);
if (npcCombo.CurrentSelection.Ids == null || npcCombo.CurrentSelection.Ids.Length == 0)
NpcIdentifier = ActorIdentifier.Invalid;
else
{
switch (_newKind)
{
case ObjectKind.BattleNpc:
case ObjectKind.EventNpc:
NpcIdentifier = _actorManager.CreateNpc(_newKind, npcCombo.CurrentSelection.Ids[0]);
break;
case ObjectKind.MountType:
case ObjectKind.Companion:
var currentPlayer = _actorManager.GetCurrentPlayer();
NpcIdentifier = _actorManager.CreateOwned(currentPlayer.PlayerName, currentPlayer.HomeWorld, _newKind, npcCombo.CurrentSelection.Ids[0]);
break;
default:
throw new NotImplementedException();
}
}
}
}

View File

@@ -78,14 +78,13 @@ public class MainWindow : Window, IDisposable
_templateEditorEvent.Subscribe(OnTemplateEditorEvent, TemplateEditorEvent.Priority.MainWindow);
pluginInterface.UiBuilder.DisableGposeUiHide = true;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(700, 675),
MaximumSize = ImGui.GetIO().DisplaySize,
};
IsOpen = pluginInterface.IsDevMenuOpen && configuration.DebuggingModeEnabled;
IsOpen = configuration.UISettings.OpenWindowAtStart;
}
public void Dispose()

View File

@@ -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();
@@ -134,14 +134,14 @@ public class StateMonitoringTab
private void DrawSingleProfile(string prefix, Profile profile)
{
string name = profile.Name;
string characterName = profile.CharacterName;
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}");
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;
@@ -149,7 +149,6 @@ public class StateMonitoringTab
ImGui.Text($"ID: {profile.UniqueId}");
ImGui.Text($"Enabled: {(profile.Enabled ? "Enabled" : "Disabled")}");
ImGui.Text($"State : {(profile.IsTemporary ? "Temporary" : "Permanent")}");
ImGui.Text($"Lookup: {(profile.LimitLookupToOwnedObjects ? "Limited lookup" : "Global lookup")}");
var showTemplates = ImGui.CollapsingHeader($"Templates###{prefix}-profile-{profile.UniqueId}-templates");
if (showTemplates)

View File

@@ -16,6 +16,8 @@ using CustomizePlus.Configuration.Data;
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;
@@ -132,7 +134,8 @@ public class ProfileFileSystemSelector : FileSystemSelector<Profile, ProfileStat
case ProfileChanged.Type.Deleted:
case ProfileChanged.Type.Renamed:
case ProfileChanged.Type.Toggled:
case ProfileChanged.Type.ChangedCharacterName:
case ProfileChanged.Type.AddedCharacter:
case ProfileChanged.Type.RemovedCharacter:
case ProfileChanged.Type.ReloadedAll:
SetFilterDirty();
break;
@@ -238,10 +241,14 @@ public class ProfileFileSystemSelector : FileSystemSelector<Profile, ProfileStat
return false;
}
//todo: priority check
var identifier = _gameObjectService.GetCurrentPlayerActorIdentifier();
if (leaf.Value.Enabled)
state.Color = leaf.Value.CharacterName == _gameObjectService.GetCurrentPlayerName() ? ColorId.LocalCharacterEnabledProfile : ColorId.EnabledProfile;
state.Color = leaf.Value.Characters.Any(x => x.MatchesIgnoringOwnership(identifier)) ? ColorId.LocalCharacterEnabledProfile : ColorId.EnabledProfile;
else
state.Color = leaf.Value.CharacterName == _gameObjectService.GetCurrentPlayerName() ? 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);
}

View File

@@ -13,6 +13,12 @@ using CustomizePlus.UI.Windows.Controls;
using CustomizePlus.Templates;
using CustomizePlus.Core.Data;
using CustomizePlus.Templates.Events;
using Penumbra.GameData.Actors;
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;
@@ -23,10 +29,12 @@ public class ProfilePanel
private readonly PluginConfiguration _configuration;
private readonly TemplateCombo _templateCombo;
private readonly TemplateEditorManager _templateEditorManager;
private readonly ActorAssignmentUi _actorAssignmentUi;
private readonly ActorManager _actorManager;
private readonly TemplateEditorEvent _templateEditorEvent;
private string? _newName;
private string? _newCharacterName;
private int? _newPriority;
private Profile? _changedProfile;
private Action? _endAction;
@@ -42,6 +50,8 @@ public class ProfilePanel
PluginConfiguration configuration,
TemplateCombo templateCombo,
TemplateEditorManager templateEditorManager,
ActorAssignmentUi actorAssignmentUi,
ActorManager actorManager,
TemplateEditorEvent templateEditorEvent)
{
_selector = selector;
@@ -49,6 +59,8 @@ public class ProfilePanel
_configuration = configuration;
_templateCombo = templateCombo;
_templateEditorManager = templateEditorManager;
_actorAssignmentUi = actorAssignmentUi;
_actorManager = actorManager;
_templateEditorEvent = templateEditorEvent;
}
@@ -133,9 +145,22 @@ public class ProfilePanel
return;
DrawEnabledSetting();
ImGui.Separator();
using (var disabled = ImRaii.Disabled(_selector.Selected?.IsWriteProtected ?? true))
{
DrawBasicSettings();
ImGui.Separator();
var isShouldDraw = ImGui.CollapsingHeader("Character settings");
if (isShouldDraw)
DrawCharacterArea();
ImGui.Separator();
DrawTemplateArea();
}
}
@@ -154,25 +179,6 @@ public class ProfilePanel
ImGuiUtil.LabeledHelpMarker("Enabled",
"Whether the templates in this profile should be applied at all. Only one profile can be enabled for a character at the same time.");
}
ImGui.SameLine();
var isDefault = _manager.DefaultProfile == _selector.Selected;
var isDefaultOrCurrentProfilesEnabled = _manager.DefaultProfile?.Enabled ?? false || enabled;
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. This 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.");
}
}
}
@@ -211,48 +217,200 @@ public class ProfilePanel
ImGui.TableNextRow();
ImGuiUtil.DrawFrameColumn("Character Name");
ImGuiUtil.DrawFrameColumn("Priority");
ImGui.TableNextColumn();
width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0);
name = _newCharacterName ?? _selector.Selected!.CharacterName;
ImGui.SetNextItemWidth(width.X);
if(_manager.DefaultProfile != _selector.Selected)
var priority = _newPriority ?? _selector.Selected!.Priority;
ImGui.SetNextItemWidth(50);
if (ImGui.InputInt("##Priority", ref priority, 0, 0))
{
if (!_selector.IncognitoMode)
{
if (ImGui.InputText("##CharacterName", ref name, 128))
{
_newCharacterName = name;
_newPriority = priority;
_changedProfile = _selector.Selected;
}
if (ImGui.IsItemDeactivatedAfterEdit() && _changedProfile != null)
{
_manager.ChangeCharacterName(_changedProfile, name);
_newCharacterName = 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.");
}
else
ImGui.TextUnformatted("Incognito active");
}
}
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();
var enabled = _selector.Selected?.LimitLookupToOwnedObjects ?? false;
if (ImGui.Checkbox("##LimitLookupToOwnedObjects", ref enabled))
_manager.SetLimitLookupToOwned(_selector.Selected!, enabled);
ImGuiUtil.LabeledHelpMarker("Limit to my creatures",
"When enabled limits the character search to only your own summons, mounts and minions.\nUseful when there is possibility there will be another character with that name owned by another player.\n* For battle chocobo use \"Chocobo\" as character name.");
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");
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.TextUnformatted("All players and retainers");
{
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.");
}
}
}
_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;
@@ -343,4 +501,9 @@ public class ProfilePanel
}
}
}
private void UpdateIdentifiers()
{
}
}

View File

@@ -14,6 +14,7 @@ using CustomizePlus.Templates;
using CustomizePlus.Core.Helpers;
using CustomizePlus.Armatures.Services;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs;
@@ -21,6 +22,7 @@ public class SettingsTab
{
private const uint DiscordColor = 0xFFDA8972;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly PluginConfiguration _configuration;
private readonly ArmatureManager _armatureManager;
private readonly HookingService _hookingService;
@@ -30,6 +32,7 @@ public class SettingsTab
private readonly SupportLogBuilderService _supportLogBuilderService;
public SettingsTab(
IDalamudPluginInterface pluginInterface,
PluginConfiguration configuration,
ArmatureManager armatureManager,
HookingService hookingService,
@@ -38,6 +41,7 @@ public class SettingsTab
MessageService messageService,
SupportLogBuilderService supportLogBuilderService)
{
_pluginInterface = pluginInterface;
_configuration = configuration;
_armatureManager = armatureManager;
_hookingService = hookingService;
@@ -49,6 +53,7 @@ public class SettingsTab
public void Draw()
{
UiHelpers.SetupCommonSizes();
using var child = ImRaii.Child("MainWindowChild");
if (!child)
return;
@@ -209,16 +214,40 @@ public class SettingsTab
if (!isShouldDraw)
return;
DrawOpenWindowAtStart();
DrawHideWindowInCutscene();
DrawHideWindowWhenUiHidden();
DrawHideWindowInGPose();
UiHelpers.DefaultLineSpace();
DrawFoldersDefaultOpen();
UiHelpers.DefaultLineSpace();
DrawSetPreviewToCurrentCharacterOnLogin();
UiHelpers.DefaultLineSpace();
if (Widget.DoubleModifierSelector("Template Deletion Modifier",
"A modifier you need to hold while clicking the Delete Template button for it to take effect.", 100 * ImGuiHelpers.GlobalScale,
_configuration.UISettings.DeleteTemplateModifier, v => _configuration.UISettings.DeleteTemplateModifier = v))
_configuration.Save();
}
private void DrawOpenWindowAtStart()
{
var isChecked = _configuration.UISettings.OpenWindowAtStart;
if (CtrlHelper.CheckboxWithTextAndHelp("##openwindowatstart", "Open Customize+ Window at Game Start",
"Controls whether main Customize+ window will be opened when you launch the game or not.", ref isChecked))
{
_configuration.UISettings.OpenWindowAtStart = isChecked;
_configuration.Save();
}
}
private void DrawHideWindowInCutscene()
{
var isChecked = _configuration.UISettings.HideWindowInCutscene;
@@ -226,7 +255,35 @@ public class SettingsTab
if (CtrlHelper.CheckboxWithTextAndHelp("##hidewindowincutscene", "Hide Plugin Windows in Cutscenes",
"Controls whether any Customize+ windows are hidden during cutscenes or not.", ref isChecked))
{
_pluginInterface.UiBuilder.DisableCutsceneUiHide = !isChecked;
_configuration.UISettings.HideWindowInCutscene = isChecked;
_configuration.Save();
}
}
private void DrawHideWindowWhenUiHidden()
{
var isChecked = _configuration.UISettings.HideWindowWhenUiHidden;
if (CtrlHelper.CheckboxWithTextAndHelp("##hidewindowwhenuihidden", "Hide Plugin Windows when UI is Hidden",
"Controls whether any Customize+ windows are hidden when you manually hide the in-game user interface.", ref isChecked))
{
_pluginInterface.UiBuilder.DisableUserUiHide = !isChecked;
_configuration.UISettings.HideWindowWhenUiHidden = isChecked;
_configuration.Save();
}
}
private void DrawHideWindowInGPose()
{
var isChecked = _configuration.UISettings.HideWindowInGPose;
if (CtrlHelper.CheckboxWithTextAndHelp("##hidewindowingpose", "Hide Plugin Windows in GPose",
"Controls whether any Customize+ windows are hidden when you enter GPose.", ref isChecked))
{
_pluginInterface.UiBuilder.DisableGposeUiHide = !isChecked;
_configuration.UISettings.HideWindowInGPose = isChecked;
_configuration.Save();
}
}

View File

@@ -15,6 +15,10 @@ using CustomizePlus.Core.Helpers;
using CustomizePlus.Templates;
using CustomizePlus.Game.Services;
using CustomizePlus.Templates.Data;
using CustomizePlus.UI.Windows.Controls;
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using Penumbra.GameData.Actors;
using CustomizePlus.GameData.Extensions;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
@@ -24,6 +28,7 @@ public class BoneEditorPanel
private readonly TemplateEditorManager _editorManager;
private readonly PluginConfiguration _configuration;
private readonly GameObjectService _gameObjectService;
private readonly ActorAssignmentUi _actorAssignmentUi;
private BoneAttribute _editingAttribute;
private int _precision;
@@ -31,8 +36,6 @@ public class BoneEditorPanel
private bool _isShowLiveBones;
private bool _isMirrorModeEnabled;
private string? _newCharacterName;
private Dictionary<BoneData.BoneFamily, bool> _groupExpandedState = new();
private bool _openSavePopup;
@@ -48,12 +51,14 @@ public class BoneEditorPanel
TemplateFileSystemSelector templateFileSystemSelector,
TemplateEditorManager editorManager,
PluginConfiguration configuration,
GameObjectService gameObjectService)
GameObjectService gameObjectService,
ActorAssignmentUi actorAssignmentUi)
{
_templateFileSystemSelector = templateFileSystemSelector;
_editorManager = editorManager;
_configuration = configuration;
_gameObjectService = gameObjectService;
_actorAssignmentUi = actorAssignmentUi;
_isShowLiveBones = configuration.EditorConfiguration.ShowLiveBones;
_isMirrorModeEnabled = configuration.EditorConfiguration.BoneMirroringEnabled;
@@ -65,7 +70,7 @@ public class BoneEditorPanel
{
if (_editorManager.EnableEditor(template))
{
_editorManager.SetLimitLookupToOwned(_configuration.EditorConfiguration.LimitLookupToOwnedObjects);
//_editorManager.SetLimitLookupToOwned(_configuration.EditorConfiguration.LimitLookupToOwnedObjects);
return true;
}
@@ -96,53 +101,58 @@ public class BoneEditorPanel
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f)))
{
using (var table = ImRaii.Table("BasicSettings", 2))
{
ImGui.TableSetupColumn("BasicCol1", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Show editor preview on").X);
ImGui.TableSetupColumn("BasicCol2", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextRow();
var isShouldDraw = ImGui.CollapsingHeader("Preview settings");
ImGuiUtil.DrawFrameColumn("Show editor preview on");
ImGui.TableNextColumn();
if (isShouldDraw)
{
var width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0);
var name = _newCharacterName ?? _editorManager.CharacterName;
ImGui.SetNextItemWidth(width.X);
using (var disabled = ImRaii.Disabled(!IsEditorActive || IsEditorPaused))
{
if (!_templateFileSystemSelector.IncognitoMode)
{
if (ImGui.InputText("##PreviewCharacterName", ref name, 128))
{
_newCharacterName = name;
}
ImGui.Text(_editorManager.Character.IsValid ? $"Applies to {(_editorManager.Character.Type == Penumbra.GameData.Enums.IdentifierType.Owned ?
_editorManager.Character.ToNameWithoutOwnerName() : _editorManager.Character.ToString())}" : "No valid character selected");
ImGui.Separator();
if (ImGui.IsItemDeactivatedAfterEdit())
{
if (string.IsNullOrWhiteSpace(_newCharacterName))
_newCharacterName = _gameObjectService.GetCurrentPlayerName();
_actorAssignmentUi.DrawWorldCombo(width.X / 2);
ImGui.SameLine();
_actorAssignmentUi.DrawPlayerInput(width.X / 2);
_editorManager.ChangeEditorCharacter(_newCharacterName);
var buttonWidth = new Vector2(165 * ImGuiHelpers.GlobalScale - ImGui.GetStyle().ItemSpacing.X / 2, 0);
_newCharacterName = null;
}
if (ImGuiUtil.DrawDisabledButton("Apply to player character", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetPlayer))
_editorManager.ChangeEditorCharacter(_actorAssignmentUi.PlayerIdentifier);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Apply to retainer", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetRetainer))
_editorManager.ChangeEditorCharacter(_actorAssignmentUi.RetainerIdentifier);
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Apply to mannequin", buttonWidth, string.Empty, !_actorAssignmentUi.CanSetMannequin))
_editorManager.ChangeEditorCharacter(_actorAssignmentUi.MannequinIdentifier);
var currentPlayer = _gameObjectService.GetCurrentPlayerActorIdentifier();
if (ImGuiUtil.DrawDisabledButton("Apply to current character", buttonWidth, string.Empty, !currentPlayer.IsValid))
_editorManager.ChangeEditorCharacter(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))
_editorManager.ChangeEditorCharacter(_actorAssignmentUi.NpcIdentifier);
}
else
ImGui.TextUnformatted("Incognito active");
}
}
ImGui.SameLine();
var enabled = _editorManager.EditorProfile.LimitLookupToOwnedObjects;
if (ImGui.Checkbox("##LimitLookupToOwnedObjects", ref enabled))
{
_editorManager.SetLimitLookupToOwned(enabled);
_configuration.EditorConfiguration.LimitLookupToOwnedObjects = enabled;
_configuration.Save();
}
ImGuiUtil.LabeledHelpMarker("Limit to my creatures",
"When enabled limits the character search to only your own summons, mounts and minions.\nUseful when there is possibility there will be another character with that name owned by another player.\n* For battle chocobo use \"Chocobo\" as character name.");
}
}
ImGui.Separator();
using (var table = ImRaii.Table("BoneEditorMenu", 2))
{

View File

@@ -105,17 +105,6 @@ public class TemplatePanel : IDisposable
Disabled = _boneEditor.IsEditorActive
};
/*private HeaderDrawer.Button SetFromClipboardButton()
=> new()
{
Description =
"Try to apply a template from your clipboard over this template.",
Icon = FontAwesomeIcon.Clipboard,
OnClick = SetFromClipboard,
Visible = _selector.Selected != null,
Disabled = (_selector.Selected?.IsWriteProtected ?? true) || _boneEditor.IsEditorActive,
};*/
private HeaderDrawer.Button ExportToClipboardButton()
=> new()
{