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