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