474 lines
15 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|