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