diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs
index 25e68b641c..d3a0a5ec9e 100644
--- a/OpenRA.Game/Map/MapPreview.cs
+++ b/OpenRA.Game/Map/MapPreview.cs
@@ -55,6 +55,65 @@ namespace OpenRA
public class MapPreview : IDisposable, IReadOnlyFileSystem
{
+ /// Wrapper that enables map data to be replaced in an atomic fashion
+ 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;
+ public bool SuitableForInitialMap;
+
+ Lazy rules;
+ public Ruleset Rules { get { return rules != null ? rules.Value : null; } }
+ public bool InvalidCustomRules { get; private set; }
+ public bool RulesLoaded { get; private set; }
+
+ public void SetRulesetGenerator(ModData modData, Func generator)
+ {
+ InvalidCustomRules = false;
+ RulesLoaded = false;
+
+ // Note: multiple threads may try to access the value at the same time
+ // We rely on the thread-safety guarantees given by Lazy 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
+ {
+ return generator();
+ }
+ 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[] { };
MapCache cache;
ModData modData;
@@ -63,25 +122,26 @@ namespace OpenRA
public IReadOnlyPackage Package { get; private set; }
IReadOnlyPackage parentPackage;
- public string Title { get; private set; }
- public string[] Categories { get; private set; }
- public string Author { get; private set; }
- public string TileSet { get; private set; }
- public MapPlayers Players { get; private set; }
- public int PlayerCount { get; private set; }
- public CPos[] SpawnPoints { get; private set; }
- public MapGridType GridType { get; private set; }
- public Rectangle Bounds { get; private set; }
- public Bitmap Preview { get; private set; }
- public MapStatus Status { get; private set; }
- public MapClassification Class { get; private set; }
- public MapVisibility Visibility { get; private set; }
- public bool SuitableForInitialMap { get; private set; }
+ volatile InnerData innerData;
- Lazy rules;
- public Ruleset Rules { get { return rules != null ? rules.Value : null; } }
- public bool InvalidCustomRules { get; private set; }
- public bool RulesLoaded { get; private set; }
+ 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 bool SuitableForInitialMap { get { return innerData.SuitableForInitialMap; } }
+
+ public Ruleset Rules { get { return innerData.Rules; } }
+ public bool InvalidCustomRules { get { return innerData.InvalidCustomRules; } }
+ public bool RulesLoaded { get { return innerData.RulesLoaded; } }
Download download;
public long DownloadBytes { get; private set; }
@@ -115,16 +175,23 @@ namespace OpenRA
this.modData = modData;
Uid = uid;
- Title = "Unknown Map";
- Categories = new[] { "Unknown" };
- Author = "Unknown Author";
- PlayerCount = 0;
- Bounds = Rectangle.Empty;
- SpawnPoints = NoSpawns;
- GridType = gridType;
- Status = MapStatus.Unavailable;
- Class = MapClassification.Unknown;
- Visibility = MapVisibility.Lobby;
+ 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,
+ SuitableForInitialMap = false
+ };
}
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType)
@@ -140,8 +207,10 @@ namespace OpenRA
Package = p;
parentPackage = parent;
- GridType = gridType;
- Class = classification;
+
+ var newData = innerData.Clone();
+ newData.GridType = gridType;
+ newData.Class = classification;
MiniYaml temp;
if (yaml.TryGetValue("MapFormat", out temp))
@@ -152,23 +221,29 @@ namespace OpenRA
}
if (yaml.TryGetValue("Title", out temp))
- Title = temp.Value;
+ newData.Title = temp.Value;
+
if (yaml.TryGetValue("Categories", out temp))
- Categories = FieldLoader.GetValue("Categories", temp.Value);
+ newData.Categories = FieldLoader.GetValue("Categories", temp.Value);
+
if (yaml.TryGetValue("Tileset", out temp))
- TileSet = temp.Value;
+ newData.TileSet = temp.Value;
+
if (yaml.TryGetValue("Author", out temp))
- Author = temp.Value;
+ newData.Author = temp.Value;
+
if (yaml.TryGetValue("Bounds", out temp))
- Bounds = FieldLoader.GetValue("Bounds", temp.Value);
+ newData.Bounds = FieldLoader.GetValue("Bounds", temp.Value);
+
if (yaml.TryGetValue("Visibility", out temp))
- Visibility = FieldLoader.GetValue("Visibility", temp.Value);
+ newData.Visibility = FieldLoader.GetValue("Visibility", temp.Value);
string requiresMod = string.Empty;
if (yaml.TryGetValue("RequiresMod", out temp))
requiresMod = temp.Value;
- Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ? MapStatus.Available : MapStatus.Unavailable;
+ newData.Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ?
+ MapStatus.Available : MapStatus.Unavailable;
try
{
@@ -183,15 +258,15 @@ namespace OpenRA
spawns.Add(s.InitDict.Get().Value(null));
}
- SpawnPoints = spawns.ToArray();
+ newData.SpawnPoints = spawns.ToArray();
}
else
- SpawnPoints = new CPos[0];
+ newData.SpawnPoints = new CPos[0];
}
catch (Exception)
{
- SpawnPoints = new CPos[0];
- Status = MapStatus.Unavailable;
+ newData.SpawnPoints = new CPos[0];
+ newData.Status = MapStatus.Unavailable;
}
try
@@ -200,47 +275,34 @@ namespace OpenRA
MiniYaml playerDefinitions;
if (yaml.TryGetValue("Players", out playerDefinitions))
{
- Players = new MapPlayers(playerDefinitions.Nodes);
- PlayerCount = Players.Players.Count(x => x.Value.Playable);
- SuitableForInitialMap = EvaluateUserFriendliness(Players.Players);
+ newData.Players = new MapPlayers(playerDefinitions.Nodes);
+ newData.PlayerCount = newData.Players.Players.Count(x => x.Value.Playable);
+ newData.SuitableForInitialMap = EvaluateUserFriendliness(newData.Players.Players);
}
}
catch (Exception)
{
- Status = MapStatus.Unavailable;
+ newData.Status = MapStatus.Unavailable;
}
- // Note: multiple threads may try to access the value at the same time
- // We rely on the thread-safety guarantees given by Lazy to prevent race conitions.
- // If you're thinking about replacing this, then you must be careful to keep this safe.
- rules = Exts.Lazy(() =>
+ newData.SetRulesetGenerator(modData, () =>
{
- try
- {
- 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");
- return Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions,
- voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions);
- }
- 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;
- }
+ 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");
+ return Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions,
+ voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions);
});
if (p.Contains("map.png"))
using (var dataStream = p.GetStream("map.png"))
- Preview = new Bitmap(dataStream);
+ newData.Preview = new Bitmap(dataStream);
+
+ // Assign the new data atomically
+ innerData = newData;
}
MiniYaml LoadRuleSection(Dictionary yaml, string section)
@@ -274,45 +336,48 @@ namespace OpenRA
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml)
{
- // Update on the main thread to ensure consistency
- Game.RunAfterTick(() =>
+ var newData = innerData.Clone();
+ newData.Status = status;
+ newData.Class = MapClassification.Remote;
+
+ if (status == MapStatus.DownloadAvailable)
{
- if (status == MapStatus.DownloadAvailable)
+ try
{
- try
+ var r = FieldLoader.Load(yaml);
+
+ // Map download has been disabled server side
+ if (!r.downloading)
{
- var r = FieldLoader.Load(yaml);
-
- // Map download has been disabled server side
- if (!r.downloading)
- {
- Status = MapStatus.Unavailable;
- return;
- }
-
- Title = r.title;
- Categories = r.categories;
- Author = r.author;
- PlayerCount = r.players;
- Bounds = r.bounds;
-
- 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]);
- SpawnPoints = spawns;
- GridType = r.map_grid_type;
-
- Preview = new Bitmap(new MemoryStream(Convert.FromBase64String(r.minimap)));
+ newData.Status = MapStatus.Unavailable;
+ return;
}
- catch (Exception) { }
- if (Preview != null)
- cache.CacheMinimap(this);
+ newData.Title = r.title;
+ newData.Categories = r.categories;
+ newData.Author = r.author;
+ newData.PlayerCount = r.players;
+ newData.Bounds = r.bounds;
+
+ 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;
+
+ newData.Preview = new Bitmap(new MemoryStream(Convert.FromBase64String(r.minimap)));
}
+ catch (Exception) { }
- Status = status;
- Class = MapClassification.Remote;
- });
+ // Commit updated data before running the callbacks
+ innerData = newData;
+
+ if (innerData.Preview != null)
+ cache.CacheMinimap(this);
+ }
+
+ // Update the status and class unconditionally
+ innerData = newData;
}
public void Install(Action onSuccess)
@@ -320,12 +385,12 @@ namespace OpenRA
if (Status != MapStatus.DownloadAvailable || !Game.Settings.Game.AllowDownloading)
return;
- Status = MapStatus.Downloading;
+ 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");
- Status = MapStatus.DownloadError;
+ innerData.Status = MapStatus.DownloadError;
return;
}
@@ -346,7 +411,7 @@ namespace OpenRA
// Map not found
if (res.Headers["Content-Disposition"] == null)
{
- Status = MapStatus.DownloadError;
+ innerData.Status = MapStatus.DownloadError;
return;
}
@@ -363,7 +428,7 @@ namespace OpenRA
Log.Write("debug", "Remote map download failed with error: {0}", i.Error != null ? i.Error.Message : "cancelled");
Log.Write("debug", "URL was: {0}", mapUrl);
- Status = MapStatus.DownloadError;
+ innerData.Status = MapStatus.DownloadError;
return;
}
@@ -382,7 +447,7 @@ namespace OpenRA
catch (Exception e)
{
Console.WriteLine(e.Message);
- Status = MapStatus.DownloadError;
+ innerData.Status = MapStatus.DownloadError;
}
}).Start();
}
@@ -398,7 +463,7 @@ namespace OpenRA
public void Invalidate()
{
- Status = MapStatus.Unavailable;
+ innerData.Status = MapStatus.Unavailable;
}
public void Dispose()