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<GUIComponent> traitorElements = new List<GUIComponent>();
        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;
        /// <summary>
        /// Contains the elements that control starting the round (start button, spectate button, "ready to start" tickbox)
        /// </summary>
        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<RespawnMode> respawnModeSelection;
        private GUITextBlock respawnModeLabel;
        private GUIComponent respawnIntervalElement;
        
        private readonly List<GUIComponent> midRoundRespawnSettings = new List<GUIComponent>();
        private readonly List<GUIComponent> permadeathEnabledRespawnSettings = new List<GUIComponent>();
        private readonly List<GUIComponent> permadeathDisabledRespawnSettings = new List<GUIComponent>();
        private readonly List<GUIComponent> ironmanDisabledRespawnSettings = new List<GUIComponent>();
        private readonly List<GUIComponent> campaignDisabledElements = new List<GUIComponent>();
        private readonly List<GUIComponent> campaignHiddenElements = new List<GUIComponent>();
        private readonly List<GUIComponent> pvpOnlyElements = new();
        private readonly List<GUIComponent> disembarkPerkSettings = new();
        private readonly List<GUIComponent> 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;
        }

        /// <summary>
        /// Elements that can only be used by the host or people with server settings management permissions (but are visible to everyone)
        /// </summary>
        private readonly List<GUIComponent> clientDisabledElements = new List<GUIComponent>();

        /// <summary>
        /// Elements that are only visible to the host or people with server settings management permissions
        /// </summary>
        private readonly List<GUIComponent> clientHiddenElements = new List<GUIComponent>();

        private readonly List<GUIComponent> botSettingsElements = new List<GUIComponent>();

        private readonly Dictionary<GUIComponent, string> settingAssignedComponents = new Dictionary<GUIComponent, string>(); 

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

        /// <summary>
        /// Tab buttons above the chat panel (chat and server log tabs)
        /// </summary>
        private readonly List<GUIButton> chatPanelTabButtons = new List<GUIButton>();

        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<SubmarineInfo> GetSubList()
            => (IReadOnlyList<SubmarineInfo>)GameMain.Client?.ServerSubmarines
               ?? Array.Empty<SubmarineInfo>();

        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<Identifier> 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<JobVariant> 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<JobVariant>();
                }

                List<JobVariant> jobPreferences = new List<JobVariant>();
                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;
        /// <summary>
        /// Spacing between different elements in the panels
        /// </summary>
        private const float PanelSpacing = 0.005f;

        /// <summary>
        /// Size of the outer border of the panels (= empty area round the contents of the panel)
        /// </summary>
        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<GUIComponent>())
            {
                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<Identifier> 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<Identifier> 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<Identifier> 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<Identifier> 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<int>(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<BotSpawnMode>(new RectTransform(new Vector2(0.5f, 1.0f), botSpawnModeSettingHolder.RectTransform));
            foreach (var botSpawnMode in Enum.GetValues(typeof(BotSpawnMode)).Cast<BotSpawnMode>())
            {
                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<int>(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<GUIComponent> elementsToEnable,
                                     ICollection<GUIComponent> 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<RespawnMode>(new RectTransform(new Vector2(0.6f, 1.0f), respawnModeHolder.RectTransform));
            foreach (var respawnMode in Enum.GetValues(typeof(RespawnMode)).Cast<RespawnMode>().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;

        /// <summary>
        /// Used to prevent disembarkPerkSettingList.AfterSelected from firing when the server settings are updated.
        /// </summary>
        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<Identifier>()
                };

                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<GUITextBlock>())
                        {
                            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<GUITextBlock>())
                    {
                        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
                    SetAFKSelected(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<CoroutineStatus> WaitForStartRound(GUIButton startButton)
        {
            GUI.SetCursorWaiting();
            LocalizedString headerText = TextManager.Get("RoundStartingPleaseWait");
            var msgBox = new GUIMessageBox(headerText, TextManager.Get("RoundStarting"), Array.Empty<LocalizedString>())
            {
                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 SetAFKSelected(bool selected)
        {
            if (afkBox.Selected != selected)
            {
                afkBox.Flash(GUIStyle.Green);
                afkBox.Selected = selected;
            }
        }

        public void SetAutoRestart(bool enabled, float timer = 0.0f)
        {
            autoRestartBox.Selected = enabled;
            autoRestartTimer = timer;
        }

        public void SetMissionTypes(IEnumerable<Identifier> 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<Identifier> validOutpostTagsForMissions = new HashSet<Identifier>();

            IEnumerable<Type> 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<SubmarineInfo> 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<string, float>("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<SpriteBatch, GUICustomComponent> 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<string, float> pair && pair.First == "soundicon");
            var soundIconDisabled = PlayerFrame.FindChild("soundicondisabled");

            Pair<string, float> userdata = soundIcon.UserData as Pair<string, float>;

            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<string, float> pair && pair.First == "soundicon");
            Pair<string, float> userdata = soundIcon.UserData as Pair<string, float>;
            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<ContextMenuOption> options = new List<ContextMenuOption>();

            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<ContextMenuOption> rankOptions = new List<ContextMenuOption>();
            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<GUIListBox>().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<GUIListBox>().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<string, float> 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<Sprite> 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<Type> 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<GUITextBlock>())
                {
                    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<GUILayoutGroup>();

                var imageFrame = container.GetChild<GUIFrame>();

                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<GUITextBlock>())
                            {
                                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(ignoreSelectSound: true);
            }

            //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<GUICustomComponent>();
            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<GUIListBox>();
            /*foreach (Sprite sprite in jobPreferenceSprites) { sprite.Remove(); }
            jobPreferenceSprites.Clear();*/

            List<MultiplayerPreferences.JobPreference> jobPreferences = new List<MultiplayerPreferences.JobPreference>();

            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<FailedSubInfo> FailedCampaignSubs = new List<FailedSubInfo>();
        public List<FailedSubInfo> FailedOwnedSubs = new List<FailedSubInfo>();

        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<GUITextBlock>();
                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<SubmarineInfo> visibilityMenuOrder = new List<SubmarineInfo>();
        public const string SeparatistsIconUserData = "separatistsIcon";
        public const string CoalitionIconUserData = "coalitionIcon";

        private void CreateSubmarineVisibilityMenu()
        {
            var messageBox = new GUIMessageBox(TextManager.Get("SubmarineVisibility"), "",
                buttons: Array.Empty<LocalizedString>(),
                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<DisembarkPerkPrefab> team1IncompatiblePerks, string team2SubName, ImmutableArray<DisembarkPerkPrefab> team2IncompatiblePerks)
        {
            DateTime startTime = DateTime.UtcNow;
            TimeSpan differenceFromStart = waitUntilTime.ToUtcValue() - startTime;

            StopWaitingForStartRound();
            GUIMessageBox.MessageBoxes.OfType<GUIMessageBox>().ForEachMod(static mod =>
            {
                if (mod.UserData is PleaseWaitPopupUserData)
                {
                    mod.Close();
                }
            });

            var messageBox = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("startgamewarning"), Array.Empty<LocalizedString>(), 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<GUIMessageBox>().ForEachMod(static mod =>
            {
                if (mod.UserData is RoundStartWarningBoxUserData)
                {
                    mod.Close();
                }
            });
        }
    }
}
