From 0bbb32e8ac2e1bf295a2c9962b85d56f9a37b76f Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Tue, 6 Apr 2021 13:53:38 +0100 Subject: [PATCH] Rework MapPreview custom rule handling. The previous asynchronous approach did not work particularly well, leading to large janks when switching to custom maps or opening the mission browser. This commit introduces two key changes: * Rule loading for WorldActorInfo and PlayerActorInfo is made synchronous, in preparation for the next commit which will significantly optimize this path. * The full ruleset loading, which is required for map validation, is moved to the server-side and managed by a new ServerMapStatusCache. The previous syntax check is expanded to include the ability to run lint tests. --- OpenRA.Game/Map/MapPreview.cs | 172 +++++++----------- OpenRA.Game/Network/Session.cs | 12 ++ OpenRA.Game/Server/MapStatusCache.cs | 100 ++++++++++ OpenRA.Game/Server/ProtocolVersion.cs | 2 +- OpenRA.Game/Server/Server.cs | 22 ++- OpenRA.Game/Settings.cs | 3 + .../ServerTraits/LobbyCommands.cs | 3 +- .../Widgets/Logic/Lobby/LobbyLogic.cs | 66 +++---- .../Widgets/Logic/Lobby/LobbyOptionsLogic.cs | 22 +-- .../Widgets/Logic/Lobby/LobbyUtils.cs | 5 +- .../Widgets/Logic/Lobby/MapPreviewLogic.cs | 87 +++++---- .../Widgets/Logic/MissionBrowserLogic.cs | 10 +- .../Widgets/Logic/ReplayBrowserLogic.cs | 21 +-- launch-dedicated.cmd | 3 +- launch-dedicated.sh | 2 + mods/cnc/chrome/lobby-mappreview.yaml | 36 ++++ mods/common/chrome/lobby-mappreview.yaml | 36 ++++ 17 files changed, 373 insertions(+), 229 deletions(-) create mode 100644 OpenRA.Game/Server/MapStatusCache.cs diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index 2f88782796..24f5c9387e 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -38,9 +38,6 @@ namespace OpenRA 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.")] @@ -85,45 +82,50 @@ namespace OpenRA public MapClassification Class; public MapVisibility Visibility; - Lazy rules; - public bool InvalidCustomRules { get; private set; } - public bool DefinesUnsafeCustomRules { get; private set; } - public bool RulesLoaded { get; private set; } + public MiniYaml RuleDefinitions; + public MiniYaml WeaponDefinitions; + public MiniYaml VoiceDefinitions; + public MiniYaml MusicDefinitions; + public MiniYaml NotificationDefinitions; + public MiniYaml SequenceDefinitions; + public MiniYaml ModelSequenceDefinitions; - public ActorInfo WorldActorInfo => rules?.Value.Actors[SystemActors.World]; - public ActorInfo PlayerActorInfo => rules?.Value.Actors[SystemActors.Player]; + public ActorInfo WorldActorInfo { get; private set; } + public ActorInfo PlayerActorInfo { get; private set; } - public void SetRulesetGenerator(ModData modData, Func<(Ruleset Ruleset, bool DefinesUnsafeCustomRules)> generator) + static MiniYaml LoadRuleSection(Dictionary yaml, string section) { - InvalidCustomRules = false; - RulesLoaded = false; - DefinesUnsafeCustomRules = false; + if (!yaml.TryGetValue(section, out var node)) + return null; - // 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(() => + return node; + } + + public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary yaml) + { + RuleDefinitions = LoadRuleSection(yaml, "Rules"); + WeaponDefinitions = LoadRuleSection(yaml, "Weapons"); + VoiceDefinitions = LoadRuleSection(yaml, "Voices"); + MusicDefinitions = LoadRuleSection(yaml, "Music"); + NotificationDefinitions = LoadRuleSection(yaml, "Notifications"); + SequenceDefinitions = LoadRuleSection(yaml, "Sequences"); + ModelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences"); + + try { - if (generator == null) - return Ruleset.LoadDefaultsForTileSet(modData, TileSet); + var rules = Ruleset.Load(modData, fileSystem, TileSet, RuleDefinitions, + WeaponDefinitions, VoiceDefinitions, NotificationDefinitions, + MusicDefinitions, SequenceDefinitions, ModelSequenceDefinitions); - try - { - var ret = generator(); - DefinesUnsafeCustomRules = ret.DefinesUnsafeCustomRules; - return ret.Ruleset; - } - 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; - } - }); + WorldActorInfo = rules.Actors[SystemActors.World]; + PlayerActorInfo = rules.Actors[SystemActors.Player]; + } + catch (Exception e) + { + Log.Write("debug", "Failed to load rules for `{0}` with error :{1}", Title, e.Message); + WorldActorInfo = modData.DefaultRules.Actors[SystemActors.World]; + PlayerActorInfo = modData.DefaultRules.Actors[SystemActors.Player]; + } } public InnerData Clone() @@ -132,7 +134,7 @@ namespace OpenRA } } - static readonly CPos[] NoSpawns = new CPos[] { }; + static readonly CPos[] NoSpawns = { }; readonly MapCache cache; readonly ModData modData; @@ -156,22 +158,9 @@ namespace OpenRA public MapClassification Class => innerData.Class; public MapVisibility Visibility => innerData.Visibility; - public bool InvalidCustomRules => innerData.InvalidCustomRules; - public bool RulesLoaded => innerData.RulesLoaded; - public ActorInfo WorldActorInfo => innerData.WorldActorInfo; public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo; - public bool DefinesUnsafeCustomRules - { - get - { - // Force lazy rules to be evaluated - var force = innerData.WorldActorInfo; - return innerData.DefinesUnsafeCustomRules; - } - } - public long DownloadBytes { get; private set; } public int DownloadPercentage { get; private set; } @@ -197,6 +186,20 @@ namespace OpenRA generatingMinimap = false; } + public bool DefinesUnsafeCustomRules() + { + return Ruleset.DefinesUnsafeCustomRules(modData, this, innerData.RuleDefinitions, + innerData.WeaponDefinitions, innerData.VoiceDefinitions, + innerData.NotificationDefinitions, innerData.SequenceDefinitions); + } + + public Ruleset LoadRuleset() + { + return Ruleset.Load(modData, this, TileSet, innerData.RuleDefinitions, + innerData.WeaponDefinitions, innerData.VoiceDefinitions, innerData.NotificationDefinitions, + innerData.MusicDefinitions, innerData.SequenceDefinitions, innerData.ModelSequenceDefinitions); + } + public MapPreview(ModData modData, string uid, MapGridType gridType, MapCache cache) { this.cache = cache; @@ -308,21 +311,7 @@ namespace OpenRA 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 (rules, flagged); - }); + newData.SetCustomRules(modData, this, yaml); if (p.Contains("map.png")) using (var dataStream = p.GetStream("map.png")) @@ -332,19 +321,6 @@ namespace OpenRA innerData = newData; } - MiniYaml LoadRuleSection(Dictionary yaml, string section) - { - if (!yaml.TryGetValue(section, out var node)) - return null; - - return node; - } - - public void PreloadRules() - { - var unused = WorldActorInfo; - } - public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action parseMetadata = null) { var newData = innerData.Clone(); @@ -389,23 +365,9 @@ namespace OpenRA 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 (rules, flagged); - }); + var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules)); + var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary(); + newData.SetCustomRules(modData, this, rulesYaml); } catch (Exception e) { @@ -473,21 +435,19 @@ namespace OpenRA mapInstallPackage.Update(mapFilename, fileStream.ToArray()); 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 { - var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles); - if (package == null) - innerData.Status = MapStatus.DownloadError; - else - { - UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType); - onSuccess(); - } - }); + UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType); + Game.RunAfterTick(onSuccess); + } } catch (Exception e) { - Console.WriteLine(e.Message); + Log.Write("debug", "Map installation failed with error: {0}", e); innerData.Status = MapStatus.DownloadError; } }); diff --git a/OpenRA.Game/Network/Session.cs b/OpenRA.Game/Network/Session.cs index 86face63dd..3ec4484357 100644 --- a/OpenRA.Game/Network/Session.cs +++ b/OpenRA.Game/Network/Session.cs @@ -15,6 +15,7 @@ using System.Linq; using System.Net; using System.Net.Sockets; using OpenRA.Primitives; +using OpenRA.Server; namespace OpenRA.Network { @@ -218,10 +219,21 @@ namespace OpenRA.Network public bool IsEnabled => Value == "True"; } + [Flags] + public enum MapStatus + { + Unknown = 0, + Validating = 1, + Playable = 2, + Incompatible = 4, + UnsafeCustomRules = 8, + } + public class Global { public string ServerName; public string Map; + public MapStatus MapStatus; public int RandomSeed = 0; public bool AllowSpectators = true; public string GameUid; diff --git a/OpenRA.Game/Server/MapStatusCache.cs b/OpenRA.Game/Server/MapStatusCache.cs new file mode 100644 index 0000000000..65f40fa42c --- /dev/null +++ b/OpenRA.Game/Server/MapStatusCache.cs @@ -0,0 +1,100 @@ +#region Copyright & License Information +/* + * Copyright 2007-2021 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.Threading; +using OpenRA.Network; + +namespace OpenRA.Server +{ + public interface ILintServerMapPass { void Run(Action emitError, Action emitWarning, ModData modData, MapPreview map, Ruleset mapRules); } + + public class MapStatusCache + { + readonly Dictionary cache = new Dictionary(); + readonly Action onStatusChanged; + readonly bool enableRemoteLinting; + readonly ModData modData; + + public MapStatusCache(ModData modData, Action onStatusChanged, bool enableRemoteLinting) + { + this.modData = modData; + this.enableRemoteLinting = enableRemoteLinting; + this.onStatusChanged = onStatusChanged; + } + + void RunLintTests(MapPreview map, Ruleset rules) + { + var status = cache[map]; + var failed = false; + + Action onLintFailure = message => + { + Log.Write("server", "Map {0} failed lint with error: {1}", map.Title, message); + failed = true; + }; + + Action onLintWarning = _ => { }; + + foreach (var customMapPassType in modData.ObjectCreator.GetTypesImplementing()) + { + try + { + var customMapPass = (ILintServerMapPass)modData.ObjectCreator.CreateBasic(customMapPassType); + customMapPass.Run(onLintFailure, onLintWarning, modData, map, rules); + } + catch (Exception e) + { + onLintFailure(e.ToString()); + } + } + + status &= ~Session.MapStatus.Validating; + status |= failed ? Session.MapStatus.Incompatible : Session.MapStatus.Playable; + + cache[map] = status; + onStatusChanged?.Invoke(map.Uid, status); + } + + public Session.MapStatus this[MapPreview map] + { + get + { + if (cache.TryGetValue(map, out var status)) + return status; + + Ruleset rules = null; + try + { + rules = map.LoadRuleset(); + + // Locally installed maps are assumed to not require linting + status = enableRemoteLinting && map.Status != MapStatus.Available ? Session.MapStatus.Validating : Session.MapStatus.Playable; + if (map.DefinesUnsafeCustomRules()) + status |= Session.MapStatus.UnsafeCustomRules; + } + catch (Exception e) + { + Log.Write("server", "Failed to load rules for `{0}` with error :{1}", map.Title, e.Message); + status = Session.MapStatus.Incompatible; + } + + cache[map] = status; + + if ((status & Session.MapStatus.Validating) != 0) + ThreadPool.QueueUserWorkItem(_ => RunLintTests(map, rules)); + + return status; + } + } + } +} diff --git a/OpenRA.Game/Server/ProtocolVersion.cs b/OpenRA.Game/Server/ProtocolVersion.cs index 83964c861b..e980b1115c 100644 --- a/OpenRA.Game/Server/ProtocolVersion.cs +++ b/OpenRA.Game/Server/ProtocolVersion.cs @@ -68,6 +68,6 @@ namespace OpenRA.Server // The protocol for server and world orders // This applies after the handshake has completed, and is provided to support // alternative server implementations that wish to support multiple versions in parallel - public const int Orders = 11; + public const int Orders = 12; } } diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index f251db246e..ee14124f4e 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -61,6 +61,7 @@ namespace OpenRA.Server // Managed by LobbyCommands public MapPreview Map; + public readonly MapStatusCache MapStatusCache; public GameSave GameSave = null; readonly int randomSeed; @@ -170,6 +171,17 @@ namespace OpenRA.Server }.Serialize()); } + void MapStatusChanged(string uid, Session.MapStatus status) + { + lock (LobbyInfo) + { + if (LobbyInfo.GlobalSettings.Map == uid) + LobbyInfo.GlobalSettings.MapStatus = status; + + SyncLobbyInfo(); + } + } + public Server(List endpoints, ServerSettings settings, ModData modData, ServerType type) { Log.AddChannel("server", "server.log", true); @@ -229,12 +241,16 @@ namespace OpenRA.Server serverTraits.TrimExcess(); + Map = ModData.MapCache[settings.Map]; + MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks); + LobbyInfo = new Session { GlobalSettings = { RandomSeed = randomSeed, - Map = settings.Map, + Map = Map.Uid, + MapStatus = Session.MapStatus.Unknown, ServerName = settings.Name, EnableSingleplayer = settings.EnableSingleplayer || Type != ServerType.Dedicated, EnableSyncReports = settings.EnableSyncReports, @@ -254,6 +270,8 @@ namespace OpenRA.Server new Thread(_ => { + // Initial status is set off the main thread to avoid triggering a load screen when joining a skirmish game + LobbyInfo.GlobalSettings.MapStatus = MapStatusCache[Map]; foreach (var t in serverTraits.WithInterface()) t.ServerStarted(this); @@ -541,7 +559,7 @@ namespace OpenRA.Server SendOrderTo(newConn, "Message", motd); } - if (Map.DefinesUnsafeCustomRules) + if ((LobbyInfo.GlobalSettings.MapStatus & Session.MapStatus.UnsafeCustomRules) != 0) SendOrderTo(newConn, "Message", "This map contains custom rules. Game experience may change."); if (!LobbyInfo.GlobalSettings.EnableSingleplayer) diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index d3e1e4ef0c..854723c8e4 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -91,6 +91,9 @@ namespace OpenRA [Desc("For dedicated servers only, save replays for all games played.")] public bool RecordReplays = false; + [Desc("For dedicated servers only, treat maps that fail the lint checks as invalid.")] + public bool EnableLintChecks = true; + public ServerSettings Clone() { return (ServerSettings)MemberwiseClone(); diff --git a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs index b04b792bfb..d1db8e45d1 100644 --- a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs @@ -431,6 +431,7 @@ namespace OpenRA.Mods.Common.Server var oldSlots = server.LobbyInfo.Slots.Keys.ToArray(); server.Map = server.ModData.MapCache[server.LobbyInfo.GlobalSettings.Map]; + server.LobbyInfo.GlobalSettings.MapStatus = server.MapStatusCache[server.Map]; server.LobbyInfo.Slots = server.Map.Players.Players .Select(p => MakeSlotFromPlayerReference(p.Value)) @@ -492,7 +493,7 @@ namespace OpenRA.Mods.Common.Server server.SendMessage("{0} changed the map to {1}.".F(client.Name, server.Map.Title)); - if (server.Map.DefinesUnsafeCustomRules) + if ((server.LobbyInfo.GlobalSettings.MapStatus & Session.MapStatus.UnsafeCustomRules) != 0) server.SendMessage("This map contains custom rules. Game experience may change."); if (!server.LobbyInfo.GlobalSettings.EnableSingleplayer) diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index 19744c407c..4bfc6fed3a 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -58,6 +58,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly TabCompletionLogic tabCompletion = new TabCompletionLogic(); MapPreview map; + Session.MapStatus mapStatus; + bool addBotOnMapLoad; bool disableTeamChat; bool insufficientPlayerSpawns; @@ -67,6 +69,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly string chatLineSound = ChromeMetrics.Get("ChatLineSound"); + bool MapIsPlayable => (mapStatus & Session.MapStatus.Playable) == Session.MapStatus.Playable; + // Listen for connection failures void ConnectionStateChanged(OrderManager om) { @@ -131,7 +135,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var mapContainer = Ui.LoadWidget("MAP_PREVIEW", lobby.Get("MAP_PREVIEW_ROOT"), new WidgetArgs { { "orderManager", orderManager }, - { "getMap", (Func)(() => map) }, + { "getMap", (Func<(MapPreview, Session.MapStatus)>)(() => (map, mapStatus)) }, { "onMouseDown", (Action)((preview, mapPreview, mi) => LobbyUtils.SelectSpawnPoint(orderManager, preview, mapPreview, mi)) @@ -163,8 +167,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var gameStarting = false; Func configurationDisabled = () => !Game.IsHost || gameStarting || - panel == PanelType.Kick || panel == PanelType.ForceStart || - !map.RulesLoaded || map.InvalidCustomRules || + panel == PanelType.Kick || panel == PanelType.ForceStart || !MapIsPlayable || orderManager.LocalClient == null || orderManager.LocalClient.IsReady; var mapButton = lobby.GetOrNull("CHANGEMAP_BUTTON"); @@ -330,7 +333,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic optionsTab.IsHighlighted = () => panel == PanelType.Options; optionsTab.IsDisabled = OptionsTabDisabled; optionsTab.OnClick = () => panel = PanelType.Options; - optionsTab.GetText = () => !map.RulesLoaded ? "Loading..." : optionsTab.Text; var playersTab = tabContainer.Get("PLAYERS_TAB"); playersTab.IsHighlighted = () => panel == PanelType.Players; @@ -476,7 +478,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic bool OptionsTabDisabled() { - return !map.RulesLoaded || map.InvalidCustomRules || panel == PanelType.Kick || panel == PanelType.ForceStart; + return !MapIsPlayable || panel == PanelType.Kick || panel == PanelType.ForceStart; } public override void Tick() @@ -505,14 +507,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic return true; } - void LoadMapPreviewRules(MapPreview map) - { - // Force map rules to be loaded on this background thread - new Task(map.PreloadRules).Start(); - } - void UpdateCurrentMap() { + mapStatus = orderManager.LobbyInfo.GlobalSettings.MapStatus; var uid = orderManager.LobbyInfo.GlobalSettings.Map; if (map.Uid == uid) return; @@ -520,39 +517,22 @@ namespace OpenRA.Mods.Common.Widgets.Logic map = modData.MapCache[uid]; if (map.Status == MapStatus.Available) { - // Maps need to be validated and pre-loaded before they can be accessed - var currentMap = map; - new Task(() => + // Tell the server that we have the map + orderManager.IssueOrder(Order.Command("state {0}".F(Session.ClientState.NotReady))); + + if (addBotOnMapLoad) { - // Force map rules to be loaded on this background thread - currentMap.PreloadRules(); - Game.RunAfterTick(() => - { - // Map may have changed in the meantime - if (currentMap != map) - return; + var slot = orderManager.LobbyInfo.FirstEmptyBotSlot(); + var bot = map.PlayerActorInfo.TraitInfos().Select(t => t.Type).FirstOrDefault(); + var botController = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.IsAdmin); + if (slot != null && bot != null) + orderManager.IssueOrder(Order.Command("slot_bot {0} {1} {2}".F(slot, botController.Index, bot))); - // Tell the server that we have the map - if (!currentMap.InvalidCustomRules) - orderManager.IssueOrder(Order.Command("state {0}".F(Session.ClientState.NotReady))); - - if (addBotOnMapLoad) - { - var slot = orderManager.LobbyInfo.FirstEmptyBotSlot(); - var bot = currentMap.PlayerActorInfo.TraitInfos().Select(t => t.Type).FirstOrDefault(); - var botController = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.IsAdmin); - if (slot != null && bot != null) - orderManager.IssueOrder(Order.Command("slot_bot {0} {1} {2}".F(slot, botController.Index, bot))); - - addBotOnMapLoad = false; - } - }); - }).Start(); + addBotOnMapLoad = false; + } } - else if (map.Status == MapStatus.DownloadAvailable) - LoadMapPreviewRules(map); - else if (Game.Settings.Game.AllowDownloading) - modData.MapCache.QueryRemoteMapDetails(services.MapRepository, new[] { uid }, LoadMapPreviewRules); + else if (map.Status != MapStatus.DownloadAvailable && Game.Settings.Game.AllowDownloading) + modData.MapCache.QueryRemoteMapDetails(services.MapRepository, new[] { uid }); } void UpdatePlayerList() @@ -626,7 +606,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, map); LobbyUtils.SetupEditableHandicapWidget(template, slot, client, orderManager, map); LobbyUtils.SetupEditableSpawnWidget(template, slot, client, orderManager, map); - LobbyUtils.SetupEditableReadyWidget(template, slot, client, orderManager, map); + LobbyUtils.SetupEditableReadyWidget(template, slot, client, orderManager, map, MapIsPlayable); } else { @@ -686,7 +666,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic LobbyUtils.SetupEditableNameWidget(template, null, c, orderManager, worldRenderer); if (client.IsAdmin) - LobbyUtils.SetupEditableReadyWidget(template, null, client, orderManager, map); + LobbyUtils.SetupEditableReadyWidget(template, null, client, orderManager, map, MapIsPlayable); else LobbyUtils.HideReadyWidgets(template); } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyOptionsLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyOptionsLogic.cs index 8b484f83eb..fb181fa976 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyOptionsLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyOptionsLogic.cs @@ -30,7 +30,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly OrderManager orderManager; readonly Func configurationDisabled; MapPreview mapPreview; - bool validOptions; [ObjectCreator.UseCtor] internal LobbyOptionsLogic(Widget widget, OrderManager orderManager, Func getMap, Func configurationDisabled) @@ -42,7 +41,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic panel = (ScrollPanelWidget)widget; optionsContainer = widget.Get("LOBBY_OPTIONS"); yMargin = optionsContainer.Bounds.Y; - optionsContainer.IsVisible = () => validOptions; checkboxRowTemplate = optionsContainer.Get("CHECKBOX_ROW_TEMPLATE"); dropdownRowTemplate = optionsContainer.Get("DROPDOWN_ROW_TEMPLATE"); @@ -56,24 +54,18 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (newMapPreview == mapPreview) return; - if (newMapPreview.RulesLoaded) + // We are currently enumerating the widget tree and so can't modify any layout + // Defer it to the end of tick instead + Game.RunAfterTick(() => { - // We are currently enumerating the widget tree and so can't modify any layout - // Defer it to the end of tick instead - Game.RunAfterTick(() => - { - mapPreview = newMapPreview; - RebuildOptions(); - validOptions = true; - }); - } - else - validOptions = false; + mapPreview = newMapPreview; + RebuildOptions(); + }); } void RebuildOptions() { - if (mapPreview == null || mapPreview.WorldActorInfo == null || mapPreview.InvalidCustomRules) + if (mapPreview == null || mapPreview.WorldActorInfo == null) return; optionsContainer.RemoveChildren(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs index 5ad7220c92..787e4332c1 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs @@ -630,13 +630,12 @@ namespace OpenRA.Mods.Common.Widgets.Logic HideChildWidget(parent, "SPAWN_DROPDOWN"); } - public static void SetupEditableReadyWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, MapPreview map) + public static void SetupEditableReadyWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, MapPreview map, bool isEnabled) { var status = parent.Get("STATUS_CHECKBOX"); status.IsChecked = () => orderManager.LocalClient.IsReady || c.Bot != null; status.IsVisible = () => true; - status.IsDisabled = () => c.Bot != null || map.Status != MapStatus.Available || - !map.RulesLoaded || map.InvalidCustomRules; + status.IsDisabled = () => c.Bot != null || map.Status != MapStatus.Available || !isEnabled; var state = orderManager.LocalClient.IsReady ? Session.ClientState.NotReady : Session.ClientState.Ready; status.OnClick = () => orderManager.IssueOrder(Order.Command("state {0}".F(state))); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/MapPreviewLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/MapPreviewLogic.cs index 6f3c095370..af40e21cf7 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/MapPreviewLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/MapPreviewLogic.cs @@ -24,8 +24,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic int blinkTick; [ObjectCreator.UseCtor] - internal MapPreviewLogic(Widget widget, ModData modData, OrderManager orderManager, Func getMap, Action onMouseDown, - Func> getSpawnOccupants, Func> getDisabledSpawnPoints, bool showUnoccupiedSpawnpoints) + internal MapPreviewLogic(Widget widget, ModData modData, OrderManager orderManager, Func<(MapPreview Map, Session.MapStatus Status)> getMap, + Action onMouseDown, Func> getSpawnOccupants, + Func> getDisabledSpawnPoints, bool showUnoccupiedSpawnpoints) { var mapRepository = modData.Manifest.Get().MapRepository; @@ -34,8 +35,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic { available.IsVisible = () => { - var map = getMap(); - return map.Status == MapStatus.Available && (!map.RulesLoaded || !map.InvalidCustomRules); + var (map, serverStatus) = getMap(); + var isPlayable = (serverStatus & Session.MapStatus.Playable) == Session.MapStatus.Playable; + return map.Status == MapStatus.Available && isPlayable; }; SetupWidgets(available, getMap, onMouseDown, getSpawnOccupants, getDisabledSpawnPoints, showUnoccupiedSpawnpoints); @@ -46,17 +48,30 @@ namespace OpenRA.Mods.Common.Widgets.Logic { invalid.IsVisible = () => { - var map = getMap(); - return map.Status == MapStatus.Available && map.InvalidCustomRules; + var (map, serverStatus) = getMap(); + return map.Status == MapStatus.Available && (serverStatus & Session.MapStatus.Incompatible) != 0; }; SetupWidgets(invalid, getMap, onMouseDown, getSpawnOccupants, getDisabledSpawnPoints, showUnoccupiedSpawnpoints); } + var validating = widget.GetOrNull("MAP_VALIDATING"); + if (validating != null) + { + validating.IsVisible = () => + { + var (map, serverStatus) = getMap(); + return map.Status == MapStatus.Available && (serverStatus & Session.MapStatus.Validating) != 0; + }; + + SetupWidgets(validating, getMap, onMouseDown, getSpawnOccupants, getDisabledSpawnPoints, showUnoccupiedSpawnpoints); + } + var download = widget.GetOrNull("MAP_DOWNLOADABLE"); if (download != null) { - download.IsVisible = () => getMap().Status == MapStatus.DownloadAvailable; + download.IsVisible = () => getMap().Map.Status == MapStatus.DownloadAvailable; + SetupWidgets(download, getMap, onMouseDown, getSpawnOccupants, getDisabledSpawnPoints, showUnoccupiedSpawnpoints); var install = download.GetOrNull("MAP_INSTALL"); @@ -64,10 +79,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic { install.OnClick = () => { - var map = getMap(); - map.Install(mapRepository, () => + getMap().Map.Install(mapRepository, () => { - map.PreloadRules(); if (orderManager != null) Game.RunAfterTick(() => orderManager.IssueOrder(Order.Command("state {0}".F(Session.ClientState.NotReady)))); }); @@ -82,7 +95,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic { progress.IsVisible = () => { - var map = getMap(); + var (map, _) = getMap(); return map.Status != MapStatus.Available && map.Status != MapStatus.DownloadAvailable; }; @@ -90,29 +103,36 @@ namespace OpenRA.Mods.Common.Widgets.Logic var statusSearching = progress.GetOrNull("MAP_STATUS_SEARCHING"); if (statusSearching != null) - statusSearching.IsVisible = () => getMap().Status == MapStatus.Searching; + { + statusSearching.IsVisible = () => + { + var (map, _) = getMap(); + return map.Status == MapStatus.Searching; + }; + } var statusUnavailable = progress.GetOrNull("MAP_STATUS_UNAVAILABLE"); if (statusUnavailable != null) { statusUnavailable.IsVisible = () => { - var map = getMap(); + var (map, _) = getMap(); return map.Status == MapStatus.Unavailable && map != MapCache.UnknownMap; }; } var statusError = progress.GetOrNull("MAP_STATUS_ERROR"); if (statusError != null) - statusError.IsVisible = () => getMap().Status == MapStatus.DownloadError; + statusError.IsVisible = () => getMap().Map.Status == MapStatus.DownloadError; var statusDownloading = progress.GetOrNull("MAP_STATUS_DOWNLOADING"); if (statusDownloading != null) { - statusDownloading.IsVisible = () => getMap().Status == MapStatus.Downloading; + statusDownloading.IsVisible = () => getMap().Map.Status == MapStatus.Downloading; + statusDownloading.GetText = () => { - var map = getMap(); + var (map, _) = getMap(); if (map.DownloadBytes == 0) return "Connecting..."; @@ -129,18 +149,17 @@ namespace OpenRA.Mods.Common.Widgets.Logic { retry.IsVisible = () => { - var map = getMap(); + var (map, _) = getMap(); return (map.Status == MapStatus.DownloadError || map.Status == MapStatus.Unavailable) && map != MapCache.UnknownMap; }; retry.OnClick = () => { - var map = getMap(); + var (map, _) = getMap(); if (map.Status == MapStatus.DownloadError) { map.Install(mapRepository, () => { - map.PreloadRules(); if (orderManager != null) Game.RunAfterTick(() => orderManager.IssueOrder(Order.Command("state {0}".F(Session.ClientState.NotReady)))); }); @@ -149,15 +168,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic modData.MapCache.QueryRemoteMapDetails(mapRepository, new[] { map.Uid }); }; - retry.GetText = () => getMap().Status == MapStatus.DownloadError ? "Retry Install" : "Retry Search"; + retry.GetText = () => getMap().Map.Status == MapStatus.DownloadError ? "Retry Install" : "Retry Search"; } var progressbar = progress.GetOrNull("MAP_PROGRESSBAR"); if (progressbar != null) { - progressbar.IsIndeterminate = () => getMap().DownloadPercentage == 0; - progressbar.GetPercentage = () => getMap().DownloadPercentage; - progressbar.IsVisible = () => getMap().Status == MapStatus.Downloading; + progressbar.IsIndeterminate = () => getMap().Map.DownloadPercentage == 0; + progressbar.GetPercentage = () => getMap().Map.DownloadPercentage; + progressbar.IsVisible = () => getMap().Map.Status == MapStatus.Downloading; } } } @@ -171,12 +190,16 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } - void SetupWidgets(Widget parent, Func getMap, - Action onMouseDown, Func> getSpawnOccupants, Func> getDisabledSpawnPoints, bool showUnoccupiedSpawnpoints) + void SetupWidgets(Widget parent, + Func<(MapPreview Map, Session.MapStatus Status)> getMap, + Action onMouseDown, + Func> getSpawnOccupants, + Func> getDisabledSpawnPoints, + bool showUnoccupiedSpawnpoints) { var preview = parent.Get("MAP_PREVIEW"); - preview.Preview = () => getMap(); - preview.OnMouseDown = mi => onMouseDown(preview, getMap(), mi); + preview.Preview = () => getMap().Map; + preview.OnMouseDown = mi => onMouseDown(preview, getMap().Map, mi); preview.SpawnOccupants = getSpawnOccupants; preview.DisabledSpawnPoints = getDisabledSpawnPoints; preview.ShowUnoccupiedSpawnpoints = showUnoccupiedSpawnpoints; @@ -184,18 +207,18 @@ namespace OpenRA.Mods.Common.Widgets.Logic var titleLabel = parent.GetOrNull("MAP_TITLE"); if (titleLabel != null) { - titleLabel.IsVisible = () => getMap() != MapCache.UnknownMap; + titleLabel.IsVisible = () => getMap().Map != MapCache.UnknownMap; var font = Game.Renderer.Fonts[titleLabel.Font]; var title = new CachedTransform(m => WidgetUtils.TruncateText(m.Title, titleLabel.Bounds.Width, font)); - titleLabel.GetText = () => title.Update(getMap()); - titleLabel.GetTooltipText = () => getMap().Title; + titleLabel.GetText = () => title.Update(getMap().Map); + titleLabel.GetTooltipText = () => getMap().Map.Title; } var typeLabel = parent.GetOrNull("MAP_TYPE"); if (typeLabel != null) { var type = new CachedTransform(m => m.Categories.FirstOrDefault() ?? ""); - typeLabel.GetText = () => type.Update(getMap()); + typeLabel.GetText = () => type.Update(getMap().Map); } var authorLabel = parent.GetOrNull("MAP_AUTHOR"); @@ -204,7 +227,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var font = Game.Renderer.Fonts[authorLabel.Font]; var author = new CachedTransform( m => WidgetUtils.TruncateText("Created by {0}".F(m.Author), authorLabel.Bounds.Width, font)); - authorLabel.GetText = () => author.Update(getMap()); + authorLabel.GetText = () => author.Update(getMap().Map); } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs index d027cf2f8f..6cb28130b0 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MissionBrowserLogic.cs @@ -139,19 +139,16 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (allPreviews.Any()) SelectMap(allPreviews.First()); - // Preload map preview and rules to reduce jank + // Preload map preview to reduce jank new Thread(() => { foreach (var p in allPreviews) - { p.GetMinimap(); - p.PreloadRules(); - } }).Start(); var startButton = widget.Get("STARTGAME_BUTTON"); startButton.OnClick = StartMissionClicked; - startButton.IsDisabled = () => selectedMap == null || selectedMap.InvalidCustomRules; + startButton.IsDisabled = () => selectedMap == null; widget.Get("BACK_BUTTON").OnClick = () => { @@ -373,9 +370,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic { StopVideo(videoPlayer); - if (selectedMap.InvalidCustomRules) - return; - var orders = new List(); if (difficulty != null) orders.Add(Order.Command("option difficulty {0}".F(difficulty))); diff --git a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs index c457dd2043..ebfc057dab 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs @@ -17,6 +17,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using OpenRA.FileFormats; +using OpenRA.Network; using OpenRA.Primitives; using OpenRA.Traits; using OpenRA.Widgets; @@ -93,7 +94,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic Ui.LoadWidget("MAP_PREVIEW", mapPreviewRoot, new WidgetArgs { { "orderManager", null }, - { "getMap", (Func)(() => map) }, + { "getMap", (Func<(MapPreview, Session.MapStatus)>)(() => (map, Session.MapStatus.Playable)) }, { "onMouseDown", (Action)((preview, mapPreview, mi) => { }) }, { "getSpawnOccupants", (Func>)(() => spawnOccupants.Update(selectedReplay)) }, { "getDisabledSpawnPoints", (Func>)(() => disabledSpawnPoints.Update(selectedReplay)) }, @@ -623,13 +624,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic try { - if (map.Status != MapStatus.Available) - { - if (map.Status == MapStatus.DownloadAvailable) - LoadMapPreviewRules(map); - else if (Game.Settings.Game.AllowDownloading) - modData.MapCache.QueryRemoteMapDetails(services.MapRepository, new[] { map.Uid }, LoadMapPreviewRules); - } + if (map.Status == MapStatus.Unavailable && Game.Settings.Game.AllowDownloading) + modData.MapCache.QueryRemoteMapDetails(services.MapRepository, new[] { map.Uid }); var players = replay.GameInfo.Players .GroupBy(p => p.Team) @@ -685,15 +681,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } - void LoadMapPreviewRules(MapPreview map) - { - new Task(() => - { - // Force map rules to be loaded on this background thread - map.PreloadRules(); - }).Start(); - } - void WatchReplay() { if (selectedReplay != null && ReplayUtils.PromptConfirmReplayCompatibility(selectedReplay)) diff --git a/launch-dedicated.cmd b/launch-dedicated.cmd index 0e0b5f551f..ad943bc332 100644 --- a/launch-dedicated.cmd +++ b/launch-dedicated.cmd @@ -16,12 +16,13 @@ set ProfileIDWhitelist="" set EnableSingleplayer=False set EnableSyncReports=False set EnableGeoIP=True +set EnableLintChecks=True set ShareAnonymizedIPs=True set SupportDir="" :loop -bin\OpenRA.Server.exe Engine.EngineDir=".." Game.Mod=%Mod% Server.Name=%Name% Server.ListenPort=%ListenPort% Server.AdvertiseOnline=%AdvertiseOnline% Server.EnableSingleplayer=%EnableSingleplayer% Server.Password=%Password% Server.RecordReplays=%RecordReplays% Server.RequireAuthentication=%RequireAuthentication% Server.ProfileIDBlacklist=%ProfileIDBlacklist% Server.ProfileIDWhitelist=%ProfileIDWhitelist% Server.EnableSyncReports=%EnableSyncReports% Server.EnableGeoIP=%EnableGeoIP% Server.ShareAnonymizedIPs=%ShareAnonymizedIPs% Engine.SupportDir=%SupportDir% +bin\OpenRA.Server.exe Engine.EngineDir=".." Game.Mod=%Mod% Server.Name=%Name% Server.ListenPort=%ListenPort% Server.AdvertiseOnline=%AdvertiseOnline% Server.EnableSingleplayer=%EnableSingleplayer% Server.Password=%Password% Server.RecordReplays=%RecordReplays% Server.RequireAuthentication=%RequireAuthentication% Server.ProfileIDBlacklist=%ProfileIDBlacklist% Server.ProfileIDWhitelist=%ProfileIDWhitelist% Server.EnableSyncReports=%EnableSyncReports% Server.EnableGeoIP=%EnableGeoIP% Server.EnableLintChecks=%EnableLintChecks% Server.ShareAnonymizedIPs=%ShareAnonymizedIPs% Engine.SupportDir=%SupportDir% goto loop diff --git a/launch-dedicated.sh b/launch-dedicated.sh index 8db56b6485..4cb632a605 100755 --- a/launch-dedicated.sh +++ b/launch-dedicated.sh @@ -26,6 +26,7 @@ ProfileIDWhitelist="${ProfileIDWhitelist:-""}" EnableSingleplayer="${EnableSingleplayer:-"False"}" EnableSyncReports="${EnableSyncReports:-"False"}" EnableGeoIP="${EnableGeoIP:-"True"}" +EnableLintChecks="${EnableLintChecks:-"True"}" ShareAnonymizedIPs="${ShareAnonymizedIPs:-"True"}" SupportDir="${SupportDir:-""}" @@ -43,6 +44,7 @@ while true; do Server.ProfileIDWhitelist="$ProfileIDWhitelist" \ Server.EnableSyncReports="$EnableSyncReports" \ Server.EnableGeoIP="$EnableGeoIP" \ + Server.EnableLintChecks="$EnableLintChecks" \ Server.ShareAnonymizedIPs="$ShareAnonymizedIPs" \ Engine.SupportDir="$SupportDir" done diff --git a/mods/cnc/chrome/lobby-mappreview.yaml b/mods/cnc/chrome/lobby-mappreview.yaml index 8f5a3eb729..2d3d129960 100644 --- a/mods/cnc/chrome/lobby-mappreview.yaml +++ b/mods/cnc/chrome/lobby-mappreview.yaml @@ -77,6 +77,42 @@ Container@MAP_PREVIEW: Font: Tiny Align: Center Text: with this version of OpenRA + Container@MAP_VALIDATING: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Children: + Background@MAP_BG: + Width: PARENT_RIGHT + Height: 158 + Background: panel-gray + Children: + MapPreview@MAP_PREVIEW: + X: 1 + Y: 1 + Width: PARENT_RIGHT - 2 + Height: PARENT_BOTTOM - 2 + TooltipContainer: TOOLTIP_CONTAINER + LabelWithTooltip@MAP_TITLE: + Y: 159 + Width: PARENT_RIGHT + Height: 25 + Font: Bold + Align: Center + TooltipContainer: TOOLTIP_CONTAINER + TooltipTemplate: SIMPLE_TOOLTIP + Label@MAP_STATUS_VALIDATING: + Y: 174 + Width: PARENT_RIGHT + Height: 25 + Font: Tiny + Align: Center + Text: Validating... + IgnoreMouseOver: true + ProgressBar@MAP_PROGRESSBAR: + Y: 194 + Width: PARENT_RIGHT + Height: 25 + Indeterminate: True Container@MAP_DOWNLOADABLE: Width: PARENT_RIGHT Height: PARENT_BOTTOM diff --git a/mods/common/chrome/lobby-mappreview.yaml b/mods/common/chrome/lobby-mappreview.yaml index 5660150dee..5be5d09eba 100644 --- a/mods/common/chrome/lobby-mappreview.yaml +++ b/mods/common/chrome/lobby-mappreview.yaml @@ -77,6 +77,42 @@ Container@MAP_PREVIEW: Font: Tiny Align: Center Text: with this version of OpenRA + Container@MAP_VALIDATING: + Width: PARENT_RIGHT + Height: PARENT_BOTTOM + Children: + Background@MAP_BG: + Width: PARENT_RIGHT + Height: 158 + Background: dialog3 + Children: + MapPreview@MAP_PREVIEW: + X: 1 + Y: 1 + Width: PARENT_RIGHT - 2 + Height: PARENT_BOTTOM - 2 + TooltipContainer: TOOLTIP_CONTAINER + LabelWithTooltip@MAP_TITLE: + Y: 159 + Width: PARENT_RIGHT + Height: 25 + Font: Bold + Align: Center + TooltipContainer: TOOLTIP_CONTAINER + TooltipTemplate: SIMPLE_TOOLTIP + Label@MAP_STATUS_VALIDATING: + Y: 174 + Width: PARENT_RIGHT + Height: 25 + Font: Tiny + Align: Center + Text: Validating... + IgnoreMouseOver: true + ProgressBar@MAP_PROGRESSBAR: + Y: 194 + Width: PARENT_RIGHT + Height: 25 + Indeterminate: True Container@MAP_DOWNLOADABLE: Width: PARENT_RIGHT Height: PARENT_BOTTOM