diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 0157eb9e66..84872f98b2 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -128,7 +128,8 @@ namespace OpenRA.Server // Managed by LobbyCommands public MapPreview Map; public readonly MapStatusCache MapStatusCache; - public GameSave GameSave = null; + public GameSave GameSave; + public HashSet MapPool; // Default to the next frame for ServerType.Local - MP servers take the value from the selected GameSpeed. public int OrderLatency = 1; @@ -316,7 +317,6 @@ namespace OpenRA.Server serverTraits.TrimExcess(); - Map = ModData.MapCache[settings.Map]; MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks); playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo); @@ -327,8 +327,6 @@ namespace OpenRA.Server GlobalSettings = { RandomSeed = randomSeed, - Map = Map.Uid, - MapStatus = Session.MapStatus.Unknown, ServerName = settings.Name, EnableSingleplayer = settings.EnableSingleplayer || Type != ServerType.Dedicated, EnableSyncReports = settings.EnableSyncReports, @@ -348,8 +346,7 @@ 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]; + // Note: at least one of these is required to set the initial LobbyInfo.Map and MapStatus foreach (var t in serverTraits.WithInterface()) t.ServerStarted(this); @@ -1434,6 +1431,27 @@ namespace OpenRA.Server return new ConnectionTarget(endpoints); } + public bool MapIsUnknown(string uid) + { + if (string.IsNullOrEmpty(uid)) + return true; + + var status = ModData.MapCache[uid].Status; + return status != MapStatus.Available && status != MapStatus.DownloadAvailable; + } + + public bool MapIsKnown(string uid) + { + if (string.IsNullOrEmpty(uid)) + return false; + + if (MapPool != null && !MapPool.Contains(uid)) + return false; + + var status = ModData.MapCache[uid].Status; + return status == MapStatus.Available || status == MapStatus.DownloadAvailable; + } + interface IServerEvent { void Invoke(Server server); } sealed class ConnectionConnectEvent : IServerEvent diff --git a/OpenRA.Game/Settings.cs b/OpenRA.Game/Settings.cs index 5ca4d2ee07..942ebc097b 100644 --- a/OpenRA.Game/Settings.cs +++ b/OpenRA.Game/Settings.cs @@ -102,6 +102,9 @@ namespace OpenRA [Desc("For dedicated servers only, treat maps that fail the lint checks as invalid.")] public bool EnableLintChecks = true; + [Desc("For dedicated servers only, a comma separated list of map uids that are allowed to be used.")] + public string[] MapPool = Array.Empty(); + [Desc("Delay in milliseconds before newly joined players can send chat messages.")] public int FloodLimitJoinCooldown = 5000; diff --git a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs index 6e8944e5d3..2f67c42007 100644 --- a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs +++ b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs @@ -11,12 +11,15 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Threading; using OpenRA.Mods.Common.Traits; using OpenRA.Mods.Common.Widgets.Logic; using OpenRA.Network; using OpenRA.Primitives; using OpenRA.Server; +using OpenRA.Support; using OpenRA.Traits; using S = OpenRA.Server.Server; @@ -570,6 +573,12 @@ namespace OpenRA.Mods.Common.Server return true; } + if (server.MapPool != null && !server.MapPool.Contains(s)) + { + QueryFailed(); + return true; + } + var lastMap = server.LobbyInfo.GlobalSettings.Map; void SelectMap(MapPreview map) { @@ -659,8 +668,6 @@ namespace OpenRA.Mods.Common.Server } } - void QueryFailed() => server.SendLocalizedMessageTo(conn, UnknownMap); - var m = server.ModData.MapCache[s]; if (m.Status == MapStatus.Available || m.Status == MapStatus.DownloadAvailable) SelectMap(m); @@ -682,6 +689,8 @@ namespace OpenRA.Mods.Common.Server return true; } + + void QueryFailed() => server.SendLocalizedMessageTo(conn, UnknownMap); } static bool Option(S server, Connection conn, Session.Client client, string s) @@ -1227,16 +1236,68 @@ namespace OpenRA.Mods.Common.Server } } + static void InitializeMapPool(S server) + { + if (server.Type != ServerType.Dedicated) + return; + + var mapCache = server.ModData.MapCache; + if (server.Settings.MapPool.Length > 0) + server.MapPool = server.Settings.MapPool.ToHashSet(); + else if (!server.Settings.QueryMapRepository) + server.MapPool = mapCache + .Where(p => p.Status == MapStatus.Available && p.Visibility.HasFlag(MapVisibility.Lobby)) + .Select(p => p.Uid) + .ToHashSet(); + else + return; + + var unknownMaps = server.MapPool.Where(server.MapIsUnknown); + if (server.Settings.QueryMapRepository && unknownMaps.Any()) + { + Log.Write("server", $"Querying Resource Center for information on {unknownMaps.Count()} maps..."); + + // Query any missing maps and wait up to 10 seconds for a response + // Maps that have not resolved will not be valid for the initial map choice + var mapRepository = server.ModData.Manifest.Get().MapRepository; + mapCache.QueryRemoteMapDetails(mapRepository, unknownMaps); + + var searchingMaps = server.MapPool.Where(uid => mapCache[uid].Status == MapStatus.Searching); + var stopwatch = Stopwatch.StartNew(); + while (searchingMaps.Any() && stopwatch.ElapsedMilliseconds < 10000) + Thread.Sleep(100); + } + + if (unknownMaps.Any()) + Log.Write("server", "Failed to resolve maps: " + unknownMaps.JoinWith(", ")); + } + + static string ChooseInitialMap(S server) + { + if (server.MapIsKnown(server.Settings.Map)) + return server.Settings.Map; + + if (server.MapPool == null) + return server.ModData.MapCache.ChooseInitialMap(server.Settings.Map, new MersenneTwister()); + + return server.MapPool + .Where(server.MapIsKnown) + .RandomOrDefault(new MersenneTwister()); + } + public void ServerStarted(S server) { lock (server.LobbyInfo) { - // 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 InvalidOperationException($"Map {uid} not found"); + InitializeMapPool(server); + var uid = ChooseInitialMap(server); + if (string.IsNullOrEmpty(uid)) + throw new InvalidOperationException("Unable to resolve a valid initial map"); + + server.LobbyInfo.GlobalSettings.Map = server.Settings.Map = uid; + server.Map = server.ModData.MapCache[uid]; + server.LobbyInfo.GlobalSettings.MapStatus = server.MapStatusCache[server.Map]; server.LobbyInfo.Slots = server.Map.Players.Players .Select(p => MakeSlotFromPlayerReference(p.Value)) .Where(s => s != null) diff --git a/OpenRA.Server/Program.cs b/OpenRA.Server/Program.cs index 8bf51c742d..21697e1f06 100644 --- a/OpenRA.Server/Program.cs +++ b/OpenRA.Server/Program.cs @@ -16,7 +16,6 @@ using System.IO; using System.Net; using System.Threading; using OpenRA.Network; -using OpenRA.Support; namespace OpenRA.Server { @@ -90,8 +89,6 @@ namespace OpenRA.Server // HACK: Related to the above one, initialize the translations so we can load maps with their (translated) lobby options. TranslationProvider.Initialize(modData, modData.DefaultFileSystem); - settings.Map = modData.MapCache.ChooseInitialMap(settings.Map, new MersenneTwister()); - var endpoints = new List { new IPEndPoint(IPAddress.IPv6Any, settings.ListenPort), new IPEndPoint(IPAddress.Any, settings.ListenPort) }; var server = new Server(endpoints, settings, modData, ServerType.Dedicated);