1 #region Copyright & License Information 2 /* 3 * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) 4 * This file is part of OpenRA, which is free software. It is made 5 * available to you under the terms of the GNU General Public License 6 * as published by the Free Software Foundation, either version 3 of 7 * the License, or (at your option) any later version. For more 8 * information, see COPYING. 9 */ 10 #endregion 11 12 using System; 13 using System.Collections.Generic; 14 using System.IO; 15 using System.Linq; 16 using OpenRA.FileSystem; 17 using OpenRA.Graphics; 18 using OpenRA.Widgets; 19 using FS = OpenRA.FileSystem.FileSystem; 20 21 namespace OpenRA 22 { 23 public sealed class ModData : IDisposable 24 { 25 public readonly Manifest Manifest; 26 public readonly ObjectCreator ObjectCreator; 27 public readonly WidgetLoader WidgetLoader; 28 public readonly MapCache MapCache; 29 public readonly IPackageLoader[] PackageLoaders; 30 public readonly ISoundLoader[] SoundLoaders; 31 public readonly ISpriteLoader[] SpriteLoaders; 32 public readonly ISpriteSequenceLoader SpriteSequenceLoader; 33 public readonly IModelSequenceLoader ModelSequenceLoader; 34 public readonly HotkeyManager Hotkeys; 35 public ILoadScreen LoadScreen { get; private set; } 36 public CursorProvider CursorProvider { get; private set; } 37 public FS ModFiles; 38 public IReadOnlyFileSystem DefaultFileSystem { get { return ModFiles; } } 39 40 readonly Lazy<Ruleset> defaultRules; 41 public Ruleset DefaultRules { get { return defaultRules.Value; } } 42 43 readonly Lazy<IReadOnlyDictionary<string, TileSet>> defaultTileSets; 44 public IReadOnlyDictionary<string, TileSet> DefaultTileSets { get { return defaultTileSets.Value; } } 45 46 readonly Lazy<IReadOnlyDictionary<string, SequenceProvider>> defaultSequences; 47 public IReadOnlyDictionary<string, SequenceProvider> DefaultSequences { get { return defaultSequences.Value; } } 48 ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false)49 public ModData(Manifest mod, InstalledMods mods, bool useLoadScreen = false) 50 { 51 Languages = new string[0]; 52 53 // Take a local copy of the manifest 54 Manifest = new Manifest(mod.Id, mod.Package); 55 ObjectCreator = new ObjectCreator(Manifest, mods); 56 PackageLoaders = ObjectCreator.GetLoaders<IPackageLoader>(Manifest.PackageFormats, "package"); 57 58 ModFiles = new FS(mod.Id, mods, PackageLoaders); 59 ModFiles.LoadFromManifest(Manifest); 60 Manifest.LoadCustomData(ObjectCreator); 61 62 if (useLoadScreen) 63 { 64 LoadScreen = ObjectCreator.CreateObject<ILoadScreen>(Manifest.LoadScreen.Value); 65 LoadScreen.Init(this, Manifest.LoadScreen.ToDictionary(my => my.Value)); 66 LoadScreen.Display(); 67 } 68 69 WidgetLoader = new WidgetLoader(this); 70 MapCache = new MapCache(this); 71 72 SoundLoaders = ObjectCreator.GetLoaders<ISoundLoader>(Manifest.SoundFormats, "sound"); 73 SpriteLoaders = ObjectCreator.GetLoaders<ISpriteLoader>(Manifest.SpriteFormats, "sprite"); 74 75 var sequenceFormat = Manifest.Get<SpriteSequenceFormat>(); 76 var sequenceLoader = ObjectCreator.FindType(sequenceFormat.Type + "Loader"); 77 var sequenceCtor = sequenceLoader != null ? sequenceLoader.GetConstructor(new[] { typeof(ModData) }) : null; 78 if (sequenceLoader == null || !sequenceLoader.GetInterfaces().Contains(typeof(ISpriteSequenceLoader)) || sequenceCtor == null) 79 throw new InvalidOperationException("Unable to find a sequence loader for type '{0}'.".F(sequenceFormat.Type)); 80 81 SpriteSequenceLoader = (ISpriteSequenceLoader)sequenceCtor.Invoke(new[] { this }); 82 SpriteSequenceLoader.OnMissingSpriteError = s => Log.Write("debug", s); 83 84 var modelFormat = Manifest.Get<ModelSequenceFormat>(); 85 var modelLoader = ObjectCreator.FindType(modelFormat.Type + "Loader"); 86 var modelCtor = modelLoader != null ? modelLoader.GetConstructor(new[] { typeof(ModData) }) : null; 87 if (modelLoader == null || !modelLoader.GetInterfaces().Contains(typeof(IModelSequenceLoader)) || modelCtor == null) 88 throw new InvalidOperationException("Unable to find a model loader for type '{0}'.".F(modelFormat.Type)); 89 90 ModelSequenceLoader = (IModelSequenceLoader)modelCtor.Invoke(new[] { this }); 91 ModelSequenceLoader.OnMissingModelError = s => Log.Write("debug", s); 92 93 Hotkeys = new HotkeyManager(ModFiles, Game.Settings.Keys, Manifest); 94 95 defaultRules = Exts.Lazy(() => Ruleset.LoadDefaults(this)); 96 defaultTileSets = Exts.Lazy(() => 97 { 98 var items = new Dictionary<string, TileSet>(); 99 100 foreach (var file in Manifest.TileSets) 101 { 102 var t = new TileSet(DefaultFileSystem, file); 103 items.Add(t.Id, t); 104 } 105 106 return (IReadOnlyDictionary<string, TileSet>)(new ReadOnlyDictionary<string, TileSet>(items)); 107 }); 108 109 defaultSequences = Exts.Lazy(() => 110 { 111 var items = DefaultTileSets.ToDictionary(t => t.Key, t => new SequenceProvider(DefaultFileSystem, this, t.Value, null)); 112 return (IReadOnlyDictionary<string, SequenceProvider>)(new ReadOnlyDictionary<string, SequenceProvider>(items)); 113 }); 114 115 initialThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; 116 } 117 118 // HACK: Only update the loading screen if we're in the main thread. 119 int initialThreadId; HandleLoadingProgress()120 internal void HandleLoadingProgress() 121 { 122 if (LoadScreen != null && IsOnMainThread) 123 LoadScreen.Display(); 124 } 125 126 internal bool IsOnMainThread { get { return System.Threading.Thread.CurrentThread.ManagedThreadId == initialThreadId; } } 127 InitializeLoaders(IReadOnlyFileSystem fileSystem)128 public void InitializeLoaders(IReadOnlyFileSystem fileSystem) 129 { 130 // all this manipulation of static crap here is nasty and breaks 131 // horribly when you use ModData in unexpected ways. 132 ChromeMetrics.Initialize(this); 133 ChromeProvider.Initialize(this); 134 135 Game.Sound.Initialize(SoundLoaders, fileSystem); 136 137 CursorProvider = new CursorProvider(this); 138 } 139 140 public IEnumerable<string> Languages { get; private set; } 141 LoadTranslations(Map map)142 void LoadTranslations(Map map) 143 { 144 var selectedTranslations = new Dictionary<string, string>(); 145 var defaultTranslations = new Dictionary<string, string>(); 146 147 if (!Manifest.Translations.Any()) 148 { 149 Languages = new string[0]; 150 return; 151 } 152 153 var yaml = MiniYaml.Load(map, Manifest.Translations, map.TranslationDefinitions); 154 Languages = yaml.Select(t => t.Key).ToArray(); 155 156 foreach (var y in yaml) 157 { 158 if (y.Key == Game.Settings.Graphics.Language) 159 selectedTranslations = y.Value.ToDictionary(my => my.Value ?? ""); 160 else if (y.Key == Game.Settings.Graphics.DefaultLanguage) 161 defaultTranslations = y.Value.ToDictionary(my => my.Value ?? ""); 162 } 163 164 var translations = new Dictionary<string, string>(); 165 foreach (var tkv in defaultTranslations.Concat(selectedTranslations)) 166 { 167 if (translations.ContainsKey(tkv.Key)) 168 continue; 169 if (selectedTranslations.ContainsKey(tkv.Key)) 170 translations.Add(tkv.Key, selectedTranslations[tkv.Key]); 171 else 172 translations.Add(tkv.Key, tkv.Value); 173 } 174 175 FieldLoader.SetTranslations(translations); 176 } 177 PrepareMap(string uid)178 public Map PrepareMap(string uid) 179 { 180 if (LoadScreen != null) 181 LoadScreen.Display(); 182 183 if (MapCache[uid].Status != MapStatus.Available) 184 throw new InvalidDataException("Invalid map uid: {0}".F(uid)); 185 186 Map map; 187 using (new Support.PerfTimer("Map")) 188 map = new Map(this, MapCache[uid].Package); 189 190 LoadTranslations(map); 191 192 // Reinitialize all our assets 193 InitializeLoaders(map); 194 195 // Load music with map assets mounted 196 using (new Support.PerfTimer("Map.Music")) 197 foreach (var entry in map.Rules.Music) 198 entry.Value.Load(map); 199 200 return map; 201 } 202 Dispose()203 public void Dispose() 204 { 205 if (LoadScreen != null) 206 LoadScreen.Dispose(); 207 MapCache.Dispose(); 208 209 if (ObjectCreator != null) 210 ObjectCreator.Dispose(); 211 } 212 } 213 214 public interface ILoadScreen : IDisposable 215 { 216 /// <summary>Initializes the loadscreen with yaml data from the LoadScreen block in mod.yaml.</summary> Init(ModData m, Dictionary<string, string> info)217 void Init(ModData m, Dictionary<string, string> info); 218 219 /// <summary>Called at arbitrary times during mod load to rerender the loadscreen.</summary> Display()220 void Display(); 221 222 /// <summary> 223 /// Called before loading the mod assets. 224 /// Returns false if mod loading should be aborted (e.g. switching to another mod instead). 225 /// </summary> BeforeLoad()226 bool BeforeLoad(); 227 228 /// <summary>Called when the engine expects to connect to a server/replay or load the shellmap.</summary> StartGame(Arguments args)229 void StartGame(Arguments args); 230 } 231 } 232