Client LuaCsForBarotrauma
AnimationParams.cs
1 using Microsoft.Xna.Framework;
2 using System.Collections.Generic;
3 using System.Globalization;
4 using Barotrauma.IO;
5 using System;
6 using System.Diagnostics;
7 using System.Linq;
8 using System.Xml.Linq;
10 
11 namespace Barotrauma
12 {
13  public enum AnimationType
14  {
15  NotDefined = 0,
16  Walk = 1,
17  Run = 2,
18  SwimSlow = 3,
19  SwimFast = 4,
20  Crouch = 5
21  }
22 
24  {
25  [Header("Legs")]
26  [Serialize("1.0, 1.0", IsPropertySaveable.Yes, description: "How big steps the character takes."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
27  public Vector2 StepSize
28  {
29  get;
30  set;
31  }
32 
33  [Header("Standing")]
34  [Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's head is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
35  public float HeadPosition { get; set; }
36 
37  [Serialize(0f, IsPropertySaveable.Yes, description: "How high above the ground the character's torso is positioned."), Editable(DecimalCount = 2, ValueStep = 0.1f)]
38  public float TorsoPosition { get; set; }
39 
40  [Header("Step lift")]
41  [Serialize(1f, IsPropertySaveable.Yes, description: "Separate multiplier for the head lift"), Editable(MinValueFloat = 0, MaxValueFloat = 2, ValueStep = 0.1f)]
42  public float StepLiftHeadMultiplier { get; set; }
43 
44  [Serialize(0f, IsPropertySaveable.Yes, description: "How much the body raises when taking a step."), Editable(MinValueFloat = 0, MaxValueFloat = 100, ValueStep = 0.1f)]
45  public float StepLiftAmount { get; set; }
46 
48  public bool MultiplyByDir { get; set; }
49 
50  [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)]
51  public float StepLiftOffset { get; set; }
52 
53  [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)]
54  public float StepLiftFrequency { get; set; }
55 
56  [Header("Movement")]
57  [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)]
58  public float BackwardsMovementMultiplier { get; set; }
59  }
60 
61  abstract class SwimParams : AnimationParams
62  {
63  [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)]
64  public float SteerTorque { get; set; }
65 
66  [Serialize(25.0f, IsPropertySaveable.Yes, description: "How much torque is used to move the legs."), Editable(MinValueFloat = 0, MaxValueFloat = 1000, ValueStep = 1)]
67  public float LegTorque { get; set; }
68  }
69 
70  abstract class AnimationParams : EditableParams, IMemorizable<AnimationParams>
71  {
72  public Identifier SpeciesName { get; private set; }
74  public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast;
75 
76  [Header("General")]
78  public virtual AnimationType AnimationType { get; protected set; }
82  private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
83 
84  [Header("Movement")]
85  [Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
86  public float MovementSpeed { get; set; }
87 
88  [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)"),
89  Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)]
90  public float CycleSpeed { get; set; }
91 
95  [Header("Standing")]
96  [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
97  public float HeadAngle
98  {
99  get => float.IsNaN(HeadAngleInRadians) ? float.NaN : MathHelper.ToDegrees(HeadAngleInRadians);
100  set
101  {
102  if (!float.IsNaN(value))
103  {
104  HeadAngleInRadians = MathHelper.ToRadians(value);
105  }
106  }
107  }
108  public float HeadAngleInRadians { get; private set; } = float.NaN;
109 
113  [Serialize(float.NaN, IsPropertySaveable.Yes), Editable(-360f, 360f)]
114  public float TorsoAngle
115  {
116  get => float.IsNaN(TorsoAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TorsoAngleInRadians);
117  set
118  {
119  if (!float.IsNaN(value))
120  {
121  TorsoAngleInRadians = MathHelper.ToRadians(value);
122  }
123  }
124  }
125 
126  public float TorsoAngleInRadians { get; private set; } = float.NaN;
127 
128  [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)]
129  public float HeadTorque { get; set; }
130 
131  [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)]
132  public float TorsoTorque { get; set; }
133 
134  [Header("Legs")]
135  [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)]
136  public float FootTorque { get; set; }
137 
138  [Header("Arms")]
139  [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)]
140  public float ArmIKStrength { get; set; }
141 
142  [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)]
143  public float HandIKStrength { get; set; }
144 
145  public static string GetDefaultFileName(Identifier speciesName, AnimationType animType) => $"{speciesName.Value.CapitaliseFirstInvariant()}{animType}";
146  public static string GetDefaultFile(Identifier speciesName, AnimationType animType) => Barotrauma.IO.Path.Combine(GetFolder(speciesName), $"{GetDefaultFileName(speciesName, animType)}.xml");
147 
148  public static string GetFolder(Identifier speciesName)
149  {
150  CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(speciesName);
151  if (prefab?.ConfigElement == null)
152  {
153  DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'");
154  return string.Empty;
155  }
156  return GetFolder(prefab.ConfigElement, prefab.FilePath.Value);
157  }
158 
159  private static string GetFolder(ContentXElement root, string filePath)
160  {
161  Debug.Assert(filePath != null);
162  Debug.Assert(root != null);
163  string folder = root.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value;
164  if (string.IsNullOrEmpty(folder) || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
165  {
166  folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Animations");
167  }
168  return folder.CleanUpPathCrossPlatform(correctFilenameCase: true);
169  }
170 
174  public static IEnumerable<string> FilterAndSortFiles(IEnumerable<string> filePaths, AnimationType type)
175  {
176  return filePaths.Where(f => AnimationPredicate(f, type)).OrderBy(f => f, StringComparer.OrdinalIgnoreCase);
177 
178  static bool AnimationPredicate(string filePath, AnimationType type)
179  {
180  XDocument doc = XMLExtensions.TryLoadXml(filePath);
181  if (doc == null) { return false; }
182  return doc.GetRootExcludingOverride().GetAttributeEnum("animationtype", AnimationType.NotDefined) == type;
183  }
184  }
185 
186  protected static T GetDefaultAnimParams<T>(Character character, AnimationType animType) where T : AnimationParams, new()
187  {
188  // Using a null file definition means we are taking a first matching file from the folder.
189  return GetAnimParams<T>(character, animType, file: null, throwErrors: true);
190  }
191 
192  protected static T GetAnimParams<T>(Character character, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
193  {
194  Identifier speciesName = character.SpeciesName;
195  Identifier animSpecies = speciesName;
196  if (!character.VariantOf.IsEmpty)
197  {
198  string folder = character.Params.VariantFile?.GetRootExcludingOverride().GetChildElement("animations")?.GetAttributeContentPath("folder", character.Prefab.ContentPackage)?.Value;
199  if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
200  {
201  // Use the animations defined in the base definition file.
202  animSpecies = character.Prefab.GetBaseCharacterSpeciesName(speciesName);
203  }
204  }
205  return GetAnimParams<T>(speciesName, animSpecies, fallbackSpecies: character.Prefab.GetBaseCharacterSpeciesName(speciesName), animType, file, throwErrors);
206  }
207 
208  private static readonly List<string> errorMessages = new List<string>();
209 
210  private static T GetAnimParams<T>(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies, AnimationType animType, Either<string, ContentPath> file, bool throwErrors = true) where T : AnimationParams, new()
211  {
212  Debug.Assert(!speciesName.IsEmpty);
213  Debug.Assert(!animSpecies.IsEmpty);
214  ContentPath contentPath = null;
215  string fileName = null;
216  if (file != null)
217  {
218  if (!file.TryGet(out fileName))
219  {
220  file.TryGet(out contentPath);
221  }
222  Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
223  }
224  ContentPackage contentPackage = contentPath?.ContentPackage ?? CharacterPrefab.FindBySpeciesName(speciesName)?.ContentPackage;
225  Debug.Assert(contentPackage != null);
226  if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
227  {
228  animations = new Dictionary<string, AnimationParams>();
229  allAnimations.Add(speciesName, animations);
230  }
231  string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(animSpecies, animType);
232  if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
233  {
234  // Already cached.
235  return (T)anim;
236  }
237  if (!contentPath.IsNullOrEmpty())
238  {
239  // Load the animation from path.
240  T animInstance = new T();
241  if (animInstance.Load(contentPath, speciesName))
242  {
243  if (animInstance.AnimationType == animType)
244  {
245  animations.TryAdd(contentPath.Value, animInstance);
246  return animInstance;
247  }
248  else
249  {
250  errorMessages.Add($"[AnimationParams] Animation type mismatch. Expected: {animType}, Actual: {animInstance.AnimationType}. Using the default animation.");
251  }
252  }
253  else
254  {
255  errorMessages.Add($"[AnimationParams] Failed to load an animation {animInstance} of type {animType} from {contentPath.Value} for the character {speciesName}. Using the default animation.");
256  }
257  }
258  // Seek the correct animation from the character's animation folder.
259  string selectedFile = null;
260  string folder = GetFolder(animSpecies);
261  if (Directory.Exists(folder))
262  {
263  string[] files = Directory.GetFiles(folder);
264  if (files.None())
265  {
266  errorMessages.Add($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation.");
267  }
268  else
269  {
270  var filteredFiles = FilterAndSortFiles(files, animType);
271  if (filteredFiles.None())
272  {
273  errorMessages.Add($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation.");
274  }
275  else if (string.IsNullOrEmpty(fileName))
276  {
277  // Files found, but none specified -> Get a matching animation from the specified folder.
278  // First try to find a file that matches the default file name. If that fails, just take any file.
279  string defaultFileName = GetDefaultFileName(animSpecies, animType);
280  selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First();
281  }
282  else
283  {
284  selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName));
285  if (selectedFile == null)
286  {
287  errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
288  }
289  }
290  }
291  }
292  else
293  {
294  errorMessages.Add($"[AnimationParams] Invalid directory: {folder}. Using the default animation.");
295  }
296  selectedFile ??= GetDefaultFile(fallbackSpecies, animType);
297  Debug.Assert(selectedFile != null);
298  if (errorMessages.None())
299  {
300  DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}.");
301  }
302  T animationInstance = new T();
303  if (animationInstance.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName))
304  {
305  animations.TryAdd(key, animationInstance);
306  }
307  else
308  {
309  errorMessages.Add($"[AnimationParams] Failed to load an animation {animationInstance} at {selectedFile} of type {animType} for the character {speciesName}");
310  }
311  foreach (string errorMsg in errorMessages)
312  {
313  if (throwErrors)
314  {
315  DebugConsole.ThrowError(errorMsg, contentPackage: contentPackage);
316  }
317  else
318  {
319  DebugConsole.Log("Logging a supressed (potential) error: " + errorMsg);
320  }
321  }
322  errorMessages.Clear();
323  return animationInstance;
324 
325  static bool PathMatchesFile(string p, string f) => IO.Path.GetFileNameWithoutExtension(p).Equals(f, StringComparison.OrdinalIgnoreCase);
326  }
327 
328  public static void ClearCache() => allAnimations.Clear();
329 
330  public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type animationParamsType)
331  {
332  if (animationParamsType == typeof(HumanWalkParams))
333  {
334  return Create<HumanWalkParams>(fullPath, speciesName, animationType);
335  }
336  if (animationParamsType == typeof(HumanRunParams))
337  {
338  return Create<HumanRunParams>(fullPath, speciesName, animationType);
339  }
340  if (animationParamsType == typeof(HumanSwimSlowParams))
341  {
342  return Create<HumanSwimSlowParams>(fullPath, speciesName, animationType);
343  }
344  if (animationParamsType == typeof(HumanSwimFastParams))
345  {
346  return Create<HumanSwimFastParams>(fullPath, speciesName, animationType);
347  }
348  if (animationParamsType == typeof(HumanCrouchParams))
349  {
350  return Create<HumanCrouchParams>(fullPath, speciesName, animationType);
351  }
352  if (animationParamsType == typeof(FishWalkParams))
353  {
354  return Create<FishWalkParams>(fullPath, speciesName, animationType);
355  }
356  if (animationParamsType == typeof(FishRunParams))
357  {
358  return Create<FishRunParams>(fullPath, speciesName, animationType);
359  }
360  if (animationParamsType == typeof(FishSwimSlowParams))
361  {
362  return Create<FishSwimSlowParams>(fullPath, speciesName, animationType);
363  }
364  if (animationParamsType == typeof(FishSwimFastParams))
365  {
366  return Create<FishSwimFastParams>(fullPath, speciesName, animationType);
367  }
368  throw new NotImplementedException(animationParamsType.ToString());
369  }
370 
374  public static T Create<T>(string fullPath, Identifier speciesName, AnimationType animationType) where T : AnimationParams, new()
375  {
376  if (animationType == AnimationType.NotDefined)
377  {
378  throw new Exception("Cannot create an animation file of type " + animationType.ToString());
379  }
380  if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
381  {
382  anims = new Dictionary<string, AnimationParams>();
383  allAnimations.Add(speciesName, anims);
384  }
385  string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
386  if (anims.ContainsKey(fileName))
387  {
388  DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
389  anims.Remove(fileName);
390  }
391  var instance = new T();
392  XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
393  instance.doc = new XDocument(animationElement);
394  var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
395  Debug.Assert(characterPrefab != null);
396  var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath);
397  instance.UpdatePath(contentPath);
398  instance.IsLoaded = instance.Deserialize(animationElement);
399  instance.Save();
400  instance.Load(contentPath, speciesName);
401  anims.Add(fileName, instance);
402  DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
403  return instance;
404  }
405 
406  public bool Serialize() => base.Serialize();
407  public bool Deserialize() => base.Deserialize();
408 
409  protected bool Load(ContentPath file, Identifier speciesName)
410  {
411  if (Load(file))
412  {
413  SpeciesName = speciesName;
414  return true;
415  }
416  return false;
417  }
418 
419  protected override void UpdatePath(ContentPath newPath)
420  {
421  if (SpeciesName == null)
422  {
423  base.UpdatePath(newPath);
424  }
425  else
426  {
427  // Update the key by removing and re-adding the animation.
428  string fileName = FileNameWithoutExtension;
429  if (allAnimations.TryGetValue(SpeciesName, out Dictionary<string, AnimationParams> animations))
430  {
431  animations.Remove(fileName);
432  }
433  base.UpdatePath(newPath);
434  if (animations != null)
435  {
436  if (!animations.ContainsKey(fileName))
437  {
438  animations.Add(fileName, this);
439  }
440  }
441  }
442  }
443 
444  protected static string ParseFootAngles(Dictionary<int, float> footAngles)
445  {
446  //convert to the format "id1:angle,id2:angle,id3:angle"
447  return string.Join(",", footAngles.Select(kv => kv.Key + ": " + kv.Value.ToString("G", CultureInfo.InvariantCulture)).ToArray());
448  }
449 
450  protected static void SetFootAngles(Dictionary<int, float> footAngles, string value)
451  {
452  footAngles.Clear();
453  if (string.IsNullOrEmpty(value))
454  {
455  return;
456  }
457 
458  string[] keyValuePairs = value.Split(',');
459  foreach (string joinedKvp in keyValuePairs)
460  {
461  string[] keyValuePair = joinedKvp.Split(':');
462  if (keyValuePair.Length != 2 ||
463  !int.TryParse(keyValuePair[0].Trim(), out int limbIndex) ||
464  !float.TryParse(keyValuePair[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float angle))
465  {
466  DebugConsole.ThrowError("Failed to parse foot angles (" + value + ")");
467  continue;
468  }
469  footAngles[limbIndex] = angle;
470  }
471  }
472 
473  public static Type GetParamTypeFromAnimType(AnimationType type, bool isHumanoid)
474  {
475  if (isHumanoid)
476  {
477  return type switch
478  {
479  AnimationType.Walk => typeof(HumanWalkParams),
480  AnimationType.Run => typeof(HumanRunParams),
481  AnimationType.Crouch => typeof(HumanCrouchParams),
482  AnimationType.SwimSlow => typeof(HumanSwimSlowParams),
483  AnimationType.SwimFast => typeof(HumanSwimFastParams),
484  _ => throw new NotImplementedException(type.ToString())
485  };
486  }
487  else
488  {
489  return type switch
490  {
491  AnimationType.Walk => typeof(FishWalkParams),
492  AnimationType.Run => typeof(FishRunParams),
493  AnimationType.SwimSlow => typeof(FishSwimSlowParams),
494  AnimationType.SwimFast => typeof(FishSwimFastParams),
495  _ => throw new NotImplementedException(type.ToString())
496  };
497  }
498  }
499 
500  #region Memento
501  public Memento<AnimationParams> Memento { get; protected set; } = new Memento<AnimationParams>();
502  public abstract void StoreSnapshot();
503  protected void StoreSnapshot<T>() where T : AnimationParams, new()
504  {
505  if (doc == null)
506  {
507  DebugConsole.ThrowError("[AnimationParams] The source XML Document is null!");
508  return;
509  }
510  Serialize();
511  var copy = new T
512  {
513  IsLoaded = true,
514  doc = new XDocument(doc),
515  Path = Path
516  };
517  copy.Deserialize();
518  copy.Serialize();
519  Memento.Store(copy);
520  }
521  public void Undo() => Deserialize(Memento.Undo().MainElement);
522  public void Redo() => Deserialize(Memento.Redo().MainElement);
523  public void ClearHistory() => Memento.Clear();
524  #endregion
525  }
526 }
static string ParseFootAngles(Dictionary< int, float > footAngles)
static Type GetParamTypeFromAnimType(AnimationType type, bool isHumanoid)
static IEnumerable< string > FilterAndSortFiles(IEnumerable< string > filePaths, AnimationType type)
Selects all file paths that match the specified animation type and filters them alphabetically.
static T GetAnimParams< T >(Character character, AnimationType animType, Either< string, ContentPath > file, bool throwErrors=true)
static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type animationParamsType)
static string GetDefaultFile(Identifier speciesName, AnimationType animType)
abstract void StoreSnapshot()
static T GetDefaultAnimParams< T >(Character character, AnimationType animType)
float? HeadAngle
In degrees.
static string GetDefaultFileName(Identifier speciesName, AnimationType animType)
static T Create< T >(string fullPath, Identifier speciesName, AnimationType animationType)
Note: Overrides old animations, if found!
float? TorsoAngle
In degrees.
bool Load(ContentPath file, Identifier speciesName)
static void SetFootAngles(Dictionary< int, float > footAngles, string value)
override void UpdatePath(ContentPath newPath)
static string GetFolder(Identifier speciesName)
Identifier GetBaseCharacterSpeciesName(Identifier speciesName)
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
ContentXElement ConfigElement
ContentPackage(XDocument doc, string path)
readonly? ContentPackage ContentPackage
Definition: ContentPath.cs:21
static ContentPath FromRaw(string? rawValue)
string???????????? Value
Definition: ContentPath.cs:27
ContentPath? GetAttributeContentPath(string key)
ContentXElement? GetChildElement(string name)
void Store(T newState)
Definition: Memento.cs:25
ContentPackage? ContentPackage
Definition: Prefab.cs:37
ContentPath FilePath
Definition: Prefab.cs:38