1 using Microsoft.Xna.Framework;
2 using System.Collections.Generic;
3 using System.Globalization;
6 using System.Diagnostics;
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)]
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)]
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)]
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)]
82 private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations =
new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
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)]
102 if (!
float.IsNaN(value))
119 if (!
float.IsNaN(value))
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)]
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)]
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)]
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)]
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)]
151 if (prefab?.ConfigElement ==
null)
153 DebugConsole.ThrowError($
"Failed to find config file for '{speciesName}'");
161 Debug.Assert(filePath !=
null);
162 Debug.Assert(root !=
null);
164 if (
string.IsNullOrEmpty(folder) || folder.Equals(
"default", StringComparison.OrdinalIgnoreCase))
166 folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath),
"Animations");
168 return folder.CleanUpPathCrossPlatform(correctFilenameCase:
true);
176 return filePaths.Where(f => AnimationPredicate(f, type)).OrderBy(f => f, StringComparer.OrdinalIgnoreCase);
178 static bool AnimationPredicate(
string filePath,
AnimationType type)
180 XDocument
doc = XMLExtensions.TryLoadXml(filePath);
181 if (
doc ==
null) {
return false; }
182 return doc.GetRootExcludingOverride().GetAttributeEnum(
"animationtype",
AnimationType.NotDefined) == type;
189 return GetAnimParams<T>(character, animType, file:
null, throwErrors:
true);
195 Identifier animSpecies = speciesName;
199 if (folder.IsNullOrEmpty() || folder.Equals(
"default", StringComparison.OrdinalIgnoreCase))
208 private static readonly List<string> errorMessages =
new List<string>();
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()
212 Debug.Assert(!speciesName.IsEmpty);
213 Debug.Assert(!animSpecies.IsEmpty);
215 string fileName =
null;
218 if (!file.TryGet(out fileName))
220 file.TryGet(out contentPath);
222 Debug.Assert(!fileName.IsNullOrWhiteSpace() || !contentPath.IsNullOrWhiteSpace());
225 Debug.Assert(contentPackage !=
null);
226 if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
228 animations =
new Dictionary<string, AnimationParams>();
229 allAnimations.Add(speciesName, animations);
232 if (animations.TryGetValue(key, out AnimationParams anim) && anim.AnimationType == animType)
237 if (!contentPath.IsNullOrEmpty())
240 T animInstance =
new T();
241 if (animInstance.Load(contentPath, speciesName))
243 if (animInstance.AnimationType == animType)
245 animations.TryAdd(contentPath.
Value, animInstance);
250 errorMessages.Add($
"[AnimationParams] Animation type mismatch. Expected: {animType}, Actual: {animInstance.AnimationType}. Using the default animation.");
255 errorMessages.Add($
"[AnimationParams] Failed to load an animation {animInstance} of type {animType} from {contentPath.Value} for the character {speciesName}. Using the default animation.");
259 string selectedFile =
null;
261 if (Directory.Exists(folder))
263 string[] files = Directory.GetFiles(folder);
266 errorMessages.Add($
"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation.");
271 if (filteredFiles.None())
273 errorMessages.Add($
"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation.");
275 else if (
string.IsNullOrEmpty(fileName))
280 selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First();
284 selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName));
285 if (selectedFile ==
null)
287 errorMessages.Add($
"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
294 errorMessages.Add($
"[AnimationParams] Invalid directory: {folder}. Using the default animation.");
297 Debug.Assert(selectedFile !=
null);
298 if (errorMessages.None())
300 DebugConsole.Log($
"[AnimationParams] Loading animations from {selectedFile}.");
302 T animationInstance =
new T();
303 if (animationInstance.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName))
305 animations.TryAdd(key, animationInstance);
309 errorMessages.Add($
"[AnimationParams] Failed to load an animation {animationInstance} at {selectedFile} of type {animType} for the character {speciesName}");
311 foreach (
string errorMsg
in errorMessages)
315 DebugConsole.ThrowError(errorMsg, contentPackage: contentPackage);
319 DebugConsole.Log(
"Logging a supressed (potential) error: " + errorMsg);
322 errorMessages.Clear();
323 return animationInstance;
325 static bool PathMatchesFile(
string p,
string f) => IO.Path.GetFileNameWithoutExtension(p).Equals(f, StringComparison.OrdinalIgnoreCase);
334 return Create<HumanWalkParams>(fullPath, speciesName, animationType);
338 return Create<HumanRunParams>(fullPath, speciesName, animationType);
342 return Create<HumanSwimSlowParams>(fullPath, speciesName, animationType);
346 return Create<HumanSwimFastParams>(fullPath, speciesName, animationType);
350 return Create<HumanCrouchParams>(fullPath, speciesName, animationType);
354 return Create<FishWalkParams>(fullPath, speciesName, animationType);
358 return Create<FishRunParams>(fullPath, speciesName, animationType);
362 return Create<FishSwimSlowParams>(fullPath, speciesName, animationType);
366 return Create<FishSwimFastParams>(fullPath, speciesName, animationType);
368 throw new NotImplementedException(animationParamsType.ToString());
378 throw new Exception(
"Cannot create an animation file of type " + animationType.ToString());
380 if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
382 anims =
new Dictionary<string, AnimationParams>();
383 allAnimations.Add(speciesName, anims);
385 string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
386 if (anims.ContainsKey(fileName))
388 DebugConsole.NewMessage($
"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
389 anims.Remove(fileName);
391 var instance =
new T();
392 XElement animationElement =
new XElement(
GetDefaultFileName(speciesName, animationType),
new XAttribute(
"animationtype", animationType.ToString()));
393 instance.doc =
new XDocument(animationElement);
395 Debug.Assert(characterPrefab !=
null);
397 instance.UpdatePath(contentPath);
398 instance.IsLoaded = instance.Deserialize(animationElement);
400 instance.Load(contentPath, speciesName);
401 anims.Add(fileName, instance);
402 DebugConsole.NewMessage($
"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
423 base.UpdatePath(newPath);
429 if (allAnimations.TryGetValue(
SpeciesName, out Dictionary<string, AnimationParams> animations))
431 animations.Remove(fileName);
433 base.UpdatePath(newPath);
434 if (animations !=
null)
436 if (!animations.ContainsKey(fileName))
438 animations.Add(fileName,
this);
447 return string.Join(
",", footAngles.Select(kv => kv.Key +
": " + kv.Value.ToString(
"G", CultureInfo.InvariantCulture)).ToArray());
450 protected static void SetFootAngles(Dictionary<int, float> footAngles,
string value)
453 if (
string.IsNullOrEmpty(value))
458 string[] keyValuePairs = value.Split(
',');
459 foreach (
string joinedKvp
in keyValuePairs)
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))
466 DebugConsole.ThrowError(
"Failed to parse foot angles (" + value +
")");
469 footAngles[limbIndex] = angle;
484 _ =>
throw new NotImplementedException(type.ToString())
495 _ =>
throw new NotImplementedException(type.ToString())
507 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
float BackwardsMovementMultiplier
float StepLiftHeadMultiplier
ContentPackage? ContentPackage