Implemented rest of profile methods for IPC

This commit is contained in:
RisaDev
2024-03-18 00:15:03 +03:00
parent 37c2882a98
commit 5b71cd479e
12 changed files with 702 additions and 36 deletions

View File

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

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
[EzIPCEvent("Profile.OnUpdate")]
private Action<Character, Guid?> OnProfileUpdate;
/// <summary>
/// Retrieve list of all user profiles
/// </summary>
@@ -24,21 +47,302 @@ public partial class CustomizePlusIpc
}
/// <summary>
/// Enable profile using its Unique ID
/// Get JSON copy of profile with specified unique id
/// </summary>
/// <param name="uniqueId"></param>
[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);
}
}
/// <summary>
/// Disable profile using its Unique ID
/// Enable profile using its Unique ID. Does not work on temporary profiles.
/// </summary>
/// <param name="uniqueId"></param>
[EzIPC("Profile.EnableByUniqueId")]
private ErrorCode EnableProfileByUniqueId(Guid uniqueId)
{
return SetProfileStateInternal(uniqueId, true);
}
/// <summary>
/// Disable profile using its Unique ID. Does not work on temporary profiles.
/// </summary>
[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;
}
}
/// <summary>
/// Get JSON copy of active profile for character.
/// </summary>
[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);
}
}
/// <summary>
/// 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.
/// </summary>
[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<IPCCharacterProfile>(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);
}
}
/// <summary>
/// Delete temporary profile currently active on character
/// </summary>
[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;
}
}
/// <summary>
/// Delete temporary profile using its unique id
/// </summary>
[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);
}
}

View File

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

View File

@@ -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;
/// <summary>
/// Bare essentials version of character profile
/// </summary>
public class IPCCharacterProfile
{
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,
Bones = new Dictionary<string, IPCBoneTransform>()
};
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<Template>(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<string, BoneTransform>(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);
}
/// <summary>
/// Clamp all vector values to be within allowed limits.
/// </summary>
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;
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CustomizePlus.Api.Enums;
/// <summary>
/// Error codes returned by some API methods
/// </summary>
public enum ErrorCode
{
Success = 0,
/// <summary>
/// Returned when invalid character address was provided
/// </summary>
InvalidCharacter = 1,
/// <summary>
/// Returned if IPCCharacterProfile could not be deserialized or deserialized into an empty object
/// </summary>
CorruptedProfile = 2,
/// <summary>
/// Provided character does not have active profiles, provided profile id is invalid or provided profile id is not valid for use in current function
/// </summary>
ProfileNotFound = 3,
UnknownError = 255
}