Files
CustomizeTool/CustomizePlus/Armatures/Services/ArmatureManager.cs
RisaDev 7a0ee53756 Hopefully this doesn't break anything, I can't test this properly on my free trial account
Removed "Limit to my creatures", the code now automatically detects this for all owned actors. If you liked to apply edits to minions and stuff of other players... too bad.
Implemented UI for setting profiles to NPC, minions and mounts (still WIP, will probably have to implement multiple characters per profile)
2024-10-08 00:32:58 +03:00

557 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using CustomizePlus.Armatures.Data;
using CustomizePlus.Armatures.Events;
using CustomizePlus.Core.Data;
using CustomizePlus.Core.Extensions;
using CustomizePlus.Game.Services;
using CustomizePlus.GameData.Extensions;
using CustomizePlus.GameData.Services;
using CustomizePlus.Profiles;
using CustomizePlus.Profiles.Data;
using CustomizePlus.Profiles.Events;
using CustomizePlus.Templates.Events;
using Dalamud.Plugin.Services;
using OtterGui.Classes;
using OtterGui.Log;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using ObjectManager = CustomizePlus.GameData.Services.ObjectManager;
namespace CustomizePlus.Armatures.Services;
public unsafe sealed class ArmatureManager : IDisposable
{
private readonly ProfileManager _profileManager;
private readonly IObjectTable _objectTable;
private readonly GameObjectService _gameObjectService;
private readonly TemplateChanged _templateChangedEvent;
private readonly ProfileChanged _profileChangedEvent;
private readonly Logger _logger;
private readonly FrameworkManager _framework;
private readonly ObjectManager _objectManager;
private readonly ActorManager _actorManager;
private readonly ArmatureChanged _event;
public Dictionary<ActorIdentifier, Armature> Armatures { get; private set; } = new();
public ArmatureManager(
ProfileManager profileManager,
IObjectTable objectTable,
GameObjectService gameObjectService,
TemplateChanged templateChangedEvent,
ProfileChanged profileChangedEvent,
Logger logger,
FrameworkManager framework,
ObjectManager objectManager,
ActorManager actorManager,
ArmatureChanged @event)
{
_profileManager = profileManager;
_objectTable = objectTable;
_gameObjectService = gameObjectService;
_templateChangedEvent = templateChangedEvent;
_profileChangedEvent = profileChangedEvent;
_logger = logger;
_framework = framework;
_objectManager = objectManager;
_actorManager = actorManager;
_event = @event;
_templateChangedEvent.Subscribe(OnTemplateChange, TemplateChanged.Priority.ArmatureManager);
_profileChangedEvent.Subscribe(OnProfileChange, ProfileChanged.Priority.ArmatureManager);
}
public void Dispose()
{
_templateChangedEvent.Unsubscribe(OnTemplateChange);
_profileChangedEvent.Unsubscribe(OnProfileChange);
}
/// <summary>
/// Main rendering function, called from rendering hook
/// </summary>
public void OnRender()
{
try
{
RefreshArmatures();
ApplyArmatureTransforms();
}
catch (Exception ex)
{
_logger.Error($"Exception while rendering armatures:\n\t{ex}");
}
}
/// <summary>
/// Function called when game object movement is detected
/// </summary>
public void OnGameObjectMove(Actor actor)
{
if (!actor.Identifier(_actorManager, out var identifier))
return;
if (Armatures.TryGetValue(identifier, out var armature) && armature.IsBuilt && armature.IsVisible)
ApplyRootTranslation(armature, actor);
}
/// <summary>
/// Force profile rebind for all armatures
/// </summary>
public void RebindAllArmatures()
{
foreach (var kvPair in Armatures)
kvPair.Value.IsPendingProfileRebind = true;
}
/// <summary>
/// Deletes armatures which no longer have actor associated with them and creates armatures for new actors
/// </summary>
private void RefreshArmatures()
{
_objectManager.Update();
var currentTime = DateTime.UtcNow;
var armatureExpirationDateTime = currentTime.AddSeconds(-30);
foreach (var kvPair in Armatures.ToList())
{
var armature = kvPair.Value;
//Only remove armatures which haven't been seen for a while
//But remove armatures of special actors (like examine screen) right away
if (!_objectManager.Identifiers.ContainsKey(kvPair.Value.ActorIdentifier) &&
(armature.LastSeen <= armatureExpirationDateTime || armature.ActorIdentifier.Type == IdentifierType.Special))
{
_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;
}
foreach (var obj in _objectManager.Identifiers)
{
var actorIdentifier = obj.Key.CreatePermanent();
if (!Armatures.ContainsKey(actorIdentifier))
{
var activeProfile = _profileManager.GetEnabledProfilesByActor(actorIdentifier).FirstOrDefault();
if (activeProfile == null)
continue;
var newArm = new Armature(actorIdentifier, activeProfile);
TryLinkSkeleton(newArm);
Armatures.Add(actorIdentifier, newArm);
_logger.Debug($"Added '{newArm}' for {actorIdentifier.IncognitoDebug()} to cache");
_event.Invoke(ArmatureChanged.Type.Created, newArm, activeProfile);
continue;
}
var armature = Armatures[actorIdentifier];
armature.UpdateLastSeen(currentTime);
if (armature.IsPendingProfileRebind)
{
_logger.Debug($"Armature {armature} is pending profile/bone rebind, rebinding...");
armature.IsPendingProfileRebind = false;
var activeProfile = _profileManager.GetEnabledProfilesByActor(actorIdentifier).FirstOrDefault();
Profile? oldProfile = armature.Profile;
if (activeProfile != armature.Profile)
{
if (activeProfile == null)
{
_logger.Debug($"Removing armature {armature} because it doesn't have any active profiles");
RemoveArmature(armature, ArmatureChanged.DeletionReason.NoActiveProfiles);
continue;
}
armature.Profile.Armatures.Remove(armature);
armature.Profile = activeProfile;
activeProfile.Armatures.Add(armature);
}
armature.RebuildBoneTemplateBinding();
_event.Invoke(ArmatureChanged.Type.Updated, armature, (activeProfile, oldProfile));
}
//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);
}
}
private unsafe void ApplyArmatureTransforms()
{
foreach (var kvPair in Armatures)
{
var armature = kvPair.Value;
if (armature.IsBuilt && armature.IsVisible && _objectManager.TryGetValue(armature.ActorIdentifier, out var actorData))
{
foreach (var actor in actorData.Objects)
ApplyPiecewiseTransformation(armature, actor, armature.ActorIdentifier);
}
}
}
/// <summary>
/// Returns whether or not a link can be established between the armature and an in-game object.
/// If unbuilt, the armature will be rebuilded.
/// </summary>
private bool TryLinkSkeleton(Armature armature, bool forceRebuild = false)
{
_objectManager.Update();
try
{
if (!_objectManager.Identifiers.ContainsKey(armature.ActorIdentifier))
return false;
var actor = _objectManager[armature.ActorIdentifier].Objects[0];
if (!armature.IsBuilt || forceRebuild)
{
armature.RebuildSkeleton(actor.Model.AsCharacterBase);
}
else if (armature.NewBonesAvailable(actor.Model.AsCharacterBase))
{
armature.AugmentSkeleton(actor.Model.AsCharacterBase);
}
return true;
}
catch (Exception ex)
{
// This is on wait until isse #191 on Github responds. Keeping it in code, delete it if I forget and this is longer then a month ago.
// Disabling this if its any Default Profile due to Log spam. A bit crazy but hey, if its for me id Remove Default profiles all together so this is as much as ill do for now! :)
//if(!(Profile.CharacterName.Equals(Constants.DefaultProfileCharacterName) || Profile.CharacterName.Equals("DefaultCutscene"))) {
_logger.Error($"Error occured while attempting to link skeleton: {armature}");
throw;
//}
}
}
/// <summary>
/// Iterate through the skeleton of the given character base, and apply any transformations
/// for which this armature contains corresponding model bones. This method of application
/// is safer but more computationally costly
/// </summary>
private void ApplyPiecewiseTransformation(Armature armature, Actor actor, ActorIdentifier actorIdentifier)
{
var cBase = actor.Model.AsCharacterBase;
var isMount = actorIdentifier.Type == IdentifierType.Owned &&
actorIdentifier.Kind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.MountType;
Actor? mountOwner = null;
Armature? mountOwnerArmature = null;
if (isMount)
{
(var ident, mountOwner) = _gameObjectService.FindActorsByName(actorIdentifier.PlayerName.ToString()).FirstOrDefault();
Armatures.TryGetValue(ident, out mountOwnerArmature);
}
if (cBase != null)
{
for (var pSkeleIndex = 0; pSkeleIndex < cBase->Skeleton->PartialSkeletonCount; ++pSkeleIndex)
{
var currentPose = cBase->Skeleton->PartialSkeletons[pSkeleIndex].GetHavokPose(Constants.TruePoseIndex);
if (currentPose != null)
{
for (var boneIndex = 0; boneIndex < currentPose->Skeleton->Bones.Length; ++boneIndex)
{
if (armature.GetBoneAt(pSkeleIndex, boneIndex) is ModelBone mb
&& mb != null
&& mb.BoneName == currentPose->Skeleton->Bones[boneIndex].Name.String)
{
if (mb == armature.MainRootBone)
{
if (_gameObjectService.IsActorHasScalableRoot(actor) && mb.IsModifiedScale())
{
cBase->DrawObject.Object.Scale = mb.CustomizedTransform!.Scaling;
//Fix mount owner's scale if needed
//todo: always keep owner's scale proper instead of scaling with mount if no armature found
if (isMount && mountOwner != null && mountOwnerArmature != null)
{
var ownerDrawObject = cBase->DrawObject.Object.ChildObject;
//limit to only modified scales because that is just easier to handle
//because we don't need to hook into dismount code to reset character scale
//todo: hook into dismount
//https://github.com/Cytraen/SeatedSidekickSpectator/blob/main/SetModeHook.cs?
if (cBase->DrawObject.Object.ChildObject == mountOwner.Value.Model &&
mountOwnerArmature.MainRootBone.IsModifiedScale())
{
var baseScale = mountOwnerArmature.MainRootBone.CustomizedTransform!.Scaling;
ownerDrawObject->Scale = new Vector3(Math.Abs(baseScale.X / cBase->DrawObject.Object.Scale.X),
Math.Abs(baseScale.Y / cBase->DrawObject.Object.Scale.Y),
Math.Abs(baseScale.Z / cBase->DrawObject.Object.Scale.Z));
}
}
}
}
else
{
mb.ApplyModelTransform(cBase);
}
}
}
}
}
}
}
private void ApplyRootTranslation(Armature arm, Actor actor)
{
//I'm honestly not sure if we should or even can check if cBase->DrawObject or cBase->DrawObject.Object is a valid object
//So for now let's assume we don't need to check for that
var cBase = actor.Model.AsCharacterBase;
if (cBase != null)
{
var rootBoneTransform = arm.GetAppliedBoneTransform("n_root");
if (rootBoneTransform == null)
return;
if (rootBoneTransform.Translation.X == 0 &&
rootBoneTransform.Translation.Y == 0 &&
rootBoneTransform.Translation.Z == 0)
return;
if (!cBase->DrawObject.IsVisible)
return;
var newPosition = new FFXIVClientStructs.FFXIV.Common.Math.Vector3
{
X = cBase->DrawObject.Object.Position.X + rootBoneTransform.Translation.X,
Y = cBase->DrawObject.Object.Position.Y + rootBoneTransform.Translation.Y,
Z = cBase->DrawObject.Object.Position.Z + rootBoneTransform.Translation.Z
};
cBase->DrawObject.Object.Position = newPosition;
}
}
private void RemoveArmature(Armature armature, ArmatureChanged.DeletionReason reason)
{
armature.Profile.Armatures.Remove(armature);
Armatures.Remove(armature.ActorIdentifier);
_logger.Debug($"Armature {armature} removed from cache");
_event.Invoke(ArmatureChanged.Type.Deleted, armature, reason);
}
private void OnTemplateChange(TemplateChanged.Type type, Templates.Data.Template? template, object? arg3)
{
if (type is not TemplateChanged.Type.NewBone &&
type is not TemplateChanged.Type.DeletedBone &&
type is not TemplateChanged.Type.EditorCharacterChanged &&
type is not TemplateChanged.Type.EditorEnabled &&
type is not TemplateChanged.Type.EditorDisabled)
return;
if (type == TemplateChanged.Type.NewBone ||
type == TemplateChanged.Type.DeletedBone) //type == TemplateChanged.Type.EditorCharacterChanged?
{
//In case a lot of events are triggered at the same time for the same template this should limit the amount of times bindings are unneccessary rebuilt
_framework.RegisterImportant($"TemplateRebuild @ {template.UniqueId}", () =>
{
foreach (var profile in _profileManager.GetProfilesUsingTemplate(template))
{
_logger.Debug($"ArmatureManager.OnTemplateChange New/Deleted bone or character changed: {type}, template: {template.Name.Text.Incognify()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}->{profile.Armatures.Count} armatures");
if (!profile.Enabled || profile.Armatures.Count == 0)
continue;
profile.Armatures.ForEach(x => x.IsPendingProfileRebind = true);
}
});
return;
}
if (type == TemplateChanged.Type.EditorCharacterChanged)
{
(var characterName, var profile) = ((string, Profile))arg3;
foreach (var armature in GetArmaturesForCharacterName(characterName))
{
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile character name changed, armature rebind scheduled: {type}, {armature}");
}
if (profile.Armatures.Count == 0)
return;
//Rebuild armatures for previous character
foreach (var armature in profile.Armatures)
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile character name changed, armature rebind scheduled: {type}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}, new name: {characterName.Incognify()}");
return;
}
if (type == TemplateChanged.Type.EditorEnabled ||
type == TemplateChanged.Type.EditorDisabled)
{
foreach (var armature in GetArmaturesForCharacterName((string)arg3!))
{
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnTemplateChange template editor enabled/disabled: {type}, pending profile set for {armature}");
}
return;
}
}
private void OnProfileChange(ProfileChanged.Type type, Profile? profile, object? arg3)
{
if (type is not ProfileChanged.Type.AddedTemplate &&
type is not ProfileChanged.Type.RemovedTemplate &&
type is not ProfileChanged.Type.MovedTemplate &&
type is not ProfileChanged.Type.ChangedTemplate &&
type is not ProfileChanged.Type.Toggled &&
type is not ProfileChanged.Type.Deleted &&
type is not ProfileChanged.Type.TemporaryProfileAdded &&
type is not ProfileChanged.Type.TemporaryProfileDeleted &&
type is not ProfileChanged.Type.ChangedCharacter &&
type is not ProfileChanged.Type.ChangedDefaultProfile &&
type is not ProfileChanged.Type.ApplyToCurrentlyActiveCharacterChanged)
return;
if (type == ProfileChanged.Type.ChangedDefaultProfile)
{
var oldProfile = (Profile?)arg3;
if (oldProfile == null || oldProfile.Armatures.Count == 0)
return;
foreach (var armature in oldProfile.Armatures)
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnProfileChange Profile no longer default, armatures rebind scheduled: {type}, old profile: {oldProfile.Name.Text.Incognify()}->{oldProfile.Enabled}");
return;
}
if (profile == null)
{
_logger.Error($"ArmatureManager.OnProfileChange Invalid input for event: {type}, profile is null.");
return;
}
if (type == ProfileChanged.Type.Toggled)
{
if (!profile.Enabled && profile.Armatures.Count == 0)
return;
if (profile == _profileManager.DefaultProfile)
{
foreach (var kvPair in Armatures)
{
var armature = kvPair.Value;
if (armature.Profile == profile)
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnProfileChange default profile toggled, planning rebind for armature {armature}");
}
return;
}
if (!profile.Character.IsValid)
return;
foreach (var armature in GetArmaturesForCharacter(profile.Character))
{
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnProfileChange profile {profile} toggled, planning rebind for armature {armature}");
}
return;
}
if (type == ProfileChanged.Type.TemporaryProfileAdded)
{
if (!profile.Character.IsValid || !Armatures.ContainsKey(profile.Character)) //todo: any world support
return;
var armature = Armatures[profile.Character];
if (armature.Profile == profile)
return;
armature.UpdateLastSeen();
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnProfileChange TemporaryProfileAdded, calling rebind for existing armature: {type}, data payload: {arg3?.ToString()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
return;
}
if (type == ProfileChanged.Type.ChangedCharacter ||
type == ProfileChanged.Type.Deleted ||
type == ProfileChanged.Type.TemporaryProfileDeleted ||
type == ProfileChanged.Type.ApplyToCurrentlyActiveCharacterChanged)
{
if (profile.Armatures.Count == 0)
return;
foreach (var armature in profile.Armatures)
{
if (type == ProfileChanged.Type.TemporaryProfileDeleted)
armature.UpdateLastSeen(); //just to be safe
armature.IsPendingProfileRebind = true;
}
_logger.Debug($"ArmatureManager.OnProfileChange CC/DEL/TPD/ATCACC, armature rebind scheduled: {type}, data payload: {arg3?.ToString()?.Incognify()}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
return;
}
//todo: shouldn't happen, but happens sometimes? I think?
if (profile.Armatures.Count == 0)
return;
_logger.Debug($"ArmatureManager.OnProfileChange Added/Deleted/Moved/Changed template: {type}, data payload: {arg3?.ToString()}, profile: {profile.Name}->{profile.Enabled}->{profile.Armatures.Count} armatures");
profile!.Armatures.ForEach(x => x.IsPendingProfileRebind = true);
}
private IEnumerable<Armature> GetArmaturesForCharacterName(string characterName)
{
foreach(var kvPair in Armatures)
{
(var actorIdentifier, _) = _gameObjectService.GetTrueActorForSpecialTypeActor(kvPair.Key);
if(actorIdentifier.ToNameWithoutOwnerName() == characterName)
yield return kvPair.Value;
}
}
private IEnumerable<Armature> GetArmaturesForCharacter(ActorIdentifier actorIdentifier)
{
foreach (var kvPair in Armatures)
{
(var armatureActorIdentifier, _) = _gameObjectService.GetTrueActorForSpecialTypeActor(kvPair.Key);
if (actorIdentifier.IsValid && armatureActorIdentifier.CompareIgnoringOwnership(actorIdentifier) &&
(armatureActorIdentifier.Type != IdentifierType.Owned || armatureActorIdentifier.IsOwnedByLocalPlayer()))
yield return kvPair.Value;
}
}
}