Code commit

This commit is contained in:
RisaDev
2024-01-06 01:21:41 +03:00
parent a7d7297c59
commit a486dd2c96
90 changed files with 11576 additions and 0 deletions

View File

@@ -0,0 +1,535 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using OtterGui.Log;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
using System.Numerics;
using CustomizePlus.Core.Data;
using CustomizePlus.Armatures.Events;
using CustomizePlus.Armatures.Data;
using CustomizePlus.Profiles;
using CustomizePlus.Profiles.Data;
using CustomizePlus.Game.Services;
using CustomizePlus.Templates.Events;
using CustomizePlus.Profiles.Events;
using CustomizePlus.Core.Extensions;
using CustomizePlus.GameData.Data;
using CustomizePlus.GameData.Services;
using CustomizePlus.GameData.Extensions;
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 ActorService _actorService;
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,
ActorService actorService,
ArmatureChanged @event)
{
_profileManager = profileManager;
_objectTable = objectTable;
_gameObjectService = gameObjectService;
_templateChangedEvent = templateChangedEvent;
_profileChangedEvent = profileChangedEvent;
_logger = logger;
_framework = framework;
_objectManager = objectManager;
_actorService = actorService;
_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(_actorService.AwaitedService, out var identifier))
return;
if (Armatures.TryGetValue(identifier, out var armature) && armature.IsBuilt && armature.IsVisible)
ApplyRootTranslation(armature, actor);
}
/// <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;
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
{
_logger.Debug($"Removing armature {armature} because {kvPair.Key.IncognitoDebug()} is gone");
RemoveArmature(armature, ArmatureChanged.DeletionReason.Gone);
}
}
Profile? GetProfileForActor(ActorIdentifier identifier)
{
foreach (var profile in _profileManager.GetEnabledProfilesByActor(identifier))
{
if (profile.LimitLookupToOwnedObjects &&
identifier.Type == IdentifierType.Owned &&
identifier.PlayerName != _objectManager.PlayerData.Identifier.PlayerName)
continue;
return profile;
}
return null;
}
foreach (var obj in _objectManager)
{
if (!Armatures.ContainsKey(obj.Key))
{
var activeProfile = GetProfileForActor(obj.Key);
if (activeProfile == null)
continue;
var newArm = new Armature(obj.Key, activeProfile);
TryLinkSkeleton(newArm);
Armatures.Add(obj.Key, newArm);
_logger.Debug($"Added '{newArm}' for {obj.Key.IncognitoDebug()} to cache");
continue;
}
var armature = Armatures[obj.Key];
if (armature.IsPendingProfileRebind)
{
_logger.Debug($"Armature {armature} is pending profile rebind, rebinding...");
armature.IsPendingProfileRebind = false;
var activeProfile = GetProfileForActor(obj.Key);
if (activeProfile == armature.Profile)
continue;
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();
}
armature.IsVisible = armature.Profile.Enabled && TryLinkSkeleton(armature); //todo: remove armatures which are not visible?
}
}
private unsafe void ApplyArmatureTransforms()
{
foreach (var kvPair in Armatures)
{
var armature = kvPair.Value;
if (armature.IsBuilt && armature.IsVisible && _objectManager.ContainsKey(armature.ActorIdentifier))
{
foreach (var actor in _objectManager[armature.ActorIdentifier].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.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 + MathF.Max(rootBoneTransform.Translation.X, 0.01f),
Y = cBase->DrawObject.Object.Position.Y + MathF.Max(rootBoneTransform.Translation.Y, 0.01f),
Z = cBase->DrawObject.Object.Position.Z + MathF.Max(rootBoneTransform.Translation.Z, 0.01f)
};
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.RebuildBoneTemplateBinding());
}
});
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.ChangedCharacterName &&
type is not ProfileChanged.Type.ChangedDefaultProfile &&
type is not ProfileChanged.Type.LimitLookupToOwnedChanged)
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 (string.IsNullOrWhiteSpace(profile.CharacterName))
return;
foreach (var armature in GetArmaturesForCharacterName(profile.CharacterName))
{
armature.IsPendingProfileRebind = true;
_logger.Debug($"ArmatureManager.OnProfileChange profile {profile} toggled, planning rebind for armature {armature}");
}
return;
}
if (type == ProfileChanged.Type.TemporaryProfileAdded)
{
if (!profile.TemporaryActor.IsValid || !Armatures.ContainsKey(profile.TemporaryActor))
return;
var armature = Armatures[profile.TemporaryActor];
if (armature.Profile == profile)
return;
armature.ProtectFromRemoval();
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.ChangedCharacterName ||
type == ProfileChanged.Type.Deleted ||
type == ProfileChanged.Type.TemporaryProfileDeleted ||
type == ProfileChanged.Type.LimitLookupToOwnedChanged)
{
if (profile.Armatures.Count == 0)
return;
foreach (var armature in profile.Armatures)
{
if (type == ProfileChanged.Type.TemporaryProfileDeleted)
armature.ProtectFromRemoval(); //just to be safe
armature.IsPendingProfileRebind = true;
}
_logger.Debug($"ArmatureManager.OnProfileChange CCN/DEL/TPD/LLTOC, 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.RebuildBoneTemplateBinding());
}
private IEnumerable<Armature> GetArmaturesForCharacterName(string characterName)
{
var actors = _gameObjectService.FindActorsByName(characterName).ToList();
if (actors.Count == 0)
yield break;
foreach (var actorData in actors)
{
if (!Armatures.TryGetValue(actorData.Item1, out var armature))
continue;
yield return armature;
}
}
}