v1.4.4.1 (Blood in the Water Update)

This commit is contained in:
Regalis11
2024-04-24 18:09:05 +03:00
parent 89b91d1c3e
commit ff1b8951a7
397 changed files with 15250 additions and 6479 deletions
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using Barotrauma.IO;
using System;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
@@ -65,22 +66,19 @@ namespace Barotrauma
abstract class AnimationParams : EditableParams, IMemorizable<AnimationParams>
{
public Identifier SpeciesName { get; private set; }
public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run || AnimationType == AnimationType.Crouch;
public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast;
public bool IsGroundedAnimation => AnimationType is AnimationType.Walk or AnimationType.Run or AnimationType.Crouch;
public bool IsSwimAnimation => AnimationType is AnimationType.SwimSlow or AnimationType.SwimFast;
protected static Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
/// allAnimations[speciesName][fileName]
/// <summary>
/// The cached animations of all the characters that have been loaded.
/// </summary>
private static readonly Dictionary<Identifier, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<Identifier, Dictionary<string, AnimationParams>>();
private float _movementSpeed;
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(DecimalCount = 2, MinValueFloat = 0, MaxValueFloat = Ragdoll.MAX_SPEED, ValueStep = 0.1f)]
public float MovementSpeed
{
get => _movementSpeed;
set => _movementSpeed = value;
}
[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 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>
@@ -152,169 +150,214 @@ namespace Barotrauma
private static string GetFolder(ContentXElement root, string filePath)
{
var folder = root?.GetChildElement("animations")?.GetAttributeContentPath("folder")?.Value;
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(true);
return folder.CleanUpPathCrossPlatform(correctFilenameCase: true);
}
/// <summary>
/// Selects a random filepath from multiple paths, matching the specified animation type.
/// Selects all file paths that match the specified animation type and filters them alphabetically.
/// </summary>
public static string GetRandomFilePath(IReadOnlyList<string> filePaths, AnimationType type)
public static IEnumerable<string> FilterAndSortFiles(IEnumerable<string> filePaths, AnimationType type)
{
return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.ServerAndClient);
}
/// <summary>
/// Selects all file paths that match the specified animation type.
/// </summary>
public static IEnumerable<string> FilterFilesByType(IEnumerable<string> filePaths, AnimationType type)
{
return filePaths.Where(f => AnimationPredicate(f, type));
}
private static bool AnimationPredicate(string filePath, AnimationType type)
{
var doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { return false; }
var typeString = doc.Root.GetAttributeString("animationtype", null);
if (string.IsNullOrWhiteSpace(typeString))
return filePaths.Where(f => AnimationPredicate(f, type)).OrderBy(f => f, StringComparer.OrdinalIgnoreCase);
static bool AnimationPredicate(string filePath, AnimationType type)
{
typeString = doc.Root.GetAttributeString("AnimationType", "NotDefined");
XDocument doc = XMLExtensions.TryLoadXml(filePath);
if (doc == null) { return false; }
return doc.GetRootExcludingOverride().GetAttributeEnum("animationtype", AnimationType.NotDefined) == type;
}
return Enum.TryParse(typeString, out AnimationType fileType) && fileType == type;
}
public static T GetDefaultAnimParams<T>(Character character, AnimationType animType) where T : AnimationParams, new()
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;
if (!character.VariantOf.IsEmpty
&& (character.Params.VariantFile?.Root?.GetChildElement("animations")?.GetAttributeStringUnrestricted("folder", null)).IsNullOrEmpty())
Identifier animSpecies = speciesName;
if (!character.VariantOf.IsEmpty)
{
// Use the base animations defined in the base definition file.
speciesName = character.VariantOf;
}
return GetAnimParams<T>(speciesName, animType, GetDefaultFileName(speciesName, animType));
}
/// <summary>
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
/// If a custom folder is used, it's defined in the character info file.
/// </summary>
public static T GetAnimParams<T>(Identifier speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new()
{
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
{
anims = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, anims);
}
if (fileName == null || !anims.TryGetValue(fileName, out AnimationParams anim))
{
string selectedFile = null;
string folder = GetFolder(speciesName);
if (Directory.Exists(folder))
string folder = character.Params.VariantFile?.GetRootExcludingOverride().GetChildElement("animations")?.GetAttributeContentPath("folder", character.Prefab.ContentPackage)?.Value;
if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
var files = Directory.GetFiles(folder);
if (files.None())
// 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);
}
private static readonly List<string> errorMessages = new List<string>();
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);
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> animations))
{
animations = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, animations);
}
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)
{
DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation.");
selectedFile = GetDefaultFile(speciesName, animType);
}
var filteredFiles = FilterFilesByType(files, animType);
if (filteredFiles.None())
{
DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation.");
selectedFile = GetDefaultFile(speciesName, animType);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified.
selectedFile = GetDefaultFile(speciesName, animType);
animations.TryAdd(contentPath.Value, animInstance);
return animInstance;
}
else
{
selectedFile = filteredFiles.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
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.
string defaultFileName = GetDefaultFileName(animSpecies, animType);
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, defaultFileName)) ?? filteredFiles.First();
}
else
{
selectedFile = filteredFiles.FirstOrDefault(path => PathMatchesFile(path, fileName));
if (selectedFile == null)
{
DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
selectedFile = GetDefaultFile(speciesName, animType);
errorMessages.Add($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
}
}
}
}
else
{
DebugConsole.ThrowError($"[Animationparams] Invalid directory: {folder}. Using the default animation.");
selectedFile = GetDefaultFile(speciesName, animType);
}
if (selectedFile == null)
{
throw new Exception("[AnimationParams] Selected file null!");
}
DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}.");
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
T a = new T();
if (a.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), speciesName))
{
fileName = IO.Path.GetFileNameWithoutExtension(selectedFile);
if (!anims.ContainsKey(fileName))
{
anims.Add(fileName, a);
}
}
else
{
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}",
contentPackage: characterPrefab.ContentPackage);
}
return a;
}
return (T)anim;
else
{
errorMessages.Add($"[AnimationParams] Invalid directory: {folder}. Using the default animation.");
}
selectedFile ??= GetDefaultFile(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 type)
public static AnimationParams Create(string fullPath, Identifier speciesName, AnimationType animationType, Type animationParamsType)
{
if (type == typeof(HumanWalkParams))
if (animationParamsType == typeof(HumanWalkParams))
{
return Create<HumanWalkParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanRunParams))
if (animationParamsType == typeof(HumanRunParams))
{
return Create<HumanRunParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanSwimSlowParams))
if (animationParamsType == typeof(HumanSwimSlowParams))
{
return Create<HumanSwimSlowParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanSwimFastParams))
if (animationParamsType == typeof(HumanSwimFastParams))
{
return Create<HumanSwimFastParams>(fullPath, speciesName, animationType);
}
if (type == typeof(HumanCrouchParams))
if (animationParamsType == typeof(HumanCrouchParams))
{
return Create<HumanCrouchParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishWalkParams))
if (animationParamsType == typeof(FishWalkParams))
{
return Create<FishWalkParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishRunParams))
if (animationParamsType == typeof(FishRunParams))
{
return Create<FishRunParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishSwimSlowParams))
if (animationParamsType == typeof(FishSwimSlowParams))
{
return Create<FishSwimSlowParams>(fullPath, speciesName, animationType);
}
if (type == typeof(FishSwimFastParams))
if (animationParamsType == typeof(FishSwimFastParams))
{
return Create<FishSwimFastParams>(fullPath, speciesName, animationType);
}
throw new NotImplementedException(type.ToString());
throw new NotImplementedException(animationParamsType.ToString());
}
/// <summary>
@@ -331,7 +374,7 @@ namespace Barotrauma
anims = new Dictionary<string, AnimationParams>();
allAnimations.Add(speciesName, anims);
}
var fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
string fileName = IO.Path.GetFileNameWithoutExtension(fullPath);
if (anims.ContainsKey(fileName))
{
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
@@ -340,7 +383,8 @@ namespace Barotrauma
var instance = new T();
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
instance.doc = new XDocument(animationElement);
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
var characterPrefab = CharacterPrefab.FindBySpeciesName(speciesName);
Debug.Assert(characterPrefab != null);
var contentPath = ContentPath.FromRaw(characterPrefab.ContentPackage, fullPath);
instance.UpdatePath(contentPath);
instance.IsLoaded = instance.Deserialize(animationElement);
@@ -373,16 +417,17 @@ namespace Barotrauma
else
{
// Update the key by removing and re-adding the animation.
string fileName = FileNameWithoutExtension;
if (allAnimations.TryGetValue(SpeciesName, out Dictionary<string, AnimationParams> animations))
{
animations.Remove(Name);
animations.Remove(fileName);
}
base.UpdatePath(newPath);
if (animations != null)
{
if (!animations.ContainsKey(Name))
if (!animations.ContainsKey(fileName))
{
animations.Add(Name, this);
animations.Add(fileName, this);
}
}
}
@@ -421,37 +466,26 @@ namespace Barotrauma
{
if (isHumanoid)
{
switch (type)
return type switch
{
case AnimationType.Walk:
return typeof(HumanWalkParams);
case AnimationType.Run:
return typeof(HumanRunParams);
case AnimationType.Crouch:
return typeof(HumanCrouchParams);
case AnimationType.SwimSlow:
return typeof(HumanSwimSlowParams);
case AnimationType.SwimFast:
return typeof(HumanSwimFastParams);
default:
throw new NotImplementedException(type.ToString());
}
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
{
switch (type)
return type switch
{
case AnimationType.Walk:
return typeof(FishWalkParams);
case AnimationType.Run:
return typeof(FishRunParams);
case AnimationType.SwimSlow:
return typeof(FishSwimSlowParams);
case AnimationType.SwimFast:
return typeof(FishSwimFastParams);
default:
throw new NotImplementedException(type.ToString());
}
AnimationType.Walk => typeof(FishWalkParams),
AnimationType.Run => typeof(FishRunParams),
AnimationType.SwimSlow => typeof(FishSwimSlowParams),
AnimationType.SwimFast => typeof(FishSwimFastParams),
_ => throw new NotImplementedException(type.ToString())
};
}
}
@@ -9,12 +9,12 @@ namespace Barotrauma
{
return Check(character) ? GetDefaultAnimParams<FishWalkParams>(character, AnimationType.Walk) : Empty;
}
public static FishWalkParams GetAnimParams(Character character, string fileName = null)
public static FishWalkParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return Check(character) ? GetAnimParams<FishWalkParams>(character.SpeciesName, AnimationType.Walk, fileName) : Empty;
return Check(character) ? GetAnimParams<FishWalkParams>(character, AnimationType.Walk, file, throwErrors) : null;
}
protected static FishWalkParams Empty = new FishWalkParams();
protected static readonly FishWalkParams Empty = new FishWalkParams();
public override void StoreSnapshot() => StoreSnapshot<FishWalkParams>();
}
@@ -25,12 +25,12 @@ namespace Barotrauma
{
return Check(character) ? GetDefaultAnimParams<FishRunParams>(character, AnimationType.Run) : Empty;
}
public static FishRunParams GetAnimParams(Character character, string fileName = null)
public static FishRunParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return Check(character) ? GetAnimParams<FishRunParams>(character.SpeciesName, AnimationType.Run, fileName) : Empty;
return Check(character) ? GetAnimParams<FishRunParams>(character, AnimationType.Run, file, throwErrors) : null;
}
protected static FishRunParams Empty = new FishRunParams();
protected static readonly FishRunParams Empty = new FishRunParams();
public override void StoreSnapshot() => StoreSnapshot<FishRunParams>();
}
@@ -38,9 +38,9 @@ namespace Barotrauma
class FishSwimFastParams : FishSwimParams
{
public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimFastParams>(character, AnimationType.SwimFast);
public static FishSwimFastParams GetAnimParams(Character character, string fileName = null)
public static FishSwimFastParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
return GetAnimParams<FishSwimFastParams>(character, AnimationType.SwimFast, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimFastParams>();
@@ -49,9 +49,9 @@ namespace Barotrauma
class FishSwimSlowParams : FishSwimParams
{
public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimSlowParams>(character, AnimationType.SwimSlow);
public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null)
public static FishSwimSlowParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
return GetAnimParams<FishSwimSlowParams>(character, AnimationType.SwimSlow, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<FishSwimSlowParams>();
@@ -5,9 +5,9 @@ namespace Barotrauma
class HumanWalkParams : HumanGroundedParams
{
public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanWalkParams>(character, AnimationType.Walk);
public static HumanWalkParams GetAnimParams(Character character, string fileName = null)
public static HumanWalkParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanWalkParams>(character.SpeciesName, AnimationType.Walk, fileName);
return GetAnimParams<HumanWalkParams>(character, AnimationType.Walk, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<HumanWalkParams>();
@@ -16,9 +16,9 @@ namespace Barotrauma
class HumanRunParams : HumanGroundedParams
{
public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanRunParams>(character, AnimationType.Run);
public static HumanRunParams GetAnimParams(Character character, string fileName = null)
public static HumanRunParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanRunParams>(character.SpeciesName, AnimationType.Run, fileName);
return GetAnimParams<HumanRunParams>(character, AnimationType.Run, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<HumanRunParams>();
@@ -36,9 +36,9 @@ namespace Barotrauma
public float ExtraTorsoAngleWhenStationary { get; set; }
public static HumanCrouchParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanCrouchParams>(character, AnimationType.Crouch);
public static HumanCrouchParams GetAnimParams(Character character, string fileName = null)
public static HumanCrouchParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanCrouchParams>(character.SpeciesName, AnimationType.Crouch, fileName);
return GetAnimParams<HumanCrouchParams>(character, AnimationType.Crouch, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<HumanCrouchParams>();
@@ -47,9 +47,9 @@ namespace Barotrauma
class HumanSwimFastParams: HumanSwimParams
{
public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimFastParams>(character, AnimationType.SwimFast);
public static HumanSwimFastParams GetAnimParams(Character character, string fileName = null)
public static HumanSwimFastParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
return GetAnimParams<HumanSwimFastParams>(character, AnimationType.SwimFast, file, throwErrors);
}
@@ -59,9 +59,9 @@ namespace Barotrauma
class HumanSwimSlowParams : HumanSwimParams
{
public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimSlowParams>(character, AnimationType.SwimSlow);
public static HumanSwimSlowParams GetAnimParams(Character character, string fileName = null)
public static HumanSwimSlowParams GetAnimParams(Character character, Either<string, ContentPath> file, bool throwErrors = true)
{
return GetAnimParams<HumanSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
return GetAnimParams<HumanSwimSlowParams>(character, AnimationType.SwimSlow, file, throwErrors);
}
public override void StoreSnapshot() => StoreSnapshot<HumanSwimSlowParams>();
@@ -125,6 +125,13 @@ namespace Barotrauma
[Serialize(0f, IsPropertySaveable.Yes, description: "How much the horizontal difference of waist and the foot positions has an effect to lifting the foot."), Editable(DecimalCount = 2, ValueStep = 0.1f, MinValueFloat = 0f, MaxValueFloat = 1f)]
public float FootLiftHorizontalFactor { get; set; }
[Serialize("0,0", IsPropertySaveable.Yes, description: "Normally the character's feet are positioned at a scaled-down version of it's normal step position - this can be used to override that value if you want to e.g. make the character to spread out it's feet more when standing."), Editable(DecimalCount = 2, ValueStep = 0.01f)]
public Vector2 StepSizeWhenStanding
{
get;
set;
}
/// <summary>
/// In degrees.
/// </summary>
@@ -20,31 +20,31 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes), Editable]
public Identifier SpeciesName { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the creature is a variant that needs to use a pre-existing translation."), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "References to another species. Define only if the creature is a variant that needs to use a pre-existing translation."), Editable]
public Identifier SpeciesTranslationOverride { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "Overrides the name of the character, shown to the player. If the display name is not defined, the game first tries to find the translated name. If that is not found, the species name will be used."), Editable]
public string DisplayName { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group are considered like the characters of the same species by the AI."), Editable]
[Serialize("", IsPropertySaveable.Yes, description: "If defined, different species of the same group consider each other friendly and do not attack each other."), Editable]
public Identifier Group { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)]
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the character is a humanoid and has different animation constraints relative to non-humanoid characters."), Editable(ReadOnly = true)]
public bool Humanoid { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable(ReadOnly = true)]
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, jobs can be assigned to characters of this species. Should be true for the player characters."), Editable(ReadOnly = true)]
public bool HasInfo { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature interact with items?"), Editable]
public bool CanInteract { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should this character be treated as a husk?"), Editable]
public bool Husk { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description:"Should this character use a special husk appendage, attached to the ragdoll, when it turns into a husk?"), Editable]
public bool UseHuskAppendage { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Does this character need oxygen to survive? Enabling this also makes the character vulnerable to high pressure when swimming outside of the submarine."), Editable]
public bool NeedsAir { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Can the creature live without water or does it die on dry land?"), Editable]
@@ -56,13 +56,13 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Is this creature an artificial creature, like robot or machine that shouldn't be affected by afflictions that affect only organic creatures? Overrides DoesBleed."), Editable]
public bool IsMachine { get; set; }
[Serialize(false, IsPropertySaveable.No), Editable]
[Serialize(false, IsPropertySaveable.No, description:"Is the character able to send messages in the chat?"), Editable]
public bool CanSpeak { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
[Serialize(true, IsPropertySaveable.Yes, description:"Is there a health bar shown above the character when it takes damage? Defaults to true."), Editable]
public bool ShowHealthBar { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Is this character's health shown at the top of the player's screen when they are in an active encounter?"), Editable]
public bool UseBossHealthBar { get; private set; }
[Serialize(100f, IsPropertySaveable.Yes, description: "How much noise the character makes when moving?"), Editable(minValue: 0f, maxValue: 100000f)]
@@ -80,7 +80,7 @@ namespace Barotrauma
[Serialize("waterblood", IsPropertySaveable.Yes), Editable]
public string BleedParticleWater { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable]
[Serialize(1f, IsPropertySaveable.Yes, description: "A multiplier to increase or decrease the number of bleeding particles to create."), Editable]
public float BleedParticleMultiplier { get; private set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Can the creature eat bodies? Used by player controlled creatures to allow them to eat. Currently applicable only to non-humanoids. To allow an AI controller to eat, just add an ai target with the state \"eat\""), Editable]
@@ -89,22 +89,22 @@ namespace Barotrauma
[Serialize(10f, IsPropertySaveable.Yes, description: "How effectively/easily the character eats other characters. Affects the forces, the amount of particles, and the time required before the target is eaten away"), Editable(MinValueFloat = 1, MaxValueFloat = 1000, ValueStep = 1)]
public float EatingSpeed { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
[Serialize(true, IsPropertySaveable.Yes, description: "Should the character AI use waypoints defined in the level to find a path to its targets?"), Editable]
public bool UsePathFinding { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, "Decreases the intensive path finding call frequency. Set to a lower value for insignificant creatures to improve performance."), Editable(minValue: 0f, maxValue: 1f)]
public float PathFinderPriority { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the character be hidden in the sonar?"), Editable]
public bool HideInSonar { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the character be hidden when using thermal goggles?"), Editable]
public bool HideInThermalGoggles { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "If set to a value greater than zero, this character creates disrupting noise on the sonar when within range."), Editable]
public float SonarDisruption { get; set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "Range at which \"long distance\" blips for this character will appear on the sonar (used on some of the Abyss monsters)."), Editable]
public float DistantSonarRange { get; set; }
[Serialize(25000f, IsPropertySaveable.Yes, "If the character is farther than this (in pixels) from the sub and the players, it will be disabled. The halved value is used for triggering simple physics where the ragdoll is disabled and only the main collider is updated."), Editable(MinValueFloat = 10000f, MaxValueFloat = 100000f)]
@@ -113,7 +113,7 @@ namespace Barotrauma
[Serialize(10f, IsPropertySaveable.Yes, "How frequent the recurring idle and attack sounds are?"), Editable(MinValueFloat = 1f, MaxValueFloat = 100f)]
public float SoundInterval { get; set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description: "Should the character be drawn on top of characters that do not have this set? This currently has no effect if the character has no deformable sprites."), Editable]
public bool DrawLast { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, "Tells the bots how much they should prefer targeting this character with submarine weapons. Defaults to 1. Set 0 to tell the bots not to target this character at all. Distance to the target affects the decision making."), Editable]
@@ -543,7 +543,7 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes), Editable]
public bool PoisonImmunity { get; set; }
[Serialize(1f, IsPropertySaveable.Yes, description: "1 = default, 0 = immune."), Editable(MinValueFloat = 0f, MaxValueFloat = 1000, DecimalCount = 1)]
public float PoisonVulnerability { get; set; }
@@ -552,6 +552,19 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Can afflictions affect the face/body tint of the character."), Editable]
public bool ApplyAfflictionColors { get; private set; }
[Serialize("", IsPropertySaveable.Yes, description:"A comma-separated list of identifiers of afflictions that the creature is immune to."), Editable]
public string Immunities { get; private set; }
private ImmutableHashSet<Identifier> _immunityIdentifiers;
public IEnumerable<Identifier> ImmunityIdentifiers
{
get
{
_immunityIdentifiers ??= Element.GetAttributeIdentifierArray("immunities", Array.Empty<Identifier>()).ToImmutableHashSet();
return _immunityIdentifiers;
}
}
// TODO: limbhealths, sprite?
@@ -634,6 +647,9 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Affects how far the character can hear the targets. Used as a multiplier."), Editable(minValue: 0f, maxValue: 10f)]
public float Hearing { get; private set; }
[Serialize(-1.0f, IsPropertySaveable.Yes, description: "Hard limit to how far the character can spot targets from, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable]
public float MaxPerceptionDistance { get; set; }
[Serialize(100f, IsPropertySaveable.Yes, description: "How much the targeting priority increases each time the character takes damage. Works like the greed value, described above. The default value is 100."), Editable(minValue: -1000f, maxValue: 1000f)]
public float AggressionHurt { get; private set; }
@@ -673,7 +689,7 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description:"Does the creature know how to open doors (still requires a proper ID card). Humans can always open doors (They don't use this AI definition)."), Editable]
public bool CanOpenDoors { get; private set; }
[Serialize(false, IsPropertySaveable.Yes), Editable]
[Serialize(false, IsPropertySaveable.Yes, description:"Unlike human AI, monsters normally only use pathfinding when they are inside the submarine. When this is enabled, the monsters can also use pathfinding to get inside the sub. In practice, via doors and hatches."), Editable]
public bool UsePathFindingToGetInside { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Does the creature close the doors behind it. Humans don't use this AI definition."), Editable]
@@ -690,17 +706,18 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, "Does the creature patrol the dry hulls while idling inside a friendly submarine?"), Editable]
public bool PatrolDry { get; set; }
[Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "Initial aggression used in the circle attack pattern (0-100). The aggression affects how close and how fast to the target the monster circles."), Editable]
public float StartAggression { get; private set; }
[Serialize(100f, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(100f, IsPropertySaveable.Yes, description: "Maximum aggression used in the circle attack pattern (0-100). The aggression affects how close and how fast to the target the monster circles."), Editable]
public float MaxAggression { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(0f, IsPropertySaveable.Yes, description: "How quickly the aggression level increases from StartAggression to MaxAggression when using the circle attack pattern. Artificial amount, applied once per attack cycle."), Editable]
public float AggressionCumulation { get; private set; }
[Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: ""), Editable]
[Serialize(WallTargetingMethod.Target, IsPropertySaveable.Yes, description: "Defines the method of checking whether there's a blocking (submarine) wall."), Editable]
public WallTargetingMethod WallTargetingMethod { get; private set; }
public IEnumerable<TargetParams> Targets => targets;
@@ -851,6 +868,12 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored while the creature is outside. Doesn't matter where the target is."), Editable]
public bool IgnoreOutside { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside. Doesn't matter where the creature itself is."), Editable]
public bool IgnoreTargetInside { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's outside. Doesn't matter where the creature itself is."), Editable]
public bool IgnoreTargetOutside { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Should the target be ignored if it's inside a different submarine than us? Normally only some targets are ignored when they are not inside the same sub."), Editable]
public bool IgnoreIfNotInSameSub { get; set; }
@@ -866,10 +889,16 @@ namespace Barotrauma
[Serialize(-1f, IsPropertySaveable.Yes, description: "A generic max threshold. Not used if set to negative."), Editable]
public float ThresholdMax { get; private set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes), Editable]
[Serialize(1.0f, IsPropertySaveable.Yes, description: "Can be used to make the monster perceive the target further than it normally can."), Editable]
public float PerceptionDistanceMultiplier { get; private set; }
[Serialize(-1.0f, IsPropertySaveable.Yes, description: "Maximum distance at which the monster can perceive the target, regardless of the sight/hearing or how visible or how much noise the target is making. Not used if set to negative."), Editable]
public float MaxPerceptionDistance { get; private set; }
[Serialize("0.0, 0.0", IsPropertySaveable.Yes, description: "A generic offset. Used for example for offsetting the react distance (vector length) and for offsetting the target position when a guardian flees to a pod."), Editable]
public Vector2 Offset { get; private set; }
[Serialize(AttackPattern.Straight, IsPropertySaveable.Yes), Editable]
[Serialize(AttackPattern.Straight, IsPropertySaveable.Yes, description: "Defines the movement pattern of the character when approaching a target."), Editable]
public AttackPattern AttackPattern { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, the AI will give more priority to targets close to the horizontal middle of the sub. Only applies to walls, hulls, and items like sonar. Circle and Sweep always does this regardless of this property."), Editable]
@@ -887,31 +916,48 @@ namespace Barotrauma
#endregion
#region Circle
[Serialize(5000f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
[Serialize(5000f, IsPropertySaveable.Yes, description:"How close to the target the character should be, before they start using the circle pattern instead of directional approaching."), Editable(MinValueFloat = 0f, MaxValueFloat = 20000f)]
public float CircleStartDistance { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description:"Normally the target size is taken into account when calculating the distance to the target. Set this true to skip that.")]
public bool IgnoreTargetSize { get; private set; }
[Serialize(1f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)]
[Serialize(1f, IsPropertySaveable.Yes, description:"Determines the rate how quickly the target movement position is rotated towards the attack target. The actual rotation is calculated once per each attack cycle, based on the current aggression level."), Editable(MinValueFloat = 0f, MaxValueFloat = 100f)]
public float CircleRotationSpeed { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description:"When enabled, the circle rotation speed can change when the target is far. When this setting is disabled (default), the character will head directly towards the target when it's too far."), Editable]
public bool DynamicCircleRotationSpeed { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)]
[Serialize(0f, IsPropertySaveable.Yes, description:"How much the turn speed can differ between attack cycles (stays constant during the cycle)"), Editable(MinValueFloat = 0f, MaxValueFloat = 1f)]
public float CircleRandomRotationFactor { get; private set; }
[Serialize(5f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)]
[Serialize(5f, IsPropertySaveable.Yes, description:"Affects how close to the target the character has to be before the strike phase of the circle behavior triggers. In the strike phase, the creature moves directly towards the target."), Editable(MinValueFloat = 0f, MaxValueFloat = 10f)]
public float CircleStrikeDistanceMultiplier { get; private set; }
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
[Serialize(0f, IsPropertySaveable.Yes, description:"How much the target position is offset at maximum. Low values make the character hit the target earlier/always, higher values make it miss the target when the aggression intensity is low (early in the encounter)."), Editable(MinValueFloat = 0f, MaxValueFloat = 50f)]
public float CircleMaxRandomOffset { get; private set; }
#endregion
public TargetParams(ContentXElement element, CharacterParams character) : base(element, character) { }
/// <summary>
/// Conditionals that must be met for the character to be able to use these targeting parameters.
/// </summary>
public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
public TargetParams(string tag, AIState state, float priority, CharacterParams character) : base(CreateNewElement(character, tag, state, priority), character) { }
public TargetParams(string tag, AIState state, float priority, CharacterParams character) :
this(CreateNewElement(character, tag, state, priority), character) { }
public TargetParams(ContentXElement element, CharacterParams character) : base(element, character)
{
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "conditional":
Conditionals.AddRange(PropertyConditional.FromXElement(subElement));
break;
}
}
}
public static ContentXElement CreateNewElement(CharacterParams character, Identifier tag, AIState state, float priority) =>
CreateNewElement(character, tag.Value, state, priority);
@@ -16,6 +16,7 @@ namespace Barotrauma
public bool IsLoaded { get; protected set; }
public string Name { get; private set; }
public string FileName { get; private set; }
public string FileNameWithoutExtension { get; private set; }
public string Folder { get; private set; }
public ContentPath Path { get; protected set; } = ContentPath.Empty;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; protected set; }
@@ -103,8 +104,9 @@ namespace Barotrauma
{
Path = fullPath;
Name = GetName();
FileName = System.IO.Path.GetFileName(Path.Value);
Folder = System.IO.Path.GetDirectoryName(Path.Value);
FileName = Barotrauma.IO.Path.GetFileName(Path.Value);
FileNameWithoutExtension = Barotrauma.IO.Path.GetFileNameWithoutExtension(Path.Value);
Folder = Barotrauma.IO.Path.GetDirectoryName(Path.Value);
}
public virtual bool Save(string fileNameWithoutExtension = null, System.Xml.XmlWriterSettings settings = null)
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml.Linq;
using System.Linq;
using Barotrauma.IO;
@@ -13,15 +14,31 @@ using Barotrauma.SpriteDeformations;
namespace Barotrauma
{
public enum CanEnterSubmarine
{
/// <summary>
/// No part of the ragdoll can go inside a submarine
/// </summary>
False,
/// <summary>
/// Can fully enter a submarine. Make sure to only allow this on small/medium sized creatures that can reasonably fit inside rooms.
/// </summary>
True,
/// <summary>
/// The ragdoll's limbs can enter the sub, but the collider can't.
/// Can be used to e.g. allow the monster's head to poke into the sub to bite characters, even if the whole monster can't fit in the sub.
/// </summary>
Partial
}
class HumanRagdollParams : RagdollParams
{
public static HumanRagdollParams GetRagdollParams(Identifier speciesName, string fileName = null) => GetRagdollParams<HumanRagdollParams>(speciesName, fileName);
public static HumanRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams<HumanRagdollParams>(speciesName);
public static HumanRagdollParams GetDefaultRagdollParams(Character character) => GetDefaultRagdollParams<HumanRagdollParams>(character);
}
class FishRagdollParams : RagdollParams
{
public static FishRagdollParams GetDefaultRagdollParams(Identifier speciesName) => GetDefaultRagdollParams<FishRagdollParams>(speciesName);
public static FishRagdollParams GetDefaultRagdollParams(Character character) => GetDefaultRagdollParams<FishRagdollParams>(character);
}
class RagdollParams : EditableParams, IMemorizable<RagdollParams>
@@ -37,8 +54,11 @@ namespace Barotrauma
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes), Editable()]
public Color Color { get; set; }
[Serialize(0.0f, IsPropertySaveable.Yes, description: "The orientation of the sprites as drawn on the sprite sheet. Can be overridden by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "General orientation of the sprites as drawn on the spritesheet. " +
"Defines the \"forward direction\" of the sprites. Should be configured as the direction pointing outwards from the main limb. " +
"Incorrectly defined orientations may lead to limbs being rotated incorrectly when e.g. when the character aims or flips to face a different direction. " +
"Can be overridden per sprite by setting a value for Limb's 'Sprite Orientation'."), Editable(-360, 360)]
public float SpritesheetOrientation { get; set; }
public bool IsSpritesheetOrientationHorizontal
@@ -53,11 +73,19 @@ namespace Barotrauma
private float limbScale;
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float LimbScale { get { return limbScale; } set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } }
public float LimbScale
{
get { return limbScale; }
set { limbScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); }
}
private float jointScale;
[Serialize(1.0f, IsPropertySaveable.Yes), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
public float JointScale { get { return jointScale; } set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); } }
public float JointScale
{
get { return jointScale; }
set { jointScale = MathHelper.Clamp(value, MIN_SCALE, MAX_SCALE); }
}
// Don't show in the editor, because shouldn't be edited in runtime. Requires that the limb scale and the collider sizes are adjusted. TODO: automatize?
[Serialize(1f, IsPropertySaveable.No)]
@@ -69,8 +97,8 @@ namespace Barotrauma
[Serialize(50f, IsPropertySaveable.Yes, description: "How much impact is required before the character takes impact damage?"), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
public float ImpactTolerance { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()]
public bool CanEnterSubmarine { get; set; }
[Serialize(CanEnterSubmarine.True, IsPropertySaveable.Yes, description: "Can the creature enter submarine. Creatures that cannot enter submarines, always collide with it, even when there is a gap."), Editable()]
public CanEnterSubmarine CanEnterSubmarine { get; set; }
[Serialize(true, IsPropertySaveable.Yes), Editable]
public bool CanWalk { get; set; }
@@ -86,7 +114,7 @@ namespace Barotrauma
/// key2: File path
/// value: Ragdoll parameters
/// </summary>
private readonly static Dictionary<Identifier, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<Identifier, Dictionary<string, RagdollParams>>();
private static readonly Dictionary<Identifier, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<Identifier, Dictionary<string, RagdollParams>>();
public List<ColliderParams> Colliders { get; private set; } = new List<ColliderParams>();
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
@@ -106,8 +134,7 @@ namespace Barotrauma
CharacterPrefab prefab = CharacterPrefab.Find(p => p.Identifier == speciesName && (contentPackage == null || p.ContentFile.ContentPackage == contentPackage));
if (prefab?.ConfigElement == null)
{
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'",
contentPackage: contentPackage);
DebugConsole.ThrowError($"Failed to find config file for '{speciesName}'", contentPackage: contentPackage);
return string.Empty;
}
return GetFolder(prefab.ConfigElement, prefab.ContentFile.Path.Value);
@@ -115,99 +142,151 @@ namespace Barotrauma
private static string GetFolder(ContentXElement root, string filePath)
{
var folder = root?.GetChildElement("ragdolls")?.GetAttributeContentPath("folder")?.Value;
Debug.Assert(filePath != null);
Debug.Assert(root != null);
string folder = (root.GetChildElement("ragdolls") ?? root.GetChildElement("ragdoll"))?.GetAttributeContentPath("folder")?.Value;
if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
folder = IO.Path.Combine(IO.Path.GetDirectoryName(filePath), "Ragdolls") + IO.Path.DirectorySeparatorChar;
}
return folder.CleanUpPathCrossPlatform(correctFilenameCase: true);
}
public static T GetDefaultRagdollParams<T>(Identifier speciesName) where T : RagdollParams, new() => GetRagdollParams<T>(speciesName);
/// <summary>
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
/// If a custom folder is used, it's defined in the character info file.
/// </summary>
public static T GetRagdollParams<T>(Identifier speciesName, string fileName = null) where T : RagdollParams, new()
public static T GetDefaultRagdollParams<T>(Character character) where T : RagdollParams, new() => GetDefaultRagdollParams<T>(character.SpeciesName, character.Params, character.Prefab.ContentPackage);
public static T GetDefaultRagdollParams<T>(Identifier speciesName, CharacterParams characterParams, ContentPackage contentPackage) where T : RagdollParams, new()
{
if (speciesName.IsEmpty)
XElement mainElement = characterParams.VariantFile?.Root ?? characterParams.MainElement;
return GetDefaultRagdollParams<T>(speciesName, mainElement, contentPackage);
}
public static T GetDefaultRagdollParams<T>(Identifier speciesName, XElement characterRootElement, ContentPackage contentPackage) where T : RagdollParams, new()
{
Debug.Assert(contentPackage != null);
if (characterRootElement.IsOverride())
{
throw new Exception($"Species name null or empty!");
characterRootElement = characterRootElement.FirstElement();
}
Identifier ragdollSpecies = speciesName;
Identifier variantOf = characterRootElement.VariantOf();
if (characterRootElement != null && (characterRootElement.GetChildElement("ragdolls") ?? characterRootElement.GetChildElement("ragdoll")) is XElement ragdollElement)
{
if ((ragdollElement.GetAttributeContentPath("path", contentPackage) ?? ragdollElement.GetAttributeContentPath("file", contentPackage)) is ContentPath path)
{
return GetRagdollParams<T>(speciesName, ragdollSpecies, file: path, contentPackage);
}
else if (!variantOf.IsEmpty)
{
string folder = ragdollElement.GetAttributeContentPath("folder", contentPackage)?.Value;
if (folder.IsNullOrEmpty() || folder.Equals("default", StringComparison.OrdinalIgnoreCase))
{
// Folder attribute not defined or set to default -> use the ragdoll defined in the base definition file.
if (CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab)
{
ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf);
}
}
}
}
else if (!variantOf.IsEmpty && CharacterPrefab.FindBySpeciesName(variantOf) is CharacterPrefab prefab)
{
// Ragdoll element not defined -> use the ragdoll defined in the base definition file.
ragdollSpecies = prefab.GetBaseCharacterSpeciesName(variantOf);
}
// Using a null file definition means we use the default animations found in the Ragdolls folder.
return GetRagdollParams<T>(speciesName, ragdollSpecies, file: null, contentPackage);
}
public static T GetRagdollParams<T>(Identifier speciesName, Identifier ragdollSpecies, Either<string, ContentPath> file, ContentPackage contentPackage) where T : RagdollParams, new()
{
Debug.Assert(!speciesName.IsEmpty);
Debug.Assert(!ragdollSpecies.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());
}
Debug.Assert(contentPackage != null);
if (!allRagdolls.TryGetValue(speciesName, out Dictionary<string, RagdollParams> ragdolls))
{
ragdolls = new Dictionary<string, RagdollParams>();
allRagdolls.Add(speciesName, ragdolls);
}
if (!string.IsNullOrEmpty(fileName) && ragdolls.TryGetValue(fileName, out RagdollParams ragdoll))
string key = fileName ?? contentPath?.Value ?? GetDefaultFileName(ragdollSpecies);
if (ragdolls.TryGetValue(key, out RagdollParams ragdoll))
{
// Already cached.
return (T)ragdoll;
}
string selectedFile = null;
Identifier ragdollSpecies = speciesName;
if (CharacterPrefab.Prefabs.TryGet(speciesName, out var prefab))
if (!contentPath.IsNullOrEmpty())
{
if (!prefab.VariantOf.IsEmpty)
// Load the ragdoll from path.
T ragdollInstance = new T();
if (ragdollInstance.Load(contentPath, ragdollSpecies))
{
ragdollSpecies = prefab.VariantOf;
ragdolls.TryAdd(contentPath.Value, ragdollInstance);
return ragdollInstance;
}
string error = null;
string folder = GetFolder(ragdollSpecies);
if (!Directory.Exists(folder))
else
{
error = $"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.";
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {ragdollInstance} from {contentPath.Value} for the character {speciesName}. Using the default ragdoll.", contentPackage: contentPackage);
}
}
// Seek the default ragdoll from the character's ragdoll folder.
string selectedFile;
string folder = GetFolder(ragdollSpecies);
if (Directory.Exists(folder))
{
var files = Directory.GetFiles(folder).OrderBy(f => f, StringComparer.OrdinalIgnoreCase);
if (files.None())
{
DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.", contentPackage: contentPackage);
selectedFile = GetDefaultFile(ragdollSpecies);
}
else
{
string[] files = Directory.GetFiles(folder);
if (files.None())
if (string.IsNullOrEmpty(fileName))
{
error = $"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.";
selectedFile = GetDefaultFile(ragdollSpecies);
}
else if (string.IsNullOrEmpty(fileName))
{
// Files found, but none specified
selectedFile = GetDefaultFile(ragdollSpecies);
// Files found, but none specified -> Get a matching ragdoll from the specified folder.
// First try to find a file that matches the default file name. If that fails, just take any file.
string defaultFileName = GetDefaultFileName(ragdollSpecies);
selectedFile = files.FirstOrDefault(f => f.Contains(defaultFileName, StringComparison.OrdinalIgnoreCase)) ?? files.First();
}
else
{
selectedFile = files.FirstOrDefault(f => IO.Path.GetFileNameWithoutExtension(f).Equals(fileName, StringComparison.OrdinalIgnoreCase));
if (selectedFile == null)
{
error = $"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.";
DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.", contentPackage: contentPackage);
selectedFile = GetDefaultFile(ragdollSpecies);
}
}
}
}
if (error != null)
{
DebugConsole.ThrowError(error,
contentPackage: prefab?.ContentPackage);
}
}
if (selectedFile == null)
{
throw new Exception("[RagdollParams] Selected file null!");
}
DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}.");
var characterPrefab = CharacterPrefab.Prefabs[speciesName];
T r = new T();
if (r.Load(ContentPath.FromRaw(characterPrefab.ContentPackage, selectedFile), ragdollSpecies))
{
if (!ragdolls.ContainsKey(r.Name))
{
ragdolls.Add(r.Name, r);
}
return r;
}
else
{
// Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harded to debug. It's better to fail early.
DebugConsole.ThrowError($"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.", contentPackage: contentPackage);
selectedFile = GetDefaultFile(ragdollSpecies);
}
Debug.Assert(selectedFile != null);
DebugConsole.Log($"[RagdollParams] Loading the ragdoll from {selectedFile}.");
T r = new T();
if (r.Load(ContentPath.FromRaw(contentPackage, selectedFile), speciesName))
{
ragdolls.TryAdd(key, r);
}
else
{
// Failing to create a ragdoll causes so many issues that cannot be handled. Dummy ragdoll just seems to make things harder to debug. It's better to fail early.
throw new Exception($"[RagdollParams] Failed to load ragdoll {r.Name} from {selectedFile} for the character {speciesName}.");
}
return r;
}
/// <summary>
@@ -234,9 +313,9 @@ namespace Barotrauma
instance.IsLoaded = instance.Deserialize(mainElement);
instance.Save();
instance.Load(contentPath, speciesName);
ragdolls.Add(instance.Name, instance);
ragdolls.Add(instance.FileNameWithoutExtension, instance);
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
return instance as T;
return instance;
}
public static void ClearCache() => allRagdolls.Clear();
@@ -250,16 +329,17 @@ namespace Barotrauma
else
{
// Update the key by removing and re-adding the ragdoll.
string fileName = FileNameWithoutExtension;
if (allRagdolls.TryGetValue(SpeciesName, out Dictionary<string, RagdollParams> ragdolls))
{
ragdolls.Remove(Name);
ragdolls.Remove(fileName);
}
base.UpdatePath(fullPath);
if (ragdolls != null)
{
if (!ragdolls.ContainsKey(Name))
if (!ragdolls.ContainsKey(fileName))
{
ragdolls.Add(Name, this);
ragdolls.Add(fileName, this);
}
}
}
@@ -282,6 +362,7 @@ namespace Barotrauma
{
if (Load(file))
{
isVariantScaleApplied = false;
SpeciesName = speciesName;
CreateColliders();
CreateLimbs();
@@ -398,18 +479,21 @@ namespace Barotrauma
}
#endif
private bool variantScaleApplied;
public void ApplyVariantScale(XDocument variantFile)
private bool isVariantScaleApplied;
public void TryApplyVariantScale(XDocument variantFile)
{
if (variantScaleApplied) { return; }
if (isVariantScaleApplied) { return; }
if (variantFile == null) { return; }
var scaleMultiplier = variantFile.Root.GetChildElement("ragdoll")?.GetAttributeFloat("scalemultiplier", 1f);
if (scaleMultiplier.HasValue)
if (variantFile.GetRootExcludingOverride() is XElement root)
{
JointScale *= scaleMultiplier.Value;
LimbScale *= scaleMultiplier.Value;
if ((root.GetChildElement("ragdoll") ?? root.GetChildElement("ragdolls")) is XElement ragdollElement)
{
float scaleMultiplier = ragdollElement.GetAttributeFloat("scalemultiplier", 1f);
JointScale *= scaleMultiplier;
LimbScale *= scaleMultiplier;
}
}
variantScaleApplied = true;
isVariantScaleApplied = true;
}
#endregion
@@ -623,8 +707,11 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.Yes, description: "Disable drawing for this limb."), Editable()]
public bool Hide { get; set; }
[Serialize(float.NaN, IsPropertySaveable.Yes, description: "The orientation of the sprite as drawn on the sprite sheet. Overrides the value defined in the Ragdoll settings."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
[Serialize(float.NaN, IsPropertySaveable.Yes, description: "Orientation of the sprite as drawn on the spritesheet. " +
"Defines the \"forward direction\" of the sprite. Should be configured as the direction pointing outwards from the main limb." +
"Incorrectly defined orientations may lead to limbs being rotated incorrectly when e.g. when the character aims or flips to face a different direction. " +
"Overrides the value of 'Spritesheet Orientation' for this limb."), Editable(-360, 360, ValueStep = 90, DecimalCount = 0)]
public float SpriteOrientation { get; set; }
[Serialize(LimbType.None, IsPropertySaveable.Yes, description: "If set, the limb sprite will use the same sprite depth as the specified limb. Generally only useful for limbs that get added on the ragdoll on the fly (e.g. extra limbs added via gene splicing).")]
@@ -744,6 +831,9 @@ namespace Barotrauma
[Serialize(0.05f, IsPropertySaveable.Yes)]
public float Restitution { get; set; }
[Serialize(true, IsPropertySaveable.Yes, description: "Can the limb enter submarines? Only valid if the ragdoll's CanEnterSubmarine is set to Partial, otherwise the limb can enter if the ragdoll can."), Editable]
public bool CanEnterSubmarine { get; private set; }
public LimbParams(ContentXElement element, RagdollParams ragdoll) : base(element, ragdoll)
{
var spriteElement = element.GetChildElement("sprite");