1 using Microsoft.Xna.Framework;
2 using System.Collections.Generic;
3 using System.Globalization;
6 using System.Diagnostics;
47 [
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)]
50 [
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 [
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)]
57 [
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)]
60 [
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)]
63 [
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)]
66 [
Serialize(5.2f,
IsPropertySaveable.Yes, description:
"Force applied to the hands when climbing ladders."),
Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
69 [
Serialize(10.0f,
IsPropertySaveable.Yes, description:
"Force applied to the feet when climbing ladders."),
Editable(MinValueFloat = 0.1f, MaxValueFloat = 100f, DecimalCount = 1)]
80 const string climbSpeedName = nameof(
ClimbSpeed);
81 if (element.GetAttribute(climbSpeedName) ==
null)
83 element.SetAttribute(climbSpeedName, 2.0f);
86 if (element.GetAttribute(climbStepName) ==
null)
88 element.SetAttribute(climbStepName, 60.0f);
90 const string slideSpeedName = nameof(
SlideSpeed);
91 if (element.GetAttribute(slideSpeedName) ==
null)
93 element.SetAttribute(slideSpeedName, 4.0f);
96 return base.Deserialize(element);
102 [
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)]
121 private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations =
new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
127 [
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)"),
128 Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2, ValueStep = 0.01f)]
141 if (!
float.IsNaN(value))
158 if (!
float.IsNaN(value))
167 [
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)]
170 [
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)]
174 [
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)]
178 [
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)]
181 [
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)]
190 if (prefab?.ConfigElement ==
null)
192 DebugConsole.ThrowError($
"Failed to find config file for '{speciesName}'");
200 Debug.Assert(filePath !=
null);
201 Debug.Assert(root !=
null);
203 if (
string.IsNullOrEmpty(folder) || folder.Equals(
"default", StringComparison.OrdinalIgnoreCase))
205 folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath),
"Animations");
207 return folder.CleanUpPathCrossPlatform(correctFilenameCase:
true);
215 return filePaths.Where(f => AnimationPredicate(f, type)).OrderBy(f => f, StringComparer.OrdinalIgnoreCase);
217 static bool AnimationPredicate(
string filePath,
AnimationType type)
219 XDocument
doc = XMLExtensions.TryLoadXml(filePath);
220 if (
doc ==
null) {
return false; }
221 return doc.GetRootExcludingOverride().GetAttributeEnum(
"animationtype",
AnimationType.NotDefined) == type;
228 return GetAnimParams<T>(character, animType, file:
null, throwErrors:
true);
234 Identifier animSpecies = speciesName;
238 if (folder.IsNullOrEmpty() || folder.Equals(
"default", StringComparison.OrdinalIgnoreCase))
247 private static readonly List<string> errorMessages =
new List<string>();
249 private static T
GetAnimParams<T>(Identifier speciesName, Identifier animSpecies, Identifier fallbackSpecies,
AnimationType animType, Either<string, ContentPath> file,
bool throwErrors =
true) where T :
AnimationParams, new()
251 Debug.Assert(!speciesName.IsEmpty);
252 Debug.Assert(!animSpecies.IsEmpty);
254 string fileName =
null;
257 if (!file.TryGet(out fileName))
259 file.TryGet(out contentPath);
261 Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
264 Debug.Assert(contentPackage !=
null);
265 if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
267 animations =
new Dictionary<string, AnimationParams>();
268 allAnimations.Add(speciesName, animations);
271 if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
276 if (!contentPath.IsNullOrEmpty())
279 T animInstance =
new T();
280 if (animInstance.Load(contentPath, speciesName))
282 if (animInstance.AnimationType == animType)
284 animations.TryAdd(contentPath.
Value, animInstance);
289 errorMessages.Add($
"[AnimationParams] Animation type mismatch. Expected: {animType}, Actual: {animInstance.AnimationType}. Using the default animation.");
294 errorMessages.Add($
"[AnimationParams] Failed to load an animation {animInstance} of type {animType} from {contentPath.Value} for the character {speciesName}. Using the default animation.");
298 string selectedFile =
null;
300 if (Directory.Exists(folder))
302 string[] files = Directory.GetFiles(folder);
305 errorMessages.Add($
"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation.");
310 if (filteredFiles.None())
312 errorMessages.Add($
"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation.");
314 else if (
string.IsNullOrEmpty(fileName))
319 selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First();
323 selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName));
324 if (selectedFile ==
null)
326 errorMessages.Add($
"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
333 errorMessages.Add($
"[AnimationParams] Invalid directory: {folder}. Using the default animation.");
336 Debug.Assert(selectedFile !=
null);
337 if (errorMessages.None())
339 DebugConsole.Log($
"[AnimationParams] Loading animations from {selectedFile}.");
341 T animationInstance =
new T();
342 if (animationInstance.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName))
344 animations.TryAdd(key, animationInstance);
348 errorMessages.Add($
"[AnimationParams] Failed to load an animation {animationInstance} at {selectedFile} of type {animType} for the character {speciesName}");
350 foreach (
string errorMsg
in errorMessages)
354 DebugConsole.ThrowError(errorMsg, contentPackage: contentPackage);
358 DebugConsole.Log(
"Logging a supressed (potential) error: " + errorMsg);
361 errorMessages.Clear();
362 return animationInstance;
364 static bool PathMatchesFile(
string p,
string f) => IO.Path.GetFileNameWithoutExtension(p).Equals(f, StringComparison.OrdinalIgnoreCase);
373 return Create<HumanWalkParams>(fullPath, speciesName, animationType);
377 return Create<HumanRunParams>(fullPath, speciesName, animationType);
381 return Create<HumanSwimSlowParams>(fullPath, speciesName, animationType);
385 return Create<HumanSwimFastParams>(fullPath, speciesName, animationType);
389 return Create<HumanCrouchParams>(fullPath, speciesName, animationType);
393 return Create<FishWalkParams>(fullPath, speciesName, animationType);
397 return Create<FishRunParams>(fullPath, speciesName, animationType);
401 return Create<FishSwimSlowParams>(fullPath, speciesName, animationType);
405 return Create<FishSwimFastParams>(fullPath, speciesName, animationType);
407 throw new NotImplementedException(animationParamsType.ToString());
417 throw new Exception(
"Cannot create an animation file of type " + animationType.ToString());
419 if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
421 anims =
new Dictionary<string, AnimationParams>();
422 allAnimations.Add(speciesName, anims);
424 string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
425 if (anims.ContainsKey(fileName))
427 DebugConsole.NewMessage($
"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
428 anims.Remove(fileName);
430 var instance =
new T();
431 XElement animationElement =
new XElement(
GetDefaultFileName(speciesName, animationType),
new XAttribute(
"animationtype", animationType.ToString()));
432 instance.doc =
new XDocument(animationElement);
434 Debug.Assert(characterPrefab !=
null);
436 instance.UpdatePath(contentPath);
437 instance.IsLoaded = instance.Deserialize(animationElement);
439 instance.Load(contentPath, speciesName);
440 anims.Add(fileName, instance);
441 DebugConsole.NewMessage($
"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
462 base.UpdatePath(newPath);
468 if (allAnimations.TryGetValue(
SpeciesName, out Dictionary<string, AnimationParams> animations))
470 animations.Remove(fileName);
472 base.UpdatePath(newPath);
473 if (animations !=
null)
475 if (!animations.ContainsKey(fileName))
477 animations.Add(fileName,
this);
486 return string.Join(
",", footAngles.Select(kv => kv.Key +
": " + kv.Value.ToString(
"G", CultureInfo.InvariantCulture)).ToArray());
489 protected static void SetFootAngles(Dictionary<int, float> footAngles,
string value)
492 if (
string.IsNullOrEmpty(value))
497 string[] keyValuePairs = value.Split(
',');
498 foreach (
string joinedKvp
in keyValuePairs)
500 string[] keyValuePair = joinedKvp.Split(
':');
501 if (keyValuePair.Length != 2 ||
502 !
int.TryParse(keyValuePair[0].Trim(), out
int limbIndex) ||
503 !
float.TryParse(keyValuePair[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out
float angle))
505 DebugConsole.ThrowError(
"Failed to parse foot angles (" + value +
")");
508 footAngles[limbIndex] = angle;
523 _ =>
throw new NotImplementedException(type.ToString())
534 _ =>
throw new NotImplementedException(type.ToString())
546 DebugConsole.ThrowError(
"[AnimationParams] The source XML Document is null!");
float TorsoAngleInRadians
void StoreSnapshot< T >()
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)
readonly CharacterParams Params
readonly CharacterPrefab Prefab
Identifier GetBaseCharacterSpeciesName(Identifier speciesName)
static CharacterPrefab FindBySpeciesName(Identifier speciesName)
ContentXElement ConfigElement
ContentPackage(XDocument doc, string path)
readonly? ContentPackage ContentPackage
static ContentPath FromRaw(string? rawValue)
ContentPath? GetAttributeContentPath(string key)
ContentXElement? GetChildElement(string name)
string FileNameWithoutExtension
override bool Deserialize(XElement element=null)
float BackwardsMovementMultiplier
float StepLiftHeadMultiplier
ContentPackage? ContentPackage