ArmatureChanged.Type.Rebound -> Updated IsPendingProfileRebind is now being set instead of calling RebuildBoneTemplateBinding and that function is called as a part of rebind process (not always though, pending rewrite) Profiles can no longer be toggled in the UI while editor is active ProfileChanged is no longer used in IPC, instead we are fully relying on armature events Added warnings to legacy IPC messages to not use it
335 lines
12 KiB
C#
335 lines
12 KiB
C#
using CustomizePlus.Profiles.Enums;
|
|
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 Penumbra.GameData.Interop;
|
|
//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. It is not recommended to assume that this will always be the case and not perform any checks on your side.
|
|
/// Ignores temporary profiles.
|
|
/// /!\ If no profile is set on specified character profile id will be equal to Guid.Empty
|
|
/// </summary>
|
|
[EzIPCEvent("Profile.OnUpdate")]
|
|
private Action<Character, Guid> OnProfileUpdate;
|
|
|
|
/// <summary>
|
|
/// Retrieve list of all user profiles
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
[EzIPC("Profile.GetList")]
|
|
private IList<IPCProfileDataTuple> GetProfileList()
|
|
{
|
|
return _profileManager.Profiles
|
|
.Where(x => x.ProfileType == ProfileType.Normal)
|
|
.Select(x => (x.UniqueId, x.Name.Text, x.CharacterName.Text, x.Enabled))
|
|
.ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get JSON copy of profile with specified unique id
|
|
/// </summary>
|
|
[EzIPC("Profile.GetByUniqueId")]
|
|
private (int, string?) GetProfileByUniqueId(Guid uniqueId)
|
|
{
|
|
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>
|
|
/// Enable profile using its Unique ID. Does not work on temporary profiles.
|
|
/// </summary>
|
|
/// <param name="uniqueId"></param>
|
|
[EzIPC("Profile.EnableByUniqueId")]
|
|
private int EnableProfileByUniqueId(Guid uniqueId)
|
|
{
|
|
return (int)SetProfileStateInternal(uniqueId, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disable profile using its Unique ID. Does not work on temporary profiles.
|
|
/// </summary>
|
|
[EzIPC("Profile.DisableByUniqueId")]
|
|
private int DisableProfileByUniqueId(Guid uniqueId)
|
|
{
|
|
return (int)SetProfileStateInternal(uniqueId, false);
|
|
}
|
|
|
|
private ErrorCode SetProfileStateInternal(Guid uniqueId, bool state)
|
|
{
|
|
if (uniqueId == Guid.Empty)
|
|
return ErrorCode.ProfileNotFound;
|
|
|
|
try
|
|
{
|
|
_profileManager.SetEnabled(uniqueId, state);
|
|
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 unique id of currently active profile for character.
|
|
/// </summary>
|
|
[EzIPC("Profile.GetActiveProfileIdOnCharacter")]
|
|
private (int, Guid?) GetActiveProfileIdOnCharacter(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);
|
|
|
|
return ((int)ErrorCode.Success, profile.UniqueId);
|
|
}
|
|
|
|
/// <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 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 ||
|
|
type == ArmatureChanged.Type.Updated)
|
|
{
|
|
if (armature.Profile == null)
|
|
_logger.Fatal("INTEGRITY ERROR: Armature created/rebound and profile is null");
|
|
|
|
(Profile? activeProfile, Profile? oldProfile) = (null, null);
|
|
if (type == ArmatureChanged.Type.Created)
|
|
(activeProfile, oldProfile) = ((Profile?)arg3, null);
|
|
else
|
|
(activeProfile, oldProfile) = ((Profile?, Profile?))arg3;
|
|
|
|
if (activeProfile != null)
|
|
{
|
|
if (activeProfile == _profileManager.DefaultProfile)
|
|
return; //default profiles are not allowed to be sent
|
|
|
|
if (activeProfile.ProfileType == ProfileType.Editor)
|
|
{
|
|
if (activeProfile == oldProfile) //ignore any changes while player is in editor
|
|
return;
|
|
|
|
OnProfileUpdateInternal(localPlayerCharacter, null); //send empty profile when player enters editor
|
|
return;
|
|
}
|
|
}
|
|
|
|
OnProfileUpdateInternal(localPlayerCharacter, activeProfile);
|
|
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 : Guid.Empty);
|
|
}
|
|
}
|