diff --git a/MultiAdmin.sln b/MultiAdmin.sln index e785bd9..2c2a669 100644 --- a/MultiAdmin.sln +++ b/MultiAdmin.sln @@ -2,7 +2,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27130.2026 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiAdmin", "MultiAdmin/MultiAdmin.csproj", "{8384BF3C-5FC8-4395-A3DE-440C6C531D36}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiAdmin", "MultiAdmin\MultiAdmin.csproj", "{8384BF3C-5FC8-4395-A3DE-440C6C531D36}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiAdminTests", "MultiAdminTests\MultiAdminTests.csproj", "{D56F8899-C7BB-4ADE-A62C-DEC4DC8C2EE8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -14,6 +16,10 @@ Global {8384BF3C-5FC8-4395-A3DE-440C6C531D36}.Debug|Any CPU.Build.0 = Release|Any CPU {8384BF3C-5FC8-4395-A3DE-440C6C531D36}.Release|Any CPU.ActiveCfg = Release|Any CPU {8384BF3C-5FC8-4395-A3DE-440C6C531D36}.Release|Any CPU.Build.0 = Release|Any CPU + {D56F8899-C7BB-4ADE-A62C-DEC4DC8C2EE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D56F8899-C7BB-4ADE-A62C-DEC4DC8C2EE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D56F8899-C7BB-4ADE-A62C-DEC4DC8C2EE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D56F8899-C7BB-4ADE-A62C-DEC4DC8C2EE8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MultiAdmin/Config/Config.cs b/MultiAdmin/Config/Config.cs index 2c3cf8a..16c5dd5 100644 --- a/MultiAdmin/Config/Config.cs +++ b/MultiAdmin/Config/Config.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using MultiAdmin.ConsoleTools; +using MultiAdmin.Utility; namespace MultiAdmin.Config { @@ -59,7 +60,7 @@ public void ReadConfigFile() public bool Contains(string key) { - return rawData != null && rawData.Any(entry => entry.ToLower().StartsWith(key.ToLower() + ":")); + return rawData != null && rawData.Any(entry => entry.StartsWith($"{key}:", StringComparison.CurrentCultureIgnoreCase)); } private static string CleanValue(string value) @@ -184,6 +185,40 @@ public float GetFloat(string key, float def = 0) return def; } + public double GetDouble(string key, double def = 0) + { + try + { + string value = GetString(key); + + if (!string.IsNullOrEmpty(value) && double.TryParse(value, out double parsedValue)) + return parsedValue; + } + catch (Exception e) + { + Program.LogDebugException(nameof(GetDouble), e); + } + + return def; + } + + public decimal GetDecimal(string key, decimal def = 0) + { + try + { + string value = GetString(key); + + if (!string.IsNullOrEmpty(value) && decimal.TryParse(value, out decimal parsedValue)) + return parsedValue; + } + catch (Exception e) + { + Program.LogDebugException(nameof(GetDecimal), e); + } + + return def; + } + public bool GetBool(string key, bool def = false) { try diff --git a/MultiAdmin/Config/MultiAdminConfig.cs b/MultiAdmin/Config/MultiAdminConfig.cs index d8caf49..eb878cd 100644 --- a/MultiAdmin/Config/MultiAdminConfig.cs +++ b/MultiAdmin/Config/MultiAdminConfig.cs @@ -5,6 +5,8 @@ using System.Reflection; using MultiAdmin.Config.ConfigHandler; using MultiAdmin.ConsoleTools; +using MultiAdmin.ServerIO; +using MultiAdmin.Utility; namespace MultiAdmin.Config { @@ -33,7 +35,7 @@ public class MultiAdminConfig : InheritableConfigRegister "MultiAdmin Debug Logging", "Enables MultiAdmin debug logging, this logs to a separate file than any other logs"); public ConfigEntry DebugLogBlacklist { get; } = - new ConfigEntry("multiadmin_debug_log_blacklist", new string[] {"ProcessFile"}, + new ConfigEntry("multiadmin_debug_log_blacklist", new string[] {nameof(OutputHandler.ProcessFile), nameof(Utils.StringMatches)}, "MultiAdmin Debug Logging Blacklist", "Which tags to block for MultiAdmin debug logging"); public ConfigEntry DebugLogWhitelist { get; } = @@ -84,22 +86,26 @@ public class MultiAdminConfig : InheritableConfigRegister new ConfigEntry("manual_start", false, "Manual Start", "Whether or not to start the server automatically when launching MultiAdmin"); - public ConfigEntry MaxMemory { get; } = - new ConfigEntry("max_memory", 2048, + public ConfigEntry MaxMemory { get; } = + new ConfigEntry("max_memory", 2048, "Max Memory", "The amount of memory in megabytes for MultiAdmin to check against"); - public ConfigEntry RestartLowMemory { get; } = - new ConfigEntry("restart_low_memory", 400, + public ConfigEntry RestartLowMemory { get; } = + new ConfigEntry("restart_low_memory", 400, "Restart Low Memory", "Restart if the game's remaining memory falls below this value in megabytes"); - public ConfigEntry RestartLowMemoryRoundEnd { get; } = - new ConfigEntry("restart_low_memory_roundend", 450, + public ConfigEntry RestartLowMemoryRoundEnd { get; } = + new ConfigEntry("restart_low_memory_roundend", 450, "Restart Low Memory Round-End", "Restart at the end of the round if the game's remaining memory falls below this value in megabytes"); public ConfigEntry MaxPlayers { get; } = new ConfigEntry("max_players", 20, "Max Players", "The number of players to display as the maximum for the server (within MultiAdmin, not in-game)"); + public ConfigEntry OutputReadAttempts { get; } = + new ConfigEntry("output_read_attempts", 100, + "Output Read Attempts", "The number of times to attempt reading a message from the server before giving up"); + public ConfigEntry RandomInputColors { get; } = new ConfigEntry("random_input_colors", false, "Random Input Colors", "Randomize the new input system's colors every time a message is input"); @@ -108,16 +114,28 @@ public class MultiAdminConfig : InheritableConfigRegister new ConfigEntry("restart_every_num_rounds", -1, "Restart Every Number of Rounds", "Restart the server every number of rounds"); + public ConfigEntry RestartEveryNumRoundsCounting { get; } = + new ConfigEntry("restart_every_num_rounds_counting", false, + "Restart Every Number of Rounds Counting", "Whether to print the count of rounds passed after each round if the server is set to restart after a number of rounds"); + public ConfigEntry SafeServerShutdown { get; } = new ConfigEntry("safe_server_shutdown", true, - "Safe Server Shutdown", "When MultiAdmin closes, if this is true, MultiAdmin will attempt to safely shutdown all the servers"); + "Safe Server Shutdown", "When MultiAdmin closes, if this is true, MultiAdmin will attempt to safely shutdown all servers"); + + public ConfigEntry SafeShutdownCheckDelay { get; } = + new ConfigEntry("safe_shutdown_check_delay", 100, + "Safe Shutdown Check Delay", "The time in milliseconds between checking if a server is still running when safely shutting down"); - public ConfigEntry ServerRestartTimeout { get; } = - new ConfigEntry("server_restart_timeout", 10, + public ConfigEntry SafeShutdownTimeout { get; } = + new ConfigEntry("safe_shutdown_timeout", 10000, + "Safe Shutdown Timeout", "The time in milliseconds before MultiAdmin gives up on safely shutting down a server"); + + public ConfigEntry ServerRestartTimeout { get; } = + new ConfigEntry("server_restart_timeout", 10, "Server Restart Timeout", "The time in seconds before MultiAdmin forces a server restart if it doesn't respond to the regular restart command"); - public ConfigEntry ServerStopTimeout { get; } = - new ConfigEntry("server_stop_timeout", 10, + public ConfigEntry ServerStopTimeout { get; } = + new ConfigEntry("server_stop_timeout", 10, "Server Stop Timeout", "The time in seconds before MultiAdmin forces a server shutdown if it doesn't respond to the regular shutdown command"); public ConfigEntry ServersFolder { get; } = @@ -241,6 +259,18 @@ public override void UpdateConfigValueInheritable(ConfigEntry configEntry) break; } + case ConfigEntry config: + { + config.Value = Config.GetDouble(config.Key, config.Default); + break; + } + + case ConfigEntry config: + { + config.Value = Config.GetDecimal(config.Key, config.Default); + break; + } + case ConfigEntry config: { config.Value = Config.GetBool(config.Key, config.Default); @@ -283,7 +313,7 @@ public MultiAdminConfig[] GetConfigHierarchy(bool highestToLowest = true) { List configHierarchy = new List(); - foreach (InheritableConfigRegister configRegister in GetConfigRegisterHierarchy(highestToLowest)) + foreach (ConfigRegister configRegister in GetConfigRegisterHierarchy(highestToLowest)) { if (configRegister is MultiAdminConfig config) configHierarchy.Add(config); diff --git a/MultiAdmin/ConsoleTools/ColoredConsole.cs b/MultiAdmin/ConsoleTools/ColoredConsole.cs index 9241e8e..cd979e2 100644 --- a/MultiAdmin/ConsoleTools/ColoredConsole.cs +++ b/MultiAdmin/ConsoleTools/ColoredConsole.cs @@ -5,7 +5,7 @@ namespace MultiAdmin.ConsoleTools { - public class ColoredConsole + public static class ColoredConsole { public static readonly object WriteLock = new object(); @@ -94,6 +94,58 @@ public ColoredMessage(string text, ConsoleColor? textColor = null, ConsoleColor? this.backgroundColor = backgroundColor; } + public bool Equals(ColoredMessage other) + { + return string.Equals(text, other.text) && textColor == other.textColor && backgroundColor == other.backgroundColor; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((ColoredMessage)obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = text != null ? text.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ textColor.GetHashCode(); + hashCode = (hashCode * 397) ^ backgroundColor.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ColoredMessage firstMessage, ColoredMessage secondMessage) + { + if (ReferenceEquals(firstMessage, secondMessage)) + return true; + + if (ReferenceEquals(firstMessage, null) || ReferenceEquals(secondMessage, null)) + return false; + + return firstMessage.Equals(secondMessage); + } + + public static bool operator !=(ColoredMessage firstMessage, ColoredMessage secondMessage) + { + return !(firstMessage == secondMessage); + } + public override string ToString() { return text; diff --git a/MultiAdmin/Interfaces.cs b/MultiAdmin/EventInterfaces.cs similarity index 100% rename from MultiAdmin/Interfaces.cs rename to MultiAdmin/EventInterfaces.cs diff --git a/MultiAdmin/Features/ConfigReload.cs b/MultiAdmin/Features/ConfigReload.cs index ef02035..76c5747 100644 --- a/MultiAdmin/Features/ConfigReload.cs +++ b/MultiAdmin/Features/ConfigReload.cs @@ -1,5 +1,5 @@ -using System.Linq; using MultiAdmin.Features.Attributes; +using MultiAdmin.Utility; namespace MultiAdmin.Features { @@ -27,7 +27,7 @@ public string GetUsage() public void OnCall(string[] args) { - if (!args.Any() || !args[0].ToLower().Equals("reload")) return; + if (args.IsNullOrEmpty() || !args[0].ToLower().Equals("reload")) return; Server.Write("Reloading configs..."); @@ -48,7 +48,7 @@ public override string GetFeatureDescription() public override string GetFeatureName() { - return "Config reload"; + return "Config Reload"; } public override void Init() diff --git a/MultiAdmin/Features/FolderCopyRoundQueue.cs b/MultiAdmin/Features/FolderCopyRoundQueue.cs index eaf72b8..6f91266 100644 --- a/MultiAdmin/Features/FolderCopyRoundQueue.cs +++ b/MultiAdmin/Features/FolderCopyRoundQueue.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using MultiAdmin.Features.Attributes; +using MultiAdmin.Utility; namespace MultiAdmin.Features { @@ -17,7 +17,7 @@ public FileCopyRoundQueue(Server server) : base(server) { } - public bool HasValidQueue => queue != null && queue.Any(); + public bool HasValidQueue => !queue.IsNullOrEmpty(); public void OnRoundEnd() { diff --git a/MultiAdmin/Features/GithubGenerator.cs b/MultiAdmin/Features/GithubGenerator.cs index 40bf1fe..1b2963f 100644 --- a/MultiAdmin/Features/GithubGenerator.cs +++ b/MultiAdmin/Features/GithubGenerator.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using MultiAdmin.Config; using MultiAdmin.Config.ConfigHandler; using MultiAdmin.Features.Attributes; +using MultiAdmin.Utility; namespace MultiAdmin.Features { @@ -35,7 +35,7 @@ public string GetUsage() public void OnCall(string[] args) { - if (!args.Any()) + if (args.IsEmpty()) { Server.Write("You must specify the location of the file."); return; @@ -43,7 +43,7 @@ public void OnCall(string[] args) string dir = string.Join(" ", args); - List lines = new List {"# MultiAdmin", string.Empty, "## Features"}; + List lines = new List {"# MultiAdmin", string.Empty, "## Features", string.Empty}; foreach (Feature feature in Server.features) { @@ -80,7 +80,7 @@ public void OnCall(string[] args) case ConfigEntry config: { - stringBuilder.Append($"String List{ColumnSeparator}{(!config.Default?.Any() ?? true ? EmptyIndicator : string.Join(", ", config.Default))}"); + stringBuilder.Append($"String List{ColumnSeparator}{(config.Default?.IsEmpty() ?? true ? EmptyIndicator : string.Join(", ", config.Default))}"); break; } @@ -102,6 +102,18 @@ public void OnCall(string[] args) break; } + case ConfigEntry config: + { + stringBuilder.Append($"Double{ColumnSeparator}{config.Default}"); + break; + } + + case ConfigEntry config: + { + stringBuilder.Append($"Decimal{ColumnSeparator}{config.Default}"); + break; + } + case ConfigEntry config: { stringBuilder.Append($"Boolean{ColumnSeparator}{config.Default}"); diff --git a/MultiAdmin/Features/HelpCommand.cs b/MultiAdmin/Features/HelpCommand.cs index 48de384..7e27c3a 100644 --- a/MultiAdmin/Features/HelpCommand.cs +++ b/MultiAdmin/Features/HelpCommand.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using MultiAdmin.Features.Attributes; +using MultiAdmin.Utility; namespace MultiAdmin.Features { @@ -29,7 +29,7 @@ public void OnCall(string[] args) foreach (KeyValuePair command in Server.commands) { string usage = command.Value.GetUsage(); - if (usage.Any()) usage = " " + usage; + if (!usage.IsEmpty()) usage = " " + usage; string output = $"{command.Key.ToUpper()}{usage}: {command.Value.GetCommandDescription()}"; helpOutput.Add(output); } diff --git a/MultiAdmin/Features/MemoryChecker.cs b/MultiAdmin/Features/MemoryChecker.cs index 252490a..be1ec57 100644 --- a/MultiAdmin/Features/MemoryChecker.cs +++ b/MultiAdmin/Features/MemoryChecker.cs @@ -6,7 +6,9 @@ namespace MultiAdmin.Features [Feature] internal class MemoryChecker : Feature, IEventTick, IEventRoundEnd { - private const long BytesInMegabyte = 1048576L; + private const decimal BytesInMegabyte = 1048576; + + private const int OutputPrecision = 2; private uint tickCount; private uint tickCountSoft; @@ -42,34 +44,26 @@ public long MemoryUsedBytes public long MemoryLeftBytes => MaxBytes - MemoryUsedBytes; - public float LowMb + public decimal LowMb { - get => LowBytes / (float)BytesInMegabyte; - set => LowBytes = (long)(value * BytesInMegabyte); + get => decimal.Divide(LowBytes, BytesInMegabyte); + set => LowBytes = (long)decimal.Multiply(value, BytesInMegabyte); } - public float LowMbSoft + public decimal LowMbSoft { - get => LowBytesSoft / (float)BytesInMegabyte; - set => LowBytesSoft = (long)(value * BytesInMegabyte); + get => decimal.Divide(LowBytesSoft, BytesInMegabyte); + set => LowBytesSoft = (long)decimal.Multiply(value, BytesInMegabyte); } - public float MaxMb + public decimal MaxMb { - get => MaxBytes / (float)BytesInMegabyte; - set => MaxBytes = (long)(value * BytesInMegabyte); + get => decimal.Divide(MaxBytes, BytesInMegabyte); + set => MaxBytes = (long)decimal.Multiply(value, BytesInMegabyte); } - public float MemoryUsedMb => MemoryUsedBytes / (float)BytesInMegabyte; - public float MemoryLeftMb => MemoryLeftBytes / (float)BytesInMegabyte; - - //public decimal DecimalMemoryUsedMb => DecimalDivide(MemoryUsedBytes, BytesInMegabyte, 2); - public decimal DecimalMemoryLeftMb => DecimalDivide(MemoryLeftBytes, BytesInMegabyte, 2); - - private static decimal DecimalDivide(long numerator, long denominator, int decimals) - { - return decimal.Round(new decimal(numerator) / new decimal(denominator), decimals); - } + public decimal MemoryUsedMb => decimal.Divide(MemoryUsedBytes, BytesInMegabyte); + public decimal MemoryLeftMb => decimal.Divide(MemoryLeftBytes, BytesInMegabyte); #endregion @@ -90,7 +84,7 @@ public void OnTick() if (tickCount < MaxTicks && LowBytes >= 0 && MemoryLeftBytes <= LowBytes) { - Server.Write($"Warning: Program is running low on memory ({DecimalMemoryLeftMb} MB left), the server will restart if it continues", + Server.Write($"Warning: Program is running low on memory ({decimal.Round(MemoryLeftMb, OutputPrecision)} MB left), the server will restart if it continues", ConsoleColor.Red); tickCount++; } @@ -102,7 +96,7 @@ public void OnTick() if (!restart && tickCountSoft < MaxTicksSoft && LowBytesSoft >= 0 && MemoryLeftBytes <= LowBytesSoft) { Server.Write( - $"Warning: Program is running low on memory ({DecimalMemoryLeftMb} MB left), the server will restart at the end of the round if it continues", + $"Warning: Program is running low on memory ({decimal.Round(MemoryLeftMb, OutputPrecision)} MB left), the server will restart at the end of the round if it continues", ConsoleColor.Red); tickCountSoft++; } diff --git a/MultiAdmin/Features/ModLog.cs b/MultiAdmin/Features/ModLog.cs index 42320fc..a5b4633 100644 --- a/MultiAdmin/Features/ModLog.cs +++ b/MultiAdmin/Features/ModLog.cs @@ -1,5 +1,6 @@ using System.IO; using MultiAdmin.Features.Attributes; +using MultiAdmin.Utility; namespace MultiAdmin.Features { diff --git a/MultiAdmin/Features/NewCommand.cs b/MultiAdmin/Features/NewCommand.cs index f10012a..de59241 100644 --- a/MultiAdmin/Features/NewCommand.cs +++ b/MultiAdmin/Features/NewCommand.cs @@ -1,6 +1,6 @@ using System.Diagnostics; -using System.Linq; using MultiAdmin.Features.Attributes; +using MultiAdmin.Utility; namespace MultiAdmin.Features { @@ -16,7 +16,11 @@ public NewCommand(Server server) : base(server) public void OnCall(string[] args) { - if (args.Any()) + if (args.IsEmpty()) + { + Server.Write("Error: Missing Server ID!"); + } + else { string serverId = string.Join(" ", args); @@ -26,10 +30,6 @@ public void OnCall(string[] args) Program.StartServer(new Server(serverId)); } - else - { - Server.Write("Error: Missing Server ID!"); - } } public string GetCommand() diff --git a/MultiAdmin/Features/Restart.cs b/MultiAdmin/Features/Restart.cs index 6fbc75e..1ab1255 100644 --- a/MultiAdmin/Features/Restart.cs +++ b/MultiAdmin/Features/Restart.cs @@ -11,7 +11,7 @@ public Restart(Server server) : base(server) public string GetCommand() { - return "restart"; + return "RESTART"; } public string GetCommandDescription() diff --git a/MultiAdmin/Features/RestartRoundCounter.cs b/MultiAdmin/Features/RestartRoundCounter.cs index 36c291f..d46321a 100644 --- a/MultiAdmin/Features/RestartRoundCounter.cs +++ b/MultiAdmin/Features/RestartRoundCounter.cs @@ -1,3 +1,4 @@ +using System; using MultiAdmin.Features.Attributes; namespace MultiAdmin.Features @@ -14,16 +15,23 @@ public RestartRoundCounter(Server server) : base(server) public void OnRoundEnd() { - count++; - // If the config value is set to an invalid value, disable this feature - // Or if the count is less than the set number of rounds to go through - if (restartAfter <= 0 || count < restartAfter) return; - - Server.Write($"{count}/{restartAfter} rounds have passed, restarting..."); - - Server.SoftRestartServer(); - count = 0; + if (restartAfter <= 0) + return; + + // If the count is less than the set number of rounds to go through + if (++count < restartAfter) + { + if (Server.ServerConfig.RestartEveryNumRoundsCounting.Value) + Server.Write($"{count}/{restartAfter} rounds have passed..."); + } + else + { + Server.Write($"{count}/{restartAfter} rounds have passed, restarting..."); + + Server.SoftRestartServer(); + count = 0; + } } public override void Init() diff --git a/MultiAdmin/MultiAdmin.csproj b/MultiAdmin/MultiAdmin.csproj index a0b2c45..f22a2aa 100644 --- a/MultiAdmin/MultiAdmin.csproj +++ b/MultiAdmin/MultiAdmin.csproj @@ -104,14 +104,16 @@ - - + + - + + + diff --git a/MultiAdmin/Program.cs b/MultiAdmin/Program.cs index ac2a3d3..65389ec 100644 --- a/MultiAdmin/Program.cs +++ b/MultiAdmin/Program.cs @@ -9,12 +9,13 @@ using MultiAdmin.ConsoleTools; using MultiAdmin.NativeExitSignal; using MultiAdmin.ServerIO; +using MultiAdmin.Utility; namespace MultiAdmin { public static class Program { - public const string MaVersion = "3.2.1"; + public const string MaVersion = "3.2.2.2"; public const string RecommendedMonoVersion = "5.18.0"; private static readonly List InstantiatedServers = new List(); @@ -71,7 +72,7 @@ public static void Write(string message, ConsoleColor color = ConsoleColor.DarkY private static bool IsDebugLogTagAllowed(string tag) { - return (!MultiAdminConfig.GlobalConfig?.DebugLogBlacklist?.Value?.Contains(tag) ?? true) && ((!MultiAdminConfig.GlobalConfig?.DebugLogWhitelist?.Value?.Any() ?? true) || MultiAdminConfig.GlobalConfig.DebugLogWhitelist.Value.Contains(tag)); + return (!MultiAdminConfig.GlobalConfig?.DebugLogBlacklist?.Value?.Contains(tag) ?? true) && ((MultiAdminConfig.GlobalConfig?.DebugLogWhitelist?.Value?.IsEmpty() ?? true) || MultiAdminConfig.GlobalConfig.DebugLogWhitelist.Value.Contains(tag)); } public static void LogDebugException(string tag, Exception exception) @@ -131,9 +132,19 @@ private static void OnExit(object sender, EventArgs e) server.StopServer(); // Wait for server to exit + int timeToWait = Math.Max(MultiAdminConfig.GlobalConfig.SafeShutdownCheckDelay.Value, 0); + int timeWaited = 0; + while (server.IsGameProcessRunning) { - Thread.Sleep(100); + Thread.Sleep(timeToWait); + timeWaited += timeToWait; + + if (timeWaited >= MultiAdminConfig.GlobalConfig.SafeShutdownTimeout.Value) + { + Write($"Failed to server with ID \"{server.serverId}\" within {timeWaited} ms, giving up...", ConsoleColor.Red); + break; + } } } catch (Exception ex) @@ -183,11 +194,26 @@ public static void Main() } else { - if (Servers.Any()) + if (Servers.IsEmpty()) + { + server = new Server(port: portArg); + + InstantiatedServers.Add(server); + } + else { Server[] autoStartServers = AutoStartServers; - if (autoStartServers.Any()) + if (autoStartServers.IsEmpty()) + { + Write("No servers are set to automatically start, please enter a Server ID to start:"); + InputHandler.InputPrefix?.Write(); + + server = new Server(Console.ReadLine(), port: portArg); + + InstantiatedServers.Add(server); + } + else { Write("Starting this instance in multi server mode..."); @@ -205,21 +231,6 @@ public static void Main() } } } - else - { - Write("No servers are set to automatically start, please enter a Server ID to start:"); - InputThread.InputPrefix?.Write(); - - server = new Server(Console.ReadLine(), port: portArg); - - InstantiatedServers.Add(server); - } - } - else - { - server = new Server(port: portArg); - - InstantiatedServers.Add(server); } } @@ -241,14 +252,12 @@ public static void Main() } } - private static bool ArrayIsNullOrEmpty(ICollection array) - { - return array == null || !array.Any(); - } - public static string GetParamFromArgs(string[] keys = null, string[] aliases = null) { - if (ArrayIsNullOrEmpty(keys) && ArrayIsNullOrEmpty(aliases)) return null; + bool hasKeys = !keys.IsNullOrEmpty(); + bool hasAliases = !aliases.IsNullOrEmpty(); + + if (!hasKeys && !hasAliases) return null; string[] args = Environment.GetCommandLineArgs(); @@ -258,7 +267,7 @@ public static string GetParamFromArgs(string[] keys = null, string[] aliases = n if (string.IsNullOrEmpty(lowArg)) continue; - if (!ArrayIsNullOrEmpty(keys)) + if (hasKeys) { if (keys.Any(key => !string.IsNullOrEmpty(key) && lowArg == $"--{key.ToLower()}")) { @@ -266,7 +275,7 @@ public static string GetParamFromArgs(string[] keys = null, string[] aliases = n } } - if (!ArrayIsNullOrEmpty(aliases)) + if (hasAliases) { if (aliases.Any(alias => !string.IsNullOrEmpty(alias) && lowArg == $"-{alias.ToLower()}")) { @@ -286,7 +295,7 @@ public static bool ArgsContainsParam(string[] keys = null, string[] aliases = nu if (string.IsNullOrEmpty(lowArg)) continue; - if (!ArrayIsNullOrEmpty(keys)) + if (!keys.IsNullOrEmpty()) { if (keys.Any(key => !string.IsNullOrEmpty(key) && lowArg == $"--{key.ToLower()}")) { @@ -294,7 +303,7 @@ public static bool ArgsContainsParam(string[] keys = null, string[] aliases = nu } } - if (!ArrayIsNullOrEmpty(aliases)) + if (!aliases.IsNullOrEmpty()) { if (aliases.Any(alias => !string.IsNullOrEmpty(alias) && lowArg == $"-{alias.ToLower()}")) { @@ -308,7 +317,7 @@ public static bool ArgsContainsParam(string[] keys = null, string[] aliases = nu public static bool GetFlagFromArgs(string[] keys = null, string[] aliases = null) { - if (ArrayIsNullOrEmpty(keys) && ArrayIsNullOrEmpty(aliases)) return false; + if (keys.IsNullOrEmpty() && aliases.IsNullOrEmpty()) return false; return bool.TryParse(GetParamFromArgs(keys, aliases), out bool result) ? result : ArgsContainsParam(keys, aliases); } @@ -374,34 +383,6 @@ private static bool IsVersionFormat(string input) return true; } - private static int CompareVersionStrings(string firstVersion, string secondVersion) - { - if (firstVersion == null || secondVersion == null) - return -1; - - string[] firstVersionNums = firstVersion.Split('.'); - string[] secondVersionNums = secondVersion.Split('.'); - - int returnValue = 0; - - for (int i = 0; i < Math.Min(firstVersionNums.Length, secondVersionNums.Length); i++) - { - if (!int.TryParse(firstVersionNums[i], out int current) || !int.TryParse(secondVersionNums[i], out int recommended)) - continue; - - if (current > recommended) - { - returnValue = 1; - } - else if (current < recommended) - { - return -1; - } - } - - return returnValue; - } - public static void CheckMonoVersion() { try @@ -412,7 +393,7 @@ public static void CheckMonoVersion() if (string.IsNullOrEmpty(monoVersion)) return; - int versionDifference = CompareVersionStrings(monoVersion, RecommendedMonoVersion); + int versionDifference = Utils.CompareVersionStrings(monoVersion, RecommendedMonoVersion); if (versionDifference >= 0 && (versionDifference != 0 || monoVersion.Length >= RecommendedMonoVersion.Length)) return; diff --git a/MultiAdmin/Server.cs b/MultiAdmin/Server.cs index 7a7ec4c..bba559c 100644 --- a/MultiAdmin/Server.cs +++ b/MultiAdmin/Server.cs @@ -9,6 +9,7 @@ using MultiAdmin.ConsoleTools; using MultiAdmin.Features.Attributes; using MultiAdmin.ServerIO; +using MultiAdmin.Utility; namespace MultiAdmin { @@ -348,9 +349,10 @@ public void StartServer(bool restartOnCrash = true) eventPreStart.OnServerPreStart(); // Start the input reader - Thread inputReaderThread = new Thread(() => InputThread.Write(this)); + Thread inputHandlerThread = new Thread(() => InputHandler.Write(this)); + if (!Program.Headless) - inputReaderThread.Start(); + inputHandlerThread.Start(); // Start the output reader OutputHandler outputHandler = new OutputHandler(this); @@ -392,7 +394,13 @@ public void StartServer(bool restartOnCrash = true) GameProcess.Dispose(); GameProcess = null; - inputReaderThread.Abort(); + // Stop the input handler if it's running + if (inputHandlerThread.IsAlive) + { + inputHandlerThread.Abort(); + inputHandlerThread.Join(); + } + outputHandler.Dispose(); DeleteSession(); @@ -404,10 +412,12 @@ public void StartServer(bool restartOnCrash = true) } catch (Exception e) { - Write("Failed - Executable file not found or config issue!", ConsoleColor.Red); + Write("Failed - Executable file not found or config issue! Waiting for 1 second before continuing...", ConsoleColor.Red); Write(e.Message, ConsoleColor.Red); - shouldRestart = false; + Program.LogDebugException(nameof(StartServer), e); + + Thread.Sleep(1000); } finally { @@ -479,8 +489,8 @@ private static IEnumerable GetTypesWithAttribute(Type attribute) { foreach (Type type in assembly.GetTypes()) { - object[] attributes = type.GetCustomAttributes(attribute, false); - if (attributes.Any()) yield return type; + object[] attributes = type.GetCustomAttributes(attribute, true); + if (!attributes.IsEmpty()) yield return type; } } } @@ -611,7 +621,7 @@ public void Write(ColoredMessage[] messages, ConsoleColor? timeStampColor = null timeStampedMessage.WriteLine(ServerConfig.UseNewInputSystem.Value); if (ServerConfig.UseNewInputSystem.Value) - InputThread.WriteInputAndSetCursor(); + InputHandler.WriteInputAndSetCursor(); } } @@ -666,7 +676,7 @@ public bool ServerModCheck(int major, int minor, int fix) string[] parts = serverModVersion.Split('.'); - if (!parts.Any()) + if (parts.IsEmpty()) return false; int.TryParse(parts[0], out int verMajor); diff --git a/MultiAdmin/ServerIO/InputThread.cs b/MultiAdmin/ServerIO/InputHandler.cs similarity index 86% rename from MultiAdmin/ServerIO/InputThread.cs rename to MultiAdmin/ServerIO/InputHandler.cs index efc2f19..7153ca3 100644 --- a/MultiAdmin/ServerIO/InputThread.cs +++ b/MultiAdmin/ServerIO/InputHandler.cs @@ -4,10 +4,11 @@ using System.Threading; using MultiAdmin.Config; using MultiAdmin.ConsoleTools; +using MultiAdmin.Utility; namespace MultiAdmin.ServerIO { - public static class InputThread + public static class InputHandler { private static readonly char[] Separator = {' '}; @@ -18,9 +19,12 @@ public static class InputThread public static readonly ColoredMessage RightSideIndicator = new ColoredMessage("...", ConsoleColor.Yellow); public static int InputPrefixLength => InputPrefix?.Length ?? 0; + public static int LeftSideIndicatorLength => LeftSideIndicator?.Length ?? 0; public static int RightSideIndicatorLength => RightSideIndicator?.Length ?? 0; + public static int TotalIndicatorLength => LeftSideIndicatorLength + RightSideIndicatorLength; + public static int SectionBufferWidth { get @@ -43,37 +47,44 @@ public static int SectionBufferWidth public static void Write(Server server) { - ShiftingList prevMessages = new ShiftingList(25); - - while (server.IsRunning && !server.IsStopping) + try { - if (Program.Headless) + ShiftingList prevMessages = new ShiftingList(25); + + while (server.IsRunning && !server.IsStopping) { - Thread.Sleep(5000); - continue; - } + if (Program.Headless) + { + Thread.Sleep(5000); + continue; + } - string message = server.ServerConfig.UseNewInputSystem.Value ? GetInputLineNew(server, prevMessages) : Console.ReadLine(); + string message = server.ServerConfig.UseNewInputSystem.Value ? GetInputLineNew(server, prevMessages) : Console.ReadLine(); - if (string.IsNullOrEmpty(message)) continue; + if (string.IsNullOrEmpty(message)) continue; - server.Write($">>> {message}", ConsoleColor.DarkMagenta); + server.Write($">>> {message}", ConsoleColor.DarkMagenta); - string[] messageSplit = message.Split(Separator, StringSplitOptions.RemoveEmptyEntries); - if (!messageSplit.Any()) continue; + string[] messageSplit = message.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + if (messageSplit.IsEmpty()) continue; - bool callServer = true; - server.commands.TryGetValue(messageSplit[0].ToLower().Trim(), out ICommand command); - if (command != null) - { - command.OnCall(messageSplit.Skip(1).Take(messageSplit.Length - 1).ToArray()); - callServer = command.PassToGame(); + bool callServer = true; + server.commands.TryGetValue(messageSplit[0].ToLower().Trim(), out ICommand command); + if (command != null) + { + command.OnCall(messageSplit.Skip(1).Take(messageSplit.Length - 1).ToArray()); + callServer = command.PassToGame(); + } + + if (callServer) server.SendMessage(message); } - if (callServer) server.SendMessage(message); + ResetInputParams(); + } + catch (ThreadInterruptedException) + { + // Exit the Thread immediately if interrupted } - - ResetInputParams(); } public static string GetInputLineNew(Server server, ShiftingList prevMessages) @@ -97,7 +108,7 @@ public static string GetInputLineNew(Server server, ShiftingList prevMessages) switch (key.Key) { case ConsoleKey.Backspace: - if (messageCursor > 0 && message.Any()) + if (messageCursor > 0 && !message.IsEmpty()) message = message.Remove(--messageCursor, 1); break; @@ -147,11 +158,11 @@ public static string GetInputLineNew(Server server, ShiftingList prevMessages) break; case ConsoleKey.PageUp: - messageCursor -= SectionBufferWidth - (LeftSideIndicatorLength + RightSideIndicatorLength); + messageCursor -= SectionBufferWidth - TotalIndicatorLength; break; case ConsoleKey.PageDown: - messageCursor += SectionBufferWidth - (LeftSideIndicatorLength + RightSideIndicatorLength); + messageCursor += SectionBufferWidth - TotalIndicatorLength; break; default: diff --git a/MultiAdmin/ServerIO/OutputHandler.cs b/MultiAdmin/ServerIO/OutputHandler.cs index 2130477..2d64076 100644 --- a/MultiAdmin/ServerIO/OutputHandler.cs +++ b/MultiAdmin/ServerIO/OutputHandler.cs @@ -1,9 +1,9 @@ using System; using System.IO; -using System.Linq; using System.Text.RegularExpressions; using System.Threading; using MultiAdmin.ConsoleTools; +using MultiAdmin.Utility; namespace MultiAdmin.ServerIO { @@ -74,7 +74,7 @@ private void OnMapiCreated(FileSystemEventArgs e, Server server) } } - private void ProcessFile(Server server, string file) + public void ProcessFile(Server server, string file) { string stream = string.Empty; string command = "open"; @@ -84,7 +84,7 @@ private void ProcessFile(Server server, string file) // Lock this object to wait for this event to finish before trying to read another file lock (this) { - for (int attempts = 0; attempts < 100; attempts++) + for (int attempts = 0; attempts < server.ServerConfig.OutputReadAttempts.Value; attempts++) { try { @@ -107,12 +107,12 @@ private void ProcessFile(Server server, string file) catch (UnauthorizedAccessException e) { Program.LogDebugException(nameof(ProcessFile), e); - Thread.Sleep(5); + Thread.Sleep(8); } catch (Exception e) { Program.LogDebugException(nameof(ProcessFile), e); - Thread.Sleep(2); + Thread.Sleep(5); } } } @@ -209,7 +209,7 @@ private void ProcessFile(Server server, string file) // This should work fine with older ServerMod versions too string[] streamSplit = stream.Replace("ServerMod - Version", string.Empty).Split('-'); - if (streamSplit.Any()) + if (!streamSplit.IsEmpty()) { server.serverModVersion = streamSplit[0].Trim(); server.serverModBuild = (streamSplit.Length > 1 ? streamSplit[1] : "A").Trim(); diff --git a/MultiAdmin/ServerIO/ShiftingList.cs b/MultiAdmin/ServerIO/ShiftingList.cs index f906b9c..f18703b 100644 --- a/MultiAdmin/ServerIO/ShiftingList.cs +++ b/MultiAdmin/ServerIO/ShiftingList.cs @@ -20,7 +20,7 @@ private void LimitLength() { while (Items.Count > MaxCount) { - Items.RemoveAt(Items.Count - 1); + RemoveFromEnd(); } } @@ -34,8 +34,23 @@ public void Add(string item, int index = 0) } } - /* - public void Remove(int index) + public void Remove(string item) + { + lock (Items) + { + Items.Remove(item); + } + } + + public void RemoveFromEnd() + { + lock (Items) + { + Items.RemoveAt(Items.Count - 1); + } + } + + public void RemoveAt(int index) { lock (Items) { @@ -47,10 +62,9 @@ public void Replace(string item, int index = 0) { lock (Items) { - Remove(index); + RemoveAt(index); Add(item, index); } } - */ } } diff --git a/MultiAdmin/ServerIO/StringSections.cs b/MultiAdmin/ServerIO/StringSections.cs index 6dc027f..d324730 100644 --- a/MultiAdmin/ServerIO/StringSections.cs +++ b/MultiAdmin/ServerIO/StringSections.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; -using System.Linq; +using System.Text; using MultiAdmin.ConsoleTools; +using MultiAdmin.Utility; namespace MultiAdmin.ServerIO { @@ -42,7 +43,7 @@ public StringSections(StringSection[] sections) return null; } - public static StringSections FromString(string fullString, int sectionLength, ColoredMessage leftIndicator, ColoredMessage rightIndicator, ColoredMessage sectionBase) + public static StringSections FromString(string fullString, int sectionLength, ColoredMessage leftIndicator = null, ColoredMessage rightIndicator = null, ColoredMessage sectionBase = null) { List sections = new List(); @@ -57,14 +58,14 @@ public static StringSections FromString(string fullString, int sectionLength, Co int sectionStartIndex = 0; // The text of the current section being created - string curSecString = string.Empty; + StringBuilder curSecBuilder = new StringBuilder(); for (int i = 0; i < fullString.Length; i++) { - curSecString += fullString[i]; + curSecBuilder.Append(fullString[i]); // If the section is less than the smallest possible section size, skip processing - if (curSecString.Length < sectionLength - ((leftIndicator?.Length ?? 0) + (rightIndicator?.Length ?? 0))) continue; + if (curSecBuilder.Length < sectionLength - ((leftIndicator?.Length ?? 0) + (rightIndicator?.Length ?? 0))) continue; // Decide what the left indicator text should be accounting for the leftmost section ColoredMessage leftIndicatorSection = sections.Count > 0 ? leftIndicator : null; @@ -72,30 +73,30 @@ public static StringSections FromString(string fullString, int sectionLength, Co ColoredMessage rightIndicatorSection = i < fullString.Length - (1 + (rightIndicator?.Length ?? 0)) ? rightIndicator : null; // Check the section length against the final section length - if (curSecString.Length >= sectionLength - ((leftIndicatorSection?.Length ?? 0) + (rightIndicatorSection?.Length ?? 0))) + if (curSecBuilder.Length >= sectionLength - ((leftIndicatorSection?.Length ?? 0) + (rightIndicatorSection?.Length ?? 0))) { // Copy the section base message and replace the text ColoredMessage section = sectionBase.Clone(); - section.text = curSecString; + section.text = curSecBuilder.ToString(); // Instantiate the section with the final parameters sections.Add(new StringSection(section, leftIndicatorSection, rightIndicatorSection, sectionStartIndex, i)); // Reset the current section being worked on - curSecString = string.Empty; + curSecBuilder.Clear(); sectionStartIndex = i + 1; } } // If there's still text remaining in a section that hasn't been processed, add it as a section - if (curSecString.Any()) + if (!curSecBuilder.IsEmpty()) { // Only decide for the left indicator, as this last section will always be the rightmost section ColoredMessage leftIndicatorSection = sections.Count > 0 ? leftIndicator : null; // Copy the section base message and replace the text ColoredMessage section = sectionBase.Clone(); - section.text = curSecString; + section.text = curSecBuilder.ToString(); // Instantiate the section with the final parameters sections.Add(new StringSection(section, leftIndicatorSection, null, sectionStartIndex, fullString.Length)); diff --git a/MultiAdmin/Utility/EmptyExtensions.cs b/MultiAdmin/Utility/EmptyExtensions.cs new file mode 100644 index 0000000..98b469c --- /dev/null +++ b/MultiAdmin/Utility/EmptyExtensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MultiAdmin.Utility +{ + public static class EmptyExtensions + { + public static bool IsEmpty(this IEnumerable enumerable) + { + return !enumerable.Any(); + } + + public static bool IsNullOrEmpty(this IEnumerable enumerable) + { + return enumerable?.IsEmpty() ?? true; + } + + public static bool IsEmpty(this Array array) + { + return array.Length <= 0; + } + + public static bool IsNullOrEmpty(this Array array) + { + return array?.IsEmpty() ?? true; + } + + public static bool IsEmpty(this T[] array) + { + return array.Length <= 0; + } + + public static bool IsNullOrEmpty(this T[] array) + { + return array?.IsEmpty() ?? true; + } + + public static bool IsEmpty(this ICollection collection) + { + return collection.Count <= 0; + } + + public static bool IsNullOrEmpty(this ICollection collection) + { + return collection?.IsEmpty() ?? true; + } + + public static bool IsEmpty(this List list) + { + return list.Count <= 0; + } + + public static bool IsNullOrEmpty(this List list) + { + return list?.IsEmpty() ?? true; + } + + public static bool IsEmpty(this Dictionary dictionary) + { + return dictionary.Count <= 0; + } + + public static bool IsNullOrEmpty(this Dictionary dictionary) + { + return dictionary?.IsEmpty() ?? true; + } + + public static bool IsEmpty(this StringBuilder stringBuilder) + { + return stringBuilder.Length <= 0; + } + + public static bool IsNullOrEmpty(this StringBuilder stringBuilder) + { + return stringBuilder?.IsEmpty() ?? true; + } + } +} diff --git a/MultiAdmin/Utility/StringExtensions.cs b/MultiAdmin/Utility/StringExtensions.cs new file mode 100644 index 0000000..1cf0b6f --- /dev/null +++ b/MultiAdmin/Utility/StringExtensions.cs @@ -0,0 +1,32 @@ +using System; + +namespace MultiAdmin.Utility +{ + public static class StringExtensions + { + public static bool Equals(this string input, string value, int startIndex, int count) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + if (startIndex < 0 || startIndex > input.Length) + throw new ArgumentOutOfRangeException(nameof(startIndex)); + if (count < 0 || startIndex > input.Length - count) + throw new ArgumentOutOfRangeException(nameof(count)); + + for (int i = 0; i < count; i++) + { + int curIndex = startIndex + i; + + if (input[curIndex] != value[i]) + return false; + } + + return true; + } + + public static bool Equals(this string input, string value, int startIndex) + { + return Equals(input, value, startIndex, input.Length - startIndex); + } + } +} diff --git a/MultiAdmin/Utils.cs b/MultiAdmin/Utility/Utils.cs similarity index 58% rename from MultiAdmin/Utils.cs rename to MultiAdmin/Utility/Utils.cs index 1add09d..724606b 100644 --- a/MultiAdmin/Utils.cs +++ b/MultiAdmin/Utility/Utils.cs @@ -1,10 +1,9 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using MultiAdmin.ConsoleTools; -namespace MultiAdmin +namespace MultiAdmin.Utility { public static class Utils { @@ -52,12 +51,12 @@ public static ColoredMessage[] TimeStampMessage(ColoredMessage message, ConsoleC public static string GetFullPathSafe(string path) { - return !string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(path.Trim()) ? Path.GetFullPath(path) : null; + return string.IsNullOrWhiteSpace(path) ? null : Path.GetFullPath(path); } private const char WildCard = '*'; - private static bool StringMatches(string input, string pattern) + public static bool StringMatches(string input, string pattern) { if (input == null && pattern == null) return true; @@ -65,16 +64,16 @@ private static bool StringMatches(string input, string pattern) if (pattern == null) return false; - if (pattern.Any() && pattern == new string(WildCard, pattern.Length)) + if (!pattern.IsEmpty() && pattern == new string(WildCard, pattern.Length)) return true; if (input == null) return false; - if (!input.Any() && !pattern.Any()) + if (input.IsEmpty() && pattern.IsEmpty()) return true; - if (!input.Any() || !pattern.Any()) + if (input.IsEmpty() || pattern.IsEmpty()) return false; string[] wildCardSections = pattern.Split(WildCard); @@ -82,49 +81,59 @@ private static bool StringMatches(string input, string pattern) int matchIndex = 0; foreach (string wildCardSection in wildCardSections) { - if (!wildCardSection.Any()) + // If there's a wildcard with nothing on the other side + if (wildCardSection.IsEmpty()) + { continue; + } - if (matchIndex < 0 || matchIndex >= pattern.Length) + if (matchIndex < 0 || matchIndex >= input.Length) return false; - try - { - // new ColoredMessage($"Debug: Matching \"{wildCardSection}\" with \"{input.Substring(matchIndex)}\"...").WriteLine(); - - matchIndex = input.IndexOf(wildCardSection, matchIndex); + Program.LogDebug(nameof(StringMatches), $"Matching \"{wildCardSection}\" with \"{input.Substring(matchIndex)}\"...");; - if (matchIndex < 0) + if (matchIndex <= 0 && pattern[0] != WildCard) + { + if (!input.Equals(wildCardSection, matchIndex, wildCardSection.Length)) return false; matchIndex += wildCardSection.Length; - // new ColoredMessage($"Debug: Match found! Match end index at {matchIndex}.").WriteLine(); + Program.LogDebug(nameof(StringMatches), $"Exact match found! Match end index at {matchIndex}."); } - catch + else { - return false; + try + { + matchIndex = input.IndexOf(wildCardSection, matchIndex); + + if (matchIndex < 0) + return false; + + matchIndex += wildCardSection.Length; + + Program.LogDebug(nameof(StringMatches), $"Match found! Match end index at {matchIndex}."); + } + catch + { + return false; + } } } - // new ColoredMessage($"Debug: Done matching. Matches = {matchIndex == input.Length || !wildCardSections[wildCardSections.Length - 1].Any()}.").WriteLine(); + Program.LogDebug(nameof(StringMatches), $"Done matching. Matches = {matchIndex == input.Length || wildCardSections[wildCardSections.Length - 1].IsEmpty()}."); - return matchIndex == input.Length || !wildCardSections[wildCardSections.Length - 1].Any(); + return matchIndex == input.Length || wildCardSections[wildCardSections.Length - 1].IsEmpty(); } - private static bool FileNamesContains(IEnumerable namePatterns, string input) + public static bool InputMatchesAnyPattern(string input, params string[] namePatterns) { - return namePatterns != null && namePatterns.Any(namePattern => StringMatches(input, namePattern)); - } - - private static bool IsArrayNullOrEmpty(string[] array) - { - return array == null || !array.Any(); + return !namePatterns.IsNullOrEmpty() && namePatterns.Any(namePattern => StringMatches(input, namePattern)); } private static bool PassesWhitelistAndBlacklist(string toCheck, string[] whitelist = null, string[] blacklist = null) { - return (IsArrayNullOrEmpty(whitelist) || FileNamesContains(whitelist, toCheck)) && (IsArrayNullOrEmpty(blacklist) || !FileNamesContains(blacklist, toCheck)); + return (whitelist.IsNullOrEmpty() || InputMatchesAnyPattern(toCheck, whitelist)) && (blacklist.IsNullOrEmpty() || !InputMatchesAnyPattern(toCheck, blacklist)); } public static void CopyAll(DirectoryInfo source, DirectoryInfo target, string[] fileWhitelist = null, string[] fileBlacklist = null) @@ -159,5 +168,33 @@ public static void CopyAll(string source, string target, string[] fileWhitelist { CopyAll(new DirectoryInfo(source), new DirectoryInfo(target), fileWhitelist, fileBlacklist); } + + public static int CompareVersionStrings(string firstVersion, string secondVersion) + { + if (firstVersion == null || secondVersion == null) + return -1; + + string[] firstVersionNums = firstVersion.Split('.'); + string[] secondVersionNums = secondVersion.Split('.'); + int minVersionLength = Math.Min(firstVersionNums.Length, secondVersionNums.Length); + + for (int i = 0; i < minVersionLength; i++) + { + if (!int.TryParse(firstVersionNums[i], out int first) || !int.TryParse(secondVersionNums[i], out int second)) + continue; + + if (first > second) + { + return 1; + } + + if (first < second) + { + return -1; + } + } + + return 0; + } } } diff --git a/MultiAdminTests/MultiAdminTests.csproj b/MultiAdminTests/MultiAdminTests.csproj new file mode 100644 index 0000000..1893f58 --- /dev/null +++ b/MultiAdminTests/MultiAdminTests.csproj @@ -0,0 +1,109 @@ + + + + + Debug + AnyCPU + {D56F8899-C7BB-4ADE-A62C-DEC4DC8C2EE8} + Library + Properties + MultiAdminTests + MultiAdminTests + v4.7.1 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + + + + + + + + + + {8384BF3C-5FC8-4395-A3DE-440C6C531D36} + MultiAdmin + + + + + + + False + + + False + + + False + + + False + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + \ No newline at end of file diff --git a/MultiAdminTests/Properties/AssemblyInfo.cs b/MultiAdminTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6557efc --- /dev/null +++ b/MultiAdminTests/Properties/AssemblyInfo.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using MultiAdmin; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle(nameof(MultiAdminTests))] +[assembly: AssemblyDescription("A set of Unit Tests for " + nameof(MultiAdmin) + " v" + Program.MaVersion)] +[assembly: AssemblyProduct(nameof(MultiAdminTests))] +[assembly: AssemblyCopyright("Copyright © Grover 2019")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion(Program.MaVersion)] diff --git a/MultiAdminTests/ServerIO/ShiftingListTests.cs b/MultiAdminTests/ServerIO/ShiftingListTests.cs new file mode 100644 index 0000000..61cf106 --- /dev/null +++ b/MultiAdminTests/ServerIO/ShiftingListTests.cs @@ -0,0 +1,89 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MultiAdmin.ServerIO; + +namespace MultiAdminTests.ServerIO +{ + [TestClass] + public class ShiftingListTests + { + [TestMethod] + public void ShiftingListTest() + { + const int maxCount = 2; + ShiftingList shiftingList = new ShiftingList(maxCount); + + Assert.AreEqual(shiftingList.MaxCount, maxCount); + } + + [TestMethod] + public void AddTest() + { + const int maxCount = 2; + const int entriesToAdd = 6; + ShiftingList shiftingList = new ShiftingList(maxCount); + + for (int i = 0; i < entriesToAdd; i++) + { + shiftingList.Add($"Test{i}"); + } + + Assert.AreEqual(shiftingList.Count, maxCount); + + for (int i = 0; i < shiftingList.Count; i++) + { + Assert.AreEqual(shiftingList[i], $"Test{entriesToAdd - i - 1}"); + } + } + + [TestMethod] + public void RemoveFromEndTest() + { + const int maxCount = 6; + const int entriesToRemove = 2; + ShiftingList shiftingList = new ShiftingList(maxCount); + + for (int i = 0; i < maxCount; i++) + { + shiftingList.Add($"Test{i}"); + } + + for (int i = 0; i < entriesToRemove; i++) + { + shiftingList.RemoveFromEnd(); + } + + Assert.AreEqual(shiftingList.Count, Math.Max(maxCount - entriesToRemove, 0)); + + for (int i = 0; i < shiftingList.Count; i++) + { + Assert.AreEqual(shiftingList[i], $"Test{maxCount - i - 1}"); + } + } + + [TestMethod] + public void ReplaceTest() + { + const int maxCount = 6; + const int indexToReplace = 2; + ShiftingList shiftingList = new ShiftingList(maxCount); + + for (int i = 0; i < maxCount; i++) + { + shiftingList.Add($"Test{i}"); + } + + for (int i = 0; i < maxCount; i++) + { + if (i == indexToReplace) + { + shiftingList.Replace("Replaced", indexToReplace); + } + } + + Assert.AreEqual(shiftingList.Count, maxCount); + + Assert.AreEqual(shiftingList[indexToReplace], "Replaced"); + } + } +} diff --git a/MultiAdminTests/ServerIO/StringSectionsTests.cs b/MultiAdminTests/ServerIO/StringSectionsTests.cs new file mode 100644 index 0000000..7e5abb3 --- /dev/null +++ b/MultiAdminTests/ServerIO/StringSectionsTests.cs @@ -0,0 +1,38 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MultiAdmin.ServerIO; + +namespace MultiAdminTests.ServerIO +{ + [TestClass] + public class StringSectionsTests + { + [TestMethod] + public void FromStringTest() + { + string[] expectedSections = + { + "te", + "st", + " s", + "tr", + "in", + "g" + }; + + StringSections sections = StringSections.FromString("test string", 2); + + Assert.IsNotNull(sections); + Assert.IsNotNull(sections.Sections); + + Assert.IsTrue(sections.Sections.Length == expectedSections.Length, $"Expected sections length \"{expectedSections.Length}\", got \"{sections.Sections.Length}\""); + + for (int i = 0; i < expectedSections.Length; i++) + { + string expected = expectedSections[i]; + string result = sections.Sections[i].Text?.text; + + Assert.AreEqual(expected, result, $"Failed at section index {i}: Expected section text to be \"{expected ?? "null"}\", got \"{result ?? "null"}\""); + } + } + } +} diff --git a/MultiAdminTests/Utility/StringExtensionsTests.cs b/MultiAdminTests/Utility/StringExtensionsTests.cs new file mode 100644 index 0000000..7d94475 --- /dev/null +++ b/MultiAdminTests/Utility/StringExtensionsTests.cs @@ -0,0 +1,24 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MultiAdmin.Utility; + +namespace MultiAdminTests.Utility +{ + [TestClass] + public class StringExtensionsTests + { + [TestMethod] + public void EqualsTest() + { + Assert.IsTrue("test".Equals("test", startIndex: 0)); + Assert.IsFalse("test".Equals("other", startIndex: 0)); + + Assert.IsTrue("test".Equals("st", startIndex: 2)); + Assert.IsTrue("test".Equals("te", 0, 2)); + + Assert.IsFalse("test".Equals("te", startIndex: 2)); + Assert.IsFalse("test".Equals("st", 0, 2)); + + Assert.IsTrue("test".Equals("es", 1, 2)); + } + } +} diff --git a/MultiAdminTests/Utility/UtilsTests.cs b/MultiAdminTests/Utility/UtilsTests.cs new file mode 100644 index 0000000..eda349a --- /dev/null +++ b/MultiAdminTests/Utility/UtilsTests.cs @@ -0,0 +1,119 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MultiAdmin.Utility; + +namespace MultiAdminTests.Utility +{ + [TestClass] + public class UtilsTests + { + private struct StringMatchingTemplate + { + public readonly string input; + public readonly string pattern; + + public readonly bool expectedResult; + + public StringMatchingTemplate(string input, string pattern, bool expectedResult) + { + this.input = input; + this.pattern = pattern; + this.expectedResult = expectedResult; + } + } + + private struct CompareVersionTemplate + { + public readonly string firstVersion; + public readonly string secondVersion; + + public readonly int expectedResult; + + public CompareVersionTemplate(string firstVersion, string secondVersion, int expectedResult) + { + this.firstVersion = firstVersion; + this.secondVersion = secondVersion; + this.expectedResult = expectedResult; + } + + public bool CheckResult(int result) + { + if (expectedResult == result) + return true; + + if (expectedResult < 0 && result < 0) + return true; + + if (expectedResult > 0 && result > 0) + return true; + + return false; + } + } + + [TestMethod] + public void GetFullPathSafeTest() + { + string result = Utils.GetFullPathSafe(" "); + Assert.IsNull(result, $"Expected \"null\", got \"{result}\""); + } + + [TestMethod] + public void StringMatchesTest() + { + StringMatchingTemplate[] matchTests = + { + new StringMatchingTemplate("test", "*", true), + new StringMatchingTemplate("test", "te*", true), + new StringMatchingTemplate("test", "*st", true), + new StringMatchingTemplate("test", "******", true), + new StringMatchingTemplate("test", "te*t", true), + new StringMatchingTemplate("test", "t**st", true), + new StringMatchingTemplate("test", "s*", false), + new StringMatchingTemplate("longstringtestmessage", "l*s*t*e*g*", true), + }; + + for (int i = 0; i < matchTests.Length; i++) + { + StringMatchingTemplate test = matchTests[i]; + + bool result = Utils.StringMatches(test.input, test.pattern); + + Assert.IsTrue(test.expectedResult == result, $"Failed on test index {i}: Expected \"{test.expectedResult}\", got \"{result}\""); + } + } + + [TestMethod] + public void CompareVersionStringsTest() + { + CompareVersionTemplate[] versionTests = + { + new CompareVersionTemplate("1.0.0.0", "2.0.0.0", -1), + new CompareVersionTemplate("1.0.0.0", "1.0.0.0", 0), + new CompareVersionTemplate("2.0.0.0", "1.0.0.0", 1), + + new CompareVersionTemplate("1.0", "2.0.0.0", -1), + new CompareVersionTemplate("1.0", "1.0.0.0", 0), + new CompareVersionTemplate("2.0", "1.0.0.0", 1), + + new CompareVersionTemplate("1.0.0.0", "2.0", -1), + new CompareVersionTemplate("1.0.0.0", "1.0", 0), + new CompareVersionTemplate("2.0.0.0", "1.0", 1), + + new CompareVersionTemplate("6.0.0.313", "5.18.0", 1), + new CompareVersionTemplate("5.18.0", "6.0.0.313", -1), + + new CompareVersionTemplate("5.18.0", "5.18.0", 0), + new CompareVersionTemplate("5.18", "5.18.0", 0) + }; + + for (int i = 0; i < versionTests.Length; i++) + { + CompareVersionTemplate test = versionTests[i]; + + int result = Utils.CompareVersionStrings(test.firstVersion, test.secondVersion); + + Assert.IsTrue(test.CheckResult(result), $"Failed on test index {i}: Expected \"{test.expectedResult}\", got \"{result}\""); + } + } + } +} diff --git a/MultiAdminTests/packages.config b/MultiAdminTests/packages.config new file mode 100644 index 0000000..1eae809 --- /dev/null +++ b/MultiAdminTests/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/README.md b/README.md index eedc8ee..d3930ca 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,14 @@ Make sure that you are running Mono 5.18.0 or higher, otherwise you might have i 4. Optional: Create a file named `scp_multiadmin.cfg` within your server's folder for configuring MultiAdmin specifically for that server ## Features -- Autoscale: Auto-starts a new server once this one becomes full (Requires ServerMod to function fully) - Config Reload: Reloads the MultiAdmin configuration file - Exit Command: Adds a graceful exit command +- Folder Copy Round Queue: Copies files from folders in a queue - Help: Display a full list of MultiAdmin commands and in game commands - Stop Server When Inactive: Stops the server after a period inactivity - Restart On Low Memory: Restarts the server if the working memory becomes too low -- Restart On Low Memory at Round End: Restarts the server if the working memory becomes too low at the end of the round - ModLog: Logs admin messages to separate file, or prints them -- MultiAdminInfo: Prints MultiAdmin license information +- MultiAdminInfo: Prints MultiAdmin license and version information - New: Adds a command to start a new server given a config folder - Restart Command: Allows the game to be restarted without restarting MultiAdmin - Restart Next Round: Restarts the server after the current round ends @@ -46,7 +45,7 @@ This does not include ServerMod or ingame commands, for a full list type `HELP` - EXIT: Exits the server - GITHUBGEN [FILE LOCATION]: Generates a github .md file outlining all the features/commands - HELP: Prints out available commands and their function -- INFO: Prints MultiAdmin license information +- INFO: Prints MultiAdmin license and version information - NEW : Starts a new server with the given Server ID - RESTART: Restarts the game server (MultiAdmin will not restart, just the game) - RESTARTNEXTROUND: Restarts the server at the end of this round @@ -71,7 +70,7 @@ disable_config_validation | Boolean | False | Disable the config validator share_non_configs | Boolean | True | Makes all files other than the config files store in AppData multiadmin_nolog | Boolean | False | Disable logging to file multiadmin_debug_log | Boolean | True | Enables MultiAdmin debug logging, this logs to a separate file than any other logs -multiadmin_debug_log_blacklist | String List | ProcessFile | Which tags to block for MultiAdmin debug logging +multiadmin_debug_log_blacklist | String List | ProcessFile, StringMatches | Which tags to block for MultiAdmin debug logging multiadmin_debug_log_whitelist | String List | **Empty** | Which tags to log for MultiAdmin debug logging (Defaults to logging all if none are provided) use_new_input_system | Boolean | True | Whether to use the new input system, if false, the original input system will be used port | Unsigned Integer | 7777 | The port for the server to use (Preparing for next game release, currently does nothing) @@ -84,15 +83,19 @@ folder_copy_round_queue_blacklist | String List | **Empty** | The list of file n randomize_folder_copy_round_queue | Boolean | False | Whether to randomize the order of entries in `folder_copy_round_queue` log_mod_actions_to_own_file | Boolean | False | Logs admin messages to separate file manual_start | Boolean | False | Whether or not to start the server automatically when launching MultiAdmin -max_memory | Float | 2048 | The amount of memory in megabytes for MultiAdmin to check against -restart_low_memory | Float | 400 | Restart if the game's remaining memory falls below this value in megabytes -restart_low_memory_roundend | Float | 450 | Restart at the end of the round if the game's remaining memory falls below this value in megabytes +max_memory | Decimal | 2048 | The amount of memory in megabytes for MultiAdmin to check against +restart_low_memory | Decimal | 400 | Restart if the game's remaining memory falls below this value in megabytes +restart_low_memory_roundend | Decimal | 450 | Restart at the end of the round if the game's remaining memory falls below this value in megabytes max_players | Integer | 20 | The number of players to display as the maximum for the server (within MultiAdmin, not in-game) +output_read_attempts | Integer | 100 | The number of times to attempt reading a message from the server before giving up random_input_colors | Boolean | False | Randomize the new input system's colors every time a message is input restart_every_num_rounds | Integer | -1 | Restart the server every number of rounds -safe_server_shutdown | Boolean | True | When MultiAdmin closes, if this is true, MultiAdmin will attempt to safely shutdown all the servers -server_restart_timeout | Float | 10 | The time in seconds before MultiAdmin forces a server restart if it doesn't respond to the regular restart command -server_stop_timeout | Float | 10 | The time in seconds before MultiAdmin forces a server shutdown if it doesn't respond to the regular shutdown command +restart_every_num_rounds_counting | Boolean | False | Whether to print the count of rounds passed after each round if the server is set to restart after a number of rounds +safe_server_shutdown | Boolean | True | When MultiAdmin closes, if this is true, MultiAdmin will attempt to safely shutdown all servers +safe_shutdown_check_delay | Integer | 100 | The time in milliseconds between checking if a server is still running when safely shutting down +safe_shutdown_timeout | Integer | 10000 | The time in milliseconds before MultiAdmin gives up on safely shutting down a server +server_restart_timeout | Double | 10 | The time in seconds before MultiAdmin forces a server restart if it doesn't respond to the regular restart command +server_stop_timeout | Double | 10 | The time in seconds before MultiAdmin forces a server shutdown if it doesn't respond to the regular shutdown command servers_folder | String | servers | The location of the `servers` folder for MultiAdmin to load multiple server configurations from set_title_bar | Boolean | True | Whether to set the console window's titlebar, if false, this feature won't be used shutdown_when_empty_for | Integer | -1 | Shutdown the server once a round hasn't started in a number of seconds