562 lines
28 KiB
C#
562 lines
28 KiB
C#
using Microsoft.Xna.Framework;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using Barotrauma.IO;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Xml.Linq;
|
|
using Barotrauma.Extensions;
|
|
|
|
namespace Barotrauma
|
|
{
|
|
public enum AnimationType
|
|
{
|
|
NotDefined = 0,
|
|
Walk = 1,
|
|
Run = 2,
|
|
SwimSlow = 3,
|
|
SwimFast = 4,
|
|
Crouch = 5
|
|
}
|
|
|
|
abstract class GroundedMovementParams : AnimationParams
|
|
{
|
|
[Header("Legs")]
|
|
[Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
|
|
public Vector2 StepSize
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Header("Standing")]
|
|
[Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
|
|
public float HeadPosition { get; set; }
|
|
|
|
[Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
|
|
public float TorsoPosition { get; set; }
|
|
|
|
[Header("Step lift")]
|
|
[Serialize(1f, IsPropertySaveable.Yes, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
|
|
public float StepLiftHeadMultiplier { get; set; }
|
|
|
|
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)]
|
|
public float StepLiftAmount { get; set; }
|
|
|
|
[Serialize(0.5f, IsPropertySaveable.Yes, description: "When does the body raise when taking a step. The default (0.5) is in the middle of the step."), Editable(MinValueFloat = -1, MaxValueFloat = 1, DecimalCount = 2, ValueStep = 0.1f)]
|
|
public float StepLiftOffset { get; set; }
|
|
|
|
[Serialize(2f, IsPropertySaveable.Yes, description: "How frequently the body raises when taking a step. The default is 2 (after every step)."), Editable(MinValueFloat = 0, MaxValueFloat = 10, ValueStep = 0.1f)]
|
|
public float StepLiftFrequency { get; set; }
|
|
|
|
[Header("Movement")]
|
|
[Serialize(0.75f, IsPropertySaveable.Yes, description: "The character's movement speed is multiplied with this value when moving backwards."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2)]
|
|
public float BackwardsMovementMultiplier { get; set; }
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Adjusts the maximum speed while climbing. The actual speed is affected by the MovementSpeed."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10f, DecimalCount = 2)]
|
|
public float ClimbSpeed { get; set; }
|
|
|
|
[Serialize(2.0f, IsPropertySaveable.Yes, description: "Used instead of ClimbSpeed when descending ladders while moving fast (running). Not used if lower than ClimbSpeed."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 10f, DecimalCount = 2)]
|
|
public float SlideSpeed { get; set; }
|
|
|
|
[Serialize(10.5f, IsPropertySaveable.Yes, description: "Force applied to the main collider, torso and head, when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
|
|
public float ClimbBodyMoveForce { get; set; }
|
|
|
|
[Serialize(5.2f, IsPropertySaveable.Yes, description: "Force applied to the hands when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
|
|
public float ClimbHandMoveForce { get; set; }
|
|
|
|
[Serialize(10.0f, IsPropertySaveable.Yes, description: "Force applied to the feet when climbing ladders."), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
|
|
public float ClimbFootMoveForce { get; set; }
|
|
|
|
[Serialize(30.0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
|
|
public float ClimbStepHeight { get; set; }
|
|
|
|
protected override bool Deserialize(XElement element = null)
|
|
{
|
|
if (element.GetAttributeEnum(nameof(AnimationType), AnimationType.NotDefined) is AnimationType.Run)
|
|
{
|
|
// These values were previously hard-coded when running, so we need to set different default values for the run animations, when they are not defined.
|
|
const string climbSpeedName = nameof(ClimbSpeed);
|
|
if (element.GetAttribute(climbSpeedName) == null)
|
|
{
|
|
element.SetAttribute(climbSpeedName, 2.0f);
|
|
}
|
|
const string climbStepName = nameof(ClimbStepHeight);
|
|
if (element.GetAttribute(climbStepName) == null)
|
|
{
|
|
element.SetAttribute(climbStepName, 60.0f);
|
|
}
|
|
const string slideSpeedName = nameof(SlideSpeed);
|
|
if (element.GetAttribute(slideSpeedName) == null)
|
|
{
|
|
element.SetAttribute(slideSpeedName, 4.0f);
|
|
}
|
|
}
|
|
return base.Deserialize(element);
|
|
}
|
|
}
|
|
|
|
abstract class SwimParams : AnimationParams
|
|
{
|
|
[Serialize(25.0f, IsPropertySaveable.Yes, description: "Turning speed (or rather a force applied on the main collider to make it turn). Note that you can set a limb-specific steering forces too (additional)."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
|
public float SteerTorque { get; set; }
|
|
|
|
[Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
|
public float LegTorque { get; set; }
|
|
}
|
|
|
|
abstract class AnimationParams : EditableParams, IMemorizable<AnimationParams>
|
|
{
|
|
public Identifier SpeciesName { get; private set; }
|
|
public bool IsGroundedAnimation => AnimationType is AnimationType.Walk or AnimationType.Run or AnimationType.Crouch;
|
|
public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast;
|
|
|
|
[Header("General")]
|
|
[Serialize(AnimationType.NotDefined, IsPropertySaveable.Yes), Editable]
|
|
public virtual AnimationType AnimationType { get; protected set; }
|
|
/// <summary>
|
|
/// The cached animations of all the characters that have been loaded.
|
|
/// Thread-safe cache using ConcurrentDictionary.
|
|
/// </summary>
|
|
private static readonly ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>> allAnimations = new ConcurrentDictionary<Identifier, ConcurrentDictionary<string, AnimationParams>>();
|
|
|
|
[Header("Movement")]
|
|
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
|
|
public float MovementSpeed { get; set; }
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)"),
|
|
Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)]
|
|
public float CycleSpeed { get; set; }
|
|
|
|
/// <summary>
|
|
/// In degrees.
|
|
/// </summary>
|
|
[Header("Orientation")]
|
|
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
|
|
public float HeadAngle
|
|
{
|
|
get => float.IsNaN(HeadAngleInRadians) ? float.NaN : MathHelper.ToDegrees(HeadAngleInRadians);
|
|
set
|
|
{
|
|
if (!float.IsNaN(value))
|
|
{
|
|
HeadAngleInRadians = MathHelper.ToRadians(value);
|
|
}
|
|
}
|
|
}
|
|
public float HeadAngleInRadians { get; private set; } = float.NaN;
|
|
|
|
/// <summary>
|
|
/// In degrees.
|
|
/// </summary>
|
|
[Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
|
|
public float TorsoAngle
|
|
{
|
|
get => float.IsNaN(TorsoAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TorsoAngleInRadians);
|
|
set
|
|
{
|
|
if (!float.IsNaN(value))
|
|
{
|
|
TorsoAngleInRadians = MathHelper.ToRadians(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
public float TorsoAngleInRadians { get; private set; } = float.NaN;
|
|
|
|
[Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the head to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
|
public float HeadTorque { get; set; }
|
|
|
|
[Serialize(50.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the torso to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
|
public float TorsoTorque { get; set; }
|
|
|
|
[Header("Legs")]
|
|
[Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to rotate the feet to the correct orientation."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
|
|
public float FootTorque { get; set; }
|
|
|
|
[Header("Arms")]
|
|
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the arms to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
|
|
public float ArmIKStrength { get; set; }
|
|
|
|
[Serialize(1f, IsPropertySaveable.Yes, description: "How much force is used to rotate the hands to the IK position."), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
|
|
public float HandIKStrength { get; set; }
|
|
|
|
public static string GetDefaultFileName(Identifier speciesName, AnimationType animType) => $"{speciesName.Value.CapitaliseFirstInvariant()}{animType}";
|
|
public static string GetDefaultFilePath(Identifier speciesName, AnimationType animType) => Barotrauma.IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
|
|
|
|
public static string GetFolder(Identifier speciesName)
|
|
{
|
|
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
|
if (prefab?.ConfigElement == null)
|
|
{
|
|
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: prefab?.ContentPackage);
|
|
return string.Empty;
|
|
}
|
|
return GetFolder(prefab.ConfigElement, prefab.FilePath.Value);
|
|
}
|
|
|
|
private static string GetFolder(ContentXElement root, string filePath)
|
|
{
|
|
Debug.Assert(filePath != null);
|
|
Debug.Assert(root != null);
|
|
string folder = root.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value;
|
|
if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Animations");
|
|
}
|
|
return folder.CleanUpPathCrossPlatform(correctFilenameCase: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects all file paths that match the specified animation type and filters them alphabetically.
|
|
/// </summary>
|
|
public static IEnumerable<string> FilterAndSortFiles(IEnumerable<string> filePaths, AnimationType type)
|
|
{
|
|
return filePaths.Where(f => AnimationPredicate(f, type)).OrderBy(f => f, StringComparer.OrdinalIgnoreCase);
|
|
|
|
static bool AnimationPredicate(string filePath, AnimationType type)
|
|
{
|
|
XDocument doc = XMLExtensions.TryLoadXml(filePath);
|
|
if (doc == null) { return false; }
|
|
return doc.GetRootExcludingOverride().GetAttributeEnum("animationtype", AnimationType.NotDefined) == type;
|
|
}
|
|
}
|
|
|
|
protected static T GetDefaultAnimParams<T>(Character character, AnimationType animType) where T : AnimationParams, new()
|
|
{
|
|
// Using a null file definition means we are taking a first matching file from the folder.
|
|
return GetAnimParams<T>(character, animType, file: null, throwErrors: true);
|
|
}
|
|
|
|
protected static T GetAnimParams<T>(Character character, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
|
|
{
|
|
Identifier speciesName = character.SpeciesName;
|
|
Identifier animSpecies = speciesName;
|
|
if (!character.VariantOf.IsEmpty)
|
|
{
|
|
string folder = character.Params.VariantFile?.GetRootExcludingOverride().GetChildElement("animations")?.GetAttributeContentPath("folder", character.Prefab.ContentPackage)?.Value;
|
|
if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Use the animations defined in the base definition file.
|
|
animSpecies = character.Prefab.GetBaseCharacterSpeciesName(speciesName);
|
|
}
|
|
}
|
|
return GetAnimParams<T>(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors);
|
|
}
|
|
|
|
// ThreadLocal for thread-safe error message collection during animation loading
|
|
private static readonly ThreadLocal<List<string>> errorMessagesLocal = new ThreadLocal<List<string>>(() => new List<string>());
|
|
private static List<string> errorMessages => errorMessagesLocal.Value;
|
|
|
|
private static T GetAnimParams<T>(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
|
|
{
|
|
Debug.Assert(!speciesName.IsEmpty);
|
|
Debug.Assert(!animSpecies.IsEmpty);
|
|
ContentPath contentPath = null;
|
|
string fileName = null;
|
|
if (file != null)
|
|
{
|
|
if (!file.TryGet(out fileName))
|
|
{
|
|
file.TryGet(out contentPath);
|
|
}
|
|
Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
|
|
}
|
|
ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
|
|
Debug.Assert(contentPackage != null);
|
|
var animations = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
|
|
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType);
|
|
if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
|
|
{
|
|
// Already cached.
|
|
return (T)anim;
|
|
}
|
|
if (!contentPath.IsNullOrEmpty())
|
|
{
|
|
// Load the animation from path.
|
|
T animInstance = new T();
|
|
if (animInstance.Load(contentPath, speciesName))
|
|
{
|
|
if (animInstance.AnimationType == animType)
|
|
{
|
|
animations.TryAdd(contentPath.Value, animInstance);
|
|
return animInstance;
|
|
}
|
|
else
|
|
{
|
|
errorMessages.Add($"[AnimationParams] Animation type mismatch. Expected: {animType}, Actual: {animInstance.AnimationType}. Using the default animation.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
errorMessages.Add($"[AnimationParams] Failed to load an animation {animInstance} of type {animType} from {contentPath.Value} for the character {speciesName}. Using the default animation.");
|
|
}
|
|
}
|
|
// Seek the correct animation from the character's animation folder.
|
|
string selectedFile = null;
|
|
string folder = GetFolder(animSpecies);
|
|
if (Directory.Exists(folder))
|
|
{
|
|
string[] files = Directory.GetFiles(folder);
|
|
if (files.None())
|
|
{
|
|
errorMessages.Add($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation.");
|
|
}
|
|
else
|
|
{
|
|
var filteredFiles = FilterAndSortFiles(files, animType);
|
|
if (filteredFiles.None())
|
|
{
|
|
errorMessages.Add($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation.");
|
|
}
|
|
else if (string.IsNullOrEmpty(fileName))
|
|
{
|
|
// Files found, but none specified -> Get a matching animation from the specified folder.
|
|
// First try to find a file that matches the default file name. If that fails, just take any file of the matching type.
|
|
string defaultFileName = GetDefaultFileName(animSpecies, animType);
|
|
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First();
|
|
}
|
|
else
|
|
{
|
|
// Try to get the specified file. If that fails, just take any file of the matching type.
|
|
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName));
|
|
if (selectedFile == null)
|
|
{
|
|
errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the first file of the matching type.");
|
|
selectedFile = filteredFiles.First();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
errorMessages.Add($"[AnimationParams] Invalid directory: {folder}. Using the default animation.");
|
|
}
|
|
selectedFile ??= GetDefaultFilePath(fallbackSpecies, animType);
|
|
Debug.Assert(selectedFile != null);
|
|
if (errorMessages.None())
|
|
{
|
|
DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}.");
|
|
}
|
|
T animationInstance = new T();
|
|
if (animationInstance.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName))
|
|
{
|
|
animations.TryAdd(key, animationInstance);
|
|
}
|
|
else
|
|
{
|
|
errorMessages.Add($"[AnimationParams] Failed to load an animation {animationInstance} at {selectedFile} of type {animType} for the character {speciesName}");
|
|
}
|
|
foreach (string errorMsg in errorMessages)
|
|
{
|
|
if (throwErrors)
|
|
{
|
|
DebugConsole.ThrowError(errorMsg, contentPackage: contentPackage);
|
|
}
|
|
else
|
|
{
|
|
DebugConsole.Log("Logging a supressed (potential) error: " + errorMsg);
|
|
}
|
|
}
|
|
errorMessages.Clear();
|
|
return animationInstance;
|
|
|
|
static bool PathMatchesFile(string p, string f) => IO.Path.GetFileNameWithoutExtension(p).Equals(f, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public static void ClearCache() => allAnimations.Clear();
|
|
|
|
public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type animationParamsType)
|
|
{
|
|
if (animationParamsType == typeof(HumanWalkParams))
|
|
{
|
|
return Create<HumanWalkParams>(fullPath, speciesName, animationType);
|
|
}
|
|
if (animationParamsType == typeof(HumanRunParams))
|
|
{
|
|
return Create<HumanRunParams>(fullPath, speciesName, animationType);
|
|
}
|
|
if (animationParamsType == typeof(HumanSwimSlowParams))
|
|
{
|
|
return Create<HumanSwimSlowParams>(fullPath, speciesName, animationType);
|
|
}
|
|
if (animationParamsType == typeof(HumanSwimFastParams))
|
|
{
|
|
return Create<HumanSwimFastParams>(fullPath, speciesName, animationType);
|
|
}
|
|
if (animationParamsType == typeof(HumanCrouchParams))
|
|
{
|
|
return Create<HumanCrouchParams>(fullPath, speciesName, animationType);
|
|
}
|
|
if (animationParamsType == typeof(FishWalkParams))
|
|
{
|
|
return Create<FishWalkParams>(fullPath, speciesName, animationType);
|
|
}
|
|
if (animationParamsType == typeof(FishRunParams))
|
|
{
|
|
return Create<FishRunParams>(fullPath, speciesName, animationType);
|
|
}
|
|
if (animationParamsType == typeof(FishSwimSlowParams))
|
|
{
|
|
return Create<FishSwimSlowParams>(fullPath, speciesName, animationType);
|
|
}
|
|
if (animationParamsType == typeof(FishSwimFastParams))
|
|
{
|
|
return Create<FishSwimFastParams>(fullPath, speciesName, animationType);
|
|
}
|
|
throw new NotImplementedException(animationParamsType.ToString());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Note: Overrides old animations, if found!
|
|
/// </summary>
|
|
public static T Create<T>(string fullPath, Identifier speciesName, AnimationType animationType) where T : AnimationParams, new()
|
|
{
|
|
if (animationType == AnimationType.NotDefined)
|
|
{
|
|
throw new Exception("Cannot create an animation file of type " + animationType);
|
|
}
|
|
var anims = allAnimations.GetOrAdd(speciesName, _ => new ConcurrentDictionary<string, AnimationParams>());
|
|
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
|
|
if (anims.ContainsKey(fileName))
|
|
{
|
|
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
|
|
anims.TryRemove(fileName, out _);
|
|
}
|
|
var instance = new T();
|
|
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
|
|
instance.doc = new XDocument(animationElement);
|
|
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
|
Debug.Assert(characterPrefab != null);
|
|
var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath);
|
|
instance.UpdatePath(contentPath);
|
|
instance.IsLoaded = instance.Deserialize(animationElement);
|
|
instance.Save();
|
|
instance.Load(contentPath, speciesName);
|
|
anims.TryAdd(fileName, instance);
|
|
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
|
|
return instance;
|
|
}
|
|
|
|
public bool Serialize() => base.Serialize();
|
|
public bool Deserialize() => base.Deserialize();
|
|
|
|
protected bool Load(ContentPath file, Identifier speciesName)
|
|
{
|
|
if (Load(file))
|
|
{
|
|
SpeciesName = speciesName;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected override void UpdatePath(ContentPath newPath)
|
|
{
|
|
if (SpeciesName == null)
|
|
{
|
|
base.UpdatePath(newPath);
|
|
}
|
|
else
|
|
{
|
|
// Update the key by removing and re-adding the animation.
|
|
string fileName = FileNameWithoutExtension;
|
|
if (allAnimations.TryGetValue(SpeciesName, out ConcurrentDictionary<string, AnimationParams> animations))
|
|
{
|
|
animations.TryRemove(fileName, out _);
|
|
}
|
|
base.UpdatePath(newPath);
|
|
if (animations != null)
|
|
{
|
|
animations.TryAdd(fileName, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected static string ParseFootAngles(Dictionary<int, float> footAngles)
|
|
{
|
|
//convert to the format "id1:angle,id2:angle,id3:angle"
|
|
return string.Join(",", footAngles.Select(kv => kv.Key + ": " + kv.Value.ToString("G", CultureInfo.InvariantCulture)).ToArray());
|
|
}
|
|
|
|
protected static void SetFootAngles(Dictionary<int, float> footAngles, string value)
|
|
{
|
|
footAngles.Clear();
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string[] keyValuePairs = value.Split(',');
|
|
foreach (string joinedKvp in keyValuePairs)
|
|
{
|
|
string[] keyValuePair = joinedKvp.Split(':');
|
|
if (keyValuePair.Length != 2 ||
|
|
!int.TryParse(keyValuePair[0].Trim(), out int limbIndex) ||
|
|
!float.TryParse(keyValuePair[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float angle))
|
|
{
|
|
DebugConsole.ThrowError("Failed to parse foot angles (" + value + ")");
|
|
continue;
|
|
}
|
|
footAngles[limbIndex] = angle;
|
|
}
|
|
}
|
|
|
|
public static Type GetParamTypeFromAnimType(AnimationType type, bool isHumanoid)
|
|
{
|
|
if (isHumanoid)
|
|
{
|
|
return type switch
|
|
{
|
|
AnimationType.Walk => typeof(HumanWalkParams),
|
|
AnimationType.Run => typeof(HumanRunParams),
|
|
AnimationType.Crouch => typeof(HumanCrouchParams),
|
|
AnimationType.SwimSlow => typeof(HumanSwimSlowParams),
|
|
AnimationType.SwimFast => typeof(HumanSwimFastParams),
|
|
_ => throw new NotImplementedException(type.ToString())
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return type switch
|
|
{
|
|
AnimationType.Walk => typeof(FishWalkParams),
|
|
AnimationType.Run => typeof(FishRunParams),
|
|
AnimationType.SwimSlow => typeof(FishSwimSlowParams),
|
|
AnimationType.SwimFast => typeof(FishSwimFastParams),
|
|
_ => throw new NotImplementedException(type.ToString())
|
|
};
|
|
}
|
|
}
|
|
|
|
#region Memento
|
|
public Memento<AnimationParams> Memento { get; protected set; } = new Memento<AnimationParams>();
|
|
public abstract void StoreSnapshot();
|
|
protected void StoreSnapshot<T>() where T : AnimationParams, new()
|
|
{
|
|
if (doc == null)
|
|
{
|
|
DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!", contentPackage: Path.ContentPackage);
|
|
return;
|
|
}
|
|
Serialize();
|
|
var copy = new T
|
|
{
|
|
IsLoaded = true,
|
|
doc = new XDocument(doc),
|
|
Path = Path
|
|
};
|
|
copy.Deserialize();
|
|
copy.Serialize();
|
|
Memento.Store(copy);
|
|
}
|
|
public void Undo() => Deserialize(Memento.Undo().MainElement);
|
|
public void Redo() => Deserialize(Memento.Redo().MainElement);
|
|
public void ClearHistory() => Memento.Clear();
|
|
#endregion
|
|
}
|
|
}
|