Rework MapPreview custom rule handling.

The previous asynchronous approach did not work particularly well,
leading to large janks when switching to custom maps or opening the
mission browser.

This commit introduces two key changes:

 * Rule loading for WorldActorInfo and PlayerActorInfo is made
   synchronous, in preparation for the next commit which will
   significantly optimize this path.
 * The full ruleset loading, which is required for map validation,
   is moved to the server-side and managed by a new ServerMapStatusCache.
   The previous syntax check is expanded to include the ability to run
   lint tests.
This commit is contained in:
Paul Chote
2021-04-06 13:53:38 +01:00
committed by teinarss
parent 61d64287e1
commit 0bbb32e8ac
17 changed files with 373 additions and 229 deletions

View File

@@ -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<Ruleset> 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<string, MiniYaml> 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<T> 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<string, MiniYaml> yaml)
{
if (generator == null)
return Ruleset.LoadDefaultsForTileSet(modData, TileSet);
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
{
var ret = generator();
DefinesUnsafeCustomRules = ret.DefinesUnsafeCustomRules;
return ret.Ruleset;
var rules = Ruleset.Load(modData, fileSystem, TileSet, RuleDefinitions,
WeaponDefinitions, VoiceDefinitions, NotificationDefinitions,
MusicDefinitions, SequenceDefinitions, ModelSequenceDefinitions);
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);
InvalidCustomRules = true;
return Ruleset.LoadDefaultsForTileSet(modData, TileSet);
WorldActorInfo = modData.DefaultRules.Actors[SystemActors.World];
PlayerActorInfo = modData.DefaultRules.Actors[SystemActors.Player];
}
finally
{
RulesLoaded = true;
}
});
}
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<string, MiniYaml> 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<MapPreview> 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);
});
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
{
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType);
onSuccess();
Game.RunAfterTick(onSuccess);
}
});
}
catch (Exception e)
{
Console.WriteLine(e.Message);
Log.Write("debug", "Map installation failed with error: {0}", e);
innerData.Status = MapStatus.DownloadError;
}
});

View File

@@ -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;

View File

@@ -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<string> emitError, Action<string> emitWarning, ModData modData, MapPreview map, Ruleset mapRules); }
public class MapStatusCache
{
readonly Dictionary<MapPreview, Session.MapStatus> cache = new Dictionary<MapPreview, Session.MapStatus>();
readonly Action<string, Session.MapStatus> onStatusChanged;
readonly bool enableRemoteLinting;
readonly ModData modData;
public MapStatusCache(ModData modData, Action<string, Session.MapStatus> 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<string> onLintFailure = message =>
{
Log.Write("server", "Map {0} failed lint with error: {1}", map.Title, message);
failed = true;
};
Action<string> onLintWarning = _ => { };
foreach (var customMapPassType in modData.ObjectCreator.GetTypesImplementing<ILintServerMapPass>())
{
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;
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<IPEndPoint> 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<INotifyServerStart>())
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)

View File

@@ -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();

View File

@@ -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)

View File

@@ -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<string>("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<MapPreview>)(() => map) },
{ "getMap", (Func<(MapPreview, Session.MapStatus)>)(() => (map, mapStatus)) },
{
"onMouseDown", (Action<MapPreviewWidget, MapPreview, MouseInput>)((preview, mapPreview, mi) =>
LobbyUtils.SelectSpawnPoint(orderManager, preview, mapPreview, mi))
@@ -163,8 +167,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var gameStarting = false;
Func<bool> 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<ButtonWidget>("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<ButtonWidget>("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(() =>
{
// 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;
// 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<IBotInfo>().Select(t => t.Type).FirstOrDefault();
var bot = map.PlayerActorInfo.TraitInfos<IBotInfo>().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();
}
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);
}

View File

@@ -30,7 +30,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic
readonly OrderManager orderManager;
readonly Func<bool> configurationDisabled;
MapPreview mapPreview;
bool validOptions;
[ObjectCreator.UseCtor]
internal LobbyOptionsLogic(Widget widget, OrderManager orderManager, Func<MapPreview> getMap, Func<bool> 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(() =>
{
mapPreview = newMapPreview;
RebuildOptions();
validOptions = true;
});
}
else
validOptions = false;
}
void RebuildOptions()
{
if (mapPreview == null || mapPreview.WorldActorInfo == null || mapPreview.InvalidCustomRules)
if (mapPreview == null || mapPreview.WorldActorInfo == null)
return;
optionsContainer.RemoveChildren();

View File

@@ -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<CheckboxWidget>("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)));

View File

@@ -24,8 +24,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
int blinkTick;
[ObjectCreator.UseCtor]
internal MapPreviewLogic(Widget widget, ModData modData, OrderManager orderManager, Func<MapPreview> getMap, Action<MapPreviewWidget, MapPreview, MouseInput> onMouseDown,
Func<Dictionary<int, SpawnOccupant>> getSpawnOccupants, Func<HashSet<int>> getDisabledSpawnPoints, bool showUnoccupiedSpawnpoints)
internal MapPreviewLogic(Widget widget, ModData modData, OrderManager orderManager, Func<(MapPreview Map, Session.MapStatus Status)> getMap,
Action<MapPreviewWidget, MapPreview, MouseInput> onMouseDown, Func<Dictionary<int, SpawnOccupant>> getSpawnOccupants,
Func<HashSet<int>> getDisabledSpawnPoints, bool showUnoccupiedSpawnpoints)
{
var mapRepository = modData.Manifest.Get<WebServices>().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<ButtonWidget>("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<LabelWidget>("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<ProgressBarWidget>("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<MapPreview> getMap,
Action<MapPreviewWidget, MapPreview, MouseInput> onMouseDown, Func<Dictionary<int, SpawnOccupant>> getSpawnOccupants, Func<HashSet<int>> getDisabledSpawnPoints, bool showUnoccupiedSpawnpoints)
void SetupWidgets(Widget parent,
Func<(MapPreview Map, Session.MapStatus Status)> getMap,
Action<MapPreviewWidget, MapPreview, MouseInput> onMouseDown,
Func<Dictionary<int, SpawnOccupant>> getSpawnOccupants,
Func<HashSet<int>> getDisabledSpawnPoints,
bool showUnoccupiedSpawnpoints)
{
var preview = parent.Get<MapPreviewWidget>("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<LabelWithTooltipWidget>("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<MapPreview, string>(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<LabelWidget>("MAP_TYPE");
if (typeLabel != null)
{
var type = new CachedTransform<MapPreview, string>(m => m.Categories.FirstOrDefault() ?? "");
typeLabel.GetText = () => type.Update(getMap());
typeLabel.GetText = () => type.Update(getMap().Map);
}
var authorLabel = parent.GetOrNull<LabelWidget>("MAP_AUTHOR");
@@ -204,7 +227,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var font = Game.Renderer.Fonts[authorLabel.Font];
var author = new CachedTransform<MapPreview, string>(
m => WidgetUtils.TruncateText("Created by {0}".F(m.Author), authorLabel.Bounds.Width, font));
authorLabel.GetText = () => author.Update(getMap());
authorLabel.GetText = () => author.Update(getMap().Map);
}
}
}

View File

@@ -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<ButtonWidget>("STARTGAME_BUTTON");
startButton.OnClick = StartMissionClicked;
startButton.IsDisabled = () => selectedMap == null || selectedMap.InvalidCustomRules;
startButton.IsDisabled = () => selectedMap == null;
widget.Get<ButtonWidget>("BACK_BUTTON").OnClick = () =>
{
@@ -373,9 +370,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic
{
StopVideo(videoPlayer);
if (selectedMap.InvalidCustomRules)
return;
var orders = new List<Order>();
if (difficulty != null)
orders.Add(Order.Command("option difficulty {0}".F(difficulty)));

View File

@@ -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<MapPreview>)(() => map) },
{ "getMap", (Func<(MapPreview, Session.MapStatus)>)(() => (map, Session.MapStatus.Playable)) },
{ "onMouseDown", (Action<MapPreviewWidget, MapPreview, MouseInput>)((preview, mapPreview, mi) => { }) },
{ "getSpawnOccupants", (Func<Dictionary<int, SpawnOccupant>>)(() => spawnOccupants.Update(selectedReplay)) },
{ "getDisabledSpawnPoints", (Func<HashSet<int>>)(() => 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))

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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