From 81f22f8871ff6e42a3c12ca286e0c1a8fadfcff7 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Mon, 28 Mar 2016 13:47:14 +0100 Subject: [PATCH 1/4] Remove Game.RunAfterTick from UpdateRemoteSearch. RunAfterTick isn't available from the Server. --- OpenRA.Game/Map/MapPreview.cs | 283 +++++++++++++++++++++------------- 1 file changed, 174 insertions(+), 109 deletions(-) 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() From 65f7d460257263827339378f0cd51615747b633b Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 27 Mar 2016 13:11:37 +0100 Subject: [PATCH 2/4] Parse map rules and players from remote maps. --- OpenRA.Game/Map/MapCache.cs | 4 +-- OpenRA.Game/Map/MapPreview.cs | 28 +++++++++++++++++-- .../Widgets/Logic/Lobby/LobbyLogic.cs | 27 ++++++++++++------ 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/OpenRA.Game/Map/MapCache.cs b/OpenRA.Game/Map/MapCache.cs index a05bd954f3..de966d3252 100644 --- a/OpenRA.Game/Map/MapCache.cs +++ b/OpenRA.Game/Map/MapCache.cs @@ -112,7 +112,7 @@ namespace OpenRA } } - public void QueryRemoteMapDetails(IEnumerable uids) + public void QueryRemoteMapDetails(IEnumerable uids, Action mapDetailsReceived = null) { var maps = uids.Distinct() .Select(uid => previews[uid]) @@ -144,7 +144,7 @@ namespace OpenRA { var yaml = MiniYaml.FromString(data); foreach (var kv in yaml) - maps[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value); + maps[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived); } catch { diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index d3a0a5ec9e..e21fa9df54 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -17,6 +17,7 @@ using System.Drawing; using System.IO; using System.Linq; using System.Net; +using System.Text; using System.Threading; using OpenRA.FileSystem; using OpenRA.Graphics; @@ -51,6 +52,9 @@ namespace OpenRA 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 @@ -334,7 +338,7 @@ namespace OpenRA return true; } - public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml) + public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action parseMetadata = null) { var newData = innerData.Clone(); newData.Status = status; @@ -358,14 +362,31 @@ namespace OpenRA 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; - newData.Preview = new Bitmap(new MemoryStream(Convert.FromBase64String(r.minimap))); + + 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"); + return Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions, + voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions); + }); } catch (Exception) { } @@ -374,6 +395,9 @@ namespace OpenRA if (innerData.Preview != null) cache.CacheMinimap(this); + + if (parseMetadata != null) + parseMetadata(this); } // Update the status and class unconditionally diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index e6bfdfbf78..ad4f177d31 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -326,7 +326,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var startGameButton = lobby.GetOrNull("START_GAME_BUTTON"); if (startGameButton != null) { - startGameButton.IsDisabled = () => configurationDisabled() || + startGameButton.IsDisabled = () => configurationDisabled() || Map.Status != MapStatus.Available || orderManager.LobbyInfo.Slots.Any(sl => sl.Value.Required && orderManager.LobbyInfo.ClientInSlot(sl.Key) == null) || (orderManager.LobbyInfo.GlobalSettings.DisableSingleplayer && orderManager.LobbyInfo.IsSinglePlayer); @@ -464,8 +464,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic }; startingUnits.IsDisabled = () => configurationDisabled() || startUnitsLocked.Update(Map); - startingUnits.GetText = () => Map.Status != MapStatus.Available || - !Map.RulesLoaded || startUnitsLocked.Update(Map) ? "Not Available" : className(orderManager.LobbyInfo.GlobalSettings.StartingUnitsClass); + startingUnits.GetText = () => !Map.RulesLoaded || startUnitsLocked.Update(Map) ? + "Not Available" : className(orderManager.LobbyInfo.GlobalSettings.StartingUnitsClass); startingUnits.OnMouseDown = _ => { var classes = startUnitsInfos.Update(Map).Select(a => a.Class).Distinct(); @@ -496,8 +496,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic map => map.Rules.Actors["player"].TraitInfo()); startingCash.IsDisabled = () => configurationDisabled() || playerResources.Update(Map).DefaultCashLocked; - startingCash.GetText = () => Map.Status != MapStatus.Available || - !Map.RulesLoaded || playerResources.Update(Map).DefaultCashLocked ? "Not Available" : "${0}".F(orderManager.LobbyInfo.GlobalSettings.StartingCash); + startingCash.GetText = () => !Map.RulesLoaded || playerResources.Update(Map).DefaultCashLocked ? + "Not Available" : "${0}".F(orderManager.LobbyInfo.GlobalSettings.StartingCash); startingCash.OnMouseDown = _ => { var options = playerResources.Update(Map).SelectableCash.Select(c => new DropDownOption @@ -533,8 +533,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic techLevelDescription.IsVisible = techLevel.IsVisible; techLevel.IsDisabled = () => configurationDisabled() || mapOptions.Update(Map).TechLevelLocked; - techLevel.GetText = () => Map.Status != MapStatus.Available || - !Map.RulesLoaded || mapOptions.Update(Map).TechLevelLocked ? "Not Available" : "{0}".F(orderManager.LobbyInfo.GlobalSettings.TechLevel); + techLevel.GetText = () => !Map.RulesLoaded || mapOptions.Update(Map).TechLevelLocked ? + "Not Available" : "{0}".F(orderManager.LobbyInfo.GlobalSettings.TechLevel); techLevel.OnMouseDown = _ => { var options = techLevels.Update(Map).Select(c => new DropDownOption @@ -776,6 +776,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic return true; } + void LoadMapPreviewRules(MapPreview map) + { + new Task(() => + { + // Force map rules to be loaded on this background thread + var unused = map.Rules; + }).Start(); + } + void UpdateCurrentMap() { var uid = orderManager.LobbyInfo.GlobalSettings.Map; @@ -814,8 +823,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic }); }).Start(); } + else if (Map.Status == MapStatus.DownloadAvailable) + LoadMapPreviewRules(Map); else if (Game.Settings.Game.AllowDownloading) - modData.MapCache.QueryRemoteMapDetails(new[] { uid }); + modData.MapCache.QueryRemoteMapDetails(new[] { uid }, LoadMapPreviewRules); } void UpdatePlayerList() From 4ec1369553d4141b402751d3a81606108f9e2a61 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Mon, 28 Mar 2016 13:12:44 +0100 Subject: [PATCH 3/4] Let servers query map details from the resource centre. --- OpenRA.Game/Map/MapCache.cs | 7 +- OpenRA.Game/Settings.cs | 3 + .../ServerTraits/LobbyCommands.cs | 145 ++++++++++-------- 3 files changed, 93 insertions(+), 62 deletions(-) diff --git a/OpenRA.Game/Map/MapCache.cs b/OpenRA.Game/Map/MapCache.cs index de966d3252..a1ac688605 100644 --- a/OpenRA.Game/Map/MapCache.cs +++ b/OpenRA.Game/Map/MapCache.cs @@ -112,7 +112,7 @@ namespace OpenRA } } - public void QueryRemoteMapDetails(IEnumerable uids, Action mapDetailsReceived = null) + public void QueryRemoteMapDetails(IEnumerable uids, Action mapDetailsReceived = null, Action queryFailed = null) { var maps = uids.Distinct() .Select(uid => previews[uid]) @@ -136,6 +136,9 @@ namespace OpenRA foreach (var p in maps.Values) p.UpdateRemoteSearch(MapStatus.Unavailable, null); + if (queryFailed != null) + queryFailed(); + return; } @@ -149,6 +152,8 @@ namespace OpenRA catch { Log.Write("debug", "Can't parse remote map search data:\n{0}", data); + if (queryFailed != null) + queryFailed(); } }; diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index ce24c64b20..6454c6f220 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -74,6 +74,9 @@ namespace OpenRA [Desc("Disallow games where only one player plays with bots.")] public bool DisableSinglePlayer = false; + [Desc("Query map information from the Resource Center if they are not available locally.")] + public bool QueryMapRepository = true; + public string TimestampFormat = "s"; public ServerSettings Clone() diff --git a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs index 671cb96374..330abbefc8 100644 --- a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Threading; using OpenRA.Graphics; using OpenRA.Mods.Common.Traits; using OpenRA.Network; @@ -338,64 +339,87 @@ namespace OpenRA.Mods.Common.Server return true; } - if (server.ModData.MapCache[s].Status != MapStatus.Available) + var lastMap = server.LobbyInfo.GlobalSettings.Map; + Action selectMap = map => { - server.SendOrderTo(conn, "Message", "Map was not found on server."); - return true; - } + // Make sure the map hasn't changed in the meantime + if (server.LobbyInfo.GlobalSettings.Map != lastMap) + return; - server.LobbyInfo.GlobalSettings.Map = s; + server.LobbyInfo.GlobalSettings.Map = map.Uid; - var oldSlots = server.LobbyInfo.Slots.Keys.ToArray(); - LoadMap(server); + var oldSlots = server.LobbyInfo.Slots.Keys.ToArray(); + server.Map = server.ModData.MapCache[server.LobbyInfo.GlobalSettings.Map]; - // Reset client states - foreach (var c in server.LobbyInfo.Clients) - c.State = Session.ClientState.Invalid; + server.LobbyInfo.Slots = server.Map.Players.Players + .Select(p => MakeSlotFromPlayerReference(p.Value)) + .Where(ss => ss != null) + .ToDictionary(ss => ss.PlayerReference, ss => ss); - // Reassign players into new slots based on their old slots: - // - Observers remain as observers - // - Players who now lack a slot are made observers - // - Bots who now lack a slot are dropped - // - Bots who are not defined in the map rules are dropped - var botNames = server.Map.Rules.Actors["player"].TraitInfos().Select(t => t.Name); - var slots = server.LobbyInfo.Slots.Keys.ToArray(); - var i = 0; - foreach (var os in oldSlots) - { - var c = server.LobbyInfo.ClientInSlot(os); - if (c == null) - continue; + LoadMapSettings(server.LobbyInfo.GlobalSettings, server.Map.Rules); - c.SpawnPoint = 0; - c.Slot = i < slots.Length ? slots[i++] : null; - if (c.Slot != null) + // Reset client states + foreach (var c in server.LobbyInfo.Clients) + c.State = Session.ClientState.Invalid; + + // Reassign players into new slots based on their old slots: + // - Observers remain as observers + // - Players who now lack a slot are made observers + // - Bots who now lack a slot are dropped + // - Bots who are not defined in the map rules are dropped + var botNames = server.Map.Rules.Actors["player"].TraitInfos().Select(t => t.Name); + var slots = server.LobbyInfo.Slots.Keys.ToArray(); + var i = 0; + foreach (var os in oldSlots) { - // Remove Bot from slot if slot forbids bots - if (c.Bot != null && (!server.Map.Players.Players[c.Slot].AllowBots || !botNames.Contains(c.Bot))) + var c = server.LobbyInfo.ClientInSlot(os); + if (c == null) + continue; + + c.SpawnPoint = 0; + c.Slot = i < slots.Length ? slots[i++] : null; + if (c.Slot != null) + { + // Remove Bot from slot if slot forbids bots + if (c.Bot != null && (!server.Map.Players.Players[c.Slot].AllowBots || !botNames.Contains(c.Bot))) + server.LobbyInfo.Clients.Remove(c); + S.SyncClientToPlayerReference(c, server.Map.Players.Players[c.Slot]); + } + else if (c.Bot != null) server.LobbyInfo.Clients.Remove(c); - S.SyncClientToPlayerReference(c, server.Map.Players.Players[c.Slot]); } - else if (c.Bot != null) - server.LobbyInfo.Clients.Remove(c); + + // Validate if color is allowed and get an alternative it isn't + foreach (var c in server.LobbyInfo.Clients) + if (c.Slot == null || (c.Slot != null && !server.LobbyInfo.Slots[c.Slot].LockColor)) + c.Color = c.PreferredColor = SanitizePlayerColor(server, c.Color, c.Index, conn); + + server.SyncLobbyInfo(); + + server.SendMessage("{0} changed the map to {1}.".F(client.Name, server.Map.Title)); + + if (server.Map.Rules.Actors != server.ModData.DefaultRules.Actors) + server.SendMessage("This map contains custom rules. Game experience may change."); + + if (server.Settings.DisableSinglePlayer) + server.SendMessage("Singleplayer games have been disabled on this server."); + else if (server.Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots)) + server.SendMessage("Bots have been disabled on this map."); + }; + + Action queryFailed = () => + server.SendOrderTo(conn, "Message", "Map was not found on server."); + + var m = server.ModData.MapCache[s]; + if (m.Status == MapStatus.Available || m.Status == MapStatus.DownloadAvailable) + selectMap(m); + else if (server.Settings.QueryMapRepository) + { + server.SendOrderTo(conn, "Message", "Searching for map on the Resource Center..."); + server.ModData.MapCache.QueryRemoteMapDetails(new[] { s }, selectMap, queryFailed); } - - // Validate if color is allowed and get an alternative it isn't - foreach (var c in server.LobbyInfo.Clients) - if (c.Slot == null || (c.Slot != null && !server.LobbyInfo.Slots[c.Slot].LockColor)) - c.Color = c.PreferredColor = SanitizePlayerColor(server, c.Color, c.Index, conn); - - server.SyncLobbyInfo(); - - server.SendMessage("{0} changed the map to {1}.".F(client.Name, server.Map.Title)); - - if (server.Map.Rules.Actors != server.ModData.DefaultRules.Actors) - server.SendMessage("This map contains custom rules. Game experience may change."); - - if (server.Settings.DisableSinglePlayer) - server.SendMessage("Singleplayer games have been disabled on this server."); - else if (server.Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots)) - server.SendMessage("Bots have been disabled on this map."); + else + queryFailed(); return true; } @@ -996,7 +1020,18 @@ namespace OpenRA.Mods.Common.Server public void ServerStarted(S server) { - LoadMap(server); + // Remote maps are not supported for the initial map + var uid = server.LobbyInfo.GlobalSettings.Map; + server.Map = server.ModData.MapCache[uid]; + if (server.Map.Status != MapStatus.Available) + throw new Exception("Map {0} not found".F(uid)); + + server.LobbyInfo.Slots = server.Map.Players.Players + .Select(p => MakeSlotFromPlayerReference(p.Value)) + .Where(s => s != null) + .ToDictionary(s => s.PlayerReference, s => s); + + LoadMapSettings(server.LobbyInfo.GlobalSettings, server.Map.Rules); } static Session.Slot MakeSlotFromPlayerReference(PlayerReference pr) @@ -1042,18 +1077,6 @@ namespace OpenRA.Mods.Common.Server gs.Difficulty = mapOptions.Difficulty ?? mapOptions.Difficulties.FirstOrDefault(); } - static void LoadMap(S server) - { - server.Map = server.ModData.MapCache[server.LobbyInfo.GlobalSettings.Map]; - - server.LobbyInfo.Slots = server.Map.Players.Players - .Select(p => MakeSlotFromPlayerReference(p.Value)) - .Where(s => s != null) - .ToDictionary(s => s.PlayerReference, s => s); - - LoadMapSettings(server.LobbyInfo.GlobalSettings, server.Map.Rules); - } - static HSLColor SanitizePlayerColor(S server, HSLColor askedColor, int playerIndex, Connection connectionToEcho = null) { var validator = server.ModData.Manifest.Get(); From 6593895ea119ea84f4bcda6ee8a9a0562115be2a Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Wed, 30 Mar 2016 17:40:24 +0100 Subject: [PATCH 4/4] Fix comment typo in LobbyCommands.cs. --- OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs index 330abbefc8..4195115655 100644 --- a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs @@ -389,7 +389,7 @@ namespace OpenRA.Mods.Common.Server server.LobbyInfo.Clients.Remove(c); } - // Validate if color is allowed and get an alternative it isn't + // Validate if color is allowed and get an alternative if it isn't foreach (var c in server.LobbyInfo.Clients) if (c.Slot == null || (c.Slot != null && !server.LobbyInfo.Slots[c.Slot].LockColor)) c.Color = c.PreferredColor = SanitizePlayerColor(server, c.Color, c.Index, conn);