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