using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; namespace Barotrauma { partial class NetLobbyScreen : Screen { private GUIListBox chatBox; private GUILayoutGroup chatRow; private GUIButton serverLogReverseButton; private GUIListBox serverLogBox, serverLogFilterTicks; private static GUIComponent jobVariantTooltip; private GUIComponent playStyleIconContainer; private GUIDropDown chatSelector; public static bool TeamChatSelected = false; private GUITextBox chatInput; private GUITextBox serverLogFilter; public GUITextBox ChatInput { get { return chatInput; } } private GUIImage micIcon; private GUIScrollBar levelDifficultySlider; private readonly List traitorElements = new List(); private GUIScrollBar traitorProbabilitySlider; private GUILayoutGroup traitorDangerGroup; private GUIDropDown outpostDropdown; private bool outpostDropdownUpToDate; public GUIFrame MissionTypeFrame { get; private set; } public GUIFrame CampaignSetupFrame { get; private set; } public GUIFrame CampaignFrame { get; private set; } public GUIButton QuitCampaignButton { get; private set; } private GUITickBox[] missionTypeTickBoxes; private GUIListBox missionTypeList; public GUITextBox LevelSeedBox { get; private set; } private GUIButton joinOnGoingRoundButton; /// /// Contains the elements that control starting the round (start button, spectate button, "ready to start" tickbox) /// private GUILayoutGroup roundControlsHolder; public GUIButton SettingsButton { get; private set; } public GUIButton ServerMessageButton { get; private set; } public static GUIButton JobInfoFrame { get; set; } private GUITickBox spectateBox, afkBox; public bool Spectating => spectateBox is { Selected: true, Visible: true }; public bool AFKSelected => afkBox is { Selected: true, Visible: true }; public bool PermadeathMode => GameMain.Client?.ServerSettings?.RespawnMode == RespawnMode.Permadeath; public bool PermanentlyDead => campaignCharacterInfo?.PermanentlyDead ?? false; private GUILayoutGroup playerInfoContent; private GUIComponent changesPendingText; private bool createPendingChangesText = true; public GUIButton PlayerFrame { get; private set; } public GUIButton SubVisibilityButton { get; private set; } private GUITextBox subSearchBox; private GUIComponent subPreviewContainer; private GUITickBox autoRestartBox; private GUITextBlock autoRestartText; private GUITickBox shuttleTickBox; private Sprite backgroundSprite; private GUIButton jobPreferencesButton; private GUIButton appearanceButton; private GUIFrame characterInfoFrame; private GUIFrame appearanceFrame; private GUISelectionCarousel respawnModeSelection; private GUITextBlock respawnModeLabel; private GUIComponent respawnIntervalElement; private readonly List midRoundRespawnSettings = new List(); private readonly List permadeathEnabledRespawnSettings = new List(); private readonly List permadeathDisabledRespawnSettings = new List(); private readonly List ironmanDisabledRespawnSettings = new List(); private readonly List campaignDisabledElements = new List(); private readonly List campaignHiddenElements = new List(); private readonly List pvpOnlyElements = new(); private readonly List disembarkPerkSettings = new(); private readonly List respawnSettings = new(); public CharacterInfo.AppearanceCustomizationMenu CharacterAppearanceCustomizationMenu { get; set; } private Point prevResolutionForJobSelectionFrame; public GUIFrame JobSelectionFrame { get; private set; } public GUIFrame JobPreferenceContainer { get; private set; } public GUIListBox JobList { get; private set; } private Identifier micIconStyle; private float micCheckTimer; const float MicCheckInterval = 1.0f; private float autoRestartTimer; //persistent characterinfo provided by the server //(character settings cannot be edited when this is set) private CharacterInfo campaignCharacterInfo; public bool CampaignCharacterDiscarded { get; set; } /// /// Elements that can only be used by the host or people with server settings management permissions (but are visible to everyone) /// private readonly List clientDisabledElements = new List(); /// /// Elements that are only visible to the host or people with server settings management permissions /// private readonly List clientHiddenElements = new List(); private readonly List botSettingsElements = new List(); private readonly Dictionary settingAssignedComponents = new Dictionary(); public GUIComponent FileTransferFrame { get; private set; } public GUITextBlock FileTransferTitle { get; private set; } public GUIProgressBar FileTransferProgressBar { get; private set; } public GUITextBlock FileTransferProgressText { get; private set; } public GUITickBox Favorite { get; private set; } public GUILayoutGroup LogButtons { get; private set; } /// /// Tab buttons above the chat panel (chat and server log tabs) /// private readonly List chatPanelTabButtons = new List(); private GUITextBlock publicOrPrivateText, playstyleText; public GUIListBox SubList { get; private set; } public GUIDropDown ShuttleList { get; private set; } public GUIListBox ModeList { get; private set; } private int selectedModeIndex; public int SelectedModeIndex { get { return selectedModeIndex; } set { if (HighlightedModeIndex == selectedModeIndex) { ModeList.Select(value); } selectedModeIndex = value; } } public int HighlightedModeIndex { get { return ModeList.SelectedIndex; } set { ModeList.Select(value, GUIListBox.Force.Yes); } } //No, this should not be static even though your IDE might say so! There's a server-side version of this which needs to be an instance method. public IReadOnlyList GetSubList() => (IReadOnlyList)GameMain.Client?.ServerSubmarines ?? Array.Empty(); public GUIListBox PlayerList; public int Team1Count; public int Team2Count; public GUITextBox CharacterNameBox { get; private set; } public GUIListBox TeamPreferenceListBox { get; private set; } private GUITextBlock pvpTeamChoiceTeam1; private GUITextBlock pvpTeamChoiceMiddleButton; private GUITextBlock pvpTeamChoiceTeam2; private CharacterTeamType TeamPreference => SelectedMode == GameModePreset.PvP ? MultiplayerPreferences.Instance.TeamPreference : CharacterTeamType.Team1; public GUIButton StartButton { get; private set; } public GUIButton EndButton { get; private set; } public GUITickBox ReadyToStartBox { get; private set; } [AllowNull, MaybeNull] public SubmarineInfo SelectedSub; [AllowNull, MaybeNull] public SubmarineInfo SelectedEnemySub; public SubmarineInfo SelectedShuttle => ShuttleList.SelectedData as SubmarineInfo; public MultiPlayerCampaignSetupUI CampaignSetupUI; public bool UsingShuttle { get { return shuttleTickBox.Selected; } set { shuttleTickBox.Selected = value; } } public GameModePreset SelectedMode { get { return ModeList.SelectedData as GameModePreset; } } public IEnumerable MissionTypes { get { return missionTypeTickBoxes.Where(t => t.Selected).Select(t => (Identifier)t.UserData); } set { bool changed = false; foreach (var missionTypeTickBox in missionTypeTickBoxes) { bool prevSelected = missionTypeTickBox.Selected; missionTypeTickBox.Selected = value.Contains((Identifier)missionTypeTickBox.UserData); if (prevSelected != missionTypeTickBox.Selected) { changed = true; } } if (changed) { RefreshOutpostDropdown(); } } } public List JobPreferences { get { // JobList if the server has already assigned the player a job // (e.g. the player has a pre-existing campaign character) if (JobList?.Content == null) { return new List(); } List jobPreferences = new List(); foreach (GUIComponent child in JobList.Content.Children) { if (child.UserData is not JobVariant jobPrefab) { continue; } jobPreferences.Add(jobPrefab); } return jobPreferences; } } public string LevelSeed { get { return levelSeed; } set { if (levelSeed == value) { return; } levelSeed = value; int intSeed = ToolBox.StringToInt(levelSeed); backgroundSprite = LocationType.Random(new MTRandom(intSeed), predicate: lt => lt.UsePortraitInRandomLoadingScreens)?.GetPortrait(intSeed); LevelSeedBox.Text = levelSeed; } } private const float MainPanelWidth = 0.7f; private const float SidePanelWidth = 0.3f; /// /// Spacing between different elements in the panels /// private const float PanelSpacing = 0.005f; /// /// Size of the outer border of the panels (= empty area round the contents of the panel) /// private static int PanelBorderSize => GUI.IntScale(20); private static Point GetSizeWithoutBorder(GUIComponent parent) => new Point(parent.Rect.Width - PanelBorderSize * 2, parent.Rect.Height - PanelBorderSize * 2); public NetLobbyScreen() { var contentArea = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), Frame.RectTransform, Anchor.Center), isHorizontal: false) { Stretch = true, RelativeSpacing = PanelSpacing }; var horizontalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, contentArea.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, RelativeSpacing = PanelSpacing }; var mainPanel = new GUIFrame(new RectTransform(new Vector2(MainPanelWidth, 1.0f), horizontalLayout.RectTransform)); var mainPanelLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width, mainPanel.Rect.Height - PanelBorderSize), mainPanel.RectTransform, Anchor.TopCenter), childAnchor: Anchor.TopCenter) { Stretch = true, //more spacing to more clearly separate the top and bottom RelativeSpacing = PanelSpacing * 4 }; GUILayoutGroup serverInfoHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), mainPanelLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.025f }; CreateServerInfoContents(serverInfoHolder); var mainPanelTopLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width - PanelBorderSize * 2, mainPanel.Rect.Height / 2), mainPanelLayout.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, RelativeSpacing = PanelSpacing }; var mainPanelBottomLayout = new GUILayoutGroup(new RectTransform(new Point(mainPanel.Rect.Width - PanelBorderSize * 2, mainPanel.Rect.Height / 2), mainPanelLayout.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, RelativeSpacing = PanelSpacing }; //-------------------------------------------------------------------------------------------------------------------------------- //top panel (game mode, submarine) //-------------------------------------------------------------------------------------------------------------------------------- CreateGameModeDropdown(mainPanelTopLayout); CreateSubmarineListPanel(mainPanelTopLayout); CreateSubmarineInfoPanel(mainPanelTopLayout); //-------------------------------------------------------------------------------------------------------------------------------- //bottom panel (settings) //-------------------------------------------------------------------------------------------------------------------------------- CreateGameModePanel(mainPanelBottomLayout); CreateGameModeSettingsPanel(mainPanelBottomLayout); CreateGeneralSettingsPanel(mainPanelBottomLayout); mainPanelBottomLayout.Recalculate(); foreach (var child in mainPanelBottomLayout.GetAllChildren()) { if (traitorDangerGroup.Children.Contains(child)) { //don't touch the colors of the traitor danger indicators, they're intentionally very dim when disabled continue; } //make the disabled colors slightly less dim (these should be readable, despite being non-interactable) child.DisabledColor = new Color(child.Color, child.Color.A / 255.0f * 0.8f); if (child is GUITextBlock textBlock) { textBlock.DisabledTextColor = new Color(textBlock.TextColor, textBlock.TextColor.A / 255.0f * 0.8f); } } //-------------------------------------------------------------------------------------------------------------------------------- //right panel (Character customization/Chat) //-------------------------------------------------------------------------------------------------------------------------------- var sidePanel = new GUIFrame(new RectTransform(new Vector2(SidePanelWidth, 1.0f), horizontalLayout.RectTransform)); GUILayoutGroup sidePanelLayout = new GUILayoutGroup(new RectTransform(GetSizeWithoutBorder(sidePanel), sidePanel.RectTransform, Anchor.Center)) { RelativeSpacing = PanelSpacing * 4, Stretch = true }; CreateSidePanelContents(sidePanelLayout); //-------------------------------------------------------------------------------------------------------------------------------- // bottom panel (start round, quit, transfers, ready to start...) ------------------------------------------------------------ //-------------------------------------------------------------------------------------------------------------------------------- GUILayoutGroup bottomBar = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), contentArea.RectTransform), childAnchor: Anchor.CenterLeft) { Stretch = true, IsHorizontal = true, RelativeSpacing = PanelSpacing }; CreateBottomPanelContents(bottomBar); } private void AssignComponentToServerSetting(GUIComponent component, string settingName) { settingAssignedComponents[component] = settingName; } public void AssignComponentsToServerSettings() { settingAssignedComponents.ForEach(kvp => GameMain.Client.ServerSettings.AssignGUIComponent(kvp.Value, kvp.Key)); } private void CreateServerInfoContents(GUIComponent parent) { GUIFrame serverInfoFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); var serverBanner = new GUICustomComponent(new RectTransform(Vector2.One, serverInfoFrame.RectTransform), DrawServerBanner) { HideElementsOutsideFrame = true, IgnoreLayoutGroups = true }; GUIFrame serverInfoContent = new GUIFrame(new RectTransform(new Vector2(0.98f, 0.9f), serverInfoFrame.RectTransform, Anchor.Center), style: null); var serverLabelContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.05f), serverInfoContent.RectTransform), isHorizontal: true) { AbsoluteSpacing = GUI.IntScale(5) }; playstyleText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), serverLabelContainer.RectTransform), "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); publicOrPrivateText = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), serverLabelContainer.RectTransform), "", font: GUIStyle.SmallFont, textAlignment: Alignment.Center, textColor: Color.White, style: "GUISlopedHeader"); var serverNameShadow = new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.3f), serverInfoContent.RectTransform, Anchor.CenterLeft) { AbsoluteOffset = new Point(GUI.IntScale(3)) }, string.Empty, font: GUIStyle.LargeFont, textColor: Color.Black) { IgnoreLayoutGroups = true }; var serverName = new GUITextBlock(new RectTransform(new Vector2(0.2f, 0.3f), serverInfoContent.RectTransform, Anchor.CenterLeft), string.Empty, font: GUIStyle.LargeFont, textColor: GUIStyle.TextColorBright) { IgnoreLayoutGroups = true, TextGetter = serverNameShadow.TextGetter = () => GameMain.Client?.ServerName }; ServerMessageButton = new GUIButton(new RectTransform(new Vector2(0.2f, 0.15f), serverInfoContent.RectTransform, Anchor.BottomLeft), TextManager.Get("workshopitemdescription"), style: "GUIButtonSmall") { IgnoreLayoutGroups = true, OnClicked = (bt, userdata) => { if (GameMain.Client?.ServerSettings is { } serverSettings) { CreateServerMessagePopup(serverSettings.ServerName, serverSettings.ServerMessageText); } return true; } }; playStyleIconContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.4f), serverInfoContent.RectTransform, Anchor.BottomRight), isHorizontal: true, childAnchor: Anchor.BottomRight) { AbsoluteSpacing = GUI.IntScale(5) }; var topRightContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 0.5f), serverInfoContent.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.TopRight) { AbsoluteSpacing = GUI.IntScale(5), CanBeFocused = true }; SettingsButton = new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), topRightContainer.RectTransform, Anchor.TopRight), TextManager.Get("ServerSettingsButton"), style: "GUIButtonFreeScale"); Favorite = new GUITickBox(new RectTransform(Vector2.One, topRightContainer.RectTransform, Anchor.TopRight, scaleBasis: ScaleBasis.BothHeight), "", null, "GUIServerListFavoriteTickBox") { Selected = false, ToolTip = TextManager.Get("addtofavorites"), OnSelected = (tickbox) => { if (GameMain.Client == null) { return true; } ServerInfo info = GameMain.Client.CreateServerInfoFromSettings(); if (tickbox.Selected) { GameMain.ServerListScreen.AddToFavoriteServers(info); } else { GameMain.ServerListScreen.RemoveFromFavoriteServers(info); } tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites"); return true; } }; } private void CreateServerMessagePopup(string serverName, string message) { if (string.IsNullOrEmpty(message)) { return; } var popup = new GUIMessageBox(serverName, string.Empty, minSize: new Point(GUI.IntScale(650), GUI.IntScale(650))); //popup.Content.Stretch = true; popup.Header.Font = GUIStyle.LargeFont; popup.Header.RectTransform.MinSize = new Point(0, (int)popup.Header.TextSize.Y); var textListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), popup.Content.RectTransform)); var text = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), textListBox.Content.RectTransform), message, wrap: true) { CanBeFocused = false }; text.RectTransform.MinSize = new Point(0, (int)text.TextSize.Y); } public void RefreshPlaystyleIcons() { playStyleIconContainer?.ClearChildren(); if (GameMain.Client?.ClientPeer?.ServerConnection is not { } serverConnection || serverConnection.Endpoint == null) { return; } var serverInfo = ServerInfo.FromServerEndpoints(serverConnection.Endpoint.ToEnumerable().ToImmutableArray(), GameMain.Client.ServerSettings); var playStyleTags = serverInfo.GetPlayStyleTags(); foreach (var tag in playStyleTags) { var playStyleIcon = GUIStyle.GetComponentStyle($"PlayStyleIcon.{tag}") ?.GetSprite(GUIComponent.ComponentState.None); if (playStyleIcon is null) { continue; } new GUIImage(new RectTransform(Vector2.One, playStyleIconContainer.RectTransform, scaleBasis: ScaleBasis.BothHeight), playStyleIcon, scaleToFit: true) { ToolTip = TextManager.Get($"servertagdescription.{tag}"), Color = Color.White }; } } private void CreateGameModeDropdown(GUIComponent parent) { //------------------------------------------------------------------------------------------------------------------ // Gamemode panel //------------------------------------------------------------------------------------------------------------------ GUILayoutGroup gameModeHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) { Stretch = true, RelativeSpacing = 0.005f }; var modeLabel = CreateSubHeader("GameMode", gameModeHolder); var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), modeLabel.RectTransform, Anchor.TopRight), TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) { UserData = "modevotes", Visible = false }; ModeList = new GUIListBox(new RectTransform(Vector2.One, gameModeHolder.RectTransform)) { PlaySoundOnSelect = true, OnSelected = VotableClicked }; foreach (GameModePreset mode in GameModePreset.List) { if (mode.IsSinglePlayer) { continue; } var modeFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.25f), ModeList.Content.RectTransform), style: null) { UserData = mode }; var modeContent = new GUILayoutGroup(new RectTransform(new Vector2(0.76f, 0.9f), modeFrame.RectTransform, Anchor.CenterRight)) { AbsoluteSpacing = GUI.IntScale(5), Stretch = true }; var modeTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Name, font: GUIStyle.SubHeadingFont); modeTitle.RectTransform.NonScaledSize = new Point(int.MaxValue, (int)modeTitle.TextSize.Y); modeTitle.RectTransform.IsFixedSize = true; var modeDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), modeContent.RectTransform), mode.Description, font: GUIStyle.SmallFont, wrap: true); //leave some padding for the vote count text modeDescription.Padding = new Vector4(modeDescription.Padding.X, modeDescription.Padding.Y, GUI.IntScale(30), modeDescription.Padding.W); modeTitle.HoverColor = modeDescription.HoverColor = modeTitle.SelectedColor = modeDescription.SelectedColor = Color.Transparent; modeTitle.HoverTextColor = modeDescription.HoverTextColor = modeTitle.TextColor; modeTitle.TextColor = modeDescription.TextColor = modeTitle.TextColor * 0.5f; modeFrame.OnAddedToGUIUpdateList = (c) => { modeTitle.State = modeDescription.State = c.State; }; modeDescription.RectTransform.SizeChanged += () => { modeDescription.RectTransform.NonScaledSize = new Point(modeDescription.Rect.Width, (int)modeDescription.TextSize.Y); modeFrame.RectTransform.MinSize = new Point(0, (int)(modeContent.Children.Sum(c => c.Rect.Height + modeContent.AbsoluteSpacing) / modeContent.RectTransform.RelativeSize.Y)); }; new GUIImage(new RectTransform(new Vector2(0.2f, 0.8f), modeFrame.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }, style: "GameModeIcon." + mode.Identifier, scaleToFit: true); } } private void CreateSubmarineListPanel(GUIComponent parent) { var submarineListHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) { Stretch = true, RelativeSpacing = 0.005f }; var subLabel = CreateSubHeader("Submarine", submarineListHolder); SubVisibilityButton = new GUIButton( new RectTransform(Vector2.One * 1.2f, subLabel.RectTransform, anchor: Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(0, GUI.IntScale(5)) }, style: "EyeButton") { OnClicked = (button, o) => { CreateSubmarineVisibilityMenu(); return false; } }; clientHiddenElements.Add(SubVisibilityButton); var filterContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), submarineListHolder.RectTransform), isHorizontal: true) { Stretch = true }; var searchTitle = new GUITextBlock(new RectTransform(new Vector2(0.001f, 1.0f), filterContainer.RectTransform), TextManager.Get("serverlog.filter"), textAlignment: Alignment.CenterLeft, font: GUIStyle.Font); subSearchBox = new GUITextBox(new RectTransform(Vector2.One, filterContainer.RectTransform, Anchor.CenterRight), font: GUIStyle.Font, createClearButton: true); filterContainer.RectTransform.MinSize = subSearchBox.RectTransform.MinSize; subSearchBox.OnSelected += (sender, userdata) => { searchTitle.Visible = false; }; subSearchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = true; }; subSearchBox.OnTextChanged += (textBox, text) => { UpdateSubVisibility(); return true; }; SubList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.93f), submarineListHolder.RectTransform)) { PlaySoundOnSelect = true, OnSelected = VotableClicked }; var voteText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subLabel.RectTransform, Anchor.TopRight), TextManager.Get("Votes"), textAlignment: Alignment.CenterRight) { UserData = "subvotes", Visible = false, CanBeFocused = false }; } private void CreateSubmarineInfoPanel(GUIComponent parent) { var submarineInfoHolder = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.005f }; //submarine preview ------------------------------------------------------------------ subPreviewContainer = new GUIFrame(new RectTransform(Vector2.One, submarineInfoHolder.RectTransform), style: null); subPreviewContainer.RectTransform.SizeChanged += () => { if (SelectedSub != null) { CreateSubPreview(SelectedSub); } }; } private GUIComponent CreateGameModePanel(GUIComponent parent) { var gameModeSpecificFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); CampaignSetupFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) { Visible = false }; CampaignFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null) { Visible = false }; GUILayoutGroup campaignContent = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.5f), CampaignFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.05f, Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), TextManager.Get("gamemode.multiplayercampaign"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); QuitCampaignButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.3f), campaignContent.RectTransform), TextManager.Get("quitbutton"), textAlignment: Alignment.Center) { OnClicked = (_, __) => { if (GameMain.Client == null) { return false; } if (GameMain.Client.GameStarted) { GameMain.Client.RequestEndRound(save: false); } else { GameMain.Client.RequestEndRound(save: false, quitCampaign: true); } return true; } }; //mission type ------------------------------------------------------------------ MissionTypeFrame = new GUIFrame(new RectTransform(Vector2.One, gameModeSpecificFrame.RectTransform), style: null); GUILayoutGroup missionHolder = new GUILayoutGroup(new RectTransform(Vector2.One, MissionTypeFrame.RectTransform)) { Stretch = true }; CreateSubHeader("MissionType", missionHolder); missionTypeList = new GUIListBox(new RectTransform(Vector2.One, missionHolder.RectTransform)) { OnSelected = (component, obj) => { return false; } }; clientDisabledElements.Add(missionTypeList); List missionTypes = MissionPrefab.GetAllMultiplayerSelectableMissionTypes().ToList(); GUILayoutGroup buttonGroup = new(new RectTransform(Vector2.UnitX, missionTypeList.Content.RectTransform), true) { Stretch = true }; GUIButton selectAllMissionsButton = new(new RectTransform(new Vector2(0.5f, 1f), buttonGroup.RectTransform), TextManager.Get("selectall")) { OnClicked = (_, _) => { IEnumerable validMissions = GetValidMissions(); validMissions.ForEach(missionType => GameMain.Client.ServerSettings?.ClientAdminWrite(ServerSettings.NetFlags.Misc, addedMissionType: missionType)); return true; } }; GUIButton deselectAllMissionsButton = new(new RectTransform(new Vector2(0.5f, 1f), buttonGroup.RectTransform), TextManager.Get("deselectall")) { OnClicked = (_, _) => { IEnumerable validMissions = GetValidMissions(); // The server must have at least one mission selected, so ensure the first in the list is enabled. GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, addedMissionType: validMissions.First()); validMissions.Skip(1).ForEach(missionType => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, removedMissionType: missionType)); return true; } }; buttonGroup.RectTransform.MinSize = (0, buttonGroup.Children.Max(child => child.Rect.Height)); missionTypeTickBoxes = new GUITickBox[missionTypes.Count]; int index = 0; foreach (var missionType in missionTypes.OrderBy(t => TextManager.Get("MissionType." + t.Value).Value)) { GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.05f), missionTypeList.Content.RectTransform) { MinSize = new Point(0, GUI.IntScale(30)) }, style: null) { UserData = missionType, }; missionTypeTickBoxes[index] = new GUITickBox(new RectTransform(Vector2.One, frame.RectTransform), TextManager.Get("MissionType." + missionType.ToString())) { UserData = missionType, ToolTip = TextManager.Get("MissionTypeDescription." + missionType.ToString()), OnSelected = (tickbox) => { RefreshOutpostDropdown(); if (tickbox.Selected) { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, addedMissionType: (Identifier)tickbox.UserData); } else { Identifier firstValidMission = GetValidMissions().First(); if (missionTypeTickBoxes.None(tickBox => tickBox.Selected && tickBox.Parent.Visible)) { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, addedMissionType: firstValidMission); if ((Identifier)tickbox.UserData == firstValidMission) { return true; } } GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, removedMissionType: (Identifier)tickbox.UserData); } return true; } }; frame.RectTransform.MinSize = missionTypeTickBoxes[index].RectTransform.MinSize; index++; } clientDisabledElements.Add(selectAllMissionsButton); clientDisabledElements.Add(deselectAllMissionsButton); clientDisabledElements.AddRange(missionTypeTickBoxes); return gameModeSpecificFrame; IEnumerable GetValidMissions() => missionTypeTickBoxes .Where(tickBox => tickBox.Parent.Visible) .Select(tickBox => (Identifier)tickBox.UserData); } private GUIFrame gameModeSettingsContent; private GUILayoutGroup gameModeSettingsLayout; private GUIComponent CreateGameModeSettingsPanel(GUIComponent parent) { //------------------------------------------------------------------ // settings panel //------------------------------------------------------------------ gameModeSettingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) { Stretch = true }; CreateSubHeader("GameModeSettings", gameModeSettingsLayout); gameModeSettingsContent = new GUIListBox(new RectTransform(Vector2.One, gameModeSettingsLayout.RectTransform)).Content; var winScoreHeader = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), gameModeSettingsContent.RectTransform), TextManager.Get("ServerSettingsWinScorePvP")) { CanBeFocused = false }; clientDisabledElements.Add(winScoreHeader); pvpOnlyElements.Add(winScoreHeader); var winScoreContainer = CreateLabeledSlider(gameModeSettingsContent, headerTag: string.Empty, valueLabelTag: string.Empty, tooltipTag: "ServerSettingsWinScorePvPTooltip", out var winScorePvPSlider, out var winScorePvPSliderLabel); winScorePvPSlider.Range = new Vector2(10, 1000); winScorePvPSlider.StepValue = 10; winScorePvPSlider.OnMoved = (scrollBar, _) => { if (scrollBar.UserData is not GUITextBlock text) { return false; } text.Text = TextManager.GetWithVariable("ServerSettingsWinScoreValuePvP", "[value]", ((int)Math.Round(scrollBar.BarScrollValue, digits: 0)).ToString()); return true; }; winScorePvPSlider.OnReleased = (scrollBar, _) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; AssignComponentToServerSetting(winScorePvPSlider, nameof(ServerSettings.WinScorePvP)); winScorePvPSlider.OnMoved(winScorePvPSlider, winScorePvPSlider.BarScroll); clientDisabledElements.AddRange(winScoreContainer.GetAllChildren()); pvpOnlyElements.Add(winScoreContainer); //(pvp) stun resistance ------------------------------------------------- var sliderContainer = CreateLabeledSlider(gameModeSettingsContent, headerTag: string.Empty, valueLabelTag: "gamemodesettings.stunresistance", tooltipTag: "gamemodesettings.stunresistancetooltip", out var slider, out var sliderLabel); LocalizedString stunResistLabel = sliderLabel.Text; slider.Step = 0.1f; slider.Range = new Vector2(0.0f, 1.0f); slider.OnReleased = (scrollbar, value) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; slider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { ((GUITextBlock)scrollBar.UserData).Text = stunResistLabel.Replace("[percentage]", ((int)MathUtils.Round(scrollBar.BarScrollValue * 100.0f, 10.0f)).ToString()); return true; }; AssignComponentToServerSetting(slider, nameof(ServerSettings.PvPStunResist)); slider.OnMoved(slider, slider.BarScroll); clientDisabledElements.AddRange(sliderContainer.GetAllChildren()); pvpOnlyElements.Add(sliderContainer); //(pvp) mark enemy location toggle -------------------------------------- var markApproximateEnemyLocationToggle = new GUITickBox(new RectTransform(new Vector2(0.4f, 0.06f), gameModeSettingsContent.RectTransform), TextManager.Get("ServerSettingsTrackOpponentInPvP")) { ToolTip = TextManager.Get("gamemodesettings.markenemylocationtooltip"), Selected = GameMain.Client != null && GameMain.Client.ServerSettings.TrackOpponentInPvP, OnSelected = (tt) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; } }; AssignComponentToServerSetting(markApproximateEnemyLocationToggle, nameof(ServerSettings.TrackOpponentInPvP)); clientDisabledElements.Add(markApproximateEnemyLocationToggle); pvpOnlyElements.Add(markApproximateEnemyLocationToggle); //make the header use the height of the tickboxes to get the layout to be a little more uniform winScoreHeader.RectTransform.MinSize = new Point(0, markApproximateEnemyLocationToggle.RectTransform.MinSize.Y); //(pvp) spawn monsters tickbox ----------------------------------------- var spawnMonstersTickbox = new GUITickBox(new RectTransform(Vector2.One, gameModeSettingsContent.RectTransform), TextManager.Get("gamemodesettings.spawnmonsters")) { ToolTip = TextManager.Get("gamemodesettings.spawnmonsterstooltip"), Selected = GameMain.Client != null && GameMain.Client.ServerSettings.PvPSpawnMonsters, OnSelected = (GUITickBox box) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; } }; AssignComponentToServerSetting(spawnMonstersTickbox, nameof(ServerSettings.PvPSpawnMonsters)); clientDisabledElements.Add(spawnMonstersTickbox); pvpOnlyElements.Add(spawnMonstersTickbox); //(pvp) spawn wrecks tickbox ------------------------------------------- var spawnWrecksTickbox = new GUITickBox(new RectTransform(Vector2.One, gameModeSettingsContent.RectTransform), TextManager.Get("gamemodesettings.spawnwrecks")) { ToolTip = TextManager.Get("gamemodesettings.spawnwreckstooltip"), Selected = GameMain.Client != null && GameMain.Client.ServerSettings.PvPSpawnWrecks, OnSelected = (GUITickBox box) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; } }; AssignComponentToServerSetting(spawnWrecksTickbox, nameof(ServerSettings.PvPSpawnWrecks)); clientDisabledElements.Add(spawnWrecksTickbox); pvpOnlyElements.Add(spawnWrecksTickbox); // outpost ----------------------------------------------------------------------------- GUILayoutGroup outpostHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), gameModeSettingsContent.RectTransform), isHorizontal: true) { Visible = false, Stretch = true }; var outpostLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), outpostHolder.RectTransform), TextManager.Get("gamemodesettings.outpost"), wrap: true); outpostDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), outpostHolder.RectTransform), elementCount: 6, listBoxScale: 2.0f) { ToolTip = TextManager.Get("gamemodesettings.outposttooltip"), AfterSelected = (component, obj) => { //don't register selecting the outpost until we've refreshed the available outposts, //otherwise a client may request selecting "nothing" just because there's nothing in the list yet if (outpostDropdownUpToDate && obj != null) { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); } return true; } }; outpostDropdown.ListBox.RectTransform.SetPosition(Anchor.BottomLeft, Pivot.TopLeft); //do this before adding the contents, otherwise they get disabled too (and we just want to disable the dropdown itself) clientDisabledElements.AddRange(outpostHolder.GetAllChildren()); outpostDropdown.AddItem(TextManager.Get("random"), "Random".ToIdentifier()); foreach (var submarineInfo in SubmarineInfo.SavedSubmarines.DistinctBy(s => s.Name)) { outpostDropdown.AddItem(submarineInfo.DisplayName, userData: submarineInfo.Name.ToIdentifier(), toolTip: submarineInfo.Description); } AssignComponentToServerSetting(outpostDropdown, nameof(ServerSettings.SelectedOutpostName)); outpostHolder.RectTransform.MinSize = new Point(0, outpostDropdown.RectTransform.MinSize.Y); campaignHiddenElements.Add(outpostHolder); // biome ----------------------------------------------------------------------------- GUILayoutGroup biomeHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), gameModeSettingsContent.RectTransform), isHorizontal: true) { Stretch = true }; var biomeLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), biomeHolder.RectTransform), TextManager.Get("biome"), wrap: true); var biomeDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), biomeHolder.RectTransform), elementCount: 6, listBoxScale: 2.0f) { AfterSelected = (component, obj) => { if (obj != null) { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); } return true; } }; biomeDropdown.ListBox.RectTransform.SetPosition(Anchor.BottomLeft, Pivot.TopLeft); //do this before adding the contents, otherwise they get disabled too (and we just want to disable the dropdown itself) clientDisabledElements.AddRange(biomeHolder.GetAllChildren()); biomeDropdown.AddItem(TextManager.Get("random"), "Random".ToIdentifier()); foreach (var biome in Biome.Prefabs.OrderBy(b => b.MinDifficulty)) { if (biome.IsEndBiome) { continue; } biomeDropdown.AddItem(biome.DisplayName, biome.Identifier); } AssignComponentToServerSetting(biomeDropdown, nameof(ServerSettings.Biome)); biomeHolder.RectTransform.MinSize = new Point(0, biomeDropdown.RectTransform.MinSize.Y); campaignHiddenElements.Add(biomeHolder); //seed ------------------------------------------------------------------ var seedLabel = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), TextManager.Get("LevelSeed")) { CanBeFocused = false }; LevelSeedBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), seedLabel.RectTransform, Anchor.CenterRight)); LevelSeedBox.OnDeselected += (textBox, key) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.LevelSeed); }; campaignDisabledElements.Add(LevelSeedBox); campaignDisabledElements.Add(seedLabel); clientDisabledElements.Add(LevelSeedBox); clientDisabledElements.Add(seedLabel); LevelSeed = ToolBox.RandomSeed(8); //level difficulty ------------------------------------------------------------------ var levelDifficultyHolder = CreateLabeledSlider(gameModeSettingsContent, "LevelDifficulty", "", "LevelDifficultyExplanation", out levelDifficultySlider, out var difficultySliderLabel, step: 0.01f, range: new Vector2(0.0f, 100.0f)); levelDifficultySlider.OnReleased = (scrollbar, value) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; levelDifficultySlider.OnMoved = (scrollbar, value) => { if (!EventManagerSettings.Prefabs.Any()) { return true; } difficultySliderLabel.Text = EventManagerSettings.GetByDifficultyPercentile(value).Name + $" ({TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue)).ToString())})"; difficultySliderLabel.TextColor = ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Green, GUIStyle.Orange, GUIStyle.Red); return true; }; AssignComponentToServerSetting(levelDifficultySlider, nameof(ServerSettings.SelectedLevelDifficulty)); campaignDisabledElements.AddRange(levelDifficultyHolder.GetAllChildren()); clientDisabledElements.AddRange(levelDifficultyHolder.GetAllChildren()); //bot count ------------------------------------------------------------------ CreateSubHeader("BotSettings", gameModeSettingsContent); var botCountSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botCountSettingHolder.RectTransform), TextManager.Get("BotCount"), wrap: true); var botCountSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), botCountSettingHolder.RectTransform)); for (int i = 0; i <= NetConfig.MaxPlayers; i++) { botCountSelection.AddElement(i, i.ToString()); } AssignComponentToServerSetting(botCountSelection, nameof(ServerSettings.BotCount)); clientDisabledElements.AddRange(botCountSettingHolder.GetAllChildren()); botSettingsElements.Add(botCountSelection); var botSpawnModeSettingHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), botSpawnModeSettingHolder.RectTransform), TextManager.Get("BotSpawnMode"), wrap: true); var botSpawnModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform)); foreach (var botSpawnMode in Enum.GetValues(typeof(BotSpawnMode)).Cast()) { botSpawnModeSelection.AddElement(botSpawnMode, botSpawnMode.ToString(), TextManager.Get($"botspawnmode.{botSpawnMode}.tooltip")); } botSpawnModeSelection.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); AssignComponentToServerSetting(botSpawnModeSelection, nameof(ServerSettings.BotSpawnMode)); clientDisabledElements.AddRange(botSpawnModeSettingHolder.GetAllChildren()); botSettingsElements.Add(botSpawnModeSelection); botCountSelection.OnValueChanged += (_) => { botSpawnModeSelection.Enabled = GameMain.Client.ServerSettings.BotCount > 0; GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); }; //traitor probability ------------------------------------------------------------------ CreateSubHeader("TraitorSettings", gameModeSettingsContent); //spacing new GUIFrame(new RectTransform(new Point(1, GUI.IntScale(5)), gameModeSettingsContent.RectTransform), style: null); //the probability slider is a traitor element, but we don't add it to traitorElements //because we don't want to disable it when sliding it to 0 (need to be able to slide it back!) var traitorProbabilityHolder = CreateLabeledSlider(gameModeSettingsContent, "traitor.probability", "", "traitor.probability.tooltip", out traitorProbabilitySlider, out var traitorProbabilityText, step: 0.01f, range: new Vector2(0.0f, 1.0f)); traitorProbabilitySlider.OnMoved = (scrollbar, value) => { traitorProbabilityText.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollbar.BarScrollValue * 100)).ToString()); traitorProbabilityText.TextColor = value <= 0.0f ? GUIStyle.Green : ToolBox.GradientLerp(scrollbar.BarScroll, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red); RefreshEnabledElements(); return true; }; traitorProbabilitySlider.OnMoved(traitorProbabilitySlider, traitorProbabilitySlider.BarScroll); traitorProbabilitySlider.OnReleased += (scrollbar, value) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; AssignComponentToServerSetting(traitorProbabilitySlider, nameof(ServerSettings.TraitorProbability)); traitorElements.Clear(); clientDisabledElements.AddRange(traitorProbabilityHolder.GetAllChildren()); var traitorDangerHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; var dangerLevelLabel = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerHolder.RectTransform), TextManager.Get("traitor.dangerlevelsetting"), wrap: true) { ToolTip = TextManager.Get("traitor.dangerlevelsetting.tooltip") }; var traitorDangerContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), traitorDangerHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { RelativeSpacing = 0.05f, Stretch = true }; var traitorDangerButtons = new GUIButton[2]; traitorDangerButtons[0] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleLeft") { OnClicked = (button, obj) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: -1); return true; } }; traitorDangerGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 1.0f), traitorDangerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, AbsoluteSpacing = 1 }; for (int i = TraitorEventPrefab.MinDangerLevel; i <= TraitorEventPrefab.MaxDangerLevel; i++) { var difficultyColor = Mission.GetDifficultyColor(i); new GUIImage(new RectTransform(new Vector2(0.75f), traitorDangerGroup.RectTransform), "DifficultyIndicator", scaleToFit: true) { ToolTip = RichString.Rich( $"‖color:{Color.White.ToStringHex()}‖{TextManager.Get($"traitor.dangerlevel.{i}")}‖color:end‖" + '\n' + TextManager.Get($"traitor.dangerlevel.{i}.description")), Color = difficultyColor, DisabledColor = Color.Gray * 0.5f, }; } traitorDangerButtons[1] = new GUIButton(new RectTransform(new Vector2(0.15f, 1.0f), traitorDangerContainer.RectTransform), style: "GUIButtonToggleRight") { OnClicked = (button, obj) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Misc, traitorDangerLevel: 1); return true; } }; traitorDangerContainer.InheritTotalChildrenMinHeight(); SetTraitorDangerIndicators(GameMain.Client?.ServerSettings.TraitorDangerLevel ?? TraitorEventPrefab.MinDangerLevel); traitorElements.Add(dangerLevelLabel); traitorElements.AddRange(traitorDangerGroup.Children); traitorElements.AddRange(traitorDangerButtons); var traitorsMinPlayerCountHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), gameModeSettingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.7f, 0.0f), traitorsMinPlayerCountHolder.RectTransform), TextManager.Get("ServerSettingsTraitorsMinPlayerCount"), wrap: true) { ToolTip = TextManager.Get("ServerSettingsTraitorsMinPlayerCountToolTip") }; var traitorsMinPlayerCount = new GUISelectionCarousel(new RectTransform(new Vector2(0.5f, 1.0f), traitorsMinPlayerCountHolder.RectTransform)); for (int i = 1; i <= NetConfig.MaxPlayers; i++) { traitorsMinPlayerCount.AddElement(i, i.ToString()); } traitorsMinPlayerCount.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); AssignComponentToServerSetting(traitorsMinPlayerCount, nameof(ServerSettings.TraitorsMinPlayerCount)); traitorElements.AddRange(traitorsMinPlayerCountHolder.Children); foreach (var traitorElement in traitorElements) { if (!clientDisabledElements.Contains(traitorElement)) { clientDisabledElements.Add(traitorElement); } } return gameModeSettingsContent; } private GUIButton upgradesTabButton, respawnTabButton; private void SelectRespawnTab() => SelectTabShared(buttonToEnable: respawnTabButton, buttonToDisable: upgradesTabButton, elementsToEnable: respawnSettings, elementsToDisable: disembarkPerkSettings); private void SelectUpgradesTab() => SelectTabShared(buttonToEnable: upgradesTabButton, buttonToDisable: respawnTabButton, elementsToEnable: disembarkPerkSettings, elementsToDisable: respawnSettings); private void SelectTabShared(GUIButton buttonToEnable, GUIButton buttonToDisable, ICollection elementsToEnable, ICollection elementsToDisable) { if (buttonToEnable is null || buttonToDisable is null) { return; } buttonToDisable.Selected = false; buttonToEnable.Selected = true; foreach (var element in elementsToDisable) { element.Visible = element.Enabled = false; } foreach (var element in elementsToEnable) { element.Visible = element.Enabled = true; } } private GUIComponent CreateGeneralSettingsPanel(GUIComponent parent) { //------------------------------------------------------------------ // settings panel //------------------------------------------------------------------ GUILayoutGroup mainContainer = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)); GUILayoutGroup tabContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.066f), mainContainer.RectTransform), isHorizontal: true) { RelativeSpacing = 0.02f, Stretch = true }; respawnTabButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), tabContainer.RectTransform), TextManager.Get("respawnsettings"), style: "GUITabButton") { Selected = true }; upgradesTabButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), tabContainer.RectTransform), TextManager.Get("disembarkpointsettings"), style: "GUITabButton"); respawnTabButton.OnClicked = (button, _) => { SelectRespawnTab(); return true; }; upgradesTabButton.OnClicked = (button, _) => { SelectUpgradesTab(); return true; }; GUIFrame mainFrame = new GUIFrame(new RectTransform(new Vector2(1f, 1.0f - tabContainer.RectTransform.RelativeSize.Y), mainContainer.RectTransform), style: null); GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, mainFrame.RectTransform)); var settingsList = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)); respawnSettings.Add(settingsLayout); CreateDisembarkPointPanel(mainFrame); var settingsContent = settingsList.Content; // ------------------------------------------------------------------ var respawnModeHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), settingsContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; respawnModeLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 0.0f), respawnModeHolder.RectTransform), TextManager.Get("RespawnMode"), wrap: true); respawnModeSelection = new GUISelectionCarousel(new RectTransform(new Vector2(0.6f, 1.0f), respawnModeHolder.RectTransform)); foreach (var respawnMode in Enum.GetValues(typeof(RespawnMode)).Cast().Where(rm => rm != RespawnMode.None)) { respawnModeSelection.AddElement(respawnMode, TextManager.Get($"respawnmode.{respawnMode}"), TextManager.Get($"respawnmode.{respawnMode}.tooltip")); } respawnModeSelection.ElementSelectionCondition += (value) => value != RespawnMode.Permadeath || SelectedMode == GameModePreset.MultiPlayerCampaign; respawnModeSelection.OnValueChanged += (_) => GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); AssignComponentToServerSetting(respawnModeSelection, nameof(ServerSettings.RespawnMode)); GUILayoutGroup shuttleHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), settingsContent.RectTransform), isHorizontal: true) { Stretch = true }; shuttleTickBox = new GUITickBox(new RectTransform(Vector2.One, shuttleHolder.RectTransform), TextManager.Get("RespawnShuttle")) { ToolTip = TextManager.Get("RespawnShuttleExplanation"), Selected = true, OnSelected = (GUITickBox box) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; } }; AssignComponentToServerSetting(shuttleTickBox, nameof(ServerSettings.UseRespawnShuttle)); midRoundRespawnSettings.Add(shuttleTickBox); shuttleTickBox.TextBlock.RectTransform.SizeChanged += () => { shuttleTickBox.TextBlock.AutoScaleHorizontal = true; shuttleTickBox.TextBlock.TextScale = 1.0f; if (shuttleTickBox.TextBlock.TextScale < 0.75f) { shuttleTickBox.TextBlock.Wrap = true; shuttleTickBox.TextBlock.AutoScaleHorizontal = true; shuttleTickBox.TextBlock.TextScale = 1.0f; } }; ShuttleList = new GUIDropDown(new RectTransform(Vector2.One, shuttleHolder.RectTransform), elementCount: 10) { OnSelected = (component, obj) => { SubmarineInfo subInfo = (SubmarineInfo)obj; ShuttleList.Text = subInfo.DisplayName; ShuttleList.ToolTip = subInfo.Description; SelectShuttle(subInfo); return true; } }; ShuttleList.ListBox.RectTransform.MinSize = new Point(250, 0); shuttleHolder.RectTransform.MinSize = new Point(0, ShuttleList.RectTransform.Children.Max(c => c.MinSize.Y)); midRoundRespawnSettings.Add(ShuttleList); respawnIntervalElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnInterval", "", "", out var respawnIntervalSlider, out var respawnIntervalSliderLabel, range: new Vector2(10.0f, 600.0f)); LocalizedString intervalLabel = respawnIntervalSliderLabel.Text; respawnIntervalSlider.StepValue = 10.0f; respawnIntervalSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { GUITextBlock text = scrollBar.UserData as GUITextBlock; text.Text = intervalLabel + " " + ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); return true; }; respawnIntervalSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; respawnIntervalSlider.OnMoved(respawnIntervalSlider, respawnIntervalSlider.BarScroll); AssignComponentToServerSetting(respawnIntervalSlider, nameof(ServerSettings.RespawnInterval)); var minRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsMinRespawn", "", "ServerSettingsMinRespawnToolTip", out var minRespawnSlider, out var minRespawnSliderLabel, step: 0.1f, range: new Vector2(0.0f, 1.0f)); minRespawnSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { GUITextBlock text = scrollBar.UserData as GUITextBlock; text.Text = ToolBox.GetFormattedPercentage(scrollBar.BarScrollValue); return true; }; minRespawnSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; minRespawnSlider.OnMoved(minRespawnSlider, minRespawnSlider.BarScroll); midRoundRespawnSettings.AddRange(minRespawnElement.GetAllChildren()); AssignComponentToServerSetting(minRespawnSlider, nameof(ServerSettings.MinRespawnRatio)); var respawnDurationElement = CreateLabeledSlider(settingsContent, "ServerSettingsRespawnDuration", "", "ServerSettingsRespawnDurationTooltip", out var respawnDurationSlider, out var respawnDurationSliderLabel, step: 0.1f, range: new Vector2(60.0f, 660.0f)); respawnDurationSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { GUITextBlock text = scrollBar.UserData as GUITextBlock; text.Text = scrollBar.BarScrollValue <= 0 ? TextManager.Get("Unlimited") : ToolBox.SecondsToReadableTime(scrollBar.BarScrollValue); return true; }; respawnDurationSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; respawnDurationSlider.ScrollToValue = (GUIScrollBar scrollBar, float barScroll) => { return barScroll >= 1.0f ? 0.0f : barScroll * (scrollBar.Range.Y - scrollBar.Range.X) + scrollBar.Range.X; }; respawnDurationSlider.ValueToScroll = (GUIScrollBar scrollBar, float value) => { return value <= 0.0f ? 1.0f : (value - scrollBar.Range.X) / (scrollBar.Range.Y - scrollBar.Range.X); }; respawnDurationSlider.OnMoved(respawnDurationSlider, respawnDurationSlider.BarScroll); midRoundRespawnSettings.AddRange(respawnDurationElement.GetAllChildren()); AssignComponentToServerSetting(respawnDurationSlider, nameof(ServerSettings.MaxTransportTime)); var skillLossElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnDeath", "", "ServerSettingsSkillLossPercentageOnDeathToolTip", out var skillLossSlider, out var skillLossSliderLabel, range: new Vector2(0, 100)); skillLossSlider.StepValue = 1; skillLossSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { GUITextBlock text = scrollBar.UserData as GUITextBlock; text.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString()); return true; }; skillLossSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; permadeathDisabledRespawnSettings.AddRange(skillLossElement.GetAllChildren()); clientDisabledElements.AddRange(skillLossElement.GetAllChildren()); AssignComponentToServerSetting(skillLossSlider, nameof(ServerSettings.SkillLossPercentageOnDeath)); skillLossSlider.OnMoved(skillLossSlider, skillLossSlider.BarScroll); var skillLossImmediateRespawnElement = CreateLabeledSlider(settingsContent, "ServerSettingsSkillLossPercentageOnImmediateRespawn", "", "ServerSettingsSkillLossPercentageOnImmediateRespawnToolTip", out var skillLossImmediateRespawnSlider, out var skillLossImmediateRespawnSliderLabel, range: new Vector2(0, 100)); skillLossImmediateRespawnSlider.StepValue = 1; skillLossImmediateRespawnSlider.OnMoved = (GUIScrollBar scrollBar, float barScroll) => { GUITextBlock text = scrollBar.UserData as GUITextBlock; text.Text = TextManager.GetWithVariable("percentageformat", "[value]", ((int)Math.Round(scrollBar.BarScrollValue)).ToString()); return true; }; skillLossImmediateRespawnSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; midRoundRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren()); permadeathDisabledRespawnSettings.AddRange(skillLossImmediateRespawnElement.GetAllChildren()); AssignComponentToServerSetting(skillLossImmediateRespawnSlider, nameof(ServerSettings.SkillLossPercentageOnImmediateRespawn)); skillLossImmediateRespawnSlider.OnMoved(skillLossImmediateRespawnSlider, skillLossImmediateRespawnSlider.BarScroll); var newCharacterCostSliderElement = CreateLabeledSlider(settingsContent, "ServerSettings.ReplaceCostPercentage", "", "ServerSettings.ReplaceCostPercentage.tooltip", out var newCharacterCostSlider, out var newCharacterCostSliderLabel, range: new Vector2(0, 200), step: 10f); newCharacterCostSlider.StepValue = 10f; newCharacterCostSlider.OnMoved = (GUIScrollBar scrollBar, float _) => { GUITextBlock textBlock = scrollBar.UserData as GUITextBlock; int currentMultiplier = (int)Math.Round(scrollBar.BarScrollValue); if (currentMultiplier < 1) { textBlock.Text = TextManager.Get("ServerSettings.ReplaceCostPercentage.Free"); } else { textBlock.Text = TextManager.GetWithVariable("percentageformat", "[value]", currentMultiplier.ToString()); } return true; }; newCharacterCostSlider.OnReleased = (GUIScrollBar scrollBar, float barScroll) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; }; clientDisabledElements.AddRange(newCharacterCostSliderElement.GetAllChildren()); permadeathEnabledRespawnSettings.AddRange(newCharacterCostSliderElement.GetAllChildren()); ironmanDisabledRespawnSettings.AddRange(newCharacterCostSliderElement.GetAllChildren()); AssignComponentToServerSetting(newCharacterCostSlider, nameof(ServerSettings.ReplaceCostPercentage)); newCharacterCostSlider.OnMoved(newCharacterCostSlider, newCharacterCostSlider.BarScroll); // initialize var allowBotTakeoverTickbox = new GUITickBox(new RectTransform(Vector2.One, settingsContent.RectTransform), TextManager.Get("AllowBotTakeover")) { ToolTip = TextManager.Get("AllowBotTakeover.Tooltip"), Selected = GameMain.Client != null && GameMain.Client.ServerSettings.AllowBotTakeoverOnPermadeath, OnSelected = (GUITickBox box) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; } }; AssignComponentToServerSetting(allowBotTakeoverTickbox, nameof(ServerSettings.AllowBotTakeoverOnPermadeath)); permadeathEnabledRespawnSettings.Add(allowBotTakeoverTickbox); ironmanDisabledRespawnSettings.Add(allowBotTakeoverTickbox); clientDisabledElements.Add(allowBotTakeoverTickbox); var ironmanTickbox = new GUITickBox(new RectTransform(Vector2.One, settingsContent.RectTransform), TextManager.Get("IronmanMode").ToUpper()) { ToolTip = TextManager.Get("IronmanMode.Tooltip"), Selected = GameMain.Client != null && GameMain.Client.ServerSettings.IronmanMode, OnSelected = (GUITickBox box) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; } }; AssignComponentToServerSetting(ironmanTickbox, nameof(ServerSettings.IronmanMode)); permadeathEnabledRespawnSettings.Add(ironmanTickbox); clientDisabledElements.Add(ironmanTickbox); foreach (var respawnElement in midRoundRespawnSettings) { if (!clientDisabledElements.Contains(respawnElement)) { clientDisabledElements.Add(respawnElement); } } return settingsContent; } private GUIListBox disembarkPerkSettingList; private GUIComponent disembarkPerkDisabledDisclaimer; private GUIComponent noPerksAvailableDisclaimer; private GUITextBlock disembarkPerkFooterText; /// /// Used to prevent disembarkPerkSettingList.AfterSelected from firing when the server settings are updated. /// private bool isUpdatingPerks; public void CreateDisembarkPointPanel(GUIComponent parent) { GUILayoutGroup settingsLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform)) { Stretch = true, Visible = false, }; var settingsList = new GUIListBox(new RectTransform(Vector2.One, settingsLayout.RectTransform)) { SelectMultiple = true, DisabledColor = Color.White * 0.1f }; disembarkPerkSettingList = settingsList; noPerksAvailableDisclaimer = new GUIFrame(new RectTransform(Vector2.One, settingsLayout.RectTransform), style: "GUIBackgroundBlocker") { Visible = false, IgnoreLayoutGroups = true }; new GUITextBlock(new RectTransform(Vector2.One, noPerksAvailableDisclaimer.RectTransform), TextManager.Get("noperksavailable"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) { TextColor = GUIStyle.Red, Shadow = true, }; disembarkPerkDisabledDisclaimer = new GUIFrame(new RectTransform(Vector2.One, settingsLayout.RectTransform), style: "GUIBackgroundBlocker") { IgnoreLayoutGroups = true, }; var disclaimerLayout = new GUILayoutGroup(new RectTransform(Vector2.One, disembarkPerkDisabledDisclaimer.RectTransform)); new GUITextBlock(new RectTransform(new Vector2(1f, 0.3f), disclaimerLayout.RectTransform), TextManager.Get("disembarkpointselectteam"), textAlignment: Alignment.BottomCenter, font: GUIStyle.LargeFont) { TextColor = GUIStyle.Red }; var teamSelectLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.7f), disclaimerLayout.RectTransform), isHorizontal: true); CreateTeamDisclaimerButtons(teamSelectLayout); disembarkPerkFooterText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), settingsLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(28)) }, string.Empty, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterRight, textColor: GUIStyle.TextColorBright, color: Color.Black * 0.8f, style: null) { Padding = new Vector4(10, 0, 10, 0) * GUI.Scale }; UpdatePerkFooterText(settingsList); settingsList.AfterSelected = (component, o) => { if (GameMain.Client?.ServerSettings is not { } settings) { return false; } UpdatePerkFooterText(settingsList); if (isUpdatingPerks) { return false; } bool canChangePerks = ServerSettings.HasPermissionToChangePerks(); if (!canChangePerks) { return false; } switch (MultiplayerPreferences.Instance.TeamPreference) { case CharacterTeamType.Team2: { settings.SelectedSeparatistsPerks = PerksFromSelectedElements(); break; } default: { settings.SelectedCoalitionPerks = PerksFromSelectedElements(); break; } Identifier[] PerksFromSelectedElements() { var list = settingsList.AllSelected.Select(static c => ((DisembarkPerkPrefab)c.UserData)).ToList(); bool potentiallyHasOrphanedPerks = true; do { potentiallyHasOrphanedPerks = false; if (list.None()) { break; } list.ForEachMod(perk => { if (perk.Prerequisite.IsEmpty) { return; } if (list.All(p => p.Identifier != perk.Prerequisite)) { list.Remove(perk); potentiallyHasOrphanedPerks = true; } }); } while (potentiallyHasOrphanedPerks); return list.Select(static p => p.Identifier).ToArray(); } } settings.ClientAdminWritePerks(); return true; }; disembarkPerkSettings.Add(settingsLayout); Identifier disembarkPerkCategory = Identifier.Empty; foreach (var disembarkPerkPrefab in DisembarkPerkPrefab.Prefabs .OrderBy(static p => p.SortCategory) .ThenBy(static p => p.SortKey) .ThenBy(static p => p.Cost)) { if (disembarkPerkCategory != disembarkPerkPrefab.SortCategory) { disembarkPerkCategory = disembarkPerkPrefab.SortCategory; if (!disembarkPerkCategory.IsEmpty) { GUIFrame categoryFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), settingsList.Content.RectTransform), style: null) { CanBeFocused = false }; new GUITextBlock(new RectTransform(Vector2.One, categoryFrame.RectTransform), TextManager.Get($"perkcategory.{disembarkPerkPrefab.SortCategory}"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); } } GUIFrame frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), settingsList.Content.RectTransform), style: "ListBoxElement") { UserData = disembarkPerkPrefab, ToolTip = disembarkPerkPrefab.Description }; GUILayoutGroup prefabLayout = new GUILayoutGroup(new RectTransform(Vector2.One, frame.RectTransform), isHorizontal: true) { Stretch = true }; var perkLabel = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1.0f), prefabLayout.RectTransform), disembarkPerkPrefab.Name, textAlignment: Alignment.CenterLeft) { DisabledTextColor = Color.White * 0.1f, DisabledColor = Color.White * 0.1f, CanBeFocused = false, }; perkLabel.Text = ToolBox.LimitString(perkLabel.Text, perkLabel.Font, perkLabel.Rect.Width); var costLabel = new GUITextBlock(new RectTransform(new Vector2(0.2f, 1.0f), prefabLayout.RectTransform), disembarkPerkPrefab.Cost.ToString(), textAlignment: Alignment.Right) { DisabledTextColor = Color.White * 0.1f, DisabledColor = Color.White * 0.1f, CanBeFocused = false, }; } GameMain.Client?.OnPermissionChanged?.RegisterOverwriteExisting(nameof(CreateDisembarkPointPanel).ToIdentifier(), _ => { UpdateDisembarkPointListFromServerSettings(); }); void CreateTeamDisclaimerButtons(GUILayoutGroup buttonParent) { var team1Button = new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), buttonParent.RectTransform), style: "CoalitionButton") { OnClicked = (button, obj) => { TeamPreferenceListBox?.Select(CharacterTeamType.Team1); return true; } }; var team2Button = new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), buttonParent.RectTransform), style: "SeparatistButton") { OnClicked = (button, obj) => { TeamPreferenceListBox?.Select(CharacterTeamType.Team2); return true; } }; } } private void UpdatePerkFooterText(GUIListBox box) { int pointsLeft = GameMain.NetworkMember?.ServerSettings?.DisembarkPointAllowance ?? -1; bool ignorePerksThatCantApplyWithoutSub = GameSession.ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(SelectedMode, MissionTypes); foreach (GUIComponent child in box.Content.Children) { if (box.AllSelected.Contains(child) && child.UserData is DisembarkPerkPrefab perkPrefab) { if (ignorePerksThatCantApplyWithoutSub && perkPrefab.PerkBehaviors.Any(static b => !b.CanApplyWithoutSubmarine())) { continue; } pointsLeft -= perkPrefab.Cost; } } disembarkPerkFooterText.Text = TextManager.GetWithVariable("disembarkpointleft", "[amount]", pointsLeft.ToString()); disembarkPerkFooterText.TextColor = pointsLeft < 0 ? GUIStyle.Red : GUIStyle.TextColorBright; } public void UpdateDisembarkPointListFromServerSettings() { if (disembarkPerkSettingList is null || disembarkPerkDisabledDisclaimer is null || disembarkPerkFooterText is null) { return; } CharacterTeamType teamPreference = MultiplayerPreferences.Instance.TeamPreference; bool hasTeamPreference = teamPreference is (CharacterTeamType.Team1 or CharacterTeamType.Team2); if (SelectedMode != GameModePreset.PvP) { teamPreference = CharacterTeamType.Team1; hasTeamPreference = true; } disembarkPerkDisabledDisclaimer.Visible = !hasTeamPreference; disembarkPerkFooterText.Visible = hasTeamPreference; SetEnabled(hasTeamPreference); bool canManagePerks = ServerSettings.HasPermissionToChangePerks(); if (!canManagePerks) { SetEnabled(false); } isUpdatingPerks = true; bool hasAvailablePerks = false; if (GameMain.Client?.ServerSettings is { } settings) { Identifier[] selectedPerks = teamPreference switch { CharacterTeamType.Team1 => settings.SelectedCoalitionPerks, CharacterTeamType.Team2 => settings.SelectedSeparatistsPerks, _ => Array.Empty() }; bool ignorePerksThatCantApplyWithoutSub = GameSession.ShouldIgnorePerksThatCanNotApplyWithoutSubmarine(SelectedMode, MissionTypes); disembarkPerkSettingList.Deselect(); foreach (GUIComponent child in disembarkPerkSettingList.Content.Children) { if (child.UserData is not DisembarkPerkPrefab perkPrefab) { continue; } bool shouldSelect = selectedPerks.Contains(perkPrefab.Identifier); bool hasPrerequisite = !perkPrefab.Prerequisite.IsEmpty; bool isMutuallyExclusivePerkSelected = selectedPerks.Any(p => perkPrefab.MutuallyExclusivePerks.Contains(p)); TogglePerkElement(enabled: true); if (shouldSelect) { disembarkPerkSettingList.Select(child.UserData, force: GUIListBox.Force.Yes, GUIListBox.AutoScroll.Disabled); } if (hasPrerequisite) { bool enabled = selectedPerks.Contains(perkPrefab.Prerequisite); TogglePerkElement(enabled); } if (isMutuallyExclusivePerkSelected) { TogglePerkElement(enabled: false); } if (ignorePerksThatCantApplyWithoutSub) { if (perkPrefab.PerkBehaviors.Any(static b => !b.CanApplyWithoutSubmarine())) { TogglePerkElement(enabled: false); } } if (child.Enabled) { hasAvailablePerks = true; } void TogglePerkElement(bool enabled) { child.Enabled = enabled; foreach (GUITextBlock text in child.GetAllChildren()) { text.Enabled = enabled; } } } } noPerksAvailableDisclaimer.Visible = !hasAvailablePerks; if (!hasAvailablePerks) { disembarkPerkDisabledDisclaimer.Visible = false; } UpdatePerkFooterText(disembarkPerkSettingList); isUpdatingPerks = false; void SetEnabled(bool enabled) { disembarkPerkSettingList.Enabled = enabled; foreach (GUIComponent child in disembarkPerkSettingList.Content.Children) { //child.Enabled = enabled; foreach (GUITextBlock block in child.GetAllChildren()) { block.Enabled = enabled; } } } } public static void SelectShuttle(SubmarineInfo info) { GameMain.Client?.RequestSelectSub(info, SelectedSubType.Shuttle); } public static GUITextBlock CreateSubHeader(string textTag, GUIComponent parent, string toolTipTag = null) { var header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.055f), parent.RectTransform) { MinSize = new Point(0, GUI.IntScale(28)) }, TextManager.Get(textTag), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft, textColor: GUIStyle.TextColorBright) { CanBeFocused = false }; if (!toolTipTag.IsNullOrEmpty()) { header.ToolTip = TextManager.Get(toolTipTag); } return header; } public static GUIComponent CreateLabeledSlider(GUIComponent parent, string headerTag, string valueLabelTag, string tooltipTag, out GUIScrollBar slider, out GUITextBlock label, float? step = null, Vector2? range = null) { return CreateLabeledSlider(parent, headerTag, valueLabelTag, tooltipTag, out slider, out label, out GUITextBlock _, step, range); } public static GUIComponent CreateLabeledSlider(GUIComponent parent, string headerTag, string valueLabelTag, string tooltipTag, out GUIScrollBar slider, out GUITextBlock label, out GUITextBlock header, float? step = null, Vector2? range = null) { GUILayoutGroup verticalLayout = null; header = null; if (!headerTag.IsNullOrEmpty()) { verticalLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), isHorizontal: false) { Stretch = true }; header = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), verticalLayout.RectTransform), TextManager.Get(headerTag), textAlignment: Alignment.CenterLeft) { CanBeFocused = false }; header.RectTransform.MinSize = new Point(0, (int)header.TextSize.Y); } var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, headerTag == null ? 0.0f : 0.5f), (verticalLayout ?? parent).RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; //spacing new GUIFrame(new RectTransform(new Point(GUI.IntScale(5), 0), container.RectTransform), style: null); slider = new GUIScrollBar(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform), barSize: 0.1f, style: "GUISlider"); if (step.HasValue) { slider.Step = step.Value; } if (range.HasValue) { slider.Range = range.Value; } container.RectTransform.MinSize = new Point(0, slider.RectTransform.MinSize.Y); container.RectTransform.MaxSize = new Point(int.MaxValue, slider.RectTransform.MaxSize.Y); verticalLayout?.InheritTotalChildrenMinHeight(); label = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), container.RectTransform, Anchor.CenterRight), string.IsNullOrEmpty(valueLabelTag) ? "" : TextManager.Get(valueLabelTag), textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont) { CanBeFocused = false }; //slider has a reference to the label to change the text when it's used slider.UserData = label; slider.ToolTip = label.ToolTip = TextManager.Get(tooltipTag); return verticalLayout ?? container; } public static GUINumberInput CreateLabeledNumberInput(GUIComponent parent, string labelTag, int min, int max, string toolTipTag = null, GUIFont font = null) { var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), parent.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f, ToolTip = TextManager.Get(labelTag) }; var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft, font: font) { AutoScaleHorizontal = true }; if (!string.IsNullOrEmpty(toolTipTag)) { label.ToolTip = TextManager.Get(toolTipTag); } var input = new GUINumberInput(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), NumberType.Int) { MinValueInt = min, MaxValueInt = max }; container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); return input; } public static GUIDropDown CreateLabeledDropdown(GUIComponent parent, string labelTag, int numElements, string toolTipTag = null) { var container = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f, ToolTip = TextManager.Get(labelTag) }; var label = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), container.RectTransform), TextManager.Get(labelTag), textAlignment: Alignment.CenterLeft) { AutoScaleHorizontal = true }; if (!string.IsNullOrEmpty(toolTipTag)) { label.ToolTip = TextManager.Get(toolTipTag); } var input = new GUIDropDown(new RectTransform(new Vector2(0.3f, 1.0f), container.RectTransform), elementCount: numElements); container.RectTransform.MinSize = new Point(0, input.RectTransform.MinSize.Y); container.RectTransform.MaxSize = new Point(int.MaxValue, input.RectTransform.MaxSize.Y); return input; } private void CreateSidePanelContents(GUIComponent rightPanel) { //player info panel ------------------------------------------------------------ var myCharacterFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.55f), rightPanel.RectTransform), style: null); var myCharacterContent = new GUILayoutGroup(new RectTransform(new Vector2(1), myCharacterFrame.RectTransform, Anchor.Center)) { Stretch = true }; var checkBoxContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.0f), myCharacterContent.RectTransform), isHorizontal: true) { Stretch = true }; spectateBox = new GUITickBox(new RectTransform(new Vector2(0.6f, 1.0f), checkBoxContainer.RectTransform), TextManager.Get("spectatebutton")) { Selected = false, OnSelected = ToggleSpectate, UserData = "spectate" }; afkBox = new GUITickBox(new RectTransform(new Vector2(0.4f, 1.0f), checkBoxContainer.RectTransform), TextManager.Get("afkbutton")) { Selected = false, ToolTip = TextManager.Get("afkbutton.tooltip") }; checkBoxContainer.RectTransform.MinSize = new Point(0, spectateBox.RectTransform.MinSize.Y); playerInfoContent = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), myCharacterContent.RectTransform)) { Stretch = true }; // Social area GUIFrame logFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.45f), rightPanel.RectTransform), style: null); GUILayoutGroup logContents = new GUILayoutGroup(new RectTransform(Vector2.One, logFrame.RectTransform, Anchor.Center)) { Stretch = true }; GUILayoutGroup socialHolder = null; GUILayoutGroup serverLogHolder = null; LogButtons = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), logContents.RectTransform), true) { Stretch = true, RelativeSpacing = 0.02f }; clientHiddenElements.Add(LogButtons); // Show chat button chatPanelTabButtons.Add(new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), TextManager.Get("Chat"), style: "GUITabButton") { Selected = true, OnClicked = (GUIButton button, object userData) => { if (socialHolder != null) { socialHolder.Visible = true; } if (serverLogHolder != null) { serverLogHolder.Visible = false; } chatPanelTabButtons.ForEach(otherBtn => otherBtn.Selected = otherBtn == button); return true; } }); // Server log button chatPanelTabButtons.Add(new GUIButton(new RectTransform(new Vector2(0.5f, 1.25f), LogButtons.RectTransform), TextManager.Get("ServerLog"), style: "GUITabButton") { OnClicked = (GUIButton button, object userData) => { if (socialHolder != null) { socialHolder.Visible = false; } if (serverLogHolder is { Visible: false }) { if (GameMain.Client?.ServerSettings?.ServerLog == null) { return false; } serverLogHolder.Visible = true; GameMain.Client.ServerSettings.ServerLog.AssignLogFrame(serverLogReverseButton, serverLogBox, serverLogFilterTicks.Content, serverLogFilter); } chatPanelTabButtons.ForEach(otherBtn => otherBtn.Selected = otherBtn == button); return true; } }); GUITextBlock.AutoScaleAndNormalize(chatPanelTabButtons.Select(btn => btn.TextBlock)); GUIFrame logHolderBottom = new GUIFrame(new RectTransform(Vector2.One, logContents.RectTransform), style: null) { CanBeFocused = false }; socialHolder = new GUILayoutGroup(new RectTransform(Vector2.One, logHolderBottom.RectTransform, Anchor.Center)) { Stretch = true }; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), socialHolder.RectTransform), style: null) { CanBeFocused = false }; GUILayoutGroup socialHolderHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), socialHolder.RectTransform), isHorizontal: true) { Stretch = true }; //chatbox ---------------------------------------------------------------------- chatBox = new GUIListBox(new RectTransform(new Vector2(0.6f, 1.0f), socialHolderHorizontal.RectTransform)); //player list ------------------------------------------------------------------ PlayerList = new GUIListBox(new RectTransform(new Vector2(0.4f, 1.0f), socialHolderHorizontal.RectTransform)) { PlaySoundOnSelect = true, OnSelected = (component, userdata) => { SelectPlayer(userdata as Client); return true; } }; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), socialHolder.RectTransform), style: null) { CanBeFocused = false }; // Chat input chatRow = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), socialHolder.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; RefreshChatrow(); serverLogHolder = new GUILayoutGroup(new RectTransform(Vector2.One, logHolderBottom.RectTransform, Anchor.Center)) { Stretch = true, Visible = false }; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), serverLogHolder.RectTransform), style: null) { CanBeFocused = false }; GUILayoutGroup serverLogHolderHorizontal = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), serverLogHolder.RectTransform), isHorizontal: true) { Stretch = true }; //server log ---------------------------------------------------------------------- GUILayoutGroup serverLogListboxLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), serverLogHolderHorizontal.RectTransform)) { Stretch = true }; serverLogReverseButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), serverLogListboxLayout.RectTransform), style: "UIToggleButtonVertical"); serverLogBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), serverLogListboxLayout.RectTransform)) { AutoHideScrollBar = false }; //filter tickbox list ------------------------------------------------------------------ serverLogFilterTicks = new GUIListBox(new RectTransform(new Vector2(0.5f, 1.0f), serverLogHolderHorizontal.RectTransform) { MinSize = new Point(150, 0) }) { OnSelected = (component, userdata) => { return false; } }; // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), serverLogHolder.RectTransform), style: null) { CanBeFocused = false }; // Filter text input serverLogFilter = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.07f), serverLogHolder.RectTransform)) { MaxTextLength = ChatMessage.MaxLength, Font = GUIStyle.SmallFont }; } private void CreateBottomPanelContents(GUIComponent bottomBar) { //bottom panel ------------------------------------------------------------ GUILayoutGroup bottomBarLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) { Stretch = true, IsHorizontal = true, RelativeSpacing = PanelSpacing }; GUILayoutGroup bottomBarMid = new GUILayoutGroup(new RectTransform(new Vector2(0.4f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) { Stretch = true, IsHorizontal = true, RelativeSpacing = PanelSpacing }; GUILayoutGroup bottomBarRight = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 1.0f), bottomBar.RectTransform), childAnchor: Anchor.CenterLeft) { Stretch = true, IsHorizontal = true, RelativeSpacing = PanelSpacing }; var disconnectButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), bottomBarLeft.RectTransform), TextManager.Get("disconnect")) { OnClicked = (bt, userdata) => { GameMain.QuitToMainMenu(save: false, showVerificationPrompt: true); return true; } }; disconnectButton.TextBlock.AutoScaleHorizontal = true; // file transfers ------------------------------------------------------------ FileTransferFrame = new GUIFrame(new RectTransform(Vector2.One, bottomBarLeft.RectTransform), style: "TextFrame"); var fileTransferContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), FileTransferFrame.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.05f }; FileTransferTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), "", font: GUIStyle.SmallFont); var fileTransferBottom = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), fileTransferContent.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; FileTransferProgressBar = new GUIProgressBar(new RectTransform(new Vector2(0.6f, 1.0f), fileTransferBottom.RectTransform), 0.0f, Color.DarkGreen); FileTransferProgressText = new GUITextBlock(new RectTransform(Vector2.One, FileTransferProgressBar.RectTransform), "", font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft); new GUIButton(new RectTransform(new Vector2(0.4f, 1.0f), fileTransferBottom.RectTransform), TextManager.Get("cancel"), style: "GUIButtonSmall") { OnClicked = (btn, userdata) => { if (FileTransferFrame.UserData is not FileReceiver.FileTransferIn transfer) { return false; } GameMain.Client?.CancelFileTransfer(transfer); GameMain.Client?.FileReceiver.StopTransfer(transfer); return true; } }; roundControlsHolder = new GUILayoutGroup(new RectTransform(Vector2.One, bottomBarRight.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; GUIFrame readyToStartContainer = new GUIFrame(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), style: "TextFrame") { Visible = false }; // Ready to start tickbox ReadyToStartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), readyToStartContainer.RectTransform, anchor: Anchor.Center), TextManager.Get("ReadyToStartTickBox")); joinOnGoingRoundButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), TextManager.Get("ServerListJoin")); EndButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), TextManager.Get("endround")) { //spooky red color for a destructive action Color = GUIStyle.Red, OnClicked = (btn, obj) => { if (GameMain.Client == null) { return true; } GUI.CreateVerificationPrompt(GameMain.GameSession?.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd", () => { GameMain.Client?.RequestEndRound(save: false); }); return true; }, Visible = false, IgnoreLayoutGroups = true }; // Start button StartButton = new GUIButton(new RectTransform(Vector2.One, roundControlsHolder.RectTransform), TextManager.Get("StartGameButton")) { OnClicked = (btn, obj) => { if (GameMain.Client == null) { return true; } //the player presumably no longer wants to be afk if they clicked the start button if (afkBox.Selected) { afkBox.Flash(GUIStyle.Green); } afkBox.Selected = false; if (CampaignSetupFrame.Visible && CampaignSetupUI != null) { CampaignSetupUI.StartGameClicked(btn, obj); } else { //if a campaign is active, and we're not setting one up atm, start button continues the existing campaign GameMain.Client.RequestStartRound(continueCampaign: GameMain.GameSession?.GameMode is CampaignMode && CampaignSetupFrame is not { Visible: true }); CoroutineManager.StartCoroutine(WaitForStartRound(StartButton), "WaitForStartRound"); } return true; } }; clientHiddenElements.Add(StartButton); bottomBar.RectTransform.MinSize = new Point(0, (int)Math.Max(ReadyToStartBox.RectTransform.MinSize.Y / 0.75f, StartButton.RectTransform.MinSize.Y)); //autorestart ------------------------------------------------------------------ autoRestartText = new GUITextBlock(new RectTransform(Vector2.One, bottomBarMid.RectTransform), "", font: GUIStyle.SmallFont, style: "TextFrame", textAlignment: Alignment.Center); GUIFrame autoRestartBoxContainer = new GUIFrame(new RectTransform(Vector2.One, bottomBarMid.RectTransform), style: "TextFrame"); autoRestartBox = new GUITickBox(new RectTransform(new Vector2(0.95f, 0.75f), autoRestartBoxContainer.RectTransform, Anchor.Center), TextManager.Get("AutoRestart")) { OnSelected = (tickBox) => { GameMain.Client?.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.Properties); return true; } }; clientDisabledElements.Add(autoRestartBox); AssignComponentToServerSetting(autoRestartBox, nameof(ServerSettings.AutoRestart)); } public void StopWaitingForStartRound() { CoroutineManager.StopCoroutines("WaitForStartRound"); if (StartButton != null) { StartButton.Enabled = true; } GUI.ClearCursorWait(); } public const string PleaseWaitPopupUserData = "PleaseWaitPopup"; public static IEnumerable WaitForStartRound(GUIButton startButton) { GUI.SetCursorWaiting(); LocalizedString headerText = TextManager.Get("RoundStartingPleaseWait"); var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), Array.Empty()) { UserData = PleaseWaitPopupUserData }; if (startButton != null) { startButton.Enabled = false; } DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, 10); while (Selected == GameMain.NetLobbyScreen && DateTime.Now < timeOut) { msgBox.Header.Text = headerText + new string('.', ((int)Timing.TotalTime % 3 + 1)); yield return CoroutineStatus.Running; } msgBox.Close(); if (startButton != null) { startButton.Enabled = true; } GUI.ClearCursorWait(); yield return CoroutineStatus.Success; } public override void Deselect() { GameMain.Client?.OnPermissionChanged.TryDeregister(nameof(CreateDisembarkPointPanel).ToIdentifier()); SaveAppearance(); chatInput.Deselect(); CampaignCharacterDiscarded = false; CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; } public override void Select() { if (GameMain.NetworkMember == null) { return; } visibilityMenuOrder.Clear(); CharacterAppearanceCustomizationMenu?.Dispose(); JobSelectionFrame = null; Character.Controlled = null; GameMain.LightManager.LosEnabled = false; GUI.PreventPauseMenuToggle = false; CampaignCharacterDiscarded = false; changesPendingText?.Parent?.RemoveChild(changesPendingText); changesPendingText = null; RefreshChatrow(); //disable/hide elements the clients are not supposed to use/see clientDisabledElements.ForEach(c => c.Enabled = false); clientHiddenElements.ForEach(c => c.Visible = false); RefreshEnabledElements(); createPendingChangesText = false; TabMenu.PendingChanges = false; if (GameMain.Client != null) { joinOnGoingRoundButton.Visible = GameMain.Client.GameStarted; ReadyToStartBox.Selected = false; GameMain.Client.SetReadyToStart(ReadyToStartBox); } else { joinOnGoingRoundButton.Visible = false; } SetSpectate(spectateBox.Selected); if (GameMain.Client != null) { afkBox.Visible = !GameMain.Client.IsServerOwner && GameMain.Client.ServerSettings.AllowAFK; GameMain.Client.Voting.ResetVotes(GameMain.Client.ConnectedClients); joinOnGoingRoundButton.OnClicked = (btn, userdata) => { if (afkBox is { Selected: true }) { afkBox.Selected = false; afkBox.Flash(GUIStyle.Green); } GameMain.Client.SendJoinOngoingRequest(btn); return true; }; ReadyToStartBox.OnSelected = GameMain.Client.SetReadyToStart; } roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); roundControlsHolder.Recalculate(); AssignComponentsToServerSettings(); RefreshPlaystyleIcons(); base.Select(); } public void RefreshEnabledElements() { if (GameMain.Client == null) { return; } var client = GameMain.Client; var settings = client.ServerSettings; bool manageSettings = HasPermission(ClientPermissions.ManageSettings); bool campaignSelected = CampaignFrame.Visible || CampaignSetupFrame.Visible; bool campaignStarted = CampaignFrame.Visible; bool gameStarted = client != null && client.GameStarted; // First, enable or disable elements based on client permissions foreach (var element in clientDisabledElements) { element.Enabled = manageSettings; } // Then disable elements depending on other conditions traitorElements.ForEach(e => e.Enabled &= settings.TraitorProbability > 0); SetTraitorDangerIndicators(settings.TraitorDangerLevel); respawnModeSelection.Enabled = respawnModeLabel.Enabled = manageSettings && !gameStarted; midRoundRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode != RespawnMode.BetweenRounds); permadeathDisabledRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode != RespawnMode.Permadeath); permadeathEnabledRespawnSettings.ForEach(e => e.Enabled &= settings.RespawnMode == RespawnMode.Permadeath && !gameStarted); ironmanDisabledRespawnSettings.ForEach(e => e.Enabled &= !settings.IronmanMode); // The respawn interval is used even if the shuttle is not respawnIntervalElement.GetAllChildren().ForEach(e => e.Enabled = settings.RespawnMode != RespawnMode.BetweenRounds && manageSettings); //go through the individual elements that are only enabled in a specific context shuttleTickBox.Enabled &= !gameStarted; if (ShuttleList != null) { // Shuttle list depends on shuttle tickbox ShuttleList.Enabled &= shuttleTickBox.Enabled && HasPermission(ClientPermissions.SelectSub); ShuttleList.ButtonEnabled = ShuttleList.Enabled; } if (SubList != null) { SubList.Enabled = !campaignStarted && (settings.AllowSubVoting || HasPermission(ClientPermissions.SelectSub)); } if (ModeList != null) { ModeList.Enabled = !gameStarted && (settings.AllowModeVoting || HasPermission(ClientPermissions.SelectMode)); } RefreshStartButtonVisibility(); botSettingsElements.ForEach(b => b.Enabled = !campaignStarted && manageSettings); campaignDisabledElements.ForEach(e => e.Enabled = !campaignSelected && manageSettings); levelDifficultySlider.ToolTip = levelDifficultySlider.Enabled ? string.Empty : TextManager.Get("campaigndifficultydisabled"); //hide elements the client shouldn't foreach (var element in clientHiddenElements) { element.Visible = manageSettings; } //go through the individual elements that are only visible in a specific context ReadyToStartBox.Parent.Visible = !gameStarted; LogButtons.Visible = HasPermission(ClientPermissions.ServerLog); client?.UpdateLogButtonPermissions(); roundControlsHolder.Children.ForEach(c => c.IgnoreLayoutGroups = !c.Visible); roundControlsHolder.Children.ForEach(c => c.RectTransform.RelativeSize = Vector2.One); roundControlsHolder.Recalculate(); SettingsButton.OnClicked = settings.ToggleSettingsFrame; RefreshGameModeContent(); static bool HasPermission(ClientPermissions permissions) { if (GameMain.Client == null) { return false; } return GameMain.Client.HasPermission(permissions); } } public void ShowSpectateButton() { if (GameMain.Client == null) { return; } joinOnGoingRoundButton.Visible = true; joinOnGoingRoundButton.Enabled = true; StartButton.Visible = false; } public void SetCampaignCharacterInfo(CharacterInfo newCampaignCharacterInfo) { if (newCampaignCharacterInfo != null) { if (CampaignCharacterDiscarded) { return; } if (campaignCharacterInfo != newCampaignCharacterInfo) { campaignCharacterInfo = newCampaignCharacterInfo; SaveAppearance(); UpdatePlayerFrame(campaignCharacterInfo, false); } } else if (campaignCharacterInfo != null) { campaignCharacterInfo = null; UpdatePlayerFrame(null, false); } } private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing = true) { UpdatePlayerFrame(characterInfo, allowEditing, playerInfoContent); } public void CreatePlayerFrame(GUIComponent parent, bool createPendingText = true, bool alwaysAllowEditing = false) { if (GameMain.Client == null) { return; } UpdatePlayerFrame( Character.Controlled?.Info ?? playerInfoContent.UserData as CharacterInfo ?? GameMain.Client.CharacterInfo, allowEditing: alwaysAllowEditing || campaignCharacterInfo == null, parent: parent, createPendingText: createPendingText); } private void UpdatePlayerFrame(CharacterInfo characterInfo, bool allowEditing, GUIComponent parent, bool createPendingText = false) { if (GameMain.Client == null) { return; } // When permanently dead and still characterless, spectating is the only option spectateBox.Enabled = !PermanentlyDead; createPendingChangesText = createPendingText; if (characterInfo == null || CampaignCharacterDiscarded) { characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, GameMain.Client.Name, null); characterInfo.RecreateHead(MultiplayerPreferences.Instance); // not necessarily the head of the last character GameMain.Client.CharacterInfo = characterInfo; characterInfo.OmitJobInMenus = true; } parent.ClearChildren(); bool isGameRunning = GameMain.GameSession?.IsRunning ?? false; parent.ClearChildren(); parent.UserData = characterInfo; bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName; changesPendingText?.Parent?.RemoveChild(changesPendingText); changesPendingText = null; if (TabMenu.PendingChanges) { CreateChangesPendingText(); } CharacterNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.065f), parent.RectTransform), !nameChangePending ? characterInfo.Name : GameMain.Client.PendingName, textAlignment: Alignment.Center) { MaxTextLength = Client.MaxNameLength, OverflowClip = true }; if (!allowEditing || (PermanentlyDead && !characterInfo.RenamingEnabled)) { CharacterNameBox.Readonly = true; CharacterNameBox.Enabled = false; } else { CharacterNameBox.OnEnterPressed += (tb, text) => { CharacterNameBox.Deselect(); return true; }; CharacterNameBox.OnDeselected += (tb, key) => { if (GameMain.Client == null) { return; } string newName = Client.SanitizeName(tb.Text); if (newName == GameMain.Client.Name) { return; } if (string.IsNullOrWhiteSpace(newName)) { tb.Text = GameMain.Client.Name; } else { if (isGameRunning) { GameMain.Client.PendingName = tb.Text; TabMenu.PendingChanges = true; if (createPendingText) { CreateChangesPendingText(); } } else { ReadyToStartBox.Selected = false; } GameMain.Client.SetName(tb.Text); } }; } //spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.006f), parent.RectTransform), style: null); if (allowEditing && (!PermadeathMode || !isGameRunning)) { GUILayoutGroup characterInfoTabs = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.07f), parent.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f }; jobPreferencesButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), characterInfoTabs.RectTransform), TextManager.Get("JobPreferences"), style: "GUITabButton") { Selected = true, OnClicked = SelectJobPreferencesTab }; appearanceButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), characterInfoTabs.RectTransform), TextManager.Get("CharacterAppearance"), style: "GUITabButton") { OnClicked = SelectAppearanceTab }; GUITextBlock.AutoScaleAndNormalize(jobPreferencesButton.TextBlock, appearanceButton.TextBlock); // Unsubscribe from previous events, not even sure if this matters here but it doesn't hurt so why not if (characterInfoFrame != null) { characterInfoFrame.RectTransform.SizeChanged -= RecalculateSubDescription; } characterInfoFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform), style: null); characterInfoFrame.RectTransform.SizeChanged += RecalculateSubDescription; JobPreferenceContainer = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), style: "GUIFrameListBox"); characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0f, 0.025f) }); JobList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.6f), JobPreferenceContainer.RectTransform, Anchor.BottomCenter), true) { Enabled = true, PlaySoundOnSelect = true, OnSelected = (child, obj) => { if (child.IsParentOf(GUI.MouseOn)) { return false; } return OpenJobSelection(child, obj); } }; for (int i = 0; i < 3; i++) { JobVariant jobPrefab = null; while (i < MultiplayerPreferences.Instance.JobPreferences.Count) { var jobPreference = MultiplayerPreferences.Instance.JobPreferences[i]; if (!JobPrefab.Prefabs.TryGet(jobPreference.JobIdentifier, out JobPrefab prefab) || prefab.HiddenJob) { MultiplayerPreferences.Instance.JobPreferences.RemoveAt(i); continue; } // The old job variant system used one-based indexing // so let's make sure no one get to pick a variant which doesn't exist int variant = Math.Min(jobPreference.Variant, prefab.Variants - 1); jobPrefab = new JobVariant(prefab, variant); break; } var slot = new GUIFrame(new RectTransform(new Vector2(0.333f, 1.0f), JobList.Content.RectTransform), style: "ListBoxElementSquare") { CanBeFocused = true, UserData = jobPrefab }; } UpdateJobPreferences(characterInfo); appearanceFrame = new GUIFrame(new RectTransform(Vector2.One, characterInfoFrame.RectTransform), style: "GUIFrameListBox") { Visible = false, Color = Color.White }; } else { characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.16f), parent.RectTransform, Anchor.TopCenter)); if (PermanentlyDead) { new GUITextBlock( new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("deceased"), textAlignment: Alignment.Center, font: GUIStyle.LargeFont); if (GameMain.Client?.ServerSettings is { IronmanModeActive: true }) { new GUITextBlock( new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("lobby.ironmaninfo"), textAlignment: Alignment.Center, wrap: true); } else { new GUITextBlock( new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("lobby.permadeathinfo"), textAlignment: Alignment.Center, wrap: true); new GUITextBlock( new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("lobby.permadeathoptionsexplanation"), textAlignment: Alignment.Center, wrap: true); } } else { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), characterInfo.Job.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont, wrap: true) { HoverColor = Color.Transparent, SelectedColor = Color.Transparent }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), TextManager.Get("Skills"), font: GUIStyle.SubHeadingFont); foreach (Skill skill in characterInfo.Job.GetSkills()) { Color textColor = Color.White * (0.5f + skill.Level / 200.0f); var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), parent.RectTransform), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + skill.Identifier), ((int)skill.Level).ToString()), textColor, font: GUIStyle.SmallFont); } } // Spacing new GUIFrame(new RectTransform(new Vector2(1.0f, 0.15f), parent.RectTransform), style: null); if (GameMain.Client?.ServerSettings?.RespawnMode != RespawnMode.Permadeath) { // Button to create new character new GUIButton(new RectTransform(new Vector2(0.8f, 0.1f), parent.RectTransform, Anchor.BottomCenter), TextManager.Get("CreateNew")) { IgnoreLayoutGroups = true, OnClicked = (btn, userdata) => { TryDiscardCampaignCharacter(() => { UpdatePlayerFrame(null, true, parent); }); return true; } }; } } TeamPreferenceListBox = null; if (SelectedMode == GameModePreset.PvP) { TeamPreferenceListBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.04f), parent.RectTransform, anchor: Anchor.TopLeft, pivot: Pivot.TopLeft), isHorizontal: true, style: null) { Enabled = true, KeepSpaceForScrollBar = false, PlaySoundOnSelect = true, ScrollBarEnabled = false, ScrollBarVisible = false }; TeamPreferenceListBox.RectTransform.MinSize = new Point(0, GUI.IntScale(30)); TeamPreferenceListBox.UpdateDimensions(); Color team1Color = new Color(0, 110, 150, 255); pvpTeamChoiceTeam1 = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team1"), textAlignment: Alignment.Center, style: null) { UserData = CharacterTeamType.Team1, CanBeFocused = true, Padding = Vector4.One * 10.0f * GUI.Scale, Color = Color.Lerp(team1Color, Color.Black, 0.7f) * 0.7f, HoverColor = team1Color * 0.95f, SelectedColor = team1Color * 0.8f, OutlineColor = team1Color, TextColor = Color.White, HoverTextColor = Color.White, SelectedTextColor = Color.White, DisabledColor = team1Color * 0.25f, DisabledTextColor = Color.Gray, }; Color noPreferenceColor = new Color(100, 100, 100, 255); pvpTeamChoiceMiddleButton = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1.0f), TeamPreferenceListBox.Content.RectTransform), "", textAlignment: Alignment.Center, style: null) { UserData = CharacterTeamType.None, CanBeFocused = true, Padding = Vector4.One * 10.0f * GUI.Scale, Color = Color.Lerp(noPreferenceColor, Color.Black, 0.7f) * 0.7f, HoverColor = noPreferenceColor * 0.95f, SelectedColor = noPreferenceColor * 0.8f, OutlineColor = noPreferenceColor, TextColor = Color.White, HoverTextColor = Color.White, SelectedTextColor = Color.White, }; Color team2Color = new Color(150, 110, 0, 255); pvpTeamChoiceTeam2 = new GUITextBlock(new RectTransform(new Vector2(0.3f, 1.0f), TeamPreferenceListBox.Content.RectTransform), TextManager.Get("teampreference.team2"), textAlignment: Alignment.Center, style: null) { UserData = CharacterTeamType.Team2, CanBeFocused = true, Padding = Vector4.One * 10.0f * GUI.Scale, Color = Color.Lerp(team2Color, Color.Black, 0.7f) * 0.7f, HoverColor = team2Color * 0.95f, SelectedColor = team2Color * 0.8f, OutlineColor = team2Color, TextColor = Color.White, HoverTextColor = Color.White, SelectedTextColor = Color.White, DisabledColor = team2Color * 0.25f, DisabledTextColor = Color.Gray, }; var prevTeamSelection = MultiplayerPreferences.Instance.TeamPreference; ResetPvpTeamSelection(); // Handle special case: middle button in Player Choice mode should pick a random team, if possible TeamPreferenceListBox.OnSelected += (component, obj) => { CharacterTeamType newTeamPreference = (CharacterTeamType)obj; if (newTeamPreference == CharacterTeamType.None && GameMain.Client?.ServerSettings?.PvpTeamSelectionMode == PvpTeamSelectionMode.PlayerChoice) { TeamPreferenceListBox.Select(Rand.Value() < 0.5 ? CharacterTeamType.Team1 : CharacterTeamType.Team2); var teamColor = (CharacterTeamType)TeamPreferenceListBox.SelectedData == CharacterTeamType.Team1 ? team1Color : team2Color; TeamPreferenceListBox.SelectedComponent.Flash(teamColor, useRectangleFlash: true, flashDuration: 1.0f); return true; } return false; // Allow the next delegate to handle other cases }; // Handle everything else TeamPreferenceListBox.OnSelected += (component, obj) => { CharacterTeamType newTeamPreference = (CharacterTeamType)obj; if (newTeamPreference == CharacterTeamType.None && GameMain.Client?.ServerSettings?.PvpTeamSelectionMode == PvpTeamSelectionMode.PlayerChoice) { return false; } // Already handled by delegate above var oldPreference = MultiplayerPreferences.Instance.TeamPreference; MultiplayerPreferences.Instance.TeamPreference = newTeamPreference; UpdateSelectedSub(newTeamPreference); if (newTeamPreference != oldPreference) { GameMain.Client?.ForceNameJobTeamUpdate(); GameSettings.SaveCurrentConfig(); } RefreshPvpTeamSelectionButtons(); UpdateDisembarkPointListFromServerSettings(); //need to update job preferences and close the selection frame //because the team selection might affect the uniform sprite and the loadouts UpdateJobPreferences(GameMain.Client?.CharacterInfo ?? Character.Controlled?.Info); JobSelectionFrame = null; RefreshChatrow(); // to enable/disable team chat according to current selection return true; }; if (prevTeamSelection != CharacterTeamType.None) { TeamPreferenceListBox.Select(prevTeamSelection); } } } public void UpdateSelectedSub(CharacterTeamType preference) { bool votingEnabled = GameMain.NetworkMember.ServerSettings.SubSelectionMode == SelectionMode.Vote; SubList.OnSelected -= VotableClicked; switch (preference) { case CharacterTeamType.Team1 or CharacterTeamType.None when SelectedSub is { } selectedSub: TrySelectSub(selectedSub.Name, selectedSub.MD5Hash.StringRepresentation, SelectedSubType.Sub, SubList, showPreview: false); if (!votingEnabled) { SubList.Select(selectedSub, autoScroll: GUIListBox.AutoScroll.Disabled); } break; case CharacterTeamType.Team2 when SelectedEnemySub is { } selectedEnemySub: TrySelectSub(selectedEnemySub.Name, selectedEnemySub.MD5Hash.StringRepresentation, SelectedSubType.EnemySub, SubList, showPreview: false); if (!votingEnabled) { SubList.Select(selectedEnemySub, autoScroll: GUIListBox.AutoScroll.Disabled); } break; } SubList.OnSelected += VotableClicked; } public void TryDiscardCampaignCharacter(Action onYes) { var confirmation = new GUIMessageBox(TextManager.Get("NewCampaignCharacterHeader"), TextManager.Get("NewCampaignCharacterText"), new[] { TextManager.Get("Yes"), TextManager.Get("No") }); confirmation.Buttons[0].OnClicked += confirmation.Close; confirmation.Buttons[0].OnClicked += (btn2, userdata2) => { CampaignCharacterDiscarded = true; campaignCharacterInfo = null; onYes(); return true; }; confirmation.Buttons[1].OnClicked += confirmation.Close; } private void CreateChangesPendingText() { if (!createPendingChangesText || changesPendingText != null || playerInfoContent == null) { return; } //remove the previous one changesPendingText?.Parent?.RemoveChild(changesPendingText); changesPendingText = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.065f), playerInfoContent.RectTransform, Anchor.BottomCenter, Pivot.TopCenter) { RelativeOffset = new Vector2(0f, -0.03f) }, style: "OuterGlow") { Color = Color.Black, IgnoreLayoutGroups = true }; var text = new GUITextBlock(new RectTransform(Vector2.One, changesPendingText.RectTransform, Anchor.Center), TextManager.Get("tabmenu.characterchangespending"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, style: null); changesPendingText.RectTransform.MinSize = new Point((int)(text.TextSize.X * 1.2f), (int)(text.TextSize.Y * 2.0f)); } public static void CreateChangesPendingFrame(GUIComponent parent) { parent.ClearChildren(); GUIFrame changesPendingFrame = new GUIFrame(new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center), style: "OuterGlow") { Color = Color.Black }; new GUITextBlock(new RectTransform(Vector2.One, changesPendingFrame.RectTransform, Anchor.Center), TextManager.Get("tabmenu.characterchangespending"), textColor: GUIStyle.Orange, textAlignment: Alignment.Center, style: null) { AutoScaleHorizontal = true }; } private static void CreateJobVariantTooltip(JobPrefab jobPrefab, CharacterTeamType team, int variant, bool isPvPMode, GUIComponent parentSlot) { jobVariantTooltip = new GUIFrame(new RectTransform(new Point((int)(400 * GUI.Scale), (int)(180 * GUI.Scale)), GUI.Canvas, pivot: Pivot.BottomRight), style: "GUIToolTip") { UserData = new JobVariant(jobPrefab, variant) }; jobVariantTooltip.RectTransform.AbsoluteOffset = new Point(parentSlot.Rect.Right, parentSlot.Rect.Y); if (jobVariantTooltip.Rect.X < 0) { jobVariantTooltip.RectTransform.SetPosition(anchor: Anchor.TopLeft, pivot: Pivot.BottomLeft); jobVariantTooltip.RectTransform.AbsoluteOffset = new Point(parentSlot.Rect.X, parentSlot.Rect.Y); } var content = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), jobVariantTooltip.RectTransform, Anchor.Center)) { Stretch = true, AbsoluteSpacing = (int)(15 * GUI.Scale) }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform), TextManager.GetWithVariable("startingequipmentname", "[number]", (variant + 1).ToString()), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center); var itemIdentifiers = jobPrefab.GetJobItems(variant, it => it.ShowPreview) .Select(it => it.GetItemIdentifier(team, isPvPMode)) .Distinct(); int itemsPerRow = 5; int rows = (int)Math.Max(Math.Ceiling(itemIdentifiers.Count() / (float)itemsPerRow), 1); new GUICustomComponent(new RectTransform(new Vector2(1.0f, 0.4f * rows), content.RectTransform, Anchor.BottomCenter), onDraw: (sb, component) => { DrawJobVariantItems(sb, component, new JobVariant(jobPrefab, variant), team, isPvPMode, itemsPerRow); }); jobVariantTooltip.RectTransform.MinSize = new Point(0, content.RectTransform.Children.Sum(c => c.Rect.Height + content.AbsoluteSpacing)); } private void SetTraitorDangerIndicators(int dangerLevel) { int i = 0; foreach (var child in traitorDangerGroup.Children) { child.Enabled = i < dangerLevel && GameMain.Client?.ServerSettings is { TraitorProbability: > 0 }; i++; } } public bool ToggleSpectate(GUITickBox tickBox) { SetSpectate(tickBox.Selected); return false; } public void SetSpectate(bool spectate) { if (GameMain.Client == null) { return; } spectateBox.Selected = spectate; if (spectate) { GameMain.Client.CharacterInfo?.Remove(); GameMain.Client.CharacterInfo = null; // TODO: The following lines are ancient, unexplained, and they cause a client spectating because of permadeath // to get kicked from the server at round transition because the server expects to be in control of // removing Characters and the client to still have one. Commenting these lines out for now, but // if no side-effects occur, they can just be deleted. //GameMain.Client.Character?.Remove(); //GameMain.Client.Character = null; playerInfoContent.ClearChildren(); new GUITextBlock(new RectTransform(Vector2.One, playerInfoContent.RectTransform, Anchor.Center), TextManager.Get("PlayingAsSpectator"), textAlignment: Alignment.Center); if (SelectedMode == GameModePreset.PvP) { // In PvP mode, becoming a spectator should reset any existing team selection ResetPvpTeamSelection(); } } else { UpdatePlayerFrame(campaignCharacterInfo, allowEditing: campaignCharacterInfo == null); } } public void RefreshPvpTeamSelectionButtons() { if (pvpTeamChoiceMiddleButton == null || pvpTeamChoiceTeam1 == null || pvpTeamChoiceTeam2 == null) { return; } ServerSettings serverSettings = GameMain.Client.ServerSettings; CharacterTeamType currentTeam = MultiplayerPreferences.Instance.TeamPreference; bool pvpPlayerChoiceMode = serverSettings.PvpTeamSelectionMode == PvpTeamSelectionMode.PlayerChoice; pvpTeamChoiceMiddleButton.Text = TextManager.Get(pvpPlayerChoiceMode ? "PvP.PickRandom" : "teampreference.nopreference"); if (pvpPlayerChoiceMode && serverSettings.PvpAutoBalanceThreshold > 0) { pvpTeamChoiceTeam1.Enabled = currentTeam == CharacterTeamType.Team1 || CanJoinTeam1(); pvpTeamChoiceTeam2.Enabled = currentTeam == CharacterTeamType.Team2 || CanJoinTeam2(); pvpTeamChoiceTeam1.ToolTip = !pvpTeamChoiceTeam1.Enabled ? TextManager.Get("PvP.TeamDisabledBecauseBalance") : null; pvpTeamChoiceTeam2.ToolTip = !pvpTeamChoiceTeam2.Enabled ? TextManager.Get("PvP.TeamDisabledBecauseBalance") : null; pvpTeamChoiceMiddleButton.Enabled = CanJoinTeam1() && CanJoinTeam2(); } else { pvpTeamChoiceTeam1.Enabled = true; pvpTeamChoiceTeam2.Enabled = true; pvpTeamChoiceTeam1.ToolTip = null; pvpTeamChoiceTeam2.ToolTip = null; pvpTeamChoiceMiddleButton.Enabled = true; } bool CanJoinTeam1() { int newTeam1Count = Team1Count + (currentTeam == CharacterTeamType.Team1 ? 0 : 1); int newTeam2Count = Team2Count - (currentTeam == CharacterTeamType.Team2 ? 1 : 0); return newTeam1Count - newTeam2Count <= serverSettings.PvpAutoBalanceThreshold; } bool CanJoinTeam2() { int newTeam2Count = Team2Count + (currentTeam == CharacterTeamType.Team2 ? 0 : 1); int newTeam1Count = Team1Count - (currentTeam == CharacterTeamType.Team1 ? 1 : 0); return newTeam2Count - newTeam1Count <= serverSettings.PvpAutoBalanceThreshold; } } public void ResetPvpTeamSelection() { TeamPreferenceListBox?.Deselect(); MultiplayerPreferences.Instance.TeamPreference = CharacterTeamType.None; RefreshPvpTeamSelectionButtons(); RefreshChatrow(); GameMain.Client.ForceNameJobTeamUpdate(); } public void SetAllowSpectating(bool allowSpectating) { // Server owner is allowed to spectate regardless of the server settings if (GameMain.Client != null && GameMain.Client.IsServerOwner) { return; } // A client whose character has faced permadeath and hasn't chosen a new // character yet has no choice but to spectate if (campaignCharacterInfo != null && campaignCharacterInfo.PermanentlyDead) { return; } // Show the player config menu if spectating is not allowed if (spectateBox.Selected && !allowSpectating) { spectateBox.Selected = false; } // Hide spectate tickbox if spectating is not allowed spectateBox.Visible = allowSpectating; } public void SetAllowAFK(bool allowAFK) { if (afkBox.Visible != allowAFK) { //reset selection when the AFK option becomes available or unavailable afkBox.Selected = false; afkBox.Visible = allowAFK; } } public void SetAutoRestart(bool enabled, float timer = 0.0f) { autoRestartBox.Selected = enabled; autoRestartTimer = timer; } public void SetMissionTypes(IEnumerable missionTypes) { MissionTypes = missionTypes; } private void RefreshOutpostDropdown() { Identifier randomOutpostIdentifier = "Random".ToIdentifier(); outpostDropdown.Parent.Visible = MissionTypeFrame.Visible; if (!outpostDropdown.Parent.Visible) { return; } outpostDropdownUpToDate = false; Identifier prevSelected = GameMain.NetworkMember?.ServerSettings.SelectedOutpostName ?? Identifier.Empty; outpostDropdown.ClearChildren(); outpostDropdown.AddItem(TextManager.Get("Random"), randomOutpostIdentifier); HashSet validOutpostTagsForMissions = new HashSet(); IEnumerable suitableMissionClasses = SelectedMode == GameModePreset.PvP ? MissionPrefab.PvPMissionClasses.Values : MissionPrefab.CoOpMissionClasses.Values; foreach (var missionType in MissionTypes) { foreach (var missionPrefab in MissionPrefab.Prefabs) { if (!suitableMissionClasses.Contains(missionPrefab.MissionClass)) { continue; } if (missionPrefab.Type != missionType || missionPrefab.SingleplayerOnly) { continue; } if (!missionPrefab.AllowOutpostSelectionFromTag.IsEmpty) { validOutpostTagsForMissions.Add(missionPrefab.AllowOutpostSelectionFromTag); } } } if (validOutpostTagsForMissions.Any()) { foreach (var submarineInfo in SubmarineInfo.SavedSubmarines.DistinctBy(s => s.Name)) { if (submarineInfo.Type == SubmarineType.Outpost && validOutpostTagsForMissions.Any(submarineInfo.OutpostTags.Contains)) { outpostDropdown.AddItem(submarineInfo.DisplayName, userData: submarineInfo.Name.ToIdentifier(), toolTip: submarineInfo.Description); } } if (!outpostDropdown.ListBox.Select(prevSelected)) { //could not select the previously selected outpost (not suitable for the selected missions) // -> choose random instead if (outpostDropdown.SelectedData is Identifier selectedIdentifier && selectedIdentifier != randomOutpostIdentifier) { outpostDropdown.Flash(GUIStyle.Red); } outpostDropdown.ListBox.Select(randomOutpostIdentifier); } GameMain.Client.ServerSettings.AssignGUIComponent(nameof(ServerSettings.SelectedOutpostName), outpostDropdown); } else { outpostDropdown.Parent.Visible = false; //remove assignment, we shouldn't try selecting the outpost when there's none to select GameMain.Client.ServerSettings.AssignGUIComponent(nameof(ServerSettings.SelectedOutpostName), null); } outpostDropdownUpToDate = true; } public void UpdateSubList(GUIComponent subList, IEnumerable submarines) { if (subList == null) { return; } subList.ClearChildren(); foreach (SubmarineInfo sub in submarines) { AddSubmarine(subList, sub); } } private void AddSubmarine(GUIComponent subList, SubmarineInfo sub) { if (subList is GUIListBox listBox) { subList = listBox.Content; } else if (subList is GUIDropDown dropDown) { subList = dropDown.ListBox.Content; } var frame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.1f), subList.RectTransform) { //enough space for 2 lines (price and class) + some padding MinSize = new Point(0, (int)(GUIStyle.SmallFont.LineHeight * 2.3f)) }, style: "ListBoxElement") { ToolTip = sub.Description, UserData = sub }; var frameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.75f, 1f), frame.RectTransform), isHorizontal: true); var subTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.7f, 1.0f), frameLayout.RectTransform, Anchor.CenterLeft), ToolBox.LimitString(sub.DisplayName.Value, GUIStyle.Font, subList.Rect.Width - 65), textAlignment: Alignment.CenterLeft) { ToolTip = sub.Description, UserData = "nametext", CanBeFocused = true }; var pvpContainer = new GUIFrame(new RectTransform(new Vector2(0.3f, 1f), frameLayout.RectTransform, Anchor.CenterRight), style: null) { CanBeFocused = false }; var coalitionIcon = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), pvpContainer.RectTransform, Anchor.CenterLeft), style: "CoalitionIcon") { Visible = false, UserData = CoalitionIconUserData, CanBeFocused = false }; var separatistsIcon = new GUIFrame(new RectTransform(new Vector2(0.5f, 1f), pvpContainer.RectTransform, Anchor.CenterRight), style: "SeparatistIcon") { Visible = false, UserData = SeparatistsIconUserData, CanBeFocused = false }; var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name && s.MD5Hash?.StringRepresentation == sub.MD5Hash?.StringRepresentation) ?? SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == sub.Name); if (matchingSub == null) { subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); frame.ToolTip = TextManager.Get("SubNotFound"); } else if (matchingSub?.MD5Hash == null || matchingSub.MD5Hash?.StringRepresentation != sub.MD5Hash?.StringRepresentation) { subTextBlock.TextColor = new Color(subTextBlock.TextColor, 0.5f); frame.ToolTip = TextManager.Get("SubDoesntMatch"); } else { if (subList == ShuttleList || subList == ShuttleList.ListBox || subList == ShuttleList.ListBox.Content) { subTextBlock.TextColor = new Color(subTextBlock.TextColor, sub.HasTag(SubmarineTag.Shuttle) ? 1.0f : 0.6f); } } if (!sub.RequiredContentPackagesInstalled) { subTextBlock.TextColor = Color.Lerp(subTextBlock.TextColor, Color.DarkRed, 0.5f); frame.ToolTip = TextManager.Get("ContentPackageMismatch") + "\n\n" + frame.ToolTip.SanitizedString; } CreateSubmarineClassText( frame, sub, subTextBlock, subList); } private void CreateSubmarineClassText( GUIComponent parent, SubmarineInfo sub, GUITextBlock subTextBlock, GUIComponent subList) { if (sub.HasTag(SubmarineTag.Shuttle)) { new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, TextManager.Get("Shuttle", "RespawnShuttle"), textAlignment: Alignment.CenterRight, font: GUIStyle.SmallFont) { TextColor = subTextBlock.TextColor * 0.8f, ToolTip = subTextBlock.ToolTip?.SanitizedString, CanBeFocused = false }; //make shuttles more dim in the sub list (selecting a shuttle as the main sub is allowed but not recommended) if (subList == SubList.Content) { subTextBlock.TextColor *= 0.8f; foreach (GUIComponent child in parent.Children) { child.Color *= 0.8f; } } } else { var infoContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1.0f), parent.RectTransform, Anchor.CenterRight) { AbsoluteOffset = new Point(GUI.IntScale(20), 0) }, isHorizontal: false); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), TextManager.GetWithVariable("currencyformat", "[credits]", string.Format(CultureInfo.InvariantCulture, "{0:N0}", sub.Price)), textAlignment: Alignment.BottomRight, font: GUIStyle.SmallFont) { Padding = Vector4.Zero, UserData = "pricetext", TextColor = subTextBlock.TextColor * 0.8f, CanBeFocused = false }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.5f), infoContainer.RectTransform), TextManager.Get($"submarineclass.{sub.SubmarineClass}"), textAlignment: Alignment.TopRight, font: GUIStyle.SmallFont) { Padding = Vector4.Zero, UserData = "classtext", TextColor = subTextBlock.TextColor * 0.8f, ToolTip = subTextBlock.ToolTip, CanBeFocused = false }; } } public bool VotableClicked(GUIComponent component, object userData) { if (GameMain.Client == null) { return false; } VoteType voteType; if (component.Parent == GameMain.NetLobbyScreen.SubList.Content) { if (SelectedMode == GameModePreset.PvP && MultiplayerPreferences.Instance.TeamPreference is not (CharacterTeamType.Team1 or CharacterTeamType.Team2)) { if (TeamPreferenceListBox == null) { //refresh player frame to ensure we create the team preference list box UpdatePlayerFrame(characterInfo: GameMain.Client?.CharacterInfo); } // we are in PvP but don't have a team selected, so we can't select a sub // and also highlight the team selection list foreach (GUIComponent child in TeamPreferenceListBox.Content.Children) { if (child.UserData is CharacterTeamType.None) { continue; } child.Flash(GUIStyle.Red, useRectangleFlash: true, flashDuration: 1f); } return false; } if (!GameMain.Client.ServerSettings.AllowSubVoting) { var selectedSub = (SubmarineInfo)component.UserData; var type = SelectedMode != GameModePreset.PvP ? SelectedSubType.Sub : MultiplayerPreferences.Instance.TeamPreference switch { CharacterTeamType.None or CharacterTeamType.Team1 => SelectedSubType.Sub, CharacterTeamType.Team2 => SelectedSubType.EnemySub, _ => throw new NotImplementedException() }; if (SelectedMode == GameModePreset.MultiPlayerCampaign && CampaignSetupUI != null) { if (selectedSub.Price > CampaignSettings.CurrentSettings.InitialMoney) { new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubtooexpensive")); } if (!selectedSub.IsCampaignCompatible) { new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("campaignsubincompatible")); } } if (!selectedSub.RequiredContentPackagesInstalled) { var msgBox = new GUIMessageBox(TextManager.Get("ContentPackageMismatch"), selectedSub.RequiredContentPackages.Any() ? TextManager.GetWithVariable("ContentPackageMismatchWarning", "[requiredcontentpackages]", string.Join(", ", selectedSub.RequiredContentPackages)) : TextManager.Get("ContentPackageMismatchWarningGeneric"), new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = msgBox.Close; msgBox.Buttons[0].OnClicked += (button, obj) => { GameMain.Client.RequestSelectSub(obj as SubmarineInfo, type); return true; }; msgBox.Buttons[1].OnClicked = msgBox.Close; return false; } else if (GameMain.Client.HasPermission(ClientPermissions.SelectSub)) { GameMain.Client.RequestSelectSub(selectedSub, type); return true; } return false; } if (component.UserData is SubmarineInfo sub) { CreateSubPreview(sub); } voteType = VoteType.Sub; } else if (component.Parent == GameMain.NetLobbyScreen.ModeList.Content) { if (!GameMain.Client.ServerSettings.AllowModeVoting) { if (GameMain.Client.HasPermission(ClientPermissions.SelectMode)) { Identifier presetName = ((GameModePreset)component.UserData).Identifier; //display a verification prompt when switching away from the campaign if (HighlightedModeIndex == SelectedModeIndex && (GameMain.NetLobbyScreen.ModeList.SelectedData as GameModePreset) == GameModePreset.MultiPlayerCampaign && presetName != GameModePreset.MultiPlayerCampaign.Identifier) { var verificationBox = new GUIMessageBox("", TextManager.Get("endcampaignverification"), new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") }); verificationBox.Buttons[0].OnClicked += (btn, userdata) => { GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); HighlightMode(SelectedModeIndex); verificationBox.Close(btn, userdata); return true; }; verificationBox.Buttons[1].OnClicked = verificationBox.Close; return false; } GameMain.Client.RequestSelectMode(component.Parent.GetChildIndex(component)); HighlightMode(SelectedModeIndex); if (presetName == "multiplayercampaign") { GUI.SetCursorWaiting(endCondition: () => { return CampaignFrame.Visible || CampaignSetupFrame.Visible; }); } return presetName != "multiplayercampaign"; } return false; } else if (!((GameModePreset)userData).Votable) { return false; } voteType = VoteType.Mode; } else { return false; } GameMain.Client.Vote(voteType, userData); return true; } public void AddPlayer(Client client) { GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), PlayerList.Content.RectTransform) { MinSize = new Point(0, (int)(30 * GUI.Scale)) }, client.Name, textAlignment: Alignment.CenterLeft, font: GUIStyle.SmallFont, style: null) { Padding = Vector4.One * 10.0f * GUI.Scale, Color = Color.White * 0.25f, HoverColor = Color.White * 0.5f, SelectedColor = Color.White * 0.85f, OutlineColor = Color.White * 0.5f, TextColor = Color.White, SelectedTextColor = Color.Black, UserData = client }; var soundIcon = new GUIImage(new RectTransform(Vector2.One * 0.8f, textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) }, sprite: GUIStyle.GetComponentStyle("GUISoundIcon").GetDefaultSprite(), scaleToFit: true) { UserData = new Pair("soundicon", 0.0f), CanBeFocused = false, Visible = true, OverrideState = GUIComponent.ComponentState.None, HoverColor = Color.White }; var soundIconDisabled = new GUIImage(new RectTransform(Vector2.One * 0.8f, textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(5, 0) }, "GUISoundIconDisabled") { UserData = "soundicondisabled", CanBeFocused = true, Visible = false, OverrideState = GUIComponent.ComponentState.None, HoverColor = Color.White }; var readyTick = new GUIFrame(new RectTransform(new Vector2(0.6f, 0.6f), textBlock.RectTransform, Anchor.CenterRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point(10 + soundIcon.Rect.Width, 0) }, style: "GUIReadyToStart") { Visible = false, CanBeFocused = false, ToolTip = TextManager.Get("ReadyToStartTickBox"), UserData = "clientready" }; var downloadingThrobber = new GUICustomComponent( new RectTransform(Vector2.One, textBlock.RectTransform, scaleBasis: ScaleBasis.BothHeight), onUpdate: null, onDraw: DrawDownloadThrobber(client, soundIcon, soundIconDisabled, readyTick)); } private Action DrawDownloadThrobber(Client client, params GUIComponent[] otherComponents) => (sb, c) => DrawDownloadThrobber(client, otherComponents, sb, c); //poor man's currying private static void DrawDownloadThrobber(Client client, GUIComponent[] otherComponents, SpriteBatch spriteBatch, GUICustomComponent component) { if (!client.IsDownloading) { component.ToolTip = ""; return; } component.HideElementsOutsideFrame = false; int drawRectX = otherComponents.Where(c => c.Visible) .Select(c => c.Rect) .Concat(new Rectangle(component.Parent.Rect.Right, component.Parent.Rect.Y, 0, component.Parent.Rect.Height).ToEnumerable()) .Min(r => r.X) - component.Parent.Rect.Height - 10; Rectangle drawRect = new Rectangle(drawRectX, component.Rect.Y, component.Parent.Rect.Height, component.Parent.Rect.Height); component.RectTransform.AbsoluteOffset = drawRect.Location - component.Parent.Rect.Location; component.RectTransform.NonScaledSize = drawRect.Size; var sheet = GUIStyle.GenericThrobber; sheet.Draw( spriteBatch, pos: drawRect.Location.ToVector2(), spriteIndex: (int)Math.Floor(Timing.TotalTime * 24.0f) % sheet.FrameCount, color: Color.White, origin: Vector2.Zero, rotate: 0.0f, scale: Vector2.One * component.Parent.Rect.Height / sheet.FrameSize.ToVector2()); if (component.ToolTip.IsNullOrEmpty()) { component.ToolTip = TextManager.Get("PlayerIsDownloadingFiles"); } } public void SetPlayerNameAndJobPreference(Client client) { var playerFrame = (GUITextBlock)PlayerList.Content.FindChild(client); if (playerFrame == null) { return; } playerFrame.Text = client.Name; playerFrame.ToolTip = ""; Color color = Color.White; if (SelectedMode == GameModePreset.PvP) { switch (client.PreferredTeam) { case CharacterTeamType.Team1: color = new Color(0, 110, 150, 255); playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("teampreference.team1")); break; case CharacterTeamType.Team2: color = new Color(150, 110, 0, 255); playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("teampreference.team2")); break; default: playerFrame.ToolTip = TextManager.GetWithVariable("teampreference", "[team]", TextManager.Get("none")); break; } } else { if (JobPrefab.Prefabs.ContainsKey(client.PreferredJob)) { color = JobPrefab.Prefabs[client.PreferredJob].UIColor; playerFrame.ToolTip = TextManager.GetWithVariable("jobpreference", "[job]", JobPrefab.Prefabs[client.PreferredJob].Name); } else { playerFrame.ToolTip = TextManager.GetWithVariable("jobpreference", "[job]", TextManager.Get("none")); } } playerFrame.Color = color * 0.4f; playerFrame.HoverColor = color * 0.6f; playerFrame.SelectedColor = color * 0.8f; playerFrame.OutlineColor = color * 0.5f; playerFrame.TextColor = color; } public void SetPlayerVoiceIconState(Client client, bool muted, bool mutedLocally) { var PlayerFrame = PlayerList.Content.FindChild(client); if (PlayerFrame == null) { return; } var soundIcon = PlayerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); var soundIconDisabled = PlayerFrame.FindChild("soundicondisabled"); Pair userdata = soundIcon.UserData as Pair; if (!soundIcon.Visible) { userdata.Second = 0.0f; } soundIcon.Visible = !muted && !mutedLocally; soundIconDisabled.Visible = muted || mutedLocally; soundIconDisabled.ToolTip = TextManager.Get(mutedLocally ? "MutedLocally" : "MutedGlobally"); } public void SetPlayerSpeaking(Client client) { var PlayerFrame = PlayerList.Content.FindChild(client); if (PlayerFrame == null) { return; } var soundIcon = PlayerFrame.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon"); Pair userdata = soundIcon.UserData as Pair; userdata.Second = Math.Max(userdata.Second, 0.18f); soundIcon.Visible = true; } public void RemovePlayer(Client client) { GUIComponent child = PlayerList.Content.GetChildByUserData(client); if (child != null) { PlayerList.RemoveChild(child); } } public static Client ExtractClientFromClickableArea(GUITextBlock.ClickableArea area) => area.Data.ExtractClient(); public void SelectPlayer(GUITextBlock component, GUITextBlock.ClickableArea area) { var client = ExtractClientFromClickableArea(area); if (client is null) { return; } GameMain.NetLobbyScreen.SelectPlayer(client); } public void ShowPlayerContextMenu(GUITextBlock component, GUITextBlock.ClickableArea area) { var client = ExtractClientFromClickableArea(area); if (client is null) { return; } CreateModerationContextMenu(client); } #region Context Menu public static void CreateModerationContextMenu(Client client) { if (GUIContextMenu.CurrentContextMenu != null) { return; } if (GameMain.IsSingleplayer || client == null) { return; } if (!(GameMain.Client is { PreviouslyConnectedClients: var previouslyConnectedClients }) || !previouslyConnectedClients.Contains(client)) { return; } bool hasAccountId = client.AccountId.IsSome(); bool canKick = GameMain.Client.HasPermission(ClientPermissions.Kick); bool canBan = GameMain.Client.HasPermission(ClientPermissions.Ban) && client.AllowKicking; bool canManagePermissions = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); // Disable options if we are targeting ourselves if (client.SessionId == GameMain.Client.SessionId) { canKick = canBan = canManagePermissions = false; } List options = new List(); if (client.AccountId.TryUnwrap(out var accountId)) { options.Add(new ContextMenuOption(accountId.ViewProfileLabel(), isEnabled: hasAccountId, onSelected: () => { accountId.OpenProfile(); })); } options.Add(new ContextMenuOption("ModerationMenu.ManagePlayer", isEnabled: true, onSelected: () => { GameMain.NetLobbyScreen?.SelectPlayer(client); })); // Creates sub context menu options for all the ranks List rankOptions = new List(); foreach (PermissionPreset rank in PermissionPreset.List) { rankOptions.Add(new ContextMenuOption(rank.DisplayName, isEnabled: true, onSelected: () => { LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.DisplayName)); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); msgBox.Buttons[0].OnClicked = delegate { client.SetPermissions(rank.Permissions, rank.PermittedCommands); GameMain.Client.UpdateClientPermissions(client); msgBox.Close(); return true; }; msgBox.Buttons[1].OnClicked = delegate { msgBox.Close(); return true; }; }) { Tooltip = rank.Description }); } options.Add(new ContextMenuOption("Rank", isEnabled: canManagePermissions, options: rankOptions.ToArray())); Color clientColor = client.Character?.Info?.Job.Prefab.UIColor ?? Color.White; if (GameMain.Client.ConnectedClients.Contains(client)) { options.Add(new ContextMenuOption(client.MutedLocally ? "Unmute" : "Mute", isEnabled: client.SessionId != GameMain.Client.SessionId, onSelected: delegate { client.MutedLocally = !client.MutedLocally; })); bool kickEnabled = client.SessionId != GameMain.Client.SessionId && client.AllowKicking; // if the user can kick create a kick option else create the votekick option ContextMenuOption kickOption; if (canKick) { kickOption = new ContextMenuOption("Kick", isEnabled: kickEnabled, onSelected: delegate { GameMain.Client?.CreateKickReasonPrompt(client.Name, false); }); } else { kickOption = new ContextMenuOption("VoteToKick", isEnabled: kickEnabled, onSelected: delegate { GameMain.Client?.VoteForKick(client); }); } options.Add(kickOption); } if (GameMain.Client?.ServerSettings?.BanList?.BannedPlayers?.Any(bp => bp.MatchesClient(client)) ?? false) { options.Add(new ContextMenuOption("clientpermission.unban", isEnabled: canBan, onSelected: delegate { GameMain.Client?.UnbanPlayer(client.Name); })); } else { options.Add(new ContextMenuOption("Ban", isEnabled: canBan, onSelected: delegate { GameMain.Client?.CreateKickReasonPrompt(client.Name, true); })); } GUIContextMenu.CreateContextMenu(null, client.Name, headerColor: clientColor, options.ToArray()); } #endregion public bool SelectPlayer(Client selectedClient) { bool myClient = selectedClient.SessionId == GameMain.Client.SessionId; bool hasManagePermissions = GameMain.Client.HasPermission(ClientPermissions.ManagePermissions); PlayerFrame = new GUIButton(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) { OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) { ClosePlayerFrame(btn, userdata); } return true; } }; new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PlayerFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); Vector2 frameSize = hasManagePermissions ? new Vector2(.28f, .5f) : new Vector2(.28f, .15f); var playerFrameInner = new GUIFrame(new RectTransform(frameSize, PlayerFrame.RectTransform, Anchor.Center) { MinSize = new Point(550, 0) }); var paddedPlayerFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.88f), playerFrameInner.RectTransform, Anchor.Center)) { Stretch = true, RelativeSpacing = 0.03f }; var headerContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), paddedPlayerFrame.RectTransform), isHorizontal: false); var headerTextContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), headerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; var headerVolumeContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), headerContainer.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; var nameText = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), headerTextContainer.RectTransform), text: selectedClient.Name, font: GUIStyle.LargeFont); nameText.Text = ToolBox.LimitString(nameText.Text, nameText.Font, (int)(nameText.Rect.Width * 0.95f)); if (hasManagePermissions && !selectedClient.IsOwner) { PlayerFrame.UserData = selectedClient; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank"), font: GUIStyle.SubHeadingFont); var rankDropDown = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), TextManager.Get("Rank")) { UserData = selectedClient, Enabled = !myClient }; foreach (PermissionPreset permissionPreset in PermissionPreset.List) { rankDropDown.AddItem(permissionPreset.DisplayName, permissionPreset, permissionPreset.Description); } rankDropDown.AddItem(TextManager.Get("CustomRank"), null); PermissionPreset currentPreset = PermissionPreset.List.Find(p => p.Permissions == selectedClient.Permissions && p.PermittedCommands.Count == selectedClient.PermittedConsoleCommands.Count && !p.PermittedCommands.Except(selectedClient.PermittedConsoleCommands).Any()); rankDropDown.SelectItem(currentPreset); rankDropDown.OnSelected += (c, userdata) => { PermissionPreset selectedPreset = (PermissionPreset)userdata; if (selectedPreset != null) { var client = PlayerFrame.UserData as Client; client.SetPermissions(selectedPreset.Permissions, selectedPreset.PermittedCommands); GameMain.Client.UpdateClientPermissions(client); PlayerFrame = null; SelectPlayer(client); } return true; }; var permissionLabels = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), paddedPlayerFrame.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; var permissionLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("Permissions"), font: GUIStyle.SubHeadingFont); var consoleCommandLabel = new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), permissionLabels.RectTransform), TextManager.Get("PermittedConsoleCommands"), wrap: true, font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(permissionLabel, consoleCommandLabel); var permissionContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), paddedPlayerFrame.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; var listBoxContainerLeft = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), permissionContainer.RectTransform)) { Stretch = true, RelativeSpacing = 0.05f }; new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerLeft.RectTransform), TextManager.Get("all", "clientpermission.all")) { Enabled = !myClient, OnSelected = (tickbox) => { //reset rank to custom rankDropDown.SelectItem(null); if (PlayerFrame.UserData is not Client client) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { var permissionTickBox = child as GUITickBox; permissionTickBox.Enabled = false; permissionTickBox.Selected = tickbox.Selected; permissionTickBox.Enabled = true; } GameMain.Client.UpdateClientPermissions(client); return true; } }; var permissionsBox = new GUIListBox(new RectTransform(Vector2.One, listBoxContainerLeft.RectTransform)) { UserData = selectedClient }; foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { if (permission == ClientPermissions.None || permission == ClientPermissions.All) { continue; } var permissionTick = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), permissionsBox.Content.RectTransform), TextManager.Get("ClientPermission." + permission), font: GUIStyle.SmallFont) { UserData = permission, Selected = selectedClient.HasPermission(permission), Enabled = !myClient, OnSelected = (tickBox) => { //reset rank to custom rankDropDown.SelectItem(null); if (PlayerFrame.UserData is not Client client) { return false; } var thisPermission = (ClientPermissions)tickBox.UserData; if (tickBox.Selected) { client.GivePermission(thisPermission); } else { client.RemovePermission(thisPermission); } if (tickBox.Enabled) { GameMain.Client.UpdateClientPermissions(client); } return true; } }; permissionTick.ToolTip = permissionTick.TextBlock.ToolTip = TextManager.Get("ClientPermission." + permission + ".description"); } var listBoxContainerRight = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1.0f), permissionContainer.RectTransform)) { Stretch = true, RelativeSpacing = 0.05f }; new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), listBoxContainerRight.RectTransform), TextManager.Get("all", "clientpermission.all")) { Enabled = !myClient, OnSelected = (tickbox) => { //reset rank to custom rankDropDown.SelectItem(null); if (PlayerFrame.UserData is not Client client) { return false; } foreach (GUIComponent child in tickbox.Parent.GetChild().Content.Children) { var commandTickBox = child as GUITickBox; commandTickBox.Enabled = false; commandTickBox.Selected = tickbox.Selected; commandTickBox.Enabled = true; } GameMain.Client.UpdateClientPermissions(client); return true; } }; var commandList = new GUIListBox(new RectTransform(Vector2.One, listBoxContainerRight.RectTransform)) { UserData = selectedClient }; foreach (DebugConsole.Command command in DebugConsole.Commands) { var commandTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.15f), commandList.Content.RectTransform), command.Names[0].Value, font: GUIStyle.SmallFont) { Selected = selectedClient.PermittedConsoleCommands.Contains(command), Enabled = !myClient, ToolTip = command.Help, UserData = command }; commandTickBox.OnSelected += (GUITickBox tickBox) => { //reset rank to custom rankDropDown.SelectItem(null); DebugConsole.Command selectedCommand = tickBox.UserData as DebugConsole.Command; if (PlayerFrame.UserData is not Client client) { return false; } if (!tickBox.Selected) { client.PermittedConsoleCommands.Remove(selectedCommand); } else if (!client.PermittedConsoleCommands.Contains(selectedCommand)) { client.PermittedConsoleCommands.Add(selectedCommand); } if (tickBox.Enabled) { GameMain.Client.UpdateClientPermissions(client); } return true; }; } } var buttonAreaTop = myClient ? null : new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true); var buttonAreaLower = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.08f), paddedPlayerFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); if (!myClient) { if (GameMain.Client.HasPermission(ClientPermissions.Ban)) { GUIButton banButton; if (GameMain.Client?.ServerSettings?.BanList?.BannedPlayers?.Any(bp => bp.MatchesClient(selectedClient)) ?? false) { banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), TextManager.Get("clientpermission.unban")) { UserData = selectedClient, OnClicked = (bt, userdata) => { GameMain.Client?.UnbanPlayer(selectedClient.Name); return true; } }; } else { banButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaTop.RectTransform), TextManager.Get("Ban")) { UserData = selectedClient, OnClicked = (bt, userdata) => { BanPlayer(selectedClient); return true; } }; } banButton.OnClicked += ClosePlayerFrame; } if (GameMain.Client != null && GameMain.Client.ConnectedClients.Contains(selectedClient)) { if (GameMain.Client.ServerSettings.AllowVoteKick && selectedClient != null && selectedClient.AllowKicking) { var kickVoteButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), TextManager.Get("VoteToKick")) { OnClicked = (btn, userdata) => { GameMain.Client.VoteForKick(selectedClient); btn.Enabled = false; return true; }, UserData = selectedClient }; } if (GameMain.Client.HasPermission(ClientPermissions.Kick) && selectedClient != null && selectedClient.AllowKicking) { var kickButton = new GUIButton(new RectTransform(new Vector2(0.34f, 1.0f), buttonAreaLower.RectTransform), TextManager.Get("Kick")) { UserData = selectedClient, OnClicked = (bt, userdata) => { KickPlayer(selectedClient); return true; } }; kickButton.OnClicked += ClosePlayerFrame; } var volumeLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.5f, 1f), headerVolumeContainer.RectTransform), isHorizontal: false); var volumeTextLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); new GUITextBlock(new RectTransform(new Vector2(0.6f, 1f), volumeTextLayout.RectTransform), TextManager.Get("VoiceChatVolume")); var volumePercentageText = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), volumeTextLayout.RectTransform), ToolBox.GetFormattedPercentage(selectedClient.VoiceVolume), textAlignment: Alignment.Right); new GUIScrollBar(new RectTransform(new Vector2(1f, 0.5f), volumeLayout.RectTransform), barSize: 0.1f, style: "GUISlider") { Range = new Vector2(0f, 1f), BarScroll = selectedClient.VoiceVolume / Client.MaxVoiceChatBoost, OnMoved = (_, barScroll) => { float newVolume = barScroll * Client.MaxVoiceChatBoost; selectedClient.VoiceVolume = newVolume; volumePercentageText.Text = ToolBox.GetFormattedPercentage(newVolume); return true; } }; new GUITickBox(new RectTransform(new Vector2(0.175f, 1.0f), headerVolumeContainer.RectTransform, Anchor.TopRight), TextManager.Get("Mute")) { Selected = selectedClient.MutedLocally, OnSelected = (tickBox) => { selectedClient.MutedLocally = tickBox.Selected; return true; } }; } if (buttonAreaTop.CountChildren > 0) { GUITextBlock.AutoScaleAndNormalize(buttonAreaTop.Children.Select(c => ((GUIButton)c).TextBlock).Concat(buttonAreaLower.Children.Select(c => ((GUIButton)c).TextBlock))); } } if (selectedClient.AccountId.TryUnwrap(out var accountId)) { var viewSteamProfileButton = new GUIButton(new RectTransform(new Vector2(0.3f, 1.0f), headerTextContainer.RectTransform, Anchor.TopCenter) { MaxSize = new Point(int.MaxValue, (int)(40 * GUI.Scale)) }, accountId.ViewProfileLabel()) { UserData = selectedClient }; viewSteamProfileButton.TextBlock.AutoScaleHorizontal = true; viewSteamProfileButton.OnClicked = (bt, userdata) => { accountId.OpenProfile(); return true; }; } var closeButton = new GUIButton(new RectTransform(new Vector2(0f, 1.0f), buttonAreaLower.RectTransform, Anchor.CenterRight), TextManager.Get("Close")) { IgnoreLayoutGroups = true, OnClicked = ClosePlayerFrame }; float xSize = 1f / buttonAreaLower.CountChildren; for (int i = 0; i < buttonAreaLower.CountChildren; i++) { buttonAreaLower.GetChild(i).RectTransform.RelativeSize = new Vector2(xSize, 1f); } buttonAreaLower.RectTransform.NonScaledSize = new Point(buttonAreaLower.Rect.Width, buttonAreaLower.RectTransform.Children.Max(c => c.NonScaledSize.Y)); if (buttonAreaTop != null) { if (buttonAreaTop.CountChildren == 0) { paddedPlayerFrame.RemoveChild(buttonAreaTop); } else { for (int i = 0; i < buttonAreaTop.CountChildren; i++) { buttonAreaTop.GetChild(i).RectTransform.RelativeSize = new Vector2(1f / 3f, 1f); } buttonAreaTop.RectTransform.NonScaledSize = buttonAreaLower.RectTransform.NonScaledSize = new Point(buttonAreaLower.Rect.Width, Math.Max(buttonAreaLower.RectTransform.NonScaledSize.Y, buttonAreaTop.RectTransform.Children.Max(c => c.NonScaledSize.Y))); } } return false; } private bool ClosePlayerFrame(GUIButton button, object userData) { PlayerFrame = null; PlayerList.Deselect(); return true; } public static void KickPlayer(Client client) { if (GameMain.NetworkMember == null || client == null) { return; } GameMain.Client.CreateKickReasonPrompt(client.Name, false); } public static void BanPlayer(Client client) { if (GameMain.NetworkMember == null || client == null) { return; } GameMain.Client.CreateKickReasonPrompt(client.Name, ban: true); } public override void AddToGUIUpdateList() { base.AddToGUIUpdateList(); //CampaignSetupUI?.AddToGUIUpdateList(); JobInfoFrame?.AddToGUIUpdateList(); CharacterAppearanceCustomizationMenu?.AddToGUIUpdateList(); JobSelectionFrame?.AddToGUIUpdateList(order: 1); } public override void Update(double deltaTime) { if (GameMain.Client == null) { return; } UpdateMicIcon((float)deltaTime); foreach (GUIComponent child in PlayerList.Content.Children) { if (child.UserData is Client client) { if (child.FindChild(c => c.UserData is Pair pair && pair.First == "soundicon") is GUIImage soundIcon) { double voipAmplitude = 0.0f; if (client.SessionId != GameMain.Client.SessionId) { voipAmplitude = client.VoipSound?.CurrentAmplitude ?? 0.0f; } else { var voip = VoipCapture.Instance; if (voip == null) { voipAmplitude = 0; } else if (voip.LastEnqueueAudio > DateTime.Now - new TimeSpan(0, 0, 0, 0, milliseconds: 100)) { voipAmplitude = voip.LastAmplitude; } } VoipClient.UpdateVoiceIndicator(soundIcon, (float)voipAmplitude, (float)deltaTime); } } } autoRestartText.Visible = autoRestartTimer > 0.0f && autoRestartBox.Selected; if (!MathUtils.NearlyEqual(autoRestartTimer, 0.0f) && autoRestartBox.Selected) { autoRestartTimer = Math.Max(autoRestartTimer - (float)deltaTime, 0.0f); if (autoRestartTimer > 0.0f) { autoRestartText.Text = TextManager.Get("RestartingIn") + " " + ToolBox.SecondsToReadableTime(Math.Max(autoRestartTimer, 0)); } } CharacterAppearanceCustomizationMenu?.Update(); if (JobSelectionFrame != null && PlayerInput.PrimaryMouseButtonDown() && !GUI.IsMouseOn(JobSelectionFrame)) { JobList.Deselect(); JobSelectionFrame.Visible = false; } UpdateJobVariantSelectionIfNeeded(); } public static void UpdateJobVariantSelectionIfNeeded() { if (GUI.MouseOn?.UserData is JobVariant jobPrefab && GUI.MouseOn.Style?.Name == "JobVariantButton" && GUI.MouseOn.Parent != null) { bool isMultiplayer = GameMain.NetLobbyScreen != null && GameMain.NetworkMember != null; var teamPreference = isMultiplayer ? GameMain.NetLobbyScreen.TeamPreference : CharacterTeamType.Team1; var isPvPMode = isMultiplayer ? GameMain.NetLobbyScreen.SelectedMode == GameModePreset.PvP : false; if (jobVariantTooltip?.UserData is not JobVariant prevVisibleVariant || prevVisibleVariant.Prefab != jobPrefab.Prefab || prevVisibleVariant.Variant != jobPrefab.Variant) { CreateJobVariantTooltip(jobPrefab.Prefab, teamPreference, jobPrefab.Variant, isPvPMode, GUI.MouseOn.Parent); } } if (jobVariantTooltip != null) { jobVariantTooltip?.AddToGUIUpdateList(order: 1); Rectangle mouseRect = jobVariantTooltip.MouseRect; mouseRect.Inflate(60 * GUI.Scale, 60 * GUI.Scale); if (!mouseRect.Contains(PlayerInput.MousePosition)) { jobVariantTooltip = null; } } } private void UpdateMicIcon(float deltaTime) { micCheckTimer -= deltaTime; if (micCheckTimer > 0.0f) { return; } Identifier newMicIconStyle = "GUIMicrophoneEnabled".ToIdentifier(); if (GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { newMicIconStyle = "GUIMicrophoneDisabled".ToIdentifier(); } else { var voipCaptureDeviceNames = VoipCapture.GetCaptureDeviceNames(); if (voipCaptureDeviceNames.Count == 0) { newMicIconStyle = "GUIMicrophoneUnavailable".ToIdentifier(); } } if (newMicIconStyle != micIconStyle) { micIconStyle = newMicIconStyle; GUIStyle.Apply(micIcon, newMicIconStyle); } micCheckTimer = MicCheckInterval; } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { if (backgroundSprite?.Texture == null) { return; } graphics.Clear(Color.Black); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); GUI.DrawBackgroundSprite(spriteBatch, backgroundSprite, Color.White); GUI.Draw(Cam, spriteBatch); spriteBatch.End(); } private PlayStyle? prevPlayStyle = null; private bool? prevIsPublic = null; private void DrawServerBanner(SpriteBatch spriteBatch, GUICustomComponent component) { if (GameMain.NetworkMember?.ServerSettings == null) { return; } PlayStyle playStyle = GameMain.NetworkMember.ServerSettings.PlayStyle; Sprite sprite = GUIStyle .GetComponentStyle($"PlayStyleBanner.{playStyle}")? .GetSprite(GUIComponent.ComponentState.None); if (sprite is null) { return; } GUI.DrawBackgroundSprite(spriteBatch, sprite, Color.White, drawArea: component.Rect); if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value) { playstyleText.Text = TextManager.Get($"ServerTag.{playStyle}"); playstyleText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White; playstyleText.RectTransform.NonScaledSize = (playstyleText.Font.MeasureString(playstyleText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); prevPlayStyle = playStyle; (playstyleText.Parent as GUILayoutGroup)?.Recalculate(); playstyleText.ToolTip = TextManager.Get($"ServerTagDescription.{playStyle}"); } if (!prevIsPublic.HasValue || GameMain.NetworkMember.ServerSettings.IsPublic != prevIsPublic.Value) { publicOrPrivateText.Text = GameMain.NetworkMember.ServerSettings.IsPublic ? TextManager.Get("PublicLobbyTag") : TextManager.Get("PrivateLobbyTag"); publicOrPrivateText.RectTransform.NonScaledSize = (publicOrPrivateText.Font.MeasureString(publicOrPrivateText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); (publicOrPrivateText.Parent as GUILayoutGroup)?.Recalculate(); prevIsPublic = GameMain.NetworkMember.ServerSettings.IsPublic; } } private static void DrawJobVariantItems(SpriteBatch spriteBatch, GUICustomComponent component, JobVariant jobVariant, CharacterTeamType team, bool isPvPMode, int itemsPerRow) { var allJobItems = jobVariant.Prefab.GetJobItems(jobVariant.Variant, it => it.ShowPreview); var itemIdentifiers = allJobItems .Select(it => it.GetItemIdentifier(team, isPvPMode)) .Distinct(); Point slotSize = new Point(component.Rect.Height); int spacing = (int)(5 * GUI.Scale); int slotCount = itemIdentifiers.Count(); int slotCountPerRow = Math.Min(slotCount, itemsPerRow); int rows = (int)Math.Max(Math.Ceiling(itemIdentifiers.Count() / (float)itemsPerRow), 1); float totalWidth = slotSize.X * slotCountPerRow + spacing * (slotCountPerRow - 1); float totalHeight = slotSize.Y * rows + spacing * (rows - 1); if (totalWidth > component.Rect.Width) { slotSize = new Point( Math.Min((int)Math.Floor((slotSize.X - spacing) * (component.Rect.Width / totalWidth)), (int)Math.Floor((slotSize.Y - spacing) * (component.Rect.Height / totalHeight)))); } int i = 0; Rectangle tooltipRect = Rectangle.Empty; LocalizedString tooltip = null; foreach (Identifier itemIdentifier in itemIdentifiers) { if (MapEntityPrefab.FindByIdentifier(identifier: itemIdentifier) is not ItemPrefab itemPrefab) { continue; } int row = (int)Math.Floor(i / (float)slotCountPerRow); int slotsPerThisRow = Math.Min((slotCount - row * slotCountPerRow), slotCountPerRow); Vector2 slotPos = new Vector2( component.Rect.Center.X + (slotSize.X + spacing) * (i % slotCountPerRow - slotsPerThisRow * 0.5f), component.Rect.Bottom - (rows * (slotSize.Y + spacing)) + (slotSize.Y + spacing) * row); Rectangle slotRect = new Rectangle(slotPos.ToPoint(), slotSize); Inventory.SlotSpriteSmall.Draw(spriteBatch, slotPos, scale: slotSize.X / (float)Inventory.SlotSpriteSmall.SourceRect.Width, color: slotRect.Contains(PlayerInput.MousePosition) ? Color.White : Color.White * 0.6f); Sprite icon = itemPrefab.InventoryIcon ?? itemPrefab.Sprite; float iconScale = Math.Min(Math.Min(slotSize.X / icon.size.X, slotSize.Y / icon.size.Y), 2.0f) * 0.9f; icon.Draw(spriteBatch, slotPos + slotSize.ToVector2() * 0.5f, scale: iconScale); int count = allJobItems.Where(it => it.GetItemIdentifier(team, isPvPMode) == itemIdentifier).Sum(it => it.Amount); if (count > 1) { string itemCountText = "x" + count; GUIStyle.Font.DrawString(spriteBatch, itemCountText, slotPos + slotSize.ToVector2() - GUIStyle.Font.MeasureString(itemCountText) - Vector2.UnitX * 5, Color.White); } if (slotRect.Contains(PlayerInput.MousePosition)) { tooltipRect = slotRect; tooltip = itemPrefab.Name + '\n' + itemPrefab.Description; } i++; } if (!tooltip.IsNullOrEmpty()) { GUIComponent.DrawToolTip(spriteBatch, tooltip, tooltipRect); } } public void NewChatMessage(ChatMessage message) { float prevSize = chatBox.BarSize; while (chatBox.Content.CountChildren > 60) { chatBox.RemoveChild(chatBox.Content.Children.First()); } LocalizedString displayedChatRow = ChatMessage.GetTimeStamp(); if (message.Type == ChatMessageType.Private) { displayedChatRow += TextManager.Get("PrivateMessageTag") + " "; } else if (message.Type == ChatMessageType.Team) { displayedChatRow += TextManager.Get("PvP.ChatMode.Team.ChatPrefixTag") + " "; } displayedChatRow += message.TextWithSender; GUITextBlock msg = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), chatBox.Content.RectTransform), text: RichString.Rich(displayedChatRow), textColor: message.Color, color: ((chatBox.CountChildren % 2) == 0) ? Color.Transparent : Color.Black * 0.1f, wrap: true, font: GUIStyle.SmallFont) { UserData = message, CanBeFocused = false }; msg.CalculateHeightFromText(); if (msg.RichTextData != null) { foreach (var data in msg.RichTextData) { msg.ClickableAreas.Add(new GUITextBlock.ClickableArea() { Data = data, OnClick = GameMain.NetLobbyScreen.SelectPlayer, OnSecondaryClick = GameMain.NetLobbyScreen.ShowPlayerContextMenu }); } } msg.RectTransform.SizeChanged += Recalculate; void Recalculate() { msg.RectTransform.SizeChanged -= Recalculate; msg.CalculateHeightFromText(); msg.RectTransform.SizeChanged += Recalculate; } if ((prevSize == 1.0f && chatBox.BarScroll == 0.0f) || (prevSize < 1.0f && chatBox.BarScroll == 1.0f)) { chatBox.BarScroll = 1.0f; } } private bool SelectJobPreferencesTab(GUIButton button, object userData) { jobPreferencesButton.Selected = true; appearanceButton.Selected = false; JobPreferenceContainer.Visible = true; appearanceFrame.Visible = false; return false; } private bool SelectAppearanceTab(GUIButton button, object _) { jobPreferencesButton.Selected = false; appearanceButton.Selected = true; JobPreferenceContainer.Visible = false; appearanceFrame.Visible = true; appearanceFrame.ClearChildren(); var info = GameMain.Client.CharacterInfo ?? Character.Controlled?.Info; CharacterAppearanceCustomizationMenu?.Dispose(); CharacterAppearanceCustomizationMenu = new CharacterInfo.AppearanceCustomizationMenu(info, appearanceFrame) { OnHeadSwitch = menu => { UpdateJobPreferences(info); SelectAppearanceTab(button, _); } }; return false; } public bool SaveAppearance() { var info = GameMain.Client?.CharacterInfo; if (info?.Head == null) { return false; } var characterConfig = MultiplayerPreferences.Instance; characterConfig.TagSet.Clear(); characterConfig.TagSet.UnionWith(info.Head.Preset.TagSet); characterConfig.HairIndex = info.Head.HairIndex; characterConfig.BeardIndex = info.Head.BeardIndex; characterConfig.MoustacheIndex = info.Head.MoustacheIndex; characterConfig.FaceAttachmentIndex = info.Head.FaceAttachmentIndex; characterConfig.HairColor = info.Head.HairColor; characterConfig.FacialHairColor = info.Head.FacialHairColor; characterConfig.SkinColor = info.Head.SkinColor; if (GameMain.GameSession?.IsRunning ?? false) { TabMenu.PendingChanges = true; CreateChangesPendingText(); } GameSettings.SaveCurrentConfig(); return true; } private bool SwitchJob(GUIButton _, object obj) { if (JobList == null || GameMain.Client == null) { return false; } int childIndex = JobList.SelectedIndex; var child = JobList.SelectedComponent; if (child == null) { return false; } bool moveToNext = obj != null; var jobPrefab = (obj as JobVariant)?.Prefab; object prevObj = child.UserData; var existingChild = JobList.Content.FindChild(d => (d.UserData is JobVariant prefab) && (prefab.Prefab == jobPrefab)); if (existingChild != null && obj != null) { existingChild.UserData = prevObj; } child.UserData = obj; for (int i = 0; i < 2; i++) { if (i < 2 && JobList.Content.GetChild(i).UserData == null) { JobList.Content.GetChild(i).UserData = JobList.Content.GetChild(i + 1).UserData; JobList.Content.GetChild(i + 1).UserData = null; } } UpdateJobPreferences(GameMain.Client.CharacterInfo ?? Character.Controlled?.Info); if (moveToNext) { var emptyChild = JobList.Content.FindChild(c => c.UserData == null && c.CanBeFocused); if (emptyChild != null) { JobList.Select(JobList.Content.GetChildIndex(emptyChild)); } else { JobList.Deselect(); if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } } } else { OpenJobSelection(child, child.UserData); } return false; } private bool OpenJobSelection(GUIComponent _, object __) { //recreate if resolution has changed if (GameMain.GraphicsWidth != prevResolutionForJobSelectionFrame.X || GameMain.GraphicsHeight != prevResolutionForJobSelectionFrame.Y) { JobSelectionFrame = null; } prevResolutionForJobSelectionFrame = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); if (JobSelectionFrame != null) { JobSelectionFrame.Visible = true; return true; } var allJobs = JobPrefab.Prefabs.Where(jobPrefab => !jobPrefab.HiddenJob && jobPrefab.MaxNumber > 0); //find the jobs that aren't currently visible in the job list, create a preview of the first variant var availableJobs = allJobs.Where(jobPrefab => JobList.Content.Children.All(c => c.UserData is not JobVariant prefab || prefab.Prefab != jobPrefab)) .Select(j => new JobVariant(j, 0)); //find the jobs that are currently visible in the job list, create a preview of the variant chosen in the list availableJobs = availableJobs.Concat( allJobs.Where(jobPrefab => JobList.Content.Children.Any(c => (c.UserData is JobVariant prefab) && prefab.Prefab == jobPrefab)) .Select(j => (JobVariant)JobList.Content.FindChild(c => (c.UserData is JobVariant prefab) && prefab.Prefab == j).UserData)); availableJobs = availableJobs.ToList(); const int JobsPerRow = 3; const int MaxRows = 4; int rowCount = (int)Math.Ceiling(availableJobs.Count() / (float)JobsPerRow); int jobButtonSize = GUI.IntScale(150); const float listBoxRelativeSize = 0.95f; Point frameSize = new Point(characterInfoFrame.Rect.Width, (int)(jobButtonSize * Math.Min(rowCount, MaxRows) / listBoxRelativeSize)); JobSelectionFrame = new GUIFrame(new RectTransform(frameSize, GUI.Canvas, Anchor.TopLeft), style: "GUIFrameListBox"); PositionJobSelectionFrame(); characterInfoFrame.RectTransform.SizeChanged += () => { if (characterInfoFrame == null || JobSelectionFrame?.RectTransform == null) { return; } Point size = new Point(characterInfoFrame.Rect.Width, (int)(jobButtonSize * Math.Min(rowCount, MaxRows) / listBoxRelativeSize)); JobSelectionFrame.RectTransform.Resize(size); PositionJobSelectionFrame(); }; void PositionJobSelectionFrame() { //move to the left side of the info frame JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.X - JobSelectionFrame.Rect.Width, JobList.Rect.Y); if (JobSelectionFrame.Rect.X < 0) { //scale if goes outside the screen horizontally JobSelectionFrame.RectTransform.Resize(new Point(characterInfoFrame.Rect.X, JobSelectionFrame.Rect.Height)); JobSelectionFrame.RectTransform.AbsoluteOffset = new Point(characterInfoFrame.Rect.X - JobSelectionFrame.Rect.Width, JobSelectionFrame.RectTransform.AbsoluteOffset.Y); } } var jobSelectionList = new GUIListBox(new RectTransform(Vector2.One * listBoxRelativeSize, JobSelectionFrame.RectTransform, Anchor.Center), style: "GUIFrameListBox") { Padding = Vector4.One * GUI.IntScale(10) }; var row = new GUILayoutGroup(new RectTransform(new Point(jobSelectionList.Content.Rect.Width, jobButtonSize), jobSelectionList.Content.RectTransform), isHorizontal: true) { Stretch = true }; GUIButton jobButton = null; int itemsInRow = 0; foreach (var jobPrefab in availableJobs) { if (itemsInRow >= JobsPerRow) { row = new GUILayoutGroup(new RectTransform(new Point(jobSelectionList.Content.Rect.Width, jobButtonSize), jobSelectionList.Content.RectTransform), isHorizontal: true) { Stretch = true }; itemsInRow = 0; } jobButton = new GUIButton(new RectTransform(new Point(jobButtonSize), row.RectTransform), style: "ListBoxElementSquare") { UserData = jobPrefab, OnClicked = (btn, usdt) => { if (btn.IsParentOf(GUI.MouseOn)) return false; return SwitchJob(btn, usdt); } }; itemsInRow++; var images = AddJobSpritesToGUIComponent(jobButton, jobPrefab.Prefab, team: TeamPreference, isPvPMode: SelectedMode == GameModePreset.PvP, selectedByPlayer: false); if (images != null && images.Length > 0) { jobPrefab.Variant = Math.Min(jobPrefab.Variant, images.Length); int currVisible = jobPrefab.Variant; GUIButton currSelected = null; for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) { images[variantIndex].Visible = currVisible == variantIndex; var variantButton = CreateJobVariantButton(jobPrefab, variantIndex, images.Length, jobButton); variantButton.OnClicked = (btn, obj) => { if (currSelected != null) { currSelected.Selected = false; } int selectedVariantIndex = ((JobVariant)obj).Variant; btn.Parent.UserData = obj; for (int i = 0; i < images.Length; i++) { images[i].Visible = selectedVariantIndex == i; } currSelected = btn; currSelected.Selected = true; return false; }; if (currVisible == variantIndex) { currSelected = variantButton; } } if (currSelected != null) { currSelected.Selected = true; } } } return true; } private static GUIImage[] AddJobSpritesToGUIComponent(GUIComponent parent, JobPrefab jobPrefab, CharacterTeamType team, bool isPvPMode, bool selectedByPlayer) { GUIFrame innerFrame = null; List outfitPreviews = jobPrefab.GetJobOutfitSprites(team, isPvPMode).ToList(); innerFrame = new GUIFrame(new RectTransform(Vector2.One * 0.85f, parent.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; GUIImage[] retVal = new GUIImage[outfitPreviews.Count]; if (outfitPreviews != null && outfitPreviews.Any()) { for (int i = 0; i < outfitPreviews.Count; i++) { Sprite outfitPreview = outfitPreviews[i]; float aspectRatio = outfitPreview.size.Y / outfitPreview.size.X; retVal[i] = new GUIImage(new RectTransform(new Vector2(0.7f / aspectRatio, 0.7f), innerFrame.RectTransform, Anchor.Center), outfitPreview, scaleToFit: true) { PressedColor = Color.White, CanBeFocused = false }; } } new GUIFrame(new RectTransform(new Vector2(1.0f, 0.35f), parent.RectTransform, Anchor.BottomCenter), style: "OuterGlow") { Color = Color.Black, HoverColor = Color.Black, PressedColor = Color.Black, SelectedColor = Color.Black, CanBeFocused = false }; var textBlock = new GUITextBlock( innerFrame.CountChildren == 0 ? new RectTransform(Vector2.One, parent.RectTransform, Anchor.Center) : new RectTransform(new Vector2(selectedByPlayer ? 0.55f : 0.95f, 0.3f), parent.RectTransform, Anchor.BottomCenter), jobPrefab.Name, wrap: true, textAlignment: Alignment.BottomCenter) { Padding = Vector4.Zero, HoverColor = Color.Transparent, SelectedColor = Color.Transparent, TextColor = jobPrefab.UIColor, HoverTextColor = Color.Lerp(jobPrefab.UIColor, Color.White, 0.5f), CanBeFocused = false, AutoScaleHorizontal = true }; textBlock.TextAlignment = textBlock.WrappedText.Contains('\n') ? Alignment.BottomCenter : Alignment.Center; textBlock.RectTransform.SizeChanged += () => { textBlock.TextScale = 1.0f; }; return retVal; } public void SelectMode(int modeIndex) { if (modeIndex < 0 || modeIndex >= ModeList.Content.CountChildren) { return; } if ((GameModePreset)ModeList.Content.GetChild(modeIndex).UserData != GameModePreset.MultiPlayerCampaign) { ToggleCampaignMode(false); } var prevMode = ModeList.Content.GetChild(selectedModeIndex).UserData as GameModePreset; if ((HighlightedModeIndex == selectedModeIndex || HighlightedModeIndex < 0) && ModeList.SelectedIndex != modeIndex) { ModeList.Select(modeIndex, GUIListBox.Force.Yes); } selectedModeIndex = modeIndex; if ((prevMode == GameModePreset.PvP) != (SelectedMode == GameModePreset.PvP)) { SaveAppearance(); UpdatePlayerFrame(null); GameMain.Client.ConnectedClients.ForEach(SetPlayerNameAndJobPreference); ResetPvpTeamSelection(); } if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this) { GameMain.GameSession = null; } respawnModeSelection.Refresh(); // not all respawn modes are compatible with all game modes RefreshGameModeContent(); RefreshEnabledElements(); UpdateDisembarkPointListFromServerSettings(); } public void HighlightMode(int modeIndex) { if (modeIndex < 0 || modeIndex >= ModeList.Content.CountChildren) { return; } HighlightedModeIndex = modeIndex; RefreshGameModeContent(); RefreshEnabledElements(); } private void RefreshMissionTypes() { IEnumerable suitableMissionClasses; if (SelectedMode == GameModePreset.Mission) { suitableMissionClasses = MissionPrefab.CoOpMissionClasses.Values; } else if (SelectedMode == GameModePreset.PvP) { suitableMissionClasses = MissionPrefab.PvPMissionClasses.Values; } else { return; } for (int i = 0; i < missionTypeTickBoxes.Length; i++) { Identifier missionType = (Identifier)missionTypeTickBoxes[i].UserData; missionTypeTickBoxes[i].Parent.Visible = MissionPrefab.Prefabs.Any(p => p.Type == missionType && suitableMissionClasses.Contains(p.MissionClass)); } } private void RefreshGameModeSettingsContent() { foreach (var element in campaignHiddenElements) { SetElementVisible(element, SelectedMode != GameModePreset.MultiPlayerCampaign && SelectedMode != GameModePreset.SinglePlayerCampaign); } foreach (var element in pvpOnlyElements) { SetElementVisible(element, SelectedMode == GameModePreset.PvP); } if (respawnTabButton != null && upgradesTabButton != null) { if (SelectedMode == GameModePreset.MultiPlayerCampaign) { SelectRespawnTab(); respawnTabButton.Enabled = upgradesTabButton.Enabled = false; } else { respawnTabButton.Enabled = upgradesTabButton.Enabled = true; } } static void SetElementVisible(GUIComponent element, bool enabled) { element.Visible = enabled; } gameModeSettingsLayout.Recalculate(); } private void RefreshGameModeContent() { if (GameMain.Client == null) { return; } foreach (var subElement in SubList.Content.Children) { subElement.CanBeFocused = true; foreach (var textBlock in subElement.GetAllChildren()) { textBlock.Enabled = true; } } SubList.Content.RectTransform.SortChildren((rt1, rt2) => { SubmarineInfo s1 = rt1.GUIComponent.UserData as SubmarineInfo; SubmarineInfo s2 = rt2.GUIComponent.UserData as SubmarineInfo; return s1.Name.CompareTo(s2.Name); }); autoRestartBox.Parent.Visible = true; UpdateDisembarkPointListFromServerSettings(); bool isPvP = SelectedMode == GameModePreset.PvP; foreach (GUIComponent child in SubList.Content.Children) { var container = child.GetChild(); var imageFrame = container.GetChild(); var coalIcon = imageFrame.GetChildByUserData(CoalitionIconUserData); var sepIcon = imageFrame.GetChildByUserData(SeparatistsIconUserData); coalIcon.Visible = isPvP; sepIcon.Visible = isPvP; if (GameMain.NetworkMember.ServerSettings.SubSelectionMode != SelectionMode.Vote) { coalIcon.Enabled = sepIcon.Enabled = false; if (child.UserData is not SubmarineInfo info) { continue; } if (SelectedSub == info) { coalIcon.Enabled = true; } if (SelectedEnemySub == info) { sepIcon.Enabled = true; } } } UpdateSelectedSub(isPvP ? MultiplayerPreferences.Instance.TeamPreference : CharacterTeamType.None); RefreshGameModeSettingsContent(); if (SelectedMode == GameModePreset.Mission || SelectedMode == GameModePreset.PvP) { MissionTypeFrame.Visible = true; CampaignFrame.Visible = CampaignSetupFrame.Visible = false; RefreshMissionTypes(); } else if (SelectedMode == GameModePreset.MultiPlayerCampaign) { MissionTypeFrame.Visible = autoRestartBox.Parent.Visible = false; if (GameMain.GameSession?.GameMode is CampaignMode campaign && campaign.Map != null) { //campaign running CampaignFrame.Visible = QuitCampaignButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound); CampaignSetupFrame.Visible = false; } else { CampaignFrame.Visible = false; CampaignSetupFrame.Visible = true; if (!CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageRound)) { CampaignSetupFrame.ClearChildren(); new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.5f), CampaignSetupFrame.RectTransform, Anchor.Center), TextManager.Get("campaignstarting"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.Center, wrap: true); } } if (CampaignSetupUI != null) { foreach (var subElement in SubList.Content.Children) { var sub = subElement.UserData as SubmarineInfo; bool tooExpensive = sub.Price > CampaignSettings.CurrentSettings.InitialMoney; if (tooExpensive || !sub.IsCampaignCompatible) { foreach (var textBlock in subElement.GetAllChildren()) { textBlock.DisabledTextColor = (textBlock.UserData as string == "pricetext" && tooExpensive ? GUIStyle.Red : GUIStyle.TextColorNormal) * 0.7f; textBlock.Enabled = false; } } } SubList.Content.RectTransform.SortChildren((rt1, rt2) => { SubmarineInfo s1 = rt1.GUIComponent.UserData as SubmarineInfo; SubmarineInfo s2 = rt2.GUIComponent.UserData as SubmarineInfo; int p1 = s1.Price; if (!s1.IsCampaignCompatible) { p1 += 100000; } int p2 = s2.Price; if (!s2.IsCampaignCompatible) { p2 += 100000; } return p1.CompareTo(p2) * 100 + s1.Name.CompareTo(s2.Name); }); } } else { MissionTypeFrame.Visible = CampaignFrame.Visible = CampaignSetupFrame.Visible = false; CampaignFrame.Visible = CampaignSetupFrame.Visible = false; } ReadyToStartBox.Parent.Visible = !GameMain.Client.GameStarted; RefreshStartButtonVisibility(); RefreshOutpostDropdown(); } public void RefreshStartButtonVisibility() { bool campaignActive = GameMain.GameSession?.GameMode is CampaignMode; if (CampaignSetupUI != null && CampaignSetupFrame is { Visible: true }) { //setting up a campaign -> start button only visible if we're in the "new game" tab (load game menu not visible) StartButton.Visible = !GameMain.Client.GameStarted && !CampaignSetupUI.LoadGameMenuVisible && (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || GameMain.Client.HasPermission(ClientPermissions.ManageCampaign)); } else { //if a campaign is currently running, we must show the start button to allow continuing StartButton.Visible = (SelectedMode != GameModePreset.MultiPlayerCampaign || campaignActive) && !GameMain.Client.GameStarted && GameMain.Client.HasPermission(ClientPermissions.ManageRound); } StartButton.Enabled = true; if (GameSession.ShouldApplyDisembarkPoints(SelectedMode)) { StartButton.Enabled = GameSession.ValidatedDisembarkPoints(SelectedMode, MissionTypes); StartButton.ToolTip = !StartButton.Enabled ? TextManager.Get("DisembarkPointsNotValid") : string.Empty; } StartButton.IgnoreLayoutGroups = !StartButton.Visible; //can end the round if round is running EndButton.Visible = !StartButton.Visible && GameMain.Client is { GameStarted: true } && (GameMain.Client.HasPermission(ClientPermissions.ManageRound) || (campaignActive && GameMain.Client.HasPermission(ClientPermissions.ManageCampaign))); EndButton.IgnoreLayoutGroups = !EndButton.Visible; } public void RefreshChatrow() { chatRow.ClearChildren(); // Team chat only makes sense when in a team (in "player preference" team selection mode, team assignments only happen at round start) if (SelectedMode == GameModePreset.PvP && GameMain.Client?.ServerSettings?.PvpTeamSelectionMode == PvpTeamSelectionMode.PlayerChoice && MultiplayerPreferences.Instance.TeamPreference != CharacterTeamType.None) { var chatSelectorRT = new RectTransform(new Vector2(0.25f, 1.0f), chatRow.RectTransform, Anchor.CenterLeft); chatSelector = new GUIDropDown(chatSelectorRT, elementCount: 2) { OnSelected = (_, userdata) => { TeamChatSelected = (bool)userdata; return true; } }; chatSelector.AddItem(TextManager.Get($"PvP.ChatMode.Team"), userData: true, color: ChatMessage.MessageColor[(int)ChatMessageType.Team]); chatSelector.AddItem(TextManager.Get($"PvP.ChatMode.All"), userData: false, color: ChatMessage.MessageColor[(int)ChatMessageType.Default]); chatSelector.SelectItem(TeamChatSelected); } else { TeamChatSelected = false; } if (chatInput != null) { chatInput.RectTransform.Parent = chatRow.RectTransform; } else { chatInput = new GUITextBox(new RectTransform(new Vector2(0.75f, 1.0f), chatRow.RectTransform, Anchor.CenterRight)) { MaxTextLength = ChatMessage.MaxLength, Font = GUIStyle.SmallFont, DeselectAfterMessage = false }; micIcon = new GUIImage(new RectTransform(new Vector2(0.05f, 1.0f), chatRow.RectTransform), style: "GUIMicrophoneUnavailable"); chatInput.Select(); } //this needs to be done even if we're using the existing chatinput instance instead of creating a new one, //because the client might not have existed when the input box was first created if (GameMain.Client != null) { chatInput.ResetDelegates(); chatInput.OnEnterPressed = GameMain.Client.EnterChatMessage; chatInput.OnTextChanged += GameMain.Client.TypingChatMessage; chatInput.OnDeselected += (sender, key) => { GameMain.Client?.ChatBox.ChatManager.Clear(); }; ChatManager.RegisterKeys(chatInput, GameMain.Client.ChatBox.ChatManager); } chatRow.Recalculate(); } public void ToggleCampaignMode(bool enabled) { if (!enabled) { //remove campaign character from the panel if (campaignCharacterInfo != null) { campaignCharacterInfo = null; UpdatePlayerFrame(null); SetSpectate(spectateBox.Selected); } CampaignCharacterDiscarded = false; } RefreshEnabledElements(); if (enabled && SelectedMode != GameModePreset.MultiPlayerCampaign) { ModeList.Select(GameModePreset.MultiPlayerCampaign, GUIListBox.Force.Yes); } } public void TryDisplayCampaignSubmarine(SubmarineInfo submarine) { string name = submarine?.Name; bool displayed = false; SubList.OnSelected -= VotableClicked; SubList.Deselect(); subPreviewContainer.ClearChildren(); foreach (GUIComponent child in SubList.Content.Children) { if (child.UserData is not SubmarineInfo sub) { continue; } //just check the name, even though the campaign sub may not be the exact same version //we're selecting the sub just for show, the selection is not actually used for anything if (sub.Name == name) { SubList.Select(sub); if (SubmarineInfo.SavedSubmarines.Contains(sub)) { CreateSubPreview(sub); displayed = true; } break; } } SubList.OnSelected += VotableClicked; if (!displayed) { CreateSubPreview(submarine); } UpdateSubVisibility(); } private bool ViewJobInfo(GUIButton button, object obj) { if (button.UserData is not JobVariant jobPrefab) { return false; } JobInfoFrame = jobPrefab.Prefab.CreateInfoFrame(isPvP: SelectedMode == GameModePreset.PvP, out GUIComponent buttonContainer); GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), buttonContainer.RectTransform, Anchor.BottomRight), TextManager.Get("Close")) { OnClicked = CloseJobInfo }; JobInfoFrame.OnClicked = (btn, userdata) => { if (GUI.MouseOn == btn || GUI.MouseOn == btn.TextBlock) CloseJobInfo(btn, userdata); return true; }; return true; } private bool CloseJobInfo(GUIButton button, object obj) { JobInfoFrame = null; return true; } private void UpdateJobPreferences(CharacterInfo characterInfo) { if (characterInfo == null) { return; } GUICustomComponent characterIcon = JobPreferenceContainer.GetChild(); JobPreferenceContainer.RemoveChild(characterIcon); characterInfo.CreateIcon(new RectTransform(new Vector2(1.0f, 0.4f), JobPreferenceContainer.RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.025f) }); GUIListBox listBox = JobPreferenceContainer.GetChild(); /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); } jobPreferenceSprites.Clear();*/ List jobPreferences = new List(); bool disableNext = false; for (int i = 0; i < listBox.Content.CountChildren; i++) { GUIComponent slot = listBox.Content.GetChild(i); slot.ClearChildren(); slot.CanBeFocused = !disableNext; if (slot.UserData is JobVariant jobPrefab) { var images = AddJobSpritesToGUIComponent(slot, jobPrefab.Prefab, team: TeamPreference, isPvPMode: SelectedMode == GameModePreset.PvP, selectedByPlayer: true); for (int variantIndex = 0; variantIndex < images.Length; variantIndex++) { int selectedVariantIndex = Math.Min(jobPrefab.Variant, images.Length); images[variantIndex].Visible = images.Length == 1 || selectedVariantIndex == variantIndex; if (images.Length > 0) { var variantButton = CreateJobVariantButton(jobPrefab, variantIndex, images.Length, slot); variantButton.OnClicked = (btn, obj) => { btn.Parent.UserData = obj; UpdateJobPreferences(characterInfo); return false; }; } } // Info button new GUIButton(new RectTransform(new Vector2(0.15f), slot.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.BothWidth) { RelativeOffset = new Vector2(0.075f) }, style: "GUIButtonInfo") { UserData = jobPrefab, OnClicked = ViewJobInfo }; // Remove button new GUIButton(new RectTransform(new Vector2(0.15f), slot.RectTransform, Anchor.BottomRight, scaleBasis: ScaleBasis.BothWidth) { RelativeOffset = new Vector2(0.075f) }, style: "GUICancelButton") { UserData = i, OnClicked = (btn, obj) => { JobList.Select((int)obj, GUIListBox.Force.Yes); SwitchJob(btn, null); if (JobSelectionFrame != null) { JobSelectionFrame.Visible = false; } JobList.Deselect(); return false; } }; jobPreferences.Add(new MultiplayerPreferences.JobPreference(jobPrefab.Prefab.Identifier, jobPrefab.Variant)); } else { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.6f), slot.RectTransform), (i + 1).ToString(), textColor: Color.White * (disableNext ? 0.15f : 0.5f), textAlignment: Alignment.Center, font: GUIStyle.LargeFont) { CanBeFocused = false }; if (!disableNext) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), slot.RectTransform, Anchor.BottomCenter), TextManager.Get("clicktoselectjob"), font: GUIStyle.SmallFont, wrap: true, textAlignment: Alignment.Center) { CanBeFocused = false }; } disableNext = true; } } GameMain.Client.ForceNameJobTeamUpdate(); if (!MultiplayerPreferences.Instance.AreJobPreferencesEqual(jobPreferences)) { if (GameMain.GameSession?.IsRunning ?? false) { TabMenu.PendingChanges = true; CreateChangesPendingText(); } MultiplayerPreferences.Instance.JobPreferences.Clear(); MultiplayerPreferences.Instance.JobPreferences.AddRange(jobPreferences); GameSettings.SaveCurrentConfig(); } } private static GUIButton CreateJobVariantButton(JobVariant jobPrefab, int variantIndex, int variantCount, GUIComponent slot) { float relativeSize = 0.18f; var btn = new GUIButton(new RectTransform(new Vector2(relativeSize), slot.RectTransform, Anchor.TopCenter, scaleBasis: ScaleBasis.BothHeight) { RelativeOffset = new Vector2(relativeSize * 1.3f * (variantIndex - (variantCount - 1) / 2.0f), 0.02f) }, (variantIndex + 1).ToString(), style: "JobVariantButton") { Selected = jobPrefab.Variant == variantIndex, UserData = new JobVariant(jobPrefab.Prefab, variantIndex), }; return btn; } public readonly struct FailedSubInfo { public readonly string Name; public readonly string Hash; public FailedSubInfo(string name, string hash) { Name = name; Hash = hash; } public void Deconstruct(out string name, out string hash) { name = Name; hash = Hash; } private static bool StringsEqual(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase); public static bool operator ==(FailedSubInfo a, FailedSubInfo b) => StringsEqual(a.Name, b.Name) && StringsEqual(a.Hash, b.Hash); public static bool operator !=(FailedSubInfo a, FailedSubInfo b) => !(a == b); public override int GetHashCode() { return HashCode.Combine(Name, Hash); } public override bool Equals(object obj) { return obj is FailedSubInfo info && Name == info.Name && Hash == info.Hash; } } public FailedSubInfo? FailedSelectedSub; public FailedSubInfo? FailedSelectedEnemySub; public FailedSubInfo? FailedSelectedShuttle; public List FailedCampaignSubs = new List(); public List FailedOwnedSubs = new List(); public bool TrySelectSub(string subName, string md5Hash, SelectedSubType type, GUIListBox subList, bool showPreview = true) { UpdateSubVisibility(); if (GameMain.Client == null) { return false; } //already downloading the selected sub file if (GameMain.Client.FileReceiver.ActiveTransfers.Any(t => t.FileName == subName + ".sub")) { return false; } SubmarineInfo sub = subList.Content.Children .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName && s.MD5Hash?.StringRepresentation == md5Hash)? .UserData as SubmarineInfo; //matching sub found and already selected, all good if (sub != null) { if (subList == SubList && showPreview) { if (type is not SelectedSubType.EnemySub || MultiplayerPreferences.Instance.TeamPreference == CharacterTeamType.Team2) { CreateSubPreview(sub); } } SubmarineInfo selectedSub = type switch { SelectedSubType.Sub => SelectedSub, SelectedSubType.EnemySub => SelectedEnemySub, SelectedSubType.Shuttle => SelectedShuttle, _ => null }; if (selectedSub != null && selectedSub.MD5Hash?.StringRepresentation == md5Hash && Barotrauma.IO.File.Exists(sub.FilePath)) { //ensure the selected sub matches the correct submarineInfo instance (which may have been just downloaded from the server) switch (type) { case SelectedSubType.Sub: SelectedSub = sub; break; case SelectedSubType.EnemySub: SelectedEnemySub = sub; break; } return true; } } //sub not found, see if we have a sub with the same name if (sub == null) { sub = subList.Content.Children .FirstOrDefault(c => c.UserData is SubmarineInfo s && s.Name == subName)? .UserData as SubmarineInfo; } //found a sub that at least has the same name, select it if (sub != null) { if (subList.Parent is GUIDropDown subDropDown) { subDropDown.SelectItem(sub); } else { subList.OnSelected -= VotableClicked; var preference = MultiplayerPreferences.Instance.TeamPreference; switch (type) { case SelectedSubType.Sub: if (preference is CharacterTeamType.Team1 or CharacterTeamType.None) { subList.Select(sub); } SelectedSub = sub; break; case SelectedSubType.EnemySub: if (preference is CharacterTeamType.Team2) { subList.Select(sub); } SelectedEnemySub = sub; break; } subList.OnSelected += VotableClicked; } switch (type) { case SelectedSubType.Sub: FailedSelectedSub = null; break; case SelectedSubType.EnemySub: FailedSelectedEnemySub = null; break; case SelectedSubType.Shuttle: FailedSelectedShuttle = null; break; } //hashes match, all good if (sub.MD5Hash?.StringRepresentation == md5Hash && SubmarineInfo.SavedSubmarines.Contains(sub)) { return true; } } //------------------------------------------------------------------------------------- //if we get to this point, a matching sub was not found or it has an incorrect MD5 hash switch (type) { case SelectedSubType.Sub: FailedSelectedSub = new FailedSubInfo(subName, md5Hash); break; case SelectedSubType.EnemySub: FailedSelectedEnemySub = new FailedSubInfo(subName, md5Hash); break; case SelectedSubType.Shuttle: FailedSelectedShuttle = new FailedSubInfo(subName, md5Hash); break; } LocalizedString errorMsg = ""; if (sub == null || !SubmarineInfo.SavedSubmarines.Contains(sub)) { errorMsg = TextManager.GetWithVariable("SubNotFoundError", "[subname]", subName) + " "; } else if (sub.MD5Hash?.StringRepresentation == null) { errorMsg = TextManager.GetWithVariable("SubLoadError", "[subname]", subName) + " "; GUITextBlock textBlock = subList.Content.GetChildByUserData(sub)?.GetChild(); if (textBlock != null) { textBlock.TextColor = GUIStyle.Red; } } else { errorMsg = TextManager.GetWithVariables("SubDoesntMatchError", ("[subname]", sub.Name), ("[myhash]", sub.MD5Hash.ShortRepresentation), ("[serverhash]", Md5Hash.GetShortHash(md5Hash))) + " "; } if (GameMain.Client.ServerSettings.AllowFileTransfers) { GameMain.Client?.RequestFile(FileTransferType.Submarine, subName, md5Hash); } else { new GUIMessageBox(TextManager.Get("DownloadSubLabel"), errorMsg); } return false; } public enum SubmarineDeliveryData { Owned, Campaign } public bool CheckIfCampaignSubMatches(SubmarineInfo serverSubmarine, SubmarineDeliveryData deliveryData) { if (GameMain.Client == null) { return false; } //already downloading the selected sub file if (GameMain.Client.FileReceiver.ActiveTransfers.Any(t => t.FileName == serverSubmarine.Name + ".sub")) { return false; } SubmarineInfo purchasableSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSubmarine.Name && s.MD5Hash?.StringRepresentation == serverSubmarine.MD5Hash?.StringRepresentation); if (purchasableSub != null) { return true; } FailedSubInfo fileInfo = new FailedSubInfo(serverSubmarine.Name, serverSubmarine.MD5Hash.StringRepresentation); switch (deliveryData) { case SubmarineDeliveryData.Owned: FailedOwnedSubs.Add(fileInfo); break; case SubmarineDeliveryData.Campaign: FailedCampaignSubs.Add(fileInfo); break; } GameMain.Client?.RequestFile(FileTransferType.Submarine, fileInfo.Name, fileInfo.Hash); return false; } private void CreateSubPreview(SubmarineInfo sub) { subPreviewContainer?.ClearChildren(); sub.CreatePreviewWindow(subPreviewContainer); RecalculateSubDescription(); } private void RecalculateSubDescription() { var descriptionBox = subPreviewContainer?.FindChild("descriptionbox", recursive: true); if (descriptionBox != null && characterInfoFrame != null) { //if description box and character info box are roughly the same size, scale them to the same size if (Math.Abs(descriptionBox.Rect.Height - characterInfoFrame.Rect.Height) < 80 * GUI.Scale) { descriptionBox.RectTransform.MaxSize = new Point(descriptionBox.Rect.Width, characterInfoFrame.Rect.Height); } } } private readonly List visibilityMenuOrder = new List(); public const string SeparatistsIconUserData = "separatistsIcon"; public const string CoalitionIconUserData = "coalitionIcon"; private void CreateSubmarineVisibilityMenu() { var messageBox = new GUIMessageBox(TextManager.Get("SubmarineVisibility"), "", buttons: Array.Empty(), relativeSize: new Vector2(0.75f, 0.75f)); messageBox.Content.ChildAnchor = Anchor.TopCenter; var columns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.9f), messageBox.Content.RectTransform), isHorizontal: true); GUILayoutGroup createColumn(float width) => new GUILayoutGroup(new RectTransform(new Vector2(width, 1.0f), columns.RectTransform)) { Stretch = true }; GUIListBox createColumnListBox(string labelTag) { var column = createColumn(0.45f); var label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.1f), column.RectTransform), TextManager.Get(labelTag), textAlignment: Alignment.Center); return new GUIListBox(new RectTransform(new Vector2(1.0f, 0.9f), column.RectTransform)) { CurrentSelectMode = GUIListBox.SelectMode.RequireShiftToSelectMultiple, CurrentDragMode = GUIListBox.DragMode.DragOutsideBox, HideDraggedElement = true }; } void handleDraggingAcrossLists(GUIListBox from, GUIListBox to) { //TODO: put this in a static class once modding-refactor gets merged if (to.Rect.Contains(PlayerInput.MousePosition) && from.DraggedElement != null) { //move the dragged elements to the index determined previously var draggedElement = from.DraggedElement; var selected = from.AllSelected.ToList(); selected.Sort((a, b) => from.Content.GetChildIndex(a) - from.Content.GetChildIndex(b)); float oldCount = to.Content.CountChildren; float newCount = oldCount + selected.Count; var offset = draggedElement.RectTransform.AbsoluteOffset; offset += from.Content.Rect.Location; offset -= to.Content.Rect.Location; for (int i = 0; i < selected.Count; i++) { var c = selected[i]; c.Parent.RemoveChild(c); c.RectTransform.Parent = to.Content.RectTransform; c.RectTransform.RepositionChildInHierarchy((int)oldCount+i); } from.DraggedElement = null; from.Deselect(); from.RecalculateChildren(); from.RectTransform.RecalculateScale(true); to.RecalculateChildren(); to.RectTransform.RecalculateScale(true); to.Select(selected); //recalculate the dragged element's offset so it doesn't jump around draggedElement.RectTransform.AbsoluteOffset = offset; to.DraggedElement = draggedElement; to.BarScroll *= (oldCount / newCount); } } var visibleSubsList = createColumnListBox("VisibleSubmarines"); var centerColumn = createColumn(0.1f); void centerSpacing() { new GUIFrame(new RectTransform(new Vector2(1.0f, 0.4f), centerColumn.RectTransform), style: null); } GUIButton centerButton(string style) => new GUIButton( new RectTransform(new Vector2(1.0f, 0.1f), centerColumn.RectTransform), style: style); var hiddenSubsList = createColumnListBox("HiddenSubmarines"); void addSubToList(SubmarineInfo sub, GUIListBox list) { var modFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.08f), list.Content.RectTransform), style: "ListBoxElement") { UserData = sub }; var frameContent = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), modFrame.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true, RelativeSpacing = 0.02f }; var dragIndicator = new GUIButton(new RectTransform(new Vector2(0.5f, 0.5f), frameContent.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIDragIndicator") { CanBeFocused = false }; var subName = new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), frameContent.RectTransform), text: sub.DisplayName) { UserData = "nametext", ToolTip = sub.Description, CanBeFocused = false }; CreateSubmarineClassText( frameContent, sub, subName, list.Content); } foreach (var sub in GameMain.Client.ServerSubmarines .OrderBy(s => visibilityMenuOrder.Contains(s)) .ThenBy(s => visibilityMenuOrder.IndexOf(s))) { addSubToList(sub, GameMain.Client.ServerSettings.HiddenSubs.Contains(sub.Name) ? hiddenSubsList : visibleSubsList); } void onRearranged(GUIListBox listBox, object userData) { visibilityMenuOrder.Clear(); visibilityMenuOrder.AddRange(visibleSubsList.Content.Children.Select(c => c.UserData as SubmarineInfo)); visibilityMenuOrder.AddRange(hiddenSubsList.Content.Children.Select(c => c.UserData as SubmarineInfo)); } visibleSubsList.OnRearranged = onRearranged; hiddenSubsList.OnRearranged = onRearranged; void swapListItems(GUIListBox from, GUIListBox to) { to.Deselect(); var selected = from.AllSelected.ToArray(); int lastIndex = from.Content.GetChildIndex(selected.LastOrDefault()); int nextIndex = lastIndex + 1; GUIComponent nextComponent = null; if (lastIndex >= 0 && nextIndex < from.Content.CountChildren) { nextComponent = from.Content.GetChild(nextIndex); } foreach (var frame in selected) { frame.Parent.RemoveChild(frame); frame.RectTransform.Parent = to.Content.RectTransform; } from.RecalculateChildren(); from.RectTransform.RecalculateScale(true); to.RecalculateChildren(); to.RectTransform.RecalculateScale(true); to.Select(selected); if (nextComponent != null) { from.Select(nextComponent.ToEnumerable()); } } centerSpacing(); var visibleToHidden = centerButton("GUIButtonToggleRight"); visibleToHidden.OnClicked = (button, o) => { swapListItems(visibleSubsList, hiddenSubsList); return false; }; var hiddenToVisible = centerButton("GUIButtonToggleLeft"); hiddenToVisible.OnClicked = (button, o) => { swapListItems(hiddenSubsList, visibleSubsList); return false; }; centerSpacing(); var buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.1f), messageBox.Content.RectTransform), isHorizontal: true) { RelativeSpacing = 0.01f }; var cancelButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get("Cancel")) { OnClicked = (button, o) => { messageBox.Close(); return false; } }; var okButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1.0f), buttonLayout.RectTransform), TextManager.Get("OK")) { OnClicked = (button, o) => { var hiddenSubs = GameMain.Client.ServerSettings.HiddenSubs; hiddenSubs.Clear(); hiddenSubs.UnionWith(hiddenSubsList.Content.Children.Select(c => (c.UserData as SubmarineInfo).Name)); GameMain.Client.ServerSettings.ClientAdminWrite(ServerSettings.NetFlags.HiddenSubs); messageBox.Close(); return false; } }; new GUICustomComponent(new RectTransform(Vector2.Zero, messageBox.RectTransform), onUpdate: (f, component) => { handleDraggingAcrossLists(visibleSubsList, hiddenSubsList); handleDraggingAcrossLists(hiddenSubsList, visibleSubsList); if (PlayerInput.PrimaryMouseButtonClicked() && !GUI.IsMouseOn(visibleToHidden) && !GUI.IsMouseOn(hiddenToVisible)) { if (!GUI.IsMouseOn(hiddenSubsList) || !hiddenSubsList.Content.IsParentOf(GUI.MouseOn)) { hiddenSubsList.Deselect(); } if (!GUI.IsMouseOn(visibleSubsList) || !visibleSubsList.Content.IsParentOf(GUI.MouseOn)) { visibleSubsList.Deselect(); } } }, onDraw: (spriteBatch, component) => { visibleSubsList.DraggedElement?.DrawManually(spriteBatch, true, true); hiddenSubsList.DraggedElement?.DrawManually(spriteBatch, true, true); }); } public void UpdateSubVisibility() { if (GameMain.Client == null) { return; } foreach (GUIComponent child in SubList.Content.Children) { if (child.UserData is not SubmarineInfo sub) { continue; } child.Visible = (!GameMain.Client.ServerSettings.HiddenSubs.Contains(sub.Name) || (GameMain.GameSession?.SubmarineInfo != null && GameMain.GameSession.SubmarineInfo.Name.Equals(sub.Name, StringComparison.OrdinalIgnoreCase))) && (string.IsNullOrEmpty(subSearchBox.Text) || sub.DisplayName.Contains(subSearchBox.Text, StringComparison.OrdinalIgnoreCase)); } } public void OnRoundEnded() { CampaignCharacterDiscarded = false; } private const string RoundStartWarningBoxUserData = "RoundStartWarningBox"; public void ShowStartRoundWarning(SerializableDateTime waitUntilTime, string team1SubName, ImmutableArray team1IncompatiblePerks, string team2SubName, ImmutableArray team2IncompatiblePerks) { DateTime startTime = DateTime.UtcNow; TimeSpan differenceFromStart = waitUntilTime.ToUtcValue() - startTime; StopWaitingForStartRound(); GUIMessageBox.MessageBoxes.OfType().ForEachMod(static mod => { if (mod.UserData is PleaseWaitPopupUserData) { mod.Close(); } }); var messageBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("startgamewarning"), Array.Empty(), relativeSize: new Vector2(0.3f / GUI.AspectRatioAdjustment, 0.4f), minSize: new Point(400, 300)) { UserData = RoundStartWarningBoxUserData }; GUILayoutGroup contentLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.7f), messageBox.Content.RectTransform, Anchor.BottomCenter), isHorizontal: false); GUIListBox errorList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.7f), contentLayout.RectTransform)); foreach (DisembarkPerkPrefab perk in team1IncompatiblePerks) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), errorList.Content.RectTransform), FormatWarning(perk, team1SubName)); } foreach (DisembarkPerkPrefab perk in team2IncompatiblePerks) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.33f), errorList.Content.RectTransform), FormatWarning(perk, team2SubName)); } GUIProgressBar progress = new GUIProgressBar(new RectTransform(new Vector2(1f, 0.15f), contentLayout.RectTransform), 0.0f, GUIStyle.Orange); GUITextBlock progressText = new GUITextBlock(new RectTransform(Vector2.One, progress.RectTransform), TextManager.GetWithVariable("startggamewarningprogress", "[seconds]", ((int)differenceFromStart.TotalSeconds).ToString()), textAlignment: Alignment.Center) { Shadow = true, TextColor = Color.White }; new GUICustomComponent(new RectTransform(Vector2.Zero, progress.RectTransform), onDraw: static (batch, component) => { }, onUpdate: (f, component) => { TimeSpan difference = waitUntilTime.ToUtcValue() - DateTime.UtcNow; float seconds = (float)difference.TotalSeconds; progress.BarSize = seconds / (float)differenceFromStart.TotalSeconds; progressText.Text = TextManager.GetWithVariable("startggamewarningprogress", "[seconds]", ((int)seconds).ToString()); }); GUILayoutGroup buttonLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), contentLayout.RectTransform), childAnchor: Anchor.BottomCenter); GUIButton cancelButton = new GUIButton(new RectTransform(new Vector2(0.5f, 1f), buttonLayout.RectTransform), TextManager.Get("Cancel")); cancelButton.OnClicked += (button, userData) => { IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.RESPONSE_CANCEL_STARTGAME); GameMain.Client?.ClientPeer?.Send(msg, DeliveryMethod.Reliable); messageBox.Close(); return true; }; static LocalizedString FormatWarning(DisembarkPerkPrefab prefab, string subName) { return TextManager.GetWithVariables("startgamewarningformat", ("[category]", TextManager.Get($"perkcategory.{prefab.SortCategory}")), ("[perk]", prefab.Name), ("[submarine]", subName)); } } public void CloseStartRoundWarning() { GUIMessageBox.MessageBoxes.OfType().ForEachMod(static mod => { if (mod.UserData is RoundStartWarningBoxUserData) { mod.Close(); } }); } } }