diff --git a/TShockAPI/CLI/CommandLineParser.cs b/TShockAPI/CLI/CommandLineParser.cs new file mode 100644 index 00000000..84c34b94 --- /dev/null +++ b/TShockAPI/CLI/CommandLineParser.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace TShockAPI.CLI +{ + /// + /// A simple command-line parser for retrieving basic information from a command-line. Array types are not supported + /// + public class CommandLineParser + { + private List _flags = new List(); + private Dictionary _results = new Dictionary(); + private string[] _source; + + /// + /// Resets the CommandLineParser, removing any results and flags, and clearing the source + /// + /// + public CommandLineParser Reset() + { + _flags.Clear(); + _results.Clear(); + _source = null; + + return this; + } + + /// + /// Adds a flag to be parsed + /// + /// The flag to be added + /// Whether or not the flag is followed by an argument + public CommandLineParser AddFlag(string flag, bool noArgs = false) + { + FlagSet flags = new FlagSet(flag) { NoArgs = noArgs }; + return AddFlags(flags); + } + + /// + /// Adds a flag to be parsed, with the given callback being invoked with the flag's argument when it is found. + /// The callback's parameter is the argument passed to the flag + /// + /// + /// + /// + public CommandLineParser AddFlag(string flag, Action callback) + { + FlagSet flags = new FlagSet(flag) { callback = callback }; + return AddFlags(flags); + } + + /// + /// Adds a flag to be parsed, with the given callback being invoked when the flag is found. + /// This method assumes the flag has no arguments + /// + /// + /// + /// + public CommandLineParser AddFlag(string flag, Action callback) + { + FlagSet flags = new FlagSet(flag) { NoArgs = true, callback = callback }; + return AddFlags(flags); + } + + /// + /// Adds a range of flags to be parsed + /// + /// The FlagSet to be added + /// + public CommandLineParser AddFlags(FlagSet flags) + { + if (_flags.Contains(flags)) + { + return this; + } + + _flags.Add(flags); + + return this; + } + + /// + /// Adds a range of flags to be parsed, with the given callback being invoked with the flag's argument when it is found. + /// The callback's parameter is the argument passed to the flag + /// + /// The FlagSet to be added + /// An Action with a single string parameter. This parameter is the value passed to the flag + /// + public CommandLineParser AddFlags(FlagSet flags, Action callback) + { + flags.callback = callback; + return AddFlags(flags); + } + + /// + /// Adds a range of flags to be parsed, with the given callback being invoked when the flag's argument is found. + /// This method assumes the flag has no arguments + /// + /// The FlagSet to be added + /// An Action with no parameters. + /// + public CommandLineParser AddFlags(FlagSet flags, Action callback) + { + flags.callback = callback; + flags.NoArgs = true; + return AddFlags(flags); + } + + /// + /// Adds a callback after a flag's parsing has been completed. + /// This method automatically attaches the callback to the last added flag + /// + /// An Action with no parameters. + /// + public CommandLineParser After(Action callback) + { + FlagSet flags = _flags.Last(); + flags.continuation = callback; + return this; + } + + /// + /// Gets the result of a FlagSet, cast to the given type parameter. Array types are not supported + /// + /// + /// + /// + public T Get(FlagSet flags) + { + if (!_results.ContainsKey(flags)) + { + return default(T); + } + + object result = _results[flags]; + Type t = typeof(T); + + if (t == typeof(string)) + { + if (result == null) + { + return (T)(object)string.Empty; + } + + return (T)result; + } + + if (t.IsValueType) + { + TypeConverter tc = TypeDescriptor.GetConverter(t); + return (T)tc.ConvertFromString(result.ToString()); + } + + return (T)Activator.CreateInstance(t, result); + } + + /// + /// Parses the given source for flags registered with the parser + /// + /// + /// + public CommandLineParser ParseFromSource(string[] source) + { + _source = source; + + for (int i = 0; i < source.Length - 1; i++) + { + string flag = source[i].ToLowerInvariant(); + string argument = source[i + 1]; + + FlagSet flags = _flags.FirstOrDefault(f => f.Contains(flag)); + if (flags == null) + { + continue; + } + + if (flags.NoArgs) + { + if (flags.callback != null) + { + ((Action)flags.callback).Invoke(); + } + else + { + _results.Add(flags, true); + } + } + else + { + if (flags.callback != null) + { + ((Action)flags.callback).Invoke(argument); + } + else + { + _results.Add(flags, argument); + } + } + flags.continuation?.Invoke(); + } + + return this; + } + + /// + /// Gets the result of a flag, cast to the given type parameter. Array types are not supported + /// + /// + /// + /// + public T Get(string flag) + { + FlagSet flags = _flags.FirstOrDefault(f => f.Contains(flag)); + if (flags == null) + { + return default(T); + } + + return Get(flags); + } + + /// + /// Attempts to get the result of a flag, cast to the given type parameter. Array types are not supported + /// + /// + /// + /// + /// + public bool TryGet(string flag, out T value) + { + FlagSet flags = _flags.FirstOrDefault(f => f.Contains(flag)); + if (flags == null) + { + value = default(T); + return false; + } + + return TryGet(flags, out value); + } + + /// + /// Attempts to get the result of a FlagSet, cast to the given type parameter. Array types are not supported + /// + /// + /// + /// + /// + public bool TryGet(FlagSet flags, out T value) + { + object result = _results[flags]; + + if (result == null) + { + //Null result shouldn't happen, but return false if it does + value = default(T); + return false; + } + + Type t = typeof(T); + + //Strings get special handling because the result object is a string + if (t == typeof(string)) + { + if (result == null) + { + //Null strings shouldn't happen, but return false if it does + value = default(T); + return false; + } + + value = (T)result; + return true; + } + + //Value types get converted with a TypeConverter + if (t.IsValueType) + { + try + { + TypeConverter tc = TypeDescriptor.GetConverter(t); + value = (T)tc.ConvertFrom(result); + return true; + } + catch + { + value = default(T); + return false; + } + } + + try + { + //Reference types get created with an Activator + value = (T)Activator.CreateInstance(t, result); + return true; + } + catch + { + value = default(T); + return false; + } + } + } +} diff --git a/TShockAPI/CLI/FlagSet.cs b/TShockAPI/CLI/FlagSet.cs new file mode 100644 index 00000000..71f4e07a --- /dev/null +++ b/TShockAPI/CLI/FlagSet.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TShockAPI.CLI +{ + /// + /// Describes a set of flags that are responsible for one CL argument + /// + public class FlagSet : IEquatable + { + private IEnumerable _flags; + + internal object callback; + internal Action continuation; + + /// + /// Whether or not the set of flags represented by this FlagSet is followed by an argument + /// + public bool NoArgs { get; set; } + + /// + /// Creates a new with the given flags + /// + /// Flags represented by this FlagSet + public FlagSet(params string[] flags) + { + if (flags == null) + { + throw new ArgumentNullException(nameof(flags)); + } + + _flags = flags.Select(f => f.ToLowerInvariant()); + } + + /// + /// Creates a new with the given flags and arguments option + /// + /// Flags represented by this FlagSet + /// Whether or not the flags specified will be followed by an argument + public FlagSet(string[] flags, bool noArgs) : this(flags) + { + NoArgs = noArgs; + } + + /// + /// Determines whether or not this flag set contains the given flag + /// + /// + /// + public bool Contains(string flag) + { + return _flags.Contains(flag); + } + + /// + /// Determines whether or not this flag set is equatable to another + /// + /// + /// + public bool Equals(FlagSet other) + { + if (other == null) + { + return false; + } + + return other._flags == _flags; + } + } +} diff --git a/TShockAPI/TShock.cs b/TShockAPI/TShock.cs index dcf3272e..fe4ad7e7 100644 --- a/TShockAPI/TShock.cs +++ b/TShockAPI/TShock.cs @@ -41,6 +41,7 @@ using TShockAPI.ServerSideCharacters; using Terraria.Utilities; using Microsoft.Xna.Framework; using TShockAPI.Sockets; +using TShockAPI.CLI; namespace TShockAPI { @@ -127,6 +128,10 @@ namespace TShockAPI /// instance - Static reference to the TerrariaPlugin instance. public static TerrariaPlugin instance; /// + /// Static reference to a used for simple command-line parsing + /// + public static CommandLineParser CliParser { get; } = new CommandLineParser(); + /// /// Used for implementing REST Tokens prior to the REST system starting up. /// public static Dictionary RESTStartupTokens = new Dictionary(); @@ -204,6 +209,7 @@ namespace TShockAPI try { + CliParser.Reset(); HandleCommandLine(Environment.GetCommandLineArgs()); if (!Directory.Exists(SavePath)) @@ -295,6 +301,7 @@ namespace TShockAPI File.WriteAllText(Path.Combine(SavePath, "tshock.pid"), Process.GetCurrentProcess().Id.ToString(CultureInfo.InvariantCulture)); + CliParser.Reset(); HandleCommandLinePostConfigLoad(Environment.GetCommandLineArgs()); Backups = new BackupManager(Path.Combine(SavePath, "backups")); @@ -609,217 +616,202 @@ namespace TShockAPI /// parms - The array of arguments passed in through the command line. private void HandleCommandLine(string[] parms) { - string path; - for (int i = 0; i < parms.Length; i++) + string path = null; + + //Generic method for doing a path sanity check + Action pathChecker = (p) => { - switch (parms[i].ToLower()) + if (!string.IsNullOrWhiteSpace(p) && p.IndexOfAny(Path.GetInvalidPathChars()) == -1) { - case "-configpath": - { - path = parms[++i]; - if (path.IndexOfAny(Path.GetInvalidPathChars()) == -1) - { - SavePath = path; - ServerApi.LogWriter.PluginWriteLine(this, "Config path has been set to " + path, TraceLevel.Info); - } - break; - } - case "-worldpath": - { - path = parms[++i]; - if (path.IndexOfAny(Path.GetInvalidPathChars()) == -1) - { - Main.WorldPath = path; - ServerApi.LogWriter.PluginWriteLine(this, "World path has been set to " + path, TraceLevel.Info); - } - break; - } - case "-logpath": - { - path = parms[++i]; - if (path.IndexOfAny(Path.GetInvalidPathChars()) == -1) - { - LogPath = path; - ServerApi.LogWriter.PluginWriteLine(this, "Log path has been set to " + path, TraceLevel.Info); - } - break; - } - case "-logformat": - { - LogFormat = parms[++i]; - break; - } - case "-logclear": - { - bool.TryParse(parms[++i], out LogClear); - break; - } - case "-dump": - { - Utils.PrepareLangForDump(); - Lang.setLang(true); - ConfigFile.DumpDescriptions(); - Permissions.DumpDescriptions(); - ServerSideConfig.DumpDescriptions(); - RestManager.DumpDescriptions(); - Utils.DumpBuffs("BuffList.txt"); - Utils.DumpItems("Items-1_0.txt", -48, 235); - Utils.DumpItems("Items-1_1.txt", 235, 604); - Utils.DumpItems("Items-1_2.txt", 604, 2749); - Utils.DumpItems("Items-1_3.txt", 2749, Main.maxItemTypes); - Utils.DumpNPCs("NPCs.txt"); - Utils.DumpProjectiles("Projectiles.txt"); - Utils.DumpPrefixes("Prefixes.txt"); - Environment.Exit(1); - break; - } - case "-config": - { - string filePath = parms[++i]; - ServerApi.LogWriter.PluginWriteLine(this, string.Format("Loading dedicated config file: {0}", filePath), TraceLevel.Verbose); - Main.instance.LoadDedConfig(filePath); - break; - } - case "-port": - { - int serverPort; - if (int.TryParse(parms[++i], out serverPort)) - { - Netplay.ListenPort = serverPort; - ServerApi.LogWriter.PluginWriteLine(this, string.Format("Listening on port {0}.", serverPort), TraceLevel.Verbose); - } - else - { - // The server should not start up if this argument is invalid. - throw new InvalidOperationException("Invalid value given for command line argument \"-ip\"."); - } - - break; - } - case "-worldname": - { - string worldName = parms[++i]; - Main.instance.SetWorldName(worldName); - ServerApi.LogWriter.PluginWriteLine(this, string.Format("World name will be overridden by: {0}", worldName), TraceLevel.Verbose); - - break; - } - case "-autoshutdown": - { - Main.instance.EnableAutoShutdown(); - break; - } - case "-autocreate": - { - string newOpt = parms[++i]; - Main.instance.autoCreate(newOpt); - break; - } - case "-ip": - { - IPAddress ip; - if (IPAddress.TryParse(parms[++i], out ip)) - { - Netplay.ServerIP = ip; - ServerApi.LogWriter.PluginWriteLine(this, string.Format("Listening on IP {0}.", ip), TraceLevel.Verbose); - } - else - { - // The server should not start up if this argument is invalid. - throw new InvalidOperationException("Invalid value given for command line argument \"-ip\"."); - } - - break; - } - case "-connperip": - { - int limit; - if (int.TryParse(parms[++i], out limit)) - { - /* Todo - Requires an OTAPI modification - Netplay.MaxConnections = limit; - ServerApi.LogWriter.PluginWriteLine(this, string.Format( - "Connections per IP have been limited to {0} connections.", limit), TraceLevel.Verbose);*/ - ServerApi.LogWriter.PluginWriteLine(this, "\"-connperip\" is not supported in this version of TShock.", TraceLevel.Verbose); - } - else - ServerApi.LogWriter.PluginWriteLine(this, "Invalid value given for command line argument \"-connperip\".", TraceLevel.Warning); - - break; - } - case "-killinactivesocket": - { - // Netplay.killInactive = true; - ServerApi.LogWriter.PluginWriteLine(this, "The argument -killinactivesocket is no longer present in Terraria.", TraceLevel.Warning); - break; - } - case "-lang": - { - int langIndex; - if (int.TryParse(parms[++i], out langIndex)) - { - Lang.lang = langIndex; - ServerApi.LogWriter.PluginWriteLine(this, string.Format("Language index set to {0}.", langIndex), TraceLevel.Verbose); - } - else - ServerApi.LogWriter.PluginWriteLine(this, "Invalid value given for command line argument \"-lang\".", TraceLevel.Warning); - - break; - } - case "--provider-token": - { - StatTracker.ProviderToken = parms[++i]; - break; - } - case "--stats-optout": - { - StatTracker.OptOut = true; - break; - } - case "--no-restart": - { - TShock.NoRestart = true; - break; - } + path = p; } - } + }; + + //Prepare the parser with all the flags available + CliParser + .AddFlag("-configpath", pathChecker) + //The .After Action is run after the pathChecker Action + .After(() => + { + SavePath = path ?? "tshock"; + if (path != null) + { + ServerApi.LogWriter.PluginWriteLine(this, "Config path has been set to " + path, TraceLevel.Info); + } + }) + + .AddFlag("-worldpath", pathChecker) + .After(() => + { + if (path != null) + { + Main.WorldPath = path; + ServerApi.LogWriter.PluginWriteLine(this, "World path has been set to " + path, TraceLevel.Info); + } + }) + + .AddFlag("-logpath", pathChecker) + .After(() => + { + if (path != null) + { + LogPath = path; + ServerApi.LogWriter.PluginWriteLine(this, "Log path has been set to " + path, TraceLevel.Info); + } + }) + + .AddFlag("-logformat", (format) => + { + if (!string.IsNullOrWhiteSpace(format)) { LogFormat = format; } + }) + + .AddFlag("-config", (cfg) => + { + if (!string.IsNullOrWhiteSpace(cfg)) + { + ServerApi.LogWriter.PluginWriteLine(this, string.Format("Loading dedicated config file: {0}", cfg), TraceLevel.Verbose); + Main.instance.LoadDedConfig(cfg); + } + }) + + .AddFlag("-port", (p) => + { + int port; + if (int.TryParse(p, out port)) + { + Netplay.ListenPort = port; + ServerApi.LogWriter.PluginWriteLine(this, string.Format("Listening on port {0}.", port), TraceLevel.Verbose); + } + }) + + .AddFlag("-worldname", (world) => + { + if (!string.IsNullOrWhiteSpace(world)) + { + Main.instance.SetWorldName(world); + ServerApi.LogWriter.PluginWriteLine(this, string.Format("World name will be overridden by: {0}", world), TraceLevel.Verbose); + } + }) + + .AddFlag("-ip", (ip) => + { + IPAddress addr; + if (IPAddress.TryParse(ip, out addr)) + { + Netplay.ServerIP = addr; + ServerApi.LogWriter.PluginWriteLine(this, string.Format("Listening on IP {0}.", addr), TraceLevel.Verbose); + } + else + { + // The server should not start up if this argument is invalid. + throw new InvalidOperationException("Invalid value given for command line argument \"-ip\"."); + } + }) + + .AddFlag("-lang", (l) => + { + int lang; + if (int.TryParse(l, out lang)) + { + Lang.lang = lang; + ServerApi.LogWriter.PluginWriteLine(this, string.Format("Language index set to {0}.", lang), TraceLevel.Verbose); + } + else + { + ServerApi.LogWriter.PluginWriteLine(this, "Invalid value given for command line argument \"-lang\".", TraceLevel.Warning); + } + }) + + .AddFlag("-autocreate", (size) => + { + if (!string.IsNullOrWhiteSpace(size)) + { + Main.instance.autoCreate(size); + } + }) + + .AddFlag("--provider-token", (token) => StatTracker.ProviderToken = token) + + //Flags without arguments + .AddFlag("-logclear", () => LogClear = true) + .AddFlag("-autoshutdown", () => Main.instance.EnableAutoShutdown()) + .AddFlag("-dump", () => Utils.Dump()) + .AddFlag("--stats-optout", () => StatTracker.OptOut = true) + .AddFlag("--no-restart", () => NoRestart = true); + + CliParser.ParseFromSource(parms); + + /*"-connperip": Todo - Requires an OTAPI modification + { + int limit; + if (int.TryParse(parms[++i], out limit)) + { + //Netplay.MaxConnections = limit; + //ServerApi.LogWriter.PluginWriteLine(this, string.Format( + // "Connections per IP have been limited to {0} connections.", limit), TraceLevel.Verbose); + ServerApi.LogWriter.PluginWriteLine(this, "\"-connperip\" is not supported in this version of TShock.", TraceLevel.Verbose); + } + else + ServerApi.LogWriter.PluginWriteLine(this, "Invalid value given for command line argument \"-connperip\".", TraceLevel.Warning); + }*/ } /// HandleCommandLinePostConfigLoad - Handles additional command line options after the config file is read. /// parms - The array of arguments passed in through the command line. public static void HandleCommandLinePostConfigLoad(string[] parms) { - for (int i = 0; i < parms.Length; i++) - { - switch (parms[i].ToLower()) - { - case "-port": - int port = Convert.ToInt32(parms[++i]); - Netplay.ListenPort = port; - Config.ServerPort = port; - OverridePort = true; - Log.ConsoleInfo("Port overridden by startup argument. Set to " + port); - break; - case "-rest-token": - string token = Convert.ToString(parms[++i]); + FlagSet portSet = new FlagSet("-port"); + FlagSet playerSet = new FlagSet("-maxplayers", "-players"); + FlagSet restTokenSet = new FlagSet("--rest-token", "-rest-token"); + FlagSet restEnableSet = new FlagSet("--rest-enabled", "-rest-enabled"); + FlagSet restPortSet = new FlagSet("--rest-port", "-rest-port"); + + CliParser + .AddFlags(portSet, (p) => + { + int port; + if (int.TryParse(p, out port)) + { + Netplay.ListenPort = port; + Config.ServerPort = port; + OverridePort = true; + Log.ConsoleInfo("Port overridden by startup argument. Set to " + port); + } + }) + .AddFlags(restTokenSet, (token) => + { RESTStartupTokens.Add(token, new SecureRest.TokenData { Username = "null", UserGroupName = "superadmin" }); Console.WriteLine("Startup parameter overrode REST token."); - break; - case "-rest-enabled": - Config.RestApiEnabled = Convert.ToBoolean(parms[++i]); - Console.WriteLine("Startup parameter overrode REST enable."); - break; - case "-rest-port": - Config.RestApiPort = Convert.ToInt32(parms[++i]); + }) + .AddFlags(restEnableSet, (e) => + { + bool enabled; + if (bool.TryParse(e, out enabled)) + { + Config.RestApiEnabled = enabled; + Console.WriteLine("Startup parameter overrode REST enable."); + } + }) + .AddFlags(restPortSet, (p) => + { + int restPort; + if (int.TryParse(p, out restPort)) + { + Config.RestApiPort = restPort; Console.WriteLine("Startup parameter overrode REST port."); - break; - case "-maxplayers": - case "-players": - Config.MaxSlots = Convert.ToInt32(parms[++i]); - Console.WriteLine("Startup parameter overrode maximum player slot configuration value."); - break; - } - } + } + }) + .AddFlags(playerSet, (p)=> + { + int slots; + if (int.TryParse(p, out slots)) + { + Config.MaxSlots = slots; + Console.WriteLine("Startup parameter overrode maximum player slot configuration value."); + } + }); + + CliParser.ParseFromSource(parms); } /// AuthToken - The auth token used by the /auth system to grant temporary superadmin access to new admins. @@ -1014,7 +1006,8 @@ namespace TShockAPI if (player.TilePlaceThreshold >= Config.TilePlaceThreshold) { player.Disable("Reached TilePlace threshold", flags); - lock (player.TilesCreated) { + lock (player.TilesCreated) + { TSPlayer.Server.RevertTiles(player.TilesCreated); player.TilesCreated.Clear(); } @@ -1382,7 +1375,7 @@ namespace TShockAPI } } } - + /// OnLeave - Called when a player leaves the server. /// args - The LeaveEventArgs object. private void OnLeave(LeaveEventArgs args) diff --git a/TShockAPI/TShockAPI.csproj b/TShockAPI/TShockAPI.csproj index aa0d44b4..9f610078 100644 --- a/TShockAPI/TShockAPI.csproj +++ b/TShockAPI/TShockAPI.csproj @@ -83,6 +83,8 @@ + + @@ -201,7 +203,7 @@ - +