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