diff --git a/OpenRA.Game/FileSystem/FileSystem.cs b/OpenRA.Game/FileSystem/FileSystem.cs index aa700a49e8..6a2b17b05a 100644 --- a/OpenRA.Game/FileSystem/FileSystem.cs +++ b/OpenRA.Game/FileSystem/FileSystem.cs @@ -21,8 +21,6 @@ namespace OpenRA.FileSystem { public IEnumerable MountedPackages { get { return mountedPackages.Keys; } } readonly Dictionary mountedPackages = new Dictionary(); - - static readonly Dictionary AssemblyCache = new Dictionary(); Cache> fileIndex = new Cache>(_ => new List()); public IReadWritePackage CreatePackage(string filename, Dictionary content) @@ -217,30 +215,5 @@ namespace OpenRA.FileSystem else return mountedPackages.Keys.Any(f => f.Contains(name)); } - - public static Assembly ResolveAssembly(object sender, ResolveEventArgs e) - { - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - if (assembly.FullName == e.Name) - return assembly; - - var frags = e.Name.Split(','); - var filename = frags[0] + ".dll"; - - Assembly a; - if (AssemblyCache.TryGetValue(filename, out a)) - return a; - - if (Game.ModData.ModFiles.Exists(filename)) - using (var s = Game.ModData.ModFiles.Open(filename)) - { - var buf = s.ReadBytes((int)s.Length); - a = Assembly.Load(buf); - AssemblyCache.Add(filename, a); - return a; - } - - return null; - } } } diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index bb3e8a57e4..37cb6858f8 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -235,8 +235,6 @@ namespace OpenRA { Console.WriteLine("Platform is {0}", Platform.CurrentPlatform); - AppDomain.CurrentDomain.AssemblyResolve += FileSystem.FileSystem.ResolveAssembly; - InitializeSettings(args); Log.AddChannel("perf", "perf.log"); diff --git a/OpenRA.Game/ModData.cs b/OpenRA.Game/ModData.cs index caad343c15..f4523460b7 100644 --- a/OpenRA.Game/ModData.cs +++ b/OpenRA.Game/ModData.cs @@ -43,11 +43,7 @@ namespace OpenRA Manifest = new Manifest(mod); ModFiles.LoadFromManifest(Manifest); - // Allow mods to load types from the core Game assembly, and any additional assemblies they specify. - var assemblies = new[] { typeof(Game).Assembly } - .Concat(Manifest.Assemblies.Select(path => Assembly.Load(ModFiles.Open(path).ReadAllBytes()))) - .ToList(); - ObjectCreator = new ObjectCreator(assemblies); + ObjectCreator = new ObjectCreator(Manifest, ModFiles); Manifest.LoadCustomData(ObjectCreator); if (useLoadScreen) diff --git a/OpenRA.Game/ObjectCreator.cs b/OpenRA.Game/ObjectCreator.cs index ded80b27b2..1bd7327dd1 100644 --- a/OpenRA.Game/ObjectCreator.cs +++ b/OpenRA.Game/ObjectCreator.cs @@ -10,23 +10,74 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; +using System.Security.Cryptography; +using OpenRA.FileSystem; using OpenRA.Primitives; namespace OpenRA { public class ObjectCreator { + // .NET does not support unloading assemblies, so mod libraries will leak across mod changes. + // This tracks the assemblies that have been loaded since game start so that we don't load multiple copies + static readonly Dictionary ResolvedAssemblies = new Dictionary(); + readonly Cache typeCache; readonly Cache ctorCache; readonly Pair[] assemblies; - public ObjectCreator(IEnumerable sourceAssemblies) + public ObjectCreator(Assembly a) { typeCache = new Cache(FindType); ctorCache = new Cache(GetCtor); - assemblies = sourceAssemblies.SelectMany(asm => asm.GetNamespaces().Select(ns => Pair.New(asm, ns))).ToArray(); + assemblies = a.GetNamespaces().Select(ns => Pair.New(a, ns)).ToArray(); + } + + public ObjectCreator(Manifest manifest, FileSystem.FileSystem modFiles) + { + typeCache = new Cache(FindType); + ctorCache = new Cache(GetCtor); + + // Allow mods to load types from the core Game assembly, and any additional assemblies they specify. + var assemblyList = new List() { typeof(Game).Assembly }; + foreach (var path in manifest.Assemblies) + { + var data = modFiles.Open(path).ReadAllBytes(); + + // .NET doesn't provide any way of querying the metadata of an assembly without either: + // (a) loading duplicate data into the application domain, breaking the world. + // (b) crashing if the assembly has already been loaded. + // We can't check the internal name of the assembly, so we'll work off the data instead + string hash; + using (var ms = new MemoryStream(data)) + using (var csp = SHA1.Create()) + hash = new string(csp.ComputeHash(data).SelectMany(a => a.ToString("x2")).ToArray()); + + Assembly assembly; + if (!ResolvedAssemblies.TryGetValue(hash, out assembly)) + { + assembly = Assembly.Load(data); + ResolvedAssemblies.Add(hash, assembly); + } + + assemblyList.Add(assembly); + } + + AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly; + assemblies = assemblyList.SelectMany(asm => asm.GetNamespaces().Select(ns => Pair.New(asm, ns))).ToArray(); + AppDomain.CurrentDomain.AssemblyResolve -= ResolveAssembly; + } + + Assembly ResolveAssembly(object sender, ResolveEventArgs e) + { + foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) + if (a.FullName == e.Name) + return a; + + return assemblies.Select(a => a.First).FirstOrDefault(a => a.FullName == e.Name); } public static Action MissingTypeAction = diff --git a/OpenRA.Test/OpenRA.Game/ActorInfoTest.cs b/OpenRA.Test/OpenRA.Game/ActorInfoTest.cs index 7d288db924..10f0e65b7d 100644 --- a/OpenRA.Test/OpenRA.Game/ActorInfoTest.cs +++ b/OpenRA.Test/OpenRA.Game/ActorInfoTest.cs @@ -108,7 +108,7 @@ namespace OpenRA.Test var yaml = MiniYaml.Merge(sources.Select(s => MiniYaml.FromString(s))); var allUnits = yaml.ToDictionary(node => node.Key, node => node.Value); var unit = allUnits[name]; - var creator = new ObjectCreator(new[] { typeof(ActorInfoTest).Assembly }); + var creator = new ObjectCreator(typeof(ActorInfoTest).Assembly); return new ActorInfo(creator, name, unit); } } diff --git a/OpenRA.Utility/Program.cs b/OpenRA.Utility/Program.cs index 82304cdeff..8e29915a21 100644 --- a/OpenRA.Utility/Program.cs +++ b/OpenRA.Utility/Program.cs @@ -42,8 +42,6 @@ namespace OpenRA.Utility return; } - AppDomain.CurrentDomain.AssemblyResolve += FileSystem.FileSystem.ResolveAssembly; - Log.AddChannel("perf", null); Log.AddChannel("debug", null);