From 4d47c3cd39d55adb8edf57410394b8e6ba475afa Mon Sep 17 00:00:00 2001 From: aldelaro5 Date: Tue, 20 Aug 2024 15:13:10 -0400 Subject: [PATCH] Address code review --- BepInEx.Core/Bootstrap/BaseChainloader.cs | 45 +++++++++++++------ BepInEx.Core/Bootstrap/TypeLoader.cs | 36 +++++++-------- BepInEx.Core/Contract/Attributes.cs | 2 +- BepInEx.Core/Contract/BasePluginProvider.cs | 4 +- ...IPluginLoader.cs => IPluginLoadContext.cs} | 4 +- BepInEx.Core/Contract/PluginInfo.cs | 8 ++-- ...Loader.cs => BepInExPatcherLoadContext.cs} | 2 +- .../BepInExPatcherProvider.cs | 8 ++-- ...nLoader.cs => BepInExPluginLoadContext.cs} | 2 +- .../BepInExPluginProvider.cs | 8 ++-- .../Patching/AssemblyPatcher.cs | 37 +++++++++------ BepInEx.Preloader.Core/Patching/Attributes.cs | 2 +- .../Patching/BasePatcherProvider.cs | 4 +- .../Patching/PatcherPluginMetadata.cs | 4 +- 14 files changed, 97 insertions(+), 69 deletions(-) rename BepInEx.Core/Contract/{IPluginLoader.cs => IPluginLoadContext.cs} (91%) rename BepInEx.PatcherProvider/{BepInExPatcherLoader.cs => BepInExPatcherLoadContext.cs} (87%) rename BepInEx.PluginProvider/{BepInExPluginLoader.cs => BepInExPluginLoadContext.cs} (87%) diff --git a/BepInEx.Core/Bootstrap/BaseChainloader.cs b/BepInEx.Core/Bootstrap/BaseChainloader.cs index 9489e93..a025b21 100644 --- a/BepInEx.Core/Bootstrap/BaseChainloader.cs +++ b/BepInEx.Core/Bootstrap/BaseChainloader.cs @@ -23,9 +23,10 @@ public abstract class BaseChainloader /// Analyzes the given type definition and attempts to convert it to a valid /// /// Type definition to analyze. + /// The load context of the plugin /// The filepath of the assembly, to keep as metadata. /// If the type represent a valid plugin, returns a instance. Otherwise, return null. - public static PluginInfo ToPluginInfo(TypeDefinition type, IPluginLoader loader, string assemblyLocation) + public static PluginInfo ToPluginInfo(TypeDefinition type, IPluginLoadContext loadContext, string assemblyLocation) { if (type.IsInterface || type.IsAbstract) return null; @@ -42,11 +43,11 @@ public abstract class BaseChainloader } if (type.IsSubtypeOf(typeof(BasePluginProvider))) - return GetPluginInfo(type, BepInPluginProvider.FromCecilType(type), loader, assemblyLocation); - return GetPluginInfo(type, BepInPlugin.FromCecilType(type), loader, assemblyLocation); + return GetPluginInfo(type, BepInPluginProvider.FromCecilType(type), loadContext, assemblyLocation); + return GetPluginInfo(type, BepInPlugin.FromCecilType(type), loadContext, assemblyLocation); } - private static PluginInfo GetPluginInfo(TypeDefinition type, BepInPlugin metadata, IPluginLoader loader, string assemblyLocation) + private static PluginInfo GetPluginInfo(TypeDefinition type, BepInPlugin metadata, IPluginLoadContext loadContext, string assemblyLocation) { // Perform checks that will prevent the plugin from being loaded in ALL cases if (metadata == null) @@ -91,7 +92,7 @@ public abstract class BaseChainloader TypeName = type.FullName, TargettedBepInExVersion = bepinVersion, Location = assemblyLocation, - Loader = loader + LoadContext = loadContext }; } @@ -123,18 +124,22 @@ public abstract class BaseChainloader /// /// List of all instances of all plugins loaded via the chainloader. + /// If a plugin fails to load, it is removed. This list only contains the plugins that were actually + /// loaded and have their dependencies satisfied. /// public Dictionary Plugins { get; } = new(); /// /// List of all instances of all providers loaded via the chainloader. + /// If a provider fails to load, it is removed. This list only contains the providers that were actually + /// loaded and have their dependencies satisfied. /// public Dictionary Providers { get; } = new(); /// - /// List of all discovered from all the providers + /// List of all discovered from all the providers /// - public List Loaders { get; } = new(); + public List LoadContexts { get; } = new(); /// /// Collection of error chainloader messages that occured during plugin loading. @@ -142,6 +147,16 @@ public abstract class BaseChainloader /// public List DependencyErrors { get; } = new(); + /// + /// Occurs after a plugin provider is loaded. + /// + public event Action ProviderLoaded; + + /// + /// Occurs after all plugins providers are loaded. + /// + public event Action FinishedProviders; + /// /// Occurs after a plugin is loaded. /// @@ -323,12 +338,13 @@ public abstract class BaseChainloader var builtinProviders = DiscoverPluginProvidersFrom(Paths.BepInExAssemblyDirectory); var providers = DiscoverPluginProvidersFrom(Paths.PluginProviderPath); Logger.Log(LogLevel.Info, $"{providers.Count + builtinProviders.Count} " + - $"meta plugin{(providers.Count + builtinProviders.Count == 1 ? "" : "s")} to load"); + $"plugins provider{(providers.Count + builtinProviders.Count == 1 ? "" : "s")} to load"); LoadPlugins(builtinProviders, provider: true); LoadPlugins(providers, provider: true); + FinishedProviders?.Invoke(); - var plugins = TypeLoader.GetPluginsFromLoaders(Loaders, ToPluginInfo, HasBepinPluginType, "chainloader"); + var plugins = TypeLoader.GetPluginsFromLoadContexts(LoadContexts, ToPluginInfo, HasBepinPluginType, "chainloader"); Logger.Log(LogLevel.Info, $"{plugins.Count} plugin{(plugins.Count == 1 ? "" : "s")} to load"); LoadPlugins(plugins, provider: false); Finished?.Invoke(); @@ -373,10 +389,10 @@ public abstract class BaseChainloader ass = Assembly.LoadFrom(plugin.Location); loadedAssemblies[plugin.Location] = ass; } - else if (!provider && !loadedAssemblies.TryGetValue(plugin.Loader.AssemblyIdentifier, out ass)) + else if (!provider && !loadedAssemblies.TryGetValue(plugin.LoadContext.AssemblyIdentifier, out ass)) { - ass = Assembly.Load(plugin.Loader.GetAssemblyData()); - loadedAssemblies[plugin.Loader.AssemblyIdentifier] = ass; + ass = Assembly.Load(plugin.LoadContext.GetAssemblyData()); + loadedAssemblies[plugin.LoadContext.AssemblyIdentifier] = ass; } existingPlugins[plugin.Metadata.GUID] = plugin; @@ -388,13 +404,14 @@ public abstract class BaseChainloader plugin.Instance = Activator.CreateInstance(type); AppDomain.CurrentDomain.AssemblyResolve += (_, args) => ((BasePluginProvider)plugin.Instance).ResolveAssembly(args.Name); var requestedPluginsStream = ((BasePluginProvider)plugin.Instance).GetPlugins(); - Loaders.AddRange(requestedPluginsStream); + LoadContexts.AddRange(requestedPluginsStream); + ProviderLoaded?.Invoke(plugin); } else { plugin.Instance = LoadPlugin(plugin, ass); + PluginLoaded?.Invoke(plugin); } - PluginLoaded?.Invoke(plugin); } catch (Exception ex) { diff --git a/BepInEx.Core/Bootstrap/TypeLoader.cs b/BepInEx.Core/Bootstrap/TypeLoader.cs index 111ac38..4cecee8 100644 --- a/BepInEx.Core/Bootstrap/TypeLoader.cs +++ b/BepInEx.Core/Bootstrap/TypeLoader.cs @@ -128,7 +128,7 @@ public static class TypeLoader /// selector. /// public static Dictionary> FindPluginTypes(string directory, - Func typeSelector, + Func typeSelector, Func assemblyFilter = null, string cacheName = null) where T : ICacheable, new() @@ -176,7 +176,7 @@ public static class TypeLoader /// Looks up assemblies using the given loaders and locates all types that can be loaded and collects their metadata. /// /// The specific base type to search for. - /// The loaders to obtain the assemblies from. + /// The load contexts to obtain the assemblies from. /// A function to check if a type should be selected and to build the type metadata. /// A filter function to quickly determine if the assembly can be loaded. /// The name of the cache to get cached types from. @@ -184,10 +184,10 @@ public static class TypeLoader /// A dictionary of all assemblies in the directory and the list of type metadatas of types that match the /// selector. /// - public static List GetPluginsFromLoaders(IList loaders, - Func typeSelector, - Func assemblyFilter = null, - string cacheName = null) + public static List GetPluginsFromLoadContexts(IList loadContexts, + Func typeSelector, + Func assemblyFilter = null, + string cacheName = null) where T : ICacheable, new() { var result = new Dictionary>(); @@ -197,27 +197,27 @@ public static class TypeLoader if (cacheName != null) cache = LoadAssemblyCache(cacheName); - foreach (IPluginLoader pluginLoader in loaders) + foreach (IPluginLoadContext loadContext in loadContexts) { IList plugins; - if (cache != null && pluginLoader.AssemblyHash != null && - cache.TryGetValue(pluginLoader.AssemblyIdentifier, out var cacheEntry) && - pluginLoader.AssemblyHash == cacheEntry.Hash) + if (cache != null && loadContext.AssemblyHash != null && + cache.TryGetValue(loadContext.AssemblyIdentifier, out var cacheEntry) && + loadContext.AssemblyHash == cacheEntry.Hash) { plugins = cacheEntry.CacheItems; } else { - var assemblyData = pluginLoader.GetAssemblyData(); + var assemblyData = loadContext.GetAssemblyData(); using var memory = new MemoryStream(assemblyData); - plugins = ExamineStream(typeSelector, assemblyFilter, memory, pluginLoader, null); + plugins = ExamineStream(typeSelector, assemblyFilter, memory, loadContext, null); } foreach (T pluginInfo in plugins) { - if (!result.ContainsKey(pluginLoader.AssemblyIdentifier)) - result[pluginLoader.AssemblyIdentifier] = new(); - result[pluginLoader.AssemblyIdentifier].Add(pluginInfo); + if (!result.ContainsKey(loadContext.AssemblyIdentifier)) + result[loadContext.AssemblyIdentifier] = new(); + result[loadContext.AssemblyIdentifier].Add(pluginInfo); } } @@ -227,10 +227,10 @@ public static class TypeLoader return result.SelectMany(x => x.Value).ToList(); } - private static List ExamineStream(Func typeSelector, + private static List ExamineStream(Func typeSelector, Func assemblyFilter, MemoryStream dllMs, - IPluginLoader loader, + IPluginLoadContext loadContext, string location) where T : ICacheable, new() { @@ -243,7 +243,7 @@ public static class TypeLoader } var matches = ass.MainModule.Types - .Select(t => typeSelector(t, loader, location)) + .Select(t => typeSelector(t, loadContext, location)) .Where(t => t != null).ToList(); return matches; } diff --git a/BepInEx.Core/Contract/Attributes.cs b/BepInEx.Core/Contract/Attributes.cs index 978c170..f9d495c 100644 --- a/BepInEx.Core/Contract/Attributes.cs +++ b/BepInEx.Core/Contract/Attributes.cs @@ -89,7 +89,7 @@ public class BepInPluginProvider : BepInPlugin { } - internal static BepInPluginProvider FromCecilType(TypeDefinition td) + internal new static BepInPluginProvider FromCecilType(TypeDefinition td) { var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); diff --git a/BepInEx.Core/Contract/BasePluginProvider.cs b/BepInEx.Core/Contract/BasePluginProvider.cs index 202bd3a..594044a 100644 --- a/BepInEx.Core/Contract/BasePluginProvider.cs +++ b/BepInEx.Core/Contract/BasePluginProvider.cs @@ -55,8 +55,8 @@ public abstract class BasePluginProvider /// /// Obtains a list of assemblies containing plugins to load /// - /// A list of loaders, one per assembly - public abstract IList GetPlugins(); + /// A list of load context, one per assembly + public abstract IList GetPlugins(); /// /// A custom assembly resolver that can be used by this provider to resolve assemblies diff --git a/BepInEx.Core/Contract/IPluginLoader.cs b/BepInEx.Core/Contract/IPluginLoadContext.cs similarity index 91% rename from BepInEx.Core/Contract/IPluginLoader.cs rename to BepInEx.Core/Contract/IPluginLoadContext.cs index f98c937..c17fc41 100644 --- a/BepInEx.Core/Contract/IPluginLoader.cs +++ b/BepInEx.Core/Contract/IPluginLoadContext.cs @@ -2,9 +2,9 @@ /// /// An interface that allows to dynamically load and track an assembly -/// which can be obtained from a provided +/// which can be obtained from a provider /// -public interface IPluginLoader +public interface IPluginLoadContext { /// /// An identifier that uniquely identifies an assembly from a provider no matter its revision diff --git a/BepInEx.Core/Contract/PluginInfo.cs b/BepInEx.Core/Contract/PluginInfo.cs index f4d33ca..15e2729 100644 --- a/BepInEx.Core/Contract/PluginInfo.cs +++ b/BepInEx.Core/Contract/PluginInfo.cs @@ -18,9 +18,9 @@ public class PluginInfo : ICacheable public BepInPlugin Metadata { get; internal set; } /// - /// The loader used to load this plugin, null if it is a provider + /// The load context used to load this plugin, null if it is a provider /// - public IPluginLoader Loader { get; internal set; } + public IPluginLoadContext LoadContext { get; internal set; } /// /// Collection of attributes that describe what processes the plugin can run on. @@ -52,7 +52,7 @@ public class PluginInfo : ICacheable /// public override string ToString() => $"{Metadata?.Name} {Metadata?.Version}"; - public virtual void Save(BinaryWriter bw) + void ICacheable.Save(BinaryWriter bw) { bw.Write(TypeName); @@ -78,7 +78,7 @@ public class PluginInfo : ICacheable bw.Write(TargettedBepInExVersion.ToString(4)); } - public virtual void Load(BinaryReader br) + void ICacheable.Load(BinaryReader br) { TypeName = br.ReadString(); diff --git a/BepInEx.PatcherProvider/BepInExPatcherLoader.cs b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs similarity index 87% rename from BepInEx.PatcherProvider/BepInExPatcherLoader.cs rename to BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs index 0221434..590678c 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherLoader.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherLoadContext.cs @@ -2,7 +2,7 @@ namespace BepInEx.PatcherProvider; -public class BepInExPatcherLoader : IPluginLoader +public class BepInExPatcherLoadContext : IPluginLoadContext { public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } diff --git a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs index 24ef3cc..fb2ff8c 100644 --- a/BepInEx.PatcherProvider/BepInExPatcherProvider.cs +++ b/BepInEx.PatcherProvider/BepInExPatcherProvider.cs @@ -12,15 +12,15 @@ public class BepInExPatcherProvider : BasePatcherProvider { private static readonly Dictionary AssemblyLocationsByFilename = new(); - public override IList GetPatchers() + public override IList GetPatchers() { - var loaders = new List(); + var loadContexts = new List(); foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PatcherPluginPath), "*.dll", SearchOption.AllDirectories)) { try { AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll)); - loaders.Add(new BepInExPatcherLoader + loadContexts.Add(new BepInExPatcherLoadContext { AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), AssemblyIdentifier = dll @@ -32,7 +32,7 @@ public class BepInExPatcherProvider : BasePatcherProvider } } - return loaders; + return loadContexts; } public override Assembly ResolveAssembly(string name) diff --git a/BepInEx.PluginProvider/BepInExPluginLoader.cs b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs similarity index 87% rename from BepInEx.PluginProvider/BepInExPluginLoader.cs rename to BepInEx.PluginProvider/BepInExPluginLoadContext.cs index a71f3d7..a39f649 100644 --- a/BepInEx.PluginProvider/BepInExPluginLoader.cs +++ b/BepInEx.PluginProvider/BepInExPluginLoadContext.cs @@ -2,7 +2,7 @@ namespace BepInEx.PluginProvider; -public class BepInExPluginLoader : IPluginLoader +public class BepInExPluginLoadContext : IPluginLoadContext { public string AssemblyIdentifier { get; internal set; } public string AssemblyHash { get; internal set; } diff --git a/BepInEx.PluginProvider/BepInExPluginProvider.cs b/BepInEx.PluginProvider/BepInExPluginProvider.cs index 569b304..b2f151c 100644 --- a/BepInEx.PluginProvider/BepInExPluginProvider.cs +++ b/BepInEx.PluginProvider/BepInExPluginProvider.cs @@ -11,15 +11,15 @@ public class BepInExPluginProvider : BasePluginProvider { private static readonly Dictionary AssemblyLocationsByFilename = new(); - public override IList GetPlugins() + public override IList GetPlugins() { - var loaders = new List(); + var loadContexts = new List(); foreach (var dll in Directory.GetFiles(Path.GetFullPath(Paths.PluginPath), "*.dll", SearchOption.AllDirectories)) { try { AssemblyLocationsByFilename.Add(Path.GetFileNameWithoutExtension(dll), Path.GetDirectoryName(dll)); - loaders.Add(new BepInExPluginLoader + loadContexts.Add(new BepInExPluginLoadContext { AssemblyHash = File.GetLastWriteTimeUtc(dll).ToString("O"), AssemblyIdentifier = dll @@ -31,7 +31,7 @@ public class BepInExPluginProvider : BasePluginProvider } } - return loaders; + return loadContexts; } public override Assembly ResolveAssembly(string name) diff --git a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs index d24755b..50cff04 100644 --- a/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs +++ b/BepInEx.Preloader.Core/Patching/AssemblyPatcher.cs @@ -25,7 +25,7 @@ public class AssemblyPatcher : IDisposable private Func assemblyLoader; - private readonly List loaders = new(); + private readonly List loadContexts = new(); private readonly List providers = new(); public AssemblyPatcher(Func assemblyLoader) @@ -69,7 +69,7 @@ public class AssemblyPatcher : IDisposable AppDomain.CurrentDomain.AssemblyResolve -= PatcherProvidersAssemblyResolver; } - private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, IPluginLoader loader, string assemblyPath) + private PatcherPluginMetadata ToPatcherPlugin(TypeDefinition type, IPluginLoadContext loadContext, string assemblyPath) { if (type.IsInterface || type.IsAbstract && !type.IsSealed) return null; @@ -86,11 +86,11 @@ public class AssemblyPatcher : IDisposable } if (type.IsSubtypeOf(typeof(BasePatcherProvider))) - return GetPatcherInfo(type, loader, PatcherProviderPluginInfoAttribute.FromCecilType(type)); - return GetPatcherInfo(type, loader, PatcherPluginInfoAttribute.FromCecilType(type)); + return GetPatcherInfo(type, loadContext, PatcherProviderPluginInfoAttribute.FromCecilType(type)); + return GetPatcherInfo(type, loadContext, PatcherPluginInfoAttribute.FromCecilType(type)); } - private PatcherPluginMetadata GetPatcherInfo(TypeDefinition type, IPluginLoader loader, PatcherPluginInfoAttribute metadata) + private PatcherPluginMetadata GetPatcherInfo(TypeDefinition type, IPluginLoadContext loadContext, PatcherPluginInfoAttribute metadata) { // Perform checks that will prevent the plugin from being loaded in ALL cases if (metadata == null) @@ -121,7 +121,7 @@ public class AssemblyPatcher : IDisposable return new PatcherPluginMetadata { TypeName = type.FullName, - Loader = loader + LoadContext = loadContext }; } @@ -164,7 +164,7 @@ public class AssemblyPatcher : IDisposable var instance = (BasePatcherProvider) Activator.CreateInstance(type); providers.Add(instance); - loaders.AddRange(instance.GetPatchers()); + loadContexts.AddRange(instance.GetPatchers()); } catch (Exception e) { @@ -174,7 +174,7 @@ public class AssemblyPatcher : IDisposable } } - var patchers = TypeLoader.GetPluginsFromLoaders(loaders, ToPatcherPlugin, HasPatcherType); + var patchers = TypeLoader.GetPluginsFromLoadContexts(loadContexts, ToPatcherPlugin, HasPatcherType); var sortedPatchers = new List(); @@ -182,7 +182,7 @@ public class AssemblyPatcher : IDisposable foreach (var patcherPlugin in patchers) { - var ass = Assembly.Load(patcherPlugin.Loader.GetAssemblyData()); + var ass = Assembly.Load(patcherPlugin.LoadContext.GetAssemblyData()); var patchDefinitions = LoadPatcherPlugin(ass, patcherPlugin); sortedPatchers.AddRange(patchDefinitions); @@ -196,11 +196,22 @@ public class AssemblyPatcher : IDisposable private Assembly PatcherProvidersAssemblyResolver(object sender, ResolveEventArgs args) { - foreach (BasePatcherProvider provider in providers) + BasePatcherProvider currentProvider = null; + try { - var ass = provider.ResolveAssembly(args.Name); - if (ass != null) - return ass; + foreach (BasePatcherProvider provider in providers) + { + currentProvider = provider; + var ass = provider.ResolveAssembly(args.Name); + if (ass != null) + return ass; + } + } + catch (Exception e) + { + Logger.Log(LogLevel.Error, $"The assembly resolver of the patcher provider [{currentProvider?.Info.GUID}] " + + $"threw an exception while resolving {args.Name}: {e}"); + return null; } return null; diff --git a/BepInEx.Preloader.Core/Patching/Attributes.cs b/BepInEx.Preloader.Core/Patching/Attributes.cs index a6e5675..1b2ccab 100644 --- a/BepInEx.Preloader.Core/Patching/Attributes.cs +++ b/BepInEx.Preloader.Core/Patching/Attributes.cs @@ -91,7 +91,7 @@ public class PatcherProviderPluginInfoAttribute : PatcherPluginInfoAttribute { { } - internal static PatcherProviderPluginInfoAttribute FromCecilType(TypeDefinition td) + internal new static PatcherProviderPluginInfoAttribute FromCecilType(TypeDefinition td) { var attr = MetadataHelper.GetCustomAttributes(td, false).FirstOrDefault(); diff --git a/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs b/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs index c1ef817..08c3503 100644 --- a/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs +++ b/BepInEx.Preloader.Core/Patching/BasePatcherProvider.cs @@ -39,8 +39,8 @@ public abstract class BasePatcherProvider /// /// Obtains a list of assemblies containing patchers to load /// - /// A list of loaders, one per assembly - public abstract IList GetPatchers(); + /// A list of load context, one per assembly + public abstract IList GetPatchers(); /// /// A custom assembly resolver that can be used by this provider to resolve assemblies diff --git a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs index 9d8b963..ceb88b7 100644 --- a/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs +++ b/BepInEx.Preloader.Core/Patching/PatcherPluginMetadata.cs @@ -14,9 +14,9 @@ internal class PatcherPluginMetadata : ICacheable public string TypeName { get; set; } = string.Empty; /// - /// The loader used to load this patcher, null if it is a provider + /// The load context used to load this patcher, null if it is a provider /// - public IPluginLoader Loader { get; set; } + public IPluginLoadContext LoadContext { get; set; } /// public void Save(BinaryWriter bw) => bw.Write(TypeName);