diff --git a/CustomizePlus/Api/Compatibility/CustomizePlusIpc.cs b/CustomizePlus/Api/Compatibility/CustomizePlusIpc.cs index c61ddc4..eef02f9 100644 --- a/CustomizePlus/Api/Compatibility/CustomizePlusIpc.cs +++ b/CustomizePlus/Api/Compatibility/CustomizePlusIpc.cs @@ -17,6 +17,10 @@ using CustomizePlus.Templates.Events; using CustomizePlus.Profiles.Events; using CustomizePlus.Templates.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; @@ -28,8 +32,8 @@ public class CustomizePlusIpc : IDisposable private readonly ProfileManager _profileManager; private readonly GameObjectService _gameObjectService; - private readonly TemplateChanged _templateChangedEvent; private readonly ProfileChanged _profileChangedEvent; + private readonly ArmatureChanged _armatureChangedEvent; private const int _configurationVersion = 3; @@ -37,10 +41,10 @@ public class CustomizePlusIpc : IDisposable public const string GetProfileFromCharacterLabel = $"CustomizePlus.{nameof(GetProfileFromCharacter)}"; public const string SetProfileToCharacterLabel = $"CustomizePlus.{nameof(SetProfileToCharacter)}"; 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); - //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 internal ICallGateProvider? ProviderOnProfileUpdate; internal ICallGateProvider? ProviderRevertCharacter; @@ -53,29 +57,72 @@ public class CustomizePlusIpc : IDisposable DalamudPluginInterface pluginInterface, Logger logger, ProfileManager profileManager, - GameObjectService gameObjectService//, - /*TemplateChanged templateChangedEvent, - ProfileChanged profileChangedEvent*/) + GameObjectService gameObjectService, + ArmatureChanged armatureChangedEvent, + ProfileChanged profileChangedEvent) { _objectTable = objectTable; _pluginInterface = pluginInterface; _logger = logger; _profileManager = profileManager; _gameObjectService = gameObjectService; - /* _templateChangedEvent = templateChangedEvent; - _profileChangedEvent = profileChangedEvent;*/ + _profileChangedEvent = profileChangedEvent; + _armatureChangedEvent = armatureChangedEvent; 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() { + _profileChangedEvent.Unsubscribe(OnProfileChange); + _armatureChangedEvent.Unsubscribe(OnArmatureChanged); 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() { _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}"); } - /* + try { ProviderOnProfileUpdate = _pluginInterface.GetIpcProvider(OnProfileUpdateLabel); @@ -129,7 +176,7 @@ public class CustomizePlusIpc : IDisposable catch (Exception ex) { _logger.Error($"Error registering legacy Customize+ IPC provider for {OnProfileUpdateLabel}: {ex}"); - }*/ + } } private void DisposeProviders() @@ -143,8 +190,7 @@ public class CustomizePlusIpc : IDisposable private void OnProfileUpdate(Profile? profile) { - //Get player's body profile string and send IPC message - _logger.Debug($"Sending local player update message: {profile?.Name ?? "no profile"} - {profile?.CharacterName ?? "no profile"}"); + _logger.Debug($"Sending local player update message: {(profile != null ? profile.ToString() : "no profile")}"); var convertedProfile = profile != null ? GetVersion3Profile(profile) : null; diff --git a/CustomizePlus/Armatures/Data/Armature.cs b/CustomizePlus/Armatures/Data/Armature.cs index 02b3ffb..479defe 100644 --- a/CustomizePlus/Armatures/Data/Armature.cs +++ b/CustomizePlus/Armatures/Data/Armature.cs @@ -31,6 +31,12 @@ public unsafe class Armature /// public bool IsVisible { get; set; } + /// + /// 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. + /// + public DateTime LastSeen { get; private set; } + /// /// Gets a value indicating whether or not this armature has successfully built itself with bone information. /// @@ -41,12 +47,6 @@ public unsafe class Armature /// public bool IsPendingProfileRebind { get; set; } - /// - /// 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 - /// - public DateTime ProtectedUntil { get; private set; } - /// /// For debugging purposes, each armature is assigned a globally-unique ID number upon creation. /// @@ -147,7 +147,7 @@ public unsafe class Armature Profile = profile; IsVisible = false; - ProtectFromRemoval(); + UpdateLastSeen(); Profile.Armatures.Add(this); @@ -257,11 +257,14 @@ public unsafe class Armature } /// - /// 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 /// - 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> ParseBonesFromObject(Armature arm, CharacterBase* cBase) diff --git a/CustomizePlus/Armatures/Events/ArmatureChanged.cs b/CustomizePlus/Armatures/Events/ArmatureChanged.cs index 982d938..ae91d2c 100644 --- a/CustomizePlus/Armatures/Events/ArmatureChanged.cs +++ b/CustomizePlus/Armatures/Events/ArmatureChanged.cs @@ -7,17 +7,19 @@ namespace CustomizePlus.Armatures.Events; /// /// Triggered when armature is changed /// -public sealed class ArmatureChanged() : EventWrapper(nameof(ArmatureChanged)) +public sealed class ArmatureChanged() : EventWrapper(nameof(ArmatureChanged)) { public enum Type { - //Created, - Deleted + Created, + Deleted, + Rebound } public enum Priority { - ProfileManager + ProfileManager, + CustomizePlusIpc } public enum DeletionReason diff --git a/CustomizePlus/Armatures/Services/ArmatureManager.cs b/CustomizePlus/Armatures/Services/ArmatureManager.cs index e0b6423..15da4d8 100644 --- a/CustomizePlus/Armatures/Services/ArmatureManager.cs +++ b/CustomizePlus/Armatures/Services/ArmatureManager.cs @@ -19,6 +19,7 @@ using CustomizePlus.GameData.Data; using CustomizePlus.GameData.Services; using CustomizePlus.GameData.Extensions; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using System.Drawing; namespace CustomizePlus.Armatures.Services; @@ -106,15 +107,21 @@ public unsafe sealed class ArmatureManager : IDisposable _objectManager.Update(); var currentTime = DateTime.UtcNow; + var armatureExpirationDateTime = currentTime.AddSeconds(-30); foreach (var kvPair in Armatures.ToList()) { var armature = kvPair.Value; 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"); 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) @@ -134,28 +141,32 @@ public unsafe sealed class ArmatureManager : IDisposable 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) continue; - var newArm = new Armature(obj.Key, activeProfile); + var newArm = new Armature(actorIdentifier, activeProfile); TryLinkSkeleton(newArm); - Armatures.Add(obj.Key, newArm); - _logger.Debug($"Added '{newArm}' for {obj.Key.IncognitoDebug()} to cache"); + Armatures.Add(actorIdentifier, newArm); + _logger.Debug($"Added '{newArm}' for {actorIdentifier.IncognitoDebug()} to cache"); + _event.Invoke(ArmatureChanged.Type.Created, newArm, activeProfile); continue; } - var armature = Armatures[obj.Key]; + var armature = Armatures[actorIdentifier]; + + armature.UpdateLastSeen(currentTime); if (armature.IsPendingProfileRebind) { _logger.Debug($"Armature {armature} is pending profile rebind, rebinding..."); armature.IsPendingProfileRebind = false; - var activeProfile = GetProfileForActor(obj.Key); + var activeProfile = GetProfileForActor(actorIdentifier); if (activeProfile == armature.Profile) continue; @@ -166,14 +177,19 @@ public unsafe sealed class ArmatureManager : IDisposable continue; } + Profile oldProfile = armature.Profile; + armature.Profile.Armatures.Remove(armature); armature.Profile = activeProfile; activeProfile.Armatures.Add(armature); 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) return; - armature.ProtectFromRemoval(); + armature.UpdateLastSeen(); armature.IsPendingProfileRebind = true; @@ -516,7 +532,7 @@ public unsafe sealed class ArmatureManager : IDisposable foreach (var armature in profile.Armatures) { if (type == ProfileChanged.Type.TemporaryProfileDeleted) - armature.ProtectFromRemoval(); //just to be safe + armature.UpdateLastSeen(); //just to be safe armature.IsPendingProfileRebind = true; } diff --git a/CustomizePlus/Game/Services/GameObjectService.cs b/CustomizePlus/Game/Services/GameObjectService.cs index db16885..b07f328 100644 --- a/CustomizePlus/Game/Services/GameObjectService.cs +++ b/CustomizePlus/Game/Services/GameObjectService.cs @@ -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 foreach (var obj in kvPair.Value.Objects) - yield return (kvPair.Key, obj); + yield return (kvPair.Key.CreatePermanent(), obj); else - yield return (kvPair.Key, kvPair.Value.Objects[0]); + yield return (kvPair.Key.CreatePermanent(), kvPair.Value.Objects[0]); } } } diff --git a/CustomizePlus/Profiles/ProfileManager.cs b/CustomizePlus/Profiles/ProfileManager.cs index 6e2a3e2..34e28b8 100644 --- a/CustomizePlus/Profiles/ProfileManager.cs +++ b/CustomizePlus/Profiles/ProfileManager.cs @@ -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) { diff --git a/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs b/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs index 080f1d2..b6e8396 100644 --- a/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs +++ b/CustomizePlus/UI/Windows/MainWindow/Tabs/Debug/StateMonitoringTab.cs @@ -202,7 +202,7 @@ public class StateMonitoringTab ImGui.Text($"Profile: {armature.Profile.Name.Text.Incognify()} ({armature.Profile.UniqueId})"); 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:"); //DrawSingleProfile($"armature-{armature.GetHashCode()}", armature.Profile); diff --git a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs index 66409b1..90ae124 100644 --- a/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs +++ b/CustomizePlus/UI/Windows/MainWindow/Tabs/Profiles/ProfileFileSystemSelector.cs @@ -25,6 +25,7 @@ public class ProfileFileSystemSelector : FileSystemSelector