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.Diagnostics.CodeAnalysis;
15 using System.IO;
16 using System.Linq;
17 using System.Net;
18 using System.Text;
19 using System.Threading;
20 using OpenRA.FileFormats;
21 using OpenRA.FileSystem;
22 using OpenRA.Graphics;
23 using OpenRA.Primitives;
24 
25 namespace OpenRA
26 {
27 	public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError }
28 
29 	// Used for grouping maps in the UI
30 	[Flags]
31 	public enum MapClassification
32 	{
33 		Unknown = 0,
34 		System = 1,
35 		User = 2,
36 		Remote = 4
37 	}
38 
39 	// Used for verifying map availability in the lobby
40 	public enum MapRuleStatus { Unknown, Cached, Invalid }
41 
42 	[SuppressMessage("StyleCop.CSharp.NamingRules",
43 		"SA1307:AccessibleFieldsMustBeginWithUpperCaseLetter",
44 		Justification = "Fields names must match the with the remote API.")]
45 	[SuppressMessage("StyleCop.CSharp.NamingRules",
46 		"SA1304:NonPrivateReadonlyFieldsMustBeginWithUpperCaseLetter",
47 		Justification = "Fields names must match the with the remote API.")]
48 	[SuppressMessage("StyleCop.CSharp.NamingRules",
49 		"SA1310:FieldNamesMustNotContainUnderscore",
50 		Justification = "Fields names must match the with the remote API.")]
51 	public class RemoteMapData
52 	{
53 		public readonly string title;
54 		public readonly string author;
55 		public readonly string[] categories;
56 		public readonly int players;
57 		public readonly Rectangle bounds;
58 		public readonly short[] spawnpoints = { };
59 		public readonly MapGridType map_grid_type;
60 		public readonly string minimap;
61 		public readonly bool downloading;
62 		public readonly string tileset;
63 		public readonly string rules;
64 		public readonly string players_block;
65 	}
66 
67 	public class MapPreview : IDisposable, IReadOnlyFileSystem
68 	{
69 		/// <summary>Wrapper that enables map data to be replaced in an atomic fashion</summary>
70 		class InnerData
71 		{
72 			public string Title;
73 			public string[] Categories;
74 			public string Author;
75 			public string TileSet;
76 			public MapPlayers Players;
77 			public int PlayerCount;
78 			public CPos[] SpawnPoints;
79 			public MapGridType GridType;
80 			public Rectangle Bounds;
81 			public Png Preview;
82 			public MapStatus Status;
83 			public MapClassification Class;
84 			public MapVisibility Visibility;
85 
86 			Lazy<Ruleset> rules;
87 			public Ruleset Rules { get { return rules != null ? rules.Value : null; } }
88 			public bool InvalidCustomRules { get; private set; }
89 			public bool DefinesUnsafeCustomRules { get; private set; }
90 			public bool RulesLoaded { get; private set; }
91 
SetRulesetGenerator(ModData modData, Func<Pair<Ruleset, bool>> generator)92 			public void SetRulesetGenerator(ModData modData, Func<Pair<Ruleset, bool>> generator)
93 			{
94 				InvalidCustomRules = false;
95 				RulesLoaded = false;
96 				DefinesUnsafeCustomRules = false;
97 
98 				// Note: multiple threads may try to access the value at the same time
99 				// We rely on the thread-safety guarantees given by Lazy<T> to prevent race conitions.
100 				// If you're thinking about replacing this, then you must be careful to keep this safe.
101 				rules = Exts.Lazy(() =>
102 				{
103 					if (generator == null)
104 						return Ruleset.LoadDefaultsForTileSet(modData, TileSet);
105 
106 					try
107 					{
108 						var ret = generator();
109 						DefinesUnsafeCustomRules = ret.Second;
110 						return ret.First;
111 					}
112 					catch (Exception e)
113 					{
114 						Log.Write("debug", "Failed to load rules for `{0}` with error :{1}", Title, e.Message);
115 						InvalidCustomRules = true;
116 						return Ruleset.LoadDefaultsForTileSet(modData, TileSet);
117 					}
118 					finally
119 					{
120 						RulesLoaded = true;
121 					}
122 				});
123 			}
124 
Clone()125 			public InnerData Clone()
126 			{
127 				return (InnerData)MemberwiseClone();
128 			}
129 		}
130 
131 		static readonly CPos[] NoSpawns = new CPos[] { };
132 		readonly MapCache cache;
133 		readonly ModData modData;
134 
135 		public readonly string Uid;
136 		public IReadOnlyPackage Package { get; private set; }
137 		IReadOnlyPackage parentPackage;
138 
139 		volatile InnerData innerData;
140 
141 		public string Title { get { return innerData.Title; } }
142 		public string[] Categories { get { return innerData.Categories; } }
143 		public string Author { get { return innerData.Author; } }
144 		public string TileSet { get { return innerData.TileSet; } }
145 		public MapPlayers Players { get { return innerData.Players; } }
146 		public int PlayerCount { get { return innerData.PlayerCount; } }
147 		public CPos[] SpawnPoints { get { return innerData.SpawnPoints; } }
148 		public MapGridType GridType { get { return innerData.GridType; } }
149 		public Rectangle Bounds { get { return innerData.Bounds; } }
150 		public Png Preview { get { return innerData.Preview; } }
151 		public MapStatus Status { get { return innerData.Status; } }
152 		public MapClassification Class { get { return innerData.Class; } }
153 		public MapVisibility Visibility { get { return innerData.Visibility; } }
154 
155 		public Ruleset Rules { get { return innerData.Rules; } }
156 		public bool InvalidCustomRules { get { return innerData.InvalidCustomRules; } }
157 		public bool RulesLoaded { get { return innerData.RulesLoaded; } }
158 		public bool DefinesUnsafeCustomRules
159 		{
160 			get
161 			{
162 				// Force lazy rules to be evaluated
163 				var force = innerData.Rules;
164 				return innerData.DefinesUnsafeCustomRules;
165 			}
166 		}
167 
168 		Download download;
169 		public long DownloadBytes { get; private set; }
170 		public int DownloadPercentage { get; private set; }
171 
172 		Sprite minimap;
173 		bool generatingMinimap;
GetMinimap()174 		public Sprite GetMinimap()
175 		{
176 			if (minimap != null)
177 				return minimap;
178 
179 			if (!generatingMinimap && Status == MapStatus.Available)
180 			{
181 				generatingMinimap = true;
182 				cache.CacheMinimap(this);
183 			}
184 
185 			return null;
186 		}
187 
SetMinimap(Sprite minimap)188 		internal void SetMinimap(Sprite minimap)
189 		{
190 			this.minimap = minimap;
191 			generatingMinimap = false;
192 		}
193 
MapPreview(ModData modData, string uid, MapGridType gridType, MapCache cache)194 		public MapPreview(ModData modData, string uid, MapGridType gridType, MapCache cache)
195 		{
196 			this.cache = cache;
197 			this.modData = modData;
198 
199 			Uid = uid;
200 			innerData = new InnerData
201 			{
202 				Title = "Unknown Map",
203 				Categories = new[] { "Unknown" },
204 				Author = "Unknown Author",
205 				TileSet = "unknown",
206 				Players = null,
207 				PlayerCount = 0,
208 				SpawnPoints = NoSpawns,
209 				GridType = gridType,
210 				Bounds = Rectangle.Empty,
211 				Preview = null,
212 				Status = MapStatus.Unavailable,
213 				Class = MapClassification.Unknown,
214 				Visibility = MapVisibility.Lobby,
215 			};
216 		}
217 
UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType)218 		public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType)
219 		{
220 			Dictionary<string, MiniYaml> yaml;
221 			using (var yamlStream = p.GetStream("map.yaml"))
222 			{
223 				if (yamlStream == null)
224 					throw new FileNotFoundException("Required file map.yaml not present in this map");
225 
226 				yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, "map.yaml")).ToDictionary();
227 			}
228 
229 			Package = p;
230 			parentPackage = parent;
231 
232 			var newData = innerData.Clone();
233 			newData.GridType = gridType;
234 			newData.Class = classification;
235 
236 			MiniYaml temp;
237 			if (yaml.TryGetValue("MapFormat", out temp))
238 			{
239 				var format = FieldLoader.GetValue<int>("MapFormat", temp.Value);
240 				if (format != Map.SupportedMapFormat)
241 					throw new InvalidDataException("Map format {0} is not supported.".F(format));
242 			}
243 
244 			if (yaml.TryGetValue("Title", out temp))
245 				newData.Title = temp.Value;
246 
247 			if (yaml.TryGetValue("Categories", out temp))
248 				newData.Categories = FieldLoader.GetValue<string[]>("Categories", temp.Value);
249 
250 			if (yaml.TryGetValue("Tileset", out temp))
251 				newData.TileSet = temp.Value;
252 
253 			if (yaml.TryGetValue("Author", out temp))
254 				newData.Author = temp.Value;
255 
256 			if (yaml.TryGetValue("Bounds", out temp))
257 				newData.Bounds = FieldLoader.GetValue<Rectangle>("Bounds", temp.Value);
258 
259 			if (yaml.TryGetValue("Visibility", out temp))
260 				newData.Visibility = FieldLoader.GetValue<MapVisibility>("Visibility", temp.Value);
261 
262 			string requiresMod = string.Empty;
263 			if (yaml.TryGetValue("RequiresMod", out temp))
264 				requiresMod = temp.Value;
265 
266 			newData.Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ?
267 				MapStatus.Available : MapStatus.Unavailable;
268 
269 			try
270 			{
271 				// Actor definitions may change if the map format changes
272 				MiniYaml actorDefinitions;
273 				if (yaml.TryGetValue("Actors", out actorDefinitions))
274 				{
275 					var spawns = new List<CPos>();
276 					foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn"))
277 					{
278 						var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
279 						spawns.Add(s.InitDict.Get<LocationInit>().Value(null));
280 					}
281 
282 					newData.SpawnPoints = spawns.ToArray();
283 				}
284 				else
285 					newData.SpawnPoints = new CPos[0];
286 			}
287 			catch (Exception)
288 			{
289 				newData.SpawnPoints = new CPos[0];
290 				newData.Status = MapStatus.Unavailable;
291 			}
292 
293 			try
294 			{
295 				// Player definitions may change if the map format changes
296 				MiniYaml playerDefinitions;
297 				if (yaml.TryGetValue("Players", out playerDefinitions))
298 				{
299 					newData.Players = new MapPlayers(playerDefinitions.Nodes);
300 					newData.PlayerCount = newData.Players.Players.Count(x => x.Value.Playable);
301 				}
302 			}
303 			catch (Exception)
304 			{
305 				newData.Status = MapStatus.Unavailable;
306 			}
307 
308 			newData.SetRulesetGenerator(modData, () =>
309 			{
310 				var ruleDefinitions = LoadRuleSection(yaml, "Rules");
311 				var weaponDefinitions = LoadRuleSection(yaml, "Weapons");
312 				var voiceDefinitions = LoadRuleSection(yaml, "Voices");
313 				var musicDefinitions = LoadRuleSection(yaml, "Music");
314 				var notificationDefinitions = LoadRuleSection(yaml, "Notifications");
315 				var sequenceDefinitions = LoadRuleSection(yaml, "Sequences");
316 				var modelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences");
317 				var rules = Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions,
318 					voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions, modelSequenceDefinitions);
319 				var flagged = Ruleset.DefinesUnsafeCustomRules(modData, this, ruleDefinitions,
320 					weaponDefinitions, voiceDefinitions, notificationDefinitions, sequenceDefinitions);
321 				return Pair.New(rules, flagged);
322 			});
323 
324 			if (p.Contains("map.png"))
325 				using (var dataStream = p.GetStream("map.png"))
326 					newData.Preview = new Png(dataStream);
327 
328 			// Assign the new data atomically
329 			innerData = newData;
330 		}
331 
LoadRuleSection(Dictionary<string, MiniYaml> yaml, string section)332 		MiniYaml LoadRuleSection(Dictionary<string, MiniYaml> yaml, string section)
333 		{
334 			MiniYaml node;
335 			if (!yaml.TryGetValue(section, out node))
336 				return null;
337 
338 			return node;
339 		}
340 
PreloadRules()341 		public void PreloadRules()
342 		{
343 			var unused = Rules;
344 		}
345 
UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)346 		public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
347 		{
348 			var newData = innerData.Clone();
349 			newData.Status = status;
350 			newData.Class = MapClassification.Remote;
351 
352 			if (status == MapStatus.DownloadAvailable)
353 			{
354 				try
355 				{
356 					var r = FieldLoader.Load<RemoteMapData>(yaml);
357 
358 					// Map download has been disabled server side
359 					if (!r.downloading)
360 					{
361 						newData.Status = MapStatus.Unavailable;
362 						return;
363 					}
364 
365 					newData.Title = r.title;
366 					newData.Categories = r.categories;
367 					newData.Author = r.author;
368 					newData.PlayerCount = r.players;
369 					newData.Bounds = r.bounds;
370 					newData.TileSet = r.tileset;
371 
372 					var spawns = new CPos[r.spawnpoints.Length / 2];
373 					for (var j = 0; j < r.spawnpoints.Length; j += 2)
374 						spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
375 					newData.SpawnPoints = spawns;
376 					newData.GridType = r.map_grid_type;
377 					try
378 					{
379 						newData.Preview = new Png(new MemoryStream(Convert.FromBase64String(r.minimap)));
380 					}
381 					catch (Exception e)
382 					{
383 						Log.Write("debug", "Failed parsing mapserver minimap response: {0}", e);
384 						newData.Preview = null;
385 					}
386 
387 					var playersString = Encoding.UTF8.GetString(Convert.FromBase64String(r.players_block));
388 					newData.Players = new MapPlayers(MiniYaml.FromString(playersString));
389 
390 					newData.SetRulesetGenerator(modData, () =>
391 					{
392 						var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
393 						var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
394 						var ruleDefinitions = LoadRuleSection(rulesYaml, "Rules");
395 						var weaponDefinitions = LoadRuleSection(rulesYaml, "Weapons");
396 						var voiceDefinitions = LoadRuleSection(rulesYaml, "Voices");
397 						var musicDefinitions = LoadRuleSection(rulesYaml, "Music");
398 						var notificationDefinitions = LoadRuleSection(rulesYaml, "Notifications");
399 						var sequenceDefinitions = LoadRuleSection(rulesYaml, "Sequences");
400 						var modelSequenceDefinitions = LoadRuleSection(rulesYaml, "ModelSequences");
401 						var rules = Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions,
402 							voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions, modelSequenceDefinitions);
403 						var flagged = Ruleset.DefinesUnsafeCustomRules(modData, this, ruleDefinitions,
404 							weaponDefinitions, voiceDefinitions, notificationDefinitions, sequenceDefinitions);
405 						return Pair.New(rules, flagged);
406 					});
407 				}
408 				catch (Exception e)
409 				{
410 					Log.Write("debug", "Failed parsing mapserver response: {0}", e);
411 				}
412 
413 				// Commit updated data before running the callbacks
414 				innerData = newData;
415 
416 				if (innerData.Preview != null)
417 					cache.CacheMinimap(this);
418 
419 				if (parseMetadata != null)
420 					parseMetadata(this);
421 			}
422 
423 			// Update the status and class unconditionally
424 			innerData = newData;
425 		}
426 
Install(string mapRepositoryUrl, Action onSuccess)427 		public void Install(string mapRepositoryUrl, Action onSuccess)
428 		{
429 			if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading)
430 				return;
431 
432 			innerData.Status = MapStatus.Downloading;
433 			var installLocation = cache.MapLocations.FirstOrDefault(p => p.Value == MapClassification.User);
434 			if (installLocation.Key == null || !(installLocation.Key is IReadWritePackage))
435 			{
436 				Log.Write("debug", "Map install directory not found");
437 				innerData.Status = MapStatus.DownloadError;
438 				return;
439 			}
440 
441 			var mapInstallPackage = installLocation.Key as IReadWritePackage;
442 			new Thread(() =>
443 			{
444 				// Request the filename from the server
445 				// Run in a worker thread to avoid network delays
446 				var mapUrl = mapRepositoryUrl + Uid;
447 				var mapFilename = string.Empty;
448 				try
449 				{
450 					var request = WebRequest.Create(mapUrl);
451 					request.Method = "HEAD";
452 					using (var res = request.GetResponse())
453 					{
454 						// Map not found
455 						if (res.Headers["Content-Disposition"] == null)
456 						{
457 							innerData.Status = MapStatus.DownloadError;
458 							return;
459 						}
460 
461 						mapFilename = res.Headers["Content-Disposition"].Replace("attachment; filename = ", "");
462 					}
463 
464 					Action<DownloadProgressChangedEventArgs> onDownloadProgress = i => { DownloadBytes = i.BytesReceived; DownloadPercentage = i.ProgressPercentage; };
465 					Action<DownloadDataCompletedEventArgs> onDownloadComplete = i =>
466 					{
467 						download = null;
468 
469 						if (i.Error != null)
470 						{
471 							Log.Write("debug", "Remote map download failed with error: {0}", Download.FormatErrorMessage(i.Error));
472 							Log.Write("debug", "URL was: {0}", mapUrl);
473 
474 							innerData.Status = MapStatus.DownloadError;
475 							return;
476 						}
477 
478 						mapInstallPackage.Update(mapFilename, i.Result);
479 						Log.Write("debug", "Downloaded map to '{0}'", mapFilename);
480 						Game.RunAfterTick(() =>
481 						{
482 							var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
483 							if (package == null)
484 								innerData.Status = MapStatus.DownloadError;
485 							else
486 							{
487 								UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType);
488 								onSuccess();
489 							}
490 						});
491 					};
492 
493 					download = new Download(mapUrl, onDownloadProgress, onDownloadComplete);
494 				}
495 				catch (Exception e)
496 				{
497 					Console.WriteLine(e.Message);
498 					innerData.Status = MapStatus.DownloadError;
499 				}
500 			}).Start();
501 		}
502 
CancelInstall()503 		public void CancelInstall()
504 		{
505 			if (download == null)
506 				return;
507 
508 			download.CancelAsync();
509 			download = null;
510 		}
511 
Invalidate()512 		public void Invalidate()
513 		{
514 			innerData.Status = MapStatus.Unavailable;
515 		}
516 
Dispose()517 		public void Dispose()
518 		{
519 			if (Package != null)
520 			{
521 				Package.Dispose();
522 				Package = null;
523 			}
524 		}
525 
Delete()526 		public void Delete()
527 		{
528 			Invalidate();
529 			var deleteFromPackage = parentPackage as IReadWritePackage;
530 			if (deleteFromPackage != null)
531 				deleteFromPackage.Delete(Package.Name);
532 		}
533 
IReadOnlyFileSystem.Open(string filename)534 		Stream IReadOnlyFileSystem.Open(string filename)
535 		{
536 			// Explicit package paths never refer to a map
537 			if (!filename.Contains("|") && Package.Contains(filename))
538 				return Package.GetStream(filename);
539 
540 			return modData.DefaultFileSystem.Open(filename);
541 		}
542 
IReadOnlyFileSystem.TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename)543 		bool IReadOnlyFileSystem.TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename)
544 		{
545 			// Packages aren't supported inside maps
546 			return modData.DefaultFileSystem.TryGetPackageContaining(path, out package, out filename);
547 		}
548 
IReadOnlyFileSystem.TryOpen(string filename, out Stream s)549 		bool IReadOnlyFileSystem.TryOpen(string filename, out Stream s)
550 		{
551 			// Explicit package paths never refer to a map
552 			if (!filename.Contains("|"))
553 			{
554 				s = Package.GetStream(filename);
555 				if (s != null)
556 					return true;
557 			}
558 
559 			return modData.DefaultFileSystem.TryOpen(filename, out s);
560 		}
561 
IReadOnlyFileSystem.Exists(string filename)562 		bool IReadOnlyFileSystem.Exists(string filename)
563 		{
564 			// Explicit package paths never refer to a map
565 			if (!filename.Contains("|") && Package.Contains(filename))
566 				return true;
567 
568 			return modData.DefaultFileSystem.Exists(filename);
569 		}
570 
IReadOnlyFileSystem.IsExternalModFile(string filename)571 		bool IReadOnlyFileSystem.IsExternalModFile(string filename)
572 		{
573 			// Explicit package paths never refer to a map
574 			if (filename.Contains("|"))
575 				return modData.DefaultFileSystem.IsExternalModFile(filename);
576 
577 			return false;
578 		}
579 	}
580 }
581