Client LuaCsForBarotrauma
SettingsMenu.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Collections.Immutable;
5 using System.Globalization;
6 using System.Linq;
7 using Barotrauma.Eos;
10 using Barotrauma.Sounds;
11 using Barotrauma.Steam;
12 using Microsoft.Xna.Framework;
13 using Microsoft.Xna.Framework.Graphics;
14 using Microsoft.Xna.Framework.Input;
15 using OpenAL;
16 
17 namespace Barotrauma
18 {
19  sealed class SettingsMenu
20  {
21  public static SettingsMenu? Instance { get; private set; }
22 
23  public enum Tab
24  {
25  Graphics,
26  AudioAndVC,
27  Controls,
28  Gameplay,
29  Mods
30  }
31 
32  public Tab CurrentTab { get; private set; }
33 
34  private GameSettings.Config unsavedConfig;
35 
36  private readonly GUIFrame mainFrame;
37 
38  private readonly GUILayoutGroup tabber;
39  private readonly GUIFrame contentFrame;
40  private readonly GUILayoutGroup bottom;
41 
42  public readonly WorkshopMenu WorkshopMenu;
43 
44  private static readonly ImmutableHashSet<InputType> LegacyInputTypes = new List<InputType>()
45  {
46  InputType.Chat,
47  InputType.RadioChat,
48  InputType.LocalVoice,
49  InputType.RadioVoice,
50  }.ToImmutableHashSet();
51 
52  public static SettingsMenu Create(RectTransform mainParent)
53  {
54  Instance?.Close();
55  Instance = new SettingsMenu(mainParent);
56  return Instance;
57  }
58 
59  private SettingsMenu(RectTransform mainParent, GameSettings.Config setConfig = default)
60  {
61  unsavedConfig = GameSettings.CurrentConfig;
62 
63  mainFrame = new GUIFrame(new RectTransform(Vector2.One, mainParent));
64 
65  var mainLayout = new GUILayoutGroup(new RectTransform(Vector2.One * 0.95f, mainFrame.RectTransform, Anchor.Center, Pivot.Center),
66  isHorizontal: false, childAnchor: Anchor.TopRight);
67 
68  new GUITextBlock(new RectTransform((1.0f, 0.07f), mainLayout.RectTransform), TextManager.Get("Settings"),
69  font: GUIStyle.LargeFont);
70 
71  var tabberAndContentLayout = new GUILayoutGroup(new RectTransform((1.0f, 0.86f), mainLayout.RectTransform),
72  isHorizontal: true);
73 
74  void tabberPadding()
75  => new GUIFrame(new RectTransform((0.01f, 1.0f), tabberAndContentLayout.RectTransform), style: null);
76 
77  tabberPadding();
78  tabber = new GUILayoutGroup(new RectTransform((0.06f, 1.0f), tabberAndContentLayout.RectTransform), isHorizontal: false) { AbsoluteSpacing = GUI.IntScale(5f) };
79  tabberPadding();
80  tabContents = new Dictionary<Tab, (GUIButton Button, GUIFrame Content)>();
81 
82  contentFrame = new GUIFrame(new RectTransform((0.92f, 1.0f), tabberAndContentLayout.RectTransform),
83  style: "InnerFrame");
84 
85  bottom = new GUILayoutGroup(new RectTransform((contentFrame.RectTransform.RelativeSize.X, 0.04f), mainLayout.RectTransform), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f };
86 
87  CreateGraphicsTab();
88  CreateAudioAndVCTab();
89  CreateControlsTab();
90  CreateGameplayTab();
91  CreateModsTab(out WorkshopMenu);
92 
93  CreateBottomButtons();
94 
95  SelectTab(Tab.Graphics);
96 
97  tabber.Recalculate();
98  }
99 
100  private void SwitchContent(GUIFrame newContent)
101  {
102  contentFrame.Children.ForEach(c => c.Visible = false);
103  newContent.Visible = true;
104  }
105 
106  private readonly Dictionary<Tab, (GUIButton Button, GUIFrame Content)> tabContents;
107 
108  public void SelectTab(Tab tab)
109  {
110  CurrentTab = tab;
111  SwitchContent(tabContents[tab].Content);
112  tabber.Children.ForEach(c =>
113  {
114  if (c is GUIButton btn) { btn.Selected = btn == tabContents[tab].Button; }
115  });
116  }
117 
118  private void AddButtonToTabber(Tab tab, GUIFrame content)
119  {
120  var button = new GUIButton(new RectTransform(Vector2.One, tabber.RectTransform, Anchor.TopLeft, Pivot.TopLeft, scaleBasis: ScaleBasis.Smallest), "", style: $"SettingsMenuTab.{tab}")
121  {
122  ToolTip = TextManager.Get($"SettingsTab.{tab}"),
123  OnClicked = (b, _) =>
124  {
125  SelectTab(tab);
126  return false;
127  }
128  };
129  button.RectTransform.MaxSize = RectTransform.MaxPoint;
130  button.Children.ForEach(c => c.RectTransform.MaxSize = RectTransform.MaxPoint);
131 
132  tabContents.Add(tab, (button, content));
133  }
134 
135  private GUIFrame CreateNewContentFrame(Tab tab)
136  {
137  var content = new GUIFrame(new RectTransform(Vector2.One * 0.95f, contentFrame.RectTransform, Anchor.Center, Pivot.Center), style: null);
138  AddButtonToTabber(tab, content);
139  return content;
140  }
141 
142  private static (GUILayoutGroup Left, GUILayoutGroup Right) CreateSidebars(GUIFrame parent, bool split = false)
143  {
144  GUILayoutGroup layout = new GUILayoutGroup(new RectTransform(Vector2.One, parent.RectTransform), isHorizontal: true);
145  GUILayoutGroup left = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false);
146  var centerFrame = new GUIFrame(new RectTransform((0.025f, 1.0f), layout.RectTransform), style: null);
147  if (split)
148  {
149  new GUICustomComponent(new RectTransform(Vector2.One, centerFrame.RectTransform),
150  onDraw: (sb, c) =>
151  {
152  sb.DrawLine((c.Rect.Center.X, c.Rect.Top),(c.Rect.Center.X, c.Rect.Bottom), GUIStyle.TextColorDim, 2f);
153  });
154  }
155  GUILayoutGroup right = new GUILayoutGroup(new RectTransform((0.4875f, 1.0f), layout.RectTransform), isHorizontal: false);
156  return (left, right);
157  }
158 
159  private static GUILayoutGroup CreateCenterLayout(GUIFrame parent)
160  {
161  return new GUILayoutGroup(new RectTransform((0.5f, 1.0f), parent.RectTransform, Anchor.TopCenter, Pivot.TopCenter)) { ChildAnchor = Anchor.TopCenter };
162  }
163 
164  private static RectTransform NewItemRectT(GUILayoutGroup parent)
165  => new RectTransform((1.0f, 0.06f), parent.RectTransform, Anchor.CenterLeft);
166 
167  private static void Spacer(GUILayoutGroup parent)
168  {
169  new GUIFrame(new RectTransform((1.0f, 0.03f), parent.RectTransform, Anchor.CenterLeft), style: null);
170  }
171 
172  private static GUITextBlock Label(GUILayoutGroup parent, LocalizedString str, GUIFont font)
173  {
174  return new GUITextBlock(NewItemRectT(parent), str, font: font);
175  }
176 
177  private static void DropdownEnum<T>(GUILayoutGroup parent, Func<T, LocalizedString> textFunc, Func<T, LocalizedString>? tooltipFunc, T currentValue,
178  Action<T> setter) where T : Enum
179  => Dropdown(parent, textFunc, tooltipFunc, (T[])Enum.GetValues(typeof(T)), currentValue, setter);
180 
181  private static GUIDropDown Dropdown<T>(GUILayoutGroup parent, Func<T, LocalizedString> textFunc, Func<T, LocalizedString>? tooltipFunc, IReadOnlyList<T> values, T currentValue, Action<T> setter)
182  {
183  var dropdown = new GUIDropDown(NewItemRectT(parent));
184  values.ForEach(v => dropdown.AddItem(text: textFunc(v), userData: v, toolTip: tooltipFunc?.Invoke(v) ?? null));
185  int childIndex = values.IndexOf(currentValue);
186  dropdown.Select(childIndex);
187  dropdown.ListBox.ForceLayoutRecalculation();
188  dropdown.ListBox.ScrollToElement(dropdown.ListBox.Content.GetChild(childIndex));
189  dropdown.OnSelected = (dd, obj) =>
190  {
191  setter((T)obj);
192  return true;
193  };
194  return dropdown;
195  }
196 
197  private static (GUIScrollBar slider, GUITextBlock label) Slider(GUILayoutGroup parent, Vector2 range, int steps, Func<float, string> labelFunc, float currentValue, Action<float> setter, LocalizedString? tooltip = null)
198  {
199  var layout = new GUILayoutGroup(NewItemRectT(parent), isHorizontal: true);
200  var slider = new GUIScrollBar(new RectTransform((0.72f, 1.0f), layout.RectTransform), style: "GUISlider")
201  {
202  Range = range,
203  BarScrollValue = currentValue,
204  Step = 1.0f / (float)(steps - 1),
205  BarSize = 1.0f / steps
206  };
207  if (tooltip != null)
208  {
209  slider.ToolTip = tooltip;
210  }
211  var label = new GUITextBlock(new RectTransform((0.28f, 1.0f), layout.RectTransform),
212  labelFunc(currentValue), wrap: false, textAlignment: Alignment.Center);
213  slider.OnMoved = (sb, val) =>
214  {
215  label.Text = labelFunc(sb.BarScrollValue);
216  setter(sb.BarScrollValue);
217  return true;
218  };
219  return (slider, label);
220  }
221 
222  private static GUITickBox Tickbox(GUILayoutGroup parent, LocalizedString label, LocalizedString tooltip, bool currentValue, Action<bool> setter)
223  {
224  return new GUITickBox(NewItemRectT(parent), label)
225  {
226  Selected = currentValue,
227  ToolTip = tooltip,
228  OnSelected = (tb) =>
229  {
230  setter(tb.Selected);
231  return true;
232  }
233  };
234  }
235 
236  private string Percentage(float v) => ToolBox.GetFormattedPercentage(v);
237 
238  private static int Round(float v) => MathUtils.RoundToInt(v);
239 
240  private void CreateGraphicsTab()
241  {
242  GUIFrame content = CreateNewContentFrame(Tab.Graphics);
243 
244  var (left, right) = CreateSidebars(content);
245 
246  List<(int Width, int Height)> supportedResolutions =
247  GameMain.GraphicsDeviceManager.GraphicsDevice.Adapter.SupportedDisplayModes
248  .Where(m => m.Format == SurfaceFormat.Color)
249  .Select(m => (m.Width, m.Height))
250  .Where(m => m.Width >= GameSettings.Config.GraphicsSettings.MinSupportedResolution.X
251  && m.Height >= GameSettings.Config.GraphicsSettings.MinSupportedResolution.Y)
252  .ToList();
253  var currentResolution = (unsavedConfig.Graphics.Width, unsavedConfig.Graphics.Height);
254  if (!supportedResolutions.Contains(currentResolution))
255  {
256  supportedResolutions.Add(currentResolution);
257  }
258 
259  Label(left, TextManager.Get("Resolution"), GUIStyle.SubHeadingFont);
260  Dropdown(left, (m) => $"{m.Width}x{m.Height}", null, supportedResolutions, currentResolution,
261  (res) =>
262  {
263  unsavedConfig.Graphics.Width = res.Width;
264  unsavedConfig.Graphics.Height = res.Height;
265  });
266  Spacer(left);
267 
268  Label(left, TextManager.Get("DisplayMode"), GUIStyle.SubHeadingFont);
269  DropdownEnum(left, (m) => TextManager.Get($"{m}"), null, unsavedConfig.Graphics.DisplayMode, v => unsavedConfig.Graphics.DisplayMode = v);
270  Spacer(left);
271 
272  Tickbox(left, TextManager.Get("EnableVSync"), TextManager.Get("EnableVSyncTooltip"), unsavedConfig.Graphics.VSync, v => unsavedConfig.Graphics.VSync = v);
273  Tickbox(left, TextManager.Get("EnableTextureCompression"), TextManager.Get("EnableTextureCompressionTooltip"), unsavedConfig.Graphics.CompressTextures, v => unsavedConfig.Graphics.CompressTextures = v);
274  Spacer(right);
275 
276  Label(right, TextManager.Get("LOSEffect"), GUIStyle.SubHeadingFont);
277  DropdownEnum(right, (m) => TextManager.Get($"LosMode{m}"), null, unsavedConfig.Graphics.LosMode, v => unsavedConfig.Graphics.LosMode = v);
278  Spacer(right);
279 
280  Label(right, TextManager.Get("LightMapScale"), GUIStyle.SubHeadingFont);
281  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"));
282  Spacer(right);
283 
284  Label(right, TextManager.Get("VisibleLightLimit"), GUIStyle.SubHeadingFont);
285  Slider(right, (10, 510), 21, v => v > 500 ? TextManager.Get("unlimited").Value : Round(v).ToString(), unsavedConfig.Graphics.VisibleLightLimit,
286  v => unsavedConfig.Graphics.VisibleLightLimit = v > 500 ? int.MaxValue : Round(v), TextManager.Get("VisibleLightLimitTooltip"));
287  Spacer(right);
288 
289  Tickbox(right, TextManager.Get("RadialDistortion"), TextManager.Get("RadialDistortionTooltip"), unsavedConfig.Graphics.RadialDistortion, v => unsavedConfig.Graphics.RadialDistortion = v);
290  Tickbox(right, TextManager.Get("ChromaticAberration"), TextManager.Get("ChromaticAberrationTooltip"), unsavedConfig.Graphics.ChromaticAberration, v => unsavedConfig.Graphics.ChromaticAberration = v);
291 
292  Label(right, TextManager.Get("ParticleLimit"), GUIStyle.SubHeadingFont);
293  Slider(right, (100, 1500), 15, v => Round(v).ToString(), unsavedConfig.Graphics.ParticleLimit, v => unsavedConfig.Graphics.ParticleLimit = Round(v));
294  Spacer(right);
295  }
296 
297  private static string TrimAudioDeviceName(string name)
298  {
299  if (string.IsNullOrWhiteSpace(name)) { return string.Empty; }
300  string[] prefixes = { "OpenAL Soft on " };
301  foreach (string prefix in prefixes)
302  {
303  if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
304  {
305  return name.Remove(0, prefix.Length);
306  }
307  }
308  return name;
309  }
310 
311  private static int HandleAlErrors(string message)
312  {
313  int alcError = Alc.GetError(IntPtr.Zero);
314  if (alcError != Alc.NoError)
315  {
316  DebugConsole.ThrowError($"{message}: ALC error {Alc.GetErrorString(alcError)}");
317  return alcError;
318  }
319 
320  int alError = Al.GetError();
321  if (alError != Al.NoError)
322  {
323  DebugConsole.ThrowError($"{message}: AL error {Al.GetErrorString(alError)}");
324  return alError;
325  }
326 
327  return Al.NoError;
328  }
329 
330  private static void GetAudioDevices(int listSpecifier, int defaultSpecifier, out IReadOnlyList<string> list, ref string current)
331  {
332  list = Array.Empty<string>();
333 
334  var retVal = Alc.GetStringList(IntPtr.Zero, listSpecifier).ToList();
335  if (HandleAlErrors("Alc.GetStringList failed") != Al.NoError) { return; }
336 
337  list = retVal;
338  if (string.IsNullOrEmpty(current))
339  {
340  current = Alc.GetString(IntPtr.Zero, defaultSpecifier);
341  if (HandleAlErrors("Alc.GetString failed") != Al.NoError) { return; }
342  }
343 
344  string currentVal = current;
345  if (list.Any() && !list.Any(n => n.Equals(currentVal, StringComparison.OrdinalIgnoreCase)))
346  {
347  current = list[0];
348  }
349  }
350 
351  private void CreateAudioAndVCTab()
352  {
353  if (GameMain.Client == null
354  && VoipCapture.Instance == null)
355  {
356  string currDevice = unsavedConfig.Audio.VoiceCaptureDevice;
357  GetAudioDevices(Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, out var deviceList, ref currDevice);
358 
359  if (deviceList.Any())
360  {
361  VoipCapture.Create(unsavedConfig.Audio.VoiceCaptureDevice);
362  }
363  if (VoipCapture.Instance == null)
364  {
365  unsavedConfig.Audio.VoiceSetting = VoiceMode.Disabled;
366  }
367  }
368 
369  GUIFrame content = CreateNewContentFrame(Tab.AudioAndVC);
370 
371  var (audio, voiceChat) = CreateSidebars(content, split: true);
372 
373  static void audioDeviceElement(
374  GUILayoutGroup parent,
375  Action<string> setter,
376  int listSpecifier,
377  int defaultSpecifier,
378  ref string currentDevice)
379  {
380 #if OSX
381  //At the time of writing there are no OpenAL implementations
382  //on macOS that return the list of available devices, or
383  //allow selecting any other than the default one. I'm not
384  //about to write my own OpenAL implementation to fix this
385  //so here's a workaround instead, just a label that shows the
386  //name of the current device.
387  var deviceNameContainerElement = new GUIFrame(NewItemRectT(parent), style: "GUITextBoxNoIcon");
388  var deviceNameElement = new GUITextBlock(new RectTransform(Vector2.One, deviceNameContainerElement.RectTransform), currentDevice, textAlignment: Alignment.CenterLeft);
389  new GUICustomComponent(new RectTransform(Vector2.Zero, deviceNameElement.RectTransform), onUpdate:
390  (deltaTime, component) =>
391  {
392  deviceNameElement.Text = Alc.GetString(IntPtr.Zero, listSpecifier);
393  });
394 #else
395  GetAudioDevices(listSpecifier, defaultSpecifier, out var devices, ref currentDevice);
396  Dropdown(parent, v => TrimAudioDeviceName(v), null, devices, currentDevice, setter);
397 #endif
398  }
399 
400  Label(audio, TextManager.Get("AudioOutputDevice"), GUIStyle.SubHeadingFont);
401 
402  string currentOutputDevice = unsavedConfig.Audio.AudioOutputDevice;
403  audioDeviceElement(audio, v => unsavedConfig.Audio.AudioOutputDevice = v, Alc.OutputDevicesSpecifier, Alc.DefaultDeviceSpecifier, ref currentOutputDevice);
404  Spacer(audio);
405 
406  Label(audio, TextManager.Get("SoundVolume"), GUIStyle.SubHeadingFont);
407  Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.SoundVolume, v =>
408  {
409  unsavedConfig.Audio.SoundVolume = v;
410  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryDefault, v);
411  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryWaterAmbience, v);
412  });
413 
414  Label(audio, TextManager.Get("MusicVolume"), GUIStyle.SubHeadingFont);
415  Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.MusicVolume, v =>
416  {
417  unsavedConfig.Audio.MusicVolume = v;
418  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryMusic, v);
419  });
420 
421  Label(audio, TextManager.Get("UiSoundVolume"), GUIStyle.SubHeadingFont);
422  Slider(audio, (0, 1), 101, Percentage, unsavedConfig.Audio.UiVolume, v =>
423  {
424  unsavedConfig.Audio.UiVolume = v;
425  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryUi, v);
426  });
427 
428  Tickbox(audio, TextManager.Get("MuteOnFocusLost"), TextManager.Get("MuteOnFocusLostTooltip"), unsavedConfig.Audio.MuteOnFocusLost, v => unsavedConfig.Audio.MuteOnFocusLost = v);
429  Tickbox(audio, TextManager.Get("DynamicRangeCompression"), TextManager.Get("DynamicRangeCompressionTooltip"), unsavedConfig.Audio.DynamicRangeCompressionEnabled, v => unsavedConfig.Audio.DynamicRangeCompressionEnabled = v);
430  Spacer(audio);
431 
432  Label(audio, TextManager.Get("VoiceChatVolume"), GUIStyle.SubHeadingFont);
433  Slider(audio, (0, 2), 201, Percentage, unsavedConfig.Audio.VoiceChatVolume, v =>
434  {
435  unsavedConfig.Audio.VoiceChatVolume = v;
436  GameMain.SoundManager.SetCategoryGainMultiplier(SoundManager.SoundCategoryVoip, v);
437  });
438 
439  Tickbox(audio, TextManager.Get("DirectionalVoiceChat"), TextManager.Get("DirectionalVoiceChatTooltip"), unsavedConfig.Audio.UseDirectionalVoiceChat, v => unsavedConfig.Audio.UseDirectionalVoiceChat = v);
440  Tickbox(audio, TextManager.Get("VoipAttenuation"), TextManager.Get("VoipAttenuationTooltip"), unsavedConfig.Audio.VoipAttenuationEnabled, v => unsavedConfig.Audio.VoipAttenuationEnabled = v);
441 
442  Label(voiceChat, TextManager.Get("AudioInputDevice"), GUIStyle.SubHeadingFont);
443 
444  string currentInputDevice = unsavedConfig.Audio.VoiceCaptureDevice;
445  audioDeviceElement(voiceChat, v => unsavedConfig.Audio.VoiceCaptureDevice = v, Alc.CaptureDeviceSpecifier, Alc.CaptureDefaultDeviceSpecifier, ref currentInputDevice);
446  Spacer(voiceChat);
447 
448  Label(voiceChat, TextManager.Get("VCInputMode"), GUIStyle.SubHeadingFont);
449  DropdownEnum(voiceChat, v => TextManager.Get($"VoiceMode.{v}"), v => TextManager.Get($"VoiceMode.{v}Tooltip"), unsavedConfig.Audio.VoiceSetting, v => unsavedConfig.Audio.VoiceSetting = v);
450  Spacer(voiceChat);
451 
452  var noiseGateThresholdLabel = Label(voiceChat, TextManager.Get("NoiseGateThreshold"), GUIStyle.SubHeadingFont);
453  var dbMeter = new GUIProgressBar(NewItemRectT(voiceChat), 0.0f, Color.Lime);
454  dbMeter.ProgressGetter = () =>
455  {
456  if (VoipCapture.Instance == null) { return 0.0f; }
457 
458  dbMeter.Color = unsavedConfig.Audio.VoiceSetting switch
459  {
460  VoiceMode.Activity => VoipCapture.Instance.LastdB > unsavedConfig.Audio.NoiseGateThreshold ? GUIStyle.Green : GUIStyle.Orange,
461  VoiceMode.PushToTalk => GUIStyle.Green,
462  VoiceMode.Disabled => Color.LightGray
463  };
464 
465  float scrollVal = double.IsNegativeInfinity(VoipCapture.Instance.LastdB) ? 0.0f : ((float)VoipCapture.Instance.LastdB + 100.0f) / 100.0f;
466  return scrollVal * scrollVal;
467  };
468  var noiseGateSlider = new GUIScrollBar(new RectTransform(Vector2.One, dbMeter.RectTransform, Anchor.Center), color: Color.White,
469  style: "GUISlider", barSize: 0.03f);
470  noiseGateSlider.Frame.Visible = false;
471  noiseGateSlider.Step = 0.01f;
472  noiseGateSlider.Range = new Vector2(-100.0f, 0.0f);
473  noiseGateSlider.BarScroll = MathUtils.InverseLerp(-100.0f, 0.0f, unsavedConfig.Audio.NoiseGateThreshold);
474  noiseGateSlider.BarScroll *= noiseGateSlider.BarScroll;
475  noiseGateSlider.OnMoved = (scrollBar, barScroll) =>
476  {
477  unsavedConfig.Audio.NoiseGateThreshold = MathHelper.Lerp(-100.0f, 0.0f, (float)Math.Sqrt(scrollBar.BarScroll));
478  return true;
479  };
480  new GUICustomComponent(new RectTransform(Vector2.Zero, voiceChat.RectTransform), onUpdate:
481  (deltaTime, component) =>
482  {
483  noiseGateThresholdLabel.Visible = unsavedConfig.Audio.VoiceSetting == VoiceMode.Activity;
484  noiseGateSlider.Visible = unsavedConfig.Audio.VoiceSetting == VoiceMode.Activity;
485  });
486  Spacer(voiceChat);
487 
488  Label(voiceChat, TextManager.Get("MicrophoneVolume"), GUIStyle.SubHeadingFont);
489  Slider(voiceChat, (0, 10), 101, Percentage, unsavedConfig.Audio.MicrophoneVolume, v => unsavedConfig.Audio.MicrophoneVolume = v);
490  Spacer(voiceChat);
491 
492  Label(voiceChat, TextManager.Get("CutoffPrevention"), GUIStyle.SubHeadingFont);
493  Slider(voiceChat, (0, 500), 26, v => $"{Round(v)} ms", unsavedConfig.Audio.VoiceChatCutoffPrevention, v => unsavedConfig.Audio.VoiceChatCutoffPrevention = Round(v), TextManager.Get("CutoffPreventionTooltip"));
494  }
495 
496  private readonly Dictionary<GUIButton, Func<LocalizedString>> inputButtonValueNameGetters = new Dictionary<GUIButton, Func<LocalizedString>>();
497  private bool inputBoxSelectedThisFrame = false;
498 
499  private void CreateControlsTab()
500  {
501  GUIFrame content = CreateNewContentFrame(Tab.Controls);
502 
503  GUILayoutGroup layout = CreateCenterLayout(content);
504 
505  Label(layout, TextManager.Get("AimAssist"), GUIStyle.SubHeadingFont);
506 
507  var aimAssistSlider = Slider(layout, (0, 1), 101, Percentage, unsavedConfig.AimAssistAmount, v => unsavedConfig.AimAssistAmount = v, TextManager.Get("AimAssistTooltip"));
508  Tickbox(layout, TextManager.Get("EnableMouseLook"), TextManager.Get("EnableMouseLookTooltip"), unsavedConfig.EnableMouseLook, v => unsavedConfig.EnableMouseLook = v);
509 
510  Spacer(layout);
511 
512  GUIListBox keyMapList =
513  new GUIListBox(new RectTransform((2.0f, 0.7f),
514  layout.RectTransform))
515  {
516  CanBeFocused = false,
517  OnSelected = (_, __) => false
518  };
519  Spacer(layout);
520 
521  GUILayoutGroup createInputRowLayout()
522  => new GUILayoutGroup(new RectTransform((1.0f, 0.1f), keyMapList.Content.RectTransform), isHorizontal: true);
523 
524  inputButtonValueNameGetters.Clear();
525  Action<KeyOrMouse>? currentSetter = null;
526  void addInputToRow(GUILayoutGroup currRow, LocalizedString labelText, Func<LocalizedString> valueNameGetter, Action<KeyOrMouse> valueSetter, bool isLegacyBind = false)
527  {
528  var inputFrame = new GUIFrame(new RectTransform((0.5f, 1.0f), currRow.RectTransform),
529  style: null);
530  if (isLegacyBind)
531  {
532  labelText = TextManager.GetWithVariable("legacyitemformat", "[name]", labelText);
533  }
534  var label = new GUITextBlock(new RectTransform((0.6f, 1.0f), inputFrame.RectTransform), labelText,
535  font: GUIStyle.SmallFont) {ForceUpperCase = ForceUpperCase.Yes};
536  var inputBox = new GUIButton(
537  new RectTransform((0.4f, 1.0f), inputFrame.RectTransform, Anchor.TopRight, Pivot.TopRight),
538  valueNameGetter(), style: "GUITextBoxNoIcon")
539  {
540  OnClicked = (btn, obj) =>
541  {
542  inputButtonValueNameGetters.Keys.ForEach(b =>
543  {
544  if (b != btn) { b.Selected = false; }
545  });
546  bool willBeSelected = !btn.Selected;
547  if (willBeSelected)
548  {
549  inputBoxSelectedThisFrame = true;
550  currentSetter = v =>
551  {
552  valueSetter(v);
553  btn.Text = valueNameGetter();
554  };
555  }
556 
557  btn.Selected = willBeSelected;
558  return true;
559  }
560  };
561  if (isLegacyBind)
562  {
563  label.TextColor = Color.Lerp(label.TextColor, label.DisabledTextColor, 0.5f);
564  inputBox.Color = Color.Lerp(inputBox.Color, inputBox.DisabledColor, 0.5f);
565  inputBox.TextColor = Color.Lerp(inputBox.TextColor, label.DisabledTextColor, 0.5f);
566  }
567  inputButtonValueNameGetters.Add(inputBox, valueNameGetter);
568  }
569 
570  var inputListener = new GUICustomComponent(new RectTransform(Vector2.Zero, layout.RectTransform), onUpdate: (deltaTime, component) =>
571  {
572  if (currentSetter is null) { return; }
573 
574  if (PlayerInput.PrimaryMouseButtonClicked() && inputBoxSelectedThisFrame)
575  {
576  inputBoxSelectedThisFrame = false;
577  return;
578  }
579 
580  void clearSetter()
581  {
582  currentSetter = null;
583  inputButtonValueNameGetters.Keys.ForEach(b => b.Selected = false);
584  }
585 
586  void callSetter(KeyOrMouse v)
587  {
588  currentSetter?.Invoke(v);
589  clearSetter();
590  }
591 
592  var pressedKeys = PlayerInput.GetKeyboardState.GetPressedKeys();
593  if (pressedKeys?.Any() ?? false)
594  {
595  if (pressedKeys.Contains(Keys.Escape))
596  {
597  clearSetter();
598  }
599  else
600  {
601  callSetter(pressedKeys.First());
602  }
603  }
604  else if (PlayerInput.PrimaryMouseButtonClicked() &&
605  (GUI.MouseOn == null || !(GUI.MouseOn is GUIButton) || GUI.MouseOn.IsChildOf(keyMapList.Content)))
606  {
607  callSetter(MouseButton.PrimaryMouse);
608  }
609  else if (PlayerInput.SecondaryMouseButtonClicked())
610  {
611  callSetter(MouseButton.SecondaryMouse);
612  }
613  else if (PlayerInput.MidButtonClicked())
614  {
615  callSetter(MouseButton.MiddleMouse);
616  }
617  else if (PlayerInput.Mouse4ButtonClicked())
618  {
619  callSetter(MouseButton.MouseButton4);
620  }
621  else if (PlayerInput.Mouse5ButtonClicked())
622  {
623  callSetter(MouseButton.MouseButton5);
624  }
625  else if (PlayerInput.MouseWheelUpClicked())
626  {
627  callSetter(MouseButton.MouseWheelUp);
628  }
629  else if (PlayerInput.MouseWheelDownClicked())
630  {
631  callSetter(MouseButton.MouseWheelDown);
632  }
633  });
634 
635  InputType[] inputTypes = (InputType[])Enum.GetValues(typeof(InputType));
636  InputType[][] inputTypeColumns =
637  {
638  inputTypes.Take(inputTypes.Length - (inputTypes.Length / 2)).ToArray(),
639  inputTypes.TakeLast(inputTypes.Length / 2).ToArray()
640  };
641  for (int i = 0; i < inputTypes.Length; i+=2)
642  {
643  var currRow = createInputRowLayout();
644  for (int j = 0; j < 2; j++)
645  {
646  var column = inputTypeColumns[j];
647  if (i / 2 >= column.Length) { break; }
648  var input = column[i / 2];
649  addInputToRow(
650  currRow,
651  TextManager.Get($"InputType.{input}"),
652  () => unsavedConfig.KeyMap.Bindings[input].Name,
653  v => unsavedConfig.KeyMap = unsavedConfig.KeyMap.WithBinding(input, v),
654  LegacyInputTypes.Contains(input));
655  }
656  }
657 
658  for (int i = 0; i < unsavedConfig.InventoryKeyMap.Bindings.Length; i += 2)
659  {
660  var currRow = createInputRowLayout();
661  for (int j = 0; j < 2; j++)
662  {
663  int currIndex = i + j;
664  if (currIndex >= unsavedConfig.InventoryKeyMap.Bindings.Length) { break; }
665 
666  var input = unsavedConfig.InventoryKeyMap.Bindings[currIndex];
667  addInputToRow(
668  currRow,
669  TextManager.GetWithVariable("inventoryslotkeybind", "[slotnumber]", (currIndex + 1).ToString(CultureInfo.InvariantCulture)),
670  () => unsavedConfig.InventoryKeyMap.Bindings[currIndex].Name,
671  v => unsavedConfig.InventoryKeyMap = unsavedConfig.InventoryKeyMap.WithBinding(currIndex, v));
672  }
673  }
674 
675  GUILayoutGroup resetControlsHolder =
676  new GUILayoutGroup(new RectTransform((1.75f, 0.1f), layout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center)
677  {
678  RelativeSpacing = 0.1f
679  };
680 
681  var defaultBindingsButton =
682  new GUIButton(new RectTransform(new Vector2(0.45f, 1.0f), resetControlsHolder.RectTransform),
683  TextManager.Get("Reset"), style: "GUIButtonSmall")
684  {
685  ToolTip = TextManager.Get("SetDefaultBindingsTooltip"),
686  OnClicked = (_, userdata) =>
687  {
688  unsavedConfig.InventoryKeyMap = GameSettings.Config.InventoryKeyMapping.GetDefault();
689  unsavedConfig.KeyMap = GameSettings.Config.KeyMapping.GetDefault();
690  aimAssistSlider.slider.BarScrollValue = GameSettings.Config.DefaultAimAssist;
691  aimAssistSlider.label.Text = Percentage(GameSettings.Config.DefaultAimAssist);
692  foreach (var btn in inputButtonValueNameGetters.Keys)
693  {
694  btn.Text = inputButtonValueNameGetters[btn]();
695  }
696  Instance?.SelectTab(Tab.Controls);
697  return true;
698  }
699  };
700  }
701 
702  private void CreateGameplayTab()
703  {
704  GUIFrame content = CreateNewContentFrame(Tab.Gameplay);
705 
706  var (leftColumn, rightColumn) = CreateSidebars(content, split: true);
707 
708  var languages = TextManager.AvailableLanguages
709  .OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier())
710  .ToArray();
711  Label(leftColumn, TextManager.Get("Language"), GUIStyle.SubHeadingFont);
712  Dropdown(leftColumn, v => TextManager.GetTranslatedLanguageName(v), null, languages, unsavedConfig.Language, v => unsavedConfig.Language = v);
713  Spacer(leftColumn);
714 
715  Tickbox(leftColumn, TextManager.Get("PauseOnFocusLost"), TextManager.Get("PauseOnFocusLostTooltip"), unsavedConfig.PauseOnFocusLost, v => unsavedConfig.PauseOnFocusLost = v);
716  Spacer(leftColumn);
717 
718  Tickbox(leftColumn, TextManager.Get("DisableInGameHints"), TextManager.Get("DisableInGameHintsTooltip"), unsavedConfig.DisableInGameHints, v => unsavedConfig.DisableInGameHints = v);
719  var resetInGameHintsButton =
720  new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), leftColumn.RectTransform),
721  TextManager.Get("ResetInGameHints"), style: "GUIButtonSmall")
722  {
723  OnClicked = (button, o) =>
724  {
725  var msgBox = new GUIMessageBox(TextManager.Get("ResetInGameHints"),
726  TextManager.Get("ResetInGameHintsTooltip"),
727  buttons: new[] { TextManager.Get("Yes"), TextManager.Get("No") });
728  msgBox.Buttons[0].OnClicked = (guiButton, o1) =>
729  {
730  IgnoredHints.Instance.Clear();
731  msgBox.Close();
732  return false;
733  };
734  msgBox.Buttons[1].OnClicked = msgBox.Close;
735  return false;
736  }
737  };
738 
739  Spacer(leftColumn);
740 
741  Tickbox(leftColumn, TextManager.Get("ChatSpeechBubbles"), TextManager.Get("ChatSpeechBubbles.Tooltip"), unsavedConfig.ChatSpeechBubbles, v => unsavedConfig.ChatSpeechBubbles = v);
742 
743  Label(leftColumn, TextManager.Get("ShowEnemyHealthBars"), GUIStyle.SubHeadingFont);
744  DropdownEnum(leftColumn, v => TextManager.Get($"ShowEnemyHealthBars.{v}"), null, unsavedConfig.ShowEnemyHealthBars, v => unsavedConfig.ShowEnemyHealthBars = v);
745  Spacer(leftColumn);
746  Label(leftColumn, TextManager.Get("InteractionLabels"), GUIStyle.SubHeadingFont);
747  DropdownEnum(leftColumn, v => TextManager.Get($"InteractionLabels.{v}"), null, unsavedConfig.InteractionLabelDisplayMode, v => unsavedConfig.InteractionLabelDisplayMode = v);
748 
749  Label(rightColumn, TextManager.Get("HUDScale"), GUIStyle.SubHeadingFont);
750  Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.HUDScale, v => unsavedConfig.Graphics.HUDScale = v);
751  Label(rightColumn, TextManager.Get("InventoryScale"), GUIStyle.SubHeadingFont);
752  Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.InventoryScale, v => unsavedConfig.Graphics.InventoryScale = v);
753  Label(rightColumn, TextManager.Get("TextScale"), GUIStyle.SubHeadingFont);
754  Slider(rightColumn, (0.75f, 1.25f), 51, Percentage, unsavedConfig.Graphics.TextScale, v => unsavedConfig.Graphics.TextScale = v);
755  Spacer(rightColumn);
756  var resetSpamListFilter =
757  new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), rightColumn.RectTransform),
758  TextManager.Get("clearserverlistfilters"), style: "GUIButtonSmall")
759  {
760  OnClicked = static (_, _) =>
761  {
762  GUI.AskForConfirmation(
763  header: TextManager.Get("clearserverlistfilters"),
764  body: TextManager.Get("clearserverlistfiltersconfirmation"),
765  onConfirm: SpamServerFilters.ClearLocalSpamFilter);
766  return true;
767  }
768  };
769  Spacer(rightColumn);
770 #if !OSX
771  Spacer(rightColumn);
772  var statisticsTickBox = new GUITickBox(NewItemRectT(rightColumn), TextManager.Get("statisticsconsenttickbox"))
773  {
774  OnSelected = tickBox =>
775  {
776  GUIMessageBox? loadingBox = null;
777  if (!tickBox.Selected)
778  {
779  loadingBox = GUIMessageBox.CreateLoadingBox(TextManager.Get("PleaseWait"));
780  }
781  GameAnalyticsManager.SetConsent(
782  tickBox.Selected
783  ? GameAnalyticsManager.Consent.Ask
784  : GameAnalyticsManager.Consent.No,
785  onAnswerSent: () => loadingBox?.Close());
786  return false;
787  }
788  };
789 #if DEBUG
790  statisticsTickBox.Enabled = false;
791 #endif
792  void updateGATickBoxToolTip()
793  => statisticsTickBox.ToolTip = TextManager.Get($"GameAnalyticsStatus.{GameAnalyticsManager.UserConsented}");
794  updateGATickBoxToolTip();
795 
796  var cachedConsent = GameAnalyticsManager.Consent.Unknown;
797  var statisticsTickBoxUpdater = new GUICustomComponent(
798  new RectTransform(Vector2.Zero, statisticsTickBox.RectTransform),
799  onUpdate: (deltaTime, component) =>
800  {
801  bool shouldTickBoxBeSelected = GameAnalyticsManager.UserConsented == GameAnalyticsManager.Consent.Yes;
802 
803  bool shouldUpdateTickBoxState = cachedConsent != GameAnalyticsManager.UserConsented
804  || statisticsTickBox.Selected != shouldTickBoxBeSelected;
805 
806  if (!shouldUpdateTickBoxState) { return; }
807 
808  updateGATickBoxToolTip();
809  cachedConsent = GameAnalyticsManager.UserConsented;
810  GUITickBox.OnSelectedHandler prevHandler = statisticsTickBox.OnSelected;
811  statisticsTickBox.OnSelected = null;
812  statisticsTickBox.Selected = shouldTickBoxBeSelected;
813  statisticsTickBox.OnSelected = prevHandler;
814  statisticsTickBox.Enabled &= GameAnalyticsManager.UserConsented != GameAnalyticsManager.Consent.Error;
815  });
816 #endif
817  //Steam version supports hosting/joining servers using EOS networking
818  if (SteamManager.IsInitialized)
819  {
820  bool shouldCrossplayBeEnabled = unsavedConfig.CrossplayChoice is Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled;
821  var crossplayTickBox = Tickbox(rightColumn, TextManager.Get("EosAllowCrossplay"), TextManager.Get("EosAllowCrossplayTooltip"), shouldCrossplayBeEnabled, v =>
822  {
823  unsavedConfig.CrossplayChoice = v
824  ? Eos.EosSteamPrimaryLogin.CrossplayChoice.Enabled
825  : Eos.EosSteamPrimaryLogin.CrossplayChoice.Disabled;
826  });
827  if (GameMain.NetworkMember != null)
828  {
829  crossplayTickBox.Enabled = false;
830  crossplayTickBox.ToolTip = TextManager.Get("CantAccessEOSSettingsInMP");
831  }
832  }
833  }
834 
835  private void CreateModsTab(out WorkshopMenu workshopMenu)
836  {
837  GUIFrame content = CreateNewContentFrame(Tab.Mods);
838  content.RectTransform.RelativeSize = Vector2.One;
839 
840  workshopMenu = Screen.Selected is MainMenuScreen
841  ? (WorkshopMenu)new MutableWorkshopMenu(content)
842  : (WorkshopMenu)new ImmutableWorkshopMenu(content);
843 
844  GameMain.MainMenuScreen.ResetModUpdateButton();
845  }
846 
847  private void CreateBottomButtons()
848  {
849  new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: TextManager.Get("Cancel"))
850  {
851  OnClicked = (btn, obj) =>
852  {
853  // reset any modified audio settings to current config
854  GameMain.SoundManager?.ApplySettings();
855  Close();
856  return false;
857  }
858  };
859  new GUIButton(new RectTransform(new Vector2(1.0f, 1.0f), bottom.RectTransform), text: TextManager.Get("applysettingsbutton"))
860  {
861  OnClicked = (btn, obj) =>
862  {
863  ApplyInstalledModChanges();
864  mainFrame.Flash(color: GUIStyle.Green);
865  return false;
866  },
867  OnAddedToGUIUpdateList = (GUIComponent component) =>
868  {
869  component.Enabled =
870  CurrentTab != Tab.Mods ||
871  (WorkshopMenu is MutableWorkshopMenu mutableWorkshopMenu && mutableWorkshopMenu.CurrentTab == MutableWorkshopMenu.Tab.InstalledMods && !mutableWorkshopMenu.ViewingItemDetails);
872  }
873  };
874  }
875 
877  {
878  EosSteamPrimaryLogin.HandleCrossplayChoiceChange(unsavedConfig.CrossplayChoice);
879  GameSettings.SetCurrentConfig(unsavedConfig);
880  if (WorkshopMenu is MutableWorkshopMenu { CurrentTab: MutableWorkshopMenu.Tab.InstalledMods } mutableWorkshopMenu)
881  {
882  mutableWorkshopMenu.Apply();
883  }
884  GameSettings.SaveCurrentConfig();
885  }
886 
887  public void Close()
888  {
889  if (GameMain.Client is null || GameSettings.CurrentConfig.Audio.VoiceSetting == VoiceMode.Disabled)
890  {
892  }
893  mainFrame.Parent.RemoveChild(mainFrame);
894  if (Instance == this) { Instance = null; }
895 
896  GUI.SettingsMenuOpen = false;
897  }
898  }
899 }
static GameClient Client
Definition: GameMain.cs:188
static void Create(string deviceName, UInt16? storedBufferID=null)
Definition: VoipCapture.cs:65
readonly WorkshopMenu WorkshopMenu
Definition: SettingsMenu.cs:42
void SelectTab(Tab tab)
static ? SettingsMenu Instance
Definition: SettingsMenu.cs:21
static SettingsMenu Create(RectTransform mainParent)
Definition: SettingsMenu.cs:52
Definition: Al.cs:38
static int GetError()
const int NoError
Definition: Al.cs:98
const int CaptureDeviceSpecifier
Definition: Alc.cs:108
static IReadOnlyList< string > GetStringList(IntPtr device, int param)
Definition: Alc.cs:225
static int GetError(IntPtr device)
const int DefaultDeviceSpecifier
Definition: Alc.cs:99
const int CaptureDefaultDeviceSpecifier
Definition: Alc.cs:109
const int OutputDevicesSpecifier
Definition: Alc.cs:114
const int NoError
Definition: Alc.cs:93
static string GetString(IntPtr device, int param)
Definition: Alc.cs:215
Definition: Al.cs:36