#nullable enable using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using Barotrauma.Eos; using Barotrauma.Extensions; using Barotrauma.Networking; using Barotrauma.Sounds; using Barotrauma.Steam; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using OpenAL; namespace Barotrauma { sealed class SettingsMenu { public static SettingsMenu? Instance { get; private set; } public enum Tab { Graphics, AudioAndVC, Controls, Gameplay, Mods } public Tab CurrentTab { get; private set; } private GameSettings.Config unsavedConfig; public readonly GUIFrame mainFrame; public readonly GUILayoutGroup tabber; public readonly GUIFrame contentFrame; private readonly GUILayoutGroup bottom; public readonly WorkshopMenu WorkshopMenu; private static readonly ImmutableHashSet LegacyInputTypes = new List() { InputType.Chat, InputType.RadioChat, InputType.LocalVoice, InputType.RadioVoice, }.ToImmutableHashSet(); public static SettingsMenu Create(RectTransform mainParent) { Instance?.Close(); Instance = new SettingsMenu(mainParent); return Instance; } private SettingsMenu(RectTransform mainParent, GameSettings.Config setConfig = default) { unsavedConfig = GameSettings.CurrentConfig; mainFrame = new GUIFrame(new RectTransform(Vector2.One, mainParent)); var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, mainFrame.RectTransform, Anchor.Center, Pivot.Center), isHorizontal: false, childAnchor: Anchor.TopRight); new GUITextBlock(new RectTransform((1.0f, 0.07f), mainLayout.RectTransform), TextManager.Get("Settings"), font: GUIStyle.LargeFont); var tabberAndContentLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.86f), mainLayout.RectTransform), isHorizontal: true); void tabberPadding() => new GUIFrame(new RectTransform((0.01f, 1.0f), tabberAndContentLayout.RectTransform), style: null); tabberPadding(); tabber = new GUILayoutGroup(new RectTransform((0.06f, 1.0f), tabberAndContentLayout.RectTransform), isHorizontal: false) { AbsoluteSpacing = GUI.IntScale(5f) }; tabberPadding(); tabContents = new Dictionary(); contentFrame = new GUIFrame(new RectTransform((0.92f, 1.0f), tabberAndContentLayout.RectTransform), style: "InnerFrame"); bottom = new GUILayoutGroup(new RectTransform((contentFrame.RectTransform.RelativeSize.X, 0.04f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f }; CreateGraphicsTab(); CreateAudioAndVCTab(); CreateControlsTab(); CreateGameplayTab(); CreateModsTab(out WorkshopMenu); CreateBottomButtons(); SelectTab(Tab.Graphics); tabber.Recalculate(); } private void SwitchContent(GUIFrame newContent) { contentFrame.Children.ForEach(c => c.Visible = false); newContent.Visible = true; } public readonly Dictionary tabContents; public void SelectTab(Tab tab) { if (tab == Tab.AudioAndVC && CurrentDeviceMismatchesDisplayed()) { CreateAudioAndVCTab(refresh: true); } CurrentTab = tab; SwitchContent(tabContents[tab].Content); tabber.Children.ForEach(c => { if (c is GUIButton btn) { btn.Selected = btn == tabContents[tab].Button; } }); } private void AddButtonToTabber(Tab tab, GUIFrame content) { var button = new GUIButton(new RectTransform(Vector2.One, tabber.RectTransform, Anchor.TopLeft, Pivot.TopLeft, scaleBasis: ScaleBasis.Smallest), "", style: $"SettingsMenuTab.{tab}") { ToolTip = TextManager.Get($"SettingsTab.{tab}"), OnClicked = (b, _) => { SelectTab(tab); return false; } }; button.RectTransform.MaxSize = RectTransform.MaxPoint; button.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint); tabContents.Add(tab, (button, content)); } private GUIFrame CreateNewContentFrame(Tab tab) { if (tabContents.TryGetValue(tab, out (GUIButton Button, GUIFrame Content) tabContent)) { return tabContent.Content; } var content = new GUIFrame(new RectTransform(Vector2.One * 0.95f, contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null); AddButtonToTabber(tab, content); return content; } public static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUIFrame parent, bool split = false) { GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true); GUILayoutGroup left = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); var centerFrame = new GUIFrame(new RectTransform((0.025f, 1.0f), layout.RectTransform), style: null); if (split) { new GUICustomComponent(new RectTransform(Vector2.One, centerFrame.RectTransform), onDraw: (sb, c) => { sb.DrawLine((c.Rect.Center.X, c.Rect.Top),(c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f); }); } GUILayoutGroup right = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false); return (left, right); } public static GUILayoutGroup CreateCenterLayout(GUIFrame parent) { return new GUILayoutGroup(new RectTransform((0.5f, 1.0f), parent.RectTransform, Anchor.TopCenter, Pivot.TopCenter)) { ChildAnchor = Anchor.TopCenter }; } public static RectTransform NewItemRectT(GUILayoutGroup parent) => new RectTransform((1.0f, 0.06f), parent.RectTransform, Anchor.CenterLeft); public static void Spacer(GUILayoutGroup parent, float height = 0.03f) { new GUIFrame(new RectTransform((1.0f, height), parent.RectTransform, Anchor.CenterLeft), style: null); } public static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font) { return new GUITextBlock(NewItemRectT(parent), str, font: font); } public static void DropdownEnum(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, T currentValue, Action setter) where T : Enum => Dropdown(parent, textFunc, tooltipFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter); public static GUIDropDown Dropdown(GUILayoutGroup parent, Func textFunc, Func? tooltipFunc, IReadOnlyList values, T currentValue, Action setter) { var dropdown = new GUIDropDown(NewItemRectT(parent), elementCount: values.Count); values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null)); int childIndex = values.IndexOf(currentValue); dropdown.Select(childIndex); dropdown.ListBox.ForceLayoutRecalculation(); dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex)); dropdown.OnSelected = (dd, obj) => { setter((T)obj); return true; }; return dropdown; } public static (GUIScrollBar slider, GUITextBlock label) Slider(GUILayoutGroup parent, Vector2 range, int steps, Func labelFunc, float currentValue, Action setter, LocalizedString? tooltip = null) { var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true); var slider = new GUIScrollBar(new RectTransform((0.72f, 1.0f), layout.RectTransform), style: "GUISlider") { Range = range, BarScrollValue = currentValue, Step = 1.0f / (float)(steps - 1), BarSize = 1.0f / steps }; if (tooltip != null) { slider.ToolTip = tooltip; } var label = new GUITextBlock(new RectTransform((0.28f, 1.0f), layout.RectTransform), labelFunc(currentValue), wrap: false, textAlignment: Alignment.Center); slider.OnMoved = (sb, val) => { label.Text = labelFunc(sb.BarScrollValue); setter(sb.BarScrollValue); return true; }; return (slider, label); } public static GUITickBox Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action setter) { return new GUITickBox(NewItemRectT(parent), label) { Selected = currentValue, ToolTip = tooltip, OnSelected = (tb) => { setter(tb.Selected); return true; } }; } public string Percentage(float v) => ToolBox.GetFormattedPercentage(v); public static int Round(float v) => MathUtils.RoundToInt(v); private void CreateGraphicsTab() { GUIFrame content = CreateNewContentFrame(Tab.Graphics); var (left, right) = CreateSidebars(content); List<(int Width, int Height)> supportedResolutions = GameMain.GraphicsDeviceManager.GraphicsDevice.Adapter.SupportedDisplayModes .Where(m => m.Format == SurfaceFormat.Color) .Select(m => (m.Width, m.Height)) .Where(m => m.Width >= GameSettings.Config.GraphicsSettings.MinSupportedResolution.X && m.Height >= GameSettings.Config.GraphicsSettings.MinSupportedResolution.Y) .ToList(); var currentResolution = (unsavedConfig.Graphics.Width, unsavedConfig.Graphics.Height); if (!supportedResolutions.Contains(currentResolution)) { supportedResolutions.Add(currentResolution); } Label(left, TextManager.Get("Resolution"), GUIStyle.SubHeadingFont); Dropdown(left, (m) => $"{m.Width}x{m.Height}", null, supportedResolutions, currentResolution, (res) => { unsavedConfig.Graphics.Width = res.Width; unsavedConfig.Graphics.Height = res.Height; }); Spacer(left); Label(left, TextManager.Get("DisplayMode"), GUIStyle.SubHeadingFont); DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, v => unsavedConfig.Graphics.DisplayMode = v); Spacer(left); var displayLabel = Label(left, TextManager.Get("TargetDisplay"), GUIStyle.SubHeadingFont); displayLabel.ToolTip = TextManager.Get("TargetDisplay.Tooltip"); Dropdown(left, m => TextManager.GetWithVariables(m == 0 ? "PrimaryDisplayFormat" : "SecondaryDisplayFormat", ("[num]", m.ToString()), ("[name]", Display.GetDisplayName(m))), null, Enumerable.Range(0, Display.GetNumberOfDisplays()).ToArray(), unsavedConfig.Graphics.Display, v => unsavedConfig.Graphics.Display = v); Spacer(left); Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, v => unsavedConfig.Graphics.VSync = v); Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, v => unsavedConfig.Graphics.CompressTextures = v); Spacer(right); Label(right, TextManager.Get("LOSEffect"), GUIStyle.SubHeadingFont); DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, v => unsavedConfig.Graphics.LosMode = v); Spacer(right); Label(right, TextManager.Get("LightMapScale"), GUIStyle.SubHeadingFont); Slider(right, (0.5f, 1.0f), 11, v => TextManager.GetWithVariable("percentageformat", "[value]", Round(v * 100).ToString()).Value, unsavedConfig.Graphics.LightMapScale, v => unsavedConfig.Graphics.LightMapScale = v, TextManager.Get("LightMapScaleTooltip")); Spacer(right); Label(right, TextManager.Get("VisibleLightLimit"), GUIStyle.SubHeadingFont); Slider(right, (10, 510), 21, v => v > 500 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit, v => unsavedConfig.Graphics.VisibleLightLimit = v > 500 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip")); Spacer(right); Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, v => unsavedConfig.Graphics.RadialDistortion = v); Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, v => unsavedConfig.Graphics.ChromaticAberration = v); Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont); Slider(right, (100, 1500), 15, v => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, v => unsavedConfig.Graphics.ParticleLimit = Round(v)); Spacer(right); } private static string TrimAudioDeviceName(string name) { if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } string[] prefixes = { "OpenAL Soft on " }; foreach (string prefix in prefixes) { if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return name.Remove(0, prefix.Length); } } return name; } private static int HandleAlErrors(string message) { int alcError = Alc.GetError(IntPtr.Zero); if (alcError != Alc.NoError) { DebugConsole.ThrowError($"{message}: ALC error {Alc.GetErrorString(alcError)}"); return alcError; } int alError = Al.GetError(); if (alError != Al.NoError) { DebugConsole.ThrowError($"{message}: AL error {Al.GetErrorString(alError)}"); return alError; } return Al.NoError; } private static void GetAudioDevices(int listSpecifier, int defaultSpecifier, out IReadOnlyList list, ref string current) { list = Array.Empty(); var retVal = Alc.GetStringList(IntPtr.Zero, listSpecifier).ToList(); if (HandleAlErrors("Alc.GetStringList failed") != Al.NoError) { return; } list = retVal; if (string.IsNullOrEmpty(current)) { current = Alc.GetString(IntPtr.Zero, defaultSpecifier); if (HandleAlErrors("Alc.GetString failed") != Al.NoError) { return; } } string currentVal = current; if (list.Any() && !list.Any(n => n.Equals(currentVal, StringComparison.OrdinalIgnoreCase))) { current = list[0]; } } private static bool IsCurrentDevice(string savedDeviceName, int deviceType) { try { string currentDevice = Alc.GetString(IntPtr.Zero, deviceType); if (string.IsNullOrEmpty(savedDeviceName) || string.IsNullOrEmpty(currentDevice)) { return false; } return currentDevice.Equals(savedDeviceName, StringComparison.OrdinalIgnoreCase); } catch (Exception ex) { Console.WriteLine($"Error checking output device name: {ex.Message}"); return false; } } private static bool CurrentDeviceMismatchesDisplayed() { return !IsCurrentDevice(GameSettings.CurrentConfig.Audio.VoiceCaptureDevice, Alc.CaptureDefaultDeviceSpecifier) || !IsCurrentDevice(GameSettings.CurrentConfig.Audio.AudioOutputDevice, Alc.DefaultDeviceSpecifier); } public void CreateAudioAndVCTab(bool refresh = false) { if (GameMain.Client == null && (refresh || VoipCapture.Instance == null)) { string currDevice = unsavedConfig.Audio.VoiceCaptureDevice; GetAudioDevices(Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, out var deviceList, ref currDevice); if (deviceList.Any()) { if (VoipCapture.Instance is VoipCapture currentCaptureInstance) { currentCaptureInstance.Dispose(); } VoipCapture.Create(unsavedConfig.Audio.VoiceCaptureDevice); } if (VoipCapture.Instance == null) { unsavedConfig.Audio.VoiceSetting = VoiceMode.Disabled; } } GUIFrame content = CreateNewContentFrame(Tab.AudioAndVC); if (refresh) { content.ClearChildren(); } var (audio, voiceChat) = CreateSidebars(content, split: true); static void audioDeviceElement( GUILayoutGroup parent, Action setter, int listSpecifier, int defaultSpecifier, ref string currentDevice) { #if OSX //At the time of writing there are no OpenAL implementations //on macOS that return the list of available devices, or //allow selecting any other than the default one. I'm not //about to write my own OpenAL implementation to fix this //so here's a workaround instead, just a label that shows the //name of the current device. var deviceNameContainerElement = new GUIFrame(NewItemRectT(parent), style: "GUITextBoxNoIcon"); var deviceNameElement = new GUITextBlock(new RectTransform(Vector2.One, deviceNameContainerElement.RectTransform), currentDevice, textAlignment: Alignment.CenterLeft); new GUICustomComponent(new RectTransform(Vector2.Zero, deviceNameElement.RectTransform), onUpdate: (deltaTime, component) => { deviceNameElement.Text = Alc.GetString(IntPtr.Zero, listSpecifier); }); #else GetAudioDevices(listSpecifier, defaultSpecifier, out var devices, ref currentDevice); Dropdown(parent, v => TrimAudioDeviceName(v), null, devices, currentDevice, setter); #endif } Label(audio, TextManager.Get("AudioOutputDevice"), GUIStyle.SubHeadingFont); string currentOutputDevice = unsavedConfig.Audio.AudioOutputDevice; audioDeviceElement(audio, v => unsavedConfig.Audio.AudioOutputDevice = v, Alc.OutputDevicesSpecifier, Alc.DefaultDeviceSpecifier, ref currentOutputDevice); new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), audio.RectTransform), text: TextManager.Get("RefreshAudioDevices"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("RefreshAudioDevicesToolTip"), OnClicked = (btn, obj) => { CreateAudioAndVCTab(refresh: true); return true; } }; Spacer(audio); Label(audio, TextManager.Get("SoundVolume"), GUIStyle.SubHeadingFont); Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, v => { unsavedConfig.Audio.SoundVolume = v; GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryDefault, v); GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, v); }); Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont); Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, v => { unsavedConfig.Audio.MusicVolume = v; GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryMusic, v); }); Label(audio, TextManager.Get("UiSoundVolume"), GUIStyle.SubHeadingFont); Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, v => { unsavedConfig.Audio.UiVolume = v; GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryUi, v); }); Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, v => unsavedConfig.Audio.MuteOnFocusLost = v); Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, v => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v); Spacer(audio); Label(audio, TextManager.Get("VoiceChatVolume"), GUIStyle.SubHeadingFont); Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, v => { unsavedConfig.Audio.VoiceChatVolume = v; GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryVoip, v); }); Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, v => unsavedConfig.Audio.UseDirectionalVoiceChat = v); Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, v => unsavedConfig.Audio.VoipAttenuationEnabled = v); Label(voiceChat, TextManager.Get("AudioInputDevice"), GUIStyle.SubHeadingFont); string currentInputDevice = unsavedConfig.Audio.VoiceCaptureDevice; audioDeviceElement(voiceChat, v => unsavedConfig.Audio.VoiceCaptureDevice = v, Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, ref currentInputDevice); new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), voiceChat.RectTransform), text: TextManager.Get("RefreshAudioDevices"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("RefreshAudioDevicesToolTip"), OnClicked = (btn, obj) => { CreateAudioAndVCTab(refresh: true); return true; } }; #if OSX Spacer(voiceChat, 0.003f); // On macOS, microphone permission can apparently sometimes end up in a broken state when the app binary changes (eg. after a Steam update). // The device seems to be there, but won't receive anything, even if the mic permission is fine. // This button lets the user reset it and reboot the game, so the mic permission check will be retriggered on next run. new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), voiceChat.RectTransform), text: TextManager.Get("MacResetMicPermissions"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MacResetMicPermissionsToolTip"), OnClicked = (btn, obj) => { var confirmBox = new GUIMessageBox( TextManager.Get("MacResetMicPermissions"), TextManager.Get("MacResetMicPermissionsConfirm"), [TextManager.Get("OK"), TextManager.Get("Cancel")]); confirmBox.Buttons[0].OnClicked = (_, _) => { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = "tccutil", Arguments = "reset Microphone com.FakeFish.Barotrauma", UseShellExecute = false }); } catch (Exception e) { DebugConsole.NewMessage($"Failed to reset microphone permission: {e.Message}", Color.Orange); } GameMain.Instance.Exit(); confirmBox.Close(); return true; }; confirmBox.Buttons[1].OnClicked = confirmBox.Close; return true; } }; #endif Spacer(voiceChat); Label(voiceChat, TextManager.Get("VCInputMode"), GUIStyle.SubHeadingFont); DropdownEnum(voiceChat, v => TextManager.Get($"VoiceMode.{v}"), v => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, v => unsavedConfig.Audio.VoiceSetting = v); Spacer(voiceChat); var noiseGateThresholdLabel = Label(voiceChat, TextManager.Get("NoiseGateThreshold"), GUIStyle.SubHeadingFont); var dbMeter = new GUIProgressBar(NewItemRectT(voiceChat), 0.0f, Color.Lime); dbMeter.ProgressGetter = () => { if (VoipCapture.Instance == null) { return 0.0f; } dbMeter.Color = unsavedConfig.Audio.VoiceSetting switch { VoiceMode.Activity => VoipCapture.Instance.LastdB > unsavedConfig.Audio.NoiseGateThreshold ? GUIStyle.Green : GUIStyle.Orange, VoiceMode.PushToTalk => GUIStyle.Green, VoiceMode.Disabled => Color.LightGray }; float scrollVal = double.IsNegativeInfinity(VoipCapture.Instance.LastdB) ? 0.0f : ((float)VoipCapture.Instance.LastdB + 100.0f) / 100.0f; return scrollVal * scrollVal; }; var noiseGateSlider = new GUIScrollBar(new RectTransform(Vector2.One, dbMeter.RectTransform, Anchor.Center), color: Color.White, style: "GUISlider", barSize: 0.03f); noiseGateSlider.Frame.Visible = false; noiseGateSlider.Step = 0.01f; noiseGateSlider.Range = new Vector2(-100.0f, 0.0f); noiseGateSlider.BarScroll = MathUtils.InverseLerp(-100.0f, 0.0f, unsavedConfig.Audio.NoiseGateThreshold); noiseGateSlider.BarScroll *= noiseGateSlider.BarScroll; noiseGateSlider.OnMoved = (scrollBar, barScroll) => { unsavedConfig.Audio.NoiseGateThreshold = MathHelper.Lerp(-100.0f, 0.0f, (float)Math.Sqrt(scrollBar.BarScroll)); return true; }; new GUICustomComponent(new RectTransform(Vector2.Zero, voiceChat.RectTransform), onUpdate: (deltaTime, component) => { noiseGateThresholdLabel.Visible = unsavedConfig.Audio.VoiceSetting == VoiceMode.Activity; noiseGateSlider.Visible = unsavedConfig.Audio.VoiceSetting == VoiceMode.Activity; }); Spacer(voiceChat); Label(voiceChat, TextManager.Get("MicrophoneVolume"), GUIStyle.SubHeadingFont); Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, v => unsavedConfig.Audio.MicrophoneVolume = v); Spacer(voiceChat); Label(voiceChat, TextManager.Get("CutoffPrevention"), GUIStyle.SubHeadingFont); Slider(voiceChat, (0, 500), 26, v => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, v => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip")); } private readonly Dictionary> inputButtonValueNameGetters = new Dictionary>(); private bool inputBoxSelectedThisFrame = false; private void CreateControlsTab() { GUIFrame content = CreateNewContentFrame(Tab.Controls); GUILayoutGroup layout = CreateCenterLayout(content); Label(layout, TextManager.Get("AimAssist"), GUIStyle.SubHeadingFont); var aimAssistSlider = Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, v => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip")); Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, v => unsavedConfig.EnableMouseLook = v); Spacer(layout); GUIListBox keyMapList = new GUIListBox(new RectTransform((2.0f, 0.7f), layout.RectTransform)) { CanBeFocused = false, OnSelected = (_, __) => false }; Spacer(layout); GUILayoutGroup createInputRowLayout() => new GUILayoutGroup(new RectTransform((1.0f, 0.1f), keyMapList.Content.RectTransform), isHorizontal: true); inputButtonValueNameGetters.Clear(); Action? currentSetter = null; void addInputToRow(GUILayoutGroup currRow, LocalizedString labelText, Func valueNameGetter, Action valueSetter, bool isLegacyBind = false) { var inputFrame = new GUIFrame(new RectTransform((0.5f, 1.0f), currRow.RectTransform), style: null); if (isLegacyBind) { labelText = TextManager.GetWithVariable("legacyitemformat", "[name]", labelText); } var label = new GUITextBlock(new RectTransform((0.6f, 1.0f), inputFrame.RectTransform), labelText, font: GUIStyle.SmallFont) {ForceUpperCase = ForceUpperCase.Yes}; var inputBox = new GUIButton( new RectTransform((0.4f, 1.0f), inputFrame.RectTransform, Anchor.TopRight, Pivot.TopRight), valueNameGetter(), style: "GUITextBoxNoIcon") { OnClicked = (btn, obj) => { inputButtonValueNameGetters.Keys.ForEach(b => { if (b != btn) { b.Selected = false; } }); bool willBeSelected = !btn.Selected; if (willBeSelected) { inputBoxSelectedThisFrame = true; currentSetter = v => { valueSetter(v); btn.Text = valueNameGetter(); }; } btn.Selected = willBeSelected; return true; } }; if (isLegacyBind) { label.TextColor = Color.Lerp(label.TextColor, label.DisabledTextColor, 0.5f); inputBox.Color = Color.Lerp(inputBox.Color, inputBox.DisabledColor, 0.5f); inputBox.TextColor = Color.Lerp(inputBox.TextColor, label.DisabledTextColor, 0.5f); } inputButtonValueNameGetters.Add(inputBox, valueNameGetter); } var inputListener = new GUICustomComponent(new RectTransform(Vector2.Zero, layout.RectTransform), onUpdate: (deltaTime, component) => { if (currentSetter is null) { return; } if (PlayerInput.PrimaryMouseButtonClicked() && inputBoxSelectedThisFrame) { inputBoxSelectedThisFrame = false; return; } void clearSetter() { currentSetter = null; inputButtonValueNameGetters.Keys.ForEach(b => b.Selected = false); } void callSetter(KeyOrMouse v) { currentSetter?.Invoke(v); clearSetter(); } var pressedKeys = PlayerInput.GetKeyboardState.GetPressedKeys(); if (pressedKeys?.Any() ?? false) { if (pressedKeys.Contains(Keys.Escape)) { clearSetter(); } else { callSetter(pressedKeys.First()); } } else if (PlayerInput.PrimaryMouseButtonClicked() && (GUI.MouseOn == null || !(GUI.MouseOn is GUIButton) || GUI.MouseOn.IsChildOf(keyMapList.Content))) { callSetter(MouseButton.PrimaryMouse); } else if (PlayerInput.SecondaryMouseButtonClicked()) { callSetter(MouseButton.SecondaryMouse); } else if (PlayerInput.MidButtonClicked()) { callSetter(MouseButton.MiddleMouse); } else if (PlayerInput.Mouse4ButtonClicked()) { callSetter(MouseButton.MouseButton4); } else if (PlayerInput.Mouse5ButtonClicked()) { callSetter(MouseButton.MouseButton5); } else if (PlayerInput.MouseWheelUpClicked()) { callSetter(MouseButton.MouseWheelUp); } else if (PlayerInput.MouseWheelDownClicked()) { callSetter(MouseButton.MouseWheelDown); } }); InputType[] inputTypes = (InputType[])Enum.GetValues(typeof(InputType)); InputType[][] inputTypeColumns = { inputTypes.Take(inputTypes.Length - (inputTypes.Length / 2)).ToArray(), inputTypes.TakeLast(inputTypes.Length / 2).ToArray() }; for (int i = 0; i < inputTypes.Length; i+=2) { var currRow = createInputRowLayout(); for (int j = 0; j < 2; j++) { var column = inputTypeColumns[j]; if (i / 2 >= column.Length) { break; } var input = column[i / 2]; addInputToRow( currRow, TextManager.Get($"InputType.{input}"), () => unsavedConfig.KeyMap.Bindings[input].Name, v => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v), LegacyInputTypes.Contains(input)); } } for (int i = 0; i < unsavedConfig.InventoryKeyMap.Bindings.Length; i += 2) { var currRow = createInputRowLayout(); for (int j = 0; j < 2; j++) { int currIndex = i + j; if (currIndex >= unsavedConfig.InventoryKeyMap.Bindings.Length) { break; } var input = unsavedConfig.InventoryKeyMap.Bindings[currIndex]; addInputToRow( currRow, TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex + 1).ToString(CultureInfo.InvariantCulture)), () => unsavedConfig.InventoryKeyMap.Bindings[currIndex].Name, v => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v)); } } GUILayoutGroup resetControlsHolder = new GUILayoutGroup(new RectTransform((1.75f, 0.1f), layout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center) { RelativeSpacing = 0.1f }; var defaultBindingsButton = new GUIButton(new RectTransform(new Vector2(0.45f, 1.0f), resetControlsHolder.RectTransform), TextManager.Get("Reset"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("SetDefaultBindingsTooltip"), OnClicked = (_, userdata) => { unsavedConfig.InventoryKeyMap = GameSettings.Config.InventoryKeyMapping.GetDefault(); unsavedConfig.KeyMap = GameSettings.Config.KeyMapping.GetDefault(); aimAssistSlider.slider.BarScrollValue = GameSettings.Config.DefaultAimAssist; aimAssistSlider.label.Text = Percentage(GameSettings.Config.DefaultAimAssist); foreach (var btn in inputButtonValueNameGetters.Keys) { btn.Text = inputButtonValueNameGetters[btn](); } Instance?.SelectTab(Tab.Controls); return true; } }; } private void CreateGameplayTab() { GUIFrame content = CreateNewContentFrame(Tab.Gameplay); var (leftColumn, rightColumn) = CreateSidebars(content, split: true); var languages = TextManager.AvailableLanguages .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier()) .ToArray(); Label(leftColumn, TextManager.Get("Language"), GUIStyle.SubHeadingFont); Dropdown(leftColumn, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v); Spacer(leftColumn); Tickbox(leftColumn, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v); Spacer(leftColumn); Tickbox(leftColumn, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v); var resetInGameHintsButton = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), leftColumn.RectTransform), TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall") { OnClicked = (button, o) => { var msgBox = new GUIMessageBox(TextManager.Get("ResetInGameHints"), TextManager.Get("ResetInGameHintsTooltip"), buttons: new[] { TextManager.Get("Yes"), TextManager.Get("No") }); msgBox.Buttons[0].OnClicked = (guiButton, o1) => { IgnoredHints.Instance.Clear(); msgBox.Close(); return false; }; msgBox.Buttons[1].OnClicked = msgBox.Close; return false; } }; Spacer(leftColumn); Tickbox(leftColumn, TextManager.Get("ChatSpeechBubbles"), TextManager.Get("ChatSpeechBubbles.Tooltip"), unsavedConfig.ChatSpeechBubbles, v => unsavedConfig.ChatSpeechBubbles = v); Label(leftColumn, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont); DropdownEnum(leftColumn, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v); Spacer(leftColumn); Label(leftColumn, TextManager.Get("InteractionLabels"), GUIStyle.SubHeadingFont); DropdownEnum(leftColumn, v => TextManager.Get($"InteractionLabels.{v}"), null, unsavedConfig.InteractionLabelDisplayMode, v => unsavedConfig.InteractionLabelDisplayMode = v); Label(rightColumn, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont); // Restricts the max scale to 110% on 16:9, and to 100% on 4:3. // Higher scales are allowed for wide aspect ratios, up to 125%. //float scalar = MathUtils.InverseLerp(0f, 1.0f, 0.4f - GUI.AspectRatioDifference); //float maxScale = MathHelper.Lerp(1.0f, 1.25f, scalar); const float maxScale = 1.25f; Slider(rightColumn, (0.75f, maxScale), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v); Label(rightColumn, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont); Slider(rightColumn, (0.75f, maxScale), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v); Label(rightColumn, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont); Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v); Spacer(rightColumn); var resetSpamListFilter = new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform), TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall") { OnClicked = static (_, _) => { GUI.AskForConfirmation( header: TextManager.Get("clearserverlistfilters"), body: TextManager.Get("clearserverlistfiltersconfirmation"), onConfirm: SpamServerFilters.ClearLocalSpamFilter); return true; } }; Spacer(rightColumn); #if !OSX Spacer(rightColumn); var statisticsTickBox = new GUITickBox(NewItemRectT(rightColumn), TextManager.Get("statisticsconsenttickbox")) { OnSelected = tickBox => { GUIMessageBox? loadingBox = null; if (!tickBox.Selected) { loadingBox = GUIMessageBox.CreateLoadingBox(TextManager.Get("PleaseWait")); } GameAnalyticsManager.SetConsent( tickBox.Selected ? GameAnalyticsManager.Consent.Ask : GameAnalyticsManager.Consent.No, onAnswerSent: () => loadingBox?.Close()); return false; } }; #if DEBUG statisticsTickBox.Enabled = false; #endif void updateGATickBoxToolTip() => statisticsTickBox.ToolTip = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}"); updateGATickBoxToolTip(); var cachedConsent = GameAnalyticsManager.Consent.Unknown; var statisticsTickBoxUpdater = new GUICustomComponent( new RectTransform(Vector2.Zero, statisticsTickBox.RectTransform), onUpdate: (deltaTime, component) => { bool shouldTickBoxBeSelected = GameAnalyticsManager.UserConsented == GameAnalyticsManager.Consent.Yes; bool shouldUpdateTickBoxState = cachedConsent != GameAnalyticsManager.UserConsented || statisticsTickBox.Selected != shouldTickBoxBeSelected; if (!shouldUpdateTickBoxState) { return; } updateGATickBoxToolTip(); cachedConsent = GameAnalyticsManager.UserConsented; GUITickBox.OnSelectedHandler prevHandler = statisticsTickBox.OnSelected; statisticsTickBox.OnSelected = null; statisticsTickBox.Selected = shouldTickBoxBeSelected; statisticsTickBox.OnSelected = prevHandler; statisticsTickBox.Enabled &= GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error; }); #endif //Steam version supports hosting/joining servers using EOS networking if (SteamManager.IsInitialized) { bool shouldCrossplayBeEnabled = unsavedConfig.CrossplayChoice is Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled; var crossplayTickBox = Tickbox(rightColumn, TextManager.Get("EosAllowCrossplay"), TextManager.Get("EosAllowCrossplayTooltip"), shouldCrossplayBeEnabled, v => { unsavedConfig.CrossplayChoice = v ? Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled : Eos.EosSteamPrimaryLogin.CrossplayChoice.Disabled; }); if (GameMain.NetworkMember != null) { crossplayTickBox.Enabled = false; crossplayTickBox.ToolTip = TextManager.Get("CantAccessEOSSettingsInMP"); } } } private void CreateModsTab(out WorkshopMenu workshopMenu) { GUIFrame content = CreateNewContentFrame(Tab.Mods); content.RectTransform.RelativeSize = Vector2.One; workshopMenu = Screen.Selected is MainMenuScreen ? (WorkshopMenu)new MutableWorkshopMenu(content) : (WorkshopMenu)new ImmutableWorkshopMenu(content); GameMain.MainMenuScreen.ResetModUpdateButton(); } private void CreateBottomButtons() { new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: TextManager.Get("Cancel")) { OnClicked = (btn, obj) => { // reset any modified audio settings to current config GameMain.SoundManager?.ApplySettings(); Close(); return false; } }; new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: TextManager.Get("applysettingsbutton")) { OnClicked = (btn, obj) => { ApplyInstalledModChanges(); mainFrame.Flash(color: GUIStyle.Green); return false; }, OnAddedToGUIUpdateList = (GUIComponent component) => { component.Enabled = CurrentTab != Tab.Mods || (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu && mutableWorkshopMenu.CurrentTab == MutableWorkshopMenu.Tab.InstalledMods && !mutableWorkshopMenu.ViewingItemDetails); } }; } public void ApplyInstalledModChanges() { EosSteamPrimaryLogin.HandleCrossplayChoiceChange(unsavedConfig.CrossplayChoice); GameSettings.SetCurrentConfig(unsavedConfig); if (WorkshopMenu is MutableWorkshopMenu { CurrentTab: MutableWorkshopMenu.Tab.InstalledMods } mutableWorkshopMenu) { mutableWorkshopMenu.Apply(); } GameSettings.SaveCurrentConfig(); } public void Close() { if (GameMain.Client is null || GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled) { VoipCapture.Instance?.Dispose(); } mainFrame.Parent.RemoveChild(mainFrame); if (Instance == this) { Instance = null; } GUI.SettingsMenuOpen = false; } } }