Files
OpenRA/OpenRA.Game/Settings.cs
2024-11-03 16:52:47 +02:00

474 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;
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 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 clean = SanitizedName(dirty);
if (string.IsNullOrWhiteSpace(clean) || forbiddenNames.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);
}
}
}