Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom map pool support for dedicated servers #21179

Merged
merged 3 commits into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions OpenRA.Game/Map/MapCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable<string> uids
.ToList();

foreach (var uid in queryUids)
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null);
previews[uid].UpdateRemoteSearch(MapStatus.Searching, null, null);

Task.Run(async () =>
{
Expand All @@ -251,13 +251,13 @@ public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable<string> uids

var yaml = MiniYaml.FromStream(result);
foreach (var kv in yaml)
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived);
previews[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, modData.Manifest.MapCompatibility, mapDetailsReceived);

foreach (var uid in batchUids)
{
var p = previews[uid];
if (p.Status != MapStatus.DownloadAvailable)
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
}
}
catch (Exception e)
Expand All @@ -269,7 +269,7 @@ public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable<string> uids
foreach (var uid in batchUids)
{
var p = previews[uid];
p.UpdateRemoteSearch(MapStatus.Unavailable, null);
p.UpdateRemoteSearch(MapStatus.Unavailable, null, null);
mapQueryFailed?.Invoke(p);
}
}
Expand Down
9 changes: 8 additions & 1 deletion OpenRA.Game/Map/MapPreview.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class RemoteMapData
public readonly string rules;
public readonly string players_block;
public readonly int mapformat;
public readonly string game_mod;
}

public sealed class MapPreview : IDisposable, IReadOnlyFileSystem
Expand Down Expand Up @@ -427,7 +428,7 @@ public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassi
innerData = newData;
}

public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, string[] mapCompatibility, Action<MapPreview> parseMetadata = null)
{
var newData = innerData.Clone();
newData.Status = status;
Expand Down Expand Up @@ -479,6 +480,12 @@ public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPrevie
var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
newData.SetCustomRules(modData, this, rulesYaml, null);

// Map is for a different mod: update its information so it can be displayed
// in the cross-mod server browser UI, but mark it as unavailable so it can't
// be selected in a server for the current mod.
if (!mapCompatibility.Contains(r.game_mod))
newData.Status = MapStatus.Unavailable;
}
catch (Exception e)
{
Expand Down
3 changes: 3 additions & 0 deletions OpenRA.Game/Network/OrderManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public sealed class OrderManager : IDisposable
public string ServerError = null;
public bool AuthenticationFailed = false;

// The default null means "no map restriction" while an empty set means "all maps restricted"
public HashSet<string> ServerMapPool = null;

Comment on lines +42 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really need to have a better system to store stuff (with proper lifecycle), now we are storing it here because its convenient.

public int NetFrameNumber { get; private set; }
public int LocalFrameNumber;

Expand Down
6 changes: 6 additions & 0 deletions OpenRA.Game/Network/UnitOrders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,12 @@ internal static void ProcessOrder(OrderManager orderManager, World world, int cl
break;
}

case "SyncMapPool":
{
orderManager.ServerMapPool = FieldLoader.GetValue<HashSet<string>>("SyncMapPool", order.TargetString);
break;
}

default:
{
if (world == null)
Expand Down
30 changes: 24 additions & 6 deletions OpenRA.Game/Server/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ public sealed class Server
// Managed by LobbyCommands
public MapPreview Map;
public readonly MapStatusCache MapStatusCache;
public GameSave GameSave = null;
public GameSave GameSave;
public HashSet<string> MapPool;

// Default to the next frame for ServerType.Local - MP servers take the value from the selected GameSpeed.
public int OrderLatency = 1;
Expand Down Expand Up @@ -316,7 +317,6 @@ public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modDa

serverTraits.TrimExcess();

Map = ModData.MapCache[settings.Map];
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);

playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
Expand All @@ -327,8 +327,6 @@ public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modDa
GlobalSettings =
{
RandomSeed = randomSeed,
Map = Map.Uid,
MapStatus = Session.MapStatus.Unknown,
ServerName = settings.Name,
EnableSingleplayer = settings.EnableSingleplayer || Type != ServerType.Dedicated,
EnableSyncReports = settings.EnableSyncReports,
Expand All @@ -348,8 +346,7 @@ public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modDa

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<INotifyServerStart>())
t.ServerStarted(this);

Expand Down Expand Up @@ -1434,6 +1431,27 @@ public ConnectionTarget GetEndpointForLocalConnection()
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
Expand Down
3 changes: 3 additions & 0 deletions OpenRA.Game/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ public class ServerSettings
[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<string>();

[Desc("Delay in milliseconds before newly joined players can send chat messages.")]
public int FloodLimitJoinCooldown = 5000;

Expand Down
78 changes: 71 additions & 7 deletions OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -570,6 +573,12 @@ static bool Map(S server, Connection conn, Session.Client client, string s)
return true;
}

if (server.MapPool != null && !server.MapPool.Contains(s))
{
QueryFailed();
return true;
}

var lastMap = server.LobbyInfo.GlobalSettings.Map;
void SelectMap(MapPreview map)
{
Expand Down Expand Up @@ -659,8 +668,6 @@ void SelectMap(MapPreview map)
}
}

void QueryFailed() => server.SendLocalizedMessageTo(conn, UnknownMap);

var m = server.ModData.MapCache[s];
if (m.Status == MapStatus.Available || m.Status == MapStatus.DownloadAvailable)
SelectMap(m);
Expand All @@ -682,6 +689,8 @@ void SelectMap(MapPreview map)

return true;
}

void QueryFailed() => server.SendLocalizedMessageTo(conn, UnknownMap);
}

static bool Option(S server, Connection conn, Session.Client client, string s)
Expand Down Expand Up @@ -1227,16 +1236,68 @@ static bool SyncLobby(S server, Connection conn, Session.Client client, string s
}
}

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<WebServices>().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)
Expand Down Expand Up @@ -1335,6 +1396,9 @@ public void ClientJoined(S server, Connection conn)
{
lock (server.LobbyInfo)
{
if (server.MapPool != null)
server.SendOrderTo(conn, "SyncMapPool", FieldSaver.FormatValue(server.MapPool));

var client = server.GetClient(conn);

// Validate whether color is allowed and get an alternative if it isn't
Expand Down
4 changes: 3 additions & 1 deletion OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ void OnConnect()
var onSelect = new Action<string>(uid =>
{
// Don't select the same map again, and handle map becoming unavailable
if (uid == map.Uid || modData.MapCache[uid].Status != MapStatus.Available)
var status = modData.MapCache[uid].Status;
if (uid == map.Uid || (status != MapStatus.Available && status != MapStatus.DownloadAvailable))
return;

orderManager.IssueOrder(Order.Command("map " + uid));
Expand All @@ -250,6 +251,7 @@ void OnConnect()
Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs()
{
{ "initialMap", modData.MapCache.PickLastModifiedMap(MapVisibility.Lobby) ?? map.Uid },
{ "remoteMapPool", orderManager.ServerMapPool },
{ "initialTab", MapClassification.System },
{ "onExit", modData.MapCache.UpdateMaps },
{ "onSelect", Game.IsHost ? onSelect : null },
Expand Down
1 change: 1 addition & 0 deletions OpenRA.Mods.Common/Widgets/Logic/MainMenuLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ public MainMenuLogic(Widget widget, World world, ModData modData)
Game.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs()
{
{ "initialMap", null },
{ "remoteMapPool", null },
{ "initialTab", MapClassification.User },
{ "onExit", () => SwitchMenu(MenuType.MapEditor) },
{ "onSelect", onSelect },
Expand Down