Fixes, IPC

* Armature manager now assigns permanent ActorIdentifier to armatures
* Slight changes to make sure penumbra redraw doesn't break temporary profiles
* Profile selection UI now gets updated on login/logout in order to display correct colors
* Changed logic of setting Armature.IsVisible
* Implemented OnProfileUpdate IPC
This commit is contained in:
RisaDev
2024-01-23 01:40:01 +03:00
parent 6dbd6a62ff
commit 7011914a4e
8 changed files with 124 additions and 44 deletions

View File

@@ -17,6 +17,10 @@ using CustomizePlus.Templates.Events;
using CustomizePlus.Profiles.Events; using CustomizePlus.Profiles.Events;
using CustomizePlus.Templates.Data; using CustomizePlus.Templates.Data;
using CustomizePlus.GameData.Data; using CustomizePlus.GameData.Data;
using CustomizePlus.Core.Extensions;
using CustomizePlus.Armatures.Events;
using CustomizePlus.Armatures.Data;
using CustomizePlus.GameData.Extensions;
namespace CustomizePlus.Api.Compatibility; namespace CustomizePlus.Api.Compatibility;
@@ -28,8 +32,8 @@ public class CustomizePlusIpc : IDisposable
private readonly ProfileManager _profileManager; private readonly ProfileManager _profileManager;
private readonly GameObjectService _gameObjectService; private readonly GameObjectService _gameObjectService;
private readonly TemplateChanged _templateChangedEvent;
private readonly ProfileChanged _profileChangedEvent; private readonly ProfileChanged _profileChangedEvent;
private readonly ArmatureChanged _armatureChangedEvent;
private const int _configurationVersion = 3; private const int _configurationVersion = 3;
@@ -37,10 +41,10 @@ public class CustomizePlusIpc : IDisposable
public const string GetProfileFromCharacterLabel = $"CustomizePlus.{nameof(GetProfileFromCharacter)}"; public const string GetProfileFromCharacterLabel = $"CustomizePlus.{nameof(GetProfileFromCharacter)}";
public const string SetProfileToCharacterLabel = $"CustomizePlus.{nameof(SetProfileToCharacter)}"; public const string SetProfileToCharacterLabel = $"CustomizePlus.{nameof(SetProfileToCharacter)}";
public const string RevertCharacterLabel = $"CustomizePlus.{nameof(RevertCharacter)}"; public const string RevertCharacterLabel = $"CustomizePlus.{nameof(RevertCharacter)}";
//public const string OnProfileUpdateLabel = $"CustomizePlus.{nameof(OnProfileUpdate)}"; //I'm honestly not sure this is even used by mare public const string OnProfileUpdateLabel = $"CustomizePlus.{nameof(OnProfileUpdate)}";
public static readonly (int, int) ApiVersion = (3, 0); public static readonly (int, int) ApiVersion = (3, 0);
//Sends local player's profile on hooks reload (plugin startup) as well as any updates to their profile. //Sends local player's profile every time their active profile is changed
//If no profile is applied sends null //If no profile is applied sends null
internal ICallGateProvider<string?, string?, object?>? ProviderOnProfileUpdate; internal ICallGateProvider<string?, string?, object?>? ProviderOnProfileUpdate;
internal ICallGateProvider<Character?, object>? ProviderRevertCharacter; internal ICallGateProvider<Character?, object>? ProviderRevertCharacter;
@@ -53,29 +57,72 @@ public class CustomizePlusIpc : IDisposable
DalamudPluginInterface pluginInterface, DalamudPluginInterface pluginInterface,
Logger logger, Logger logger,
ProfileManager profileManager, ProfileManager profileManager,
GameObjectService gameObjectService//, GameObjectService gameObjectService,
/*TemplateChanged templateChangedEvent, ArmatureChanged armatureChangedEvent,
ProfileChanged profileChangedEvent*/) ProfileChanged profileChangedEvent)
{ {
_objectTable = objectTable; _objectTable = objectTable;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_logger = logger; _logger = logger;
_profileManager = profileManager; _profileManager = profileManager;
_gameObjectService = gameObjectService; _gameObjectService = gameObjectService;
/* _templateChangedEvent = templateChangedEvent; _profileChangedEvent = profileChangedEvent;
_profileChangedEvent = profileChangedEvent;*/ _armatureChangedEvent = armatureChangedEvent;
InitializeProviders(); InitializeProviders();
/*_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.CustomizePlusIpc); _profileChangedEvent.Subscribe(OnProfileChange, ProfileChanged.Priority.CustomizePlusIpc);
_profileChangedEvent.Subscribe(OnProfileChange, ProfileChanged.Priority.CustomizePlusIpc);*/ _armatureChangedEvent.Subscribe(OnArmatureChanged, ArmatureChanged.Priority.CustomizePlusIpc);
} }
public void Dispose() public void Dispose()
{ {
_profileChangedEvent.Unsubscribe(OnProfileChange);
_armatureChangedEvent.Unsubscribe(OnArmatureChanged);
DisposeProviders(); DisposeProviders();
} }
//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)
return;
if (profile == null ||
!profile.Enabled ||
profile.CharacterName.Text != _gameObjectService.GetCurrentPlayerName())
return;
OnProfileUpdate(profile);
}
private void OnArmatureChanged(ArmatureChanged.Type type, Armature armature, object? arg3)
{
string currentPlayerName = _gameObjectService.GetCurrentPlayerName();
if (armature.ActorIdentifier.ToNameWithoutOwnerName() != currentPlayerName)
return;
if (type == ArmatureChanged.Type.Created ||
type == ArmatureChanged.Type.Rebound)
{
if(armature.Profile == null)
_logger.Warning("Armature created/rebound and profile is null");
OnProfileUpdate(armature.Profile);
return;
}
if(type == ArmatureChanged.Type.Deleted)
{
OnProfileUpdate(null);
return;
}
}
private void InitializeProviders() private void InitializeProviders()
{ {
_logger.Debug("Initializing legacy Customize+ IPC providers."); _logger.Debug("Initializing legacy Customize+ IPC providers.");
@@ -121,7 +168,7 @@ public class CustomizePlusIpc : IDisposable
{ {
_logger.Error($"Error registering legacy Customize+ IPC provider for {RevertCharacterLabel}: {ex}"); _logger.Error($"Error registering legacy Customize+ IPC provider for {RevertCharacterLabel}: {ex}");
} }
/*
try try
{ {
ProviderOnProfileUpdate = _pluginInterface.GetIpcProvider<string?, string?, object?>(OnProfileUpdateLabel); ProviderOnProfileUpdate = _pluginInterface.GetIpcProvider<string?, string?, object?>(OnProfileUpdateLabel);
@@ -129,7 +176,7 @@ public class CustomizePlusIpc : IDisposable
catch (Exception ex) catch (Exception ex)
{ {
_logger.Error($"Error registering legacy Customize+ IPC provider for {OnProfileUpdateLabel}: {ex}"); _logger.Error($"Error registering legacy Customize+ IPC provider for {OnProfileUpdateLabel}: {ex}");
}*/ }
} }
private void DisposeProviders() private void DisposeProviders()
@@ -143,8 +190,7 @@ public class CustomizePlusIpc : IDisposable
private void OnProfileUpdate(Profile? profile) private void OnProfileUpdate(Profile? profile)
{ {
//Get player's body profile string and send IPC message _logger.Debug($"Sending local player update message: {(profile != null ? profile.ToString() : "no profile")}");
_logger.Debug($"Sending local player update message: {profile?.Name ?? "no profile"} - {profile?.CharacterName ?? "no profile"}");
var convertedProfile = profile != null ? GetVersion3Profile(profile) : null; var convertedProfile = profile != null ? GetVersion3Profile(profile) : null;

View File

@@ -31,6 +31,12 @@ public unsafe class Armature
/// </summary> /// </summary>
public bool IsVisible { get; set; } public bool IsVisible { get; set; }
/// <summary>
/// Represents date and time when actor associated with this armature was last seen.
/// Implemented mostly as a armature cleanup protection hack for mare and penumbra.
/// </summary>
public DateTime LastSeen { get; private set; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not this armature has successfully built itself with bone information. /// Gets a value indicating whether or not this armature has successfully built itself with bone information.
/// </summary> /// </summary>
@@ -41,12 +47,6 @@ public unsafe class Armature
/// </summary> /// </summary>
public bool IsPendingProfileRebind { get; set; } public bool IsPendingProfileRebind { get; set; }
/// <summary>
/// Represents date and time until which any kind of removal protections will not be applying to this armature.
/// Implemented mostly as a armature cleanup protection hack due to how mare works when downloading files for the first time
/// </summary>
public DateTime ProtectedUntil { get; private set; }
/// <summary> /// <summary>
/// For debugging purposes, each armature is assigned a globally-unique ID number upon creation. /// For debugging purposes, each armature is assigned a globally-unique ID number upon creation.
/// </summary> /// </summary>
@@ -147,7 +147,7 @@ public unsafe class Armature
Profile = profile; Profile = profile;
IsVisible = false; IsVisible = false;
ProtectFromRemoval(); UpdateLastSeen();
Profile.Armatures.Add(this); Profile.Armatures.Add(this);
@@ -257,11 +257,14 @@ public unsafe class Armature
} }
/// <summary> /// <summary>
/// Apply removal protection for 30 seconds starting from current time. For the most part this is a hack for mare. /// Update last time actor for this armature was last seen in the game
/// </summary> /// </summary>
public void ProtectFromRemoval() public void UpdateLastSeen(DateTime? dateTime = null)
{ {
ProtectedUntil = DateTime.UtcNow.AddSeconds(30); if(dateTime == null)
dateTime = DateTime.UtcNow;
LastSeen = (DateTime)dateTime;
} }
private static unsafe List<List<ModelBone>> ParseBonesFromObject(Armature arm, CharacterBase* cBase) private static unsafe List<List<ModelBone>> ParseBonesFromObject(Armature arm, CharacterBase* cBase)

View File

@@ -7,17 +7,19 @@ namespace CustomizePlus.Armatures.Events;
/// <summary> /// <summary>
/// Triggered when armature is changed /// Triggered when armature is changed
/// </summary> /// </summary>
public sealed class ArmatureChanged() : EventWrapper<ArmatureChanged.Type, Armature?, object?, ArmatureChanged.Priority>(nameof(ArmatureChanged)) public sealed class ArmatureChanged() : EventWrapper<ArmatureChanged.Type, Armature, object?, ArmatureChanged.Priority>(nameof(ArmatureChanged))
{ {
public enum Type public enum Type
{ {
//Created, Created,
Deleted Deleted,
Rebound
} }
public enum Priority public enum Priority
{ {
ProfileManager ProfileManager,
CustomizePlusIpc
} }
public enum DeletionReason public enum DeletionReason

View File

@@ -19,6 +19,7 @@ using CustomizePlus.GameData.Data;
using CustomizePlus.GameData.Services; using CustomizePlus.GameData.Services;
using CustomizePlus.GameData.Extensions; using CustomizePlus.GameData.Extensions;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using System.Drawing;
namespace CustomizePlus.Armatures.Services; namespace CustomizePlus.Armatures.Services;
@@ -106,15 +107,21 @@ public unsafe sealed class ArmatureManager : IDisposable
_objectManager.Update(); _objectManager.Update();
var currentTime = DateTime.UtcNow; var currentTime = DateTime.UtcNow;
var armatureExpirationDateTime = currentTime.AddSeconds(-30);
foreach (var kvPair in Armatures.ToList()) foreach (var kvPair in Armatures.ToList())
{ {
var armature = kvPair.Value; var armature = kvPair.Value;
if (!_objectManager.ContainsKey(kvPair.Value.ActorIdentifier) && if (!_objectManager.ContainsKey(kvPair.Value.ActorIdentifier) &&
currentTime > armature.ProtectedUntil) //Only remove armatures which are no longer protected armature.LastSeen <= armatureExpirationDateTime) //Only remove armatures which haven't been seen for a while
{ {
_logger.Debug($"Removing armature {armature} because {kvPair.Key.IncognitoDebug()} is gone"); _logger.Debug($"Removing armature {armature} because {kvPair.Key.IncognitoDebug()} is gone");
RemoveArmature(armature, ArmatureChanged.DeletionReason.Gone); RemoveArmature(armature, ArmatureChanged.DeletionReason.Gone);
continue;
} }
//armature is considered visible if 1 or less seconds passed since last time we've seen the actor
armature.IsVisible = armature.LastSeen.AddSeconds(1) >= currentTime;
} }
Profile? GetProfileForActor(ActorIdentifier identifier) Profile? GetProfileForActor(ActorIdentifier identifier)
@@ -134,28 +141,32 @@ public unsafe sealed class ArmatureManager : IDisposable
foreach (var obj in _objectManager) foreach (var obj in _objectManager)
{ {
if (!Armatures.ContainsKey(obj.Key)) var actorIdentifier = obj.Key.CreatePermanent();
if (!Armatures.ContainsKey(actorIdentifier))
{ {
var activeProfile = GetProfileForActor(obj.Key); var activeProfile = GetProfileForActor(actorIdentifier);
if (activeProfile == null) if (activeProfile == null)
continue; continue;
var newArm = new Armature(obj.Key, activeProfile); var newArm = new Armature(actorIdentifier, activeProfile);
TryLinkSkeleton(newArm); TryLinkSkeleton(newArm);
Armatures.Add(obj.Key, newArm); Armatures.Add(actorIdentifier, newArm);
_logger.Debug($"Added '{newArm}' for {obj.Key.IncognitoDebug()} to cache"); _logger.Debug($"Added '{newArm}' for {actorIdentifier.IncognitoDebug()} to cache");
_event.Invoke(ArmatureChanged.Type.Created, newArm, activeProfile);
continue; continue;
} }
var armature = Armatures[obj.Key]; var armature = Armatures[actorIdentifier];
armature.UpdateLastSeen(currentTime);
if (armature.IsPendingProfileRebind) if (armature.IsPendingProfileRebind)
{ {
_logger.Debug($"Armature {armature} is pending profile rebind, rebinding..."); _logger.Debug($"Armature {armature} is pending profile rebind, rebinding...");
armature.IsPendingProfileRebind = false; armature.IsPendingProfileRebind = false;
var activeProfile = GetProfileForActor(obj.Key); var activeProfile = GetProfileForActor(actorIdentifier);
if (activeProfile == armature.Profile) if (activeProfile == armature.Profile)
continue; continue;
@@ -166,14 +177,19 @@ public unsafe sealed class ArmatureManager : IDisposable
continue; continue;
} }
Profile oldProfile = armature.Profile;
armature.Profile.Armatures.Remove(armature); armature.Profile.Armatures.Remove(armature);
armature.Profile = activeProfile; armature.Profile = activeProfile;
activeProfile.Armatures.Add(armature); activeProfile.Armatures.Add(armature);
armature.RebuildBoneTemplateBinding(); armature.RebuildBoneTemplateBinding();
_event.Invoke(ArmatureChanged.Type.Rebound, armature, activeProfile);
} }
armature.IsVisible = armature.Profile.Enabled && TryLinkSkeleton(armature); //todo: remove armatures which are not visible? //Needed because skeleton sometimes appears to be not ready when armature is created
//and also because we want to augment armature with new bones if they are available
TryLinkSkeleton(armature);
} }
} }
@@ -496,7 +512,7 @@ public unsafe sealed class ArmatureManager : IDisposable
if (armature.Profile == profile) if (armature.Profile == profile)
return; return;
armature.ProtectFromRemoval(); armature.UpdateLastSeen();
armature.IsPendingProfileRebind = true; armature.IsPendingProfileRebind = true;
@@ -516,7 +532,7 @@ public unsafe sealed class ArmatureManager : IDisposable
foreach (var armature in profile.Armatures) foreach (var armature in profile.Armatures)
{ {
if (type == ProfileChanged.Type.TemporaryProfileDeleted) if (type == ProfileChanged.Type.TemporaryProfileDeleted)
armature.ProtectFromRemoval(); //just to be safe armature.UpdateLastSeen(); //just to be safe
armature.IsPendingProfileRebind = true; armature.IsPendingProfileRebind = true;
} }

View File

@@ -62,9 +62,9 @@ public class GameObjectService
{ {
if (kvPair.Value.Objects.Count > 1) //in gpose we can have more than a single object for one actor 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) foreach (var obj in kvPair.Value.Objects)
yield return (kvPair.Key, obj); yield return (kvPair.Key.CreatePermanent(), obj);
else else
yield return (kvPair.Key, kvPair.Value.Objects[0]); yield return (kvPair.Key.CreatePermanent(), kvPair.Value.Objects[0]);
} }
} }
} }

View File

@@ -545,7 +545,7 @@ public class ProfileManager : IDisposable
} }
private void OnArmatureChange(ArmatureChanged.Type type, Armature? armature, object? arg3) private void OnArmatureChange(ArmatureChanged.Type type, Armature armature, object? arg3)
{ {
if (type == ArmatureChanged.Type.Deleted) if (type == ArmatureChanged.Type.Deleted)
{ {

View File

@@ -202,7 +202,7 @@ public class StateMonitoringTab
ImGui.Text($"Profile: {armature.Profile.Name.Text.Incognify()} ({armature.Profile.UniqueId})"); ImGui.Text($"Profile: {armature.Profile.Name.Text.Incognify()} ({armature.Profile.UniqueId})");
ImGui.Text($"Actor: {armature.ActorIdentifier.IncognitoDebug()}"); ImGui.Text($"Actor: {armature.ActorIdentifier.IncognitoDebug()}");
ImGui.Text($"Protection: {(armature.ProtectedUntil >= DateTime.UtcNow ? "Active" : "NOT active")} [{armature.ProtectedUntil} (UTC)]"); ImGui.Text($"Last seen: {armature.LastSeen} (UTC)");
//ImGui.Text("Profile:"); //ImGui.Text("Profile:");
//DrawSingleProfile($"armature-{armature.GetHashCode()}", armature.Profile); //DrawSingleProfile($"armature-{armature.GetHashCode()}", armature.Profile);

View File

@@ -25,6 +25,7 @@ public class ProfileFileSystemSelector : FileSystemSelector<Profile, ProfileStat
private readonly ProfileManager _profileManager; private readonly ProfileManager _profileManager;
private readonly ProfileChanged _event; private readonly ProfileChanged _event;
private readonly GameObjectService _gameObjectService; private readonly GameObjectService _gameObjectService;
private readonly IClientState _clientState;
private Profile? _cloneProfile; private Profile? _cloneProfile;
private string _newName = string.Empty; private string _newName = string.Empty;
@@ -51,16 +52,21 @@ public class ProfileFileSystemSelector : FileSystemSelector<Profile, ProfileStat
PluginConfiguration configuration, PluginConfiguration configuration,
ProfileManager profileManager, ProfileManager profileManager,
ProfileChanged @event, ProfileChanged @event,
GameObjectService gameObjectService) GameObjectService gameObjectService,
IClientState clientState)
: base(fileSystem, keyState, logger, allowMultipleSelection: true) : base(fileSystem, keyState, logger, allowMultipleSelection: true)
{ {
_configuration = configuration; _configuration = configuration;
_profileManager = profileManager; _profileManager = profileManager;
_event = @event; _event = @event;
_gameObjectService = gameObjectService; _gameObjectService = gameObjectService;
_clientState = clientState;
_event.Subscribe(OnProfileChange, ProfileChanged.Priority.ProfileFileSystemSelector); _event.Subscribe(OnProfileChange, ProfileChanged.Priority.ProfileFileSystemSelector);
_clientState.Login += OnLoginLogout;
_clientState.Logout += OnLoginLogout;
AddButton(NewButton, 0); AddButton(NewButton, 0);
AddButton(CloneButton, 20); AddButton(CloneButton, 20);
AddButton(DeleteButton, 1000); AddButton(DeleteButton, 1000);
@@ -71,6 +77,8 @@ public class ProfileFileSystemSelector : FileSystemSelector<Profile, ProfileStat
{ {
base.Dispose(); base.Dispose();
_event.Unsubscribe(OnProfileChange); _event.Unsubscribe(OnProfileChange);
_clientState.Login -= OnLoginLogout;
_clientState.Logout -= OnLoginLogout;
} }
protected override uint ExpandedFolderColor protected override uint ExpandedFolderColor
@@ -131,6 +139,11 @@ public class ProfileFileSystemSelector : FileSystemSelector<Profile, ProfileStat
} }
} }
private void OnLoginLogout()
{
SetFilterDirty();
}
private void NewButton(Vector2 size) private void NewButton(Vector2 size)
{ {
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new profile with default configuration.", false, if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new profile with default configuration.", false,