Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs
2024-04-24 18:09:05 +03:00

724 lines
30 KiB
C#

#nullable enable
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.IO;
#if CLIENT
using Barotrauma.ClientSource.Settings;
using Barotrauma.Networking;
using Microsoft.Xna.Framework.Input;
#endif
namespace Barotrauma
{
public enum WindowMode
{
Windowed, Fullscreen, BorderlessWindowed
}
public enum LosMode
{
None = 0,
Transparent = 1,
Opaque = 2
}
public enum VoiceMode
{
Disabled,
PushToTalk,
Activity
}
public enum EnemyHealthBarMode
{
ShowAll,
BossHealthBarsOnly,
HideAll
}
public enum InteractionLabelDisplayMode
{
Everything,
InteractionAvailable,
LooseItems
}
public static class GameSettings
{
public struct Config
{
public const float DefaultAimAssist = 0.05f;
public static Config GetDefault()
{
Config config = new Config
{
#if SERVER
//server defaults to English, clients get a prompt to select a language
Language = TextManager.DefaultLanguage,
#else
Language = LanguageIdentifier.None,
#endif
SubEditorUndoBuffer = 32,
MaxAutoSaves = 8,
AutoSaveIntervalSeconds = 300,
SubEditorBackground = new Color(13, 37, 69, 255),
EnableSplashScreen = true,
PauseOnFocusLost = true,
RemoteMainMenuContentUrl = "https://www.barotraumagame.com/gamedata/",
AimAssistAmount = DefaultAimAssist,
ShowEnemyHealthBars = EnemyHealthBarMode.ShowAll,
ChatSpeechBubbles = true,
InteractionLabelDisplayMode = InteractionLabelDisplayMode.Everything,
EnableMouseLook = true,
ChatOpen = true,
CrewMenuOpen = true,
ShowOffensiveServerPrompt = true,
TutorialSkipWarning = true,
CorpseDespawnDelay = 600,
CorpsesPerSubDespawnThreshold = 5,
#if OSX
UseDualModeSockets = false,
#else
UseDualModeSockets = true,
#endif
DisableInGameHints = false,
EnableSubmarineAutoSave = true,
Graphics = GraphicsSettings.GetDefault(),
Audio = AudioSettings.GetDefault(),
#if CLIENT
CrossplayChoice = Eos.EosSteamPrimaryLogin.CrossplayChoice.Unknown,
DisableGlobalSpamList = false,
KeyMap = KeyMapping.GetDefault(),
InventoryKeyMap = InventoryKeyMapping.GetDefault()
#endif
};
#if DEBUG
config.QuickStartSub = "Humpback".ToIdentifier();
config.AutomaticQuickStartEnabled = false;
config.AutomaticCampaignLoadEnabled = false;
config.TextManagerDebugModeEnabled = false;
config.ModBreakerMode = false;
#endif
return config;
}
public static Config FromElement(XElement element, in Config? fallback = null)
{
Config retVal = fallback ?? GetDefault();
retVal.DeserializeElement(element);
#if SERVER
//server defaults to English, clients get a prompt to select a language
if (retVal.Language == LanguageIdentifier.None)
{
retVal.Language = TextManager.DefaultLanguage;
}
#endif
//RemoteMainMenuContentUrl gets set to default it left empty - lets allow leaving it empty to make it possible to disable the remote content
if (element.GetAttribute("RemoteMainMenuContentUrl")?.Value == string.Empty)
{
retVal.RemoteMainMenuContentUrl = string.Empty;
}
retVal.Graphics = GraphicsSettings.FromElements(element.GetChildElements("graphicsmode", "graphicssettings"), retVal.Graphics);
retVal.Audio = AudioSettings.FromElements(element.GetChildElements("audio"), retVal.Audio);
#if CLIENT
retVal.KeyMap = new KeyMapping(element.GetChildElements("keymapping"), retVal.KeyMap);
retVal.InventoryKeyMap = new InventoryKeyMapping(element.GetChildElements("inventorykeymapping"), retVal.InventoryKeyMap);
retVal.SavedCampaignSettings = element.GetChildElement("campaignsettings");
LoadSubEditorImages(element);
#endif
return retVal;
}
public LanguageIdentifier Language;
public bool VerboseLogging;
public bool SaveDebugConsoleLogs;
public string SavePath;
public int SubEditorUndoBuffer;
public int MaxAutoSaves;
public int AutoSaveIntervalSeconds;
public Color SubEditorBackground;
public bool EnableSplashScreen;
public bool PauseOnFocusLost;
public float AimAssistAmount;
public bool EnableMouseLook;
public EnemyHealthBarMode ShowEnemyHealthBars;
public bool ChatSpeechBubbles;
public InteractionLabelDisplayMode InteractionLabelDisplayMode;
public bool ChatOpen;
public bool CrewMenuOpen;
public bool ShowOffensiveServerPrompt;
public bool TutorialSkipWarning;
public int CorpseDespawnDelay;
public int CorpsesPerSubDespawnThreshold;
public bool UseDualModeSockets;
public bool DisableInGameHints;
public bool EnableSubmarineAutoSave;
public Identifier QuickStartSub;
public string RemoteMainMenuContentUrl;
#if CLIENT
public Eos.EosSteamPrimaryLogin.CrossplayChoice CrossplayChoice;
public XElement SavedCampaignSettings;
public bool DisableGlobalSpamList;
#endif
#if DEBUG
public bool AutomaticQuickStartEnabled;
public bool AutomaticCampaignLoadEnabled;
public bool TestScreenEnabled;
public bool TextManagerDebugModeEnabled;
public bool ModBreakerMode;
#endif
public struct GraphicsSettings
{
public static readonly Point MinSupportedResolution = new Point(1024, 540);
public static GraphicsSettings GetDefault()
{
GraphicsSettings gfxSettings = new GraphicsSettings
{
RadialDistortion = true,
InventoryScale = 1.0f,
LightMapScale = 1.0f,
VisibleLightLimit = 50,
TextScale = 1.0f,
HUDScale = 1.0f,
Specularity = true,
ChromaticAberration = true,
ParticleLimit = 1500,
LosMode = LosMode.Transparent
};
gfxSettings.RadialDistortion = true;
gfxSettings.CompressTextures = true;
gfxSettings.FrameLimit = 300;
gfxSettings.VSync = true;
#if DEBUG
gfxSettings.DisplayMode = WindowMode.Windowed;
#else
gfxSettings.DisplayMode = WindowMode.BorderlessWindowed;
#endif
return gfxSettings;
}
public static GraphicsSettings FromElements(IEnumerable<XElement> elements, in GraphicsSettings? fallback = null)
{
GraphicsSettings retVal = fallback ?? GetDefault();
elements.ForEach(element => retVal.DeserializeElement(element));
return retVal;
}
public int Width;
public int Height;
public bool VSync;
public bool CompressTextures;
public int FrameLimit;
public WindowMode DisplayMode;
public int ParticleLimit;
public bool Specularity;
public bool ChromaticAberration;
public LosMode LosMode;
public float HUDScale;
public float InventoryScale;
public float LightMapScale;
public int VisibleLightLimit;
public float TextScale;
public bool RadialDistortion;
}
[StructSerialization.Skip]
public GraphicsSettings Graphics;
public struct AudioSettings
{
public static class DeviceNameHandler
{
public static string Read(string s)
=> System.Xml.XmlConvert.DecodeName(s)!;
public static string Write(string s)
=> System.Xml.XmlConvert.EncodeName(s)!;
}
public static AudioSettings GetDefault()
{
AudioSettings audioSettings = new AudioSettings
{
MusicVolume = 0.3f,
SoundVolume = 0.5f,
UiVolume = 0.3f,
VoiceChatVolume = 0.5f,
VoiceChatCutoffPrevention = 200,
MicrophoneVolume = 5,
MuteOnFocusLost = false,
DynamicRangeCompressionEnabled = true,
UseDirectionalVoiceChat = true,
VoipAttenuationEnabled = true,
VoiceSetting = VoiceMode.PushToTalk,
DisableVoiceChatFilters = false
};
return audioSettings;
}
public static AudioSettings FromElements(IEnumerable<XElement> elements, in AudioSettings? fallback = null)
{
AudioSettings retVal = fallback ?? GetDefault();
elements.ForEach(element => retVal.DeserializeElement(element));
return retVal;
}
public float MusicVolume;
public float SoundVolume;
public float UiVolume;
public float VoiceChatVolume;
public int VoiceChatCutoffPrevention;
public float MicrophoneVolume;
public bool MuteOnFocusLost;
public bool DynamicRangeCompressionEnabled;
public bool UseDirectionalVoiceChat;
public bool VoipAttenuationEnabled;
public VoiceMode VoiceSetting;
[StructSerialization.Handler(typeof(DeviceNameHandler))]
public string AudioOutputDevice;
[StructSerialization.Handler(typeof(DeviceNameHandler))]
public string VoiceCaptureDevice;
public float NoiseGateThreshold;
public bool DisableVoiceChatFilters;
}
[StructSerialization.Skip]
public AudioSettings Audio;
#if CLIENT
public struct KeyMapping
{
private readonly static ImmutableDictionary<InputType, KeyOrMouse> DefaultsQwerty =
new Dictionary<InputType, KeyOrMouse>()
{
{ InputType.Run, Keys.LeftShift },
{ InputType.Attack, Keys.R },
{ InputType.Crouch, Keys.LeftControl },
{ InputType.Grab, Keys.G },
{ InputType.Health, Keys.H },
{ InputType.Ragdoll, Keys.Space },
{ InputType.Aim, MouseButton.SecondaryMouse },
{ InputType.DropItem, Keys.None },
{ InputType.InfoTab, Keys.Tab },
{ InputType.Chat, Keys.None },
{ InputType.RadioChat, Keys.None },
{ InputType.ActiveChat, Keys.T },
{ InputType.CrewOrders, Keys.C },
{ InputType.ChatBox, Keys.B },
{ InputType.Voice, Keys.V },
{ InputType.RadioVoice, Keys.None },
{ InputType.LocalVoice, Keys.None },
{ InputType.ToggleChatMode, Keys.R },
{ InputType.Command, MouseButton.MiddleMouse },
{ InputType.ContextualCommand, Keys.LeftShift },
{ InputType.PreviousFireMode, MouseButton.MouseWheelDown },
{ InputType.NextFireMode, MouseButton.MouseWheelUp },
{ InputType.TakeHalfFromInventorySlot, Keys.LeftShift },
{ InputType.TakeOneFromInventorySlot, Keys.LeftControl },
{ InputType.Up, Keys.W },
{ InputType.Down, Keys.S },
{ InputType.Left, Keys.A },
{ InputType.Right, Keys.D },
{ InputType.ToggleInventory, Keys.Q },
{ InputType.SelectNextCharacter, Keys.Z },
{ InputType.SelectPreviousCharacter, Keys.X },
{ InputType.Use, Keys.E },
{ InputType.Select, MouseButton.PrimaryMouse },
{ InputType.Deselect, MouseButton.SecondaryMouse },
{ InputType.Shoot, MouseButton.PrimaryMouse },
{ InputType.ShowInteractionLabels, Keys.LeftAlt }
}.ToImmutableDictionary();
public static KeyMapping GetDefault() => new KeyMapping
{
Bindings = DefaultsQwerty
.Select(kvp =>
(kvp.Key, kvp.Value.MouseButton == MouseButton.None
? (KeyOrMouse)Keyboard.QwertyToCurrentLayout(kvp.Value.Key)
: (KeyOrMouse)kvp.Value.MouseButton))
.ToImmutableDictionary()
};
public KeyMapping(IEnumerable<XElement> elements, in KeyMapping? fallback)
{
var defaultBindings = GetDefault().Bindings;
Dictionary<InputType, KeyOrMouse> bindings = fallback?.Bindings?.ToMutable() ?? defaultBindings.ToMutable();
foreach (InputType inputType in (InputType[])Enum.GetValues(typeof(InputType)))
{
if (!bindings.ContainsKey(inputType))
{
bindings.Add(inputType, defaultBindings[inputType]);
}
}
Dictionary<InputType, KeyOrMouse> savedBindings = new Dictionary<InputType, KeyOrMouse>();
bool playerConfigContainsNewChatBinds = false;
bool playerConfigContainsRestoredVoipBinds = false;
foreach (XElement element in elements)
{
foreach (XAttribute attribute in element.Attributes())
{
if (Enum.TryParse(attribute.Name.LocalName, out InputType result))
{
playerConfigContainsNewChatBinds |= result == InputType.ActiveChat;
playerConfigContainsRestoredVoipBinds |= result == InputType.RadioVoice;
var keyOrMouse = element.GetAttributeKeyOrMouse(attribute.Name.LocalName, bindings[result]);
savedBindings.Add(result, keyOrMouse);
bindings[result] = keyOrMouse;
}
}
}
// Check for duplicate binds when introducing new binds
foreach (var defaultBinding in defaultBindings)
{
if (!IsSetToNone(defaultBinding.Value) && !savedBindings.ContainsKey(defaultBinding.Key))
{
foreach (var savedBinding in savedBindings)
{
if (savedBinding.Key is InputType.Run or InputType.TakeHalfFromInventorySlot &&
defaultBinding.Key == InputType.ContextualCommand)
{
//run and contextual commands have always defaulted to Shift, but the latter used to be hard-coded.
//don't show a warning about those being bound to the same key
continue;
}
if (savedBinding.Value == defaultBinding.Value)
{
OnGameMainHasLoaded += () =>
{
(string, string)[] replacements =
{
("[defaultbind]", $"\"{TextManager.Get($"inputtype.{defaultBinding.Key}")}\""),
("[savedbind]", $"\"{TextManager.Get($"inputtype.{savedBinding.Key}")}\""),
("[key]", $"\"{defaultBinding.Value.Name}\"")
};
new GUIMessageBox(TextManager.Get("warning"), TextManager.GetWithVariables("duplicatebindwarning", replacements));
};
break;
}
}
}
static bool IsSetToNone(KeyOrMouse keyOrMouse) => keyOrMouse == Keys.None && keyOrMouse == MouseButton.None;
}
// Clear the old chat binds for configs saved before the introduction of the new chat binds
if (!playerConfigContainsNewChatBinds)
{
bindings[InputType.Chat] = Keys.None;
bindings[InputType.RadioChat] = Keys.None;
}
// Clear old VOIP binds to make sure we have no overlapping binds
if (!playerConfigContainsRestoredVoipBinds)
{
bindings[InputType.LocalVoice] = Keys.None;
bindings[InputType.RadioVoice] = Keys.None;
}
Bindings = bindings.ToImmutableDictionary();
}
public KeyMapping WithBinding(InputType type, KeyOrMouse bind)
{
KeyMapping newMapping = this;
newMapping.Bindings = newMapping.Bindings
.Select(kvp =>
kvp.Key == type
? (type, bind)
: (kvp.Key, kvp.Value))
.ToImmutableDictionary();
return newMapping;
}
public ImmutableDictionary<InputType, KeyOrMouse> Bindings;
public LocalizedString KeyBindText(InputType inputType) => Bindings[inputType].Name;
}
[StructSerialization.Skip]
public KeyMapping KeyMap;
public struct InventoryKeyMapping
{
public ImmutableArray<KeyOrMouse> Bindings;
public static InventoryKeyMapping GetDefault() => new InventoryKeyMapping
{
Bindings = new KeyOrMouse[]
{
Keys.D1,
Keys.D2,
Keys.D3,
Keys.D4,
Keys.D5,
Keys.D6,
Keys.D7,
Keys.D8,
Keys.D9,
Keys.D0,
}.ToImmutableArray()
};
public InventoryKeyMapping WithBinding(int index, KeyOrMouse keyOrMouse)
{
var thisBindings = Bindings;
return new InventoryKeyMapping()
{
Bindings = Enumerable.Range(0, thisBindings.Length)
.Select(i => i == index ? keyOrMouse : thisBindings[i])
.ToImmutableArray()
};
}
public InventoryKeyMapping(IEnumerable<XElement> elements, InventoryKeyMapping? fallback)
{
var bindings = (fallback?.Bindings ?? GetDefault().Bindings).ToArray();
foreach (XElement element in elements)
{
for (int i = 0; i < bindings.Length; i++)
{
bindings[i] = element.GetAttributeKeyOrMouse($"slot{i}", bindings[i]);
}
}
Bindings = bindings.ToImmutableArray();
}
}
[StructSerialization.Skip]
public InventoryKeyMapping InventoryKeyMap;
#endif
}
public const string PlayerConfigPath = "config_player.xml";
private static Config currentConfig;
public static ref readonly Config CurrentConfig => ref currentConfig;
#if CLIENT
public static Action? OnGameMainHasLoaded;
#endif
public static void Init()
{
XDocument? currentConfigDoc = null;
if (File.Exists(PlayerConfigPath))
{
currentConfigDoc = XMLExtensions.TryLoadXml(PlayerConfigPath);
}
if (currentConfigDoc != null)
{
currentConfig = Config.FromElement(currentConfigDoc.Root ?? throw new NullReferenceException("Config XML element is invalid: document is null."));
#if CLIENT
ServerListFilters.Init(currentConfigDoc.Root.GetChildElement("serverfilters"));
MultiplayerPreferences.Init(
currentConfigDoc.Root.GetChildElement("player"),
currentConfigDoc.Root.GetChildElement("gameplay")?.GetChildElement("jobpreferences"));
IgnoredHints.Init(currentConfigDoc.Root.GetChildElement("ignoredhints"));
DebugConsoleMapping.Init(currentConfigDoc.Root.GetChildElement("debugconsolemapping"));
CompletedTutorials.Init(currentConfigDoc.Root.GetChildElement("tutorials"));
#endif
}
else
{
currentConfig = Config.GetDefault();
SaveCurrentConfig();
}
}
public static void SetCurrentConfig(in Config newConfig)
{
bool resolutionChanged =
currentConfig.Graphics.Width != newConfig.Graphics.Width ||
currentConfig.Graphics.Height != newConfig.Graphics.Height;
bool languageChanged = currentConfig.Language != newConfig.Language;
bool audioOutputChanged = currentConfig.Audio.AudioOutputDevice != newConfig.Audio.AudioOutputDevice;
bool voiceCaptureChanged = currentConfig.Audio.VoiceCaptureDevice != newConfig.Audio.VoiceCaptureDevice;
bool textScaleChanged = Math.Abs(currentConfig.Graphics.TextScale - newConfig.Graphics.TextScale) > MathF.Pow(2.0f, -7);
bool hudScaleChanged = !MathUtils.NearlyEqual(currentConfig.Graphics.HUDScale, newConfig.Graphics.HUDScale);
bool setGraphicsMode =
resolutionChanged ||
currentConfig.Graphics.VSync != newConfig.Graphics.VSync ||
currentConfig.Graphics.DisplayMode != newConfig.Graphics.DisplayMode;
#if CLIENT
bool keybindsChanged = false;
foreach (var kvp in newConfig.KeyMap.Bindings)
{
if (!currentConfig.KeyMap.Bindings.TryGetValue(kvp.Key, out var existingBinding) ||
existingBinding != kvp.Value)
{
keybindsChanged = true;
break;
}
}
#endif
currentConfig = newConfig;
#if CLIENT
if (setGraphicsMode)
{
GameMain.Instance.ApplyGraphicsSettings(recalculateFontsAndStyles: true);
}
else if (textScaleChanged)
{
GUIStyle.RecalculateFonts();
}
if (audioOutputChanged)
{
GameMain.SoundManager?.InitializeAlcDevice(currentConfig.Audio.AudioOutputDevice);
}
if (voiceCaptureChanged)
{
VoipCapture.ChangeCaptureDevice(currentConfig.Audio.VoiceCaptureDevice);
}
if (hudScaleChanged)
{
HUDLayoutSettings.CreateAreas();
GameMain.GameSession?.HUDScaleChanged();
}
if (keybindsChanged)
{
foreach (var item in Item.ItemList)
{
foreach (var ic in item.Components)
{
//parse messages because they may contain keybind texts
ic.ParseMsg();
}
}
}
GameMain.SoundManager?.ApplySettings();
#endif
if (languageChanged) { TextManager.ClearCache(); }
}
public static void SaveCurrentConfig()
{
XDocument configDoc = new XDocument();
XElement root = new XElement("config"); configDoc.Add(root);
currentConfig.SerializeElement(root);
XElement graphicsElement = new XElement("graphicssettings"); root.Add(graphicsElement);
currentConfig.Graphics.SerializeElement(graphicsElement);
XElement audioElement = new XElement("audio"); root.Add(audioElement);
currentConfig.Audio.SerializeElement(audioElement);
XElement contentPackagesElement = new XElement("contentpackages"); root.Add(contentPackagesElement);
XComment corePackageComment = new XComment(ContentPackageManager.EnabledPackages.Core?.Name ?? "Vanilla"); contentPackagesElement.Add(corePackageComment);
XElement corePackageElement = new XElement(ContentPackageManager.CorePackageElementName); contentPackagesElement.Add(corePackageElement);
corePackageElement.SetAttributeValue("path", ContentPackageManager.EnabledPackages.Core?.Path ?? ContentPackageManager.VanillaFileList);
XElement regularPackagesElement = new XElement(ContentPackageManager.RegularPackagesElementName); contentPackagesElement.Add(regularPackagesElement);
foreach (var regularPackage in ContentPackageManager.EnabledPackages.Regular)
{
XComment packageComment = new XComment(regularPackage.Name); regularPackagesElement.Add(packageComment);
XElement packageElement = new XElement(ContentPackageManager.RegularPackagesSubElementName); regularPackagesElement.Add(packageElement);
packageElement.SetAttributeValue("path", regularPackage.Path);
}
#if CLIENT
XElement serverFiltersElement = new XElement("serverfilters"); root.Add(serverFiltersElement);
ServerListFilters.Instance.SaveTo(serverFiltersElement);
XElement characterElement = new XElement("player"); root.Add(characterElement);
MultiplayerPreferences.Instance.SaveTo(characterElement);
XElement ignoredHintsElement = new XElement("ignoredhints"); root.Add(ignoredHintsElement);
IgnoredHints.Instance.SaveTo(ignoredHintsElement);
XElement debugConsoleMappingElement = new XElement("debugconsolemapping"); root.Add(debugConsoleMappingElement);
DebugConsoleMapping.Instance.SaveTo(debugConsoleMappingElement);
XElement tutorialsElement = new XElement("tutorials"); root.Add(tutorialsElement);
CompletedTutorials.Instance.SaveTo(tutorialsElement);
XElement keyMappingElement = new XElement("keymapping",
currentConfig.KeyMap.Bindings.Select(kvp
=> new XAttribute(kvp.Key.ToString(), kvp.Value.ToString())));
root.Add(keyMappingElement);
XElement inventoryKeyMappingElement = new XElement("inventorykeymapping",
Enumerable.Range(0, currentConfig.InventoryKeyMap.Bindings.Length)
.Zip(currentConfig.InventoryKeyMap.Bindings)
.Cast<(int Index, KeyOrMouse Bind)>()
.Select(kvp
=> new XAttribute($"slot{kvp.Index.ToString(CultureInfo.InvariantCulture)}", kvp.Bind.ToString())));
root.Add(inventoryKeyMappingElement);
SubEditorScreen.ImageManager.Save(root);
root.Add(CampaignSettings.CurrentSettings.Save());
#endif
configDoc.SaveSafe(PlayerConfigPath);
System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true,
NewLineOnAttributes = true
};
try
{
using (var writer = XmlWriter.Create(PlayerConfigPath, settings))
{
configDoc.WriteTo(writer);
writer.Flush();
}
}
catch (Exception e)
{
DebugConsole.ThrowError("Saving game settings failed.", e);
GameAnalyticsManager.AddErrorEventOnce("GameSettings.Save:SaveFailed", GameAnalyticsManager.ErrorSeverity.Error,
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace());
}
}
#if CLIENT
private static void LoadSubEditorImages(XElement configElement)
{
XElement? element = configElement?.Element("editorimages");
if (element == null)
{
SubEditorScreen.ImageManager.Clear(alsoPending: true);
return;
}
SubEditorScreen.ImageManager.Load(element);
}
#endif
}
}