Merge pull request #10992 from pchote/server-maps

Allow Dedicated Servers to query map info from the Resource Center.
This commit is contained in:
Matthias Mailänder
2016-04-03 12:19:41 +02:00
5 changed files with 312 additions and 181 deletions

View File

@@ -113,7 +113,7 @@ namespace OpenRA
} }
} }
public void QueryRemoteMapDetails(IEnumerable<string> uids) public void QueryRemoteMapDetails(IEnumerable<string> uids, Action<MapPreview> mapDetailsReceived = null, Action queryFailed = null)
{ {
var maps = uids.Distinct() var maps = uids.Distinct()
.Select(uid => previews[uid]) .Select(uid => previews[uid])
@@ -137,6 +137,9 @@ namespace OpenRA
foreach (var p in maps.Values) foreach (var p in maps.Values)
p.UpdateRemoteSearch(MapStatus.Unavailable, null); p.UpdateRemoteSearch(MapStatus.Unavailable, null);
if (queryFailed != null)
queryFailed();
return; return;
} }
@@ -145,11 +148,13 @@ namespace OpenRA
{ {
var yaml = MiniYaml.FromString(data); var yaml = MiniYaml.FromString(data);
foreach (var kv in yaml) foreach (var kv in yaml)
maps[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value); maps[kv.Key].UpdateRemoteSearch(MapStatus.DownloadAvailable, kv.Value, mapDetailsReceived);
} }
catch catch
{ {
Log.Write("debug", "Can't parse remote map search data:\n{0}", data); Log.Write("debug", "Can't parse remote map search data:\n{0}", data);
if (queryFailed != null)
queryFailed();
} }
}; };

View File

@@ -17,6 +17,7 @@ using System.Drawing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Text;
using System.Threading; using System.Threading;
using OpenRA.FileSystem; using OpenRA.FileSystem;
using OpenRA.Graphics; using OpenRA.Graphics;
@@ -51,10 +52,72 @@ namespace OpenRA
public readonly MapGridType map_grid_type; public readonly MapGridType map_grid_type;
public readonly string minimap; public readonly string minimap;
public readonly bool downloading; public readonly bool downloading;
public readonly string tileset;
public readonly string rules;
public readonly string players_block;
} }
public class MapPreview : IDisposable, IReadOnlyFileSystem public class MapPreview : IDisposable, IReadOnlyFileSystem
{ {
/// <summary>Wrapper that enables map data to be replaced in an atomic fashion</summary>
class InnerData
{
public string Title;
public string[] Categories;
public string Author;
public string TileSet;
public MapPlayers Players;
public int PlayerCount;
public CPos[] SpawnPoints;
public MapGridType GridType;
public Rectangle Bounds;
public Bitmap Preview;
public MapStatus Status;
public MapClassification Class;
public MapVisibility Visibility;
public bool SuitableForInitialMap;
Lazy<Ruleset> rules;
public Ruleset Rules { get { return rules != null ? rules.Value : null; } }
public bool InvalidCustomRules { get; private set; }
public bool RulesLoaded { get; private set; }
public void SetRulesetGenerator(ModData modData, Func<Ruleset> generator)
{
InvalidCustomRules = false;
RulesLoaded = false;
// Note: multiple threads may try to access the value at the same time
// We rely on the thread-safety guarantees given by Lazy<T> to prevent race conitions.
// If you're thinking about replacing this, then you must be careful to keep this safe.
rules = Exts.Lazy(() =>
{
if (generator == null)
return Ruleset.LoadDefaultsForTileSet(modData, TileSet);
try
{
return generator();
}
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;
}
});
}
public InnerData Clone()
{
return (InnerData)MemberwiseClone();
}
}
static readonly CPos[] NoSpawns = new CPos[] { }; static readonly CPos[] NoSpawns = new CPos[] { };
MapCache cache; MapCache cache;
ModData modData; ModData modData;
@@ -63,25 +126,26 @@ namespace OpenRA
public IReadOnlyPackage Package { get; private set; } public IReadOnlyPackage Package { get; private set; }
IReadOnlyPackage parentPackage; IReadOnlyPackage parentPackage;
public string Title { get; private set; } volatile InnerData innerData;
public string[] Categories { get; private set; }
public string Author { get; private set; }
public string TileSet { get; private set; }
public MapPlayers Players { get; private set; }
public int PlayerCount { get; private set; }
public CPos[] SpawnPoints { get; private set; }
public MapGridType GridType { get; private set; }
public Rectangle Bounds { get; private set; }
public Bitmap Preview { get; private set; }
public MapStatus Status { get; private set; }
public MapClassification Class { get; private set; }
public MapVisibility Visibility { get; private set; }
public bool SuitableForInitialMap { get; private set; }
Lazy<Ruleset> rules; public string Title { get { return innerData.Title; } }
public Ruleset Rules { get { return rules != null ? rules.Value : null; } } public string[] Categories { get { return innerData.Categories; } }
public bool InvalidCustomRules { get; private set; } public string Author { get { return innerData.Author; } }
public bool RulesLoaded { get; private set; } public string TileSet { get { return innerData.TileSet; } }
public MapPlayers Players { get { return innerData.Players; } }
public int PlayerCount { get { return innerData.PlayerCount; } }
public CPos[] SpawnPoints { get { return innerData.SpawnPoints; } }
public MapGridType GridType { get { return innerData.GridType; } }
public Rectangle Bounds { get { return innerData.Bounds; } }
public Bitmap Preview { get { return innerData.Preview; } }
public MapStatus Status { get { return innerData.Status; } }
public MapClassification Class { get { return innerData.Class; } }
public MapVisibility Visibility { get { return innerData.Visibility; } }
public bool SuitableForInitialMap { get { return innerData.SuitableForInitialMap; } }
public Ruleset Rules { get { return innerData.Rules; } }
public bool InvalidCustomRules { get { return innerData.InvalidCustomRules; } }
public bool RulesLoaded { get { return innerData.RulesLoaded; } }
Download download; Download download;
public long DownloadBytes { get; private set; } public long DownloadBytes { get; private set; }
@@ -115,16 +179,23 @@ namespace OpenRA
this.modData = modData; this.modData = modData;
Uid = uid; Uid = uid;
Title = "Unknown Map"; innerData = new InnerData
Categories = new[] { "Unknown" }; {
Author = "Unknown Author"; Title = "Unknown Map",
PlayerCount = 0; Categories = new[] { "Unknown" },
Bounds = Rectangle.Empty; Author = "Unknown Author",
SpawnPoints = NoSpawns; TileSet = "unknown",
GridType = gridType; Players = null,
Status = MapStatus.Unavailable; PlayerCount = 0,
Class = MapClassification.Unknown; SpawnPoints = NoSpawns,
Visibility = MapVisibility.Lobby; GridType = gridType,
Bounds = Rectangle.Empty,
Preview = null,
Status = MapStatus.Unavailable,
Class = MapClassification.Unknown,
Visibility = MapVisibility.Lobby,
SuitableForInitialMap = false
};
} }
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType) public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType)
@@ -140,8 +211,10 @@ namespace OpenRA
Package = p; Package = p;
parentPackage = parent; parentPackage = parent;
GridType = gridType;
Class = classification; var newData = innerData.Clone();
newData.GridType = gridType;
newData.Class = classification;
MiniYaml temp; MiniYaml temp;
if (yaml.TryGetValue("MapFormat", out temp)) if (yaml.TryGetValue("MapFormat", out temp))
@@ -152,23 +225,29 @@ namespace OpenRA
} }
if (yaml.TryGetValue("Title", out temp)) if (yaml.TryGetValue("Title", out temp))
Title = temp.Value; newData.Title = temp.Value;
if (yaml.TryGetValue("Categories", out temp)) if (yaml.TryGetValue("Categories", out temp))
Categories = FieldLoader.GetValue<string[]>("Categories", temp.Value); newData.Categories = FieldLoader.GetValue<string[]>("Categories", temp.Value);
if (yaml.TryGetValue("Tileset", out temp)) if (yaml.TryGetValue("Tileset", out temp))
TileSet = temp.Value; newData.TileSet = temp.Value;
if (yaml.TryGetValue("Author", out temp)) if (yaml.TryGetValue("Author", out temp))
Author = temp.Value; newData.Author = temp.Value;
if (yaml.TryGetValue("Bounds", out temp)) if (yaml.TryGetValue("Bounds", out temp))
Bounds = FieldLoader.GetValue<Rectangle>("Bounds", temp.Value); newData.Bounds = FieldLoader.GetValue<Rectangle>("Bounds", temp.Value);
if (yaml.TryGetValue("Visibility", out temp)) if (yaml.TryGetValue("Visibility", out temp))
Visibility = FieldLoader.GetValue<MapVisibility>("Visibility", temp.Value); newData.Visibility = FieldLoader.GetValue<MapVisibility>("Visibility", temp.Value);
string requiresMod = string.Empty; string requiresMod = string.Empty;
if (yaml.TryGetValue("RequiresMod", out temp)) if (yaml.TryGetValue("RequiresMod", out temp))
requiresMod = temp.Value; requiresMod = temp.Value;
Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ? MapStatus.Available : MapStatus.Unavailable; newData.Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ?
MapStatus.Available : MapStatus.Unavailable;
try try
{ {
@@ -183,15 +262,15 @@ namespace OpenRA
spawns.Add(s.InitDict.Get<LocationInit>().Value(null)); spawns.Add(s.InitDict.Get<LocationInit>().Value(null));
} }
SpawnPoints = spawns.ToArray(); newData.SpawnPoints = spawns.ToArray();
} }
else else
SpawnPoints = new CPos[0]; newData.SpawnPoints = new CPos[0];
} }
catch (Exception) catch (Exception)
{ {
SpawnPoints = new CPos[0]; newData.SpawnPoints = new CPos[0];
Status = MapStatus.Unavailable; newData.Status = MapStatus.Unavailable;
} }
try try
@@ -200,47 +279,34 @@ namespace OpenRA
MiniYaml playerDefinitions; MiniYaml playerDefinitions;
if (yaml.TryGetValue("Players", out playerDefinitions)) if (yaml.TryGetValue("Players", out playerDefinitions))
{ {
Players = new MapPlayers(playerDefinitions.Nodes); newData.Players = new MapPlayers(playerDefinitions.Nodes);
PlayerCount = Players.Players.Count(x => x.Value.Playable); newData.PlayerCount = newData.Players.Players.Count(x => x.Value.Playable);
SuitableForInitialMap = EvaluateUserFriendliness(Players.Players); newData.SuitableForInitialMap = EvaluateUserFriendliness(newData.Players.Players);
} }
} }
catch (Exception) catch (Exception)
{ {
Status = MapStatus.Unavailable; newData.Status = MapStatus.Unavailable;
} }
// Note: multiple threads may try to access the value at the same time newData.SetRulesetGenerator(modData, () =>
// We rely on the thread-safety guarantees given by Lazy<T> to prevent race conitions.
// If you're thinking about replacing this, then you must be careful to keep this safe.
rules = Exts.Lazy(() =>
{ {
try var ruleDefinitions = LoadRuleSection(yaml, "Rules");
{ var weaponDefinitions = LoadRuleSection(yaml, "Weapons");
var ruleDefinitions = LoadRuleSection(yaml, "Rules"); var voiceDefinitions = LoadRuleSection(yaml, "Voices");
var weaponDefinitions = LoadRuleSection(yaml, "Weapons"); var musicDefinitions = LoadRuleSection(yaml, "Music");
var voiceDefinitions = LoadRuleSection(yaml, "Voices"); var notificationDefinitions = LoadRuleSection(yaml, "Notifications");
var musicDefinitions = LoadRuleSection(yaml, "Music"); var sequenceDefinitions = LoadRuleSection(yaml, "Sequences");
var notificationDefinitions = LoadRuleSection(yaml, "Notifications"); return Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions,
var sequenceDefinitions = LoadRuleSection(yaml, "Sequences"); voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions);
return Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions,
voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions);
}
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;
}
}); });
if (p.Contains("map.png")) if (p.Contains("map.png"))
using (var dataStream = p.GetStream("map.png")) using (var dataStream = p.GetStream("map.png"))
Preview = new Bitmap(dataStream); newData.Preview = new Bitmap(dataStream);
// Assign the new data atomically
innerData = newData;
} }
MiniYaml LoadRuleSection(Dictionary<string, MiniYaml> yaml, string section) MiniYaml LoadRuleSection(Dictionary<string, MiniYaml> yaml, string section)
@@ -272,47 +338,70 @@ namespace OpenRA
return true; return true;
} }
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml) public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
{ {
// Update on the main thread to ensure consistency var newData = innerData.Clone();
Game.RunAfterTick(() => newData.Status = status;
newData.Class = MapClassification.Remote;
if (status == MapStatus.DownloadAvailable)
{ {
if (status == MapStatus.DownloadAvailable) try
{ {
try var r = FieldLoader.Load<RemoteMapData>(yaml);
// Map download has been disabled server side
if (!r.downloading)
{ {
var r = FieldLoader.Load<RemoteMapData>(yaml); newData.Status = MapStatus.Unavailable;
return;
// Map download has been disabled server side
if (!r.downloading)
{
Status = MapStatus.Unavailable;
return;
}
Title = r.title;
Categories = r.categories;
Author = r.author;
PlayerCount = r.players;
Bounds = r.bounds;
var spawns = new CPos[r.spawnpoints.Length / 2];
for (var j = 0; j < r.spawnpoints.Length; j += 2)
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
SpawnPoints = spawns;
GridType = r.map_grid_type;
Preview = new Bitmap(new MemoryStream(Convert.FromBase64String(r.minimap)));
} }
catch (Exception) { }
if (Preview != null) newData.Title = r.title;
cache.CacheMinimap(this); newData.Categories = r.categories;
newData.Author = r.author;
newData.PlayerCount = r.players;
newData.Bounds = r.bounds;
newData.TileSet = r.tileset;
var spawns = new CPos[r.spawnpoints.Length / 2];
for (var j = 0; j < r.spawnpoints.Length; j += 2)
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
newData.SpawnPoints = spawns;
newData.GridType = r.map_grid_type;
newData.Preview = new Bitmap(new MemoryStream(Convert.FromBase64String(r.minimap)));
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");
return Ruleset.Load(modData, this, TileSet, ruleDefinitions, weaponDefinitions,
voiceDefinitions, notificationDefinitions, musicDefinitions, sequenceDefinitions);
});
} }
catch (Exception) { }
Status = status; // Commit updated data before running the callbacks
Class = MapClassification.Remote; innerData = newData;
});
if (innerData.Preview != null)
cache.CacheMinimap(this);
if (parseMetadata != null)
parseMetadata(this);
}
// Update the status and class unconditionally
innerData = newData;
} }
public void Install(Action onSuccess) public void Install(Action onSuccess)
@@ -320,12 +409,12 @@ namespace OpenRA
if (Status != MapStatus.DownloadAvailable || !Game.Settings.Game.AllowDownloading) if (Status != MapStatus.DownloadAvailable || !Game.Settings.Game.AllowDownloading)
return; return;
Status = MapStatus.Downloading; innerData.Status = MapStatus.Downloading;
var installLocation = cache.MapLocations.FirstOrDefault(p => p.Value == MapClassification.User); var installLocation = cache.MapLocations.FirstOrDefault(p => p.Value == MapClassification.User);
if (installLocation.Key == null || !(installLocation.Key is IReadWritePackage)) if (installLocation.Key == null || !(installLocation.Key is IReadWritePackage))
{ {
Log.Write("debug", "Map install directory not found"); Log.Write("debug", "Map install directory not found");
Status = MapStatus.DownloadError; innerData.Status = MapStatus.DownloadError;
return; return;
} }
@@ -346,7 +435,7 @@ namespace OpenRA
// Map not found // Map not found
if (res.Headers["Content-Disposition"] == null) if (res.Headers["Content-Disposition"] == null)
{ {
Status = MapStatus.DownloadError; innerData.Status = MapStatus.DownloadError;
return; return;
} }
@@ -363,7 +452,7 @@ namespace OpenRA
Log.Write("debug", "Remote map download failed with error: {0}", i.Error != null ? i.Error.Message : "cancelled"); Log.Write("debug", "Remote map download failed with error: {0}", i.Error != null ? i.Error.Message : "cancelled");
Log.Write("debug", "URL was: {0}", mapUrl); Log.Write("debug", "URL was: {0}", mapUrl);
Status = MapStatus.DownloadError; innerData.Status = MapStatus.DownloadError;
return; return;
} }
@@ -382,7 +471,7 @@ namespace OpenRA
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e.Message); Console.WriteLine(e.Message);
Status = MapStatus.DownloadError; innerData.Status = MapStatus.DownloadError;
} }
}).Start(); }).Start();
} }
@@ -398,7 +487,7 @@ namespace OpenRA
public void Invalidate() public void Invalidate()
{ {
Status = MapStatus.Unavailable; innerData.Status = MapStatus.Unavailable;
} }
public void Dispose() public void Dispose()

View File

@@ -79,6 +79,9 @@ namespace OpenRA
[Desc("Disallow games where only one player plays with bots.")] [Desc("Disallow games where only one player plays with bots.")]
public bool DisableSinglePlayer = false; public bool DisableSinglePlayer = false;
[Desc("Query map information from the Resource Center if they are not available locally.")]
public bool QueryMapRepository = true;
public string TimestampFormat = "s"; public string TimestampFormat = "s";
public ServerSettings Clone() public ServerSettings Clone()

View File

@@ -13,6 +13,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Threading;
using OpenRA.Graphics; using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits; using OpenRA.Mods.Common.Traits;
using OpenRA.Network; using OpenRA.Network;
@@ -338,64 +339,87 @@ namespace OpenRA.Mods.Common.Server
return true; return true;
} }
if (server.ModData.MapCache[s].Status != MapStatus.Available) var lastMap = server.LobbyInfo.GlobalSettings.Map;
Action<MapPreview> selectMap = map =>
{ {
server.SendOrderTo(conn, "Message", "Map was not found on server."); // Make sure the map hasn't changed in the meantime
return true; if (server.LobbyInfo.GlobalSettings.Map != lastMap)
} return;
server.LobbyInfo.GlobalSettings.Map = s; server.LobbyInfo.GlobalSettings.Map = map.Uid;
var oldSlots = server.LobbyInfo.Slots.Keys.ToArray(); var oldSlots = server.LobbyInfo.Slots.Keys.ToArray();
LoadMap(server); server.Map = server.ModData.MapCache[server.LobbyInfo.GlobalSettings.Map];
// Reset client states server.LobbyInfo.Slots = server.Map.Players.Players
foreach (var c in server.LobbyInfo.Clients) .Select(p => MakeSlotFromPlayerReference(p.Value))
c.State = Session.ClientState.Invalid; .Where(ss => ss != null)
.ToDictionary(ss => ss.PlayerReference, ss => ss);
// Reassign players into new slots based on their old slots: LoadMapSettings(server.LobbyInfo.GlobalSettings, server.Map.Rules);
// - Observers remain as observers
// - Players who now lack a slot are made observers
// - Bots who now lack a slot are dropped
// - Bots who are not defined in the map rules are dropped
var botNames = server.Map.Rules.Actors["player"].TraitInfos<IBotInfo>().Select(t => t.Name);
var slots = server.LobbyInfo.Slots.Keys.ToArray();
var i = 0;
foreach (var os in oldSlots)
{
var c = server.LobbyInfo.ClientInSlot(os);
if (c == null)
continue;
c.SpawnPoint = 0; // Reset client states
c.Slot = i < slots.Length ? slots[i++] : null; foreach (var c in server.LobbyInfo.Clients)
if (c.Slot != null) c.State = Session.ClientState.Invalid;
// Reassign players into new slots based on their old slots:
// - Observers remain as observers
// - Players who now lack a slot are made observers
// - Bots who now lack a slot are dropped
// - Bots who are not defined in the map rules are dropped
var botNames = server.Map.Rules.Actors["player"].TraitInfos<IBotInfo>().Select(t => t.Name);
var slots = server.LobbyInfo.Slots.Keys.ToArray();
var i = 0;
foreach (var os in oldSlots)
{ {
// Remove Bot from slot if slot forbids bots var c = server.LobbyInfo.ClientInSlot(os);
if (c.Bot != null && (!server.Map.Players.Players[c.Slot].AllowBots || !botNames.Contains(c.Bot))) if (c == null)
continue;
c.SpawnPoint = 0;
c.Slot = i < slots.Length ? slots[i++] : null;
if (c.Slot != null)
{
// Remove Bot from slot if slot forbids bots
if (c.Bot != null && (!server.Map.Players.Players[c.Slot].AllowBots || !botNames.Contains(c.Bot)))
server.LobbyInfo.Clients.Remove(c);
S.SyncClientToPlayerReference(c, server.Map.Players.Players[c.Slot]);
}
else if (c.Bot != null)
server.LobbyInfo.Clients.Remove(c); server.LobbyInfo.Clients.Remove(c);
S.SyncClientToPlayerReference(c, server.Map.Players.Players[c.Slot]);
} }
else if (c.Bot != null)
server.LobbyInfo.Clients.Remove(c); // Validate if color is allowed and get an alternative if it isn't
foreach (var c in server.LobbyInfo.Clients)
if (c.Slot == null || (c.Slot != null && !server.LobbyInfo.Slots[c.Slot].LockColor))
c.Color = c.PreferredColor = SanitizePlayerColor(server, c.Color, c.Index, conn);
server.SyncLobbyInfo();
server.SendMessage("{0} changed the map to {1}.".F(client.Name, server.Map.Title));
if (server.Map.Rules.Actors != server.ModData.DefaultRules.Actors)
server.SendMessage("This map contains custom rules. Game experience may change.");
if (server.Settings.DisableSinglePlayer)
server.SendMessage("Singleplayer games have been disabled on this server.");
else if (server.Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots))
server.SendMessage("Bots have been disabled on this map.");
};
Action queryFailed = () =>
server.SendOrderTo(conn, "Message", "Map was not found on server.");
var m = server.ModData.MapCache[s];
if (m.Status == MapStatus.Available || m.Status == MapStatus.DownloadAvailable)
selectMap(m);
else if (server.Settings.QueryMapRepository)
{
server.SendOrderTo(conn, "Message", "Searching for map on the Resource Center...");
server.ModData.MapCache.QueryRemoteMapDetails(new[] { s }, selectMap, queryFailed);
} }
else
// Validate if color is allowed and get an alternative it isn't queryFailed();
foreach (var c in server.LobbyInfo.Clients)
if (c.Slot == null || (c.Slot != null && !server.LobbyInfo.Slots[c.Slot].LockColor))
c.Color = c.PreferredColor = SanitizePlayerColor(server, c.Color, c.Index, conn);
server.SyncLobbyInfo();
server.SendMessage("{0} changed the map to {1}.".F(client.Name, server.Map.Title));
if (server.Map.Rules.Actors != server.ModData.DefaultRules.Actors)
server.SendMessage("This map contains custom rules. Game experience may change.");
if (server.Settings.DisableSinglePlayer)
server.SendMessage("Singleplayer games have been disabled on this server.");
else if (server.Map.Players.Players.Where(p => p.Value.Playable).All(p => !p.Value.AllowBots))
server.SendMessage("Bots have been disabled on this map.");
return true; return true;
} }
@@ -996,7 +1020,18 @@ namespace OpenRA.Mods.Common.Server
public void ServerStarted(S server) public void ServerStarted(S server)
{ {
LoadMap(server); // 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 Exception("Map {0} not found".F(uid));
server.LobbyInfo.Slots = server.Map.Players.Players
.Select(p => MakeSlotFromPlayerReference(p.Value))
.Where(s => s != null)
.ToDictionary(s => s.PlayerReference, s => s);
LoadMapSettings(server.LobbyInfo.GlobalSettings, server.Map.Rules);
} }
static Session.Slot MakeSlotFromPlayerReference(PlayerReference pr) static Session.Slot MakeSlotFromPlayerReference(PlayerReference pr)
@@ -1045,18 +1080,6 @@ namespace OpenRA.Mods.Common.Server
gs.Difficulty = mapOptions.Difficulty ?? mapOptions.Difficulties.FirstOrDefault(); gs.Difficulty = mapOptions.Difficulty ?? mapOptions.Difficulties.FirstOrDefault();
} }
static void LoadMap(S server)
{
server.Map = server.ModData.MapCache[server.LobbyInfo.GlobalSettings.Map];
server.LobbyInfo.Slots = server.Map.Players.Players
.Select(p => MakeSlotFromPlayerReference(p.Value))
.Where(s => s != null)
.ToDictionary(s => s.PlayerReference, s => s);
LoadMapSettings(server.LobbyInfo.GlobalSettings, server.Map.Rules);
}
static HSLColor SanitizePlayerColor(S server, HSLColor askedColor, int playerIndex, Connection connectionToEcho = null) static HSLColor SanitizePlayerColor(S server, HSLColor askedColor, int playerIndex, Connection connectionToEcho = null)
{ {
var validator = server.ModData.Manifest.Get<ColorValidator>(); var validator = server.ModData.Manifest.Get<ColorValidator>();

View File

@@ -326,7 +326,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var startGameButton = lobby.GetOrNull<ButtonWidget>("START_GAME_BUTTON"); var startGameButton = lobby.GetOrNull<ButtonWidget>("START_GAME_BUTTON");
if (startGameButton != null) if (startGameButton != null)
{ {
startGameButton.IsDisabled = () => configurationDisabled() || startGameButton.IsDisabled = () => configurationDisabled() || Map.Status != MapStatus.Available ||
orderManager.LobbyInfo.Slots.Any(sl => sl.Value.Required && orderManager.LobbyInfo.ClientInSlot(sl.Key) == null) || orderManager.LobbyInfo.Slots.Any(sl => sl.Value.Required && orderManager.LobbyInfo.ClientInSlot(sl.Key) == null) ||
(orderManager.LobbyInfo.GlobalSettings.DisableSingleplayer && orderManager.LobbyInfo.IsSinglePlayer); (orderManager.LobbyInfo.GlobalSettings.DisableSingleplayer && orderManager.LobbyInfo.IsSinglePlayer);
@@ -464,8 +464,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
}; };
startingUnits.IsDisabled = () => configurationDisabled() || startUnitsLocked.Update(Map); startingUnits.IsDisabled = () => configurationDisabled() || startUnitsLocked.Update(Map);
startingUnits.GetText = () => Map.Status != MapStatus.Available || startingUnits.GetText = () => !Map.RulesLoaded || startUnitsLocked.Update(Map) ?
!Map.RulesLoaded || startUnitsLocked.Update(Map) ? "Not Available" : className(orderManager.LobbyInfo.GlobalSettings.StartingUnitsClass); "Not Available" : className(orderManager.LobbyInfo.GlobalSettings.StartingUnitsClass);
startingUnits.OnMouseDown = _ => startingUnits.OnMouseDown = _ =>
{ {
var classes = startUnitsInfos.Update(Map).Select(a => a.Class).Distinct(); var classes = startUnitsInfos.Update(Map).Select(a => a.Class).Distinct();
@@ -496,8 +496,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
map => map.Rules.Actors["player"].TraitInfo<PlayerResourcesInfo>()); map => map.Rules.Actors["player"].TraitInfo<PlayerResourcesInfo>());
startingCash.IsDisabled = () => configurationDisabled() || playerResources.Update(Map).DefaultCashLocked; startingCash.IsDisabled = () => configurationDisabled() || playerResources.Update(Map).DefaultCashLocked;
startingCash.GetText = () => Map.Status != MapStatus.Available || startingCash.GetText = () => !Map.RulesLoaded || playerResources.Update(Map).DefaultCashLocked ?
!Map.RulesLoaded || playerResources.Update(Map).DefaultCashLocked ? "Not Available" : "${0}".F(orderManager.LobbyInfo.GlobalSettings.StartingCash); "Not Available" : "${0}".F(orderManager.LobbyInfo.GlobalSettings.StartingCash);
startingCash.OnMouseDown = _ => startingCash.OnMouseDown = _ =>
{ {
var options = playerResources.Update(Map).SelectableCash.Select(c => new DropDownOption var options = playerResources.Update(Map).SelectableCash.Select(c => new DropDownOption
@@ -533,8 +533,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
techLevelDescription.IsVisible = techLevel.IsVisible; techLevelDescription.IsVisible = techLevel.IsVisible;
techLevel.IsDisabled = () => configurationDisabled() || mapOptions.Update(Map).TechLevelLocked; techLevel.IsDisabled = () => configurationDisabled() || mapOptions.Update(Map).TechLevelLocked;
techLevel.GetText = () => Map.Status != MapStatus.Available || techLevel.GetText = () => !Map.RulesLoaded || mapOptions.Update(Map).TechLevelLocked ?
!Map.RulesLoaded || mapOptions.Update(Map).TechLevelLocked ? "Not Available" : "{0}".F(orderManager.LobbyInfo.GlobalSettings.TechLevel); "Not Available" : "{0}".F(orderManager.LobbyInfo.GlobalSettings.TechLevel);
techLevel.OnMouseDown = _ => techLevel.OnMouseDown = _ =>
{ {
var options = techLevels.Update(Map).Select(c => new DropDownOption var options = techLevels.Update(Map).Select(c => new DropDownOption
@@ -776,6 +776,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic
return true; return true;
} }
void LoadMapPreviewRules(MapPreview map)
{
new Task(() =>
{
// Force map rules to be loaded on this background thread
var unused = map.Rules;
}).Start();
}
void UpdateCurrentMap() void UpdateCurrentMap()
{ {
var uid = orderManager.LobbyInfo.GlobalSettings.Map; var uid = orderManager.LobbyInfo.GlobalSettings.Map;
@@ -814,8 +823,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
}); });
}).Start(); }).Start();
} }
else if (Map.Status == MapStatus.DownloadAvailable)
LoadMapPreviewRules(Map);
else if (Game.Settings.Game.AllowDownloading) else if (Game.Settings.Game.AllowDownloading)
modData.MapCache.QueryRemoteMapDetails(new[] { uid }); modData.MapCache.QueryRemoteMapDetails(new[] { uid }, LoadMapPreviewRules);
} }
void UpdatePlayerList() void UpdatePlayerList()