#region Copyright & License Information /* * Copyright 2007-2019 The OpenRA Developers (see AUTHORS) * 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.Generic; using System.IO; using System.Linq; using OpenRA.FileSystem; using OpenRA.Graphics; using OpenRA.Widgets; using FS = OpenRA.FileSystem.FileSystem; namespace OpenRA { 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 IModelSequenceLoader ModelSequenceLoader; public readonly HotkeyManager Hotkeys; public ILoadScreen LoadScreen { get; private set; } public CursorProvider CursorProvider { get; private set; } public FS ModFiles; public IReadOnlyFileSystem DefaultFileSystem { get { return ModFiles; } } readonly Lazy defaultRules; public Ruleset DefaultRules { get { return defaultRules.Value; } } readonly Lazy> defaultTileSets; public IReadOnlyDictionary DefaultTileSets { get { return defaultTileSets.Value; } } readonly Lazy> defaultSequences; public IReadOnlyDictionary DefaultSequences { get { return defaultSequences.Value; } } public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false) { Languages = new string[0]; // 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); ModFiles.LoadFromManifest(Manifest); Manifest.LoadCustomData(ObjectCreator); if (useLoadScreen) { LoadScreen = ObjectCreator.CreateObject(Manifest.LoadScreen.Value); LoadScreen.Init(this, Manifest.LoadScreen.ToDictionary(my => my.Value)); LoadScreen.Display(); } WidgetLoader = new WidgetLoader(this); MapCache = new MapCache(this); SoundLoaders = ObjectCreator.GetLoaders(Manifest.SoundFormats, "sound"); SpriteLoaders = ObjectCreator.GetLoaders(Manifest.SpriteFormats, "sprite"); var sequenceFormat = Manifest.Get(); var sequenceLoader = ObjectCreator.FindType(sequenceFormat.Type + "Loader"); var sequenceCtor = sequenceLoader != null ? sequenceLoader.GetConstructor(new[] { typeof(ModData) }) : null; if (sequenceLoader == null || !sequenceLoader.GetInterfaces().Contains(typeof(ISpriteSequenceLoader)) || sequenceCtor == null) throw new InvalidOperationException("Unable to find a sequence loader for type '{0}'.".F(sequenceFormat.Type)); SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this }); SpriteSequenceLoader.OnMissingSpriteError = s => Log.Write("debug", s); var modelFormat = Manifest.Get(); var modelLoader = ObjectCreator.FindType(modelFormat.Type + "Loader"); var modelCtor = modelLoader != null ? modelLoader.GetConstructor(new[] { typeof(ModData) }) : null; if (modelLoader == null || !modelLoader.GetInterfaces().Contains(typeof(IModelSequenceLoader)) || modelCtor == null) throw new InvalidOperationException("Unable to find a model loader for type '{0}'.".F(modelFormat.Type)); ModelSequenceLoader = (IModelSequenceLoader)modelCtor.Invoke(new[] { this }); ModelSequenceLoader.OnMissingModelError = s => Log.Write("debug", s); Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest); defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this)); defaultTileSets = Exts.Lazy(() => { var items = new Dictionary(); foreach (var file in Manifest.TileSets) { var t = new TileSet(DefaultFileSystem, file); items.Add(t.Id, t); } return (IReadOnlyDictionary)(new ReadOnlyDictionary(items)); }); defaultSequences = Exts.Lazy(() => { var items = DefaultTileSets.ToDictionary(t => t.Key, t => new SequenceProvider(DefaultFileSystem, this, t.Value, null)); return (IReadOnlyDictionary)(new ReadOnlyDictionary(items)); }); initialThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; } // HACK: Only update the loading screen if we're in the main thread. int initialThreadId; internal void HandleLoadingProgress() { if (LoadScreen != null && IsOnMainThread) LoadScreen.Display(); } internal bool IsOnMainThread { get { return System.Threading.Thread.CurrentThread.ManagedThreadId == 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); Game.Sound.Initialize(SoundLoaders, fileSystem); CursorProvider = new CursorProvider(this); } public IEnumerable Languages { get; private set; } void LoadTranslations(Map map) { var selectedTranslations = new Dictionary(); var defaultTranslations = new Dictionary(); if (!Manifest.Translations.Any()) { Languages = new string[0]; return; } var yaml = MiniYaml.Load(map, Manifest.Translations, map.TranslationDefinitions); Languages = yaml.Select(t => t.Key).ToArray(); foreach (var y in yaml) { if (y.Key == Game.Settings.Graphics.Language) selectedTranslations = y.Value.ToDictionary(my => my.Value ?? ""); else if (y.Key == Game.Settings.Graphics.DefaultLanguage) defaultTranslations = y.Value.ToDictionary(my => my.Value ?? ""); } var translations = new Dictionary(); foreach (var tkv in defaultTranslations.Concat(selectedTranslations)) { if (translations.ContainsKey(tkv.Key)) continue; if (selectedTranslations.ContainsKey(tkv.Key)) translations.Add(tkv.Key, selectedTranslations[tkv.Key]); else translations.Add(tkv.Key, tkv.Value); } FieldLoader.SetTranslations(translations); } public Map PrepareMap(string uid) { if (LoadScreen != null) LoadScreen.Display(); if (MapCache[uid].Status != MapStatus.Available) throw new InvalidDataException("Invalid map uid: {0}".F(uid)); Map map; using (new Support.PerfTimer("Map")) map = new Map(this, MapCache[uid].Package); LoadTranslations(map); // Reinitialize all our assets InitializeLoaders(map); // Load music with map assets mounted using (new Support.PerfTimer("Map.Music")) foreach (var entry in map.Rules.Music) entry.Value.Load(map); return map; } public void Dispose() { if (LoadScreen != null) LoadScreen.Dispose(); MapCache.Dispose(); if (ObjectCreator != null) ObjectCreator.Dispose(); } } public interface ILoadScreen : IDisposable { /// Initializes the loadscreen with yaml data from the LoadScreen block in mod.yaml. void Init(ModData m, Dictionary info); /// 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(); /// Called when the engine expects to connect to a server/replay or load the shellmap. void StartGame(Arguments args); } }