mono was the bottleneck restricting our ability to use a newer C# version. mono 6.12 is currently available. Although poorly documented on their website, this supports C# 9. https://www.mono-project.com/docs/about-mono/versioning/#mono-source-versioning indicates mono 6.12 uses Roslyn 3.9.0. https://github.com/dotnet/roslyn/blob/main/docs/wiki/NuGet-packages.md#versioning indicates Roslyn 3.9.0 supports C# 9. This unlocks C# 8 and C# 9 features previously unavailable to us. - https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-80 - https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-9 A newer version of StyleCop is required to avoid rules tripping up on the new syntax. Currently only prerelease versions are available but their use is encouraged https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3420#issuecomment-994899135 Fix style rule violations on existing rules where the newer language version makes some existing casts redundant or allows use of the null coalescing assignment operator.
906 lines
31 KiB
C#
906 lines
31 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* 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 OpenRA.Graphics;
|
|
using OpenRA.Mods.Common.Traits;
|
|
using OpenRA.Network;
|
|
using OpenRA.Traits;
|
|
using OpenRA.Widgets;
|
|
|
|
namespace OpenRA.Mods.Common.Widgets.Logic
|
|
{
|
|
public class LobbyLogic : ChromeLogic, INotificationHandler<TextNotification>
|
|
{
|
|
[TranslationReference]
|
|
const string Add = "options-slot-admin.add-bots";
|
|
|
|
[TranslationReference]
|
|
const string Remove = "options-slot-admin.remove-bots";
|
|
|
|
[TranslationReference]
|
|
const string ConfigureBots = "options-slot-admin.configure-bots";
|
|
|
|
[TranslationReference("count")]
|
|
const string NumberTeams = "options-slot-admin.teams-count";
|
|
|
|
[TranslationReference]
|
|
const string HumanVsBots = "options-slot-admin.humans-vs-bots";
|
|
|
|
[TranslationReference]
|
|
const string FreeForAll = "options-slot-admin.free-for-all";
|
|
|
|
[TranslationReference]
|
|
const string ConfigureTeams = "options-slot-admin.configure-teams";
|
|
|
|
[TranslationReference]
|
|
const string Back = "button-back";
|
|
|
|
[TranslationReference]
|
|
const string TeamChat = "button-team-chat";
|
|
|
|
[TranslationReference]
|
|
const string GeneralChat = "button-general-chat";
|
|
|
|
[TranslationReference("seconds")]
|
|
const string ChatAvailability = "label-chat-availability";
|
|
|
|
[TranslationReference]
|
|
const string ChatDisabled = "label-chat-disabled";
|
|
|
|
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 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 Dictionary<TextNotificationPool, Widget> chatTemplates = new Dictionary<TextNotificationPool, Widget>();
|
|
readonly TextFieldWidget chatTextField;
|
|
readonly CachedTransform<int, string> chatAvailableIn;
|
|
readonly string chatDisabled;
|
|
|
|
readonly ScrollPanelWidget players;
|
|
|
|
readonly Dictionary<string, LobbyFaction> factions = new Dictionary<string, LobbyFaction>();
|
|
|
|
readonly ColorPickerManagerInfo colorManager;
|
|
|
|
readonly TabCompletionLogic tabCompletion = new TabCompletionLogic();
|
|
|
|
MapPreview map;
|
|
Session.MapStatus mapStatus;
|
|
|
|
bool chatEnabled;
|
|
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, string password, NetworkConnection connection)
|
|
{
|
|
if (connection.ConnectionState == ConnectionState.NotConnected)
|
|
{
|
|
// Show connection failed dialog
|
|
Ui.CloseWindow();
|
|
|
|
void OnConnect()
|
|
{
|
|
Game.OpenWindow("SERVER_LOBBY", new WidgetArgs()
|
|
{
|
|
{ "onExit", onExit },
|
|
{ "onStart", onStart },
|
|
{ "skirmishMode", false }
|
|
});
|
|
}
|
|
|
|
Action<string> onRetry = pass => ConnectionLogic.Connect(connection.Target, pass, OnConnect, onExit);
|
|
|
|
var switchPanel = CurrentServerSettings.ServerExternalMod != null ? "CONNECTION_SWITCHMOD_PANEL" : "CONNECTIONFAILED_PANEL";
|
|
Ui.OpenWindow(switchPanel, new WidgetArgs()
|
|
{
|
|
{ "orderManager", om },
|
|
{ "connection", connection },
|
|
{ "password", password },
|
|
{ "onAbort", onExit },
|
|
{ "onQuit", null },
|
|
{ "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;
|
|
|
|
services = modData.Manifest.Get<WebServices>();
|
|
|
|
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");
|
|
colorManager = modRules.Actors[SystemActors.World].TraitInfo<ColorPickerManagerInfo>();
|
|
colorManager.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, and handle map becoming unavailable
|
|
if (uid == map.Uid || modData.MapCache[uid].Status != MapStatus.Available)
|
|
return;
|
|
|
|
orderManager.IssueOrder(Order.Command("map " + uid));
|
|
Game.Settings.Server.Map = uid;
|
|
Game.Settings.Save();
|
|
});
|
|
|
|
// Check for updated maps, if the user has edited a map we'll preselect it for them
|
|
modData.MapCache.UpdateMaps();
|
|
|
|
Ui.OpenWindow("MAPCHOOSER_PANEL", new WidgetArgs()
|
|
{
|
|
{ "initialMap", modData.MapCache.PickLastModifiedMap(MapVisibility.Lobby) ?? map.Uid },
|
|
{ "initialTab", MapClassification.System },
|
|
{ "onExit", Game.IsHost ? UpdateSelectedMap : modData.MapCache.UpdateMaps },
|
|
{ "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.Any(s => !s.Value.LockTeam && orderManager.LobbyInfo.ClientInSlot(s.Key) != null));
|
|
|
|
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 = modData.Translation.GetString(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 && (c == null || c.Bot != null))
|
|
orderManager.IssueOrder(Order.Command($"slot_bot {slot.Key} {botController.Index} {bot}"));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if (orderManager.LobbyInfo.Clients.Any(c => c.Bot != null))
|
|
{
|
|
botOptions.Add(new DropDownOption()
|
|
{
|
|
Title = modData.Translation.GetString(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(modData.Translation.GetString(ConfigureBots), 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 = modData.Translation.GetString(NumberTeams, Translation.Arguments("count", d)),
|
|
IsSelected = () => false,
|
|
OnClick = () => orderManager.IssueOrder(Order.Command($"assignteams {d}"))
|
|
}).ToList();
|
|
|
|
if (orderManager.LobbyInfo.Slots.Any(s => s.Value.AllowBots))
|
|
{
|
|
teamOptions.Add(new DropDownOption
|
|
{
|
|
Title = modData.Translation.GetString(HumanVsBots),
|
|
IsSelected = () => false,
|
|
OnClick = () => orderManager.IssueOrder(Order.Command("assignteams 1"))
|
|
});
|
|
}
|
|
|
|
teamOptions.Add(new DropDownOption
|
|
{
|
|
Title = modData.Translation.GetString(FreeForAll),
|
|
IsSelected = () => false,
|
|
OnClick = () => orderManager.IssueOrder(Order.Command("assignteams 0"))
|
|
});
|
|
|
|
options.Add(modData.Translation.GetString(ConfigureTeams), teamOptions);
|
|
}
|
|
|
|
ScrollItemWidget SetupItem(DropDownOption option, ScrollItemWidget 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
|
|
void StartGame()
|
|
{
|
|
// Refresh MapCache and check if the selected map is available before attempting to start the game
|
|
if (modData.MapCache[map.Uid].Status == MapStatus.Available)
|
|
{
|
|
gameStarting = true;
|
|
orderManager.IssueOrder(Order.Command("startgame"));
|
|
}
|
|
else
|
|
UpdateSelectedMap();
|
|
}
|
|
|
|
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.Slots.All(sl => 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 = modData.Translation.GetString(Back);
|
|
|
|
if (logicArgs.TryGetValue("ChatTemplates", out var templateIds))
|
|
{
|
|
foreach (var item in templateIds.Nodes)
|
|
{
|
|
var key = FieldLoader.GetValue<TextNotificationPool>("key", item.Key);
|
|
chatTemplates[key] = Ui.LoadWidget(item.Value.Value, null, new WidgetArgs());
|
|
}
|
|
}
|
|
|
|
var chatMode = lobby.Get<ButtonWidget>("CHAT_MODE");
|
|
var team = modData.Translation.GetString(TeamChat);
|
|
var all = modData.Translation.GetString(GeneralChat);
|
|
chatMode.GetText = () => teamChat ? team : all;
|
|
chatMode.OnClick = () => teamChat ^= true;
|
|
chatMode.IsDisabled = () => disableTeamChat || !chatEnabled;
|
|
|
|
chatTextField = lobby.Get<TextFieldWidget>("CHAT_TEXTFIELD");
|
|
chatTextField.IsDisabled = () => !chatEnabled;
|
|
chatTextField.MaxLength = UnitOrders.ChatMessageMaxLength;
|
|
|
|
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 = e =>
|
|
{
|
|
if (!chatMode.Key.IsActivatedBy(e) || chatMode.IsDisabled())
|
|
{
|
|
chatTextField.Text = tabCompletion.Complete(chatTextField.Text);
|
|
chatTextField.CursorPosition = chatTextField.Text.Length;
|
|
}
|
|
else
|
|
chatMode.OnKeyPress(e);
|
|
|
|
return true;
|
|
};
|
|
|
|
chatTextField.OnEscKey = _ => chatTextField.YieldKeyboardFocus();
|
|
|
|
chatAvailableIn = new CachedTransform<int, string>(x => modData.Translation.GetString(ChatAvailability, Translation.Arguments("seconds", x)));
|
|
chatDisabled = modData.Translation.GetString(ChatDisabled);
|
|
|
|
lobbyChatPanel = lobby.Get<ScrollPanelWidget>("CHAT_DISPLAY");
|
|
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;
|
|
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;
|
|
|
|
var chatWasEnabled = chatEnabled;
|
|
chatEnabled = worldRenderer.World.IsReplay || (Game.RunTime >= TextNotificationsManager.ChatDisabledUntil && TextNotificationsManager.ChatDisabledUntil != uint.MaxValue);
|
|
|
|
if (chatEnabled && !chatWasEnabled)
|
|
{
|
|
chatTextField.Text = "";
|
|
if (Ui.KeyboardFocusWidget == null)
|
|
chatTextField.TakeKeyboardFocus();
|
|
}
|
|
else if (!chatEnabled)
|
|
{
|
|
var remaining = 0;
|
|
if (TextNotificationsManager.ChatDisabledUntil != uint.MaxValue)
|
|
remaining = (int)(TextNotificationsManager.ChatDisabledUntil - Game.RunTime + 999) / 1000;
|
|
|
|
chatTextField.Text = remaining == 0 ? chatDisabled : chatAvailableIn.Update(remaining);
|
|
}
|
|
}
|
|
|
|
void INotificationHandler<TextNotification>.Handle(TextNotification notification)
|
|
{
|
|
var chatLine = chatTemplates[notification.Pool].Clone();
|
|
WidgetUtils.SetupTextNotification(chatLine, notification, lobbyChatPanel.Bounds.Width - lobbyChatPanel.ScrollbarWidth, true);
|
|
|
|
var scrolledToBottom = lobbyChatPanel.ScrolledToBottom;
|
|
lobbyChatPanel.AddChild(chatLine);
|
|
if (scrolledToBottom)
|
|
lobbyChatPanel.ScrollToBottom(smooth: true);
|
|
|
|
Game.Sound.PlayNotification(modRules, null, "Sounds", chatLineSound, null);
|
|
}
|
|
|
|
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 {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 {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, map, modData);
|
|
else
|
|
LobbyUtils.SetupSlotWidget(template, modData, 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, map, modData);
|
|
else
|
|
LobbyUtils.SetupEditableNameWidget(template, client, orderManager, worldRenderer);
|
|
|
|
LobbyUtils.SetupEditableColorWidget(template, slot, client, orderManager, worldRenderer, colorManager);
|
|
LobbyUtils.SetupEditableFactionWidget(template, slot, client, orderManager, factions);
|
|
LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, map);
|
|
LobbyUtils.SetupEditableHandicapWidget(template, slot, client, orderManager);
|
|
LobbyUtils.SetupEditableSpawnWidget(template, slot, client, orderManager, map);
|
|
LobbyUtils.SetupEditableReadyWidget(template, 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, client);
|
|
LobbyUtils.SetupFactionWidget(template, client, factions);
|
|
|
|
if (isHost)
|
|
{
|
|
LobbyUtils.SetupEditableTeamWidget(template, slot, client, orderManager, map);
|
|
LobbyUtils.SetupEditableHandicapWidget(template, slot, client, orderManager);
|
|
LobbyUtils.SetupEditableSpawnWidget(template, slot, client, orderManager, map);
|
|
LobbyUtils.SetupPlayerActionWidget(template, client, orderManager, worldRenderer,
|
|
lobby, () => panel = PanelType.Kick, () => panel = PanelType.Players);
|
|
}
|
|
else
|
|
{
|
|
LobbyUtils.SetupNameWidget(template, client, orderManager, worldRenderer);
|
|
LobbyUtils.SetupTeamWidget(template, client);
|
|
LobbyUtils.SetupHandicapWidget(template, client);
|
|
LobbyUtils.SetupSpawnWidget(template, client);
|
|
}
|
|
|
|
LobbyUtils.SetupReadyWidget(template, 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, c, orderManager, worldRenderer);
|
|
|
|
if (client.IsAdmin)
|
|
LobbyUtils.SetupEditableReadyWidget(template, 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, client, orderManager, worldRenderer,
|
|
lobby, () => panel = PanelType.Kick, () => panel = PanelType.Players);
|
|
else
|
|
LobbyUtils.SetupNameWidget(template, client, orderManager, worldRenderer);
|
|
|
|
if (client.IsAdmin)
|
|
LobbyUtils.SetupReadyWidget(template, 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 = CurrentServerSettings.Target.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();
|
|
}
|
|
|
|
void UpdateSelectedMap()
|
|
{
|
|
if (modData.MapCache[map.Uid].Status == MapStatus.Available)
|
|
return;
|
|
|
|
var uid = modData.MapCache.GetUpdatedMap(map.Uid);
|
|
if (uid != null)
|
|
{
|
|
orderManager.IssueOrder(Order.Command("map " + uid));
|
|
Game.Settings.Server.Map = uid;
|
|
Game.Settings.Save();
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|