3 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Globalization;
14 using Microsoft.Xna.Framework.Input;
52 public static class GameSettings
64 Language = TextManager.DefaultLanguage,
97 CrossplayChoice = Eos.EosSteamPrimaryLogin.CrossplayChoice.Unknown,
98 DisableGlobalSpamList =
false,
100 InventoryKeyMap = InventoryKeyMapping.
GetDefault()
106 config.AutomaticQuickStartEnabled =
false;
107 config.AutomaticCampaignLoadEnabled =
false;
108 config.TextManagerDebugModeEnabled =
false;
109 config.ModBreakerMode =
false;
118 retVal.DeserializeElement(element);
123 retVal.
Language = TextManager.DefaultLanguage;
127 if (element.GetAttribute(
"RemoteMainMenuContentUrl")?.Value ==
string.Empty)
134 retVal.KeyMap =
new KeyMapping(element.GetChildElements(
"keymapping"), retVal.KeyMap);
135 retVal.InventoryKeyMap =
new InventoryKeyMapping(element.GetChildElements(
"inventorykeymapping"), retVal.InventoryKeyMap);
136 retVal.SavedCampaignSettings = element.GetChildElement(
"campaignsettings");
137 LoadSubEditorImages(element);
171 public Eos.EosSteamPrimaryLogin.CrossplayChoice CrossplayChoice;
172 public XElement SavedCampaignSettings;
173 public bool DisableGlobalSpamList;
176 public bool AutomaticQuickStartEnabled;
177 public bool AutomaticCampaignLoadEnabled;
178 public bool TestScreenEnabled;
179 public bool TextManagerDebugModeEnabled;
180 public bool ModBreakerMode;
205 gfxSettings.
VSync =
true;
217 elements.ForEach(element => retVal.DeserializeElement(element));
239 [StructSerialization.Skip]
244 public static class DeviceNameHandler
246 public static string Read(
string s)
247 => System.Xml.XmlConvert.DecodeName(s)!;
249 public static string Write(
string s)
250 => System.Xml.XmlConvert.EncodeName(s)!;
270 return audioSettings;
276 elements.ForEach(element => retVal.DeserializeElement(element));
292 [StructSerialization.Handler(typeof(DeviceNameHandler))]
294 [StructSerialization.Handler(typeof(DeviceNameHandler))]
301 [StructSerialization.Skip]
305 public struct KeyMapping
307 private readonly
static ImmutableDictionary<InputType, KeyOrMouse> DefaultsQwerty =
308 new Dictionary<InputType, KeyOrMouse>()
316 {
InputType.Aim, MouseButton.SecondaryMouse },
330 {
InputType.Command, MouseButton.MiddleMouse },
331 {
InputType.ContextualCommand, Keys.LeftShift },
332 {
InputType.PreviousFireMode, MouseButton.MouseWheelDown },
333 {
InputType.NextFireMode, MouseButton.MouseWheelUp },
335 {
InputType.TakeHalfFromInventorySlot, Keys.LeftShift },
336 {
InputType.TakeOneFromInventorySlot, Keys.LeftControl },
344 {
InputType.SelectNextCharacter, Keys.Z },
345 {
InputType.SelectPreviousCharacter, Keys.X },
348 {
InputType.Select, MouseButton.PrimaryMouse },
349 {
InputType.Deselect, MouseButton.SecondaryMouse },
350 {
InputType.Shoot, MouseButton.PrimaryMouse },
351 {
InputType.ShowInteractionLabels, Keys.LeftAlt }
352 }.ToImmutableDictionary();
354 public static KeyMapping
GetDefault() =>
new KeyMapping
356 Bindings = DefaultsQwerty
358 (kvp.Key, kvp.Value.MouseButton == MouseButton.None
359 ? (KeyOrMouse)Keyboard.QwertyToCurrentLayout(kvp.Value.Key)
360 : (KeyOrMouse)kvp.Value.MouseButton))
361 .ToImmutableDictionary()
364 public KeyMapping(IEnumerable<XElement> elements, in KeyMapping? fallback)
367 Dictionary<InputType, KeyOrMouse> bindings = fallback?.Bindings?.ToMutable() ?? defaultBindings.ToMutable();
370 if (!bindings.ContainsKey(inputType))
372 bindings.Add(inputType, defaultBindings[inputType]);
376 Dictionary<InputType, KeyOrMouse> savedBindings =
new Dictionary<InputType, KeyOrMouse>();
377 bool playerConfigContainsNewChatBinds =
false;
378 bool playerConfigContainsRestoredVoipBinds =
false;
379 foreach (XElement element
in elements)
381 foreach (XAttribute attribute
in element.Attributes())
383 if (Enum.TryParse(attribute.Name.LocalName, out
InputType result))
385 playerConfigContainsNewChatBinds |= result ==
InputType.ActiveChat;
386 playerConfigContainsRestoredVoipBinds |= result ==
InputType.RadioVoice;
387 var keyOrMouse = element.GetAttributeKeyOrMouse(attribute.Name.LocalName, bindings[result]);
388 savedBindings.Add(result, keyOrMouse);
389 bindings[result] = keyOrMouse;
395 foreach (var defaultBinding
in defaultBindings)
397 if (!IsSetToNone(defaultBinding.Value) && !savedBindings.ContainsKey(defaultBinding.Key))
399 foreach (var savedBinding
in savedBindings)
402 defaultBinding.Key ==
InputType.ContextualCommand)
408 if (savedBinding.Value == defaultBinding.Value)
410 OnGameMainHasLoaded += () =>
412 (string, string)[] replacements =
414 (
"[defaultbind]", $
"\"{TextManager.Get($"inputtype.{defaultBinding.Key}
")}\""),
415 (
"[savedbind]", $
"\"{TextManager.Get($"inputtype.{savedBinding.Key}
")}\""),
416 (
"[key]", $
"\"{defaultBinding.Value.Name}\"")
418 new GUIMessageBox(TextManager.Get(
"warning"), TextManager.GetWithVariables(
"duplicatebindwarning", replacements));
425 static bool IsSetToNone(KeyOrMouse keyOrMouse) => keyOrMouse == Keys.None && keyOrMouse == MouseButton.None;
429 if (!playerConfigContainsNewChatBinds)
432 bindings[
InputType.RadioChat] = Keys.None;
436 if (!playerConfigContainsRestoredVoipBinds)
438 bindings[
InputType.LocalVoice] = Keys.None;
439 bindings[
InputType.RadioVoice] = Keys.None;
442 Bindings = bindings.ToImmutableDictionary();
445 public KeyMapping WithBinding(
InputType type, KeyOrMouse bind)
447 KeyMapping newMapping =
this;
448 newMapping.Bindings = newMapping.Bindings
452 : (kvp.Key, kvp.Value))
453 .ToImmutableDictionary();
457 public ImmutableDictionary<InputType, KeyOrMouse> Bindings;
459 public LocalizedString KeyBindText(
InputType inputType) => Bindings[inputType].Name;
462 [StructSerialization.Skip]
463 public KeyMapping KeyMap;
465 public struct InventoryKeyMapping
467 public ImmutableArray<KeyOrMouse> Bindings;
469 public static InventoryKeyMapping GetDefault() =>
new InventoryKeyMapping
471 Bindings =
new KeyOrMouse[]
486 public InventoryKeyMapping WithBinding(
int index, KeyOrMouse keyOrMouse)
488 var thisBindings = Bindings;
489 return new InventoryKeyMapping()
491 Bindings = Enumerable.Range(0, thisBindings.Length)
492 .Select(i => i == index ? keyOrMouse : thisBindings[i])
497 public InventoryKeyMapping(IEnumerable<XElement> elements, InventoryKeyMapping? fallback)
499 var bindings = (fallback?.Bindings ?? GetDefault().Bindings).ToArray();
500 foreach (XElement element
in elements)
502 for (
int i = 0; i < bindings.Length; i++)
504 bindings[i] = element.GetAttributeKeyOrMouse($
"slot{i}", bindings[i]);
507 Bindings = bindings.ToImmutableArray();
511 [StructSerialization.Skip]
512 public InventoryKeyMapping InventoryKeyMap;
516 public const string PlayerConfigPath =
"config_player.xml";
518 private static Config currentConfig;
519 public static ref readonly Config CurrentConfig => ref currentConfig;
522 public static Action? OnGameMainHasLoaded;
525 public static void Init()
527 XDocument? currentConfigDoc =
null;
529 if (File.Exists(PlayerConfigPath))
531 currentConfigDoc = XMLExtensions.TryLoadXml(PlayerConfigPath);
534 if (currentConfigDoc !=
null)
536 currentConfig = Config.FromElement(currentConfigDoc.Root ??
throw new NullReferenceException(
"Config XML element is invalid: document is null."));
538 ServerListFilters.Init(currentConfigDoc.Root.GetChildElement(
"serverfilters"));
539 MultiplayerPreferences.Init(
540 currentConfigDoc.Root.GetChildElement(
"player"),
541 currentConfigDoc.Root.GetChildElement(
"gameplay")?.GetChildElement(
"jobpreferences"));
542 IgnoredHints.Init(currentConfigDoc.Root.GetChildElement(
"ignoredhints"));
543 DebugConsoleMapping.Init(currentConfigDoc.Root.GetChildElement(
"debugconsolemapping"));
544 CompletedTutorials.Init(currentConfigDoc.Root.GetChildElement(
"tutorials"));
549 currentConfig = Config.GetDefault();
554 public static void SetCurrentConfig(in Config newConfig)
556 bool resolutionChanged =
557 currentConfig.Graphics.Width != newConfig.Graphics.Width ||
558 currentConfig.Graphics.Height != newConfig.Graphics.Height;
559 bool languageChanged = currentConfig.Language != newConfig.Language;
560 bool audioOutputChanged = currentConfig.Audio.AudioOutputDevice != newConfig.Audio.AudioOutputDevice;
561 bool voiceCaptureChanged = currentConfig.Audio.VoiceCaptureDevice != newConfig.Audio.VoiceCaptureDevice;
562 bool textScaleChanged = Math.Abs(currentConfig.Graphics.TextScale - newConfig.Graphics.TextScale) > MathF.Pow(2.0f, -7);
564 bool hudScaleChanged = !MathUtils.NearlyEqual(currentConfig.Graphics.HUDScale, newConfig.Graphics.HUDScale);
566 bool setGraphicsMode =
568 currentConfig.Graphics.VSync != newConfig.Graphics.VSync ||
569 currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode;
572 bool keybindsChanged =
false;
573 foreach (var kvp
in newConfig.KeyMap.Bindings)
575 if (!currentConfig.KeyMap.Bindings.TryGetValue(kvp.Key, out var existingBinding) ||
576 existingBinding != kvp.Value)
578 keybindsChanged =
true;
584 currentConfig = newConfig;
589 GameMain.Instance.ApplyGraphicsSettings(recalculateFontsAndStyles:
true);
591 else if (textScaleChanged)
593 GUIStyle.RecalculateFonts();
596 if (audioOutputChanged)
598 GameMain.SoundManager?.InitializeAlcDevice(currentConfig.Audio.AudioOutputDevice);
601 if (voiceCaptureChanged)
603 VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice);
608 HUDLayoutSettings.CreateAreas();
609 GameMain.GameSession?.HUDScaleChanged();
614 foreach (var item
in Item.ItemList)
616 foreach (var ic
in item.Components)
624 GameMain.SoundManager?.ApplySettings();
626 if (languageChanged) { TextManager.ClearCache(); }
629 public static void SaveCurrentConfig()
631 XDocument configDoc =
new XDocument();
632 XElement root =
new XElement(
"config"); configDoc.Add(root);
633 currentConfig.SerializeElement(root);
635 XElement graphicsElement =
new XElement(
"graphicssettings"); root.Add(graphicsElement);
636 currentConfig.Graphics.SerializeElement(graphicsElement);
638 XElement audioElement =
new XElement(
"audio"); root.Add(audioElement);
639 currentConfig.Audio.SerializeElement(audioElement);
641 XElement contentPackagesElement =
new XElement(
"contentpackages"); root.Add(contentPackagesElement);
642 XComment corePackageComment =
new XComment(ContentPackageManager.EnabledPackages.Core?.Name ??
"Vanilla"); contentPackagesElement.Add(corePackageComment);
643 XElement corePackageElement =
new XElement(ContentPackageManager.CorePackageElementName); contentPackagesElement.Add(corePackageElement);
644 corePackageElement.SetAttributeValue(
"path", ContentPackageManager.EnabledPackages.Core?.Path ?? ContentPackageManager.VanillaFileList);
646 XElement regularPackagesElement =
new XElement(ContentPackageManager.RegularPackagesElementName); contentPackagesElement.Add(regularPackagesElement);
647 foreach (var regularPackage
in ContentPackageManager.EnabledPackages.Regular)
649 XComment packageComment =
new XComment(regularPackage.Name); regularPackagesElement.Add(packageComment);
650 XElement packageElement =
new XElement(ContentPackageManager.RegularPackagesSubElementName); regularPackagesElement.Add(packageElement);
651 packageElement.SetAttributeValue(
"path", regularPackage.Path);
655 XElement serverFiltersElement =
new XElement(
"serverfilters"); root.Add(serverFiltersElement);
656 ServerListFilters.Instance.SaveTo(serverFiltersElement);
658 XElement characterElement =
new XElement(
"player"); root.Add(characterElement);
659 MultiplayerPreferences.Instance.SaveTo(characterElement);
661 XElement ignoredHintsElement =
new XElement(
"ignoredhints"); root.Add(ignoredHintsElement);
662 IgnoredHints.Instance.SaveTo(ignoredHintsElement);
664 XElement debugConsoleMappingElement =
new XElement(
"debugconsolemapping"); root.Add(debugConsoleMappingElement);
665 DebugConsoleMapping.Instance.SaveTo(debugConsoleMappingElement);
667 XElement tutorialsElement =
new XElement(
"tutorials"); root.Add(tutorialsElement);
668 CompletedTutorials.Instance.SaveTo(tutorialsElement);
670 XElement keyMappingElement =
new XElement(
"keymapping",
671 currentConfig.KeyMap.Bindings.Select(kvp
672 =>
new XAttribute(kvp.Key.ToString(), kvp.Value.ToString())));
673 root.Add(keyMappingElement);
675 XElement inventoryKeyMappingElement =
new XElement(
"inventorykeymapping",
676 Enumerable.Range(0, currentConfig.InventoryKeyMap.Bindings.Length)
677 .Zip(currentConfig.InventoryKeyMap.Bindings)
678 .Cast<(
int Index, KeyOrMouse Bind)>()
680 =>
new XAttribute($
"slot{kvp.Index.ToString(CultureInfo.InvariantCulture)}", kvp.Bind.ToString())));
681 root.Add(inventoryKeyMappingElement);
683 SubEditorScreen.ImageManager.Save(root);
685 root.Add(CampaignSettings.CurrentSettings.Save());
688 configDoc.SaveSafe(PlayerConfigPath);
690 System.Xml.XmlWriterSettings settings =
new System.Xml.XmlWriterSettings
693 OmitXmlDeclaration =
true,
694 NewLineOnAttributes =
true
701 configDoc.WriteTo(writer);
707 DebugConsole.ThrowError(
"Saving game settings failed.", e);
708 GameAnalyticsManager.AddErrorEventOnce(
"GameSettings.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error,
709 "Saving game settings failed.\n" + e.Message +
"\n" + e.StackTrace.CleanupStackTrace());
714 private static void LoadSubEditorImages(XElement configElement)
716 XElement? element = configElement?.Element(
"editorimages");
719 SubEditorScreen.ImageManager.Clear(alsoPending:
true);
722 SubEditorScreen.ImageManager.Load(element);
static XmlWriter Create(string path, System.Xml.XmlWriterSettings settings)
InteractionLabelDisplayMode
int VoiceChatCutoffPrevention
bool DisableVoiceChatFilters
bool VoipAttenuationEnabled
static AudioSettings FromElements(IEnumerable< XElement > elements, in AudioSettings? fallback=null)
bool UseDirectionalVoiceChat
static AudioSettings GetDefault()
string VoiceCaptureDevice
bool DynamicRangeCompressionEnabled
static GraphicsSettings FromElements(IEnumerable< XElement > elements, in GraphicsSettings? fallback=null)
static GraphicsSettings GetDefault()
static readonly Point MinSupportedResolution
InteractionLabelDisplayMode InteractionLabelDisplayMode
int CorpseDespawnDelayPvP
EnemyHealthBarMode ShowEnemyHealthBars
static Config FromElement(XElement element, in Config? fallback=null)
bool ShowOffensiveServerPrompt
GraphicsSettings Graphics
static Config GetDefault()
LanguageIdentifier Language
Color SubEditorBackground
bool SaveDebugConsoleLogs
int CorpsesPerSubDespawnThreshold
const float DefaultAimAssist
string RemoteMainMenuContentUrl
bool EnableSubmarineAutoSave
int AutoSaveIntervalSeconds
static readonly LanguageIdentifier None