diff --git a/.gitignore b/.gitignore index 1a269a1..1579cdd 100644 --- a/.gitignore +++ b/.gitignore @@ -47,9 +47,10 @@ Generated\ Files/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUNIT +# NUnit *.VisualState.xml TestResult.xml +nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ @@ -72,7 +73,6 @@ StyleCopReport.xml *_p.c *_h.h *.ilk -*.meta *.obj *.iobj *.pch @@ -190,6 +190,8 @@ PublishScripts/ # NuGet Packages *.nupkg +# NuGet Symbol Packages +*.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. @@ -265,7 +267,9 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser -*- Backup*.rdl +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ @@ -447,6 +451,8 @@ test-results/ # .idea/modules.xml # .idea/*.iml # .idea/modules +# *.iml +# *.ipr # CMake cmake-build-*/ @@ -481,12 +487,10 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser -# JetBrains templates -**___jb_tmp___ - ### Windows ### # Windows thumbnail cache files Thumbs.db +Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db diff --git a/LICENSE b/LICENSE index 967412d..684f7f0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Grover +Copyright (c) 2020 Grover Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MultiAdmin/.gitignore b/MultiAdmin/.gitignore new file mode 100644 index 0000000..1579cdd --- /dev/null +++ b/MultiAdmin/.gitignore @@ -0,0 +1,516 @@ + +# Created by https://www.gitignore.io/api/git,rider,linux,macos,csharp,windows,monodevelop +# Edit at https://www.gitignore.io/?templates=git,rider,linux,macos,csharp,windows,monodevelop + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### MonoDevelop ### +#User Specific +*.usertasks + +#Mono Project Files +*.resources +test-results/ + +### Rider ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/git,rider,linux,macos,csharp,windows,monodevelop diff --git a/MultiAdmin/Config/MultiAdminConfig.cs b/MultiAdmin/Config/MultiAdminConfig.cs index a1073a0..7267f07 100644 --- a/MultiAdmin/Config/MultiAdminConfig.cs +++ b/MultiAdmin/Config/MultiAdminConfig.cs @@ -30,6 +30,10 @@ public class MultiAdminConfig : InheritableConfigRegister new ConfigEntry("share_non_configs", true, "Share Non-Configs", "Makes all files other than the config files store in AppData"); + public ConfigEntry LogLocation { get; } = + new ConfigEntry("multiadmin_log_location", "logs", + "MultiAdmin Log Location", "The folder that MultiAdmin will store logs in (a directory)"); + public ConfigEntry NoLog { get; } = new ConfigEntry("multiadmin_nolog", false, "MultiAdmin No-Logging", "Disable logging to file"); @@ -39,7 +43,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[] {nameof(OutputHandler.ProcessFile), nameof(Utils.StringMatches)}, + new ConfigEntry("multiadmin_debug_log_blacklist", new string[] {nameof(OutputHandler.HandleMessage), nameof(Utils.StringMatches), nameof(ServerSocket.MessageListener) }, "MultiAdmin Debug Logging Blacklist", "Which tags to block for MultiAdmin debug logging"); public ConfigEntry DebugLogWhitelist { get; } = @@ -82,10 +86,6 @@ public class MultiAdminConfig : InheritableConfigRegister new ConfigEntry("randomize_folder_copy_round_queue", false, "Randomize Folder Copy Round Queue", "Whether to randomize the order of entries in `folder_copy_round_queue`"); - public ConfigEntry LogModActionsToOwnFile { get; } = - new ConfigEntry("log_mod_actions_to_own_file", false, - "Log Mod Actions to Own File", "Logs admin messages to separate file"); - public ConfigEntry ManualStart { get; } = new ConfigEntry("manual_start", false, "Manual Start", "Whether or not to start the server automatically when launching MultiAdmin"); @@ -102,14 +102,6 @@ public class MultiAdminConfig : InheritableConfigRegister 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"); @@ -158,13 +150,9 @@ public class MultiAdminConfig : InheritableConfigRegister new ConfigEntry("set_title_bar", true, "Set Title Bar", "Whether to set the console window's titlebar, if false, this feature won't be used"); - public ConfigEntry ShutdownWhenEmptyFor { get; } = - new ConfigEntry("shutdown_when_empty_for", -1, - "Shutdown When Empty For", "Shutdown the server once a round hasn't started in a number of seconds"); - public ConfigEntry StartConfigOnFull { get; } = new ConfigEntry("start_config_on_full", "", - "Start Config on Full", "Start server with this config folder once the server becomes full [Requires ServerMod]"); + "Start Config on Full", "Start server with this config folder once the server becomes full [Requires Modding]"); #endregion diff --git a/MultiAdmin/ConsoleTools/ColoredConsole.cs b/MultiAdmin/ConsoleTools/ColoredConsole.cs index cd979e2..19acaac 100644 --- a/MultiAdmin/ConsoleTools/ColoredConsole.cs +++ b/MultiAdmin/ConsoleTools/ColoredConsole.cs @@ -178,47 +178,47 @@ public void WriteLine(bool clearConsoleLine = false) } } - public static class ColoredMessageEnumerableExtensions + public static class ColoredMessageArrayExtensions { - private static string JoinTextIgnoreNull(IEnumerable objects) + private static string JoinTextIgnoreNull(ColoredMessage[] coloredMessages) { - StringBuilder builder = new StringBuilder(string.Empty); + StringBuilder builder = new StringBuilder(""); - foreach (object o in objects) + foreach (ColoredMessage coloredMessage in coloredMessages) { - if (o != null) - builder.Append(o); + if (coloredMessage != null) + builder.Append(coloredMessage); } return builder.ToString(); } - public static string GetText(this IEnumerable message) + public static string GetText(this ColoredMessage[] message) { return JoinTextIgnoreNull(message); } - public static void Write(this IEnumerable message, bool clearConsoleLine = false) + public static void Write(this ColoredMessage[] message, bool clearConsoleLine = false) { lock (ColoredConsole.WriteLock) { - ColoredConsole.Write(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(message.ToArray()) : message.ToArray()); + ColoredConsole.Write(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(message) : message); } } - public static void WriteLine(this IEnumerable message, bool clearConsoleLine = false) + public static void WriteLine(this ColoredMessage[] message, bool clearConsoleLine = false) { lock (ColoredConsole.WriteLock) { - ColoredConsole.WriteLine(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(message.ToArray()) : message.ToArray()); + ColoredConsole.WriteLine(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(message) : message); } } - public static void WriteLines(this IEnumerable message, bool clearConsoleLine = false) + public static void WriteLines(this ColoredMessage[] message, bool clearConsoleLine = false) { lock (ColoredConsole.WriteLock) { - ColoredConsole.WriteLines(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(message.ToArray()) : message.ToArray()); + ColoredConsole.WriteLines(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(message) : message); } } } diff --git a/MultiAdmin/ConsoleTools/ConsoleUtils.cs b/MultiAdmin/ConsoleTools/ConsoleUtils.cs index f16edd9..705e539 100644 --- a/MultiAdmin/ConsoleTools/ConsoleUtils.cs +++ b/MultiAdmin/ConsoleTools/ConsoleUtils.cs @@ -68,12 +68,6 @@ public static ColoredMessage[] ClearConsoleLine(ColoredMessage[] message) return message; } - public static List ClearConsoleLine(List message) - { - ClearConsoleLine(message?.GetText()); - return message; - } - #endregion } } diff --git a/MultiAdmin/EventInterfaces.cs b/MultiAdmin/EventInterfaces.cs index 35111eb..7f73d22 100644 --- a/MultiAdmin/EventInterfaces.cs +++ b/MultiAdmin/EventInterfaces.cs @@ -44,30 +44,11 @@ public interface IEventTick : IMAEvent void OnTick(); } - public interface IServerMod : IMAEvent - { - } - - public interface IEventServerFull : IServerMod + public interface IEventServerFull : IMAEvent { void OnServerFull(); } - public interface IEventPlayerConnect : IServerMod - { - void OnPlayerConnect(string name); - } - - public interface IEventPlayerDisconnect : IServerMod - { - void OnPlayerDisconnect(string name); - } - - public interface IEventAdminAction : IServerMod - { - void OnAdminAction(string message); - } - public interface ICommand { void OnCall(string[] args); diff --git a/MultiAdmin/Features/EventTest.cs b/MultiAdmin/Features/EventTest.cs index e820ce1..beb6918 100644 --- a/MultiAdmin/Features/EventTest.cs +++ b/MultiAdmin/Features/EventTest.cs @@ -1,7 +1,7 @@ namespace MultiAdmin.Features { - internal class EventTest : Feature, IEventCrash, IEventPlayerConnect, IEventPlayerDisconnect, - IEventRoundEnd, IEventWaitingForPlayers, IEventRoundStart, IEventServerFull, IEventServerPreStart, IEventServerStart, IEventServerStop + internal class EventTest : Feature, IEventCrash, + IEventRoundEnd, IEventWaitingForPlayers, IEventRoundStart, IEventServerPreStart, IEventServerStart, IEventServerStop { public EventTest(Server server) : base(server) { @@ -12,16 +12,6 @@ public void OnCrash() Server.Write("EVENTTEST Crash"); } - public void OnPlayerConnect(string name) - { - Server.Write("EVENTTEST player connect " + name); - } - - public void OnPlayerDisconnect(string name) - { - Server.Write("EVENTTEST player disconnect " + name); - } - public void OnRoundEnd() { Server.Write("EVENTTEST on round end"); diff --git a/MultiAdmin/Features/ExitCommand.cs b/MultiAdmin/Features/ExitCommand.cs index 716a73c..e59aa12 100644 --- a/MultiAdmin/Features/ExitCommand.cs +++ b/MultiAdmin/Features/ExitCommand.cs @@ -21,7 +21,7 @@ public string GetCommandDescription() public string GetUsage() { - return string.Empty; + return ""; } public void OnCall(string[] args) diff --git a/MultiAdmin/Features/GithubGenerator.cs b/MultiAdmin/Features/GithubGenerator.cs index 1b2963f..641d560 100644 --- a/MultiAdmin/Features/GithubGenerator.cs +++ b/MultiAdmin/Features/GithubGenerator.cs @@ -25,7 +25,7 @@ public string GetCommand() public string GetCommandDescription() { - return "Generates a github .md file outlining all the features/commands"; + return "Generates a GitHub README file outlining all the features/commands"; } public string GetUsage() @@ -41,9 +41,9 @@ public void OnCall(string[] args) return; } - string dir = string.Join(" ", args); + string path = Utils.GetFullPathSafe(string.Join(" ", args)); - List lines = new List {"# MultiAdmin", string.Empty, "## Features", string.Empty}; + List lines = new List {"# MultiAdmin", "", "## Features", ""}; foreach (Feature feature in Server.features) { @@ -52,17 +52,17 @@ public void OnCall(string[] args) lines.Add($"- {feature.GetFeatureName()}: {feature.GetFeatureDescription()}"); } - lines.Add(string.Empty); + lines.Add(""); lines.Add("## MultiAdmin Commands"); - lines.Add(string.Empty); + lines.Add(""); foreach (ICommand comm in Server.commands.Values) { lines.Add($"- {(comm.GetCommand() + " " + comm.GetUsage()).Trim()}: {comm.GetCommandDescription()}"); } - lines.Add(string.Empty); + lines.Add(""); lines.Add("## Config Settings"); - lines.Add(string.Empty); + lines.Add(""); lines.Add($"Config Option{ColumnSeparator}Value Type{ColumnSeparator}Default Value{ColumnSeparator}Description"); lines.Add($"---{ColumnSeparator}:---:{ColumnSeparator}:---:{ColumnSeparator}:------:"); @@ -132,7 +132,8 @@ public void OnCall(string[] args) lines.Add(stringBuilder.ToString()); } - File.WriteAllLines(dir, lines); + File.WriteAllLines(path, lines); + Server.Write($"GitHub README written to \"{path}\""); } public bool PassToGame() diff --git a/MultiAdmin/Features/HelpCommand.cs b/MultiAdmin/Features/HelpCommand.cs index 7e27c3a..91a6444 100644 --- a/MultiAdmin/Features/HelpCommand.cs +++ b/MultiAdmin/Features/HelpCommand.cs @@ -48,7 +48,7 @@ public bool PassToGame() public string GetUsage() { - return string.Empty; + return ""; } public override void OnConfigReload() diff --git a/MultiAdmin/Features/InactivityShutdown.cs b/MultiAdmin/Features/InactivityShutdown.cs deleted file mode 100644 index 3d2ffbd..0000000 --- a/MultiAdmin/Features/InactivityShutdown.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using MultiAdmin.Features.Attributes; - -namespace MultiAdmin.Features -{ - [Feature] - internal class InactivityShutdown : Feature, IEventRoundStart, IEventWaitingForPlayers, IEventTick - { - private DateTime queueStartTime; - private int waitFor; - private bool waiting; - - public InactivityShutdown(Server server) : base(server) - { - } - - public void OnWaitingForPlayers() - { - queueStartTime = DateTime.Now; - waiting = true; - } - - public void OnRoundStart() - { - waiting = false; - } - - public void OnTick() - { - if (waitFor > 0 && waiting) - { - int elapsed = (DateTime.Now - queueStartTime).Seconds; - - if (elapsed >= waitFor) - { - Server.Write("Server has been inactive for " + waitFor + " seconds, shutting down"); - Server.StopServer(); - } - } - } - - public override void Init() - { - queueStartTime = DateTime.Now; - } - - public override void OnConfigReload() - { - waitFor = Server.ServerConfig.ShutdownWhenEmptyFor.Value; - } - - - public override string GetFeatureDescription() - { - return "Stops the server after a period inactivity"; - } - - public override string GetFeatureName() - { - return "Stop Server When Inactive"; - } - } -} diff --git a/MultiAdmin/Features/ModLog.cs b/MultiAdmin/Features/ModLog.cs deleted file mode 100644 index a5b4633..0000000 --- a/MultiAdmin/Features/ModLog.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.IO; -using MultiAdmin.Features.Attributes; -using MultiAdmin.Utility; - -namespace MultiAdmin.Features -{ - [Feature] - internal class ModLog : Feature, IEventAdminAction - { - private bool logToOwnFile; - - public ModLog(Server server) : base(server) - { - } - - public void OnAdminAction(string message) - { - if (!logToOwnFile || string.IsNullOrEmpty(Server.ModLogFile) || Server.ServerConfig.NoLog.Value) return; - - lock (this) - { - Directory.CreateDirectory(Server.logDir); - - using (StreamWriter sw = File.AppendText(Server.ModLogFile)) - { - message = Utils.TimeStampMessage(message); - sw.WriteLine(message); - } - } - } - - public override string GetFeatureDescription() - { - return "Logs admin messages to separate file, or prints them"; - } - - public override string GetFeatureName() - { - return "ModLog"; - } - - public override void Init() - { - } - - public override void OnConfigReload() - { - logToOwnFile = Server.ServerConfig.LogModActionsToOwnFile.Value; - } - } -} diff --git a/MultiAdmin/Features/MultiAdminInfo.cs b/MultiAdmin/Features/MultiAdminInfo.cs index 37fa014..07c32c8 100644 --- a/MultiAdmin/Features/MultiAdminInfo.cs +++ b/MultiAdmin/Features/MultiAdminInfo.cs @@ -32,7 +32,7 @@ public string GetCommandDescription() public string GetUsage() { - return string.Empty; + return ""; } public void OnServerPreStart() @@ -50,7 +50,7 @@ public override void OnConfigReload() public void PrintInfo() { - Server.Write($"{nameof(MultiAdmin)} v{Program.MaVersion} (https://github.com/Grover-c13/MultiAdmin/)\nReleased under MIT License Copyright © Grover 2019", ConsoleColor.DarkMagenta); + Server.Write($"{nameof(MultiAdmin)} v{Program.MaVersion} (https://github.com/Grover-c13/MultiAdmin/)\nReleased under MIT License Copyright © Grover 2020", ConsoleColor.DarkMagenta); } public override string GetFeatureDescription() diff --git a/MultiAdmin/Features/NewCommand.cs b/MultiAdmin/Features/NewCommand.cs index de59241..a8cb2c0 100644 --- a/MultiAdmin/Features/NewCommand.cs +++ b/MultiAdmin/Features/NewCommand.cs @@ -63,12 +63,12 @@ public override void OnConfigReload() public override string GetFeatureDescription() { - return "Adds a command to start a new server given a config folder"; + return "Adds a command to start a new server given a config folder and a config to start a new server when one is full [Config Requires Modding]"; } public override string GetFeatureName() { - return "New"; + return "New Server"; } public void OnServerFull() diff --git a/MultiAdmin/Features/Restart.cs b/MultiAdmin/Features/Restart.cs index 1ab1255..ac3947a 100644 --- a/MultiAdmin/Features/Restart.cs +++ b/MultiAdmin/Features/Restart.cs @@ -21,7 +21,7 @@ public string GetCommandDescription() public string GetUsage() { - return string.Empty; + return ""; } public void OnCall(string[] args) diff --git a/MultiAdmin/Features/RestartNextRound.cs b/MultiAdmin/Features/RestartNextRound.cs index d7ba564..8348689 100644 --- a/MultiAdmin/Features/RestartNextRound.cs +++ b/MultiAdmin/Features/RestartNextRound.cs @@ -13,7 +13,7 @@ public RestartNextRound(Server server) : base(server) public string GetCommandDescription() { - return "Restarts the server at the end of this round"; + return "Restarts the server at the end of this round [Requires Modding]"; } @@ -35,7 +35,7 @@ public string GetCommand() public string GetUsage() { - return string.Empty; + return ""; } public void OnRoundEnd() @@ -51,14 +51,9 @@ public override void Init() restart = false; } - public bool RequiresServerMod() - { - return false; - } - public override string GetFeatureDescription() { - return "Restarts the server after the current round ends"; + return "Restarts the server after the current round ends [Requires Modding]"; } public override string GetFeatureName() diff --git a/MultiAdmin/Features/RestartRoundCounter.cs b/MultiAdmin/Features/RestartRoundCounter.cs index 4ba6a4c..d9e5a36 100644 --- a/MultiAdmin/Features/RestartRoundCounter.cs +++ b/MultiAdmin/Features/RestartRoundCounter.cs @@ -45,7 +45,7 @@ public override void OnConfigReload() public override string GetFeatureDescription() { - return "Restarts the server after a number rounds completed"; + return "Restarts the server after a number rounds completed [Requires Modding]"; } public override string GetFeatureName() diff --git a/MultiAdmin/Features/StopNextRound.cs b/MultiAdmin/Features/StopNextRound.cs index 3cffdc6..36a03a8 100644 --- a/MultiAdmin/Features/StopNextRound.cs +++ b/MultiAdmin/Features/StopNextRound.cs @@ -14,7 +14,7 @@ public StopNextRound(Server server) : base(server) public string GetCommandDescription() { - return "Stops the server at the end of this round"; + return "Stops the server at the end of this round [Requires Modding]"; } public void OnCall(string[] args) @@ -35,7 +35,7 @@ public string GetCommand() public string GetUsage() { - return string.Empty; + return ""; } public void OnRoundEnd() @@ -55,14 +55,9 @@ public override void OnConfigReload() { } - public bool RequiresServerMod() - { - return false; - } - public override string GetFeatureDescription() { - return "Stops the server after the current round ends"; + return "Stops the server after the current round ends [Requires Modding]"; } public override string GetFeatureName() diff --git a/MultiAdmin/Features/TitleBar.cs b/MultiAdmin/Features/TitleBar.cs index a73fcc2..3b5a7a8 100644 --- a/MultiAdmin/Features/TitleBar.cs +++ b/MultiAdmin/Features/TitleBar.cs @@ -5,11 +5,8 @@ namespace MultiAdmin.Features { [Feature] - internal class Titlebar : Feature, IEventPlayerConnect, IEventPlayerDisconnect, IEventServerStart + internal class Titlebar : Feature, IEventServerStart { - private int maxPlayers; - private int playerCount; - private int ServerProcessId { get @@ -27,18 +24,6 @@ public Titlebar(Server server) : base(server) { } - public void OnPlayerConnect(string name) - { - playerCount++; - UpdateTitlebar(); - } - - public void OnPlayerDisconnect(string name) - { - playerCount--; - UpdateTitlebar(); - } - public void OnServerStart() { UpdateTitlebar(); @@ -46,8 +31,7 @@ public void OnServerStart() public override string GetFeatureDescription() { - return - "Updates the title bar with instance based information, such as session id and player count (Requires ServerMod to function fully)"; + return "Updates the title bar with instance based information"; } public override string GetFeatureName() @@ -57,13 +41,11 @@ public override string GetFeatureName() public override void Init() { - playerCount = -1; // -1 for the "server" player, once the server starts this will increase to 0. UpdateTitlebar(); } public override void OnConfigReload() { - maxPlayers = Server.ServerConfig.MaxPlayers.Value; UpdateTitlebar(); } @@ -71,8 +53,6 @@ private void UpdateTitlebar() { if (Program.Headless || !Server.ServerConfig.SetTitleBar.Value) return; - int displayPlayerCount = playerCount < 0 ? 0 : playerCount; - List titleBar = new List {$"MultiAdmin {Program.MaVersion}"}; if (!string.IsNullOrEmpty(Server.serverId)) @@ -80,9 +60,9 @@ private void UpdateTitlebar() titleBar.Add($"Config: {Server.serverId}"); } - if (!string.IsNullOrEmpty(Server.SessionId)) + if (Server.SessionSocket != null) { - titleBar.Add($"Session: {Server.SessionId}"); + titleBar.Add($"Console Port: {Server.SessionSocket.Port}"); } if (Server.IsGameProcessRunning) @@ -90,13 +70,6 @@ private void UpdateTitlebar() titleBar.Add($"PID: {ServerProcessId}"); } - titleBar.Add($"{displayPlayerCount}/{maxPlayers}"); - - if (Server.hasServerMod && !string.IsNullOrEmpty(Server.serverModVersion)) - { - titleBar.Add(string.IsNullOrEmpty(Server.serverModBuild) ? $"SMod {Server.serverModVersion}" : $"SMod {Server.serverModVersion}-{Server.serverModBuild}"); - } - try { Console.Title = string.Join(" | ", titleBar); diff --git a/MultiAdmin/Icon.ico b/MultiAdmin/Icon.ico new file mode 100644 index 0000000..05c1438 Binary files /dev/null and b/MultiAdmin/Icon.ico differ diff --git a/MultiAdmin/MultiAdmin.csproj b/MultiAdmin/MultiAdmin.csproj index f22a2aa..497eb92 100644 --- a/MultiAdmin/MultiAdmin.csproj +++ b/MultiAdmin/MultiAdmin.csproj @@ -31,7 +31,7 @@ true full false - E:\SteamLibrary\steamapps\common\SCP Secret Laboratory2\ + bin\Debug\ DEBUG;TRACE prompt 4 @@ -50,6 +50,9 @@ MultiAdmin.Program + + Icon.ico + @@ -84,6 +87,7 @@ + @@ -94,9 +98,7 @@ - - @@ -120,5 +122,8 @@ 4.0.0 + + + \ No newline at end of file diff --git a/MultiAdmin/Program.cs b/MultiAdmin/Program.cs index 8dca438..ef04ec1 100644 --- a/MultiAdmin/Program.cs +++ b/MultiAdmin/Program.cs @@ -15,12 +15,12 @@ namespace MultiAdmin { public static class Program { - public const string MaVersion = "3.2.5.1"; + public const string MaVersion = "3.3.0.0"; public const string RecommendedMonoVersion = "5.18"; private static readonly List InstantiatedServers = new List(); - private static readonly string MaDebugLogDir = Utils.GetFullPathSafe("logs"); + private static readonly string MaDebugLogDir = Utils.GetFullPathSafe(MultiAdminConfig.GlobalConfig.LogLocation.Value); private static readonly string MaDebugLogFile = !string.IsNullOrEmpty(MaDebugLogDir) ? Utils.GetFullPathSafe(Path.Combine(MaDebugLogDir, $"{Utils.DateTime}_MA_{MaVersion}_debug_log.txt")) : null; private static uint? portArg; diff --git a/MultiAdmin/Properties/AssemblyInfo.cs b/MultiAdmin/Properties/AssemblyInfo.cs index 1cb699e..cf29517 100644 --- a/MultiAdmin/Properties/AssemblyInfo.cs +++ b/MultiAdmin/Properties/AssemblyInfo.cs @@ -7,7 +7,7 @@ [assembly: AssemblyTitle(nameof(MultiAdmin) + " v" + Program.MaVersion)] [assembly: AssemblyDescription("A program for running a SCP: Secret Laboratory server with additional functionality")] [assembly: AssemblyProduct(nameof(MultiAdmin))] -[assembly: AssemblyCopyright("Copyright © Grover 2019")] +[assembly: AssemblyCopyright("Copyright © Grover 2020")] // Version information for an assembly consists of the following four values: // diff --git a/MultiAdmin/Server.cs b/MultiAdmin/Server.cs index ce095cf..d9772ba 100644 --- a/MultiAdmin/Server.cs +++ b/MultiAdmin/Server.cs @@ -31,13 +31,6 @@ public class Server public readonly string serverDir; public readonly string logDir; - public bool hasServerMod; - - public string serverModBuild; - public string serverModVersion; - - private int logId; - private DateTime initStopTimeoutTime; private DateTime initRestartTimeoutTime; @@ -73,7 +66,7 @@ public Server(string serverId = null, string configLocation = null, uint? port = // Set port this.port = port; - logDir = Utils.GetFullPathSafe(Path.Combine(string.IsNullOrEmpty(serverDir) ? string.Empty : serverDir, "logs")); + logDir = Utils.GetFullPathSafe(Path.Combine(string.IsNullOrEmpty(serverDir) ? "" : serverDir, serverConfig.LogLocation.Value)); // Register all features RegisterFeatures(); @@ -154,22 +147,7 @@ public bool IsGameProcessRunning public static readonly string DedicatedDir = Utils.GetFullPathSafe(Path.Combine("SCPSL_Data", "Dedicated")); - private string sessionId; - - public string SessionId - { - get => sessionId; - - private set - { - sessionId = value; - - // Update related variables - SessionDirectory = string.IsNullOrEmpty(value) ? null : Path.Combine(DedicatedDir, value); - } - } - - public string SessionDirectory { get; private set; } + public ServerSocket SessionSocket { get; private set; } #region Server Core @@ -178,8 +156,7 @@ private void MainLoop() Stopwatch timer = new Stopwatch(); while (IsGameProcessRunning) { - timer.Reset(); - timer.Start(); + timer.Restart(); foreach (IEventTick tickEvent in tick) tickEvent.OnTick(); @@ -215,25 +192,13 @@ private void MainLoop() /// public void SendMessage(string message) { - if (!Directory.Exists(SessionDirectory)) - { - Write($"Send Message error: Sending {message} failed. \"{SessionDirectory}\" does not exist!\nSkipping..."); - return; - } - - string file = Path.Combine(SessionDirectory, $"cs{logId}.mapi"); - if (File.Exists(file)) + if (SessionSocket == null || !SessionSocket.Connected) { - Write($"Send Message error: Sending {message} failed. \"{file}\" already exists!\nSkipping..."); - logId++; + Write("Unable to send command to server, the console is disconnected", ConsoleColor.Red); return; } - StreamWriter streamWriter = new StreamWriter(file); - logId++; - streamWriter.WriteLine(message + "terminator"); - streamWriter.Close(); - Write("Sending request to SCP: Secret Laboratory...", ConsoleColor.White); + SessionSocket.SendMessage(message); } #endregion @@ -283,7 +248,6 @@ public void StartServer(bool restartOnCrash = true) Status = ServerStatus.Starting; IsLoading = true; - SessionId = DateTime.Now.Ticks.ToString(); StartDateTime = Utils.DateTime; try @@ -297,9 +261,6 @@ public void StartServer(bool restartOnCrash = true) // Reload the config immediately as server is starting ReloadConfig(); - // Create session directory - PrepareSession(); - // Init features InitFeatures(); @@ -307,14 +268,21 @@ public void StartServer(bool restartOnCrash = true) Write($"Executing \"{scpslExe}\"...", ConsoleColor.DarkGreen); + // Start the console socket connection to the game server + ServerSocket consoleSocket = new ServerSocket(); + // Start the connection before the game to find an open port for communication + consoleSocket.Connect(); + + SessionSocket = consoleSocket; + List scpslArgs = new List { "-batchmode", "-nographics", "-silent-crashes", "-nodedicateddelete", - $"-key{SessionId}", $"-id{Process.GetCurrentProcess().Id}", + $"-console{consoleSocket.Port}", $"-port{port ?? ServerConfig.Port.Value}" }; @@ -368,13 +336,17 @@ public void StartServer(bool restartOnCrash = true) ForEachHandler(eventPreStart => eventPreStart.OnServerPreStart()); // Start the input reader - Thread inputHandlerThread = new Thread(() => InputHandler.Write(this)); + Thread inputHandlerThread = null; if (!Program.Headless) + { + inputHandlerThread = new Thread(() => InputHandler.Write(this)); inputHandlerThread.Start(); + } // Start the output reader OutputHandler outputHandler = new OutputHandler(this); + consoleSocket.OnReceiveMessage += outputHandler.HandleMessage; // Finally, start the game GameProcess = Process.Start(startInfo); @@ -412,16 +384,14 @@ public void StartServer(bool restartOnCrash = true) GameProcess = null; // Stop the input handler if it's running - if (inputHandlerThread.IsAlive) + if (inputHandlerThread != null && inputHandlerThread.IsAlive) { inputHandlerThread.Abort(); } - outputHandler.Dispose(); - - DeleteSession(); + consoleSocket.Disconnect(); - SessionId = null; + SessionSocket = null; StartDateTime = null; if (shouldRestart) Write("Restarting server..."); @@ -452,10 +422,6 @@ public void StartServer(bool restartOnCrash = true) Write("Startup failed! Exiting...", ConsoleColor.Red); } } - finally - { - DeleteSession(); - } } while (shouldRestart); } @@ -484,15 +450,8 @@ public void SoftRestartServer() initRestartTimeoutTime = DateTime.Now; Status = ServerStatus.Restarting; - if (hasServerMod) - { - SendMessage("RECONNECTRS"); - } - else - { - SendMessage("ROUNDRESTART"); - SendMessage("QUIT"); - } + SendMessage("ROUNDRESTART"); + SendMessage("QUIT"); } #endregion @@ -578,83 +537,6 @@ public void ForEachHandler(Action action) where T : IMAEvent #endregion - #region Session Directory Management - - public void PrepareSession() - { - try - { - Directory.CreateDirectory(SessionDirectory); - Write($"Started new session \"{SessionId}\"", ConsoleColor.DarkGreen); - } - catch (Exception e) - { - throw new UnauthorizedAccessException($"Unable to create directory \"{SessionDirectory}\", make sure that {nameof(MultiAdmin)} has access to \"{DedicatedDir}\"\n{e}"); - } - } - - public void CleanSession() - { - if (!Directory.Exists(SessionDirectory)) return; - - foreach (string file in Directory.GetFiles(SessionDirectory)) - { - for (int i = 0; i < 20; i++) - { - try - { - File.Delete(file); - break; - } - catch (UnauthorizedAccessException e) - { - Program.LogDebugException(nameof(CleanSession), e); - Thread.Sleep(8); - } - catch (Exception e) - { - Program.LogDebugException(nameof(CleanSession), e); - Thread.Sleep(5); - } - } - } - } - - public void DeleteSession() - { - try - { - CleanSession(); - - if (!Directory.Exists(SessionDirectory)) return; - - for (int i = 0; i < 20; i++) - { - try - { - Directory.Delete(SessionDirectory); - break; - } - catch (UnauthorizedAccessException e) - { - Program.LogDebugException(nameof(DeleteSession), e); - Thread.Sleep(8); - } - catch (Exception e) - { - Program.LogDebugException(nameof(DeleteSession), e); - Thread.Sleep(5); - } - } - } - catch (Exception e) - { - Program.LogDebugException(nameof(DeleteSession), e); - } - } - - #endregion - #region Console Output and Logging public void Write(ColoredMessage[] messages, ConsoleColor? timeStampColor = null) @@ -720,30 +602,6 @@ public void Log(string message) #endregion - public bool ServerModCheck(int major, int minor, int fix) - { - if (string.IsNullOrEmpty(serverModVersion)) - return false; - - string[] parts = serverModVersion.Split('.'); - - if (parts.IsEmpty()) - return false; - - int.TryParse(parts[0], out int verMajor); - - int verMinor = 0; - if (parts.Length >= 2) - int.TryParse(parts[1], out verMinor); - - int verFix = 0; - if (parts.Length >= 3) - int.TryParse(parts[2], out verFix); - - return verMajor > major || verMajor >= major && verMinor > minor || - verMajor >= major && verMinor >= minor && verFix >= fix; - } - public void ReloadConfig(bool copyFiles = true, bool runEvent = true) { ServerConfig.ReloadConfig(); diff --git a/MultiAdmin/ServerIO/InputHandler.cs b/MultiAdmin/ServerIO/InputHandler.cs index 7153ca3..de43bc5 100644 --- a/MultiAdmin/ServerIO/InputHandler.cs +++ b/MultiAdmin/ServerIO/InputHandler.cs @@ -55,11 +55,18 @@ public static void Write(Server server) { if (Program.Headless) { - Thread.Sleep(5000); - continue; + break; } - string message = server.ServerConfig.UseNewInputSystem.Value ? GetInputLineNew(server, prevMessages) : Console.ReadLine(); + string message; + if (server.ServerConfig.UseNewInputSystem.Value && SectionBufferWidth - TotalIndicatorLength > 0) + { + message = GetInputLineNew(server, prevMessages); + } + else + { + message = Console.ReadLine(); + } if (string.IsNullOrEmpty(message)) continue; @@ -92,8 +99,8 @@ public static string GetInputLineNew(Server server, ShiftingList prevMessages) if (server.ServerConfig.RandomInputColors.Value) RandomizeInputColors(); - string curMessage = string.Empty; - string message = string.Empty; + string curMessage = ""; + string message = ""; int messageCursor = 0; int prevMessageCursor = -1; StringSections curSections = null; @@ -197,7 +204,7 @@ public static string GetInputLineNew(Server server, ShiftingList prevMessages) // If the message has changed, re-write it to the console if (CurrentMessage != message) { - if (message.Length > SectionBufferWidth) + if (message.Length > SectionBufferWidth && SectionBufferWidth - TotalIndicatorLength > 0) { curSections = GetStringSections(message); diff --git a/MultiAdmin/ServerIO/OutputHandler.cs b/MultiAdmin/ServerIO/OutputHandler.cs index ef39f64..7d0e3a3 100644 --- a/MultiAdmin/ServerIO/OutputHandler.cs +++ b/MultiAdmin/ServerIO/OutputHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.Text.RegularExpressions; using System.Threading; @@ -7,273 +8,106 @@ namespace MultiAdmin.ServerIO { - public class OutputHandler : IDisposable + public class OutputHandler { public static readonly Regex SmodRegex = new Regex(@"\[(DEBUG|INFO|WARN|ERROR)\] (\[.*?\]) (.*)", RegexOptions.Compiled | RegexOptions.Singleline); - private readonly FileSystemWatcher fsWatcher; - private bool fixBuggedPlayers; - - public static ConsoleColor MapConsoleColor(string color, ConsoleColor def = ConsoleColor.Cyan) - { - try - { - return (ConsoleColor)Enum.Parse(typeof(ConsoleColor), color); - } - catch (Exception e) - { - Program.LogDebugException(nameof(MapConsoleColor), e); - return def; - } - } + private readonly Server server; public OutputHandler(Server server) { - if (server == null) - { - Program.Write("Error in OutputHandler - Server server is null!", ConsoleColor.Red); - return; - } - - if (string.IsNullOrEmpty(server.SessionDirectory)) - { - server.Write($"Missing session directory! Output is not being watched... (SessionDirectory = \"{server.SessionDirectory ?? "null"}\" SessionId = \"{server.SessionId ?? "null"}\" DedicatedDir = \"{Server.DedicatedDir ?? "null"}\")", ConsoleColor.Red); - return; - } - - fsWatcher = new FileSystemWatcher {Path = server.SessionDirectory}; - - fsWatcher.Created += (sender, eventArgs) => OnMapiCreated(eventArgs, server); - fsWatcher.Filter = "sl*.mapi"; - fsWatcher.EnableRaisingEvents = true; - } - - /* Old Windows MAPI Watching Code - private void OnDirectoryChanged(FileSystemEventArgs e, Server server) - { - if (!Directory.Exists(e.FullPath)) return; - - string[] files = Directory.GetFiles(e.FullPath, "sl*.mapi", SearchOption.TopDirectoryOnly).OrderBy(f => f) - .ToArray(); - foreach (string file in files) ProcessFile(server, file); - } - */ - - private void OnMapiCreated(FileSystemEventArgs e, Server server) - { - if (!File.Exists(e.FullPath)) return; - - try - { - ProcessFile(server, e.FullPath); - } - catch (Exception ex) - { - Program.LogDebugException(nameof(OnMapiCreated), ex); - } + this.server = server; } - public void ProcessFile(Server server, string file) + public void HandleMessage(object source, string message) { - string stream = string.Empty; - string command = "open"; - - bool isRead = false; - - // Lock this object to wait for this event to finish before trying to read another file - lock (this) - { - for (int attempts = 0; attempts < server.ServerConfig.OutputReadAttempts.Value; attempts++) - { - try - { - if (!File.Exists(file)) return; - - // Lock the file to prevent it from being modified further, or read by another instance - using (StreamReader sr = new StreamReader(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.None))) - { - command = "read"; - stream = sr.ReadToEnd(); - - isRead = true; - } - - command = "delete"; - File.Delete(file); - - break; - } - catch (UnauthorizedAccessException e) - { - Program.LogDebugException(nameof(ProcessFile), e); - Thread.Sleep(8); - } - catch (Exception e) - { - Program.LogDebugException(nameof(ProcessFile), e); - Thread.Sleep(5); - } - } - } - - if (!isRead) - { - server.Write($"Message printer warning: Could not {command} \"{file}\". Make sure that {nameof(MultiAdmin)} has all necessary read-write permissions\nSkipping..."); - + if (message == null) return; - } - bool display = true; - ConsoleColor color = ConsoleColor.Cyan; + ColoredMessage coloredMessage = new ColoredMessage(message, ConsoleColor.Cyan); - if (stream.EndsWith(Environment.NewLine)) - stream = stream.Substring(0, stream.Length - Environment.NewLine.Length); - - int logTypeIndex = stream.IndexOf("LOGTYPE"); - if (logTypeIndex >= 0) + if (coloredMessage.text.Length > 0) { - string type = stream.Substring(logTypeIndex).Trim(); - stream = stream.Substring(0, logTypeIndex).Trim(); - - switch (type) + if (byte.TryParse(Convert.ToString(coloredMessage.text[0]), NumberStyles.HexNumber, NumberFormatInfo.CurrentInfo, out byte consoleColor)) { - case "LOGTYPE02": - color = ConsoleColor.Green; - break; - case "LOGTYPE-8": - color = ConsoleColor.DarkRed; - break; - case "LOGTYPE14": - color = ConsoleColor.Magenta; - break; - default: - color = ConsoleColor.Cyan; - break; + coloredMessage.textColor = (ConsoleColor)consoleColor; + coloredMessage.text = coloredMessage.text.Substring(1); } - } - // Smod2 loggers pretty printing - Match match = SmodRegex.Match(stream); - if (match.Success) - { - if (match.Groups.Count >= 3) + // Smod2 loggers pretty printing + Match match = SmodRegex.Match(coloredMessage.text); + if (match.Success) { - ConsoleColor levelColor = ConsoleColor.Cyan; - ConsoleColor tagColor = ConsoleColor.Yellow; - ConsoleColor msgColor = ConsoleColor.White; - switch (match.Groups[1].Value.Trim()) + if (match.Groups.Count >= 3) { - case "DEBUG": - levelColor = ConsoleColor.Gray; - break; - case "INFO": - levelColor = ConsoleColor.Green; - break; - case "WARN": - levelColor = ConsoleColor.DarkYellow; - break; - case "ERROR": - levelColor = ConsoleColor.Red; - msgColor = ConsoleColor.Red; - break; - default: - color = ConsoleColor.Cyan; - break; - } + ConsoleColor levelColor = ConsoleColor.Cyan; + ConsoleColor tagColor = ConsoleColor.Yellow; + ConsoleColor msgColor = ConsoleColor.White; + switch (match.Groups[1].Value.Trim()) + { + case "DEBUG": + levelColor = ConsoleColor.Gray; + break; + case "INFO": + levelColor = ConsoleColor.Green; + break; + case "WARN": + levelColor = ConsoleColor.DarkYellow; + break; + case "ERROR": + levelColor = ConsoleColor.Red; + msgColor = ConsoleColor.Red; + break; + default: + coloredMessage.textColor = ConsoleColor.Cyan; + break; + } - server.Write(new ColoredMessage[] - { - new ColoredMessage($"[{match.Groups[1].Value}] ", levelColor), - new ColoredMessage($"{match.Groups[2].Value} ", tagColor), - new ColoredMessage(match.Groups[3].Value, msgColor) - }, ConsoleColor.Cyan); + server.Write( + new ColoredMessage[] + { + new ColoredMessage($"[{match.Groups[1].Value}] ", levelColor), + new ColoredMessage($"{match.Groups[2].Value} ", tagColor), + new ColoredMessage(match.Groups[3].Value, msgColor) + }, ConsoleColor.Cyan); - // P.S. the format is [Info] [courtney.exampleplugin] Something interesting happened - // That was just an example + // P.S. the format is [Info] [courtney.exampleplugin] Something interesting happened + // That was just an example - // This return should be here - return; + // This return should be here + return; + } } - } - - if (stream.Contains("Mod Log:")) - server.ForEachHandler(adminAction => adminAction.OnAdminAction(stream.Replace("Mod Log:", string.Empty))); - if (stream.Contains("ServerMod - Version")) - { - server.hasServerMod = true; - // This should work fine with older ServerMod versions too - string[] streamSplit = stream.Replace("ServerMod - Version", string.Empty).Split('-'); - - if (!streamSplit.IsEmpty()) + switch (coloredMessage.text) { - server.serverModVersion = streamSplit[0].Trim(); - server.serverModBuild = (streamSplit.Length > 1 ? streamSplit[1] : "A").Trim(); - } - } - - if (stream.Contains("Round restarting")) - server.ForEachHandler(roundEnd => roundEnd.OnRoundEnd()); - - if (stream.Contains("Waiting for players")) - { - server.IsLoading = false; - - server.ForEachHandler(waitingForPlayers => waitingForPlayers.OnWaitingForPlayers()); - - if (fixBuggedPlayers) - { - server.SendMessage("ROUNDRESTART"); - fixBuggedPlayers = false; - } - } - - if (stream.Contains("New round has been started")) - server.ForEachHandler(roundStart => roundStart.OnRoundStart()); - - if (stream.Contains("Level loaded. Creating match...")) - server.ForEachHandler(serverStart => serverStart.OnServerStart()); + case "The round is about to restart! Please wait..": + server.ForEachHandler(roundEnd => roundEnd.OnRoundEnd()); + break; - if (stream.Contains("Server full")) - server.ForEachHandler(serverFull => serverFull.OnServerFull()); + case "Waiting for players...": + server.IsLoading = false; - if (stream.Contains("Player connect")) - { - display = false; - server.Log("Player connect event"); + server.ForEachHandler(waitingForPlayers => + waitingForPlayers.OnWaitingForPlayers()); + break; - int index = stream.IndexOf(":"); - if (index >= 0) - { - string name = stream.Substring(index); - server.ForEachHandler(playerConnect => playerConnect.OnPlayerConnect(name)); - } - } + case "New round has been started.": + server.ForEachHandler(roundStart => roundStart.OnRoundStart()); + break; - if (stream.Contains("Player disconnect")) - { - display = false; - server.Log("Player disconnect event"); + case "Level loaded. Creating match...": + server.ForEachHandler(serverStart => serverStart.OnServerStart()); + break; - int index = stream.IndexOf(":"); - if (index >= 0) - { - string name = stream.Substring(index); - server.ForEachHandler(playerDisconnect => playerDisconnect.OnPlayerDisconnect(name)); + case "Server full": + server.ForEachHandler(serverFull => serverFull.OnServerFull()); + break; } } - if (stream.Contains("Player has connected before load is complete")) - fixBuggedPlayers = true; - - if (display) server.Write(stream, color); - } - - public void Dispose() - { - fsWatcher?.Dispose(); - GC.SuppressFinalize(this); + server.Write(coloredMessage); } } } diff --git a/MultiAdmin/ServerIO/ServerSocket.cs b/MultiAdmin/ServerIO/ServerSocket.cs new file mode 100644 index 0000000..a9c9d7b --- /dev/null +++ b/MultiAdmin/ServerIO/ServerSocket.cs @@ -0,0 +1,144 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; + +namespace MultiAdmin.ServerIO +{ + public class ServerSocket : IDisposable + { + private const int IntBytes = sizeof(int); + public static readonly UTF8Encoding Encoding = new UTF8Encoding(false, true); + + private readonly CancellationTokenSource disposeCancellationSource = new CancellationTokenSource(); + private bool disposed = false; + + private readonly TcpListener listener; + + private TcpClient client; + private NetworkStream networkStream; + + public event EventHandler OnReceiveMessage; + + public int Port + { + get + { + return ((IPEndPoint)listener.LocalEndpoint).Port; + } + } + + public bool Connected + { + get + { + return client?.Connected ?? false; + } + } + + // Port 0 automatically assigns a port + public ServerSocket(int port = 0) + { + listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port)); + } + + public void Connect() + { + if (disposed) + throw new ObjectDisposedException(nameof(ServerSocket)); + + listener.Start(); + listener.BeginAcceptTcpClient(result => + { + client = listener.EndAcceptTcpClient(result); + networkStream = client.GetStream(); + + Thread listenerThread = new Thread(MessageListener); + listenerThread.Start(); + }, listener); + } + + public void MessageListener() + { + byte[] intBuffer = new byte[IntBytes]; + while (!disposed) + { + try + { + networkStream.ReadAsync(intBuffer, 0, IntBytes, disposeCancellationSource.Token).Wait(); + } + catch (Exception e) + { + Program.LogDebugException(nameof(MessageListener), e); + continue; + } + if (disposed) + break; + + int length = BitConverter.ToInt32(intBuffer, 0); + + byte[] messageBuffer = new byte[length]; + try + { + networkStream.ReadAsync(messageBuffer, 0, length, disposeCancellationSource.Token).Wait(); + } + catch (Exception e) + { + Program.LogDebugException(nameof(MessageListener), e); + continue; + } + if (disposed) + break; + + string message = Encoding.GetString(messageBuffer, 0, length); + + OnReceiveMessage?.Invoke(this, message); + } + } + + public void SendMessage(string message) + { + if (disposed) + throw new ObjectDisposedException(nameof(ServerSocket)); + + if (networkStream == null) + throw new NullReferenceException($"{nameof(networkStream)} hasn't been initialized"); + + byte[] messageBuffer = new byte[Encoding.GetMaxByteCount(message.Length) + IntBytes]; + + int actualMessageLength = Encoding.GetBytes(message, 0, message.Length, messageBuffer, IntBytes); + Array.Copy(BitConverter.GetBytes(actualMessageLength), messageBuffer, IntBytes); + + try + { + networkStream.Write(messageBuffer, 0, actualMessageLength + IntBytes); + } + catch (Exception e) + { + Program.LogDebugException(nameof(SendMessage), e); + } + } + + public void Disconnect() + { + Dispose(); + } + + public void Dispose() + { + if (disposed) + return; + + disposed = true; + disposeCancellationSource.Cancel(); + disposeCancellationSource.Dispose(); + + networkStream?.Close(); + client?.Close(); + listener.Stop(); + + OnReceiveMessage = null; + } + } +} diff --git a/MultiAdmin/Utility/StringExtensions.cs b/MultiAdmin/Utility/StringExtensions.cs index 1cf0b6f..94722c9 100644 --- a/MultiAdmin/Utility/StringExtensions.cs +++ b/MultiAdmin/Utility/StringExtensions.cs @@ -6,11 +6,14 @@ 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) + if (input == null && value == null) + return true; + if (input == null || value == null) + return false; + + if (startIndex < 0 || startIndex >= input.Length) throw new ArgumentOutOfRangeException(nameof(startIndex)); - if (count < 0 || startIndex > input.Length - count) + if (count < 0 || count > value.Length || startIndex > input.Length - count) throw new ArgumentOutOfRangeException(nameof(count)); for (int i = 0; i < count; i++) @@ -26,7 +29,17 @@ public static bool Equals(this string input, string value, int startIndex, int c public static bool Equals(this string input, string value, int startIndex) { - return Equals(input, value, startIndex, input.Length - startIndex); + if (input == null && value == null) + return true; + if (input == null || value == null) + return false; + + int length = input.Length - startIndex; + + if (length < value.Length) + throw new ArgumentOutOfRangeException(nameof(value)); + + return Equals(input, value, startIndex, length); } } } diff --git a/MultiAdmin/Utility/Utils.cs b/MultiAdmin/Utility/Utils.cs index b4ef227..3eababf 100644 --- a/MultiAdmin/Utility/Utils.cs +++ b/MultiAdmin/Utility/Utils.cs @@ -31,22 +31,30 @@ public static string TimeStampMessage(string message) return string.IsNullOrEmpty(message) ? message : $"{TimeStamp} {message}"; } - public static ColoredMessage[] TimeStampMessage(ColoredMessage[] message, ConsoleColor? color = null) + public static ColoredMessage[] TimeStampMessage(ColoredMessage[] message, ConsoleColor? color = null, bool cloneMessages = false) { if (message == null) return null; ColoredMessage[] newMessage = new ColoredMessage[message.Length + 1]; newMessage[0] = new ColoredMessage($"{TimeStamp} ", color); - for (int i = 0; i < message.Length; i++) - newMessage[i + 1] = message[i]?.Clone(); + if (cloneMessages) + { + for (int i = 0; i < message.Length; i++) + newMessage[i + 1] = message[i]?.Clone(); + } + else + { + for (int i = 0; i < message.Length; i++) + newMessage[i + 1] = message[i]; + } return newMessage; } - public static ColoredMessage[] TimeStampMessage(ColoredMessage message, ConsoleColor? color = null) + public static ColoredMessage[] TimeStampMessage(ColoredMessage message, ConsoleColor? color = null, bool cloneMessages = false) { - return TimeStampMessage(new ColoredMessage[] {message}, color); + return TimeStampMessage(new ColoredMessage[] {message}, color, cloneMessages); } public static string GetFullPathSafe(string path) diff --git a/MultiAdminTests/.gitignore b/MultiAdminTests/.gitignore new file mode 100644 index 0000000..1579cdd --- /dev/null +++ b/MultiAdminTests/.gitignore @@ -0,0 +1,516 @@ + +# Created by https://www.gitignore.io/api/git,rider,linux,macos,csharp,windows,monodevelop +# Edit at https://www.gitignore.io/?templates=git,rider,linux,macos,csharp,windows,monodevelop + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### Linux ### + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### MonoDevelop ### +#User Specific +*.usertasks + +#Mono Project Files +*.resources +test-results/ + +### Rider ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/git,rider,linux,macos,csharp,windows,monodevelop diff --git a/MultiAdminTests/MultiAdminTests.csproj b/MultiAdminTests/MultiAdminTests.csproj index 7bbcfa9..daf551b 100644 --- a/MultiAdminTests/MultiAdminTests.csproj +++ b/MultiAdminTests/MultiAdminTests.csproj @@ -62,11 +62,13 @@ - - 1.3.2 + + 2.4.1 - - 1.3.2 + + 2.4.1 + runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/MultiAdminTests/ServerIO/ShiftingListTests.cs b/MultiAdminTests/ServerIO/ShiftingListTests.cs index 61cf106..d18ffdf 100644 --- a/MultiAdminTests/ServerIO/ShiftingListTests.cs +++ b/MultiAdminTests/ServerIO/ShiftingListTests.cs @@ -1,22 +1,21 @@ using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; using MultiAdmin.ServerIO; +using Xunit; namespace MultiAdminTests.ServerIO { - [TestClass] public class ShiftingListTests { - [TestMethod] + [Fact] public void ShiftingListTest() { const int maxCount = 2; ShiftingList shiftingList = new ShiftingList(maxCount); - Assert.AreEqual(shiftingList.MaxCount, maxCount); + Assert.Equal(maxCount, shiftingList.MaxCount); } - [TestMethod] + [Fact] public void AddTest() { const int maxCount = 2; @@ -28,15 +27,15 @@ public void AddTest() shiftingList.Add($"Test{i}"); } - Assert.AreEqual(shiftingList.Count, maxCount); + Assert.Equal(maxCount, shiftingList.Count); for (int i = 0; i < shiftingList.Count; i++) { - Assert.AreEqual(shiftingList[i], $"Test{entriesToAdd - i - 1}"); + Assert.Equal($"Test{entriesToAdd - i - 1}", shiftingList[i]); } } - [TestMethod] + [Fact] public void RemoveFromEndTest() { const int maxCount = 6; @@ -53,15 +52,15 @@ public void RemoveFromEndTest() shiftingList.RemoveFromEnd(); } - Assert.AreEqual(shiftingList.Count, Math.Max(maxCount - entriesToRemove, 0)); + Assert.Equal(Math.Max(maxCount - entriesToRemove, 0), shiftingList.Count); for (int i = 0; i < shiftingList.Count; i++) { - Assert.AreEqual(shiftingList[i], $"Test{maxCount - i - 1}"); + Assert.Equal($"Test{maxCount - i - 1}", shiftingList[i]); } } - [TestMethod] + [Fact] public void ReplaceTest() { const int maxCount = 6; @@ -81,9 +80,9 @@ public void ReplaceTest() } } - Assert.AreEqual(shiftingList.Count, maxCount); + Assert.Equal(maxCount, shiftingList.Count); - Assert.AreEqual(shiftingList[indexToReplace], "Replaced"); + Assert.Equal("Replaced", shiftingList[indexToReplace]); } } } diff --git a/MultiAdminTests/ServerIO/StringSectionsTests.cs b/MultiAdminTests/ServerIO/StringSectionsTests.cs index 15d4b2a..bbf6c0b 100644 --- a/MultiAdminTests/ServerIO/StringSectionsTests.cs +++ b/MultiAdminTests/ServerIO/StringSectionsTests.cs @@ -1,11 +1,10 @@ using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; using MultiAdmin.ConsoleTools; using MultiAdmin.ServerIO; +using Xunit; namespace MultiAdminTests.ServerIO { - [TestClass] public class StringSectionsTests { private struct FromStringTemplate @@ -28,18 +27,14 @@ public FromStringTemplate(string testString, string[] expectedSections, int sect } } - [TestMethod] + [Fact] public void FromStringTest() { - try + // No further characters can be output because of the prefix and suffix + Assert.Throws(() => { StringSections.FromString("test string", 2, new ColoredMessage("."), new ColoredMessage(".")); - Assert.Fail("This case should not be allowed, no further characters can be output because of the prefix and suffix"); - } - catch (ArgumentException) - { - // Expected behaviour - } + }); FromStringTemplate[] sectionTests = { @@ -53,17 +48,17 @@ public void FromStringTest() StringSections sections = StringSections.FromString(sectionTest.testString, sectionTest.sectionLength, sectionTest.leftIndictator, sectionTest.rightIndictator); - Assert.IsNotNull(sections); - Assert.IsNotNull(sections.Sections); + Assert.NotNull(sections); + Assert.NotNull(sections.Sections); - Assert.IsTrue(sections.Sections.Length == sectionTest.expectedSections.Length, $"Failed at index {i}: Expected sections length \"{sectionTest.expectedSections.Length}\", got \"{sections.Sections.Length}\""); + Assert.True(sections.Sections.Length == sectionTest.expectedSections.Length, $"Failed at index {i}: Expected sections length \"{sectionTest.expectedSections.Length}\", got \"{sections.Sections.Length}\""); for (int j = 0; j < sectionTest.expectedSections.Length; j++) { string expected = sectionTest.expectedSections[j]; string result = sections.Sections[j].Section.GetText(); - Assert.AreEqual(expected, result, $"Failed at index {i}: Failed at section index {j}: Expected section text to be \"{expected ?? "null"}\", got \"{result ?? "null"}\""); + Assert.True(expected == result, $"Failed at index {i}: Failed at section index {j}: Expected section text to be \"{expected ?? "null"}\", got \"{result ?? "null"}\""); } } } diff --git a/MultiAdminTests/Utility/StringExtensionsTests.cs b/MultiAdminTests/Utility/StringExtensionsTests.cs index 7d94475..30fdc74 100644 --- a/MultiAdminTests/Utility/StringExtensionsTests.cs +++ b/MultiAdminTests/Utility/StringExtensionsTests.cs @@ -1,24 +1,52 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; using MultiAdmin.Utility; +using Xunit; +using Xunit.Sdk; namespace MultiAdminTests.Utility { - [TestClass] public class StringExtensionsTests { - [TestMethod] - public void EqualsTest() + [Theory] + [InlineData("test", "test", 0)] + [InlineData("test", "test", 0, 4)] + [InlineData("test", "st", 2)] + [InlineData("test", "te", 0, 2)] + [InlineData("test", "es", 1, 2)] + [InlineData(null, null, 0)] + [InlineData(null, null, 0, 1)] + public void EqualsTest(string main, string section, int startIndex, int count = -1) { - 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.True(count < 0 ? main.Equals(section, startIndex) : main.Equals(section, startIndex, count)); + } - Assert.IsFalse("test".Equals("te", startIndex: 2)); - Assert.IsFalse("test".Equals("st", 0, 2)); + [Theory] + [InlineData("test", "other", 0, 4)] + [InlineData("test", "te", 2)] + [InlineData("test", "st", 0, 2)] + [InlineData("test", null, 0)] + [InlineData(null, "test", 0)] + [InlineData("test", null, 0, 1)] + [InlineData(null, "test", 0, 1)] + public void NotEqualsTest(string main, string section, int startIndex, int count = -1) + { + Assert.False(count < 0 ? main.Equals(section, startIndex) : main.Equals(section, startIndex, count)); + } - Assert.IsTrue("test".Equals("es", 1, 2)); + [Theory] + [InlineData(typeof(ArgumentOutOfRangeException), "longtest", "test", 1, 5)] + [InlineData(typeof(ArgumentOutOfRangeException), "test", "st", 3)] + [InlineData(typeof(ArgumentOutOfRangeException), "test", "te", -1)] + [InlineData(typeof(ArgumentOutOfRangeException), "test", "es", 4)] + public void EqualsThrowsTest(Type expected, string main, string section, int startIndex, int count = -1) + { + Assert.Throws(expected, () => + { + if (count < 0) + main.Equals(section, startIndex); + else + main.Equals(section, startIndex, count); + }); } } } diff --git a/MultiAdminTests/Utility/UtilsTests.cs b/MultiAdminTests/Utility/UtilsTests.cs index ea1c1dc..8340cd3 100644 --- a/MultiAdminTests/Utility/UtilsTests.cs +++ b/MultiAdminTests/Utility/UtilsTests.cs @@ -1,130 +1,58 @@ using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; using MultiAdmin.Utility; +using Xunit; 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] + [Fact] public void GetFullPathSafeTest() { - string result = Utils.GetFullPathSafe(" "); - Assert.IsNull(result, $"Expected \"null\", got \"{result}\""); + Assert.Null(Utils.GetFullPathSafe(" ")); } - [TestMethod] - public void StringMatchesTest() + [Theory] + [InlineData("test", "*", true)] + [InlineData("test", "te*", true)] + [InlineData("test", "*st", true)] + [InlineData("test", "******", true)] + [InlineData("test", "te*t", true)] + [InlineData("test", "t**st", true)] + [InlineData("test", "s*", false)] + [InlineData("longstringtestmessage", "l*s*t*e*g*", true)] + [InlineData("AdminToolbox", "config_remoteadmin.txt", false)] + [InlineData("config_remoteadmin.txt", "config_remoteadmin.txt", true)] + [InlineData("sizetest", "sizetest1", false)] + public void StringMatchesTest(string input, string pattern, bool expected) { - 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), - new StringMatchingTemplate("AdminToolbox", "config_remoteadmin.txt", false), - new StringMatchingTemplate("config_remoteadmin.txt", "config_remoteadmin.txt", true), - new StringMatchingTemplate("sizetest", "sizetest1", false) - }; - - for (int i = 0; i < matchTests.Length; i++) - { - try - { - 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}\""); - } - catch (Exception e) - { - Assert.Fail($"Failed on test index {i}: {e}"); - } - } + bool result = Utils.StringMatches(input, pattern); + Assert.Equal(expected, 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", -1), // The first version is shorter, so it's lower - new CompareVersionTemplate("2.0", "1.0.0.0", 1), + [Theory] + [InlineData("1.0.0.0", "2.0.0.0", -1)] + [InlineData("1.0.0.0", "1.0.0.0", 0)] + [InlineData("2.0.0.0", "1.0.0.0", 1)] - new CompareVersionTemplate("1.0.0.0", "2.0", -1), - new CompareVersionTemplate("1.0.0.0", "1.0", 1), // The first version is longer, so it's higher - new CompareVersionTemplate("2.0.0.0", "1.0", 1), + [InlineData("1.0", "2.0.0.0", -1)] + [InlineData("1.0", "1.0.0.0", -1)] // The first version is shorter, so it's lower + [InlineData("2.0", "1.0.0.0", 1)] - new CompareVersionTemplate("6.0.0.313", "5.18.0", 1), - new CompareVersionTemplate("5.18.0", "6.0.0.313", -1), + [InlineData("1.0.0.0", "2.0", -1)] + [InlineData("1.0.0.0", "1.0", 1)] // The first version is longer, so it's higher + [InlineData("2.0.0.0", "1.0", 1)] - new CompareVersionTemplate("5.18.0", "5.18.0", 0), - new CompareVersionTemplate("5.18", "5.18.0", -1) // The first version is shorter, so it's lower - }; + [InlineData("6.0.0.313", "5.18.0", 1)] + [InlineData("5.18.0", "6.0.0.313", -1)] - for (int i = 0; i < versionTests.Length; i++) - { - CompareVersionTemplate test = versionTests[i]; - - int result = Utils.CompareVersionStrings(test.firstVersion, test.secondVersion); + [InlineData("5.18.0", "5.18.0", 0)] + [InlineData("5.18", "5.18.0", -1)] // The first version is shorter, so it's lower + public void CompareVersionStringsTest(string firstVersion, string secondVersion, int expected) + { + int result = Utils.CompareVersionStrings(firstVersion, secondVersion); - Assert.IsTrue(test.CheckResult(result), $"Failed on test index {i}: Expected \"{test.expectedResult}\", got \"{result}\""); - } + Assert.Equal(expected, result); } } } diff --git a/README.md b/README.md index dbc3797..07aaad1 100644 --- a/README.md +++ b/README.md @@ -27,29 +27,27 @@ Make sure that you are running Mono 5.18.0 or higher, otherwise you might have i - 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 -- ModLog: Logs admin messages to separate file, or prints them - MultiAdminInfo: Prints MultiAdmin license and version information -- New: Adds a command to start a new server given a config folder +- New Server: Adds a command to start a new server given a config folder and a config to start a new server when one is full [Config Requires Modding] - Restart Command: Allows the game to be restarted without restarting MultiAdmin -- Restart Next Round: Restarts the server after the current round ends -- Restart After a Number of Rounds: Restarts the server after a number rounds completed -- Stop Next Round: Stops the server after the current round ends -- TitleBar: Updates the title bar with instance based information, such as session id and player count (Requires ServerMod to function fully) +- Restart Next Round: Restarts the server after the current round ends [Requires Modding] +- Restart After a Number of Rounds: Restarts the server after a number rounds completed [Requires Modding] +- Stop Next Round: Stops the server after the current round ends [Requires Modding] +- TitleBar: Updates the title bar with instance based information ## MultiAdmin Commands -This does not include ServerMod or ingame commands, for a full list type `HELP` in multiadmin which will produce all commands. +This does not include ingame commands, for a full list type `HELP` in MultiAdmin which will produce all commands. - CONFIG : Reloads the configuration file - EXIT: Exits the server -- GITHUBGEN [FILE LOCATION]: Generates a github .md file outlining all the features/commands +- GITHUBGEN [FILE LOCATION]: Generates a GitHub README file outlining all the features/commands - HELP: Prints out available commands and their function - 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 -- STOPNEXTROUND: Stops the server at the end of this round +- RESTARTNEXTROUND: Restarts the server at the end of this round [Requires Modding] +- STOPNEXTROUND: Stops the server at the end of this round [Requires Modding] ## MultiAdmin Execution Arguments The arguments available for running MultiAdmin with @@ -69,9 +67,10 @@ config_location | String | **Empty** | The default location for the game to use appdata_location | String | **Empty** | The location for the game to use for AppData (a directory) 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_log_location | String | logs | The folder that MultiAdmin will store logs in (a directory) 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, StringMatches | Which tags to block for MultiAdmin debug logging +multiadmin_debug_log_blacklist | String List | HandleMessage, StringMatches, MessageListener | 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 @@ -82,13 +81,10 @@ folder_copy_round_queue | String List | **Empty** | The location of a folder to folder_copy_round_queue_whitelist | String List | **Empty** | The list of file names to copy from the folders defined by `folder_copy_round_queue` (accepts `*` wildcards) folder_copy_round_queue_blacklist | String List | **Empty** | The list of file names to not copy from the folders defined by `folder_copy_round_queue` (accepts `*` wildcards) 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 | 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 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 @@ -101,8 +97,4 @@ server_start_retry | Boolean | True | Whether to try to start the server again a server_start_retry_delay | Integer | 10000 | The time in milliseconds to wait before trying to start the server again after crashing 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 -start_config_on_full | String | **Empty** | Start server with this config folder once the server becomes full [Requires ServerMod] - -## Upcoming Features -- Support for running multiple server instances in one MultiAdmin instance +start_config_on_full | String | **Empty** | Start server with this config folder once the server becomes full [Requires Modding]