Files
OpenRA/OpenRA.Game/Settings.cs
RoosterDragon e6914f707a Introduce FirstOrDefault extensions method for Array.Find and List.Find.
This allows the LINQ spelling to be used, but benefits from the performance improvement of the specific methods for these classes that provide the same result.
2023-11-19 19:28:57 +02:00

477 lines
15 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA
{
public enum MouseScrollType { Disabled, Standard, Inverted, Joystick }
public enum StatusBarsType { Standard, DamageShow, AlwaysShow }
public enum TargetLinesType { Disabled, Manual, Automatic }
[Flags]
public enum MPGameFilters
{
None = 0,
Waiting = 1,
Empty = 2,
Protected = 4,
Started = 8,
Incompatible = 16
}
[Flags]
public enum TextNotificationPoolFilters
{
None = 0,
Feedback = 1,
Transients = 2
}
public enum WorldViewport { Native, Close, Medium, Far }
public class ServerSettings
{
[Desc("Sets the server name.")]
public string Name = "";
[Desc("Sets the internal port.")]
public int ListenPort = 1234;
[Desc("Reports the game to the master server list.")]
public bool AdvertiseOnline = true;
[Desc("Locks the game with a password.")]
public string Password = "";
[Desc("Allow users to search UPnP/NAT-PMP enabled devices for automatic port forwarding.")]
public bool DiscoverNatDevices = false;
[Desc("Time in seconds for UPnP/NAT-PMP mappings to last.")]
public int NatPortMappingLifetime = 36000;
[Desc("Starts the game with a default map. Input as hash that can be obtained by the utility.")]
public string Map = null;
[Desc("Takes a comma separated list of IP addresses that are not allowed to join.")]
public string[] Ban = Array.Empty<string>();
[Desc("For dedicated servers only, allow anonymous clients to join.")]
public bool RequireAuthentication = false;
[Desc("For dedicated servers only, if non-empty, only allow authenticated players with these profile IDs to join.")]
public int[] ProfileIDWhitelist = Array.Empty<int>();
[Desc("For dedicated servers only, if non-empty, always reject players with these user IDs from joining.")]
public int[] ProfileIDBlacklist = Array.Empty<int>();
[Desc("For dedicated servers only, controls whether a game can be started with just one human player in the lobby.")]
public bool EnableSingleplayer = false;
[Desc("Query map information from the Resource Center if they are not available locally.")]
public bool QueryMapRepository = true;
[Desc("Enable client-side report generation to help debug desync errors.")]
public bool EnableSyncReports = false;
[Desc("Sets the timestamp format. Defaults to the ISO 8601 standard.")]
public string TimestampFormat = "yyyy-MM-ddTHH:mm:ss";
[Desc("Allow clients to see anonymised IPs for other clients.")]
public bool ShareAnonymizedIPs = true;
[Desc("Allow clients to see the country of other clients.")]
public bool EnableGeoIP = true;
[Desc("For dedicated servers only, save replays for all games played.")]
public bool RecordReplays = false;
[Desc("For dedicated servers only, treat maps that fail the lint checks as invalid.")]
public bool EnableLintChecks = true;
[Desc("For dedicated servers only, a comma separated list of map uids that are allowed to be used.")]
public string[] MapPool = Array.Empty<string>();
[Desc("Delay in milliseconds before newly joined players can send chat messages.")]
public int FloodLimitJoinCooldown = 5000;
[Desc("Amount of milliseconds player chat messages are tracked for.")]
public int FloodLimitInterval = 5000;
[Desc("Amount of chat messages per FloodLimitInterval a players can send before flood is detected.")]
public int FloodLimitMessageCount = 5;
[Desc("Delay in milliseconds before players can send chat messages after flood was detected.")]
public int FloodLimitCooldown = 15000;
[Desc("Can players vote to kick other players?")]
public bool EnableVoteKick = true;
[Desc("After how much time in miliseconds should the vote kick fail after idling?")]
public int VoteKickTimer = 30000;
[Desc("If a vote kick was unsuccessful for how long should the player who started the vote not be able to start new votes?")]
public int VoteKickerCooldown = 120000;
public ServerSettings Clone()
{
return (ServerSettings)MemberwiseClone();
}
}
public class DebugSettings
{
[Desc("Display average FPS and tick/render times")]
public bool PerfText = false;
[Desc("Display a graph with various profiling traces")]
public bool PerfGraph = false;
[Desc("Number of samples to average over when calculating tick and render times.")]
public int Samples = 25;
[Desc("Check whether a newer version is available online.")]
public bool CheckVersion = true;
[Desc("Allow the collection of anonymous data such as Operating System, .NET runtime, OpenGL version and language settings.")]
public bool SendSystemInformation = true;
[Desc("Version of sysinfo that the player last opted in or out of.")]
public int SystemInformationVersionPrompt = 0;
[Desc("Sysinfo anonymous user identifier.")]
public string UUID = Guid.NewGuid().ToString();
[Desc("Enable hidden developer settings in the Advanced settings tab.")]
public bool DisplayDeveloperSettings = false;
[Desc("Display bot debug messages in the game chat.")]
public bool BotDebug = false;
[Desc("Display Lua debug messages in the game chat.")]
public bool LuaDebug = false;
[Desc("Enable the chat field during replays to allow use of console commands.")]
public bool EnableDebugCommandsInReplays = false;
[Desc("Enable perf.log output for traits, activities and effects.")]
public bool EnableSimulationPerfLogging = false;
[Desc("Amount of time required for triggering perf.log output.")]
public float LongTickThresholdMs = 1;
[Desc("Throw an exception if the world sync hash changes while evaluating user input.")]
public bool SyncCheckUnsyncedCode = false;
[Desc("Throw an exception if the world sync hash changes while evaluating BotModules.")]
public bool SyncCheckBotModuleCode = false;
}
public class GraphicSettings
{
[Desc("This can be set to Windowed, Fullscreen or PseudoFullscreen.")]
public WindowMode Mode = WindowMode.PseudoFullscreen;
[Desc("Enable VSync.")]
public bool VSync = true;
[Desc("Screen resolution in fullscreen mode.")]
public int2 FullscreenSize = new(0, 0);
[Desc("Screen resolution in windowed mode.")]
public int2 WindowedSize = new(1024, 768);
public bool CursorDouble = false;
public WorldViewport ViewportDistance = WorldViewport.Medium;
public float UIScale = 1;
[Desc("Add a frame rate limiter.")]
public bool CapFramerate = false;
[Desc("At which frames per second to cap the framerate.")]
public int MaxFramerate = 60;
[Desc("Set a frame rate limit of 1 render frame per game simulation frame (overrides CapFramerate/MaxFramerate).")]
public bool CapFramerateToGameFps = false;
[Desc("Disable the OpenGL debug message callback feature.")]
public bool DisableGLDebugMessageCallback = false;
[Desc("Disable operating-system provided cursor rendering.")]
public bool DisableHardwareCursors = false;
[Desc("Display index to use in a multi-monitor fullscreen setup.")]
public int VideoDisplay = 0;
[Desc("Preferred OpenGL profile to use.",
"Modern: OpenGL Core Profile 3.2 or greater.",
"Embedded: OpenGL ES 3.0 or greater.",
"Legacy: OpenGL 2.1 with framebuffer_object extension (requires DisableLegacyGL: False)",
"Automatic: Use the first supported profile.")]
public GLProfile GLProfile = GLProfile.Automatic;
public int BatchSize = 8192;
public int SheetSize = 2048;
}
public class SoundSettings
{
public float SoundVolume = 0.5f;
public float MusicVolume = 0.5f;
public float VideoVolume = 0.5f;
public bool Shuffle = false;
public bool Repeat = false;
public string Device = null;
public bool CashTicks = true;
public bool Mute = false;
public bool MuteBackgroundMusic = false;
}
public class PlayerSettings
{
[Desc("Sets the player nickname.")]
public string Name = "Commander";
public Color Color = Color.FromArgb(200, 32, 32);
public string LastServer = "localhost:1234";
public Color[] CustomColors = Array.Empty<Color>();
public string Language = "en";
}
public class GameSettings
{
public string Platform = "Default";
public bool ViewportEdgeScroll = true;
public int ViewportEdgeScrollMargin = 5;
public bool LockMouseWindow = false;
public MouseScrollType MouseScroll = MouseScrollType.Joystick;
public MouseButtonPreference MouseButtonPreference = new();
public float ViewportEdgeScrollStep = 30f;
public float UIScrollSpeed = 50f;
public float ZoomSpeed = 0.04f;
public int SelectionDeadzone = 24;
public int MouseScrollDeadzone = 8;
public bool UseClassicMouseStyle = false;
public bool UseAlternateScrollButton = false;
public bool HideReplayChat = false;
public StatusBarsType StatusBars = StatusBarsType.Standard;
public TargetLinesType TargetLines = TargetLinesType.Manual;
public bool UsePlayerStanceColors = false;
public bool AllowDownloading = true;
[Desc("Filename of the authentication profile to use.")]
public string AuthProfile = "player.oraid";
public Modifiers ZoomModifier = Modifiers.None;
public bool FetchNews = true;
[Desc("Version of introduction prompt that the player last viewed.")]
public int IntroductionPromptVersion = 0;
public MPGameFilters MPGameFilters = MPGameFilters.Waiting | MPGameFilters.Empty | MPGameFilters.Protected | MPGameFilters.Started;
public bool PauseShellmap = false;
[Desc("Allow mods to enable the Discord service that can interact with a local Discord client.")]
public bool EnableDiscordService = true;
public TextNotificationPoolFilters TextNotificationPoolFilters = TextNotificationPoolFilters.Feedback | TextNotificationPoolFilters.Transients;
}
public class Settings
{
readonly string settingsFile;
public readonly PlayerSettings Player = new();
public readonly GameSettings Game = new();
public readonly SoundSettings Sound = new();
public readonly GraphicSettings Graphics = new();
public readonly ServerSettings Server = new();
public readonly DebugSettings Debug = new();
internal Dictionary<string, Hotkey> Keys = new();
public readonly Dictionary<string, object> Sections;
// A direct clone of the file loaded from disk.
// Any changed settings will be merged over this on save,
// allowing us to persist any unknown configuration keys
readonly List<MiniYamlNode> yamlCache = new();
public Settings(string file, Arguments args)
{
settingsFile = file;
Sections = new Dictionary<string, object>()
{
{ "Player", Player },
{ "Game", Game },
{ "Sound", Sound },
{ "Graphics", Graphics },
{ "Server", Server },
{ "Debug", Debug },
};
// Override fieldloader to ignore invalid entries
var err1 = FieldLoader.UnknownFieldAction;
var err2 = FieldLoader.InvalidValueAction;
try
{
FieldLoader.UnknownFieldAction = (s, f) => Console.WriteLine($"Ignoring unknown field `{s}` on `{f.Name}`");
if (File.Exists(settingsFile))
{
yamlCache = MiniYaml.FromFile(settingsFile, false);
foreach (var yamlSection in yamlCache)
{
if (yamlSection.Key != null && Sections.TryGetValue(yamlSection.Key, out var settingsSection))
LoadSectionYaml(yamlSection.Value, settingsSection);
}
var keysNode = yamlCache.FirstOrDefault(n => n.Key == "Keys");
if (keysNode != null)
foreach (var node in keysNode.Value.Nodes)
if (node.Key != null)
Keys[node.Key] = FieldLoader.GetValue<Hotkey>(node.Key, node.Value.Value);
}
// Override with commandline args
foreach (var kv in Sections)
foreach (var f in kv.Value.GetType().GetFields())
if (args.Contains(kv.Key + "." + f.Name))
FieldLoader.LoadField(kv.Value, f.Name, args.GetValue(kv.Key + "." + f.Name, ""));
}
finally
{
FieldLoader.UnknownFieldAction = err1;
FieldLoader.InvalidValueAction = err2;
}
}
public void Save()
{
var yamlCacheBuilder = yamlCache.ConvertAll(n => new MiniYamlNodeBuilder(n));
foreach (var kv in Sections)
{
var sectionYaml = yamlCacheBuilder.FirstOrDefault(x => x.Key == kv.Key);
if (sectionYaml == null)
{
sectionYaml = new MiniYamlNodeBuilder(kv.Key, new MiniYamlBuilder(""));
yamlCacheBuilder.Add(sectionYaml);
}
var defaultValues = Activator.CreateInstance(kv.Value.GetType());
var fields = FieldLoader.GetTypeLoadInfo(kv.Value.GetType());
foreach (var fli in fields)
{
var serialized = FieldSaver.FormatValue(kv.Value, fli.Field);
var defaultSerialized = FieldSaver.FormatValue(defaultValues, fli.Field);
// Fields with their default value are not saved in the settings yaml
// Make sure that we erase any previously defined custom values
if (serialized == defaultSerialized)
sectionYaml.Value.Nodes.RemoveAll(n => n.Key == fli.YamlName);
else
{
// Update or add the custom value
var fieldYaml = sectionYaml.Value.NodeWithKeyOrDefault(fli.YamlName);
if (fieldYaml != null)
fieldYaml.Value.Value = serialized;
else
sectionYaml.Value.Nodes.Add(new MiniYamlNodeBuilder(fli.YamlName, new MiniYamlBuilder(serialized)));
}
}
}
var keysYaml = yamlCacheBuilder.FirstOrDefault(x => x.Key == "Keys");
if (keysYaml == null)
{
keysYaml = new MiniYamlNodeBuilder("Keys", new MiniYamlBuilder(""));
yamlCacheBuilder.Add(keysYaml);
}
keysYaml.Value.Nodes.Clear();
foreach (var kv in Keys)
keysYaml.Value.Nodes.Add(new MiniYamlNodeBuilder(kv.Key, FieldSaver.FormatValue(kv.Value)));
yamlCacheBuilder.WriteToFile(settingsFile);
yamlCache.Clear();
yamlCache.AddRange(yamlCacheBuilder.Select(n => n.Build()));
}
static string SanitizedName(string dirty)
{
if (string.IsNullOrEmpty(dirty))
return null;
var clean = dirty;
// reserved characters for MiniYAML and JSON
var disallowedChars = new char[] { '#', '@', ':', '\n', '\t', '[', ']', '{', '}', '<', '>', '"', '`' };
foreach (var disallowedChar in disallowedChars)
clean = clean.Replace(disallowedChar.ToString(), string.Empty);
return clean;
}
public string SanitizedServerName(string dirty)
{
var clean = SanitizedName(dirty);
if (string.IsNullOrWhiteSpace(clean))
return $"{SanitizedPlayerName(Player.Name)}'s Game";
else
return clean;
}
public static string SanitizedPlayerName(string dirty)
{
var forbiddenNames = new string[] { "Open", "Closed" };
var botNames = OpenRA.Game.ModData.DefaultRules.Actors[SystemActors.Player].TraitInfos<IBotInfo>().Select(t => t.Name);
var clean = SanitizedName(dirty);
if (string.IsNullOrWhiteSpace(clean) || forbiddenNames.Contains(clean) || botNames.Contains(clean))
clean = new PlayerSettings().Name;
// avoid UI glitches
if (clean.Length > 16)
clean = clean[..16];
return clean;
}
static void LoadSectionYaml(MiniYaml yaml, object section)
{
var defaults = Activator.CreateInstance(section.GetType());
FieldLoader.InvalidValueAction = (s, t, f) =>
{
var ret = defaults.GetType().GetField(f).GetValue(defaults);
Console.WriteLine($"FieldLoader: Cannot parse `{s}` into `{f}:{t.Name}`; substituting default `{ret}`");
return ret;
};
FieldLoader.Load(section, yaml);
}
}
}