Additional checks to make sure profile update event is not getting sent when editor and default profile is involved Selected default profile can no longer be changed if profile set as default is enabled Fixed "Limit to my creatures" not ignoring objects with IdentifierType != Owned IPC test tab no longer initializes IPC if debug mode is disabled Fixed incorrect warning priority in plugin state control Some slight text changes
568 lines
23 KiB
C#
568 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>
|
|
/// 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;
|
|
if (!_objectManager.ContainsKey(kvPair.Value.ActorIdentifier) &&
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
var actorIdentifier = obj.Key.CreatePermanent();
|
|
if (!Armatures.ContainsKey(actorIdentifier))
|
|
{
|
|
var activeProfile = GetProfileForActor(actorIdentifier);
|
|
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 = GetProfileForActor(actorIdentifier);
|
|
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.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.EditorLimitLookupToOwnedChanged &&
|
|
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.EditorLimitLookupToOwnedChanged)
|
|
{
|
|
var profile = (Profile)arg3!;
|
|
|
|
if (profile.Armatures.Count == 0)
|
|
return;
|
|
|
|
foreach (var armature in profile.Armatures)
|
|
armature.IsPendingProfileRebind = true;
|
|
|
|
_logger.Debug($"ArmatureManager.OnTemplateChange Editor profile limit lookup setting changed, armature rebind scheduled: {type}, profile: {profile.Name.Text.Incognify()}->{profile.Enabled}");
|
|
|
|
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.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.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.UpdateLastSeen(); //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.IsPendingProfileRebind = true);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
} |