#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Primitives; using OpenRA.Video; using OpenRA.Widgets; using FS = OpenRA.FileSystem.FileSystem; namespace OpenRA { public interface IGlobalModData { } public sealed class ModData : IDisposable { public readonly Manifest Manifest; public readonly ObjectCreator ObjectCreator; public readonly WidgetLoader WidgetLoader; public readonly MapCache MapCache; public readonly IPackageLoader[] PackageLoaders; public readonly ISoundLoader[] SoundLoaders; public readonly ISpriteLoader[] SpriteLoaders; public readonly ISpriteSequenceLoader SpriteSequenceLoader; public readonly IVideoLoader[] VideoLoaders; public readonly HotkeyManager Hotkeys; public readonly IFileSystemLoader FileSystemLoader; public readonly FrozenDictionary Cursors; public ILoadScreen LoadScreen { get; } public FS ModFiles; public IReadOnlyFileSystem DefaultFileSystem => ModFiles; readonly Lazy defaultRules; public Ruleset DefaultRules => defaultRules.Value; readonly Lazy> defaultTerrainInfo; public IReadOnlyDictionary DefaultTerrainInfo => defaultTerrainInfo.Value; readonly TypeDictionary modules = []; public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false) { Languages = []; // Take a local copy of the manifest Manifest = new Manifest(mod.Id, mod.Package); ObjectCreator = new ObjectCreator(Manifest, mods); PackageLoaders = ObjectCreator.GetLoaders(Manifest.PackageFormats, "package"); ModFiles = new FS(mod.Id, mods, PackageLoaders); FileSystemLoader = ObjectCreator.GetLoader(Manifest.FileSystem.Value, "filesystem"); FieldLoader.Load(FileSystemLoader, Manifest.FileSystem); FileSystemLoader.Mount(Manifest, ModFiles, ObjectCreator); ModFiles.TrimExcess(); foreach (var kv in Manifest.GlobalModData) { var t = ObjectCreator.FindType(kv.Key); if (t == null || !typeof(IGlobalModData).IsAssignableFrom(t)) throw new InvalidDataException($"`{kv.Key}` is not a valid mod manifest entry."); IGlobalModData module; var ctor = t.GetConstructor([typeof(MiniYaml)]); if (ctor != null) { // Class has opted-in to DIY initialization module = (IGlobalModData)ctor.Invoke([kv.Value]); } else { // Automatically load the child nodes using FieldLoader module = ObjectCreator.CreateObject(kv.Key); FieldLoader.Load(module, kv.Value); } modules.Add(module); } FluentProvider.Initialize(Manifest, DefaultFileSystem); if (useLoadScreen) { LoadScreen = ObjectCreator.CreateObject(Manifest.LoadScreen.Value); LoadScreen.Init(Manifest, DefaultFileSystem); LoadScreen.Display(); } WidgetLoader = new WidgetLoader(Manifest, DefaultFileSystem); MapCache = new MapCache(Manifest, ModFiles); SoundLoaders = ObjectCreator.GetLoaders(Manifest.SoundFormats, "sound"); SpriteLoaders = ObjectCreator.GetLoaders(Manifest.SpriteFormats, "sprite"); VideoLoaders = ObjectCreator.GetLoaders(Manifest.VideoFormats, "video"); SpriteSequenceLoader = ObjectCreator.GetLoader(Manifest.SpriteSequenceFormat, "sequence"); Hotkeys = new HotkeyManager(ModFiles, ObjectCreator, Manifest); Cursors = ParseCursors(Manifest, DefaultFileSystem); defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this)); defaultTerrainInfo = Exts.Lazy(() => { var terrainType = ObjectCreator.FindType(Manifest.TerrainFormat + "Loader"); var terrainCtor = terrainType?.GetConstructor([typeof(ModData)]); if (terrainType == null || !terrainType.GetInterfaces().Contains(typeof(ITerrainLoader)) || terrainCtor == null) throw new InvalidOperationException($"Unable to find a terrain loader for type '{Manifest.TerrainFormat}'."); var items = new Dictionary(); var terrainLoader = (ITerrainLoader)terrainCtor.Invoke([this]); foreach (var file in Manifest.TileSets) { var t = terrainLoader.ParseTerrain(DefaultFileSystem, file); items.Add(t.Id, t); } return (IReadOnlyDictionary)new ReadOnlyDictionary(items); }); initialThreadId = Environment.CurrentManagedThreadId; } static FrozenDictionary ParseCursors(Manifest manifest, IReadOnlyFileSystem fileSystem) { var stringPool = new HashSet(); // Reuse common strings in YAML var sequenceYaml = MiniYaml.Merge(manifest.Cursors.Select( s => MiniYaml.FromStream(fileSystem.Open(s), s, stringPool: stringPool))); var cursors = new Dictionary(); foreach (var node in sequenceYaml) if (node.Key == "Cursors") foreach (var fileNode in node.Value.Nodes) foreach (var sequenceNode in fileNode.Value.Nodes) cursors.Add(sequenceNode.Key, new CursorSequence( sequenceNode.Key, fileNode.Key, fileNode.Value.Value, sequenceNode.Value)); return cursors.ToFrozenDictionary(); } // HACK: Only update the loading screen if we're in the main thread. readonly int initialThreadId; internal void HandleLoadingProgress() { if (LoadScreen != null && IsOnMainThread) LoadScreen.Display(); } internal bool IsOnMainThread => Environment.CurrentManagedThreadId == initialThreadId; public void InitializeLoaders(IReadOnlyFileSystem fileSystem) { // all this manipulation of static crap here is nasty and breaks // horribly when you use ModData in unexpected ways. ChromeMetrics.Initialize(this); ChromeProvider.Initialize(this); Ui.Initialize(this); FluentProvider.Initialize(Manifest, fileSystem); Game.Sound.Initialize(SoundLoaders, fileSystem); } public IEnumerable Languages { get; } public void PrepareMap(Map map) { LoadScreen?.Display(); // Reinitialize all our assets InitializeLoaders(map); map.Sequences.LoadSprites(); // Load music with map assets mounted using (new Support.PerfTimer("Map.Music")) foreach (var entry in map.Rules.Music) entry.Value.Load(map); } public MiniYamlNode[][] GetRulesYaml() { var stringPool = new HashSet(); // Reuse common strings in YAML return Manifest.Rules.Select(s => MiniYaml.FromStream(DefaultFileSystem.Open(s), s, stringPool: stringPool).ToArray()).ToArray(); } /// Load a cached IGlobalModData instance. public T GetOrCreate() where T : IGlobalModData { var module = modules.GetOrDefault(); // Lazily create the default values if not explicitly defined. if (module == null) { module = (T)ObjectCreator.CreateBasic(typeof(T)); modules.Add(module); } return module; } public T GetOrNull() where T : IGlobalModData { return modules.GetOrDefault(); } public T GetSettings() where T : SettingsModule { return Game.Settings.GetOrCreate(ObjectCreator, Manifest.Id); } public void Dispose() { LoadScreen?.Dispose(); MapCache.Dispose(); ObjectCreator?.Dispose(); foreach (var module in modules) { var disposableModule = module as IDisposable; disposableModule?.Dispose(); } } } public interface ILoadScreen : IDisposable { /// Initializes the loadscreen with yaml data from the LoadScreen block in mod.yaml. void Init(Manifest manifest, IReadOnlyFileSystem fileSystem); /// Called at arbitrary times during mod load to rerender the loadscreen. void Display(); /// /// Called before loading the mod assets. /// Returns false if mod loading should be aborted (e.g. switching to another mod instead). /// bool BeforeLoad(ModData modData); /// Called when the engine expects to connect to a server/replay or load the shellmap. void StartGame(Arguments args); } public interface IFileSystemLoader { void Mount(Manifest manifest, FS fileSystem, ObjectCreator objectCreator); } }