#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using Barotrauma.IO; using System.Linq; using System.Reflection; using System.Xml.Linq; namespace Barotrauma { [AttributeUsage(AttributeTargets.Class, Inherited = true)] public class NotSyncedInMultiplayer : Attribute { } /// /// Base class for content file types, which are loaded /// from filelist.xml via reflection. /// PLEASE AVOID INHERITING FROM THIS CLASS DIRECTLY. /// Inheriting from GenericPrefabFile<T> is likely what /// you want. /// public abstract class ContentFile { public class TypeInfo { public readonly Type Type; public readonly bool RequiredByCorePackage; public readonly bool NotSyncedInMultiplayer; public readonly ImmutableHashSet? AlternativeTypes; public readonly ImmutableHashSet Names; private readonly MethodInfo? contentPathMutator; public TypeInfo(Type type) { Type = type; var reqByCoreAttribute = type.GetCustomAttribute(); RequiredByCorePackage = reqByCoreAttribute != null; var notSyncedInMultiplayerAttribute = type.GetCustomAttribute(); NotSyncedInMultiplayer = notSyncedInMultiplayerAttribute != null; AlternativeTypes = reqByCoreAttribute?.AlternativeTypes; contentPathMutator = Type.GetMethod(nameof(MutateContentPath), BindingFlags.Static | BindingFlags.Public); HashSet names = new HashSet { type.Name.RemoveFromEnd("File").ToIdentifier() }; if (type.GetCustomAttribute(inherit: false)?.Names is { } altNames) { names.UnionWith(altNames); } Names = names.ToImmutableHashSet(); } public ContentPath MutateContentPath(ContentPath path) => (ContentPath?)contentPathMutator?.Invoke(null, new object[] { path }) ?? path; public ContentFile? CreateInstance(ContentPackage contentPackage, ContentPath path) => (ContentFile?)Activator.CreateInstance(Type, contentPackage, path); } public readonly static ImmutableHashSet Types; static ContentFile() { Types = ReflectionUtils.GetDerivedNonAbstract() .Select(t => new TypeInfo(t)) .ToImmutableHashSet(); } public static Result CreateFromXElement(ContentPackage contentPackage, XElement element) { static Result fail(string error, Exception? exception = null) => Result.Failure(new LoadError(error, exception)); Identifier elemName = element.NameAsIdentifier(); var type = Types.FirstOrDefault(t => t.Names.Contains(elemName)); var filePath = element.GetAttributeContentPath("file", contentPackage); if (type is null) { return fail($"Invalid content type \"{elemName}\""); } if (filePath is null) { return fail($"No content path defined for file of type \"{elemName}\""); } try { filePath = type.MutateContentPath(filePath); if (!File.Exists(filePath.FullPath)) { return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": file not found."); } var file = type.CreateInstance(contentPackage, filePath); return file is null ? throw new Exception($"Content type is not implemented correctly") : Result.Success(file); } catch (Exception e) { return fail($"Failed to load file \"{filePath}\" of type \"{elemName}\": {e.Message}", e); } } protected ContentFile(ContentPackage contentPackage, ContentPath path) { ContentPackage = contentPackage; Path = path; Hash = CalculateHash(); } public readonly ContentPackage ContentPackage; public readonly ContentPath Path; public readonly Md5Hash Hash; public abstract void LoadFile(); public abstract void UnloadFile(); public abstract void Sort(); public virtual void Preload(Action addPreloadedSprite) { } public virtual Md5Hash CalculateHash() { return Md5Hash.CalculateForFile(Path.Value, Md5Hash.StringHashOptions.IgnoreWhitespace); } public bool NotSyncedInMultiplayer => Types.Any(t => t.Type == GetType() && t.NotSyncedInMultiplayer); public readonly struct LoadError { public readonly string Message; public readonly Exception? Exception; public LoadError(string message, Exception? exception) { Message = message; Exception = exception; } public override string ToString() => Message + (Exception is { StackTrace: var stackTrace } ? '\n' + stackTrace.CleanupStackTrace() : string.Empty); } } }