diff --git a/CustomizePlus/Api/Compatibility/CustomizePlusLegacyIpc.cs b/CustomizePlus/Api/Compatibility/CustomizePlusLegacyIpc.cs
index a86cf4a..3d61f90 100644
--- a/CustomizePlus/Api/Compatibility/CustomizePlusLegacyIpc.cs
+++ b/CustomizePlus/Api/Compatibility/CustomizePlusLegacyIpc.cs
@@ -24,6 +24,7 @@ using CustomizePlus.GameData.Extensions;
namespace CustomizePlus.Api.Compatibility;
+[Obsolete("Will be removed in the next release")]
public class CustomizePlusLegacyIpc : IDisposable
{
private readonly IObjectTable _objectTable;
diff --git a/CustomizePlus/Api/CustomizePlusIpc.Profile.cs b/CustomizePlus/Api/CustomizePlusIpc.Profile.cs
index 120865b..1a9a28b 100644
--- a/CustomizePlus/Api/CustomizePlusIpc.Profile.cs
+++ b/CustomizePlus/Api/CustomizePlusIpc.Profile.cs
@@ -3,13 +3,36 @@ using System;
using System.Collections.Generic;
using System.Linq;
using ECommons.EzIpcManager;
+using Newtonsoft.Json;
+using CustomizePlus.Api.Data;
+using CustomizePlus.GameData.Data;
+using CustomizePlus.Api.Enums;
+using CustomizePlus.Profiles.Exceptions;
+using CustomizePlus.Profiles.Data;
+using CustomizePlus.Core.Extensions;
+using CustomizePlus.Profiles.Events;
+using CustomizePlus.Armatures.Data;
+using CustomizePlus.Armatures.Events;
+using CustomizePlus.GameData.Extensions;
+using Dalamud.Game.ClientState.Objects.Types;
+using Penumbra.GameData.Actors;
using IPCProfileDataTuple = (System.Guid UniqueId, string Name, string CharacterName, bool IsEnabled);
+//using OnUpdateTuple = (Dalamud.Game.ClientState.Objects.Types.Character Character, System.Guid? ProfileUniqueId, string? ProfileJson);
namespace CustomizePlus.Api;
public partial class CustomizePlusIpc
{
+ ///
+ /// Triggered when changes in currently active profiles are detected. (like changing active profile or making any changes to it)
+ /// Not triggered if any changes happen due to character no longer existing.
+ /// Right now ignores every character but local player.
+ /// Ignores temporary profiles.
+ ///
+ [EzIPCEvent("Profile.OnUpdate")]
+ private Action OnProfileUpdate;
+
///
/// Retrieve list of all user profiles
///
@@ -24,21 +47,302 @@ public partial class CustomizePlusIpc
}
///
- /// Enable profile using its Unique ID
+ /// Get JSON copy of profile with specified unique id
///
- ///
- [EzIPC("Profile.EnableByUniqueId")]
- private void EnableProfileByUniqueId(Guid uniqueId)
+ [EzIPC("Profile.GetProfileById")]
+ private (int, string?) GetProfileById(Guid uniqueId)
{
- _profileManager.SetEnabled(uniqueId, true);
+ if (uniqueId == Guid.Empty)
+ return ((int)ErrorCode.ProfileNotFound, null);
+
+ var profile = _profileManager.Profiles.Where(x => x.UniqueId == uniqueId && !x.IsTemporary).FirstOrDefault(); //todo: move into profile manager
+
+ if (profile == null)
+ return ((int)ErrorCode.ProfileNotFound, null);
+
+ var convertedProfile = IPCCharacterProfile.FromFullProfile(profile);
+
+ if (convertedProfile == null)
+ {
+ _logger.Error($"IPCCharacterProfile.FromFullProfile returned empty converted profile for id: {uniqueId}");
+ return ((int)ErrorCode.UnknownError, null);
+ }
+
+ try
+ {
+ return ((int)ErrorCode.Success, JsonConvert.SerializeObject(convertedProfile));
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Exception in IPCCharacterProfile.FromFullProfile for id {uniqueId}: {ex}");
+ return ((int)ErrorCode.UnknownError, null);
+ }
}
///
- /// Disable profile using its Unique ID
+ /// Enable profile using its Unique ID. Does not work on temporary profiles.
+ ///
+ ///
+ [EzIPC("Profile.EnableByUniqueId")]
+ private ErrorCode EnableProfileByUniqueId(Guid uniqueId)
+ {
+ return SetProfileStateInternal(uniqueId, true);
+ }
+
+ ///
+ /// Disable profile using its Unique ID. Does not work on temporary profiles.
///
[EzIPC("Profile.DisableByUniqueId")]
- private void DisableProfileByUniqueId(Guid uniqueId)
+ private ErrorCode DisableProfileByUniqueId(Guid uniqueId)
{
- _profileManager.SetEnabled(uniqueId, false);
+ return SetProfileStateInternal(uniqueId, false);
+ }
+
+ private ErrorCode SetProfileStateInternal(Guid uniqueId, bool state)
+ {
+ if (uniqueId == Guid.Empty)
+ return ErrorCode.ProfileNotFound;
+
+ try
+ {
+ _profileManager.SetEnabled(uniqueId, true);
+ return ErrorCode.Success;
+ }
+ catch (ProfileNotFoundException ex)
+ {
+ return ErrorCode.ProfileNotFound;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Exception in SetProfileStateInternal. Unique id: {uniqueId}, state: {state}, exception: {ex}.");
+ return ErrorCode.UnknownError;
+ }
+ }
+
+ ///
+ /// Get JSON copy of active profile for character.
+ ///
+ [EzIPC("Profile.GetCurrentlyActiveProfileOnCharacter")]
+ private (int, string?) GetCurrentlyActiveProfileOnCharacter(Character character)
+ {
+ if (character == null)
+ return ((int)ErrorCode.InvalidCharacter, null);
+
+ var profile = _profileManager.GetProfileByCharacterName(character.Name.ToString(), true);
+
+ if (profile == null)
+ return ((int)ErrorCode.ProfileNotFound, null);
+
+ var convertedProfile = IPCCharacterProfile.FromFullProfile(profile);
+
+ if (convertedProfile == null)
+ {
+ _logger.Error($"IPCCharacterProfile.FromFullProfile returned empty converted profile for character {character?.Name.ToString().Incognify()}, profile: {profile.UniqueId}");
+ return ((int)ErrorCode.UnknownError, null);
+ }
+
+ try
+ {
+ return ((int)ErrorCode.Success, JsonConvert.SerializeObject(convertedProfile));
+ }
+ catch(Exception ex)
+ {
+ _logger.Error($"Exception in IPCCharacterProfile.FromFullProfile for character {character?.Name.ToString().Incognify()}, profile: {profile.UniqueId}: {ex}");
+ return ((int)ErrorCode.UnknownError, null);
+ }
+ }
+
+ ///
+ /// Apply provided profile as temporary profile on specified character.
+ /// Returns profile's unique id which can be used to manipulate it at a later date.
+ ///
+ [EzIPC("Profile.SetTemporaryProfileOnCharacter")]
+ private (int, Guid?) SetTemporaryProfileOnCharacter(Character character, string profileJson)
+ {
+ if (character == null)
+ return ((int)ErrorCode.InvalidCharacter, null);
+
+ var actor = (Actor)character.Address;
+ if (!actor.Valid)
+ return ((int)ErrorCode.InvalidCharacter, null);
+
+ /*if (character == _objectTable[0])
+ {
+ _logger.Error($"Received request to set profile on local character, this is not allowed");
+ return;
+ }*/
+
+ try
+ {
+ IPCCharacterProfile? profile;
+ try
+ {
+ profile = JsonConvert.DeserializeObject(profileJson);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"IPCCharacterProfile deserialization issue. Character: {character?.Name.ToString().Incognify()}, exception: {ex}.");
+ return ((int)ErrorCode.CorruptedProfile, null);
+ }
+
+ if (profile == null)
+ {
+ _logger.Error($"IPCCharacterProfile is null after deserialization. Character: {character?.Name.ToString().Incognify()}.");
+ return ((int)ErrorCode.CorruptedProfile, null);
+ }
+
+ //todo: ideally we'd probably want to make sure ID returned by that function does not have collision with other profiles
+ var fullProfile = IPCCharacterProfile.ToFullProfile(profile).Item1;
+
+ _profileManager.AddTemporaryProfile(fullProfile, actor);
+ return ((int)ErrorCode.Success, fullProfile.UniqueId);
+
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Unable to set temporary profile. Character: {character?.Name.ToString().Incognify()}, exception: {ex}.");
+ return ((int)ErrorCode.UnknownError, null);
+ }
+ }
+
+ ///
+ /// Delete temporary profile currently active on character
+ ///
+ [EzIPC("Profile.DeleteTemporaryProfileOnCharacter")]
+ private int DeleteTemporaryProfileOnCharacter(Character character)
+ {
+ if (character == null)
+ return (int)ErrorCode.InvalidCharacter;
+
+ var actor = (Actor)character.Address;
+ if (!actor.Valid)
+ return (int)ErrorCode.InvalidCharacter;
+
+ /*if (character == _objectTable[0])
+ {
+ _logger.Error($"Received request to revert profile on local character, this is not allowed");
+ return;
+ }*/
+
+ try
+ {
+ _profileManager.RemoveTemporaryProfile(actor);
+ return (int)ErrorCode.Success;
+ }
+ catch(ProfileException ex)
+ {
+ switch(ex)
+ {
+ case ActorNotFoundException _:
+ return (int)ErrorCode.InvalidCharacter;
+ case ProfileNotFoundException:
+ return (int)ErrorCode.ProfileNotFound;
+ default:
+ _logger.Error($"Exception in DeleteTemporaryProfileOnCharacter. Character: {character?.Name.ToString().Incognify()}. Exception: {ex}");
+ return (int)ErrorCode.UnknownError;
+ }
+ }
+ catch(Exception ex)
+ {
+ _logger.Error($"Exception in DeleteTemporaryProfileOnCharacter. Character: {character?.Name.ToString().Incognify()}. Exception: {ex}");
+ return (int)ErrorCode.UnknownError;
+ }
+ }
+
+ ///
+ /// Delete temporary profile using its unique id
+ ///
+ [EzIPC("Profile.DeleteTemporaryProfileByUniqueId")]
+ private int DeleteTemporaryProfileByUniqueId(Guid uniqueId)
+ {
+ if (uniqueId == Guid.Empty)
+ return (int)ErrorCode.ProfileNotFound;
+
+ try
+ {
+ _profileManager.RemoveTemporaryProfile(uniqueId);
+ return (int)ErrorCode.Success;
+ }
+ catch (ProfileException ex)
+ {
+ switch (ex)
+ {
+ case ActorNotFoundException _:
+ return (int)ErrorCode.InvalidCharacter; //note: this is not considered an error for this case, returned just so external caller knows what is going on
+ case ProfileNotFoundException:
+ return (int)ErrorCode.ProfileNotFound;
+ default:
+ _logger.Error($"Exception in DeleteTemporaryProfileOnCharacter. Unique id: {uniqueId}. Exception: {ex}");
+ return (int)ErrorCode.UnknownError;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error($"Exception in DeleteTemporaryProfileOnCharacter. Unique id: {uniqueId}. Exception: {ex}");
+ return (int)ErrorCode.UnknownError;
+ }
+ }
+
+ //warn: limitation - ignores default profiles but why you would use default profile on your own character
+ private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? arg3)
+ {
+ if (type != ProfileChanged.Type.AddedTemplate &&
+ type != ProfileChanged.Type.RemovedTemplate &&
+ type != ProfileChanged.Type.MovedTemplate &&
+ type != ProfileChanged.Type.ChangedTemplate &&
+ type != ProfileChanged.Type.Toggled)
+ return;
+
+ if (profile == null ||
+ !profile.Enabled || //profile = null event will be sent from OnArmatureChanged
+ profile.CharacterName.Text != _gameObjectService.GetCurrentPlayerName())
+ return;
+
+ Character? localPlayerCharacter = (Character?)_gameObjectService.GetDalamudGameObjectFromActor(_gameObjectService.GetLocalPlayerActor());
+ if (localPlayerCharacter == null)
+ return;
+
+ OnProfileUpdateInternal(localPlayerCharacter, profile);
+ }
+
+ private void OnArmatureChanged(ArmatureChanged.Type type, Armature armature, object? arg3)
+ {
+ string currentPlayerName = _gameObjectService.GetCurrentPlayerName();
+
+ if (armature.ActorIdentifier.ToNameWithoutOwnerName() != currentPlayerName)
+ return;
+
+ Character? localPlayerCharacter = (Character?)_gameObjectService.GetDalamudGameObjectFromActor(_gameObjectService.GetLocalPlayerActor());
+ if (localPlayerCharacter == null)
+ return;
+
+ if (type == ArmatureChanged.Type.Created || //todo: might create second call after OnProfileChange?
+ type == ArmatureChanged.Type.Rebound)
+ {
+ if (armature.Profile == null)
+ _logger.Warning("Armature created/rebound and profile is null");
+
+ OnProfileUpdateInternal(localPlayerCharacter, armature.Profile);
+ return;
+ }
+
+ if (type == ArmatureChanged.Type.Deleted)
+ {
+ OnProfileUpdateInternal(localPlayerCharacter, null);
+ return;
+ }
+ }
+
+ private void OnProfileUpdateInternal(Character character, Profile? profile)
+ {
+ if (character == null)
+ return;
+
+ if (profile != null && profile.IsTemporary)
+ return;
+
+ _logger.Debug($"Sending player update message: Character: {character.Name.ToString().Incognify()}, Profile: {(profile != null ? profile.ToString() : "no profile")}");
+
+ OnProfileUpdate(character, profile != null ? profile.UniqueId : null);
}
}
diff --git a/CustomizePlus/Api/CustomizePlusIpc.cs b/CustomizePlus/Api/CustomizePlusIpc.cs
index 8090540..3994ba8 100644
--- a/CustomizePlus/Api/CustomizePlusIpc.cs
+++ b/CustomizePlus/Api/CustomizePlusIpc.cs
@@ -1,5 +1,8 @@
-using CustomizePlus.Core.Services;
+using CustomizePlus.Armatures.Events;
+using CustomizePlus.Core.Services;
+using CustomizePlus.Game.Services;
using CustomizePlus.Profiles;
+using CustomizePlus.Profiles.Events;
using Dalamud.Plugin;
using ECommons.EzIpcManager;
using OtterGui.Log;
@@ -7,12 +10,16 @@ using System;
namespace CustomizePlus.Api;
-public partial class CustomizePlusIpc
+public partial class CustomizePlusIpc : IDisposable
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly Logger _logger;
private readonly HookingService _hookingService;
private readonly ProfileManager _profileManager;
+ private readonly GameObjectService _gameObjectService;
+
+ private readonly ProfileChanged _profileChangedEvent;
+ private readonly ArmatureChanged _armatureChangedEvent;
///
/// Shows if IPC failed to initialize or any other unrecoverable fatal error occured.
@@ -23,13 +30,30 @@ public partial class CustomizePlusIpc
DalamudPluginInterface pluginInterface,
Logger logger,
HookingService hookingService,
- ProfileManager profileManager)
+ ProfileManager profileManager,
+ GameObjectService gameObjectService,
+ ArmatureChanged armatureChangedEvent,
+ ProfileChanged profileChangedEvent)
{
_pluginInterface = pluginInterface;
_logger = logger;
_hookingService = hookingService;
_profileManager = profileManager;
+ _gameObjectService = gameObjectService;
+
+
+ _profileChangedEvent = profileChangedEvent;
+ _armatureChangedEvent = armatureChangedEvent;
EzIPC.Init(this, "CustomizePlus");
+
+ _profileChangedEvent.Subscribe(OnProfileChange, ProfileChanged.Priority.CustomizePlusIpc);
+ _armatureChangedEvent.Subscribe(OnArmatureChanged, ArmatureChanged.Priority.CustomizePlusIpc);
+ }
+
+ public void Dispose()
+ {
+ _profileChangedEvent.Unsubscribe(OnProfileChange);
+ _armatureChangedEvent.Unsubscribe(OnArmatureChanged);
}
}
diff --git a/CustomizePlus/Api/Data/IPCCharacterProfile.cs b/CustomizePlus/Api/Data/IPCCharacterProfile.cs
new file mode 100644
index 0000000..727e73e
--- /dev/null
+++ b/CustomizePlus/Api/Data/IPCCharacterProfile.cs
@@ -0,0 +1,134 @@
+using CustomizePlus.Configuration.Data.Version3;
+using CustomizePlus.Core.Data;
+using CustomizePlus.Profiles.Data;
+using CustomizePlus.Templates.Data;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CustomizePlus.Api.Data;
+
+///
+/// Bare essentials version of character profile
+///
+public class IPCCharacterProfile
+{
+ public string CharacterName { get; set; } = "Invalid";
+ public Dictionary Bones { get; init; } = new();
+
+ public static IPCCharacterProfile FromFullProfile(Profile profile)
+ {
+ var ipcProfile = new IPCCharacterProfile
+ {
+ CharacterName = profile.CharacterName,
+ Bones = new Dictionary()
+ };
+
+ foreach (var template in profile.Templates)
+ {
+ foreach (var kvPair in template.Bones) //not super optimal but whatever
+ {
+ ipcProfile.Bones[kvPair.Key] = new IPCBoneTransform
+ {
+ Translation = kvPair.Value.Translation,
+ Rotation = kvPair.Value.Rotation,
+ Scaling = kvPair.Value.Scaling
+ };
+ }
+ }
+
+ return ipcProfile;
+ }
+
+ public static (Profile, Template) ToFullProfile(IPCCharacterProfile profile, bool isTemporary = true)
+ {
+ var fullProfile = new Profile
+ {
+ Name = $"{profile.CharacterName}'s IPC profile",
+ CharacterName = profile.CharacterName,
+ CreationDate = DateTimeOffset.UtcNow,
+ ModifiedDate = DateTimeOffset.UtcNow,
+ Enabled = true,
+ LimitLookupToOwnedObjects = false,
+ UniqueId = Guid.NewGuid(),
+ Templates = new List(1),
+ ProfileType = isTemporary ? Profiles.Enums.ProfileType.Temporary : Profiles.Enums.ProfileType.Normal
+ };
+
+ var template = new Template
+ {
+ Name = $"{fullProfile.Name}'s template",
+ CreationDate = fullProfile.CreationDate,
+ ModifiedDate = fullProfile.ModifiedDate,
+ UniqueId = Guid.NewGuid(),
+ Bones = new Dictionary(profile.Bones.Count)
+ };
+
+ foreach (var kvPair in profile.Bones)
+ template.Bones.Add(kvPair.Key,
+ new BoneTransform { Translation = kvPair.Value.Translation, Rotation = kvPair.Value.Rotation, Scaling = kvPair.Value.Scaling });
+
+ fullProfile.Templates.Add(template);
+
+ return (fullProfile, template);
+ }
+}
+
+public class IPCBoneTransform
+{
+ private Vector3 _translation;
+ public Vector3 Translation
+ {
+ get => _translation;
+ set => _translation = ClampVector(value);
+ }
+
+ private Vector3 _rotation;
+ public Vector3 Rotation
+ {
+ get => _rotation;
+ set => _rotation = ClampAngles(value);
+ }
+
+ private Vector3 _scaling;
+ public Vector3 Scaling
+ {
+ get => _scaling;
+ set => _scaling = ClampVector(value);
+ }
+
+ ///
+ /// Clamp all vector values to be within allowed limits.
+ ///
+ private Vector3 ClampVector(Vector3 vector)
+ {
+ return new Vector3
+ {
+ X = Math.Clamp(vector.X, -512, 512),
+ Y = Math.Clamp(vector.Y, -512, 512),
+ Z = Math.Clamp(vector.Z, -512, 512)
+ };
+ }
+
+ private static Vector3 ClampAngles(Vector3 rotVec)
+ {
+ static float Clamp(float angle)
+ {
+ if (angle > 180)
+ angle -= 360;
+ else if (angle < -180)
+ angle += 360;
+
+ return angle;
+ }
+
+ rotVec.X = Clamp(rotVec.X);
+ rotVec.Y = Clamp(rotVec.Y);
+ rotVec.Z = Clamp(rotVec.Z);
+
+ return rotVec;
+ }
+}
diff --git a/CustomizePlus/Api/Enums/ErrorCode.cs b/CustomizePlus/Api/Enums/ErrorCode.cs
new file mode 100644
index 0000000..e4d6b6f
--- /dev/null
+++ b/CustomizePlus/Api/Enums/ErrorCode.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CustomizePlus.Api.Enums;
+
+///
+/// Error codes returned by some API methods
+///
+public enum ErrorCode
+{
+ Success = 0,
+ ///
+ /// Returned when invalid character address was provided
+ ///
+ InvalidCharacter = 1,
+ ///
+ /// Returned if IPCCharacterProfile could not be deserialized or deserialized into an empty object
+ ///
+ CorruptedProfile = 2,
+ ///
+ /// Provided character does not have active profiles, provided profile id is invalid or provided profile id is not valid for use in current function
+ ///
+ ProfileNotFound = 3,
+
+ UnknownError = 255
+}
diff --git a/CustomizePlus/Game/Services/GameObjectService.cs b/CustomizePlus/Game/Services/GameObjectService.cs
index efa1f81..a24d121 100644
--- a/CustomizePlus/Game/Services/GameObjectService.cs
+++ b/CustomizePlus/Game/Services/GameObjectService.cs
@@ -6,6 +6,7 @@ using CustomizePlus.GameData.Data;
using CustomizePlus.GameData.Services;
using CustomizePlus.GameData.Extensions;
using Penumbra.GameData.Enums;
+using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.GameObject;
namespace CustomizePlus.Game.Services;
@@ -69,4 +70,14 @@ public class GameObjectService
}
}
}
+
+ public Actor GetLocalPlayerActor()
+ {
+ return _objectManager.Player;
+ }
+
+ public DalamudGameObject? GetDalamudGameObjectFromActor(Actor actor)
+ {
+ return _objectTable.CreateObjectReference(actor);
+ }
}
diff --git a/CustomizePlus/Profiles/Exceptions/ActorNotFoundException.cs b/CustomizePlus/Profiles/Exceptions/ActorNotFoundException.cs
new file mode 100644
index 0000000..72199d4
--- /dev/null
+++ b/CustomizePlus/Profiles/Exceptions/ActorNotFoundException.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace CustomizePlus.Profiles.Exceptions;
+
+internal class ActorNotFoundException : ProfileException
+{
+ public ActorNotFoundException()
+ {
+ }
+
+ public ActorNotFoundException(string? message) : base(message)
+ {
+ }
+
+ public ActorNotFoundException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+
+ protected ActorNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+}
diff --git a/CustomizePlus/Profiles/Exceptions/ProfileException.cs b/CustomizePlus/Profiles/Exceptions/ProfileException.cs
new file mode 100644
index 0000000..d08e7c2
--- /dev/null
+++ b/CustomizePlus/Profiles/Exceptions/ProfileException.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace CustomizePlus.Profiles.Exceptions;
+
+internal class ProfileException : Exception
+{
+ public ProfileException()
+ {
+ }
+
+ public ProfileException(string? message) : base(message)
+ {
+ }
+
+ public ProfileException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+
+ protected ProfileException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+}
diff --git a/CustomizePlus/Profiles/Exceptions/ProfileNotFoundException.cs b/CustomizePlus/Profiles/Exceptions/ProfileNotFoundException.cs
new file mode 100644
index 0000000..72f3835
--- /dev/null
+++ b/CustomizePlus/Profiles/Exceptions/ProfileNotFoundException.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace CustomizePlus.Profiles.Exceptions;
+internal class ProfileNotFoundException : ProfileException
+{
+ public ProfileNotFoundException()
+ {
+ }
+
+ public ProfileNotFoundException(string? message) : base(message)
+ {
+ }
+
+ public ProfileNotFoundException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+
+ protected ProfileNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+}
diff --git a/CustomizePlus/Profiles/ProfileManager.cs b/CustomizePlus/Profiles/ProfileManager.cs
index 40d9146..f412e1b 100644
--- a/CustomizePlus/Profiles/ProfileManager.cs
+++ b/CustomizePlus/Profiles/ProfileManager.cs
@@ -23,6 +23,7 @@ using CustomizePlus.GameData.Services;
using CustomizePlus.GameData.Extensions;
using CustomizePlus.Profiles.Enums;
using Penumbra.GameData.Enums;
+using CustomizePlus.Profiles.Exceptions;
namespace CustomizePlus.Profiles;
@@ -299,6 +300,8 @@ public class ProfileManager : IDisposable
{
SetEnabled(profile, value);
}
+ else
+ throw new ProfileNotFoundException();
}
public void SetLimitLookupToOwned(Profile profile, bool value)
@@ -390,7 +393,7 @@ public class ProfileManager : IDisposable
public void AddTemporaryProfile(Profile profile, Actor actor/*, Template template*/)
{
if (!actor.Identifier(_actorManager, out var identifier))
- return;
+ throw new ActorNotFoundException();
profile.Enabled = true;
profile.ProfileType = ProfileType.Temporary;
@@ -418,21 +421,30 @@ public class ProfileManager : IDisposable
public void RemoveTemporaryProfile(Profile profile)
{
if (!Profiles.Remove(profile))
- return;
+ throw new ProfileNotFoundException();
_logger.Debug($"Removed temporary profile for {profile.CharacterName}");
_event.Invoke(ProfileChanged.Type.TemporaryProfileDeleted, profile, null);
}
+ public void RemoveTemporaryProfile(Guid profileId)
+ {
+ var profile = Profiles.FirstOrDefault(x => x.UniqueId == profileId && x.IsTemporary);
+ if (profile == null)
+ throw new ProfileNotFoundException();
+
+ RemoveTemporaryProfile(profile);
+ }
+
public void RemoveTemporaryProfile(Actor actor)
{
if (!actor.Identifier(_actorManager, out var identifier))
- return;
+ throw new ActorNotFoundException();
var profile = Profiles.FirstOrDefault(x => x.TemporaryActor == identifier && x.IsTemporary);
if (profile == null)
- return;
+ throw new ProfileNotFoundException();
RemoveTemporaryProfile(profile);
}
diff --git a/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/IPCTestTab.cs b/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/IPCTestTab.cs
index 4f01fe3..5f4e3ea 100644
--- a/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/IPCTestTab.cs
+++ b/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/IPCTestTab.cs
@@ -17,6 +17,8 @@ using System.Collections;
using System.Collections.Generic;
using IPCProfileDataTuple = (System.Guid UniqueId, string Name, string CharacterName, bool IsEnabled);
+using OtterGui.Log;
+using CustomizePlus.Core.Extensions;
namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Debug;
@@ -28,6 +30,7 @@ public class IPCTestTab //: IDisposable
private readonly GameObjectService _gameObjectService;
private readonly ObjectManager _objectManager;
private readonly ActorManager _actorManager;
+ private readonly Logger _logger;
[EzIPC("General.GetApiVersion")]
private readonly Func<(int, int)> _getApiVersionIpcFunc;
@@ -44,9 +47,24 @@ public class IPCTestTab //: IDisposable
[EzIPC("Profile.DisableByUniqueId")]
private readonly Action _disableProfileByUniqueIdIpcFunc;
- private readonly ICallGateSubscriber? _setCharacterProfile;
- private readonly ICallGateSubscriber? _getProfileFromCharacter;
- private readonly ICallGateSubscriber? _revertCharacter;
+ [EzIPC("Profile.GetCurrentlyActiveProfileOnCharacter")]
+ private readonly Func _getCurrentlyActiveProfileOnCharacterIpcFunc;
+
+ [EzIPC("Profile.SetTemporaryProfileOnCharacter")]
+ private readonly Func _setTemporaryProfileOnCharacterIpcFunc;
+
+ [EzIPC("Profile.DeleteTemporaryProfileOnCharacter")]
+ private readonly Func _deleteTemporaryProfileOnCharacterIpcFunc;
+
+ [EzIPC("Profile.DeleteTemporaryProfileByUniqueId")]
+ private readonly Func _deleteTemporaryProfileByUniqueIdIpcFunc;
+
+ [EzIPC("Profile.GetProfileById")]
+ private readonly Func _getProfileByIdIpcFunc;
+
+ //private readonly ICallGateSubscriber? _setCharacterProfile;
+ //private readonly ICallGateSubscriber? _getProfileFromCharacter;
+ //private readonly ICallGateSubscriber? _revertCharacter;
//private readonly ICallGateSubscriber? _onProfileUpdate;
private string? _rememberedProfileJson;
@@ -66,7 +84,8 @@ public class IPCTestTab //: IDisposable
PopupSystem popupSystem,
ObjectManager objectManager,
GameObjectService gameObjectService,
- ActorManager actorManager)
+ ActorManager actorManager,
+ Logger logger)
{
_objectTable = objectTable;
_profileManager = profileManager;
@@ -74,15 +93,16 @@ public class IPCTestTab //: IDisposable
_objectManager = objectManager;
_gameObjectService = gameObjectService;
_actorManager = actorManager;
+ _logger = logger;
EzIPC.Init(this, "CustomizePlus");
if (_getApiVersionIpcFunc != null)
_apiVersion = _getApiVersionIpcFunc();
- _setCharacterProfile = pluginInterface.GetIpcSubscriber("CustomizePlus.SetProfileToCharacter");
- _getProfileFromCharacter = pluginInterface.GetIpcSubscriber("CustomizePlus.GetProfileFromCharacter");
- _revertCharacter = pluginInterface.GetIpcSubscriber("CustomizePlus.RevertCharacter");
+ //_setCharacterProfile = pluginInterface.GetIpcSubscriber("CustomizePlus.SetProfileToCharacter");
+ //_getProfileFromCharacter = pluginInterface.GetIpcSubscriber("CustomizePlus.GetProfileFromCharacter");
+ //_revertCharacter = pluginInterface.GetIpcSubscriber("CustomizePlus.RevertCharacter");
/*_onProfileUpdate = pluginInterface.GetIpcSubscriber("CustomizePlus.OnProfileUpdate");
_onProfileUpdate.Subscribe(OnProfileUpdate);*/
}
@@ -141,37 +161,59 @@ public class IPCTestTab //: IDisposable
_popupSystem.ShowPopup(PopupSystem.Messages.IPCV4ProfileRemembered);
}
- if (ImGui.Button("GetProfileFromCharacter into memory"))
+ if (ImGui.Button("GetCurrentlyActiveProfileOnCharacter into memory"))
{
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
if (actors.Count == 0)
return;
- _rememberedProfileJson = _getProfileFromCharacter!.InvokeFunc(FindCharacterByAddress(actors[0].Item2.Address));
- _popupSystem.ShowPopup(PopupSystem.Messages.IPCGetProfileFromChrRemembered);
+ (int result, _rememberedProfileJson) = _getCurrentlyActiveProfileOnCharacterIpcFunc(FindCharacterByAddress(actors[0].Item2.Address));
+
+ if(result == 0)
+ _popupSystem.ShowPopup(PopupSystem.Messages.IPCGetProfileFromChrRemembered);
+ else
+ {
+ _logger.Error($"Error code {result} while calling GetCurrentlyActiveProfileOnCharacter");
+ _popupSystem.ShowPopup(PopupSystem.Messages.ActionError);
+ }
}
using (var disabled = ImRaii.Disabled(_rememberedProfileJson == null))
{
- if (ImGui.Button("SetProfileToCharacter from memory") && _rememberedProfileJson != null)
+ if (ImGui.Button("SetTemporaryProfileOnCharacter from memory") && _rememberedProfileJson != null)
{
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
if (actors.Count == 0)
return;
- _setCharacterProfile!.InvokeAction(_rememberedProfileJson, FindCharacterByAddress(actors[0].Item2.Address));
- _popupSystem.ShowPopup(PopupSystem.Messages.IPCSetProfileToChrDone);
+ (int result, Guid? profileGuid) = _setTemporaryProfileOnCharacterIpcFunc(FindCharacterByAddress(actors[0].Item2.Address),_rememberedProfileJson);
+ if (result == 0)
+ {
+ _popupSystem.ShowPopup(PopupSystem.Messages.IPCSetProfileToChrDone);
+ _logger.Information($"Temporary profile id: {profileGuid}");
+ }
+ else
+ {
+ _logger.Error($"Error code {result} while calling SetTemporaryProfileOnCharacter");
+ _popupSystem.ShowPopup(PopupSystem.Messages.ActionError);
+ }
}
}
- if (ImGui.Button("RevertCharacter") && _rememberedProfileJson != null)
+ if (ImGui.Button("DeleteTemporaryProfileOnCharacter"))
{
var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
if (actors.Count == 0)
return;
- _revertCharacter!.InvokeAction(FindCharacterByAddress(actors[0].Item2.Address));
- _popupSystem.ShowPopup(PopupSystem.Messages.IPCRevertDone);
+ int result = _deleteTemporaryProfileOnCharacterIpcFunc(FindCharacterByAddress(actors[0].Item2.Address));
+ if (result == 0)
+ _popupSystem.ShowPopup(PopupSystem.Messages.IPCRevertDone);
+ else
+ {
+ _logger.Error($"Error code {result} while calling DeleteTemporaryProfileOnCharacter");
+ _popupSystem.ShowPopup(PopupSystem.Messages.ActionError);
+ }
}
ImGui.Separator();
@@ -179,13 +221,28 @@ public class IPCTestTab //: IDisposable
if (ImGui.Button("Copy user profile list to clipboard"))
{
ImGui.SetClipboardText(string.Join("\n", _getProfileListIpcFunc().Select(x => $"{x.UniqueId}, {x.Name}, {x.CharacterName}, {x.IsEnabled}")));
- _popupSystem.ShowPopup(PopupSystem.Messages.IPCProfileListCopied);
+ _popupSystem.ShowPopup(PopupSystem.Messages.IPCCopiedToClipboard);
}
- ImGui.Text("Profile Unique ID to set:");
+ ImGui.Text("Profile Unique ID:");
ImGui.SameLine();
ImGui.InputText("##profileguid", ref _targetProfileId, 128);
+ if (ImGui.Button("Get profile by Unique ID"))
+ {
+ (int result, string? profileJson) = _getProfileByIdIpcFunc(Guid.Parse(_targetProfileId));
+ if (result == 0)
+ {
+ ImGui.SetClipboardText(profileJson);
+ _popupSystem.ShowPopup(PopupSystem.Messages.IPCCopiedToClipboard);
+ }
+ else
+ {
+ _logger.Error($"Error code {result} while calling GetProfileById");
+ _popupSystem.ShowPopup(PopupSystem.Messages.ActionError);
+ }
+ }
+
if (ImGui.Button("Enable profile by Unique ID"))
{
_enableProfileByUniqueIdIpcFunc(Guid.Parse(_targetProfileId));
@@ -197,6 +254,28 @@ public class IPCTestTab //: IDisposable
_disableProfileByUniqueIdIpcFunc(Guid.Parse(_targetProfileId));
_popupSystem.ShowPopup(PopupSystem.Messages.IPCDisableProfileByIdDone);
}
+
+ if (ImGui.Button("DeleteTemporaryProfileByUniqueId"))
+ {
+ var actors = _gameObjectService.FindActorsByName(_targetCharacterName).ToList();
+ if (actors.Count == 0)
+ return;
+
+ int result = _deleteTemporaryProfileByUniqueIdIpcFunc(Guid.Parse(_targetProfileId));
+ if (result == 0)
+ _popupSystem.ShowPopup(PopupSystem.Messages.IPCRevertDone);
+ else
+ {
+ _logger.Error($"Error code {result} while calling DeleteTemporaryProfileByUniqueId");
+ _popupSystem.ShowPopup(PopupSystem.Messages.ActionError);
+ }
+ }
+ }
+
+ [EzIPCEvent("Profile.OnUpdate")]
+ private void OnProfileUpdate(Character Character, Guid? ProfileUniqueId)
+ {
+ _logger.Debug($"IPC Test Tab - OnProfileUpdate: Character: {Character.Name.ToString().Incognify()}, Profile ID: {(ProfileUniqueId != null ? ProfileUniqueId.ToString() : "no id")}");
}
private Character? FindCharacterByAddress(nint address)
diff --git a/CustomizePlus/UI/Windows/PopupSystem.Messages.cs b/CustomizePlus/UI/Windows/PopupSystem.Messages.cs
index 08caa93..9fec7f3 100644
--- a/CustomizePlus/UI/Windows/PopupSystem.Messages.cs
+++ b/CustomizePlus/UI/Windows/PopupSystem.Messages.cs
@@ -14,7 +14,7 @@ public partial class PopupSystem
public const string IPCGetProfileFromChrRemembered = "ipc_get_profile_from_character_remembered";
public const string IPCSetProfileToChrDone = "ipc_set_profile_to_character_done";
public const string IPCRevertDone = "ipc_revert_done";
- public const string IPCProfileListCopied = "ipc_profile_list_copied";
+ public const string IPCCopiedToClipboard = "ipc_copied_to clipboard";
public const string IPCEnableProfileByIdDone = "ipc_enable_profile_by_id_done";
public const string IPCDisableProfileByIdDone = "ipc_disable_profile_by_id_done";
@@ -32,9 +32,9 @@ public partial class PopupSystem
RegisterPopup(Messages.IPCV4ProfileRemembered, "Current profile has been copied into memory");
RegisterPopup(Messages.IPCGetProfileFromChrRemembered, "GetProfileFromCharacter result has been copied into memory");
- RegisterPopup(Messages.IPCSetProfileToChrDone, "SetProfileToCharacter has been called with data from memory");
+ RegisterPopup(Messages.IPCSetProfileToChrDone, "SetProfileToCharacter has been called with data from memory, profile id printed to log");
RegisterPopup(Messages.IPCRevertDone, "Revert has been called");
- RegisterPopup(Messages.IPCProfileListCopied, "Profile list copied into clipboard");
+ RegisterPopup(Messages.IPCCopiedToClipboard, "Copied into clipboard");
RegisterPopup(Messages.IPCEnableProfileByIdDone, "Enable profile by id has been called");
RegisterPopup(Messages.IPCDisableProfileByIdDone, "Disable profile by id has been called");