From 72646fc7ff59af8c1762354703b8dea1ee1203ba Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 9 May 2021 17:18:25 +0100 Subject: [PATCH] Add Server.MapPool setting for dedicated servers. This takes a list of map UIDs which may be locally installed or hosted on the resource center. If any maps aren't found, startup will be delayed by up to 10 seconds while it attempts to query the resource center. --- OpenRA.Game/Server/Server.cs | 30 ++++++-- OpenRA.Game/Settings.cs | 3 + .../ServerTraits/LobbyCommands.cs | 75 +++++++++++++++++-- OpenRA.Server/Program.cs | 3 - 4 files changed, 95 insertions(+), 16 deletions(-) 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);