From 5b71cd479e49c1d82a7110d026e8573333634b0d Mon Sep 17 00:00:00 2001 From: RisaDev <151885272+RisaDev@users.noreply.github.com> Date: Mon, 18 Mar 2024 00:15:03 +0300 Subject: [PATCH] Implemented rest of profile methods for IPC --- .../Compatibility/CustomizePlusLegacyIpc.cs | 1 + CustomizePlus/Api/CustomizePlusIpc.Profile.cs | 320 +++++++++++++++++- CustomizePlus/Api/CustomizePlusIpc.cs | 30 +- CustomizePlus/Api/Data/IPCCharacterProfile.cs | 134 ++++++++ CustomizePlus/Api/Enums/ErrorCode.cs | 29 ++ .../Game/Services/GameObjectService.cs | 11 + .../Exceptions/ActorNotFoundException.cs | 23 ++ .../Profiles/Exceptions/ProfileException.cs | 23 ++ .../Exceptions/ProfileNotFoundException.cs | 26 ++ CustomizePlus/Profiles/ProfileManager.cs | 20 +- .../MainWindow/Tabs/Debug/IPCTestTab.cs | 115 ++++++- .../UI/Windows/PopupSystem.Messages.cs | 6 +- 12 files changed, 702 insertions(+), 36 deletions(-) create mode 100644 CustomizePlus/Api/Data/IPCCharacterProfile.cs create mode 100644 CustomizePlus/Api/Enums/ErrorCode.cs create mode 100644 CustomizePlus/Profiles/Exceptions/ActorNotFoundException.cs create mode 100644 CustomizePlus/Profiles/Exceptions/ProfileException.cs create mode 100644 CustomizePlus/Profiles/Exceptions/ProfileNotFoundException.cs 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