Files
OpenRA/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs
Paul Chote 0bbb32e8ac 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.
2021-04-21 18:57:44 +02:00

818 lines
28 KiB
C#

#region Copyright & License Information
/*
* Copyright 2007-2020 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.Linq;
using System.Threading.Tasks;
using OpenRA.Graphics;
using OpenRA.Network;
using OpenRA.Primitives;
using OpenRA.Traits;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
{
public class LobbyLogic : ChromeLogic
{
static readonly Action DoNothing = () => { };
readonly ModData modData;
readonly Action onStart;
readonly Action onExit;
readonly OrderManager orderManager;
readonly WorldRenderer worldRenderer;
readonly bool skirmishMode;
readonly Ruleset modRules;
readonly World shellmapWorld;
readonly WebServices services;
enum PanelType { Players, Options, Music, Servers, Kick, ForceStart }
PanelType panel = PanelType.Players;
readonly Widget lobby;
readonly Widget editablePlayerTemplate;
readonly Widget nonEditablePlayerTemplate;
readonly Widget emptySlotTemplate;
readonly Widget editableSpectatorTemplate;
readonly Widget nonEditableSpectatorTemplate;
readonly Widget newSpectatorTemplate;
readonly ScrollPanelWidget lobbyChatPanel;
readonly Widget chatTemplate;
readonly ScrollPanelWidget players;
readonly Dictionary<string, LobbyFaction> factions = new Dictionary<string, LobbyFaction>();
readonly ColorPreviewManagerWidget colorPreview;
readonly TabCompletionLogic tabCompletion = new TabCompletionLogic();
MapPreview map;
Session.MapStatus mapStatus;
bool addBotOnMapLoad;
bool disableTeamChat;
bool insufficientPlayerSpawns;
bool teamChat;
bool updateDiscordStatus = true;
Dictionary<int, SpawnOccupant> spawnOccupants = new Dictionary<int, SpawnOccupant>();
readonly string chatLineSound = ChromeMetrics.Get<string>("ChatLineSound");
bool MapIsPlayable => (mapStatus & Session.MapStatus.Playable) == Session.MapStatus.Playable;
// Listen for connection failures
void ConnectionStateChanged(OrderManager om)
{
if (om.Connection.ConnectionState == ConnectionState.NotConnected)
{
// Show connection failed dialog
Ui.CloseWindow();
Action onConnect = () =>
{
Game.OpenWindow("SERVER_LOBBY", new WidgetArgs()
{
{ "onExit", onExit },
{ "onStart", onStart },
{ "skirmishMode", false }
});
};
Action<string> onRetry = password => ConnectionLogic.Connect(om.Endpoint, password, onConnect, onExit);
var switchPanel = om.ServerExternalMod != null ? "CONNECTION_SWITCHMOD_PANEL" : "CONNECTIONFAILED_PANEL";
Ui.OpenWindow(switchPanel, new WidgetArgs()
{
{ "orderManager", om },
{ "onAbort", onExit },
{ "onRetry", onRetry }
});
}
}
[ObjectCreator.UseCtor]
internal LobbyLogic(Widget widget, ModData modData, WorldRenderer worldRenderer, OrderManager orderManager,
Action onExit, Action onStart, bool skirmishMode, Dictionary<string, MiniYaml> logicArgs)
{
map = MapCache.UnknownMap;
lobby = widget;
this.modData = modData;
this.orderManager = orderManager;
this.worldRenderer = worldRenderer;
this.onStart = onStart;
this.onExit = onExit;
this.skirmishMode = skirmishMode;
// TODO: This needs to be reworked to support per-map tech levels, bots, etc.
modRules = modData.DefaultRules;
shellmapWorld = worldRenderer.World;
services = modData.Manifest.Get<WebServices>();
orderManager.AddChatLine += AddChatLine;
Game.LobbyInfoChanged += UpdateCurrentMap;
Game.LobbyInfoChanged += UpdatePlayerList;
Game.LobbyInfoChanged += UpdateDiscordStatus;
Game.LobbyInfoChanged += UpdateSpawnOccupants;
Game.BeforeGameStart += OnGameStart;
Game.ConnectionStateChanged += ConnectionStateChanged;
var name = lobby.GetOrNull<LabelWidget>("SERVER_NAME");
if (name != null)
name.GetText = () => orderManager.LobbyInfo.GlobalSettings.ServerName;
var mapContainer = Ui.LoadWidget("MAP_PREVIEW", lobby.Get("MAP_PREVIEW_ROOT"), new WidgetArgs
{
{ "orderManager", orderManager },
{ "getMap", (Func<(MapPreview, Session.MapStatus)>)(() => (map, mapStatus)) },
{
"onMouseDown", (Action<MapPreviewWidget, MapPreview, MouseInput>)((preview, mapPreview, mi) =>
LobbyUtils.SelectSpawnPoint(orderManager, preview, mapPreview, mi))
},
{ "getSpawnOccupants", (Func<Dictionary<int, SpawnOccupant>>)(() => spawnOccupants) },
{ "getDisabledSpawnPoints", (Func<HashSet<int>>)(() => orderManager.LobbyInfo.DisabledSpawnPoints) },
{ "showUnoccupiedSpawnpoints", true },
});
mapContainer.IsVisible = () => panel != PanelType.Servers;
UpdateCurrentMap();
var playerBin = Ui.LoadWidget("LOBBY_PLAYER_BIN", lobby.Get("TOP_PANELS_ROOT"), new WidgetArgs());
playerBin.IsVisible = () => panel == PanelType.Players;
players = playerBin.Get<ScrollPanelWidget>("LOBBY_PLAYERS");
editablePlayerTemplate = players.Get("TEMPLATE_EDITABLE_PLAYER");
nonEditablePlayerTemplate = players.Get("TEMPLATE_NONEDITABLE_PLAYER");
emptySlotTemplate = players.Get("TEMPLATE_EMPTY");
editableSpectatorTemplate = players.Get("TEMPLATE_EDITABLE_SPECTATOR");
nonEditableSpectatorTemplate = players.Get("TEMPLATE_NONEDITABLE_SPECTATOR");
newSpectatorTemplate = players.Get("TEMPLATE_NEW_SPECTATOR");
colorPreview = lobby.Get<ColorPreviewManagerWidget>("COLOR_MANAGER");
colorPreview.Color = Game.Settings.Player.Color;
foreach (var f in modRules.Actors[SystemActors.World].TraitInfos<FactionInfo>())
factions.Add(f.InternalName, new LobbyFaction { Selectable = f.Selectable, Name = f.Name, Side = f.Side, Description = f.Description });
var gameStarting = false;
Func<bool> configurationDisabled = () => !Game.IsHost || gameStarting ||
panel == PanelType.Kick || panel == PanelType.ForceStart || !MapIsPlayable ||
orderManager.LocalClient == null || orderManager.LocalClient.IsReady;
var mapButton = lobby.GetOrNull<ButtonWidget>("CHANGEMAP_BUTTON");
if (mapButton != null)
{
mapButton.IsVisible = () => panel != PanelType.Servers;
mapButton.IsDisabled = () => gameStarting || panel == PanelType.Kick || panel == PanelType.ForceStart ||
orderManager.LocalClient == null || orderManager.LocalClient.IsReady;
mapButton.OnClick = () =>
{
var onSelect = new Action<string>(uid =>
{
// Don't select the same map again
if (uid == map.Uid)
return;
orderManager.IssueOrder(Order.Command("map " + uid));
Game.Settings.Server.Map = uid;
Game.Settings.Save();
});
Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs()
{
{ "initialMap", map.Uid },
{ "initialTab", MapClassification.System },
{ "onExit", DoNothing },
{ "onSelect", Game.IsHost ? onSelect : null },
{ "filter", MapVisibility.Lobby },
});
};
}
var slotsButton = lobby.GetOrNull<DropDownButtonWidget>("SLOTS_DROPDOWNBUTTON");
if (slotsButton != null)
{
slotsButton.IsVisible = () => panel != PanelType.Servers;
slotsButton.IsDisabled = () => configurationDisabled() || panel != PanelType.Players ||
(orderManager.LobbyInfo.Slots.Values.All(s => !s.AllowBots) &&
orderManager.LobbyInfo.Slots.Count(s => !s.Value.LockTeam && orderManager.LobbyInfo.ClientInSlot(s.Key) != null) == 0);
slotsButton.OnMouseDown = _ =>
{
var botTypes = map.PlayerActorInfo.TraitInfos<IBotInfo>().Select(t => t.Type);
var options = new Dictionary<string, IEnumerable<DropDownOption>>();
var botController = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.IsAdmin);
if (orderManager.LobbyInfo.Slots.Values.Any(s => s.AllowBots))
{
var botOptions = new List<DropDownOption>()
{
new DropDownOption()
{
Title = "Add",
IsSelected = () => false,
OnClick = () =>
{
foreach (var slot in orderManager.LobbyInfo.Slots)
{
var bot = botTypes.Random(Game.CosmeticRandom);
var c = orderManager.LobbyInfo.ClientInSlot(slot.Key);
if (slot.Value.AllowBots == true && (c == null || c.Bot != null))
orderManager.IssueOrder(Order.Command("slot_bot {0} {1} {2}".F(slot.Key, botController.Index, bot)));
}
}
}
};
if (orderManager.LobbyInfo.Clients.Any(c => c.Bot != null))
{
botOptions.Add(new DropDownOption()
{
Title = "Remove",
IsSelected = () => false,
OnClick = () =>
{
foreach (var slot in orderManager.LobbyInfo.Slots)
{
var c = orderManager.LobbyInfo.ClientInSlot(slot.Key);
if (c != null && c.Bot != null)
orderManager.IssueOrder(Order.Command("slot_open " + slot.Value.PlayerReference));
}
}
});
}
options.Add("Configure Bots", botOptions);
}
var teamCount = (orderManager.LobbyInfo.Slots.Count(s => !s.Value.LockTeam && orderManager.LobbyInfo.ClientInSlot(s.Key) != null) + 1) / 2;
if (teamCount >= 1)
{
var teamOptions = Enumerable.Range(2, teamCount - 1).Reverse().Select(d => new DropDownOption
{
Title = "{0} Teams".F(d),
IsSelected = () => false,
OnClick = () => orderManager.IssueOrder(Order.Command("assignteams {0}".F(d.ToString())))
}).ToList();
if (orderManager.LobbyInfo.Slots.Any(s => s.Value.AllowBots))
{
teamOptions.Add(new DropDownOption
{
Title = "Humans vs Bots",
IsSelected = () => false,
OnClick = () => orderManager.IssueOrder(Order.Command("assignteams 1"))
});
}
teamOptions.Add(new DropDownOption
{
Title = "Free for all",
IsSelected = () => false,
OnClick = () => orderManager.IssueOrder(Order.Command("assignteams 0"))
});
options.Add("Configure Teams", teamOptions);
}
Func<DropDownOption, ScrollItemWidget, ScrollItemWidget> setupItem = (option, template) =>
{
var item = ScrollItemWidget.Setup(template, option.IsSelected, option.OnClick);
item.Get<LabelWidget>("LABEL").GetText = () => option.Title;
return item;
};
slotsButton.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 175, options, setupItem);
};
}
var optionsBin = Ui.LoadWidget("LOBBY_OPTIONS_BIN", lobby.Get("TOP_PANELS_ROOT"), new WidgetArgs()
{
{ "orderManager", orderManager },
{ "getMap", (Func<MapPreview>)(() => map) },
{ "configurationDisabled", configurationDisabled }
});
optionsBin.IsVisible = () => panel == PanelType.Options;
var musicBin = Ui.LoadWidget("LOBBY_MUSIC_BIN", lobby.Get("TOP_PANELS_ROOT"), new WidgetArgs
{
{ "onExit", DoNothing },
{ "world", worldRenderer.World }
});
musicBin.IsVisible = () => panel == PanelType.Music;
ServerListLogic serverListLogic = null;
if (!skirmishMode)
{
Action<GameServer> doNothingWithServer = _ => { };
var serversBin = Ui.LoadWidget("LOBBY_SERVERS_BIN", lobby.Get("TOP_PANELS_ROOT"), new WidgetArgs
{
{ "onJoin", doNothingWithServer },
});
serverListLogic = serversBin.LogicObjects.Select(l => l as ServerListLogic).FirstOrDefault(l => l != null);
serversBin.IsVisible = () => panel == PanelType.Servers;
}
var tabContainer = skirmishMode ? lobby.Get("SKIRMISH_TABS") : lobby.Get("MULTIPLAYER_TABS");
tabContainer.IsVisible = () => true;
var optionsTab = tabContainer.Get<ButtonWidget>("OPTIONS_TAB");
optionsTab.IsHighlighted = () => panel == PanelType.Options;
optionsTab.IsDisabled = OptionsTabDisabled;
optionsTab.OnClick = () => panel = PanelType.Options;
var playersTab = tabContainer.Get<ButtonWidget>("PLAYERS_TAB");
playersTab.IsHighlighted = () => panel == PanelType.Players;
playersTab.IsDisabled = () => panel == PanelType.Kick || panel == PanelType.ForceStart;
playersTab.OnClick = () => panel = PanelType.Players;
var musicTab = tabContainer.Get<ButtonWidget>("MUSIC_TAB");
musicTab.IsHighlighted = () => panel == PanelType.Music;
musicTab.IsDisabled = () => panel == PanelType.Kick || panel == PanelType.ForceStart;
musicTab.OnClick = () => panel = PanelType.Music;
var serversTab = tabContainer.GetOrNull<ButtonWidget>("SERVERS_TAB");
if (serversTab != null)
{
serversTab.IsHighlighted = () => panel == PanelType.Servers;
serversTab.IsDisabled = () => panel == PanelType.Kick || panel == PanelType.ForceStart;
serversTab.OnClick = () =>
{
// Refresh the list when switching to the servers tab
if (serverListLogic != null && panel != PanelType.Servers)
serverListLogic.RefreshServerList();
panel = PanelType.Servers;
};
}
// Force start panel
Action startGame = () =>
{
gameStarting = true;
orderManager.IssueOrder(Order.Command("startgame"));
};
var startGameButton = lobby.GetOrNull<ButtonWidget>("START_GAME_BUTTON");
if (startGameButton != null)
{
startGameButton.IsDisabled = () => configurationDisabled() || map.Status != MapStatus.Available ||
orderManager.LobbyInfo.Slots.Any(sl => sl.Value.Required && orderManager.LobbyInfo.ClientInSlot(sl.Key) == null) ||
(!orderManager.LobbyInfo.GlobalSettings.EnableSingleplayer && orderManager.LobbyInfo.NonBotPlayers.Count() < 2) ||
insufficientPlayerSpawns;
startGameButton.OnClick = () =>
{
// Bots and admins don't count
if (orderManager.LobbyInfo.Clients.Any(c => c.Slot != null && !c.IsAdmin && c.Bot == null && !c.IsReady))
panel = PanelType.ForceStart;
else
startGame();
};
}
var forceStartBin = Ui.LoadWidget("FORCE_START_DIALOG", lobby.Get("TOP_PANELS_ROOT"), new WidgetArgs());
forceStartBin.IsVisible = () => panel == PanelType.ForceStart;
forceStartBin.Get("KICK_WARNING").IsVisible = () => orderManager.LobbyInfo.Clients.Any(c => c.IsInvalid);
forceStartBin.Get<ButtonWidget>("OK_BUTTON").OnClick = startGame;
forceStartBin.Get<ButtonWidget>("CANCEL_BUTTON").OnClick = () => panel = PanelType.Players;
var disconnectButton = lobby.Get<ButtonWidget>("DISCONNECT_BUTTON");
disconnectButton.OnClick = () => { Ui.CloseWindow(); onExit(); };
if (skirmishMode)
disconnectButton.Text = "Back";
var chatMode = lobby.Get<ButtonWidget>("CHAT_MODE");
chatMode.GetText = () => teamChat ? "Team" : "All";
chatMode.OnClick = () => teamChat ^= true;
chatMode.IsDisabled = () => disableTeamChat;
var chatTextField = lobby.Get<TextFieldWidget>("CHAT_TEXTFIELD");
chatTextField.MaxLength = UnitOrders.ChatMessageMaxLength;
chatTextField.TakeKeyboardFocus();
chatTextField.OnEnterKey = () =>
{
if (chatTextField.Text.Length == 0)
return true;
// Always scroll to bottom when we've typed something
lobbyChatPanel.ScrollToBottom();
var teamNumber = 0U;
if (teamChat && orderManager.LocalClient != null)
teamNumber = orderManager.LocalClient.IsObserver ? uint.MaxValue : (uint)orderManager.LocalClient.Team;
orderManager.IssueOrder(Order.Chat(chatTextField.Text, teamNumber));
chatTextField.Text = "";
return true;
};
chatTextField.OnTabKey = () =>
{
var previousText = chatTextField.Text;
chatTextField.Text = tabCompletion.Complete(chatTextField.Text);
chatTextField.CursorPosition = chatTextField.Text.Length;
if (chatTextField.Text == previousText)
return SwitchTeamChat();
else
return true;
};
chatTextField.OnEscKey = () => { chatTextField.Text = ""; return true; };
lobbyChatPanel = lobby.Get<ScrollPanelWidget>("CHAT_DISPLAY");
chatTemplate = lobbyChatPanel.Get("CHAT_TEMPLATE");
lobbyChatPanel.RemoveChildren();
var settingsButton = lobby.GetOrNull<ButtonWidget>("SETTINGS_BUTTON");
if (settingsButton != null)
{
settingsButton.OnClick = () => Ui.OpenWindow("SETTINGS_PANEL", new WidgetArgs
{
{ "onExit", DoNothing },
{ "worldRenderer", worldRenderer }
});
}
// Add a bot on the first lobbyinfo update
if (skirmishMode)
addBotOnMapLoad = true;
if (logicArgs.TryGetValue("ChatLineSound", out var yaml))
chatLineSound = yaml.Value;
}
bool disposed;
protected override void Dispose(bool disposing)
{
if (disposing && !disposed)
{
disposed = true;
orderManager.AddChatLine -= AddChatLine;
Game.LobbyInfoChanged -= UpdateCurrentMap;
Game.LobbyInfoChanged -= UpdatePlayerList;
Game.LobbyInfoChanged -= UpdateDiscordStatus;
Game.LobbyInfoChanged -= UpdateSpawnOccupants;
Game.BeforeGameStart -= OnGameStart;
Game.ConnectionStateChanged -= ConnectionStateChanged;
}
base.Dispose(disposing);
}
bool OptionsTabDisabled()
{
return !MapIsPlayable || panel == PanelType.Kick || panel == PanelType.ForceStart;
}
public override void Tick()
{
if (panel == PanelType.Options && OptionsTabDisabled())
panel = PanelType.Players;
}
void AddChatLine(string name, Color nameColor, string text, Color textColor)
{
var template = (ContainerWidget)chatTemplate.Clone();
LobbyUtils.SetupChatLine(template, DateTime.Now, name, nameColor, text, textColor);
var scrolledToBottom = lobbyChatPanel.ScrolledToBottom;
lobbyChatPanel.AddChild(template);
if (scrolledToBottom)
lobbyChatPanel.ScrollToBottom(smooth: true);
Game.Sound.PlayNotification(modRules, null, "Sounds", chatLineSound, null);
}
bool SwitchTeamChat()
{
if (!disableTeamChat)
teamChat ^= true;
return true;
}
void UpdateCurrentMap()
{
mapStatus = orderManager.LobbyInfo.GlobalSettings.MapStatus;
var uid = orderManager.LobbyInfo.GlobalSettings.Map;
if (map.Uid == uid)
return;
map = modData.MapCache[uid];
if (map.Status == MapStatus.Available)
{
// Tell the server that we have the map
orderManager.IssueOrder(Order.Command("state {0}".F(Session.ClientState.NotReady)));
if (addBotOnMapLoad)
{
var slot = orderManager.LobbyInfo.FirstEmptyBotSlot();
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;
}
}
else if (map.Status != MapStatus.DownloadAvailable && Game.Settings.Game.AllowDownloading)
modData.MapCache.QueryRemoteMapDetails(services.MapRepository, new[] { uid });
}
void UpdatePlayerList()
{
if (orderManager.LocalClient == null)
return;
// Check if we are not assigned to any team, and are no spectator
// If we are a spectator, check if there are more and enable spectator chat
// Otherwise check if our assigned team has more players
if (orderManager.LocalClient.Team == 0 && !orderManager.LocalClient.IsObserver)
disableTeamChat = true;
else if (orderManager.LocalClient.IsObserver)
disableTeamChat = !orderManager.LobbyInfo.Clients.Any(c => c != orderManager.LocalClient && c.IsObserver);
else
disableTeamChat = !orderManager.LobbyInfo.Clients.Any(c =>
c != orderManager.LocalClient &&
c.Bot == null &&
c.Team == orderManager.LocalClient.Team);
insufficientPlayerSpawns = LobbyUtils.InsufficientEnabledSpawnPoints(map, orderManager.LobbyInfo);
if (disableTeamChat)
teamChat = false;
var isHost = Game.IsHost;
var idx = 0;
foreach (var kv in orderManager.LobbyInfo.Slots)
{
var key = kv.Key;
var slot = kv.Value;
var client = orderManager.LobbyInfo.ClientInSlot(key);
Widget template = null;
// get template for possible reuse
if (idx < players.Children.Count)
template = players.Children[idx];
if (client == null)
{
// Empty slot
if (template == null || template.Id != emptySlotTemplate.Id)
template = emptySlotTemplate.Clone();
if (isHost)
LobbyUtils.SetupEditableSlotWidget(template, slot, client, orderManager, worldRenderer, map);
else
LobbyUtils.SetupSlotWidget(template, slot, client);
var join = template.Get<ButtonWidget>("JOIN");
join.IsVisible = () => !slot.Closed;
join.IsDisabled = () => orderManager.LocalClient.IsReady;
join.OnClick = () => orderManager.IssueOrder(Order.Command("slot " + key));
}
else if ((client.Index == orderManager.LocalClient.Index) ||
(client.Bot != null && isHost))
{
// Editable player in slot
if (template == null || template.Id != editablePlayerTemplate.Id)
template = editablePlayerTemplate.Clone();
LobbyUtils.SetupLatencyWidget(template, client, orderManager);
if (client.Bot != null)
LobbyUtils.SetupEditableSlotWidget(template, slot, client, orderManager, worldRenderer, map);
else
LobbyUtils.SetupEditableNameWidget(template, slot, client, orderManager, worldRenderer);
LobbyUtils.SetupEditableColorWidget(template, slot, client, orderManager, shellmapWorld, colorPreview);
LobbyUtils.SetupEditableFactionWidget(template, slot, client, orderManager, factions);
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, MapIsPlayable);
}
else
{
// Non-editable player in slot
if (template == null || template.Id != nonEditablePlayerTemplate.Id)
template = nonEditablePlayerTemplate.Clone();
LobbyUtils.SetupLatencyWidget(template, client, orderManager);
LobbyUtils.SetupColorWidget(template, slot, client);
LobbyUtils.SetupFactionWidget(template, slot, client, factions);
if (isHost)
{
LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, map);
LobbyUtils.SetupEditableHandicapWidget(template, slot, client, orderManager, map);
LobbyUtils.SetupEditableSpawnWidget(template, slot, client, orderManager, map);
LobbyUtils.SetupPlayerActionWidget(template, slot, client, orderManager, worldRenderer,
lobby, () => panel = PanelType.Kick, () => panel = PanelType.Players);
}
else
{
LobbyUtils.SetupNameWidget(template, slot, client, orderManager, worldRenderer);
LobbyUtils.SetupTeamWidget(template, slot, client);
LobbyUtils.SetupHandicapWidget(template, slot, client);
LobbyUtils.SetupSpawnWidget(template, slot, client);
}
LobbyUtils.SetupReadyWidget(template, slot, client);
}
template.IsVisible = () => true;
if (idx >= players.Children.Count)
players.AddChild(template);
else if (players.Children[idx].Id != template.Id)
players.ReplaceChild(players.Children[idx], template);
idx++;
}
// Add spectators
foreach (var client in orderManager.LobbyInfo.Clients.Where(client => client.Slot == null))
{
Widget template = null;
var c = client;
// get template for possible reuse
if (idx < players.Children.Count)
template = players.Children[idx];
// Editable spectator
if (c.Index == orderManager.LocalClient.Index)
{
if (template == null || template.Id != editableSpectatorTemplate.Id)
template = editableSpectatorTemplate.Clone();
LobbyUtils.SetupEditableNameWidget(template, null, c, orderManager, worldRenderer);
if (client.IsAdmin)
LobbyUtils.SetupEditableReadyWidget(template, null, client, orderManager, map, MapIsPlayable);
else
LobbyUtils.HideReadyWidgets(template);
}
else
{
// Non-editable spectator
if (template == null || template.Id != nonEditableSpectatorTemplate.Id)
template = nonEditableSpectatorTemplate.Clone();
if (isHost)
LobbyUtils.SetupPlayerActionWidget(template, null, client, orderManager, worldRenderer,
lobby, () => panel = PanelType.Kick, () => panel = PanelType.Players);
else
LobbyUtils.SetupNameWidget(template, null, client, orderManager, worldRenderer);
if (client.IsAdmin)
LobbyUtils.SetupReadyWidget(template, null, client);
else
LobbyUtils.HideReadyWidgets(template);
}
LobbyUtils.SetupLatencyWidget(template, c, orderManager);
template.IsVisible = () => true;
if (idx >= players.Children.Count)
players.AddChild(template);
else if (players.Children[idx].Id != template.Id)
players.ReplaceChild(players.Children[idx], template);
idx++;
}
// Spectate button
if (orderManager.LocalClient.Slot != null)
{
Widget spec = null;
if (idx < players.Children.Count)
spec = players.Children[idx];
if (spec == null || spec.Id != newSpectatorTemplate.Id)
spec = newSpectatorTemplate.Clone();
LobbyUtils.SetupKickSpectatorsWidget(spec, orderManager, lobby,
() => panel = PanelType.Kick, () => panel = PanelType.Players, skirmishMode);
var btn = spec.Get<ButtonWidget>("SPECTATE");
btn.OnClick = () => orderManager.IssueOrder(Order.Command("spectate"));
btn.IsDisabled = () => orderManager.LocalClient.IsReady;
btn.IsVisible = () => orderManager.LobbyInfo.GlobalSettings.AllowSpectators
|| orderManager.LocalClient.IsAdmin;
spec.IsVisible = () => true;
if (idx >= players.Children.Count)
players.AddChild(spec);
else if (players.Children[idx].Id != spec.Id)
players.ReplaceChild(players.Children[idx], spec);
idx++;
}
while (players.Children.Count > idx)
players.RemoveChild(players.Children[idx]);
tabCompletion.Names = orderManager.LobbyInfo.Clients.Select(c => c.Name).Distinct().ToList();
}
void UpdateDiscordStatus()
{
var numberOfPlayers = 0;
var slots = 0;
if (!skirmishMode)
{
foreach (var kv in orderManager.LobbyInfo.Slots)
{
if (kv.Value.Closed)
continue;
slots++;
var client = orderManager.LobbyInfo.ClientInSlot(kv.Key);
if (client != null)
numberOfPlayers++;
}
}
// Add extra slots to keep the join button active for spectators
if (numberOfPlayers == slots && orderManager.LobbyInfo.GlobalSettings.AllowSpectators)
slots = numberOfPlayers + 1;
var details = map.Title + " - " + orderManager.LobbyInfo.GlobalSettings.ServerName;
if (updateDiscordStatus)
{
string secret = null;
if (orderManager.LobbyInfo.GlobalSettings.Dedicated)
{
var endpoint = orderManager.Endpoint.GetConnectEndPoints().First();
secret = string.Concat(endpoint.Address, "|", endpoint.Port);
}
var state = skirmishMode ? DiscordState.InSkirmishLobby : DiscordState.InMultiplayerLobby;
DiscordService.UpdateStatus(state, details, secret, numberOfPlayers, slots);
updateDiscordStatus = false;
}
else
{
if (!skirmishMode)
DiscordService.UpdatePlayers(numberOfPlayers, slots);
DiscordService.UpdateDetails(details);
}
}
void UpdateSpawnOccupants()
{
spawnOccupants = orderManager.LobbyInfo.Clients
.Where(c => c.SpawnPoint != 0)
.ToDictionary(c => c.SpawnPoint, c => new SpawnOccupant(c));
}
void OnGameStart()
{
Ui.CloseWindow();
var state = skirmishMode ? DiscordState.PlayingSkirmish : DiscordState.PlayingMultiplayer;
var details = map.Title + " - " + orderManager.LobbyInfo.GlobalSettings.ServerName;
DiscordService.UpdateStatus(state, details);
onStart();
}
}
public class LobbyFaction
{
public bool Selectable;
public string Name;
public string Description;
public string Side;
}
class DropDownOption
{
public string Title;
public Func<bool> IsSelected = () => false;
public Action OnClick;
}
}