├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── codeql-analysis.yml │ └── main.yml ├── .gitignore ├── LICENSE ├── MultiAdmin.Tests ├── .gitignore ├── MultiAdmin.Tests.csproj ├── Properties │ └── AssemblyInfo.cs ├── ServerIO │ ├── ShiftingListTests.cs │ └── StringSectionsTests.cs ├── Utility │ ├── CommandUtilsTests.cs │ ├── StringExtensionsTests.cs │ └── UtilsTests.cs └── nuget.config ├── MultiAdmin.sln ├── MultiAdmin ├── .gitignore ├── Config │ ├── Config.cs │ ├── ConfigHandler │ │ ├── ConfigEntry.cs │ │ ├── ConfigRegister.cs │ │ └── InheritableConfigRegister.cs │ └── MultiAdminConfig.cs ├── ConsoleTools │ ├── ColoredConsole.cs │ ├── ConsolePositioning.cs │ └── ConsoleUtils.cs ├── EventInterfaces.cs ├── Exceptions.cs ├── Feature.cs ├── Features │ ├── Attributes │ │ └── FeatureAttribute.cs │ ├── ConfigGenerator.cs │ ├── ConfigReload.cs │ ├── EventTest.cs │ ├── ExitCommand.cs │ ├── FolderCopyRoundQueue.cs │ ├── GithubGenerator.cs │ ├── HelpCommand.cs │ ├── MemoryChecker.cs │ ├── MultiAdminInfo.cs │ ├── NewCommand.cs │ ├── Restart.cs │ ├── RestartRoundCounter.cs │ └── TitleBar.cs ├── Icon.ico ├── ModFeatures.cs ├── MultiAdmin.csproj ├── NativeExitSignal │ ├── IExitSignal.cs │ ├── UnixExitSignal.cs │ └── WinExitSignal.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── Server.cs ├── ServerIO │ ├── InputHandler.cs │ ├── OutputHandler.cs │ ├── ServerSocket.cs │ ├── ShiftingList.cs │ └── StringSections.cs ├── Utility │ ├── CommandUtils.cs │ ├── EmptyExtensions.cs │ ├── StringEnumerableExtensions.cs │ ├── StringExtensions.cs │ └── Utils.cs ├── app.config └── nuget.config └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Core EditorConfig Options # 3 | ############################### 4 | # All files 5 | root = true 6 | # Code files 7 | [*.{cs,csx,vb,vbx}] 8 | indent_style = tab 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | charset = utf-8 12 | ############################### 13 | # .NET Coding Conventions # 14 | ############################### 15 | [*.{cs,vb}] 16 | # Organize usings 17 | dotnet_sort_system_directives_first = true 18 | # this. preferences 19 | dotnet_style_qualification_for_field = false:silent 20 | dotnet_style_qualification_for_property = false:silent 21 | dotnet_style_qualification_for_method = false:silent 22 | dotnet_style_qualification_for_event = false:silent 23 | # Language keywords vs BCL types preferences 24 | dotnet_style_predefined_type_for_locals_parameters_members = true:warning 25 | dotnet_style_predefined_type_for_member_access = true:warning 26 | # Parentheses preferences 27 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 28 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 29 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 30 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 31 | # Modifier preferences 32 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 33 | dotnet_style_readonly_field = true:suggestion 34 | # Expression-level preferences 35 | dotnet_style_object_initializer = true:suggestion 36 | dotnet_style_collection_initializer = true:suggestion 37 | dotnet_style_explicit_tuple_names = true:suggestion 38 | dotnet_style_null_propagation = true:suggestion 39 | dotnet_style_coalesce_expression = true:suggestion 40 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 41 | dotnet_prefer_inferred_tuple_names = true:suggestion 42 | dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion 43 | dotnet_style_prefer_auto_properties = true:silent 44 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 45 | dotnet_style_prefer_conditional_expression_over_return = true:silent 46 | ############################### 47 | # Naming Conventions # 48 | ############################### 49 | # Style Definitions 50 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 51 | # Use PascalCase for constant fields 52 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 53 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 54 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 55 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 56 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 57 | dotnet_naming_symbols.constant_fields.required_modifiers = const 58 | ############################### 59 | # C# Coding Conventions # 60 | ############################### 61 | [*.cs] 62 | # var preferences 63 | csharp_style_var_for_built_in_types = false:suggestion 64 | csharp_style_var_when_type_is_apparent = false:suggestion 65 | csharp_style_var_elsewhere = false:suggestion 66 | # Expression-bodied members 67 | csharp_style_expression_bodied_methods = false:silent 68 | csharp_style_expression_bodied_constructors = false:silent 69 | csharp_style_expression_bodied_operators = false:silent 70 | csharp_style_expression_bodied_properties = true:silent 71 | csharp_style_expression_bodied_indexers = true:silent 72 | csharp_style_expression_bodied_accessors = true:silent 73 | # Pattern matching preferences 74 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 75 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 76 | # Null-checking preferences 77 | csharp_style_throw_expression = true:suggestion 78 | csharp_style_conditional_delegate_call = true:suggestion 79 | # Modifier preferences 80 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 81 | # Expression-level preferences 82 | csharp_prefer_braces = true:silent 83 | csharp_style_deconstructed_variable_declaration = true:suggestion 84 | csharp_prefer_simple_default_expression = true:suggestion 85 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 86 | csharp_style_inlined_variable_declaration = true:suggestion 87 | ############################### 88 | # C# Formatting Rules # 89 | ############################### 90 | # New line preferences 91 | csharp_new_line_before_open_brace = all 92 | csharp_new_line_before_else = true 93 | csharp_new_line_before_catch = true 94 | csharp_new_line_before_finally = true 95 | csharp_new_line_before_members_in_object_initializers = true 96 | csharp_new_line_before_members_in_anonymous_types = true 97 | csharp_new_line_between_query_expression_clauses = true 98 | # Indentation preferences 99 | csharp_indent_case_contents = true 100 | csharp_indent_switch_labels = true 101 | csharp_indent_labels = flush_left 102 | # Space preferences 103 | csharp_space_after_cast = false 104 | csharp_space_after_keywords_in_control_flow_statements = true 105 | csharp_space_between_method_call_parameter_list_parentheses = false 106 | csharp_space_between_method_declaration_parameter_list_parentheses = false 107 | csharp_space_between_parentheses = false 108 | csharp_space_before_colon_in_inheritance_clause = true 109 | csharp_space_after_colon_in_inheritance_clause = true 110 | csharp_space_around_binary_operators = before_and_after 111 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 112 | csharp_space_between_method_call_name_and_opening_parenthesis = false 113 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 114 | # Wrapping preferences 115 | csharp_preserve_single_line_statements = true 116 | csharp_preserve_single_line_blocks = true 117 | ############################### 118 | # VB Coding Conventions # 119 | ############################### 120 | [*.vb] 121 | # Modifier preferences 122 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion 123 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '44 20 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'csharp' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: MultiAdmin Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: .NET ${{matrix.framework}} on ${{matrix.os}} 8 | runs-on: ${{matrix.os}} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-18.04, windows-latest] 12 | framework: ['6.0'] 13 | include: 14 | - os: ubuntu-18.04 15 | target: linux-x64 16 | - os: windows-latest 17 | target: win-x64 18 | timeout-minutes: 30 19 | 20 | steps: 21 | - uses: actions/checkout@v2.3.4 22 | 23 | - if: matrix.os == 'ubuntu-18.04' 24 | name: Install Linux packages 25 | run: | 26 | sudo apt update 27 | sudo apt install -y clang zlib1g-dev libkrb5-dev libtinfo5 28 | 29 | - name: Setup .NET 30 | uses: actions/setup-dotnet@v1.7.2 31 | with: 32 | dotnet-version: ${{matrix.framework}} 33 | 34 | - name: Restore for ${{matrix.target}} 35 | run: dotnet restore -r ${{matrix.target}} 36 | 37 | - name: Publish for ${{matrix.target}} 38 | run: dotnet publish -r ${{matrix.target}} -c Release -o "${{github.workspace}}/Builds/${{matrix.framework}}/${{matrix.target}}" "MultiAdmin" 39 | 40 | - name: Run unit tests 41 | run: dotnet test 42 | 43 | - name: Upload ${{matrix.target}} build 44 | uses: actions/upload-artifact@v2.2.2 45 | with: 46 | name: MultiAdmin-${{matrix.target}}-${{matrix.framework}} 47 | path: ${{github.workspace}}/Builds/${{matrix.framework}}/${{matrix.target}} 48 | 49 | - name: Upload ${{matrix.target}} build to bundle 50 | uses: actions/upload-artifact@v2.2.2 51 | with: 52 | name: MultiAdmin-all-${{matrix.framework}} 53 | path: ${{github.workspace}}/Builds/${{matrix.framework}} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Coverlet is a free, cross platform Code Coverage Tool 141 | coverage*[.json, .xml, .info] 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | 355 | ## 356 | ## Visual studio for Mac 357 | ## 358 | 359 | 360 | # globs 361 | Makefile.in 362 | *.userprefs 363 | *.usertasks 364 | config.make 365 | config.status 366 | aclocal.m4 367 | install-sh 368 | autom4te.cache/ 369 | *.tar.gz 370 | tarballs/ 371 | test-results/ 372 | 373 | # Mac bundle stuff 374 | *.dmg 375 | *.app 376 | 377 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 378 | # General 379 | .DS_Store 380 | .AppleDouble 381 | .LSOverride 382 | 383 | # Icon must end with two \r 384 | Icon 385 | 386 | 387 | # Thumbnails 388 | ._* 389 | 390 | # Files that might appear in the root of a volume 391 | .DocumentRevisions-V100 392 | .fseventsd 393 | .Spotlight-V100 394 | .TemporaryItems 395 | .Trashes 396 | .VolumeIcon.icns 397 | .com.apple.timemachine.donotpresent 398 | 399 | # Directories potentially created on remote AFP share 400 | .AppleDB 401 | .AppleDesktop 402 | Network Trash Folder 403 | Temporary Items 404 | .apdisk 405 | 406 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 407 | # Windows thumbnail cache files 408 | Thumbs.db 409 | ehthumbs.db 410 | ehthumbs_vista.db 411 | 412 | # Dump file 413 | *.stackdump 414 | 415 | # Folder config file 416 | [Dd]esktop.ini 417 | 418 | # Recycle Bin used on file shares 419 | $RECYCLE.BIN/ 420 | 421 | # Windows Installer files 422 | *.cab 423 | *.msi 424 | *.msix 425 | *.msm 426 | *.msp 427 | 428 | # Windows shortcuts 429 | *.lnk 430 | 431 | # JetBrains Rider 432 | .idea/ 433 | *.sln.iml 434 | 435 | ## 436 | ## Visual Studio Code 437 | ## 438 | .vscode/* 439 | !.vscode/settings.json 440 | !.vscode/tasks.json 441 | !.vscode/launch.json 442 | !.vscode/extensions.json 443 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Grover 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MultiAdmin.Tests/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Coverlet is a free, cross platform Code Coverage Tool 141 | coverage*[.json, .xml, .info] 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | 355 | ## 356 | ## Visual studio for Mac 357 | ## 358 | 359 | 360 | # globs 361 | Makefile.in 362 | *.userprefs 363 | *.usertasks 364 | config.make 365 | config.status 366 | aclocal.m4 367 | install-sh 368 | autom4te.cache/ 369 | *.tar.gz 370 | tarballs/ 371 | test-results/ 372 | 373 | # Mac bundle stuff 374 | *.dmg 375 | *.app 376 | 377 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 378 | # General 379 | .DS_Store 380 | .AppleDouble 381 | .LSOverride 382 | 383 | # Icon must end with two \r 384 | Icon 385 | 386 | 387 | # Thumbnails 388 | ._* 389 | 390 | # Files that might appear in the root of a volume 391 | .DocumentRevisions-V100 392 | .fseventsd 393 | .Spotlight-V100 394 | .TemporaryItems 395 | .Trashes 396 | .VolumeIcon.icns 397 | .com.apple.timemachine.donotpresent 398 | 399 | # Directories potentially created on remote AFP share 400 | .AppleDB 401 | .AppleDesktop 402 | Network Trash Folder 403 | Temporary Items 404 | .apdisk 405 | 406 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 407 | # Windows thumbnail cache files 408 | Thumbs.db 409 | ehthumbs.db 410 | ehthumbs_vista.db 411 | 412 | # Dump file 413 | *.stackdump 414 | 415 | # Folder config file 416 | [Dd]esktop.ini 417 | 418 | # Recycle Bin used on file shares 419 | $RECYCLE.BIN/ 420 | 421 | # Windows Installer files 422 | *.cab 423 | *.msi 424 | *.msix 425 | *.msm 426 | *.msp 427 | 428 | # Windows shortcuts 429 | *.lnk 430 | 431 | # JetBrains Rider 432 | .idea/ 433 | *.sln.iml 434 | 435 | ## 436 | ## Visual Studio Code 437 | ## 438 | .vscode/* 439 | !.vscode/settings.json 440 | !.vscode/tasks.json 441 | !.vscode/launch.json 442 | !.vscode/extensions.json 443 | -------------------------------------------------------------------------------- /MultiAdmin.Tests/MultiAdmin.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | 8 5 | false 6 | MultiAdmin.Tests 7 | 8 | false 9 | false 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MultiAdmin.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using MultiAdmin; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle(nameof(MultiAdmin.Tests))] 8 | [assembly: AssemblyDescription("A set of Unit Tests for " + nameof(MultiAdmin) + " v" + Program.MaVersion)] 9 | [assembly: AssemblyProduct(nameof(MultiAdmin.Tests))] 10 | [assembly: AssemblyCopyright("Copyright © Grover 2021")] 11 | 12 | // Version information for an assembly consists of the following four values: 13 | // 14 | // Major Version 15 | // Minor Version 16 | // Build Number 17 | // Revision 18 | // 19 | // You can specify all the values or you can default the Build and Revision Numbers 20 | // by using the '*' as shown below: 21 | // [assembly: AssemblyVersion("1.0.*")] 22 | [assembly: AssemblyVersion(Program.MaVersion)] 23 | -------------------------------------------------------------------------------- /MultiAdmin.Tests/ServerIO/ShiftingListTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MultiAdmin.ServerIO; 3 | using Xunit; 4 | 5 | namespace MultiAdmin.Tests.ServerIO 6 | { 7 | public class ShiftingListTests 8 | { 9 | [Fact] 10 | public void ShiftingListTest() 11 | { 12 | const int maxCount = 2; 13 | ShiftingList shiftingList = new ShiftingList(maxCount); 14 | 15 | Assert.Equal(maxCount, shiftingList.MaxCount); 16 | } 17 | 18 | [Fact] 19 | public void AddTest() 20 | { 21 | const int maxCount = 2; 22 | const int entriesToAdd = 6; 23 | ShiftingList shiftingList = new ShiftingList(maxCount); 24 | 25 | for (int i = 0; i < entriesToAdd; i++) 26 | { 27 | shiftingList.Add($"Test{i}"); 28 | } 29 | 30 | Assert.Equal(maxCount, shiftingList.Count); 31 | 32 | for (int i = 0; i < shiftingList.Count; i++) 33 | { 34 | Assert.Equal($"Test{entriesToAdd - i - 1}", shiftingList[i]); 35 | } 36 | } 37 | 38 | [Fact] 39 | public void RemoveFromEndTest() 40 | { 41 | const int maxCount = 6; 42 | const int entriesToRemove = 2; 43 | ShiftingList shiftingList = new ShiftingList(maxCount); 44 | 45 | for (int i = 0; i < maxCount; i++) 46 | { 47 | shiftingList.Add($"Test{i}"); 48 | } 49 | 50 | for (int i = 0; i < entriesToRemove; i++) 51 | { 52 | shiftingList.RemoveFromEnd(); 53 | } 54 | 55 | Assert.Equal(Math.Max(maxCount - entriesToRemove, 0), shiftingList.Count); 56 | 57 | for (int i = 0; i < shiftingList.Count; i++) 58 | { 59 | Assert.Equal($"Test{maxCount - i - 1}", shiftingList[i]); 60 | } 61 | } 62 | 63 | [Fact] 64 | public void ReplaceTest() 65 | { 66 | const int maxCount = 6; 67 | const int indexToReplace = 2; 68 | ShiftingList shiftingList = new ShiftingList(maxCount); 69 | 70 | for (int i = 0; i < maxCount; i++) 71 | { 72 | shiftingList.Add($"Test{i}"); 73 | } 74 | 75 | for (int i = 0; i < maxCount; i++) 76 | { 77 | if (i == indexToReplace) 78 | { 79 | shiftingList.Replace("Replaced", indexToReplace); 80 | } 81 | } 82 | 83 | Assert.Equal(maxCount, shiftingList.Count); 84 | 85 | Assert.Equal("Replaced", shiftingList[indexToReplace]); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /MultiAdmin.Tests/ServerIO/StringSectionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MultiAdmin.ConsoleTools; 3 | using MultiAdmin.ServerIO; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace MultiAdmin.Tests.ServerIO 8 | { 9 | public class StringSectionsTests 10 | { 11 | private readonly ITestOutputHelper output; 12 | 13 | public StringSectionsTests(ITestOutputHelper output) 14 | { 15 | this.output = output; 16 | } 17 | 18 | [Theory] 19 | [InlineData("test string", new[] {"te", "st", " s", "tr", "in", "g"}, 2)] 20 | [InlineData("test string", new[] {"tes..", ".t ..", ".st..", ".ring"}, 5, ".", "..")] 21 | public void FromStringTest(string testString, string[] expectedSections, int sectionLength, 22 | string leftIndictator = null, string rightIndictator = null) 23 | { 24 | StringSections sections = StringSections.FromString(testString, sectionLength, 25 | leftIndictator != null ? new ColoredMessage(leftIndictator) : null, 26 | rightIndictator != null ? new ColoredMessage(rightIndictator) : null); 27 | 28 | Assert.NotNull(sections); 29 | Assert.NotNull(sections.Sections); 30 | 31 | Assert.Equal(expectedSections.Length, sections.Sections.Length); 32 | 33 | for (int i = 0; i < expectedSections.Length; i++) 34 | { 35 | string expected = expectedSections[i]; 36 | string result = sections.Sections[i].Section.GetText(); 37 | 38 | output.WriteLine($"Index {i} - Comparing \"{expected}\" to \"{result}\"..."); 39 | Assert.Equal(expected, result); 40 | } 41 | } 42 | 43 | [Theory] 44 | // No further characters can be output because of the prefix and suffix 45 | [InlineData("test string", 2, ".", ".")] 46 | public void FromStringThrowsTest(string testString, int sectionLength, string leftIndictator = null, 47 | string rightIndictator = null) 48 | { 49 | Assert.Throws(() => 50 | { 51 | StringSections.FromString(testString, sectionLength, 52 | leftIndictator != null ? new ColoredMessage(leftIndictator) : null, 53 | rightIndictator != null ? new ColoredMessage(rightIndictator) : null); 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MultiAdmin.Tests/Utility/CommandUtilsTests.cs: -------------------------------------------------------------------------------- 1 | using MultiAdmin.Utility; 2 | using Xunit; 3 | 4 | namespace MultiAdmin.Tests.Utility 5 | { 6 | public class CommandUtilsTests 7 | { 8 | [Theory] 9 | [InlineData("test", new string[] { "test" })] 10 | [InlineData("configgen \"", new string[] { "configgen", "\"" })] 11 | [InlineData("test something test something", new string[] { "test", "something", "test", "something" })] 12 | [InlineData("test \"something test\" something", new string[] { "test", "something test", "something" })] 13 | [InlineData("test \\\"something test\\\" something", new string[] { "test", "\"something", "test\"", "something" })] 14 | [InlineData("test \\\"something test\" something", new string[] { "test", "\"something", "test\"", "something" })] 15 | [InlineData("test \"something test\\\" something", new string[] { "test", "\"something", "test\"", "something" })] 16 | [InlineData("test \"something test something\"", new string[] { "test", "something test something" })] 17 | [InlineData("\"test something test something\"", new string[] { "test something test something" })] 18 | [InlineData("test \"something test something\\\"", new string[] { "test", "\"something", "test", "something\"" })] 19 | public void StringToArgsTest(string input, string[] expected) 20 | { 21 | string[] result = CommandUtils.StringToArgs(input); 22 | Assert.Equal(expected, result); 23 | } 24 | 25 | [Theory] 26 | [InlineData("test", 2, 2, new string[] { "st" })] 27 | [InlineData("configgen \"", 0, 11, new string[] { "configgen", "\"" })] 28 | [InlineData("configgen \"", 0, 10, new string[] { "configgen", "" })] 29 | [InlineData("test \"something test\" something\"", 10, 22, new string[] { "thing", "test something" })] 30 | [InlineData("test \"something \"test something\"", 10, 22, new string[] { "thing", "test something" })] 31 | public void StringToArgsSubstringTest(string input, int start, int count, string[] expected) 32 | { 33 | string[] result = CommandUtils.StringToArgs(input, start, count); 34 | Assert.Equal(expected, result); 35 | } 36 | 37 | [Theory] 38 | [InlineData("test", new string[] { "test" })] 39 | [InlineData("configgen\t\"", new string[] { "configgen", "\"" })] 40 | [InlineData("test\tsomething\ttest\tsomething", new string[] { "test", "something", "test", "something" })] 41 | [InlineData("test\t\"something\ttest\"\tsomething", new string[] { "test", "something\ttest", "something" })] 42 | [InlineData("test\t\\\"something\ttest\\\"\tsomething", new string[] { "test", "\"something", "test\"", "something" })] 43 | [InlineData("test\t\\\"something\ttest\"\tsomething", new string[] { "test", "\"something", "test\"", "something" })] 44 | [InlineData("test\t\"something\ttest\\\"\tsomething", new string[] { "test", "\"something", "test\"", "something" })] 45 | [InlineData("test\t\"something\ttest\tsomething\"", new string[] { "test", "something\ttest\tsomething" })] 46 | [InlineData("\"test\tsomething\ttest\tsomething\"", new string[] { "test\tsomething\ttest\tsomething" })] 47 | [InlineData("test\t\"something\ttest\tsomething\\\"", new string[] { "test", "\"something", "test", "something\"" })] 48 | [InlineData("test\t\\\"something \ttest\"\tsomething", new string[] { "test", "\"something ", "test\"", "something" })] 49 | [InlineData("test\t\"something\ttest\\\"\t something", new string[] { "test", "\"something", "test\"", " something" })] 50 | [InlineData("test \t\"something\ttest\tsomething\"", new string[] { "test ", "something\ttest\tsomething" })] 51 | [InlineData("\"test something\ttest\tsomething\"", new string[] { "test something\ttest\tsomething" })] 52 | [InlineData("test\t\"something test\tsomething\\\"", new string[] { "test", "\"something test", "something\"" })] 53 | public void StringToArgsSeparatorTest(string input, string[] expected) 54 | { 55 | string[] result = CommandUtils.StringToArgs(input, separator: '\t'); 56 | Assert.Equal(expected, result); 57 | } 58 | 59 | [Theory] 60 | [InlineData("test \\\"something test\\\" something", new string[] { "test", "\\something test\\", "something" })] 61 | [InlineData("test \\\"something test\" something", new string[] { "test", "\\something test", "something" })] 62 | [InlineData("test \"something test\\\" something", new string[] { "test", "something test\\", "something" })] 63 | [InlineData("test \"something test something\"", new string[] { "test", "something test something" })] 64 | [InlineData("\"test something test something\"", new string[] { "test something test something" })] 65 | [InlineData("test \"something test something\\\"", new string[] { "test", "something test something\\" })] 66 | [InlineData("test $\"something test$\" something", new string[] { "test", "\"something", "test\"", "something" })] 67 | [InlineData("test $\"something test\" something", new string[] { "test", "\"something", "test\"", "something" })] 68 | [InlineData("test \"something test$\" something", new string[] { "test", "\"something", "test\"", "something" })] 69 | [InlineData("test \"something test something$\"", new string[] { "test", "\"something", "test", "something\"" })] 70 | public void StringToArgsEscapeTest(string input, string[] expected) 71 | { 72 | string[] result = CommandUtils.StringToArgs(input, escapeChar: '$'); 73 | Assert.Equal(expected, result); 74 | } 75 | 76 | [Theory] 77 | [InlineData("test \\\'something test\\\' something", new string[] { "test", "\'something", "test\'", "something" })] 78 | [InlineData("test \\\'something test\' something", new string[] { "test", "\'something", "test\'", "something" })] 79 | [InlineData("test \'something test\\\' something", new string[] { "test", "\'something", "test\'", "something" })] 80 | [InlineData("test \'something test something\'", new string[] { "test", "something test something" })] 81 | [InlineData("\'test something test something\'", new string[] { "test something test something" })] 82 | [InlineData("test \'something test something\\\'", new string[] { "test", "\'something", "test", "something\'" })] 83 | public void StringToArgsQuotesTest(string input, string[] expected) 84 | { 85 | string[] result = CommandUtils.StringToArgs(input, quoteChar: '\''); 86 | Assert.Equal(expected, result); 87 | } 88 | 89 | [Theory] 90 | [InlineData("test \\\"something test\\\" something", new string[] { "test", "\"something", "test\"", "something" })] 91 | [InlineData("test \\\"something test\" something", new string[] { "test", "\"something", "test\"", "something" })] 92 | [InlineData("test \"something test\\\" something", new string[] { "test", "\"something", "test\"", "something" })] 93 | [InlineData("test \"something test something\"", new string[] { "test", "\"something test something\"" })] 94 | [InlineData("\"test something test something\"", new string[] { "\"test something test something\"" })] 95 | [InlineData("test \"something test something\\\"", new string[] { "test", "\"something", "test", "something\"" })] 96 | public void StringToArgsKeepQuotesTest(string input, string[] expected) 97 | { 98 | string[] result = CommandUtils.StringToArgs(input, keepQuotes: true); 99 | Assert.Equal(expected, result); 100 | } 101 | 102 | [Theory] 103 | [InlineData("test \"something test something\"", new string[] { "test", "something test something" })] 104 | [InlineData("test \"\"something test something\"\"", new string[] { "test", "\"something", "test", "something\"" })] 105 | [InlineData("test \"\"something test\"\" something", new string[] { "test", "\"something", "test\"", "something" })] 106 | [InlineData("test \"\"something test\" something", new string[] { "test", "\"something", "test\"", "something" })] 107 | [InlineData("test \"something test\"\" something", new string[] { "test", "\"something", "test\"", "something" })] 108 | [InlineData("\"test something test something\"", new string[] { "test something test something" })] 109 | [InlineData("test \"something test\"\" something\" test test \"test test\" something \"something\" test", 110 | new string[] { "test", "something test\" something", "test", "test", "test test", "something", "something", "test" })] 111 | public void StringToArgsDoubleEscapeTest(string input, string[] expected) 112 | { 113 | string[] result = CommandUtils.StringToArgs(input, escapeChar: '\"', quoteChar: '\"'); 114 | Assert.Equal(expected, result); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /MultiAdmin.Tests/Utility/StringExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MultiAdmin.Utility; 3 | using Xunit; 4 | 5 | namespace MultiAdmin.Tests.Utility 6 | { 7 | public class StringExtensionsTests 8 | { 9 | [Theory] 10 | [InlineData("test", "test", 0)] 11 | [InlineData("test", "test", 0, 4)] 12 | [InlineData("test", "st", 2)] 13 | [InlineData("test", "te", 0, 2)] 14 | [InlineData("test", "es", 1, 2)] 15 | [InlineData(null, null, 0)] 16 | [InlineData(null, null, 0, 1)] 17 | public void EqualsTest(string main, string section, int startIndex, int count = -1) 18 | { 19 | Assert.True(count < 0 ? main.Equals(section, startIndex) : main.Equals(section, startIndex, count)); 20 | } 21 | 22 | [Theory] 23 | [InlineData("test", "other", 0, 4)] 24 | [InlineData("test", "te", 2)] 25 | [InlineData("test", "st", 0, 2)] 26 | [InlineData("test", null, 0)] 27 | [InlineData(null, "test", 0)] 28 | [InlineData("test", null, 0, 1)] 29 | [InlineData(null, "test", 0, 1)] 30 | public void NotEqualsTest(string main, string section, int startIndex, int count = -1) 31 | { 32 | Assert.False(count < 0 ? main.Equals(section, startIndex) : main.Equals(section, startIndex, count)); 33 | } 34 | 35 | [Theory] 36 | [InlineData(typeof(ArgumentOutOfRangeException), "longtest", "test", 1, 5)] 37 | [InlineData(typeof(ArgumentOutOfRangeException), "test", "st", 3)] 38 | [InlineData(typeof(ArgumentOutOfRangeException), "test", "te", -1)] 39 | [InlineData(typeof(ArgumentOutOfRangeException), "test", "es", 4)] 40 | public void EqualsThrowsTest(Type expected, string main, string section, int startIndex, int count = -1) 41 | { 42 | Assert.Throws(expected, () => 43 | { 44 | if (count < 0) 45 | main.Equals(section, startIndex); 46 | else 47 | main.Equals(section, startIndex, count); 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MultiAdmin.Tests/Utility/UtilsTests.cs: -------------------------------------------------------------------------------- 1 | using MultiAdmin.Utility; 2 | using Xunit; 3 | 4 | namespace MultiAdmin.Tests.Utility 5 | { 6 | public class UtilsTests 7 | { 8 | [Fact] 9 | public void GetFullPathSafeTest() 10 | { 11 | Assert.Null(Utils.GetFullPathSafe(" ")); 12 | } 13 | 14 | [Theory] 15 | [InlineData("test", "*", true)] 16 | [InlineData("test", "te*", true)] 17 | [InlineData("test", "*st", true)] 18 | [InlineData("test", "******", true)] 19 | [InlineData("test", "te*t", true)] 20 | [InlineData("test", "t**st", true)] 21 | [InlineData("test", "s*", false)] 22 | [InlineData("longstringtestmessage", "l*s*t*e*g*", true)] 23 | [InlineData("AdminToolbox", "config_remoteadmin.txt", false)] 24 | [InlineData("config_remoteadmin.txt", "config_remoteadmin.txt", true)] 25 | [InlineData("sizetest", "sizetest1", false)] 26 | public void StringMatchesTest(string input, string pattern, bool expected) 27 | { 28 | bool result = Utils.StringMatches(input, pattern); 29 | Assert.Equal(expected, result); 30 | } 31 | 32 | [Theory] 33 | [InlineData("1.0.0.0", "2.0.0.0", -1)] 34 | [InlineData("1.0.0.0", "1.0.0.0", 0)] 35 | [InlineData("2.0.0.0", "1.0.0.0", 1)] 36 | 37 | [InlineData("1.0", "2.0.0.0", -1)] 38 | [InlineData("1.0", "1.0.0.0", -1)] // The first version is shorter, so it's lower 39 | [InlineData("2.0", "1.0.0.0", 1)] 40 | 41 | [InlineData("1.0.0.0", "2.0", -1)] 42 | [InlineData("1.0.0.0", "1.0", 1)] // The first version is longer, so it's higher 43 | [InlineData("2.0.0.0", "1.0", 1)] 44 | 45 | [InlineData("6.0.0.313", "5.18.0", 1)] 46 | [InlineData("5.18.0", "6.0.0.313", -1)] 47 | 48 | [InlineData("5.18.0", "5.18.0", 0)] 49 | [InlineData("5.18", "5.18.0", -1)] // The first version is shorter, so it's lower 50 | public void CompareVersionStringsTest(string firstVersion, string secondVersion, int expected) 51 | { 52 | int result = Utils.CompareVersionStrings(firstVersion, secondVersion); 53 | 54 | Assert.Equal(expected, result); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MultiAdmin.Tests/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MultiAdmin.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiAdmin", "MultiAdmin\MultiAdmin.csproj", "{457C38EC-1251-4FEA-80D9-2EA10BD18A35}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiAdmin.Tests", "MultiAdmin.Tests\MultiAdmin.Tests.csproj", "{314971BB-616B-4FAE-B375-5A4A670D8626}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Debug|x64.Build.0 = Debug|Any CPU 27 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Debug|x86.Build.0 = Debug|Any CPU 29 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Release|x64.ActiveCfg = Release|Any CPU 32 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Release|x64.Build.0 = Release|Any CPU 33 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Release|x86.ActiveCfg = Release|Any CPU 34 | {457C38EC-1251-4FEA-80D9-2EA10BD18A35}.Release|x86.Build.0 = Release|Any CPU 35 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Debug|x64.Build.0 = Debug|Any CPU 39 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Debug|x86.Build.0 = Debug|Any CPU 41 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Release|x64.ActiveCfg = Release|Any CPU 44 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Release|x64.Build.0 = Release|Any CPU 45 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Release|x86.ActiveCfg = Release|Any CPU 46 | {314971BB-616B-4FAE-B375-5A4A670D8626}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /MultiAdmin/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Coverlet is a free, cross platform Code Coverage Tool 141 | coverage*[.json, .xml, .info] 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | 355 | ## 356 | ## Visual studio for Mac 357 | ## 358 | 359 | 360 | # globs 361 | Makefile.in 362 | *.userprefs 363 | *.usertasks 364 | config.make 365 | config.status 366 | aclocal.m4 367 | install-sh 368 | autom4te.cache/ 369 | *.tar.gz 370 | tarballs/ 371 | test-results/ 372 | 373 | # Mac bundle stuff 374 | *.dmg 375 | *.app 376 | 377 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 378 | # General 379 | .DS_Store 380 | .AppleDouble 381 | .LSOverride 382 | 383 | # Icon must end with two \r 384 | Icon 385 | 386 | 387 | # Thumbnails 388 | ._* 389 | 390 | # Files that might appear in the root of a volume 391 | .DocumentRevisions-V100 392 | .fseventsd 393 | .Spotlight-V100 394 | .TemporaryItems 395 | .Trashes 396 | .VolumeIcon.icns 397 | .com.apple.timemachine.donotpresent 398 | 399 | # Directories potentially created on remote AFP share 400 | .AppleDB 401 | .AppleDesktop 402 | Network Trash Folder 403 | Temporary Items 404 | .apdisk 405 | 406 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 407 | # Windows thumbnail cache files 408 | Thumbs.db 409 | ehthumbs.db 410 | ehthumbs_vista.db 411 | 412 | # Dump file 413 | *.stackdump 414 | 415 | # Folder config file 416 | [Dd]esktop.ini 417 | 418 | # Recycle Bin used on file shares 419 | $RECYCLE.BIN/ 420 | 421 | # Windows Installer files 422 | *.cab 423 | *.msi 424 | *.msix 425 | *.msm 426 | *.msp 427 | 428 | # Windows shortcuts 429 | *.lnk 430 | 431 | # JetBrains Rider 432 | .idea/ 433 | *.sln.iml 434 | 435 | ## 436 | ## Visual Studio Code 437 | ## 438 | .vscode/* 439 | !.vscode/settings.json 440 | !.vscode/tasks.json 441 | !.vscode/launch.json 442 | !.vscode/extensions.json 443 | -------------------------------------------------------------------------------- /MultiAdmin/Config/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | using MultiAdmin.ConsoleTools; 6 | using MultiAdmin.ServerIO; 7 | using MultiAdmin.Utility; 8 | 9 | namespace MultiAdmin.Config 10 | { 11 | public class Config 12 | { 13 | public string[] rawData = { }; 14 | 15 | public Config(string path) 16 | { 17 | ReadConfigFile(path); 18 | } 19 | 20 | private string internalConfigPath; 21 | 22 | public string ConfigPath 23 | { 24 | get => internalConfigPath; 25 | private set 26 | { 27 | try 28 | { 29 | internalConfigPath = Utils.GetFullPathSafe(value); 30 | } 31 | catch (Exception e) 32 | { 33 | internalConfigPath = value; 34 | Program.LogDebugException(nameof(ConfigPath), e); 35 | } 36 | } 37 | } 38 | 39 | public void ReadConfigFile(string configPath) 40 | { 41 | if (string.IsNullOrEmpty(configPath)) return; 42 | 43 | ConfigPath = configPath; 44 | 45 | try 46 | { 47 | rawData = File.Exists(ConfigPath) ? File.ReadAllLines(ConfigPath, Encoding.UTF8) : new string[] { }; 48 | } 49 | catch (Exception e) 50 | { 51 | Program.LogDebugException(nameof(ReadConfigFile), e); 52 | 53 | new ColoredMessage[] 54 | { 55 | new ColoredMessage($"Error while reading config (Path = {ConfigPath ?? "Null"}):", 56 | ConsoleColor.Red), 57 | new ColoredMessage(e.ToString(), ConsoleColor.Red) 58 | }.WriteLines(); 59 | } 60 | } 61 | 62 | public void ReadConfigFile() 63 | { 64 | ReadConfigFile(ConfigPath); 65 | } 66 | 67 | public bool Contains(string key) 68 | { 69 | return rawData != null && 70 | rawData.Any(entry => entry.StartsWith($"{key}:", StringComparison.CurrentCultureIgnoreCase)); 71 | } 72 | 73 | private static string CleanValue(string value, bool removeQuotes = true) 74 | { 75 | if (string.IsNullOrEmpty(value)) return value; 76 | 77 | string newValue = value.Trim(); 78 | 79 | try 80 | { 81 | if (removeQuotes && newValue.StartsWith("\"") && newValue.EndsWith("\"")) 82 | return newValue.Substring(1, newValue.Length - 2); 83 | } 84 | catch (Exception e) 85 | { 86 | Program.LogDebugException(nameof(CleanValue), e); 87 | } 88 | 89 | return newValue; 90 | } 91 | 92 | public string GetString(string key, string def = null, bool removeQuotes = true) 93 | { 94 | try 95 | { 96 | foreach (string line in rawData) 97 | { 98 | if (!line.ToLower().StartsWith(key.ToLower() + ":")) continue; 99 | 100 | try 101 | { 102 | return CleanValue(line.Substring(key.Length + 1), removeQuotes); 103 | } 104 | catch (Exception e) 105 | { 106 | Program.LogDebugException(nameof(GetString), e); 107 | } 108 | } 109 | } 110 | catch (Exception e) 111 | { 112 | Program.LogDebugException(nameof(GetString), e); 113 | } 114 | 115 | return def; 116 | } 117 | 118 | public string[] GetStringArray(string key, string[] def = null) 119 | { 120 | try 121 | { 122 | string value = GetString(key, removeQuotes: false); 123 | 124 | if (!string.IsNullOrEmpty(value)) 125 | { 126 | try 127 | { 128 | return value.Split(',').Select(entry => CleanValue(entry)).ToArray(); 129 | } 130 | catch (Exception e) 131 | { 132 | Program.LogDebugException(nameof(GetStringArray), e); 133 | } 134 | } 135 | } 136 | catch (Exception e) 137 | { 138 | Program.LogDebugException(nameof(GetStringArray), e); 139 | } 140 | 141 | return def; 142 | } 143 | 144 | public int GetInt(string key, int def = 0) 145 | { 146 | try 147 | { 148 | string value = GetString(key); 149 | 150 | if (!string.IsNullOrEmpty(value) && int.TryParse(value, out int parseValue)) 151 | return parseValue; 152 | } 153 | catch (Exception e) 154 | { 155 | Program.LogDebugException(nameof(GetInt), e); 156 | } 157 | 158 | return def; 159 | } 160 | 161 | public uint GetUInt(string key, uint def = 0) 162 | { 163 | try 164 | { 165 | string value = GetString(key); 166 | 167 | if (!string.IsNullOrEmpty(value) && uint.TryParse(value, out uint parseValue)) 168 | return parseValue; 169 | } 170 | catch (Exception e) 171 | { 172 | Program.LogDebugException(nameof(GetUInt), e); 173 | } 174 | 175 | return def; 176 | } 177 | 178 | public float GetFloat(string key, float def = 0) 179 | { 180 | try 181 | { 182 | string value = GetString(key); 183 | 184 | if (!string.IsNullOrEmpty(value) && float.TryParse(value, out float parsedValue)) 185 | return parsedValue; 186 | } 187 | catch (Exception e) 188 | { 189 | Program.LogDebugException(nameof(GetFloat), e); 190 | } 191 | 192 | return def; 193 | } 194 | 195 | public double GetDouble(string key, double def = 0) 196 | { 197 | try 198 | { 199 | string value = GetString(key); 200 | 201 | if (!string.IsNullOrEmpty(value) && double.TryParse(value, out double parsedValue)) 202 | return parsedValue; 203 | } 204 | catch (Exception e) 205 | { 206 | Program.LogDebugException(nameof(GetDouble), e); 207 | } 208 | 209 | return def; 210 | } 211 | 212 | public decimal GetDecimal(string key, decimal def = 0) 213 | { 214 | try 215 | { 216 | string value = GetString(key); 217 | 218 | if (!string.IsNullOrEmpty(value) && decimal.TryParse(value, out decimal parsedValue)) 219 | return parsedValue; 220 | } 221 | catch (Exception e) 222 | { 223 | Program.LogDebugException(nameof(GetDecimal), e); 224 | } 225 | 226 | return def; 227 | } 228 | 229 | public bool GetBool(string key, bool def = false) 230 | { 231 | try 232 | { 233 | string value = GetString(key); 234 | 235 | if (!string.IsNullOrEmpty(value) && bool.TryParse(value, out bool parsedValue)) 236 | return parsedValue; 237 | } 238 | catch (Exception e) 239 | { 240 | Program.LogDebugException(nameof(GetBool), e); 241 | } 242 | 243 | return def; 244 | } 245 | 246 | public InputHandler.ConsoleInputSystem GetConsoleInputSystem(string key, InputHandler.ConsoleInputSystem def = InputHandler.ConsoleInputSystem.New) 247 | { 248 | try 249 | { 250 | string value = GetString(key); 251 | 252 | if (!string.IsNullOrEmpty(value) && Enum.TryParse(value, out var consoleInputSystem)) 253 | return consoleInputSystem; 254 | } 255 | catch (Exception e) 256 | { 257 | Program.LogDebugException(nameof(GetConsoleInputSystem), e); 258 | } 259 | 260 | return def; 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /MultiAdmin/Config/ConfigHandler/ConfigEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MultiAdmin.Config.ConfigHandler 4 | { 5 | /// 6 | /// A base for storing config values. This can be registered to a to get config values automatically. 7 | /// 8 | public abstract class ConfigEntry 9 | { 10 | /// 11 | /// The key to read from the config file. 12 | /// 13 | public string Key { get; } 14 | 15 | /// 16 | /// The type of the value of the . 17 | /// 18 | public abstract Type ValueType { get; } 19 | 20 | /// 21 | /// The value of the . 22 | /// 23 | public abstract object ObjectValue { get; set; } 24 | 25 | /// 26 | /// The default value of the . 27 | /// 28 | public abstract object ObjectDefault { get; set; } 29 | 30 | /// 31 | /// Whether to inherit this config value from the 's parent s if they support value inheritance. 32 | /// 33 | public bool Inherit { get; } 34 | 35 | /// 36 | /// The name of the . 37 | /// 38 | public string Name { get; } 39 | 40 | /// 41 | /// The description of the . 42 | /// 43 | public string Description { get; } 44 | 45 | /// 46 | /// Creates a basic with no values and indication for whether to inherit the value. 47 | /// 48 | public ConfigEntry(string key, bool inherit = true, string name = null, string description = null) 49 | { 50 | Key = key; 51 | 52 | Inherit = inherit; 53 | 54 | Name = name; 55 | Description = description; 56 | } 57 | 58 | /// 59 | /// Creates a basic with no values. 60 | /// 61 | public ConfigEntry(string key, string name = null, string description = null) : this(key, true, name, 62 | description) 63 | { 64 | } 65 | } 66 | 67 | /// 68 | /// 69 | /// A generic for storing config values. This can be registered to a to get config values automatically. 70 | /// 71 | public class ConfigEntry : ConfigEntry 72 | { 73 | public override Type ValueType => typeof(T); 74 | 75 | /// 76 | /// The typed value of the . 77 | /// 78 | public T Value { get; set; } 79 | 80 | /// 81 | /// The typed default value of the . 82 | /// 83 | public T Default { get; set; } 84 | 85 | public override object ObjectValue 86 | { 87 | get => Value; 88 | set => Value = (T)value; 89 | } 90 | 91 | public override object ObjectDefault 92 | { 93 | get => Default; 94 | set => Default = (T)value; 95 | } 96 | 97 | /// 98 | /// 99 | /// Creates a with the provided type, default value, and indication for whether to inherit the value. 100 | /// 101 | public ConfigEntry(string key, T defaultValue = default, bool inherit = true, string name = null, 102 | string description = null) : base(key, inherit, name, description) 103 | { 104 | Default = defaultValue; 105 | } 106 | 107 | /// 108 | /// 109 | /// Creates a with the provided type and default value. 110 | /// 111 | public ConfigEntry(string key, T defaultValue = default, string name = null, string description = null) : this( 112 | key, defaultValue, true, name, description) 113 | { 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /MultiAdmin/Config/ConfigHandler/ConfigRegister.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MultiAdmin.Config.ConfigHandler 4 | { 5 | /// 6 | /// A register. This abstract class provides a base for a config handler implementation. 7 | /// 8 | public abstract class ConfigRegister 9 | { 10 | /// 11 | /// A list of registered s. 12 | /// 13 | protected readonly List registeredConfigs = new List(); 14 | 15 | /// 16 | /// Returns an array of registered s. 17 | /// 18 | public ConfigEntry[] GetRegisteredConfigs() 19 | { 20 | return registeredConfigs.ToArray(); 21 | } 22 | 23 | /// 24 | /// Returns the first with a key matching . 25 | /// 26 | /// The key of the to retrieve. 27 | public ConfigEntry GetRegisteredConfig(string key) 28 | { 29 | if (string.IsNullOrEmpty(key)) 30 | return null; 31 | 32 | key = key.ToLower(); 33 | 34 | foreach (ConfigEntry registeredConfig in registeredConfigs) 35 | { 36 | if (key == registeredConfig.Key.ToLower()) 37 | return registeredConfig; 38 | } 39 | 40 | return null; 41 | } 42 | 43 | /// 44 | /// Registers into the to be assigned a value. 45 | /// 46 | /// The to be registered. 47 | /// Whether to update the value of the config after registration. 48 | public void RegisterConfig(ConfigEntry configEntry, bool updateValue = true) 49 | { 50 | if (configEntry == null || string.IsNullOrEmpty(configEntry.Key)) 51 | return; 52 | 53 | registeredConfigs.Add(configEntry); 54 | 55 | if (updateValue) 56 | UpdateConfigValue(configEntry); 57 | } 58 | 59 | /// 60 | /// Registers into the to be assigned values. 61 | /// 62 | /// The s to be registered. 63 | /// Whether to update the value of the config after registration. 64 | public void RegisterConfigs(ConfigEntry[] configEntries, bool updateValue = true) 65 | { 66 | if (configEntries == null) 67 | return; 68 | 69 | foreach (ConfigEntry configEntry in configEntries) 70 | { 71 | RegisterConfig(configEntry, updateValue); 72 | } 73 | } 74 | 75 | /// 76 | /// Un-registers from the . 77 | /// 78 | /// The to be un-registered. 79 | public void UnRegisterConfig(ConfigEntry configEntry) 80 | { 81 | if (configEntry == null || string.IsNullOrEmpty(configEntry.Key)) 82 | return; 83 | 84 | registeredConfigs.Remove(configEntry); 85 | } 86 | 87 | /// 88 | /// Un-registers the linked to the given from the . 89 | /// 90 | /// The key of the to be un-registered. 91 | public void UnRegisterConfig(string key) 92 | { 93 | UnRegisterConfig(GetRegisteredConfig(key)); 94 | } 95 | 96 | /// 97 | /// Un-registers from the . 98 | /// 99 | /// The s to be un-registered. 100 | public void UnRegisterConfigs(params ConfigEntry[] configEntries) 101 | { 102 | if (configEntries == null) 103 | return; 104 | 105 | foreach (ConfigEntry configEntry in configEntries) 106 | { 107 | UnRegisterConfig(configEntry); 108 | } 109 | } 110 | 111 | /// 112 | /// Un-registers the s linked to the given from the . 113 | /// 114 | /// The keys of the s to be un-registered. 115 | public void UnRegisterConfigs(params string[] keys) 116 | { 117 | if (keys == null) 118 | return; 119 | 120 | foreach (string key in keys) 121 | { 122 | UnRegisterConfig(key); 123 | } 124 | } 125 | 126 | /// 127 | /// Un-registers all registered s from the . 128 | /// 129 | public void UnRegisterConfigs() 130 | { 131 | foreach (ConfigEntry configEntry in registeredConfigs) 132 | { 133 | UnRegisterConfig(configEntry); 134 | } 135 | } 136 | 137 | /// 138 | /// Updates the value of . 139 | /// 140 | /// The to be assigned a value. 141 | public abstract void UpdateConfigValue(ConfigEntry configEntry); 142 | 143 | /// 144 | /// Updates the values of the . 145 | /// 146 | /// The s to be assigned values. 147 | public void UpdateConfigValues(params ConfigEntry[] configEntries) 148 | { 149 | if (configEntries == null) 150 | return; 151 | 152 | foreach (ConfigEntry configEntry in configEntries) 153 | { 154 | UpdateConfigValue(configEntry); 155 | } 156 | } 157 | 158 | /// 159 | /// Updates the values of the registered s. 160 | /// 161 | public void UpdateRegisteredConfigValues() 162 | { 163 | foreach (ConfigEntry registeredConfig in registeredConfigs) 164 | { 165 | UpdateConfigValue(registeredConfig); 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /MultiAdmin/Config/ConfigHandler/InheritableConfigRegister.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MultiAdmin.Config.ConfigHandler 4 | { 5 | /// 6 | /// A register. This abstract class provides a base for a config handler implementation and inheritance. 7 | /// 8 | public abstract class InheritableConfigRegister : ConfigRegister 9 | { 10 | /// 11 | /// Creates an with the parent to inherit unset config values from. 12 | /// 13 | /// The to inherit unset config values from. 14 | protected InheritableConfigRegister(ConfigRegister parentConfigRegister = null) 15 | { 16 | ParentConfigRegister = parentConfigRegister; 17 | } 18 | 19 | /// 20 | /// The parent to inherit from. 21 | /// 22 | public ConfigRegister ParentConfigRegister { get; protected set; } 23 | 24 | /// 25 | /// Returns whether should be inherited from the parent . 26 | /// 27 | /// The to decide whether to inherit. 28 | public abstract bool ShouldInheritConfigEntry(ConfigEntry configEntry); 29 | 30 | /// 31 | /// Updates the value of . 32 | /// 33 | /// The to be assigned a value. 34 | public abstract void UpdateConfigValueInheritable(ConfigEntry configEntry); 35 | 36 | /// 37 | /// Updates the value of from this if the is null or if returns true. 38 | /// 39 | /// The to be assigned a value. 40 | public override void UpdateConfigValue(ConfigEntry configEntry) 41 | { 42 | if (configEntry != null && configEntry.Inherit && ParentConfigRegister != null && 43 | ShouldInheritConfigEntry(configEntry)) 44 | { 45 | ParentConfigRegister.UpdateConfigValue(configEntry); 46 | } 47 | else 48 | { 49 | UpdateConfigValueInheritable(configEntry); 50 | } 51 | } 52 | 53 | /// 54 | /// Returns an array of the hierarchy of s. 55 | /// 56 | /// Whether to order the returned array from highest in the hierarchy to the lowest. 57 | public ConfigRegister[] GetConfigRegisterHierarchy(bool highestToLowest = true) 58 | { 59 | List configRegisterHierarchy = new List(); 60 | 61 | ConfigRegister configRegister = this; 62 | while (configRegister != null && !configRegisterHierarchy.Contains(configRegister)) 63 | { 64 | configRegisterHierarchy.Add(configRegister); 65 | 66 | // If there's another InheritableConfigRegister as a parent, then get the parent of that, otherwise, break the loop as there are no more parents 67 | if (configRegister is InheritableConfigRegister inheritableConfigRegister) 68 | { 69 | configRegister = inheritableConfigRegister.ParentConfigRegister; 70 | } 71 | else 72 | { 73 | break; 74 | } 75 | } 76 | 77 | if (highestToLowest) 78 | configRegisterHierarchy.Reverse(); 79 | 80 | return configRegisterHierarchy.ToArray(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /MultiAdmin/Config/MultiAdminConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using MultiAdmin.Config.ConfigHandler; 7 | using MultiAdmin.ConsoleTools; 8 | using MultiAdmin.ServerIO; 9 | using MultiAdmin.Utility; 10 | 11 | namespace MultiAdmin.Config 12 | { 13 | public class MultiAdminConfig : InheritableConfigRegister 14 | { 15 | #region Config Keys and Values 16 | 17 | public ConfigEntry ConfigLocation { get; } = 18 | new ConfigEntry("config_location", "", false, 19 | "Config Location", "The default location for the game to use for storing configuration files (a directory)"); 20 | 21 | public ConfigEntry AppDataLocation { get; } = 22 | new ConfigEntry("appdata_location", "", 23 | "AppData Location", "The location for the game to use for AppData (a directory)"); 24 | 25 | public ConfigEntry DisableConfigValidation { get; } = 26 | new ConfigEntry("disable_config_validation", false, 27 | "Disable Config Validation", "Disable the config validator"); 28 | 29 | public ConfigEntry ShareNonConfigs { get; } = 30 | new ConfigEntry("share_non_configs", true, 31 | "Share Non-Configs", "Makes all files other than the config files store in AppData"); 32 | 33 | public ConfigEntry LogLocation { get; } = 34 | new ConfigEntry("multiadmin_log_location", "logs", 35 | "MultiAdmin Log Location", "The folder that MultiAdmin will store logs in (a directory)"); 36 | 37 | public ConfigEntry NoLog { get; } = 38 | new ConfigEntry("multiadmin_nolog", false, 39 | "MultiAdmin No-Logging", "Disable logging to file"); 40 | 41 | public ConfigEntry DebugLog { get; } = 42 | new ConfigEntry("multiadmin_debug_log", true, 43 | "MultiAdmin Debug Logging", "Enables MultiAdmin debug logging, this logs to a separate file than any other logs"); 44 | 45 | public ConfigEntry DebugLogBlacklist { get; } = 46 | new ConfigEntry("multiadmin_debug_log_blacklist", new string[] {nameof(OutputHandler.HandleMessage), nameof(Utils.StringMatches), nameof(ServerSocket.MessageListener) }, 47 | "MultiAdmin Debug Logging Blacklist", "Which tags to block for MultiAdmin debug logging"); 48 | 49 | public ConfigEntry DebugLogWhitelist { get; } = 50 | new ConfigEntry("multiadmin_debug_log_whitelist", new string[0], 51 | "MultiAdmin Debug Logging Whitelist", "Which tags to log for MultiAdmin debug logging (Defaults to logging all if none are provided)"); 52 | 53 | public ConfigEntry UseNewInputSystem { get; } = 54 | new ConfigEntry("use_new_input_system", true, 55 | "Use New Input System", "**OBSOLETE: Use `console_input_system` instead, this config option may be removed in a future version of MultiAdmin.** Whether to use the new input system, if false, the original input system will be used"); 56 | 57 | public ConfigEntry ConsoleInputSystem { get; } = 58 | new ConfigEntry("console_input_system", InputHandler.ConsoleInputSystem.New, 59 | "Console Input System", "Which console input system to use"); 60 | 61 | public ConfigEntry HideInput { get; } = 62 | new ConfigEntry("hide_input", false, 63 | "Hide Console Input", "Whether to hide console input, if true, typed input will not be printed"); 64 | 65 | public ConfigEntry Port { get; } = 66 | new ConfigEntry("port", 7777, 67 | "Game Port", "The port for the server to use"); 68 | 69 | public ConfigEntry CopyFromFolderOnReload { get; } = 70 | new ConfigEntry("copy_from_folder_on_reload", "", 71 | "Copy from Folder on Reload", "The location of a folder to copy files from into the folder defined by `config_location` whenever the configuration file is reloaded"); 72 | 73 | public ConfigEntry FolderCopyWhitelist { get; } = 74 | new ConfigEntry("folder_copy_whitelist", new string[0], 75 | "Folder Copy Whitelist", "The list of file names to copy from the folder defined by `copy_from_folder_on_reload` (accepts `*` wildcards)"); 76 | 77 | public ConfigEntry FolderCopyBlacklist { get; } = 78 | new ConfigEntry("folder_copy_blacklist", new string[0], 79 | "Folder Copy Blacklist", "The list of file names to not copy from the folder defined by `copy_from_folder_on_reload` (accepts `*` wildcards)"); 80 | 81 | public ConfigEntry FolderCopyRoundQueue { get; } = 82 | new ConfigEntry("folder_copy_round_queue", new string[0], 83 | "Folder Copy Round Queue", "The location of a folder to copy files from into the folder defined by `config_location` after each round, looping through the locations"); 84 | 85 | public ConfigEntry FolderCopyRoundQueueWhitelist { get; } = 86 | new ConfigEntry("folder_copy_round_queue_whitelist", new string[0], 87 | "Folder Copy Round Queue Whitelist", "The list of file names to copy from the folders defined by `folder_copy_round_queue` (accepts `*` wildcards)"); 88 | 89 | public ConfigEntry FolderCopyRoundQueueBlacklist { get; } = 90 | new ConfigEntry("folder_copy_round_queue_blacklist", new string[0], 91 | "Folder Copy Round Queue Blacklist", "The list of file names to not copy from the folders defined by `folder_copy_round_queue` (accepts `*` wildcards)"); 92 | 93 | public ConfigEntry RandomizeFolderCopyRoundQueue { get; } = 94 | new ConfigEntry("randomize_folder_copy_round_queue", false, 95 | "Randomize Folder Copy Round Queue", "Whether to randomize the order of entries in `folder_copy_round_queue`"); 96 | 97 | public ConfigEntry ManualStart { get; } = 98 | new ConfigEntry("manual_start", false, 99 | "Manual Start", "Whether or not to start the server automatically when launching MultiAdmin"); 100 | 101 | public ConfigEntry MaxMemory { get; } = 102 | new ConfigEntry("max_memory", 2048, 103 | "Max Memory", "The amount of memory in megabytes for MultiAdmin to check against"); 104 | 105 | public ConfigEntry RestartLowMemory { get; } = 106 | new ConfigEntry("restart_low_memory", 400, 107 | "Restart Low Memory", "Restart if the game's remaining memory falls below this value in megabytes"); 108 | 109 | public ConfigEntry RestartLowMemoryTicks { get; } = 110 | new ConfigEntry("restart_low_memory_ticks", 10, 111 | "Restart Low Memory Ticks", "The number of ticks the memory can be over the limit before restarting"); 112 | 113 | public ConfigEntry RestartLowMemoryRoundEnd { get; } = 114 | new ConfigEntry("restart_low_memory_roundend", 450, 115 | "Restart Low Memory Round-End", "Restart at the end of the round if the game's remaining memory falls below this value in megabytes"); 116 | 117 | public ConfigEntry RestartLowMemoryRoundEndTicks { get; } = 118 | new ConfigEntry("restart_low_memory_roundend_ticks", 10, 119 | "Restart Low Memory Round-End Ticks", "The number of ticks the memory can be over the limit before restarting at the end of the round"); 120 | 121 | public ConfigEntry RandomInputColors { get; } = 122 | new ConfigEntry("random_input_colors", false, 123 | "Random Input Colors", "Randomize the new input system's colors every time a message is input"); 124 | 125 | public ConfigEntry RestartEveryNumRounds { get; } = 126 | new ConfigEntry("restart_every_num_rounds", -1, 127 | "Restart Every Number of Rounds", "Restart the server every number of rounds"); 128 | 129 | public ConfigEntry RestartEveryNumRoundsCounting { get; } = 130 | new ConfigEntry("restart_every_num_rounds_counting", false, 131 | "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"); 132 | 133 | public ConfigEntry SafeServerShutdown { get; } = 134 | new ConfigEntry("safe_server_shutdown", true, 135 | "Safe Server Shutdown", "When MultiAdmin closes, if this is true, MultiAdmin will attempt to safely shutdown all servers"); 136 | 137 | public ConfigEntry SafeShutdownCheckDelay { get; } = 138 | new ConfigEntry("safe_shutdown_check_delay", 100, 139 | "Safe Shutdown Check Delay", "The time in milliseconds between checking if a server is still running when safely shutting down"); 140 | 141 | public ConfigEntry SafeShutdownTimeout { get; } = 142 | new ConfigEntry("safe_shutdown_timeout", 10000, 143 | "Safe Shutdown Timeout", "The time in milliseconds before MultiAdmin gives up on safely shutting down a server"); 144 | 145 | public ConfigEntry ServerRestartTimeout { get; } = 146 | new ConfigEntry("server_restart_timeout", 10, 147 | "Server Restart Timeout", "The time in seconds before MultiAdmin forces a server restart if it doesn't respond to the regular restart command"); 148 | 149 | public ConfigEntry ServerStopTimeout { get; } = 150 | new ConfigEntry("server_stop_timeout", 10, 151 | "Server Stop Timeout", "The time in seconds before MultiAdmin forces a server shutdown if it doesn't respond to the regular shutdown command"); 152 | 153 | public ConfigEntry ServerStartRetry { get; } = 154 | new ConfigEntry("server_start_retry", true, 155 | "Server Start Retry", "Whether to try to start the server again after crashing"); 156 | 157 | public ConfigEntry ServerStartRetryDelay { get; } = 158 | new ConfigEntry("server_start_retry_delay", 10000, 159 | "Server Start Retry Delay", "The time in milliseconds to wait before trying to start the server again after crashing"); 160 | 161 | public ConfigEntry MultiAdminTickDelay { get; } = 162 | new ConfigEntry("multiadmin_tick_delay", 1000, 163 | "MultiAdmin Tick Delay", "The time in milliseconds between MultiAdmin ticks (any features that update over time)"); 164 | 165 | public ConfigEntry ServersFolder { get; } = 166 | new ConfigEntry("servers_folder", "servers", 167 | "Servers Folder", "The location of the `servers` folder for MultiAdmin to load multiple server configurations from"); 168 | 169 | public ConfigEntry SetTitleBar { get; } = 170 | new ConfigEntry("set_title_bar", true, 171 | "Set Title Bar", "Whether to set the console window's titlebar, if false, this feature won't be used"); 172 | 173 | public ConfigEntry StartConfigOnFull { get; } = 174 | new ConfigEntry("start_config_on_full", "", 175 | "Start Config on Full", "Start server with this config folder once the server becomes full [Requires Modding]"); 176 | 177 | #endregion 178 | 179 | public InputHandler.ConsoleInputSystem ActualConsoleInputSystem 180 | { 181 | get 182 | { 183 | if (UseNewInputSystem.Value) 184 | { 185 | switch (ConsoleInputSystem.Value) 186 | { 187 | case InputHandler.ConsoleInputSystem.New: 188 | return HideInput.Value ? InputHandler.ConsoleInputSystem.Old : InputHandler.ConsoleInputSystem.New; 189 | 190 | case InputHandler.ConsoleInputSystem.Old: 191 | return InputHandler.ConsoleInputSystem.Old; 192 | } 193 | } 194 | 195 | return InputHandler.ConsoleInputSystem.Original; 196 | } 197 | } 198 | 199 | public const string ConfigFileName = "scp_multiadmin.cfg"; 200 | public static readonly string GlobalConfigFilePath = Utils.GetFullPathSafe(ConfigFileName); 201 | 202 | public static readonly MultiAdminConfig GlobalConfig = new MultiAdminConfig(GlobalConfigFilePath, null); 203 | 204 | public MultiAdminConfig ParentConfig 205 | { 206 | get => ParentConfigRegister as MultiAdminConfig; 207 | protected set => ParentConfigRegister = value; 208 | } 209 | 210 | public Config Config { get; } 211 | 212 | public MultiAdminConfig(Config config, MultiAdminConfig parentConfig, bool createConfig = true) 213 | { 214 | Config = config; 215 | ParentConfig = parentConfig; 216 | 217 | if (createConfig && !File.Exists(Config?.ConfigPath)) 218 | { 219 | try 220 | { 221 | if (Config?.ConfigPath != null) 222 | File.Create(Config.ConfigPath).Close(); 223 | } 224 | catch (Exception e) 225 | { 226 | new ColoredMessage[] 227 | { 228 | new ColoredMessage($"Error while creating config (Path = {Config?.ConfigPath ?? "Null"}):", 229 | ConsoleColor.Red), 230 | new ColoredMessage(e.ToString(), ConsoleColor.Red) 231 | }.WriteLines(); 232 | } 233 | } 234 | 235 | #region MultiAdmin Config Register 236 | 237 | foreach (PropertyInfo property in GetType().GetProperties()) 238 | { 239 | if (property.GetValue(this) is ConfigEntry entry) 240 | { 241 | RegisterConfig(entry); 242 | } 243 | } 244 | 245 | #endregion 246 | 247 | ReloadConfig(); 248 | } 249 | 250 | public MultiAdminConfig(Config config, bool createConfig = true) : this(config, GlobalConfig, createConfig) 251 | { 252 | } 253 | 254 | public MultiAdminConfig(string path, MultiAdminConfig parentConfig, bool createConfig = true) : this( 255 | new Config(path), parentConfig, createConfig) 256 | { 257 | } 258 | 259 | public MultiAdminConfig(string path, bool createConfig = true) : this(path, GlobalConfig, createConfig) 260 | { 261 | } 262 | 263 | #region Config Registration 264 | 265 | public override void UpdateConfigValueInheritable(ConfigEntry configEntry) 266 | { 267 | if (configEntry == null) 268 | throw new NullReferenceException("Config type unsupported (Config: Null)."); 269 | 270 | if (Config == null) 271 | { 272 | configEntry.ObjectValue = configEntry.ObjectDefault; 273 | return; 274 | } 275 | 276 | switch (configEntry) 277 | { 278 | case ConfigEntry config: 279 | { 280 | config.Value = Config.GetString(config.Key, config.Default); 281 | break; 282 | } 283 | 284 | case ConfigEntry config: 285 | { 286 | config.Value = Config.GetStringArray(config.Key, config.Default); 287 | break; 288 | } 289 | 290 | case ConfigEntry config: 291 | { 292 | config.Value = Config.GetInt(config.Key, config.Default); 293 | break; 294 | } 295 | 296 | case ConfigEntry config: 297 | { 298 | config.Value = Config.GetUInt(config.Key, config.Default); 299 | break; 300 | } 301 | 302 | case ConfigEntry config: 303 | { 304 | config.Value = Config.GetFloat(config.Key, config.Default); 305 | break; 306 | } 307 | 308 | case ConfigEntry config: 309 | { 310 | config.Value = Config.GetDouble(config.Key, config.Default); 311 | break; 312 | } 313 | 314 | case ConfigEntry config: 315 | { 316 | config.Value = Config.GetDecimal(config.Key, config.Default); 317 | break; 318 | } 319 | 320 | case ConfigEntry config: 321 | { 322 | config.Value = Config.GetBool(config.Key, config.Default); 323 | break; 324 | } 325 | 326 | case ConfigEntry config: 327 | { 328 | config.Value = Config.GetConsoleInputSystem(config.Key, config.Default); 329 | break; 330 | } 331 | 332 | default: 333 | { 334 | throw new ArgumentException( 335 | $"Config type unsupported (Config: Key = \"{configEntry.Key ?? "Null"}\" Type = \"{configEntry.ValueType.FullName ?? "Null"}\" Name = \"{configEntry.Name ?? "Null"}\" Description = \"{configEntry.Description ?? "Null"}\").", 336 | nameof(configEntry)); 337 | } 338 | } 339 | } 340 | 341 | public override bool ShouldInheritConfigEntry(ConfigEntry configEntry) 342 | { 343 | return !ConfigContains(configEntry.Key); 344 | } 345 | 346 | #endregion 347 | 348 | public void ReloadConfig() 349 | { 350 | ParentConfig?.ReloadConfig(); 351 | Config?.ReadConfigFile(); 352 | 353 | UpdateRegisteredConfigValues(); 354 | } 355 | 356 | public bool ConfigContains(string key) 357 | { 358 | return Config != null && Config.Contains(key); 359 | } 360 | 361 | public bool ConfigOrGlobalConfigContains(string key) 362 | { 363 | return ConfigContains(key) || GlobalConfig.ConfigContains(key); 364 | } 365 | 366 | public MultiAdminConfig[] GetConfigHierarchy(bool highestToLowest = true) 367 | { 368 | List configHierarchy = new List(); 369 | 370 | foreach (ConfigRegister configRegister in GetConfigRegisterHierarchy(highestToLowest)) 371 | { 372 | if (configRegister is MultiAdminConfig config) 373 | configHierarchy.Add(config); 374 | } 375 | 376 | return configHierarchy.ToArray(); 377 | } 378 | 379 | public bool ConfigHierarchyContainsPath(string path) 380 | { 381 | string fullPath = Utils.GetFullPathSafe(path); 382 | 383 | return !string.IsNullOrEmpty(fullPath) && 384 | GetConfigHierarchy().Any(config => config.Config?.ConfigPath == path); 385 | } 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /MultiAdmin/ConsoleTools/ColoredConsole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace MultiAdmin.ConsoleTools 5 | { 6 | public static class ColoredConsole 7 | { 8 | public static readonly object WriteLock = new object(); 9 | 10 | public static void Write(string text, ConsoleColor? textColor = null, ConsoleColor? backgroundColor = null) 11 | { 12 | lock (WriteLock) 13 | { 14 | if (text == null) return; 15 | 16 | ConsoleColor? lastFore = null; 17 | if (textColor != null) 18 | { 19 | lastFore = Console.ForegroundColor; 20 | Console.ForegroundColor = textColor.Value; 21 | } 22 | 23 | ConsoleColor? lastBack = null; 24 | if (backgroundColor != null) 25 | { 26 | lastBack = Console.BackgroundColor; 27 | Console.BackgroundColor = backgroundColor.Value; 28 | } 29 | 30 | Console.Write(text); 31 | 32 | if (lastFore != null) 33 | Console.ForegroundColor = lastFore.Value; 34 | if (lastBack != null) 35 | Console.BackgroundColor = lastBack.Value; 36 | } 37 | } 38 | 39 | public static void WriteLine(string text, ConsoleColor? textColor = null, ConsoleColor? backgroundColor = null) 40 | { 41 | lock (WriteLock) 42 | { 43 | Write(text, textColor, backgroundColor); 44 | 45 | Console.WriteLine(); 46 | } 47 | } 48 | 49 | public static void Write(params ColoredMessage[] message) 50 | { 51 | lock (WriteLock) 52 | { 53 | foreach (ColoredMessage coloredMessage in message) 54 | { 55 | if (coloredMessage != null) 56 | Write(coloredMessage.text, coloredMessage.textColor, coloredMessage.backgroundColor); 57 | } 58 | } 59 | } 60 | 61 | public static void WriteLine(params ColoredMessage[] message) 62 | { 63 | lock (WriteLock) 64 | { 65 | Write(message); 66 | 67 | Console.WriteLine(); 68 | } 69 | } 70 | 71 | public static void WriteLines(params ColoredMessage[] message) 72 | { 73 | lock (WriteLock) 74 | { 75 | foreach (ColoredMessage coloredMessage in message) WriteLine(coloredMessage); 76 | } 77 | } 78 | } 79 | 80 | public class ColoredMessage : ICloneable 81 | { 82 | public string text; 83 | public ConsoleColor? textColor; 84 | public ConsoleColor? backgroundColor; 85 | 86 | public int Length => text?.Length ?? 0; 87 | 88 | public ColoredMessage(string text, ConsoleColor? textColor = null, ConsoleColor? backgroundColor = null) 89 | { 90 | this.text = text; 91 | this.textColor = textColor; 92 | this.backgroundColor = backgroundColor; 93 | } 94 | 95 | public bool Equals(ColoredMessage other) 96 | { 97 | return string.Equals(text, other.text) && textColor == other.textColor && 98 | backgroundColor == other.backgroundColor; 99 | } 100 | 101 | public override bool Equals(object obj) 102 | { 103 | if (ReferenceEquals(null, obj)) 104 | { 105 | return false; 106 | } 107 | 108 | if (ReferenceEquals(this, obj)) 109 | { 110 | return true; 111 | } 112 | 113 | if (obj.GetType() != GetType()) 114 | { 115 | return false; 116 | } 117 | 118 | return Equals((ColoredMessage)obj); 119 | } 120 | 121 | public override int GetHashCode() 122 | { 123 | unchecked 124 | { 125 | int hashCode = text != null ? text.GetHashCode() : 0; 126 | hashCode = (hashCode * 397) ^ textColor.GetHashCode(); 127 | hashCode = (hashCode * 397) ^ backgroundColor.GetHashCode(); 128 | return hashCode; 129 | } 130 | } 131 | 132 | public static bool operator ==(ColoredMessage firstMessage, ColoredMessage secondMessage) 133 | { 134 | if (ReferenceEquals(firstMessage, secondMessage)) 135 | return true; 136 | 137 | if (ReferenceEquals(firstMessage, null) || ReferenceEquals(secondMessage, null)) 138 | return false; 139 | 140 | return firstMessage.Equals(secondMessage); 141 | } 142 | 143 | public static bool operator !=(ColoredMessage firstMessage, ColoredMessage secondMessage) 144 | { 145 | return !(firstMessage == secondMessage); 146 | } 147 | 148 | public override string ToString() 149 | { 150 | return text; 151 | } 152 | 153 | public ColoredMessage Clone() 154 | { 155 | return new ColoredMessage(text?.Clone() as string, textColor, backgroundColor); 156 | } 157 | 158 | object ICloneable.Clone() 159 | { 160 | return Clone(); 161 | } 162 | 163 | public void Write(bool clearConsoleLine = false) 164 | { 165 | lock (ColoredConsole.WriteLock) 166 | { 167 | ColoredConsole.Write(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(this) : this); 168 | } 169 | } 170 | 171 | public void WriteLine(bool clearConsoleLine = false) 172 | { 173 | lock (ColoredConsole.WriteLock) 174 | { 175 | ColoredConsole.WriteLine(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(this) : this); 176 | } 177 | } 178 | } 179 | 180 | public static class ColoredMessageArrayExtensions 181 | { 182 | private static string JoinTextIgnoreNull(ColoredMessage[] coloredMessages) 183 | { 184 | StringBuilder builder = new StringBuilder(""); 185 | 186 | foreach (ColoredMessage coloredMessage in coloredMessages) 187 | { 188 | if (coloredMessage != null) 189 | builder.Append(coloredMessage); 190 | } 191 | 192 | return builder.ToString(); 193 | } 194 | 195 | public static string GetText(this ColoredMessage[] message) 196 | { 197 | return JoinTextIgnoreNull(message); 198 | } 199 | 200 | public static void Write(this ColoredMessage[] message, bool clearConsoleLine = false) 201 | { 202 | lock (ColoredConsole.WriteLock) 203 | { 204 | ColoredConsole.Write(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(message) : message); 205 | } 206 | } 207 | 208 | public static void WriteLine(this ColoredMessage[] message, bool clearConsoleLine = false) 209 | { 210 | lock (ColoredConsole.WriteLock) 211 | { 212 | ColoredConsole.WriteLine(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(message) : message); 213 | } 214 | } 215 | 216 | public static void WriteLines(this ColoredMessage[] message, bool clearConsoleLine = false) 217 | { 218 | lock (ColoredConsole.WriteLock) 219 | { 220 | ColoredConsole.WriteLines(clearConsoleLine ? ConsoleUtils.ClearConsoleLine(message) : message); 221 | } 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /MultiAdmin/ConsoleTools/ConsolePositioning.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MultiAdmin.ConsoleTools 4 | { 5 | public static class ConsolePositioning 6 | { 7 | #region Console Point Properties 8 | 9 | public static BufferPoint BufferCursor 10 | { 11 | get => new BufferPoint(Console.CursorLeft, Console.CursorTop); 12 | set => Console.SetCursorPosition(value.x, value.y); 13 | } 14 | 15 | public static ConsolePoint ConsoleCursor 16 | { 17 | get => BufferCursor.ConsolePoint; 18 | set => BufferCursor = value.BufferPoint; 19 | } 20 | 21 | public static BufferPoint BufferLeft 22 | { 23 | get => new BufferPoint(0, 0); 24 | } 25 | 26 | public static BufferPoint BufferRight 27 | { 28 | get => new BufferPoint(Console.BufferWidth - 1, 0); 29 | } 30 | 31 | public static BufferPoint BufferTop 32 | { 33 | get => new BufferPoint(0, 0); 34 | } 35 | 36 | public static BufferPoint BufferBottom 37 | { 38 | get => new BufferPoint(0, Console.BufferHeight - 1); 39 | } 40 | 41 | #endregion 42 | } 43 | 44 | public struct ConsolePoint 45 | { 46 | public readonly int x, y; 47 | 48 | public BufferPoint BufferPoint => new BufferPoint(this); 49 | 50 | public ConsolePoint(int x, int y) 51 | { 52 | this.x = x; 53 | this.y = y; 54 | } 55 | 56 | public ConsolePoint(BufferPoint bufferPoint) : this(bufferPoint.x - Console.WindowLeft, 57 | bufferPoint.y - Console.WindowTop) 58 | { 59 | } 60 | 61 | public void SetAsCursor() 62 | { 63 | BufferPoint.SetAsCursor(); 64 | } 65 | 66 | public void SetAsCursorX() 67 | { 68 | BufferPoint.SetAsCursorX(); 69 | } 70 | 71 | public void SetAsCursorY() 72 | { 73 | BufferPoint.SetAsCursorY(); 74 | } 75 | } 76 | 77 | public struct BufferPoint 78 | { 79 | public readonly int x, y; 80 | 81 | public ConsolePoint ConsolePoint => new ConsolePoint(this); 82 | 83 | public BufferPoint(int x, int y) 84 | { 85 | this.x = x; 86 | this.y = y; 87 | } 88 | 89 | public BufferPoint(ConsolePoint consolePoint) : this(consolePoint.x + Console.WindowLeft, 90 | consolePoint.y + Console.WindowTop) 91 | { 92 | } 93 | 94 | public void SetAsCursor() 95 | { 96 | ConsolePositioning.BufferCursor = this; 97 | } 98 | 99 | public void SetAsCursorX() 100 | { 101 | Console.CursorLeft = x; 102 | } 103 | 104 | public void SetAsCursorY() 105 | { 106 | Console.CursorTop = y; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /MultiAdmin/ConsoleTools/ConsoleUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MultiAdmin.ConsoleTools 4 | { 5 | public static class ConsoleUtils 6 | { 7 | #region Clear Console Line Methods 8 | 9 | private static bool IsIndexWithinBuffer(int index) 10 | { 11 | return index >= 0 && index < Console.BufferWidth; 12 | } 13 | 14 | public static void ClearConsoleLine(int index, bool returnCursorPos = false) 15 | { 16 | lock (ColoredConsole.WriteLock) 17 | { 18 | if (Program.Headless) return; 19 | 20 | try 21 | { 22 | int cursorLeftReturnIndex = returnCursorPos ? Console.CursorLeft : 0; 23 | // Linux console uses visible section as a scrolling buffer, 24 | // that means that making the console taller moves CursorTop to a higher index, 25 | // but when the user makes the console smaller, CursorTop is left at a higher index than BufferHeight, 26 | // causing an error 27 | int cursorTopIndex = Math.Min(Console.CursorTop, Console.BufferHeight - 1); 28 | 29 | Console.SetCursorPosition(IsIndexWithinBuffer(index) ? index : 0, cursorTopIndex); 30 | 31 | // If the message stretches to the end of the console window, the console window will generally wrap the line into a new line, 32 | // so 1 is subtracted 33 | int charCount = Console.BufferWidth - Console.CursorLeft - 1; 34 | if (charCount > 0) 35 | { 36 | Console.Write(new string(' ', charCount)); 37 | } 38 | 39 | Console.SetCursorPosition(IsIndexWithinBuffer(cursorLeftReturnIndex) ? cursorLeftReturnIndex : 0, cursorTopIndex); 40 | } 41 | catch (Exception e) 42 | { 43 | Program.LogDebugException(nameof(ClearConsoleLine), e); 44 | } 45 | } 46 | } 47 | 48 | public static string ClearConsoleLine(string message) 49 | { 50 | if (!string.IsNullOrEmpty(message)) 51 | ClearConsoleLine(message.Contains(Environment.NewLine) ? 0 : message.Length); 52 | else 53 | ClearConsoleLine(0); 54 | 55 | return message; 56 | } 57 | 58 | public static ColoredMessage ClearConsoleLine(ColoredMessage message) 59 | { 60 | ClearConsoleLine(message?.text); 61 | return message; 62 | } 63 | 64 | public static ColoredMessage[] ClearConsoleLine(ColoredMessage[] message) 65 | { 66 | ClearConsoleLine(message?.GetText()); 67 | return message; 68 | } 69 | 70 | #endregion 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MultiAdmin/EventInterfaces.cs: -------------------------------------------------------------------------------- 1 | namespace MultiAdmin 2 | { 3 | public interface IMAEvent 4 | { 5 | } 6 | 7 | public interface IEventServerPreStart : IMAEvent 8 | { 9 | void OnServerPreStart(); 10 | } 11 | 12 | public interface IEventServerStart : IMAEvent 13 | { 14 | void OnServerStart(); 15 | } 16 | 17 | public interface IEventServerStop : IMAEvent 18 | { 19 | void OnServerStop(); 20 | } 21 | 22 | public interface IEventRoundEnd : IMAEvent 23 | { 24 | void OnRoundEnd(); 25 | } 26 | 27 | public interface IEventWaitingForPlayers : IMAEvent 28 | { 29 | void OnWaitingForPlayers(); 30 | } 31 | 32 | public interface IEventRoundStart : IMAEvent 33 | { 34 | void OnRoundStart(); 35 | } 36 | 37 | public interface IEventCrash : IMAEvent 38 | { 39 | void OnCrash(); 40 | } 41 | 42 | public interface IEventTick : IMAEvent 43 | { 44 | void OnTick(); 45 | } 46 | 47 | public interface IEventServerFull : IMAEvent 48 | { 49 | void OnServerFull(); 50 | } 51 | 52 | public interface IEventIdleEnter : IMAEvent 53 | { 54 | void OnIdleEnter(); 55 | } 56 | 57 | public interface IEventIdleExit : IMAEvent 58 | { 59 | void OnIdleExit(); 60 | } 61 | 62 | public interface ICommand 63 | { 64 | void OnCall(string[] args); 65 | string GetCommand(); 66 | string GetUsage(); 67 | bool PassToGame(); 68 | string GetCommandDescription(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /MultiAdmin/Exceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MultiAdmin 4 | { 5 | public static class Exceptions 6 | { 7 | [Serializable] 8 | public class ServerNotRunningException : Exception 9 | { 10 | public ServerNotRunningException() : base("The server is not running") 11 | { 12 | } 13 | } 14 | 15 | [Serializable] 16 | public class ServerAlreadyRunningException : Exception 17 | { 18 | public ServerAlreadyRunningException() : base("The server is already running") 19 | { 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MultiAdmin/Feature.cs: -------------------------------------------------------------------------------- 1 | namespace MultiAdmin 2 | { 3 | public abstract class Feature 4 | { 5 | protected Feature(Server server) 6 | { 7 | Server = server; 8 | } 9 | 10 | public Server Server { get; } 11 | 12 | public abstract string GetFeatureDescription(); 13 | public abstract void OnConfigReload(); 14 | public abstract string GetFeatureName(); 15 | public abstract void Init(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MultiAdmin/Features/Attributes/FeatureAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MultiAdmin.Features.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Class) 6 | ] 7 | public class FeatureAttribute : Attribute 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /MultiAdmin/Features/ConfigGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using MultiAdmin.Config; 5 | using MultiAdmin.Config.ConfigHandler; 6 | using MultiAdmin.Features.Attributes; 7 | using MultiAdmin.Utility; 8 | 9 | namespace MultiAdmin.Features 10 | { 11 | [Feature] 12 | internal class ConfigGenerator : Feature, ICommand 13 | { 14 | 15 | public ConfigGenerator(Server server) : base(server) 16 | { 17 | } 18 | 19 | public string GetCommand() 20 | { 21 | return "CONFIGGEN"; 22 | } 23 | 24 | public string GetCommandDescription() 25 | { 26 | return "Generates a full default MultiAdmin config file"; 27 | } 28 | 29 | public string GetUsage() 30 | { 31 | return "[FILE LOCATION]"; 32 | } 33 | 34 | public void OnCall(string[] args) 35 | { 36 | if (args.IsNullOrEmpty()) 37 | { 38 | Server.Write("You must specify the location of the file."); 39 | return; 40 | } 41 | 42 | string path = args[0]; 43 | try 44 | { 45 | FileAttributes fileAttributes = File.GetAttributes(path); 46 | 47 | if (fileAttributes.HasFlag(FileAttributes.Directory)) 48 | { 49 | // Path provided is a directory, add a default file 50 | path = Path.Combine(path, MultiAdminConfig.ConfigFileName); 51 | } 52 | } 53 | catch (ArgumentException) 54 | { 55 | Server.Write("The path provided is empty, contains only white spaces, or contains invalid characters."); 56 | return; 57 | } 58 | catch (PathTooLongException) 59 | { 60 | Server.Write("The path provided is too long."); 61 | return; 62 | } 63 | catch (NotSupportedException) 64 | { 65 | Server.Write("The path provided is in an invalid format."); 66 | return; 67 | } 68 | catch (Exception) 69 | { 70 | // Ignore, any proper exceptions will be presented when the file is written 71 | } 72 | 73 | ConfigEntry[] registeredConfigs = MultiAdminConfig.GlobalConfig.GetRegisteredConfigs(); 74 | 75 | List lines = new List(registeredConfigs.Length); 76 | foreach (ConfigEntry configEntry in registeredConfigs) 77 | { 78 | switch (configEntry) 79 | { 80 | case ConfigEntry config: 81 | { 82 | lines.Add($"{config.Key}: {(config.Default == null ? "" : string.Join(", ", config.Default))}"); 83 | break; 84 | } 85 | 86 | default: 87 | { 88 | lines.Add($"{configEntry.Key}: {configEntry.ObjectDefault ?? ""}"); 89 | break; 90 | } 91 | } 92 | } 93 | 94 | File.WriteAllLines(path, lines); 95 | Server.Write($"Default config written to \"{path}\""); 96 | } 97 | 98 | public bool PassToGame() 99 | { 100 | return false; 101 | } 102 | 103 | public override void OnConfigReload() 104 | { 105 | } 106 | 107 | public override string GetFeatureDescription() 108 | { 109 | return "Generates a full default MultiAdmin config file"; 110 | } 111 | 112 | public override string GetFeatureName() 113 | { 114 | return "Config Generator"; 115 | } 116 | 117 | public override void Init() 118 | { 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /MultiAdmin/Features/ConfigReload.cs: -------------------------------------------------------------------------------- 1 | using MultiAdmin.Features.Attributes; 2 | using MultiAdmin.Utility; 3 | 4 | namespace MultiAdmin.Features 5 | { 6 | [Feature] 7 | internal class ConfigReload : Feature, ICommand 8 | { 9 | public ConfigReload(Server server) : base(server) 10 | { 11 | } 12 | 13 | public string GetCommand() 14 | { 15 | return "CONFIG"; 16 | } 17 | 18 | public string GetCommandDescription() 19 | { 20 | return "Reloads the configuration file"; 21 | } 22 | 23 | public string GetUsage() 24 | { 25 | return ""; 26 | } 27 | 28 | public void OnCall(string[] args) 29 | { 30 | if (args.IsNullOrEmpty() || !args[0].ToLower().Equals("reload")) return; 31 | 32 | Server.Write("Reloading configs..."); 33 | 34 | Server.ReloadConfig(); 35 | 36 | Server.Write("MultiAdmin config has been reloaded!"); 37 | } 38 | 39 | public bool PassToGame() 40 | { 41 | return true; 42 | } 43 | 44 | public override string GetFeatureDescription() 45 | { 46 | return "Reloads the MultiAdmin configuration file"; 47 | } 48 | 49 | public override string GetFeatureName() 50 | { 51 | return "Config Reload"; 52 | } 53 | 54 | public override void Init() 55 | { 56 | } 57 | 58 | public override void OnConfigReload() 59 | { 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MultiAdmin/Features/EventTest.cs: -------------------------------------------------------------------------------- 1 | namespace MultiAdmin.Features 2 | { 3 | internal class EventTest : Feature, IEventCrash, 4 | IEventRoundEnd, IEventWaitingForPlayers, IEventRoundStart, IEventServerPreStart, IEventServerStart, 5 | IEventServerStop 6 | { 7 | public EventTest(Server server) : base(server) 8 | { 9 | } 10 | 11 | public void OnCrash() 12 | { 13 | Server.Write("EVENTTEST Crash"); 14 | } 15 | 16 | public void OnRoundEnd() 17 | { 18 | Server.Write("EVENTTEST on round end"); 19 | } 20 | 21 | public void OnWaitingForPlayers() 22 | { 23 | Server.Write("EVENTTEST on waiting for players"); 24 | } 25 | 26 | public void OnRoundStart() 27 | { 28 | Server.Write("EVENTTEST on round start"); 29 | } 30 | 31 | public void OnServerFull() 32 | { 33 | Server.Write("EVENTTEST Server full event"); 34 | } 35 | 36 | public void OnServerPreStart() 37 | { 38 | Server.Write("EVENTTEST on prestart"); 39 | } 40 | 41 | public void OnServerStart() 42 | { 43 | Server.Write("EVENTTEST on start"); 44 | } 45 | 46 | public void OnServerStop() 47 | { 48 | Server.Write("EVENTTEST on stop"); 49 | } 50 | 51 | public override void Init() 52 | { 53 | } 54 | 55 | public override void OnConfigReload() 56 | { 57 | } 58 | 59 | public override string GetFeatureDescription() 60 | { 61 | return "Tests the events"; 62 | } 63 | 64 | public override string GetFeatureName() 65 | { 66 | return "Test"; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /MultiAdmin/Features/ExitCommand.cs: -------------------------------------------------------------------------------- 1 | using MultiAdmin.Features.Attributes; 2 | 3 | namespace MultiAdmin.Features 4 | { 5 | [Feature] 6 | internal class ExitCommand : Feature, ICommand 7 | { 8 | public ExitCommand(Server server) : base(server) 9 | { 10 | } 11 | 12 | public string GetCommand() 13 | { 14 | return "EXIT"; 15 | } 16 | 17 | public string GetCommandDescription() 18 | { 19 | return "Exits the server"; 20 | } 21 | 22 | public string GetUsage() 23 | { 24 | return ""; 25 | } 26 | 27 | public void OnCall(string[] args) 28 | { 29 | Server.StopServer(); 30 | } 31 | 32 | public bool PassToGame() 33 | { 34 | return false; 35 | } 36 | 37 | public override void OnConfigReload() 38 | { 39 | } 40 | 41 | public override string GetFeatureDescription() 42 | { 43 | return "Adds a graceful exit command"; 44 | } 45 | 46 | public override string GetFeatureName() 47 | { 48 | return "Exit Command"; 49 | } 50 | 51 | public override void Init() 52 | { 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MultiAdmin/Features/FolderCopyRoundQueue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MultiAdmin.Features.Attributes; 3 | using MultiAdmin.Utility; 4 | 5 | namespace MultiAdmin.Features 6 | { 7 | [Feature] 8 | internal class FileCopyRoundQueue : Feature, IEventRoundEnd 9 | { 10 | private string[] queue; 11 | private string[] whitelist; 12 | private string[] blacklist; 13 | private bool randomizeQueue; 14 | private int queueIndex; 15 | 16 | public FileCopyRoundQueue(Server server) : base(server) 17 | { 18 | } 19 | 20 | public bool HasValidQueue => !queue.IsNullOrEmpty(); 21 | 22 | public void OnRoundEnd() 23 | { 24 | if (!HasValidQueue) return; 25 | 26 | CopyNextQueueFolder(); 27 | 28 | Server.SendMessage("CONFIG RELOAD"); 29 | } 30 | 31 | public void CopyNextQueueFolder() 32 | { 33 | if (!HasValidQueue) return; 34 | 35 | queueIndex = LoopingLimitIndex(queueIndex); 36 | 37 | string copyFrom = queue[queueIndex]; 38 | 39 | if (string.IsNullOrEmpty(copyFrom)) return; 40 | 41 | Server.CopyFromDir(copyFrom, whitelist, blacklist); 42 | 43 | queueIndex = randomizeQueue ? GetNextRandomIndex() : LoopingLimitIndex(queueIndex + 1); 44 | } 45 | 46 | private int LoopingLimitIndex(int index) 47 | { 48 | if (!HasValidQueue) return 0; 49 | 50 | if (index < 0) 51 | return queue.Length - 1; 52 | 53 | if (index >= queue.Length) 54 | return 0; 55 | 56 | return index; 57 | } 58 | 59 | private int GetNextRandomIndex() 60 | { 61 | if (!HasValidQueue) return 0; 62 | 63 | Random random = new Random(); 64 | 65 | int index; 66 | do 67 | { 68 | index = random.Next(0, queue.Length); 69 | } while (index == queueIndex); 70 | 71 | return index; 72 | } 73 | 74 | public override void Init() 75 | { 76 | queueIndex = 0; 77 | 78 | CopyNextQueueFolder(); 79 | } 80 | 81 | public override void OnConfigReload() 82 | { 83 | queue = Server.ServerConfig.FolderCopyRoundQueue.Value; 84 | whitelist = Server.ServerConfig.FolderCopyRoundQueueWhitelist.Value; 85 | blacklist = Server.ServerConfig.FolderCopyRoundQueueBlacklist.Value; 86 | randomizeQueue = Server.ServerConfig.RandomizeFolderCopyRoundQueue.Value; 87 | } 88 | 89 | public override string GetFeatureDescription() 90 | { 91 | return "Copies files from folders in a queue"; 92 | } 93 | 94 | public override string GetFeatureName() 95 | { 96 | return "Folder Copy Round Queue"; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /MultiAdmin/Features/GithubGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using MultiAdmin.Config; 6 | using MultiAdmin.Config.ConfigHandler; 7 | using MultiAdmin.Features.Attributes; 8 | using MultiAdmin.ServerIO; 9 | using MultiAdmin.Utility; 10 | 11 | namespace MultiAdmin.Features 12 | { 13 | [Feature] 14 | internal class GithubGenerator : Feature, ICommand 15 | { 16 | public const string EmptyIndicator = "**Empty**"; 17 | public const string ColumnSeparator = " | "; 18 | 19 | public GithubGenerator(Server server) : base(server) 20 | { 21 | } 22 | 23 | public string GetCommand() 24 | { 25 | return "GITHUBGEN"; 26 | } 27 | 28 | public string GetCommandDescription() 29 | { 30 | return "Generates a GitHub README file outlining all the features/commands"; 31 | } 32 | 33 | public string GetUsage() 34 | { 35 | return "[FILE LOCATION]"; 36 | } 37 | 38 | public void OnCall(string[] args) 39 | { 40 | if (args.IsNullOrEmpty()) 41 | { 42 | Server.Write("You must specify the location of the file."); 43 | return; 44 | } 45 | 46 | string path = args[0]; 47 | try 48 | { 49 | FileAttributes fileAttributes = File.GetAttributes(path); 50 | 51 | if (fileAttributes.HasFlag(FileAttributes.Directory)) 52 | { 53 | // Path provided is a directory, add a default file 54 | path = Path.Combine(path, "README.md"); 55 | } 56 | } 57 | catch (ArgumentException) 58 | { 59 | Server.Write("The path provided is empty, contains only white spaces, or contains invalid characters."); 60 | return; 61 | } 62 | catch (PathTooLongException) 63 | { 64 | Server.Write("The path provided is too long."); 65 | return; 66 | } 67 | catch (NotSupportedException) 68 | { 69 | Server.Write("The path provided is in an invalid format."); 70 | return; 71 | } 72 | catch (Exception) 73 | { 74 | // Ignore, any proper exceptions will be presented when the file is written 75 | } 76 | 77 | List lines = new List {"# MultiAdmin", "", "## Features", ""}; 78 | 79 | foreach (Feature feature in Server.features) 80 | { 81 | lines.Add($"- {feature.GetFeatureName()}: {feature.GetFeatureDescription()}"); 82 | } 83 | 84 | lines.Add(""); 85 | lines.Add("## MultiAdmin Commands"); 86 | lines.Add(""); 87 | foreach (ICommand comm in Server.commands.Values) 88 | { 89 | lines.Add($"- {(comm.GetCommand() + " " + comm.GetUsage()).Trim()}: {comm.GetCommandDescription()}"); 90 | } 91 | 92 | lines.Add(""); 93 | lines.Add("## Config Settings"); 94 | lines.Add(""); 95 | lines.Add( 96 | $"Config Option{ColumnSeparator}Value Type{ColumnSeparator}Default Value{ColumnSeparator}Description"); 97 | lines.Add($"---{ColumnSeparator}:---:{ColumnSeparator}:---:{ColumnSeparator}:------:"); 98 | 99 | foreach (ConfigEntry configEntry in MultiAdminConfig.GlobalConfig.GetRegisteredConfigs()) 100 | { 101 | StringBuilder stringBuilder = 102 | new StringBuilder($"{configEntry.Key ?? EmptyIndicator}{ColumnSeparator}"); 103 | 104 | switch (configEntry) 105 | { 106 | case ConfigEntry config: 107 | { 108 | stringBuilder.Append( 109 | $"String{ColumnSeparator}{(string.IsNullOrEmpty(config.Default) ? EmptyIndicator : config.Default)}"); 110 | break; 111 | } 112 | 113 | case ConfigEntry config: 114 | { 115 | stringBuilder.Append( 116 | $"String List{ColumnSeparator}{(config.Default?.IsEmpty() ?? true ? EmptyIndicator : string.Join(", ", config.Default))}"); 117 | break; 118 | } 119 | 120 | case ConfigEntry config: 121 | { 122 | stringBuilder.Append($"Integer{ColumnSeparator}{config.Default}"); 123 | break; 124 | } 125 | 126 | case ConfigEntry config: 127 | { 128 | stringBuilder.Append($"Unsigned Integer{ColumnSeparator}{config.Default}"); 129 | break; 130 | } 131 | 132 | case ConfigEntry config: 133 | { 134 | stringBuilder.Append($"Float{ColumnSeparator}{config.Default}"); 135 | break; 136 | } 137 | 138 | case ConfigEntry config: 139 | { 140 | stringBuilder.Append($"Double{ColumnSeparator}{config.Default}"); 141 | break; 142 | } 143 | 144 | case ConfigEntry config: 145 | { 146 | stringBuilder.Append($"Decimal{ColumnSeparator}{config.Default}"); 147 | break; 148 | } 149 | 150 | case ConfigEntry config: 151 | { 152 | stringBuilder.Append($"Boolean{ColumnSeparator}{config.Default}"); 153 | break; 154 | } 155 | 156 | case ConfigEntry config: 157 | { 158 | stringBuilder.Append($"[ConsoleInputSystem](#ConsoleInputSystem){ColumnSeparator}{config.Default}"); 159 | break; 160 | } 161 | 162 | default: 163 | { 164 | stringBuilder.Append( 165 | $"{configEntry.ValueType?.Name ?? EmptyIndicator}{ColumnSeparator}{configEntry.ObjectDefault ?? EmptyIndicator}"); 166 | break; 167 | } 168 | } 169 | 170 | stringBuilder.Append($"{ColumnSeparator}{configEntry.Description ?? EmptyIndicator}"); 171 | 172 | lines.Add(stringBuilder.ToString()); 173 | } 174 | 175 | File.WriteAllLines(path, lines); 176 | Server.Write($"GitHub README written to \"{path}\""); 177 | } 178 | 179 | public bool PassToGame() 180 | { 181 | return false; 182 | } 183 | 184 | public override void OnConfigReload() 185 | { 186 | } 187 | 188 | public override string GetFeatureDescription() 189 | { 190 | return "Generates a GitHub README file outlining all the features/commands"; 191 | } 192 | 193 | public override string GetFeatureName() 194 | { 195 | return "GitHub Generator"; 196 | } 197 | 198 | public override void Init() 199 | { 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /MultiAdmin/Features/HelpCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MultiAdmin.ConsoleTools; 4 | using MultiAdmin.Features.Attributes; 5 | using MultiAdmin.Utility; 6 | 7 | namespace MultiAdmin.Features 8 | { 9 | [Feature] 10 | public class HelpCommand : Feature, ICommand 11 | { 12 | private static readonly ColoredMessage helpPrefix = new ColoredMessage("Commands from MultiAdmin:\n", ConsoleColor.Yellow); 13 | 14 | public HelpCommand(Server server) : base(server) 15 | { 16 | } 17 | 18 | public string GetCommand() 19 | { 20 | return "HELP"; 21 | } 22 | 23 | public string GetCommandDescription() 24 | { 25 | return "Prints out available commands and their function"; 26 | } 27 | 28 | public void OnCall(string[] args) 29 | { 30 | ColoredMessage[] message = new ColoredMessage[2]; 31 | 32 | message[0] = helpPrefix; 33 | 34 | List helpOutput = new List(); 35 | foreach (KeyValuePair command in Server.commands) 36 | { 37 | string usage = command.Value.GetUsage(); 38 | if (!usage.IsEmpty()) usage = " " + usage; 39 | string output = $"{command.Key.ToUpper()}{usage}: {command.Value.GetCommandDescription()}"; 40 | helpOutput.Add(output); 41 | } 42 | 43 | helpOutput.Sort(); 44 | message[1] = new ColoredMessage(string.Join('\n', helpOutput), ConsoleColor.Green); 45 | 46 | Server.Write(message, helpPrefix.textColor); 47 | Server.Write("Commands from game:"); 48 | } 49 | 50 | public bool PassToGame() 51 | { 52 | return true; 53 | } 54 | 55 | public string GetUsage() 56 | { 57 | return ""; 58 | } 59 | 60 | public override void OnConfigReload() 61 | { 62 | } 63 | 64 | public override string GetFeatureDescription() 65 | { 66 | return "Display a full list of MultiAdmin commands and in game commands"; 67 | } 68 | 69 | public override string GetFeatureName() 70 | { 71 | return "Help"; 72 | } 73 | 74 | public override void Init() 75 | { 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /MultiAdmin/Features/MemoryChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MultiAdmin.Features.Attributes; 3 | 4 | namespace MultiAdmin.Features 5 | { 6 | [Feature] 7 | internal class MemoryChecker : Feature, IEventTick, IEventRoundEnd 8 | { 9 | private const decimal BytesInMegabyte = 1048576; 10 | 11 | private const int OutputPrecision = 2; 12 | 13 | private uint tickCount; 14 | private uint tickCountSoft; 15 | 16 | private uint maxTicks = 10; 17 | private uint maxTicksSoft = 10; 18 | 19 | private bool restart; 20 | 21 | public MemoryChecker(Server server) : base(server) 22 | { 23 | } 24 | 25 | #region Memory Values 26 | 27 | public long LowBytes { get; set; } 28 | public long LowBytesSoft { get; set; } 29 | 30 | public long MaxBytes { get; set; } 31 | 32 | public long MemoryUsedBytes 33 | { 34 | get 35 | { 36 | if (Server.GameProcess == null) 37 | return 0; 38 | 39 | Server.GameProcess.Refresh(); 40 | 41 | return Server.GameProcess.WorkingSet64; 42 | } 43 | } 44 | 45 | public long MemoryLeftBytes => MaxBytes - MemoryUsedBytes; 46 | 47 | public decimal LowMb 48 | { 49 | get => decimal.Divide(LowBytes, BytesInMegabyte); 50 | set => LowBytes = (long)decimal.Multiply(value, BytesInMegabyte); 51 | } 52 | 53 | public decimal LowMbSoft 54 | { 55 | get => decimal.Divide(LowBytesSoft, BytesInMegabyte); 56 | set => LowBytesSoft = (long)decimal.Multiply(value, BytesInMegabyte); 57 | } 58 | 59 | public decimal MaxMb 60 | { 61 | get => decimal.Divide(MaxBytes, BytesInMegabyte); 62 | set => MaxBytes = (long)decimal.Multiply(value, BytesInMegabyte); 63 | } 64 | 65 | public decimal MemoryUsedMb => decimal.Divide(MemoryUsedBytes, BytesInMegabyte); 66 | public decimal MemoryLeftMb => decimal.Divide(MemoryLeftBytes, BytesInMegabyte); 67 | 68 | #endregion 69 | 70 | public void OnRoundEnd() 71 | { 72 | if (!restart || Server.IsStopping) return; 73 | 74 | Server.Write("Restarting due to low memory (Round End)...", ConsoleColor.Red); 75 | 76 | Server.RestartServer(); 77 | 78 | Init(); 79 | } 80 | 81 | public void OnTick() 82 | { 83 | if (LowBytes < 0 && LowBytesSoft < 0 || MaxBytes < 0) return; 84 | 85 | if (tickCount < maxTicks && LowBytes >= 0 && MemoryLeftBytes <= LowBytes) 86 | { 87 | Server.Write( 88 | $"Warning: Program is running low on memory ({decimal.Round(MemoryLeftMb, OutputPrecision)} MB left), the server will restart if it continues", 89 | ConsoleColor.Red); 90 | tickCount++; 91 | } 92 | else 93 | { 94 | tickCount = 0; 95 | } 96 | 97 | if (!restart && tickCountSoft < maxTicksSoft && LowBytesSoft >= 0 && MemoryLeftBytes <= LowBytesSoft) 98 | { 99 | Server.Write( 100 | $"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", 101 | ConsoleColor.Red); 102 | tickCountSoft++; 103 | } 104 | else 105 | { 106 | tickCountSoft = 0; 107 | } 108 | 109 | if (Server.Status == ServerStatus.Restarting) return; 110 | 111 | if (tickCount >= maxTicks) 112 | { 113 | Server.Write("Restarting due to low memory...", ConsoleColor.Red); 114 | Server.RestartServer(); 115 | 116 | restart = false; 117 | } 118 | else if (!restart && tickCountSoft >= maxTicksSoft) 119 | { 120 | Server.Write("Server will restart at the end of the round due to low memory"); 121 | 122 | restart = true; 123 | } 124 | } 125 | 126 | public override void Init() 127 | { 128 | tickCount = 0; 129 | tickCountSoft = 0; 130 | 131 | restart = false; 132 | } 133 | 134 | public override string GetFeatureDescription() 135 | { 136 | return "Restarts the server if the working memory becomes too low"; 137 | } 138 | 139 | public override string GetFeatureName() 140 | { 141 | return "Restart On Low Memory"; 142 | } 143 | 144 | public override void OnConfigReload() 145 | { 146 | maxTicks = Server.ServerConfig.RestartLowMemoryTicks.Value; 147 | maxTicksSoft = Server.ServerConfig.RestartLowMemoryRoundEndTicks.Value; 148 | 149 | LowMb = Server.ServerConfig.RestartLowMemory.Value; 150 | LowMbSoft = Server.ServerConfig.RestartLowMemoryRoundEnd.Value; 151 | MaxMb = Server.ServerConfig.MaxMemory.Value; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /MultiAdmin/Features/MultiAdminInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MultiAdmin.Features.Attributes; 3 | 4 | namespace MultiAdmin.Features 5 | { 6 | [Feature] 7 | internal class MultiAdminInfo : Feature, IEventServerPreStart, ICommand 8 | { 9 | public MultiAdminInfo(Server server) : base(server) 10 | { 11 | } 12 | 13 | public void OnCall(string[] args) 14 | { 15 | PrintInfo(); 16 | } 17 | 18 | public string GetCommand() 19 | { 20 | return "INFO"; 21 | } 22 | 23 | public bool PassToGame() 24 | { 25 | return false; 26 | } 27 | 28 | public string GetCommandDescription() 29 | { 30 | return GetFeatureDescription(); 31 | } 32 | 33 | public string GetUsage() 34 | { 35 | return ""; 36 | } 37 | 38 | public void OnServerPreStart() 39 | { 40 | PrintInfo(); 41 | } 42 | 43 | public override void Init() 44 | { 45 | } 46 | 47 | public override void OnConfigReload() 48 | { 49 | } 50 | 51 | public void PrintInfo() 52 | { 53 | Server.Write( 54 | $"{nameof(MultiAdmin)} v{Program.MaVersion} (https://github.com/ServerMod/MultiAdmin/)\nReleased under MIT License Copyright © Grover 2021", 55 | ConsoleColor.DarkMagenta); 56 | } 57 | 58 | public override string GetFeatureDescription() 59 | { 60 | return $"Prints {nameof(MultiAdmin)} license and version information"; 61 | } 62 | 63 | public override string GetFeatureName() 64 | { 65 | return "MultiAdminInfo"; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /MultiAdmin/Features/NewCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using MultiAdmin.Features.Attributes; 3 | using MultiAdmin.Utility; 4 | 5 | namespace MultiAdmin.Features 6 | { 7 | [Feature] 8 | internal class NewCommand : Feature, ICommand, IEventServerFull 9 | { 10 | private string onFullServerId; 11 | private Process onFullServerInstance; 12 | 13 | public NewCommand(Server server) : base(server) 14 | { 15 | } 16 | 17 | public void OnCall(string[] args) 18 | { 19 | if (args.IsEmpty()) 20 | { 21 | Server.Write("Error: Missing Server ID!"); 22 | } 23 | else 24 | { 25 | string serverId = string.Join(" ", args); 26 | 27 | if (string.IsNullOrEmpty(serverId)) return; 28 | 29 | Server.Write($"Launching new server with Server ID: \"{serverId}\"..."); 30 | 31 | Program.StartServer(new Server(serverId, args: Program.Args)); 32 | } 33 | } 34 | 35 | public string GetCommand() 36 | { 37 | return "NEW"; 38 | } 39 | 40 | public bool PassToGame() 41 | { 42 | return false; 43 | } 44 | 45 | public string GetCommandDescription() 46 | { 47 | return "Starts a new server with the given Server ID"; 48 | } 49 | 50 | public string GetUsage() 51 | { 52 | return ""; 53 | } 54 | 55 | public override void Init() 56 | { 57 | } 58 | 59 | public override void OnConfigReload() 60 | { 61 | onFullServerId = Server.ServerConfig.StartConfigOnFull.Value; 62 | } 63 | 64 | public override string GetFeatureDescription() 65 | { 66 | return 67 | "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]"; 68 | } 69 | 70 | public override string GetFeatureName() 71 | { 72 | return "New Server"; 73 | } 74 | 75 | public void OnServerFull() 76 | { 77 | if (string.IsNullOrEmpty(onFullServerId)) return; 78 | 79 | // If a server instance has been started 80 | if (onFullServerInstance != null) 81 | { 82 | onFullServerInstance.Refresh(); 83 | 84 | if (!onFullServerInstance.HasExited) return; 85 | } 86 | 87 | Server.Write($"Launching new server with Server ID: \"{onFullServerId}\" due to this server being full..."); 88 | 89 | onFullServerInstance = Program.StartServer(new Server(onFullServerId, args: Program.Args)); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /MultiAdmin/Features/Restart.cs: -------------------------------------------------------------------------------- 1 | using MultiAdmin.Features.Attributes; 2 | 3 | namespace MultiAdmin.Features 4 | { 5 | [Feature] 6 | internal class Restart : Feature, ICommand 7 | { 8 | public Restart(Server server) : base(server) 9 | { 10 | } 11 | 12 | public string GetCommand() 13 | { 14 | return "RESTART"; 15 | } 16 | 17 | public string GetCommandDescription() 18 | { 19 | return "Restarts the game server (MultiAdmin will not restart, just the game)"; 20 | } 21 | 22 | public string GetUsage() 23 | { 24 | return ""; 25 | } 26 | 27 | public void OnCall(string[] args) 28 | { 29 | Server.RestartServer(); 30 | } 31 | 32 | public bool PassToGame() 33 | { 34 | return false; 35 | } 36 | 37 | public override string GetFeatureDescription() 38 | { 39 | return "Allows the game to be restarted without restarting MultiAdmin"; 40 | } 41 | 42 | public override string GetFeatureName() 43 | { 44 | return "Restart Command"; 45 | } 46 | 47 | public override void Init() 48 | { 49 | } 50 | 51 | public override void OnConfigReload() 52 | { 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MultiAdmin/Features/RestartRoundCounter.cs: -------------------------------------------------------------------------------- 1 | using MultiAdmin.Features.Attributes; 2 | 3 | namespace MultiAdmin.Features 4 | { 5 | [Feature] 6 | internal class RestartRoundCounter : Feature, IEventRoundEnd 7 | { 8 | private int count; 9 | private int restartAfter; 10 | 11 | public RestartRoundCounter(Server server) : base(server) 12 | { 13 | } 14 | 15 | public void OnRoundEnd() 16 | { 17 | // If the config value is set to an invalid value, disable this feature 18 | if (restartAfter <= 0) 19 | return; 20 | 21 | // If the count is less than the set number of rounds to go through 22 | if (++count < restartAfter) 23 | { 24 | if (Server.ServerConfig.RestartEveryNumRoundsCounting.Value) 25 | Server.Write($"{count}/{restartAfter} rounds have passed..."); 26 | } 27 | else 28 | { 29 | Server.Write($"{count}/{restartAfter} rounds have passed, restarting..."); 30 | 31 | Server.RestartServer(); 32 | count = 0; 33 | } 34 | } 35 | 36 | public override void Init() 37 | { 38 | count = 0; 39 | } 40 | 41 | public override void OnConfigReload() 42 | { 43 | restartAfter = Server.ServerConfig.RestartEveryNumRounds.Value; 44 | } 45 | 46 | public override string GetFeatureDescription() 47 | { 48 | return "Restarts the server after a number rounds completed [Requires Modding]"; 49 | } 50 | 51 | public override string GetFeatureName() 52 | { 53 | return "Restart After a Number of Rounds"; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MultiAdmin/Features/TitleBar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MultiAdmin.Features.Attributes; 4 | 5 | namespace MultiAdmin.Features 6 | { 7 | [Feature] 8 | internal class Titlebar : Feature, IEventServerStart 9 | { 10 | private int ServerProcessId 11 | { 12 | get 13 | { 14 | if (Server.GameProcess == null) 15 | return -1; 16 | 17 | Server.GameProcess.Refresh(); 18 | 19 | return Server.GameProcess.Id; 20 | } 21 | } 22 | 23 | public Titlebar(Server server) : base(server) 24 | { 25 | } 26 | 27 | public void OnServerStart() 28 | { 29 | UpdateTitlebar(); 30 | } 31 | 32 | public override string GetFeatureDescription() 33 | { 34 | return "Updates the title bar with instance based information"; 35 | } 36 | 37 | public override string GetFeatureName() 38 | { 39 | return "TitleBar"; 40 | } 41 | 42 | public override void Init() 43 | { 44 | UpdateTitlebar(); 45 | } 46 | 47 | public override void OnConfigReload() 48 | { 49 | UpdateTitlebar(); 50 | } 51 | 52 | private void UpdateTitlebar() 53 | { 54 | if (Program.Headless || !Server.ServerConfig.SetTitleBar.Value) return; 55 | 56 | List titleBar = new List {$"MultiAdmin {Program.MaVersion}"}; 57 | 58 | if (!string.IsNullOrEmpty(Server.serverId)) 59 | { 60 | titleBar.Add($"Config: {Server.serverId}"); 61 | } 62 | 63 | if (Server.IsGameProcessRunning) 64 | { 65 | titleBar.Add($"Port: {Server.Port}"); 66 | titleBar.Add($"PID: {ServerProcessId}"); 67 | } 68 | 69 | if (Server.SessionSocket != null) 70 | { 71 | titleBar.Add($"Console Port: {Server.SessionSocket.Port}"); 72 | } 73 | 74 | try 75 | { 76 | Console.Title = string.Join(" | ", titleBar); 77 | } 78 | catch (Exception e) 79 | { 80 | Program.LogDebugException(nameof(UpdateTitlebar), e); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /MultiAdmin/Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ServerMod/MultiAdmin/9403e36aaf58ff37ebdc823fb36427ac7d98da4f/MultiAdmin/Icon.ico -------------------------------------------------------------------------------- /MultiAdmin/ModFeatures.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MultiAdmin 4 | { 5 | [Flags] 6 | public enum ModFeatures 7 | { 8 | None = 0, 9 | 10 | // Replaces detecting game output with MultiAdmin events for game events 11 | CustomEvents = 1 << 0, 12 | 13 | // Supporting all current features 14 | All = ~(~0 << 1) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MultiAdmin/MultiAdmin.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0 5 | 8 6 | MultiAdmin 7 | Icon.ico 8 | 9 | false 10 | false 11 | false 12 | 13 | true 14 | false 15 | true 16 | false 17 | Speed 18 | true 19 | 20 | true 21 | 22 | 23 | false 24 | none 25 | 26 | 27 | 28 | LINUX 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /MultiAdmin/NativeExitSignal/IExitSignal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MultiAdmin.NativeExitSignal 4 | { 5 | public interface IExitSignal 6 | { 7 | event EventHandler Exit; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /MultiAdmin/NativeExitSignal/UnixExitSignal.cs: -------------------------------------------------------------------------------- 1 | #if LINUX 2 | using System; 3 | using System.Threading; 4 | using Mono.Unix; 5 | using Mono.Unix.Native; 6 | 7 | namespace MultiAdmin.NativeExitSignal 8 | { 9 | public class UnixExitSignal : IExitSignal 10 | { 11 | public event EventHandler Exit; 12 | 13 | private static readonly UnixSignal[] Signals = { 14 | new UnixSignal(Signum.SIGINT), // CTRL + C pressed 15 | new UnixSignal(Signum.SIGTERM), // Sending KILL 16 | new UnixSignal(Signum.SIGUSR1), 17 | new UnixSignal(Signum.SIGUSR2), 18 | new UnixSignal(Signum.SIGHUP) // Terminal is closed 19 | }; 20 | 21 | public UnixExitSignal() 22 | { 23 | new Thread(() => 24 | { 25 | // blocking call to wait for any kill signal 26 | UnixSignal.WaitAny(Signals, -1); 27 | 28 | Exit?.Invoke(this, EventArgs.Empty); 29 | }).Start(); 30 | } 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /MultiAdmin/NativeExitSignal/WinExitSignal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace MultiAdmin.NativeExitSignal 5 | { 6 | public class WinExitSignal : IExitSignal 7 | { 8 | public event EventHandler Exit; 9 | 10 | [DllImport("Kernel32")] 11 | public static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add); 12 | 13 | // A delegate type to be used as the handler routine 14 | // for SetConsoleCtrlHandler. 15 | public delegate bool HandlerRoutine(CtrlTypes ctrlType); 16 | 17 | // An enumerated type for the control messages 18 | // sent to the handler routine. 19 | public enum CtrlTypes 20 | { 21 | CtrlCEvent = 0, 22 | CtrlBreakEvent = 1, 23 | CtrlCloseEvent = 2, 24 | CtrlLogoffEvent = 5, 25 | CtrlShutdownEvent = 6 26 | } 27 | 28 | /// 29 | /// Need this as a member variable to avoid it being garbage collected. 30 | /// 31 | private readonly HandlerRoutine mHr; 32 | 33 | public WinExitSignal() 34 | { 35 | mHr = ConsoleCtrlCheck; 36 | 37 | SetConsoleCtrlHandler(mHr, true); 38 | } 39 | 40 | /// 41 | /// Handle the ctrl types 42 | /// 43 | /// 44 | /// 45 | private bool ConsoleCtrlCheck(CtrlTypes ctrlType) 46 | { 47 | switch (ctrlType) 48 | { 49 | case CtrlTypes.CtrlCEvent: 50 | case CtrlTypes.CtrlBreakEvent: 51 | case CtrlTypes.CtrlCloseEvent: 52 | case CtrlTypes.CtrlLogoffEvent: 53 | case CtrlTypes.CtrlShutdownEvent: 54 | Exit?.Invoke(this, EventArgs.Empty); 55 | break; 56 | } 57 | 58 | return true; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MultiAdmin/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading; 8 | using MultiAdmin.Config; 9 | using MultiAdmin.ConsoleTools; 10 | using MultiAdmin.NativeExitSignal; 11 | using MultiAdmin.ServerIO; 12 | using MultiAdmin.Utility; 13 | 14 | namespace MultiAdmin 15 | { 16 | public static class Program 17 | { 18 | public const string MaVersion = "3.4.1.0"; 19 | 20 | private static readonly List InstantiatedServers = new List(); 21 | 22 | private static readonly string MaDebugLogDir = 23 | Utils.GetFullPathSafe(MultiAdminConfig.GlobalConfig.LogLocation.Value); 24 | 25 | private static readonly string MaDebugLogFile = !string.IsNullOrEmpty(MaDebugLogDir) 26 | ? Utils.GetFullPathSafe(Path.Combine(MaDebugLogDir, $"{Utils.DateTime}_MA_{MaVersion}_debug_log.txt")) 27 | : null; 28 | 29 | private static StreamWriter debugLogStream = null; 30 | 31 | private static uint? portArg; 32 | public static readonly string[] Args = Environment.GetCommandLineArgs(); 33 | 34 | private static IExitSignal exitSignalListener; 35 | 36 | private static bool exited = false; 37 | private static readonly object ExitLock = new object(); 38 | 39 | #region Server Properties 40 | 41 | public static Server[] Servers => ServerDirectories 42 | .Select(serverDir => new Server(Path.GetFileName(serverDir), serverDir, portArg, Args)).ToArray(); 43 | 44 | public static string[] ServerDirectories 45 | { 46 | get 47 | { 48 | string globalServersFolder = MultiAdminConfig.GlobalConfig.ServersFolder.Value; 49 | return !Directory.Exists(globalServersFolder) 50 | ? new string[] { } 51 | : Directory.GetDirectories(globalServersFolder); 52 | } 53 | } 54 | 55 | public static string[] ServerIds => Servers.Select(server => server.serverId).ToArray(); 56 | 57 | #endregion 58 | 59 | #region Auto-Start Server Properties 60 | 61 | public static Server[] AutoStartServers => 62 | Servers.Where(server => !server.ServerConfig.ManualStart.Value).ToArray(); 63 | 64 | public static string[] AutoStartServerDirectories => 65 | AutoStartServers.Select(autoStartServer => autoStartServer.serverDir).ToArray(); 66 | 67 | public static string[] AutoStartServerIds => 68 | AutoStartServers.Select(autoStartServer => autoStartServer.serverId).ToArray(); 69 | 70 | #endregion 71 | 72 | public static bool Headless { get; private set; } 73 | 74 | #region Output Printing & Logging 75 | 76 | public static void Write(string message, ConsoleColor color = ConsoleColor.DarkYellow) 77 | { 78 | lock (ColoredConsole.WriteLock) 79 | { 80 | if (Headless) return; 81 | 82 | new ColoredMessage(Utils.TimeStampMessage(message), color).WriteLine(MultiAdminConfig.GlobalConfig?.ActualConsoleInputSystem == InputHandler.ConsoleInputSystem.New); 83 | } 84 | } 85 | 86 | private static bool IsDebugLogTagAllowed(string tag) 87 | { 88 | return (!MultiAdminConfig.GlobalConfig?.DebugLogBlacklist?.Value?.Contains(tag) ?? true) && 89 | ((MultiAdminConfig.GlobalConfig?.DebugLogWhitelist?.Value?.IsEmpty() ?? true) || 90 | MultiAdminConfig.GlobalConfig.DebugLogWhitelist.Value.Contains(tag)); 91 | } 92 | 93 | public static void LogDebugException(string tag, Exception exception) 94 | { 95 | lock (MaDebugLogFile) 96 | { 97 | if (tag == null || !IsDebugLogTagAllowed(tag)) return; 98 | 99 | LogDebug(tag, $"Error in \"{tag}\":{Environment.NewLine}{exception}"); 100 | } 101 | } 102 | 103 | public static void LogDebug(string tag, string message) 104 | { 105 | lock (MaDebugLogFile) 106 | { 107 | try 108 | { 109 | if ((!MultiAdminConfig.GlobalConfig?.DebugLog?.Value ?? true) || 110 | string.IsNullOrEmpty(MaDebugLogFile) || tag == null || !IsDebugLogTagAllowed(tag)) return; 111 | 112 | // Assign debug log stream as needed 113 | if (debugLogStream == null) 114 | { 115 | Directory.CreateDirectory(MaDebugLogDir); 116 | debugLogStream = File.AppendText(MaDebugLogFile); 117 | } 118 | 119 | message = Utils.TimeStampMessage($"[{tag}] {message}"); 120 | debugLogStream.Write(message); 121 | if (!message.EndsWith(Environment.NewLine)) debugLogStream.WriteLine(); 122 | 123 | debugLogStream.Flush(); 124 | } 125 | catch (Exception e) 126 | { 127 | new ColoredMessage[] 128 | { 129 | new ColoredMessage("Error while logging for MultiAdmin debug:", ConsoleColor.Red), 130 | new ColoredMessage(e.ToString(), ConsoleColor.Red) 131 | }.WriteLines(); 132 | } 133 | } 134 | } 135 | 136 | #endregion 137 | 138 | private static void OnExit(object sender, EventArgs e) 139 | { 140 | lock (ExitLock) 141 | { 142 | if (exited) 143 | return; 144 | 145 | if (MultiAdminConfig.GlobalConfig.SafeServerShutdown.Value) 146 | { 147 | Write("Stopping servers and exiting MultiAdmin...", ConsoleColor.DarkMagenta); 148 | 149 | foreach (Server server in InstantiatedServers) 150 | { 151 | if (!server.IsGameProcessRunning) 152 | continue; 153 | 154 | try 155 | { 156 | Write( 157 | string.IsNullOrEmpty(server.serverId) 158 | ? "Stopping the default server..." 159 | : $"Stopping server with ID \"{server.serverId}\"...", ConsoleColor.DarkMagenta); 160 | 161 | server.StopServer(); 162 | 163 | // Wait for server to exit 164 | int timeToWait = Math.Max(server.ServerConfig.SafeShutdownCheckDelay.Value, 0); 165 | int timeWaited = 0; 166 | 167 | while (server.IsGameProcessRunning) 168 | { 169 | Thread.Sleep(timeToWait); 170 | timeWaited += timeToWait; 171 | 172 | if (timeWaited >= server.ServerConfig.SafeShutdownTimeout.Value) 173 | { 174 | Write( 175 | string.IsNullOrEmpty(server.serverId) 176 | ? $"Failed to stop the default server within {timeWaited} ms, giving up..." 177 | : $"Failed to stop server with ID \"{server.serverId}\" within {timeWaited} ms, giving up...", 178 | ConsoleColor.Red); 179 | break; 180 | } 181 | } 182 | } 183 | catch (Exception ex) 184 | { 185 | LogDebugException(nameof(OnExit), ex); 186 | } 187 | } 188 | } 189 | 190 | debugLogStream?.Close(); 191 | debugLogStream = null; 192 | 193 | exited = true; 194 | } 195 | } 196 | 197 | public static void Main() 198 | { 199 | if (MultiAdminConfig.GlobalConfig.SafeServerShutdown.Value) 200 | { 201 | AppDomain.CurrentDomain.ProcessExit += OnExit; 202 | 203 | if (OperatingSystem.IsLinux()) 204 | { 205 | #if LINUX 206 | exitSignalListener = new UnixExitSignal(); 207 | #endif 208 | } 209 | else if (OperatingSystem.IsWindows()) 210 | { 211 | exitSignalListener = new WinExitSignal(); 212 | } 213 | 214 | if (exitSignalListener != null) 215 | exitSignalListener.Exit += OnExit; 216 | } 217 | 218 | // Remove executable path 219 | if (Args.Length > 0) 220 | Args[0] = null; 221 | 222 | Headless = GetFlagFromArgs(Args, "headless", "h"); 223 | 224 | string serverIdArg = GetParamFromArgs(Args, "server-id", "id"); 225 | string configArg = GetParamFromArgs(Args, "config", "c"); 226 | portArg = uint.TryParse(GetParamFromArgs(Args, "port", "p"), out uint port) ? (uint?)port : null; 227 | 228 | Server server = null; 229 | 230 | if (!string.IsNullOrEmpty(serverIdArg) || !string.IsNullOrEmpty(configArg)) 231 | { 232 | server = new Server(serverIdArg, configArg, portArg, Args); 233 | 234 | InstantiatedServers.Add(server); 235 | } 236 | else 237 | { 238 | if (Servers.IsEmpty()) 239 | { 240 | server = new Server(port: portArg, args: Args); 241 | 242 | InstantiatedServers.Add(server); 243 | } 244 | else 245 | { 246 | Server[] autoStartServers = AutoStartServers; 247 | 248 | if (autoStartServers.IsEmpty()) 249 | { 250 | Write("No servers are set to automatically start, please enter a Server ID to start:"); 251 | InputHandler.InputPrefix?.Write(); 252 | 253 | server = new Server(Console.ReadLine(), port: portArg, args: Args); 254 | 255 | InstantiatedServers.Add(server); 256 | } 257 | else 258 | { 259 | Write("Starting this instance in multi server mode..."); 260 | 261 | for (int i = 0; i < autoStartServers.Length; i++) 262 | { 263 | if (i == 0) 264 | { 265 | server = autoStartServers[i]; 266 | 267 | InstantiatedServers.Add(server); 268 | } 269 | else 270 | { 271 | StartServer(autoStartServers[i]); 272 | } 273 | } 274 | } 275 | } 276 | } 277 | 278 | if (server != null) 279 | { 280 | if (!string.IsNullOrEmpty(server.serverId) && !string.IsNullOrEmpty(server.configLocation)) 281 | Write( 282 | $"Starting this instance with Server ID: \"{server.serverId}\" and config directory: \"{server.configLocation}\"..."); 283 | 284 | else if (!string.IsNullOrEmpty(server.serverId)) 285 | Write($"Starting this instance with Server ID: \"{server.serverId}\"..."); 286 | 287 | else if (!string.IsNullOrEmpty(server.configLocation)) 288 | Write($"Starting this instance with config directory: \"{server.configLocation}\"..."); 289 | 290 | else 291 | Write("Starting this instance in single server mode..."); 292 | 293 | server.StartServer(); 294 | } 295 | } 296 | 297 | public static string GetParamFromArgs(string[] args, string[] keys = null, string[] aliases = null) 298 | { 299 | bool hasKeys = !keys.IsNullOrEmpty(); 300 | bool hasAliases = !aliases.IsNullOrEmpty(); 301 | 302 | if (!hasKeys && !hasAliases) return null; 303 | 304 | for (int i = 0; i < args.Length - 1; i++) 305 | { 306 | string lowArg = args[i]?.ToLower(); 307 | 308 | if (string.IsNullOrEmpty(lowArg)) continue; 309 | 310 | if (hasKeys) 311 | { 312 | if (keys.Any(key => lowArg == $"--{key?.ToLower()}")) 313 | { 314 | string value = args[i + 1]; 315 | 316 | args[i] = null; 317 | args[i + 1] = null; 318 | 319 | return value; 320 | } 321 | } 322 | 323 | if (hasAliases) 324 | { 325 | if (aliases.Any(alias => lowArg == $"-{alias?.ToLower()}")) 326 | { 327 | string value = args[i + 1]; 328 | 329 | args[i] = null; 330 | args[i + 1] = null; 331 | 332 | return value; 333 | } 334 | } 335 | } 336 | 337 | return null; 338 | } 339 | 340 | public static bool ArgsContainsParam(string[] args, string[] keys = null, string[] aliases = null) 341 | { 342 | bool hasKeys = !keys.IsNullOrEmpty(); 343 | bool hasAliases = !aliases.IsNullOrEmpty(); 344 | 345 | if (!hasKeys && !hasAliases) return false; 346 | 347 | for (int i = 0; i < args.Length; i++) 348 | { 349 | string lowArg = args[i]?.ToLower(); 350 | 351 | if (string.IsNullOrEmpty(lowArg)) continue; 352 | 353 | if (hasKeys) 354 | { 355 | if (keys.Any(key => lowArg == $"--{key?.ToLower()}")) 356 | { 357 | args[i] = null; 358 | return true; 359 | } 360 | } 361 | 362 | if (hasAliases) 363 | { 364 | if (aliases.Any(alias => lowArg == $"-{alias?.ToLower()}")) 365 | { 366 | args[i] = null; 367 | return true; 368 | } 369 | } 370 | } 371 | 372 | return false; 373 | } 374 | 375 | public static bool GetFlagFromArgs(string[] args, string[] keys = null, string[] aliases = null) 376 | { 377 | if (keys.IsNullOrEmpty() && aliases.IsNullOrEmpty()) return false; 378 | 379 | return bool.TryParse(GetParamFromArgs(args, keys, aliases), out bool result) 380 | ? result 381 | : ArgsContainsParam(args, keys, aliases); 382 | } 383 | 384 | public static string GetParamFromArgs(string[] args, string key = null, string alias = null) 385 | { 386 | return GetParamFromArgs(args, new string[] {key}, new string[] {alias}); 387 | } 388 | 389 | public static bool ArgsContainsParam(string[] args, string key = null, string alias = null) 390 | { 391 | return ArgsContainsParam(args, new string[] {key}, new string[] {alias}); 392 | } 393 | 394 | public static bool GetFlagFromArgs(string[] args, string key = null, string alias = null) 395 | { 396 | return GetFlagFromArgs(args, new string[] {key}, new string[] {alias}); 397 | } 398 | 399 | public static Process StartServer(Server server) 400 | { 401 | string assemblyLocation = Assembly.GetEntryAssembly()?.Location; 402 | 403 | if (string.IsNullOrEmpty(assemblyLocation)) 404 | { 405 | Write("Error while starting new server: Could not find the executable location!", ConsoleColor.Red); 406 | } 407 | 408 | List args = new List(server.args); 409 | 410 | if (!string.IsNullOrEmpty(server.serverId)) 411 | { 412 | args.Add("-id"); 413 | args.Add(server.serverId); 414 | } 415 | 416 | if (!string.IsNullOrEmpty(server.configLocation)) 417 | { 418 | args.Add("-c"); 419 | args.Add(server.configLocation); 420 | } 421 | 422 | if (Headless) 423 | args.Add("-h"); 424 | 425 | ProcessStartInfo startInfo = new ProcessStartInfo(assemblyLocation, args.JoinArgs()); 426 | 427 | Write($"Launching \"{startInfo.FileName}\" with arguments \"{startInfo.Arguments}\"..."); 428 | 429 | Process serverProcess = Process.Start(startInfo); 430 | 431 | InstantiatedServers.Add(server); 432 | 433 | return serverProcess; 434 | } 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /MultiAdmin/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using MultiAdmin; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle(nameof(MultiAdmin) + " v" + Program.MaVersion)] 8 | [assembly: AssemblyDescription("A program for running a SCP: Secret Laboratory server with additional functionality")] 9 | [assembly: AssemblyProduct(nameof(MultiAdmin))] 10 | [assembly: AssemblyCopyright("Copyright © Grover 2021")] 11 | 12 | // Version information for an assembly consists of the following four values: 13 | // 14 | // Major Version 15 | // Minor Version 16 | // Build Number 17 | // Revision 18 | // 19 | // You can specify all the values or you can default the Build and Revision Numbers 20 | // by using the '*' as shown below: 21 | // [assembly: AssemblyVersion("1.0.*")] 22 | [assembly: AssemblyVersion(Program.MaVersion)] 23 | -------------------------------------------------------------------------------- /MultiAdmin/ServerIO/InputHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using MultiAdmin.ConsoleTools; 8 | using MultiAdmin.Utility; 9 | 10 | namespace MultiAdmin.ServerIO 11 | { 12 | public static class InputHandler 13 | { 14 | private static readonly char[] Separator = {' '}; 15 | 16 | public static readonly ColoredMessage BaseSection = new ColoredMessage(null, ConsoleColor.White); 17 | 18 | public static readonly ColoredMessage InputPrefix = new ColoredMessage("> ", ConsoleColor.Yellow); 19 | public static readonly ColoredMessage LeftSideIndicator = new ColoredMessage("...", ConsoleColor.Yellow); 20 | public static readonly ColoredMessage RightSideIndicator = new ColoredMessage("...", ConsoleColor.Yellow); 21 | 22 | public static int InputPrefixLength => InputPrefix?.Length ?? 0; 23 | 24 | public static int LeftSideIndicatorLength => LeftSideIndicator?.Length ?? 0; 25 | public static int RightSideIndicatorLength => RightSideIndicator?.Length ?? 0; 26 | 27 | public static int TotalIndicatorLength => LeftSideIndicatorLength + RightSideIndicatorLength; 28 | 29 | public static int SectionBufferWidth 30 | { 31 | get 32 | { 33 | try 34 | { 35 | return Console.BufferWidth - (1 + InputPrefixLength); 36 | } 37 | catch (Exception e) 38 | { 39 | Program.LogDebugException(nameof(SectionBufferWidth), e); 40 | return 0; 41 | } 42 | } 43 | } 44 | 45 | public static string CurrentMessage { get; private set; } 46 | public static ColoredMessage[] CurrentInput { get; private set; } = {InputPrefix}; 47 | public static int CurrentCursor { get; private set; } 48 | 49 | public static async void Write(Server server, CancellationToken cancellationToken) 50 | { 51 | try 52 | { 53 | ShiftingList prevMessages = new ShiftingList(25); 54 | 55 | while (server.IsRunning && !server.IsStopping) 56 | { 57 | if (Program.Headless) 58 | { 59 | break; 60 | } 61 | 62 | string message; 63 | if (server.ServerConfig.ActualConsoleInputSystem == ConsoleInputSystem.New && SectionBufferWidth - TotalIndicatorLength > 0) 64 | { 65 | message = await GetInputLineNew(server, cancellationToken, prevMessages); 66 | } 67 | else if (server.ServerConfig.ActualConsoleInputSystem == ConsoleInputSystem.Old) 68 | { 69 | message = await GetInputLineOld(server, cancellationToken); 70 | } 71 | else 72 | { 73 | message = Console.ReadLine(); 74 | } 75 | 76 | if (string.IsNullOrEmpty(message)) continue; 77 | 78 | server.Write($">>> {message}", ConsoleColor.DarkMagenta); 79 | 80 | int separatorIndex = message.IndexOfAny(Separator); 81 | string commandName = (separatorIndex < 0 ? message : message.Substring(0, separatorIndex)).ToLower().Trim(); 82 | if (commandName.IsNullOrEmpty()) continue; 83 | 84 | bool callServer = true; 85 | server.commands.TryGetValue(commandName, out ICommand command); 86 | if (command != null) 87 | { 88 | try 89 | { 90 | // Use double quotation marks to escape a quotation mark 91 | command.OnCall(separatorIndex < 0 || separatorIndex + 1 >= message.Length ? Array.Empty() : CommandUtils.StringToArgs(message, separatorIndex + 1, escapeChar: '\"', quoteChar: '\"')); 92 | } 93 | catch (Exception e) 94 | { 95 | server.Write($"Error in command \"{commandName}\":{Environment.NewLine}{e}"); 96 | } 97 | 98 | callServer = command.PassToGame(); 99 | } 100 | 101 | if (callServer) server.SendMessage(message); 102 | } 103 | 104 | ResetInputParams(); 105 | } 106 | catch (TaskCanceledException) 107 | { 108 | // Exit the Task immediately if cancelled 109 | } 110 | } 111 | 112 | /// 113 | /// Waits until returns true. 114 | /// 115 | /// The cancellation token to check for cancellation. 116 | /// The task has been canceled. 117 | public static async Task WaitForKey(CancellationToken cancellationToken) 118 | { 119 | while (!Console.KeyAvailable) 120 | { 121 | await Task.Delay(10, cancellationToken); 122 | } 123 | } 124 | 125 | public static async Task GetInputLineOld(Server server, CancellationToken cancellationToken) 126 | { 127 | StringBuilder message = new StringBuilder(); 128 | while (true) 129 | { 130 | await WaitForKey(cancellationToken); 131 | 132 | ConsoleKeyInfo key = Console.ReadKey(server.ServerConfig.HideInput.Value); 133 | 134 | switch (key.Key) 135 | { 136 | case ConsoleKey.Backspace: 137 | if (!message.IsEmpty()) 138 | message.Remove(message.Length - 1, 1); 139 | break; 140 | 141 | case ConsoleKey.Enter: 142 | return message.ToString(); 143 | 144 | default: 145 | message.Append(key.KeyChar); 146 | break; 147 | } 148 | } 149 | } 150 | 151 | public static async Task GetInputLineNew(Server server, CancellationToken cancellationToken, ShiftingList prevMessages) 152 | { 153 | if (server.ServerConfig.RandomInputColors.Value) 154 | RandomizeInputColors(); 155 | 156 | string curMessage = ""; 157 | string message = ""; 158 | int messageCursor = 0; 159 | int prevMessageCursor = -1; 160 | StringSections curSections = null; 161 | int lastSectionIndex = -1; 162 | bool exitLoop = false; 163 | while (!exitLoop) 164 | { 165 | #region Key Press Handling 166 | 167 | await WaitForKey(cancellationToken); 168 | 169 | ConsoleKeyInfo key = Console.ReadKey(true); 170 | 171 | switch (key.Key) 172 | { 173 | case ConsoleKey.Backspace: 174 | if (messageCursor > 0 && !message.IsEmpty()) 175 | message = message.Remove(--messageCursor, 1); 176 | 177 | break; 178 | 179 | case ConsoleKey.Delete: 180 | if (messageCursor >= 0 && messageCursor < message.Length) 181 | message = message.Remove(messageCursor, 1); 182 | 183 | break; 184 | 185 | case ConsoleKey.Enter: 186 | exitLoop = true; 187 | break; 188 | 189 | case ConsoleKey.UpArrow: 190 | prevMessageCursor++; 191 | if (prevMessageCursor >= prevMessages.Count) 192 | prevMessageCursor = prevMessages.Count - 1; 193 | 194 | message = prevMessageCursor < 0 ? curMessage : prevMessages[prevMessageCursor]; 195 | 196 | break; 197 | 198 | case ConsoleKey.DownArrow: 199 | prevMessageCursor--; 200 | if (prevMessageCursor < -1) 201 | prevMessageCursor = -1; 202 | 203 | message = prevMessageCursor < 0 ? curMessage : prevMessages[prevMessageCursor]; 204 | 205 | break; 206 | 207 | case ConsoleKey.LeftArrow: 208 | messageCursor--; 209 | break; 210 | 211 | case ConsoleKey.RightArrow: 212 | messageCursor++; 213 | break; 214 | 215 | case ConsoleKey.Home: 216 | messageCursor = 0; 217 | break; 218 | 219 | case ConsoleKey.End: 220 | messageCursor = message.Length; 221 | break; 222 | 223 | case ConsoleKey.PageUp: 224 | messageCursor -= SectionBufferWidth - TotalIndicatorLength; 225 | break; 226 | 227 | case ConsoleKey.PageDown: 228 | messageCursor += SectionBufferWidth - TotalIndicatorLength; 229 | break; 230 | 231 | default: 232 | message = message.Insert(messageCursor++, key.KeyChar.ToString()); 233 | break; 234 | } 235 | 236 | #endregion 237 | 238 | if (prevMessageCursor < 0) 239 | curMessage = message; 240 | 241 | // If the input is done and should exit the loop, break from the while loop 242 | if (exitLoop) 243 | break; 244 | 245 | if (messageCursor < 0) 246 | messageCursor = 0; 247 | else if (messageCursor > message.Length) 248 | messageCursor = message.Length; 249 | 250 | #region Input Printing Management 251 | 252 | // If the message has changed, re-write it to the console 253 | if (CurrentMessage != message) 254 | { 255 | if (message.Length > SectionBufferWidth && SectionBufferWidth - TotalIndicatorLength > 0) 256 | { 257 | curSections = GetStringSections(message); 258 | 259 | StringSection? curSection = 260 | curSections.GetSection(IndexMinusOne(messageCursor), out int sectionIndex); 261 | 262 | if (curSection != null) 263 | { 264 | lastSectionIndex = sectionIndex; 265 | 266 | SetCurrentInput(curSection.Value.Section); 267 | CurrentCursor = curSection.Value.GetRelativeIndex(messageCursor); 268 | WriteInputAndSetCursor(true); 269 | } 270 | else 271 | { 272 | server.Write("Error while processing input string: curSection is null!", ConsoleColor.Red); 273 | } 274 | } 275 | else 276 | { 277 | curSections = null; 278 | 279 | SetCurrentInput(message); 280 | CurrentCursor = messageCursor; 281 | 282 | WriteInputAndSetCursor(true); 283 | } 284 | } 285 | else if (CurrentCursor != messageCursor) 286 | { 287 | try 288 | { 289 | // If the message length is longer than the buffer width (being cut into sections), re-write the message 290 | if (curSections != null) 291 | { 292 | StringSection? curSection = 293 | curSections.GetSection(IndexMinusOne(messageCursor), out int sectionIndex); 294 | 295 | if (curSection != null) 296 | { 297 | CurrentCursor = curSection.Value.GetRelativeIndex(messageCursor); 298 | 299 | // If the cursor index is in a different section from the last section, fully re-draw it 300 | if (lastSectionIndex != sectionIndex) 301 | { 302 | lastSectionIndex = sectionIndex; 303 | 304 | SetCurrentInput(curSection.Value.Section); 305 | 306 | WriteInputAndSetCursor(true); 307 | } 308 | 309 | // Otherwise, if only the relative cursor index has changed, set only the cursor 310 | else 311 | { 312 | SetCursor(); 313 | } 314 | } 315 | else 316 | { 317 | server.Write("Error while processing input string: curSection is null!", 318 | ConsoleColor.Red); 319 | } 320 | } 321 | else 322 | { 323 | CurrentCursor = messageCursor; 324 | SetCursor(); 325 | } 326 | } 327 | catch (Exception e) 328 | { 329 | Program.LogDebugException(nameof(Write), e); 330 | 331 | CurrentCursor = messageCursor; 332 | SetCursor(); 333 | } 334 | } 335 | 336 | CurrentMessage = message; 337 | 338 | #endregion 339 | } 340 | 341 | // Reset the current input parameters 342 | ResetInputParams(); 343 | 344 | if (!string.IsNullOrEmpty(message)) 345 | prevMessages.Add(message); 346 | 347 | return message; 348 | } 349 | 350 | public static void ResetInputParams() 351 | { 352 | CurrentMessage = null; 353 | SetCurrentInput(); 354 | CurrentCursor = 0; 355 | } 356 | 357 | public static void SetCurrentInput(params ColoredMessage[] coloredMessages) 358 | { 359 | List message = new List {InputPrefix}; 360 | 361 | if (coloredMessages != null) 362 | message.AddRange(coloredMessages); 363 | 364 | CurrentInput = message.ToArray(); 365 | } 366 | 367 | public static void SetCurrentInput(string message) 368 | { 369 | ColoredMessage baseSection = BaseSection?.Clone(); 370 | 371 | if (baseSection == null) 372 | baseSection = new ColoredMessage(message); 373 | else 374 | baseSection.text = message; 375 | 376 | SetCurrentInput(baseSection); 377 | } 378 | 379 | private static StringSections GetStringSections(string message) 380 | { 381 | return StringSections.FromString(message, SectionBufferWidth, LeftSideIndicator, RightSideIndicator, 382 | BaseSection); 383 | } 384 | 385 | private static int IndexMinusOne(int index) 386 | { 387 | // Get the current section that the cursor is in (-1 so that the text before the cursor is displayed at an indicator) 388 | return Math.Max(index - 1, 0); 389 | } 390 | 391 | #region Console Management Methods 392 | 393 | public static void SetCursor(int messageCursor) 394 | { 395 | lock (ColoredConsole.WriteLock) 396 | { 397 | if (Program.Headless) return; 398 | 399 | try 400 | { 401 | Console.CursorLeft = messageCursor + InputPrefixLength; 402 | } 403 | catch (Exception e) 404 | { 405 | Program.LogDebugException(nameof(SetCursor), e); 406 | } 407 | } 408 | } 409 | 410 | public static void SetCursor() 411 | { 412 | SetCursor(CurrentCursor); 413 | } 414 | 415 | public static void WriteInput(ColoredMessage[] message, bool clearConsoleLine = false) 416 | { 417 | lock (ColoredConsole.WriteLock) 418 | { 419 | if (Program.Headless) return; 420 | 421 | message?.Write(clearConsoleLine); 422 | 423 | CurrentInput = message; 424 | } 425 | } 426 | 427 | public static void WriteInput(bool clearConsoleLine = false) 428 | { 429 | WriteInput(CurrentInput, clearConsoleLine); 430 | } 431 | 432 | public static void WriteInputAndSetCursor(bool clearConsoleLine = false) 433 | { 434 | lock (ColoredConsole.WriteLock) 435 | { 436 | WriteInput(clearConsoleLine); 437 | SetCursor(); 438 | } 439 | } 440 | 441 | #endregion 442 | 443 | public static void RandomizeInputColors() 444 | { 445 | try 446 | { 447 | Random random = new Random(); 448 | Array colors = Enum.GetValues(typeof(ConsoleColor)); 449 | 450 | ConsoleColor random1 = (ConsoleColor)colors.GetValue(random.Next(colors.Length)); 451 | ConsoleColor random2 = (ConsoleColor)colors.GetValue(random.Next(colors.Length)); 452 | 453 | BaseSection.textColor = random1; 454 | 455 | InputPrefix.textColor = random2; 456 | LeftSideIndicator.textColor = random2; 457 | RightSideIndicator.textColor = random2; 458 | } 459 | catch (Exception e) 460 | { 461 | Program.LogDebugException(nameof(RandomizeInputColors), e); 462 | } 463 | } 464 | 465 | public enum ConsoleInputSystem 466 | { 467 | // Represents the default input system, which calls Console.ReadLine and blocks the calling context 468 | Original, 469 | // Represents the "old" input system, which calls non-blocking methods 470 | Old, 471 | // Represents the "new" input system, which also calls non-blocking methods, 472 | // but the main difference is great display 473 | New, 474 | } 475 | } 476 | } 477 | -------------------------------------------------------------------------------- /MultiAdmin/ServerIO/OutputHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using MultiAdmin.ConsoleTools; 4 | using MultiAdmin.Utility; 5 | 6 | namespace MultiAdmin.ServerIO 7 | { 8 | public class OutputHandler 9 | { 10 | public static readonly Regex SmodRegex = 11 | new Regex(@"\[(DEBUG|INFO|WARN|ERROR)\] (\[.*?\]) (.*)", RegexOptions.Compiled | RegexOptions.Singleline); 12 | public static readonly char[] TrimChars = { '.', ' ', '\t', '!', '?', ',' }; 13 | public static readonly char[] EventSplitChars = new char[] {':'}; 14 | 15 | private readonly Server server; 16 | 17 | private enum OutputCodes : byte 18 | { 19 | //0x00 - 0x0F - reserved for colors 20 | 21 | RoundRestart = 0x10, 22 | IdleEnter = 0x11, 23 | IdleExit = 0x12, 24 | ExitActionReset = 0x13, 25 | ExitActionShutdown = 0x14, 26 | ExitActionSilentShutdown = 0x15, 27 | ExitActionRestart = 0x16, 28 | RoundEnd = 0x17 29 | } 30 | 31 | // Temporary measure to handle round ends until the game updates to use this 32 | private bool roundEndCodeUsed = false; 33 | 34 | public OutputHandler(Server server) 35 | { 36 | this.server = server; 37 | } 38 | 39 | public void HandleMessage(object source, ServerSocket.MessageEventArgs message) 40 | { 41 | if (message.message == null) 42 | return; 43 | 44 | ColoredMessage coloredMessage = new ColoredMessage(message.message, ConsoleColor.White); 45 | 46 | if (!coloredMessage.text.IsEmpty()) 47 | { 48 | // Parse the color byte 49 | coloredMessage.textColor = (ConsoleColor)message.color; 50 | 51 | // Smod2 loggers pretty printing 52 | Match match = SmodRegex.Match(coloredMessage.text); 53 | if (match.Success) 54 | { 55 | if (match.Groups.Count >= 3) 56 | { 57 | ConsoleColor levelColor = ConsoleColor.Green; 58 | ConsoleColor tagColor = ConsoleColor.Yellow; 59 | ConsoleColor msgColor = coloredMessage.textColor ?? ConsoleColor.White; 60 | 61 | switch (match.Groups[1].Value.Trim()) 62 | { 63 | case "DEBUG": 64 | levelColor = ConsoleColor.DarkGray; 65 | break; 66 | 67 | case "INFO": 68 | levelColor = ConsoleColor.Green; 69 | break; 70 | 71 | case "WARN": 72 | levelColor = ConsoleColor.DarkYellow; 73 | break; 74 | 75 | case "ERROR": 76 | levelColor = ConsoleColor.Red; 77 | break; 78 | } 79 | 80 | server.Write( 81 | new[] 82 | { 83 | new ColoredMessage($"[{match.Groups[1].Value}] ", levelColor), 84 | new ColoredMessage($"{match.Groups[2].Value} ", tagColor), 85 | new ColoredMessage(match.Groups[3].Value, msgColor) 86 | }, msgColor); 87 | 88 | // P.S. the format is [Info] [courtney.exampleplugin] Something interesting happened 89 | // That was just an example 90 | 91 | // This return should be here 92 | return; 93 | } 94 | } 95 | 96 | string lowerMessage = coloredMessage.text.ToLower(); 97 | if (!server.supportedModFeatures.HasFlag(ModFeatures.CustomEvents)) 98 | { 99 | switch (lowerMessage.Trim(TrimChars)) 100 | { 101 | case "the round is about to restart! please wait": 102 | if (!roundEndCodeUsed) 103 | server.ForEachHandler(roundEnd => roundEnd.OnRoundEnd()); 104 | break; 105 | 106 | /* Replaced by OutputCodes.RoundRestart 107 | case "waiting for players": 108 | server.IsLoading = false; 109 | server.ForEachHandler(waitingForPlayers => waitingForPlayers.OnWaitingForPlayers()); 110 | break; 111 | */ 112 | 113 | case "new round has been started": 114 | server.ForEachHandler(roundStart => roundStart.OnRoundStart()); 115 | break; 116 | 117 | case "level loaded. creating match": 118 | server.ForEachHandler(serverStart => serverStart.OnServerStart()); 119 | break; 120 | 121 | case "server full": 122 | server.ForEachHandler(serverFull => serverFull.OnServerFull()); 123 | break; 124 | } 125 | } 126 | 127 | if (lowerMessage.StartsWith("multiadmin:")) 128 | { 129 | // 11 chars in "multiadmin:" 130 | string eventMessage = coloredMessage.text.Substring(11); 131 | 132 | // Split event and event data 133 | string[] eventSplit = eventMessage.Split(EventSplitChars, 2); 134 | 135 | string @event = eventSplit[0].ToLower(); 136 | string eventData = eventSplit.Length > 1 ? eventSplit[1] : null; // Handle events with no data 137 | 138 | switch (@event) 139 | { 140 | case "round-end-event": 141 | if (!roundEndCodeUsed) 142 | server.ForEachHandler(roundEnd => roundEnd.OnRoundEnd()); 143 | break; 144 | 145 | /* Replaced by OutputCodes.RoundRestart 146 | case "waiting-for-players-event": 147 | server.IsLoading = false; 148 | server.ForEachHandler(waitingForPlayers => waitingForPlayers.OnWaitingForPlayers()); 149 | break; 150 | */ 151 | 152 | case "round-start-event": 153 | server.ForEachHandler(roundStart => roundStart.OnRoundStart()); 154 | break; 155 | 156 | case "server-start-event": 157 | server.ForEachHandler(serverStart => serverStart.OnServerStart()); 158 | break; 159 | 160 | case "server-full-event": 161 | server.ForEachHandler(serverFull => serverFull.OnServerFull()); 162 | break; 163 | 164 | case "set-supported-features": 165 | if (int.TryParse(eventData, out int supportedFeatures)) 166 | { 167 | server.supportedModFeatures = (ModFeatures)supportedFeatures; 168 | } 169 | break; 170 | } 171 | 172 | // Don't print any MultiAdmin events 173 | return; 174 | } 175 | } 176 | 177 | server.Write(coloredMessage); 178 | } 179 | 180 | public void HandleAction(object source, byte action) 181 | { 182 | switch ((OutputCodes)action) 183 | { 184 | // This seems to show up at the waiting for players event 185 | case OutputCodes.RoundRestart: 186 | server.IsLoading = false; 187 | server.ForEachHandler(waitingForPlayers => waitingForPlayers.OnWaitingForPlayers()); 188 | break; 189 | 190 | case OutputCodes.IdleEnter: 191 | server.ForEachHandler(idleEnter => idleEnter.OnIdleEnter()); 192 | break; 193 | 194 | case OutputCodes.IdleExit: 195 | server.ForEachHandler(idleExit => idleExit.OnIdleExit()); 196 | break; 197 | 198 | // Requests to reset the ExitAction status 199 | case OutputCodes.ExitActionReset: 200 | server.SetServerRequestedStatus(ServerStatus.Running); 201 | break; 202 | 203 | // Requests the Shutdown ExitAction with the intent to restart at any time in the future 204 | case OutputCodes.ExitActionShutdown: 205 | server.SetServerRequestedStatus(ServerStatus.ExitActionStop); 206 | break; 207 | 208 | // Requests the SilentShutdown ExitAction with the intent to restart at any time in the future 209 | case OutputCodes.ExitActionSilentShutdown: 210 | server.SetServerRequestedStatus(ServerStatus.ExitActionStop); 211 | break; 212 | 213 | // Requests the Restart ExitAction status with the intent to restart at any time in the future 214 | case OutputCodes.ExitActionRestart: 215 | server.SetServerRequestedStatus(ServerStatus.ExitActionRestart); 216 | break; 217 | 218 | case OutputCodes.RoundEnd: 219 | roundEndCodeUsed = true; 220 | server.ForEachHandler(roundEnd => roundEnd.OnRoundEnd()); 221 | break; 222 | 223 | default: 224 | Program.LogDebug(nameof(HandleAction), $"Received unknown output code ({action}), is MultiAdmin up to date? This error can probably be safely ignored."); 225 | break; 226 | } 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /MultiAdmin/ServerIO/ServerSocket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Sockets; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace MultiAdmin.ServerIO 9 | { 10 | public class ServerSocket : IDisposable 11 | { 12 | private const int IntBytes = sizeof(int); 13 | public static readonly UTF8Encoding Encoding = new UTF8Encoding(false, true); 14 | 15 | private readonly CancellationTokenSource disposeCancellationSource = new CancellationTokenSource(); 16 | private bool disposed = false; 17 | 18 | private readonly TcpListener listener; 19 | 20 | private TcpClient client; 21 | private NetworkStream networkStream; 22 | 23 | public struct MessageEventArgs 24 | { 25 | public MessageEventArgs(string message, byte color) 26 | { 27 | this.message = message; 28 | this.color = color; 29 | } 30 | 31 | public readonly string message; 32 | public readonly byte color; 33 | } 34 | 35 | public event EventHandler OnReceiveMessage; 36 | public event EventHandler OnReceiveAction; 37 | 38 | public int Port => ((IPEndPoint)listener.LocalEndpoint).Port; 39 | 40 | public bool Connected => client?.Connected ?? false; 41 | 42 | // Port 0 automatically assigns a port 43 | public ServerSocket(int port = 0) 44 | { 45 | listener = new TcpListener(new IPEndPoint(IPAddress.Loopback, port)); 46 | } 47 | 48 | public void Connect() 49 | { 50 | if (disposed) 51 | throw new ObjectDisposedException(nameof(ServerSocket)); 52 | 53 | listener.Start(); 54 | listener.BeginAcceptTcpClient(result => 55 | { 56 | try 57 | { 58 | client = listener.EndAcceptTcpClient(result); 59 | networkStream = client.GetStream(); 60 | 61 | Task.Run(MessageListener, disposeCancellationSource.Token); 62 | } 63 | catch (ObjectDisposedException) 64 | { 65 | // IGNORE 66 | } 67 | catch (Exception e) 68 | { 69 | Program.LogDebugException(nameof(Connect), e); 70 | } 71 | }, listener); 72 | } 73 | 74 | public async void MessageListener() 75 | { 76 | byte[] typeBuffer = new byte[1]; 77 | byte[] intBuffer = new byte[IntBytes]; 78 | while (!disposed && networkStream != null) 79 | { 80 | try 81 | { 82 | int messageTypeBytesRead = 83 | await networkStream.ReadAsync(typeBuffer, 0, 1, disposeCancellationSource.Token); 84 | 85 | // Socket has been disconnected 86 | if (messageTypeBytesRead <= 0) 87 | { 88 | Disconnect(); 89 | break; 90 | } 91 | 92 | byte messageType = typeBuffer[0]; 93 | 94 | // 16 colors reserved, otherwise process as control message (action) 95 | if (messageType >= 16) 96 | { 97 | OnReceiveAction?.Invoke(this, messageType); 98 | continue; 99 | } 100 | 101 | int lengthBytesRead = 102 | await networkStream.ReadAsync(intBuffer, 0, IntBytes, disposeCancellationSource.Token); 103 | 104 | // Socket has been disconnected or integer read is invalid 105 | if (lengthBytesRead != IntBytes) 106 | { 107 | Disconnect(); 108 | break; 109 | } 110 | 111 | // Decode integer 112 | int length = (intBuffer[0] << 24) | (intBuffer[1] << 16) | (intBuffer[2] << 8) | intBuffer[3]; 113 | 114 | // Handle empty messages asap 115 | if (length == 0) 116 | { 117 | OnReceiveMessage?.Invoke(this, new MessageEventArgs("", messageType)); 118 | } 119 | else if (length < 0) 120 | { 121 | OnReceiveMessage?.Invoke(this, new MessageEventArgs(null, messageType)); 122 | } 123 | 124 | byte[] messageBuffer = new byte[length]; 125 | int messageBytesRead = 126 | await networkStream.ReadAsync(messageBuffer, 0, length, disposeCancellationSource.Token); 127 | 128 | // Socket has been disconnected 129 | if (messageBytesRead <= 0) 130 | { 131 | Disconnect(); 132 | break; 133 | } 134 | 135 | string message = Encoding.GetString(messageBuffer, 0, length); 136 | 137 | OnReceiveMessage?.Invoke(this, new MessageEventArgs(message, messageType)); 138 | } 139 | catch (Exception e) 140 | { 141 | Program.LogDebugException(nameof(MessageListener), e); 142 | } 143 | } 144 | } 145 | 146 | public void SendMessage(string message) 147 | { 148 | if (disposed) 149 | throw new ObjectDisposedException(nameof(ServerSocket)); 150 | 151 | if (networkStream == null) 152 | throw new NullReferenceException($"{nameof(networkStream)} hasn't been initialized"); 153 | 154 | byte[] messageBuffer = new byte[Encoding.GetMaxByteCount(message.Length) + IntBytes]; 155 | 156 | int actualMessageLength = Encoding.GetBytes(message, 0, message.Length, messageBuffer, IntBytes); 157 | Array.Copy(BitConverter.GetBytes(actualMessageLength), messageBuffer, IntBytes); 158 | 159 | try 160 | { 161 | networkStream.Write(messageBuffer, 0, actualMessageLength + IntBytes); 162 | } 163 | catch (Exception e) 164 | { 165 | Program.LogDebugException(nameof(SendMessage), e); 166 | } 167 | } 168 | 169 | public void Disconnect() 170 | { 171 | Dispose(); 172 | } 173 | 174 | public void Dispose() 175 | { 176 | if (disposed) 177 | return; 178 | 179 | disposed = true; 180 | disposeCancellationSource.Cancel(); 181 | disposeCancellationSource.Dispose(); 182 | 183 | networkStream?.Close(); 184 | client?.Close(); 185 | listener.Stop(); 186 | 187 | OnReceiveMessage = null; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /MultiAdmin/ServerIO/ShiftingList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace MultiAdmin.ServerIO 6 | { 7 | public class ShiftingList : ReadOnlyCollection 8 | { 9 | public int MaxCount { get; } 10 | 11 | public ShiftingList(int maxCount) : base(new List(maxCount)) 12 | { 13 | if (maxCount <= 0) 14 | throw new ArgumentException("The maximum index count can not be less than or equal to zero."); 15 | 16 | MaxCount = maxCount; 17 | } 18 | 19 | private void LimitLength() 20 | { 21 | while (Items.Count > MaxCount) 22 | { 23 | RemoveFromEnd(); 24 | } 25 | } 26 | 27 | public void Add(string item, int index = 0) 28 | { 29 | lock (Items) 30 | { 31 | Items.Insert(index, item); 32 | 33 | LimitLength(); 34 | } 35 | } 36 | 37 | public void Remove(string item) 38 | { 39 | lock (Items) 40 | { 41 | Items.Remove(item); 42 | } 43 | } 44 | 45 | public void RemoveFromEnd() 46 | { 47 | lock (Items) 48 | { 49 | Items.RemoveAt(Items.Count - 1); 50 | } 51 | } 52 | 53 | public void RemoveAt(int index) 54 | { 55 | lock (Items) 56 | { 57 | Items.RemoveAt(index); 58 | } 59 | } 60 | 61 | public void Replace(string item, int index = 0) 62 | { 63 | lock (Items) 64 | { 65 | RemoveAt(index); 66 | Add(item, index); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /MultiAdmin/ServerIO/StringSections.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using MultiAdmin.ConsoleTools; 5 | using MultiAdmin.Utility; 6 | 7 | namespace MultiAdmin.ServerIO 8 | { 9 | public class StringSections 10 | { 11 | public StringSection[] Sections { get; } 12 | 13 | public StringSections(StringSection[] sections) 14 | { 15 | Sections = sections; 16 | } 17 | 18 | public StringSection? GetSection(int index, out int sectionIndex) 19 | { 20 | sectionIndex = -1; 21 | 22 | for (int i = 0; i < Sections.Length; i++) 23 | { 24 | StringSection stringSection = Sections[i]; 25 | 26 | if (stringSection.IsWithinSection(index)) 27 | { 28 | sectionIndex = i; 29 | return stringSection; 30 | } 31 | } 32 | 33 | return null; 34 | } 35 | 36 | public StringSection? GetSection(int index) 37 | { 38 | foreach (StringSection stringSection in Sections) 39 | { 40 | if (stringSection.IsWithinSection(index)) 41 | return stringSection; 42 | } 43 | 44 | return null; 45 | } 46 | 47 | public static StringSections FromString(string fullString, int sectionLength, 48 | ColoredMessage leftIndicator = null, ColoredMessage rightIndicator = null, 49 | ColoredMessage sectionBase = null) 50 | { 51 | int rightIndicatorLength = rightIndicator?.Length ?? 0; 52 | int totalIndicatorLength = (leftIndicator?.Length ?? 0) + rightIndicatorLength; 53 | 54 | if (fullString.Length > sectionLength && sectionLength <= totalIndicatorLength) 55 | throw new ArgumentException( 56 | $"{nameof(sectionLength)} must be greater than the total length of {nameof(leftIndicator)} and {nameof(rightIndicator)}", 57 | nameof(sectionLength)); 58 | 59 | List sections = new List(); 60 | 61 | if (string.IsNullOrEmpty(fullString)) 62 | return new StringSections(sections.ToArray()); 63 | 64 | // If the section base message is null, create a default one 65 | if (sectionBase == null) 66 | sectionBase = new ColoredMessage(null); 67 | 68 | // The starting index of the current section being created 69 | int sectionStartIndex = 0; 70 | 71 | // The text of the current section being created 72 | StringBuilder curSecBuilder = new StringBuilder(); 73 | 74 | for (int i = 0; i < fullString.Length; i++) 75 | { 76 | curSecBuilder.Append(fullString[i]); 77 | 78 | // If the section is less than the smallest possible section size, skip processing 79 | if (curSecBuilder.Length < sectionLength - totalIndicatorLength) continue; 80 | 81 | // Decide what the left indicator text should be accounting for the leftmost section 82 | ColoredMessage leftIndicatorSection = sections.Count > 0 ? leftIndicator : null; 83 | // Decide what the right indicator text should be accounting for the rightmost section 84 | ColoredMessage rightIndicatorSection = 85 | i < fullString.Length - (1 + rightIndicatorLength) ? rightIndicator : null; 86 | 87 | // Check the section length against the final section length 88 | if (curSecBuilder.Length >= sectionLength - 89 | ((leftIndicatorSection?.Length ?? 0) + (rightIndicatorSection?.Length ?? 0))) 90 | { 91 | // Copy the section base message and replace the text 92 | ColoredMessage section = sectionBase.Clone(); 93 | section.text = curSecBuilder.ToString(); 94 | 95 | // Instantiate the section with the final parameters 96 | sections.Add(new StringSection(section, leftIndicatorSection, rightIndicatorSection, 97 | sectionStartIndex, i)); 98 | 99 | // Reset the current section being worked on 100 | curSecBuilder.Clear(); 101 | sectionStartIndex = i + 1; 102 | } 103 | } 104 | 105 | // If there's still text remaining in a section that hasn't been processed, add it as a section 106 | if (!curSecBuilder.IsEmpty()) 107 | { 108 | // Only decide for the left indicator, as this last section will always be the rightmost section 109 | ColoredMessage leftIndicatorSection = sections.Count > 0 ? leftIndicator : null; 110 | 111 | // Copy the section base message and replace the text 112 | ColoredMessage section = sectionBase.Clone(); 113 | section.text = curSecBuilder.ToString(); 114 | 115 | // Instantiate the section with the final parameters 116 | sections.Add(new StringSection(section, leftIndicatorSection, null, sectionStartIndex, 117 | fullString.Length)); 118 | } 119 | 120 | return new StringSections(sections.ToArray()); 121 | } 122 | } 123 | 124 | public struct StringSection 125 | { 126 | public ColoredMessage Text { get; } 127 | 128 | public ColoredMessage LeftIndicator { get; } 129 | public ColoredMessage RightIndicator { get; } 130 | 131 | public ColoredMessage[] Section => new ColoredMessage[] {LeftIndicator, Text, RightIndicator}; 132 | 133 | public int MinIndex { get; } 134 | public int MaxIndex { get; } 135 | 136 | public StringSection(ColoredMessage text, ColoredMessage leftIndicator, ColoredMessage rightIndicator, 137 | int minIndex, int maxIndex) 138 | { 139 | Text = text; 140 | 141 | LeftIndicator = leftIndicator; 142 | RightIndicator = rightIndicator; 143 | 144 | MinIndex = minIndex; 145 | MaxIndex = maxIndex; 146 | } 147 | 148 | public bool IsWithinSection(int index) 149 | { 150 | return index >= MinIndex && index <= MaxIndex; 151 | } 152 | 153 | public int GetRelativeIndex(int index) 154 | { 155 | return index - MinIndex + (LeftIndicator?.Length ?? 0); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /MultiAdmin/Utility/CommandUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace MultiAdmin.Utility 6 | { 7 | public static class CommandUtils 8 | { 9 | public static int IndexOfNonEscaped(string inString, char inChar, int startIndex, int count, char escapeChar = '\\') 10 | { 11 | if (inString == null) 12 | { 13 | throw new NullReferenceException(); 14 | } 15 | 16 | if (startIndex < 0 || startIndex >= inString.Length) 17 | { 18 | throw new ArgumentOutOfRangeException(nameof(startIndex)); 19 | } 20 | 21 | if (count < 0 || startIndex + count > inString.Length) 22 | { 23 | throw new ArgumentOutOfRangeException(nameof(count)); 24 | } 25 | 26 | bool escaped = false; 27 | for (int i = 0; i < count; i++) 28 | { 29 | int stringIndex = startIndex + i; 30 | char stringChar = inString[stringIndex]; 31 | 32 | if (!escaped) 33 | { 34 | if (stringChar == escapeChar && (escapeChar != inChar || ((i + 1) < count && inString[startIndex + i + 1] == escapeChar))) 35 | { 36 | escaped = true; 37 | continue; 38 | } 39 | } 40 | 41 | // If the character isn't escaped or the character that's escaped is an escape character then check if it matches 42 | if ((!escaped || (stringChar == escapeChar && escapeChar != inChar)) && stringChar == inChar) 43 | { 44 | return stringIndex; 45 | } 46 | 47 | escaped = false; 48 | } 49 | 50 | return -1; 51 | } 52 | 53 | public static int IndexOfNonEscaped(string inString, char inChar, int startIndex, char escapeChar = '\\') 54 | { 55 | return IndexOfNonEscaped(inString, inChar, startIndex, inString.Length - startIndex, escapeChar); 56 | } 57 | 58 | public static int IndexOfNonEscaped(string inString, char inChar, char escapeChar = '\\') 59 | { 60 | return IndexOfNonEscaped(inString, inChar, 0, inString.Length, escapeChar); 61 | } 62 | 63 | public static string[] StringToArgs(string inString, int startIndex, int count, char separator = ' ', char escapeChar = '\\', char quoteChar = '\"', bool keepQuotes = false) 64 | { 65 | if (inString == null) 66 | { 67 | return null; 68 | } 69 | 70 | if (startIndex < 0 || startIndex >= inString.Length) 71 | { 72 | throw new ArgumentOutOfRangeException(nameof(startIndex)); 73 | } 74 | 75 | if (count < 0 || startIndex + count > inString.Length) 76 | { 77 | throw new ArgumentOutOfRangeException(nameof(count)); 78 | } 79 | 80 | if (inString.IsEmpty()) 81 | return Array.Empty(); 82 | 83 | List args = new List(); 84 | StringBuilder strBuilder = new StringBuilder(); 85 | bool inQuotes = false; 86 | bool escaped = false; 87 | 88 | for (int i = 0; i < count; i++) 89 | { 90 | char stringChar = inString[startIndex + i]; 91 | 92 | if (!escaped) 93 | { 94 | if (stringChar == escapeChar && (escapeChar != quoteChar || ((i + 1) < count && inString[startIndex + i + 1] == escapeChar))) 95 | { 96 | escaped = true; 97 | continue; 98 | } 99 | 100 | if (stringChar == quoteChar && (inQuotes || ((i + 1) < count && IndexOfNonEscaped(inString, quoteChar, startIndex + (i + 1), count - (i + 1), escapeChar) > 0))) 101 | { 102 | // Ignore quotes if there's no future non-escaped quotes 103 | 104 | inQuotes = !inQuotes; 105 | if (!keepQuotes) 106 | continue; 107 | } 108 | else if (!inQuotes && stringChar == separator) 109 | { 110 | args.Add(strBuilder.ToString()); 111 | strBuilder.Clear(); 112 | continue; 113 | } 114 | } 115 | 116 | strBuilder.Append(stringChar); 117 | escaped = false; 118 | } 119 | 120 | args.Add(strBuilder.ToString()); 121 | 122 | return args.ToArray(); 123 | } 124 | 125 | public static string[] StringToArgs(string inString, int startIndex, char separator = ' ', char escapeChar = '\\', char quoteChar = '\"', bool keepQuotes = false) 126 | { 127 | return StringToArgs(inString, startIndex, inString.Length - startIndex, separator, escapeChar, quoteChar, keepQuotes); 128 | } 129 | 130 | public static string[] StringToArgs(string inString, char separator = ' ', char escapeChar = '\\', char quoteChar = '\"', bool keepQuotes = false) 131 | { 132 | return StringToArgs(inString, 0, inString.Length, separator, escapeChar, quoteChar, keepQuotes); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /MultiAdmin/Utility/EmptyExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace MultiAdmin.Utility 7 | { 8 | public static class EmptyExtensions 9 | { 10 | public static bool IsEmpty(this IEnumerable enumerable) 11 | { 12 | return !enumerable.Any(); 13 | } 14 | 15 | public static bool IsNullOrEmpty(this IEnumerable enumerable) 16 | { 17 | return enumerable?.IsEmpty() ?? true; 18 | } 19 | 20 | public static bool IsEmpty(this Array array) 21 | { 22 | return array.Length <= 0; 23 | } 24 | 25 | public static bool IsNullOrEmpty(this Array array) 26 | { 27 | return array?.IsEmpty() ?? true; 28 | } 29 | 30 | public static bool IsEmpty(this T[] array) 31 | { 32 | return array.Length <= 0; 33 | } 34 | 35 | public static bool IsNullOrEmpty(this T[] array) 36 | { 37 | return array?.IsEmpty() ?? true; 38 | } 39 | 40 | public static bool IsEmpty(this ICollection collection) 41 | { 42 | return collection.Count <= 0; 43 | } 44 | 45 | public static bool IsNullOrEmpty(this ICollection collection) 46 | { 47 | return collection?.IsEmpty() ?? true; 48 | } 49 | 50 | public static bool IsEmpty(this List list) 51 | { 52 | return list.Count <= 0; 53 | } 54 | 55 | public static bool IsNullOrEmpty(this List list) 56 | { 57 | return list?.IsEmpty() ?? true; 58 | } 59 | 60 | public static bool IsEmpty(this Dictionary dictionary) 61 | { 62 | return dictionary.Count <= 0; 63 | } 64 | 65 | public static bool IsNullOrEmpty(this Dictionary dictionary) 66 | { 67 | return dictionary?.IsEmpty() ?? true; 68 | } 69 | 70 | public static bool IsEmpty(this StringBuilder stringBuilder) 71 | { 72 | return stringBuilder.Length <= 0; 73 | } 74 | 75 | public static bool IsNullOrEmpty(this StringBuilder stringBuilder) 76 | { 77 | return stringBuilder?.IsEmpty() ?? true; 78 | } 79 | 80 | public static bool IsEmpty(this string @string) 81 | { 82 | return @string.Length <= 0; 83 | } 84 | 85 | public static bool IsNullOrEmpty(this string @string) 86 | { 87 | return @string?.IsEmpty() ?? true; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /MultiAdmin/Utility/StringEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace MultiAdmin.Utility 6 | { 7 | public static class StringEnumerableExtensions 8 | { 9 | public static string JoinArgs(this IEnumerable args) 10 | { 11 | StringBuilder argsStringBuilder = new StringBuilder(); 12 | foreach (string arg in args) 13 | { 14 | if (arg.IsNullOrEmpty()) 15 | continue; 16 | 17 | // Escape escape characters (if not on Windows) and quotation marks 18 | string escapedArg = OperatingSystem.IsWindows() ? arg.Replace("\"", "\\\"") : arg.Replace("\\", "\\\\").Replace("\"", "\\\""); 19 | 20 | // Separate with spaces 21 | if (!argsStringBuilder.IsEmpty()) 22 | argsStringBuilder.Append(' '); 23 | 24 | // Handle spaces by surrounding with quotes 25 | if (escapedArg.Contains(' ')) 26 | { 27 | argsStringBuilder.Append('"'); 28 | argsStringBuilder.Append(escapedArg); 29 | argsStringBuilder.Append('"'); 30 | } 31 | else 32 | { 33 | argsStringBuilder.Append(escapedArg); 34 | } 35 | } 36 | 37 | return argsStringBuilder.ToString(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MultiAdmin/Utility/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MultiAdmin.Utility 4 | { 5 | public static class StringExtensions 6 | { 7 | public static bool Equals(this string input, string value, int startIndex, int count) 8 | { 9 | if (input == null && value == null) 10 | return true; 11 | if (input == null || value == null) 12 | return false; 13 | 14 | if (startIndex < 0 || startIndex >= input.Length) 15 | throw new ArgumentOutOfRangeException(nameof(startIndex)); 16 | if (count < 0 || count > value.Length || startIndex > input.Length - count) 17 | throw new ArgumentOutOfRangeException(nameof(count)); 18 | 19 | for (int i = 0; i < count; i++) 20 | { 21 | if (input[startIndex + i] != value[i]) 22 | return false; 23 | } 24 | 25 | return true; 26 | } 27 | 28 | public static bool Equals(this string input, string value, int startIndex) 29 | { 30 | if (input == null && value == null) 31 | return true; 32 | if (input == null || value == null) 33 | return false; 34 | 35 | int length = input.Length - startIndex; 36 | 37 | if (length < value.Length) 38 | throw new ArgumentOutOfRangeException(nameof(value)); 39 | 40 | return Equals(input, value, startIndex, length); 41 | } 42 | 43 | /// 44 | /// Escapes this for use with 45 | /// 46 | /// The to escape 47 | /// A escaped for use with 48 | public static string EscapeFormat(this string input) 49 | { 50 | return input?.Replace("{", "{{").Replace("}", "}}"); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MultiAdmin/Utility/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using MultiAdmin.ConsoleTools; 5 | 6 | namespace MultiAdmin.Utility 7 | { 8 | public static class Utils 9 | { 10 | public static string DateTime => System.DateTime.Now.ToString("yyyy-MM-dd_HH_mm_ss"); 11 | 12 | public static string TimeStamp 13 | { 14 | get 15 | { 16 | DateTime now = System.DateTime.Now; 17 | return $"[{now.Hour:00}:{now.Minute:00}:{now.Second:00}]"; 18 | } 19 | } 20 | 21 | public static string TimeStampMessage(string message) 22 | { 23 | return string.IsNullOrEmpty(message) ? message : $"{TimeStamp} {message}"; 24 | } 25 | 26 | public static ColoredMessage[] TimeStampMessage(ColoredMessage[] message, ConsoleColor? color = null, 27 | bool cloneMessages = false) 28 | { 29 | if (message == null) return null; 30 | 31 | ColoredMessage[] newMessage = new ColoredMessage[message.Length + 1]; 32 | newMessage[0] = new ColoredMessage($"{TimeStamp} ", color); 33 | 34 | if (cloneMessages) 35 | { 36 | for (int i = 0; i < message.Length; i++) 37 | newMessage[i + 1] = message[i]?.Clone(); 38 | } 39 | else 40 | { 41 | for (int i = 0; i < message.Length; i++) 42 | newMessage[i + 1] = message[i]; 43 | } 44 | 45 | return newMessage; 46 | } 47 | 48 | public static ColoredMessage[] TimeStampMessage(ColoredMessage message, ConsoleColor? color = null, 49 | bool cloneMessages = false) 50 | { 51 | return TimeStampMessage(new ColoredMessage[] {message}, color, cloneMessages); 52 | } 53 | 54 | public static string GetFullPathSafe(string path) 55 | { 56 | return string.IsNullOrWhiteSpace(path) ? null : Path.GetFullPath(path); 57 | } 58 | 59 | private const char WildCard = '*'; 60 | 61 | public static bool StringMatches(string input, string pattern, char wildCard = WildCard) 62 | { 63 | if (input == null && pattern == null) 64 | return true; 65 | 66 | if (pattern == null) 67 | return false; 68 | 69 | if (!pattern.IsEmpty() && pattern == new string(wildCard, pattern.Length)) 70 | return true; 71 | 72 | if (input == null) 73 | return false; 74 | 75 | if (input.IsEmpty() && pattern.IsEmpty()) 76 | return true; 77 | 78 | if (input.IsEmpty() || pattern.IsEmpty()) 79 | return false; 80 | 81 | string[] wildCardSections = pattern.Split(wildCard); 82 | 83 | int matchIndex = 0; 84 | foreach (string wildCardSection in wildCardSections) 85 | { 86 | // If there's a wildcard with nothing on the other side 87 | if (wildCardSection.IsEmpty()) 88 | { 89 | continue; 90 | } 91 | 92 | if (matchIndex < 0 || matchIndex >= input.Length) 93 | return false; 94 | 95 | Program.LogDebug(nameof(StringMatches), 96 | $"Matching \"{wildCardSection}\" with \"{input.Substring(matchIndex)}\"..."); 97 | 98 | if (matchIndex <= 0 && pattern[0] != wildCard) 99 | { 100 | // If the rest of the input string isn't at least as long as the section to match 101 | if (input.Length - matchIndex < wildCardSection.Length) 102 | return false; 103 | 104 | // If the input doesn't match this section of the pattern 105 | if (!input.Equals(wildCardSection, matchIndex, wildCardSection.Length)) 106 | return false; 107 | 108 | matchIndex += wildCardSection.Length; 109 | 110 | Program.LogDebug(nameof(StringMatches), $"Exact match found! Match end index at {matchIndex}."); 111 | } 112 | else 113 | { 114 | try 115 | { 116 | matchIndex = input.IndexOf(wildCardSection, matchIndex); 117 | 118 | if (matchIndex < 0) 119 | return false; 120 | 121 | matchIndex += wildCardSection.Length; 122 | 123 | Program.LogDebug(nameof(StringMatches), $"Match found! Match end index at {matchIndex}."); 124 | } 125 | catch 126 | { 127 | return false; 128 | } 129 | } 130 | } 131 | 132 | Program.LogDebug(nameof(StringMatches), 133 | $"Done matching. Matches = {matchIndex == input.Length || wildCardSections[wildCardSections.Length - 1].IsEmpty()}."); 134 | 135 | return matchIndex == input.Length || wildCardSections[wildCardSections.Length - 1].IsEmpty(); 136 | } 137 | 138 | public static bool InputMatchesAnyPattern(string input, params string[] namePatterns) 139 | { 140 | return !namePatterns.IsNullOrEmpty() && namePatterns.Any(namePattern => StringMatches(input, namePattern)); 141 | } 142 | 143 | private static bool PassesWhitelistAndBlacklist(string toCheck, string[] whitelist = null, 144 | string[] blacklist = null) 145 | { 146 | return (whitelist.IsNullOrEmpty() || InputMatchesAnyPattern(toCheck, whitelist)) && 147 | (blacklist.IsNullOrEmpty() || !InputMatchesAnyPattern(toCheck, blacklist)); 148 | } 149 | 150 | public static void CopyAll(DirectoryInfo source, DirectoryInfo target, string[] fileWhitelist = null, 151 | string[] fileBlacklist = null) 152 | { 153 | // If the target directory is the same as the source directory 154 | if (source.FullName == target.FullName) 155 | return; 156 | 157 | Directory.CreateDirectory(target.FullName); 158 | 159 | // Copy each file 160 | foreach (FileInfo file in source.GetFiles()) 161 | { 162 | if (PassesWhitelistAndBlacklist(file.Name, fileWhitelist, fileBlacklist)) 163 | { 164 | file.CopyTo(Path.Combine(target.ToString(), file.Name), true); 165 | } 166 | } 167 | 168 | // Copy each sub-directory using recursion 169 | foreach (DirectoryInfo sourceSubDir in source.GetDirectories()) 170 | { 171 | if (PassesWhitelistAndBlacklist(sourceSubDir.Name, fileWhitelist, fileBlacklist)) 172 | { 173 | // Begin copying sub-directory 174 | CopyAll(sourceSubDir, target.CreateSubdirectory(sourceSubDir.Name)); 175 | } 176 | } 177 | } 178 | 179 | public static void CopyAll(string source, string target, string[] fileWhitelist = null, 180 | string[] fileBlacklist = null) 181 | { 182 | CopyAll(new DirectoryInfo(source), new DirectoryInfo(target), fileWhitelist, fileBlacklist); 183 | } 184 | 185 | public static int[] StringArrayToIntArray(string[] stringArray) 186 | { 187 | lock (stringArray) 188 | { 189 | int[] intArray = new int[stringArray.Length]; 190 | 191 | for (int i = 0; i < stringArray.Length; i++) 192 | { 193 | if (!int.TryParse(stringArray[i], out int intValue)) 194 | continue; 195 | 196 | intArray[i] = intValue; 197 | } 198 | 199 | return intArray; 200 | } 201 | } 202 | 203 | /// 204 | /// Compares to 205 | /// 206 | /// The version string to compare 207 | /// The version string to compare to 208 | /// The separator character between version numbers 209 | /// 210 | /// Returns 1 if is greater than (or longer if equal) , 211 | /// 0 if is exactly equal to , 212 | /// and -1 if is less than (or shorter if equal) 213 | /// 214 | public static int CompareVersionStrings(string firstVersion, string secondVersion, char separator = '.') 215 | { 216 | if (firstVersion == null || secondVersion == null) 217 | return -1; 218 | 219 | int[] firstVersionNums = StringArrayToIntArray(firstVersion.Split(separator)); 220 | int[] secondVersionNums = StringArrayToIntArray(secondVersion.Split(separator)); 221 | int minVersionLength = Math.Min(firstVersionNums.Length, secondVersionNums.Length); 222 | 223 | // Compare version numbers 224 | for (int i = 0; i < minVersionLength; i++) 225 | { 226 | if (firstVersionNums[i] > secondVersionNums[i]) 227 | { 228 | return 1; 229 | } 230 | 231 | if (firstVersionNums[i] < secondVersionNums[i]) 232 | { 233 | return -1; 234 | } 235 | } 236 | 237 | // If all the numbers are the same 238 | 239 | // Compare version lengths 240 | if (firstVersionNums.Length > secondVersionNums.Length) 241 | return 1; 242 | if (firstVersionNums.Length < secondVersionNums.Length) 243 | return -1; 244 | 245 | // If the versions are perfectly identical, return 0 246 | return 0; 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /MultiAdmin/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /MultiAdmin/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Looking for ServerMod? 2 | ServerMod is on its own repo now: https://github.com/ServerMod/Smod2/ 3 | 4 | # MultiAdmin 5 | MultiAdmin is a replacement server tool for SCP: Secret Laboratory, which was built to help enable servers to have multiple configurations per server instance. 6 | 7 | The latest release can be found here: [Release link](https://github.com/ServerMod/MultiAdmin/releases/latest) 8 | 9 | Please check the [Installation Instructions](https://github.com/ServerMod/MultiAdmin#installation-instructions) for information about installing and running MultiAdmin. 10 | 11 | ## Discord 12 | You can join our Discord server here: https://discord.gg/8nvmMTr 13 | 14 | ## Installation Instructions: 15 | Make sure that you are running Mono 5.18.0 or higher, otherwise you might have issues. The latest Mono release can be found here: https://www.mono-project.com/download/stable. 16 | ### Running a Single Server with MultiAdmin 17 | 1. Place MultiAdmin.exe in your root server directory (next to LocalAdmin.exe) 18 | 19 | ### Running Multiple Servers with MultiAdmin 20 | 1. Place MultiAdmin.exe in your root server directory (next to LocalAdmin.exe) 21 | 2. Create a new directory defined by `servers_folder` (`servers` by default) 22 | 3. For each server you'd like, create a directory within the `servers_folder` directory 23 | 4. Optional: Create a file named `scp_multiadmin.cfg` within your server's folder for configuring MultiAdmin specifically for that server 24 | 25 | ## Features 26 | 27 | - Config Generator: Generates a full default MultiAdmin config file 28 | - Config Reload: Reloads the MultiAdmin configuration file 29 | - Exit Command: Adds a graceful exit command 30 | - Folder Copy Round Queue: Copies files from folders in a queue 31 | - GitHub Generator: Generates a GitHub README file outlining all the features/commands 32 | - Help: Display a full list of MultiAdmin commands and in game commands 33 | - Restart On Low Memory: Restarts the server if the working memory becomes too low 34 | - MultiAdminInfo: Prints MultiAdmin license and version information 35 | - 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] 36 | - Restart Command: Allows the game to be restarted without restarting MultiAdmin 37 | - Restart After a Number of Rounds: Restarts the server after a number rounds completed [Requires Modding] 38 | - TitleBar: Updates the title bar with instance based information 39 | 40 | ## MultiAdmin Commands 41 | This does not include ingame commands, for a full list type `HELP` in MultiAdmin which will produce all commands. 42 | 43 | - CONFIGGEN [FILE LOCATION]: Generates a full default MultiAdmin config file 44 | - CONFIG : Reloads the configuration file 45 | - EXIT: Exits the server 46 | - GITHUBGEN [FILE LOCATION]: Generates a GitHub README file outlining all the features/commands 47 | - HELP: Prints out available commands and their function 48 | - INFO: Prints MultiAdmin license and version information 49 | - NEW : Starts a new server with the given Server ID 50 | - RESTART: Restarts the game server (MultiAdmin will not restart, just the game) 51 | 52 | ## MultiAdmin Execution Arguments 53 | The arguments available for running MultiAdmin with 54 | 55 | - `--headless` or `-h`: Runs MultiAdmin in headless mode, this makes MultiAdmin not accept any input at all and only output to log files, not in console (Note: This argument is inherited by processes started by this MultiAdmin process) 56 | - `--server-id ` or `-id `: The Server ID to run this MultiAdmin instance with a config location (`--config` or `-c`) so that it reads the configs from the location, but stores the logs in the Server ID's folder 57 | - `--config ` or `-c `: The config location to use for this MultiAdmin instance (Note: This is used over the config option `config_location`) 58 | - `--port ` or `-p `: The port to use for this MultiAdmin instance (Note: This is used over the config option `port` and is inherited by processes started by this MultiAdmin process) 59 | 60 | ## Config Settings 61 | All configuration settings go into a file named `scp_multiadmin.cfg` in the same directory as MultiAdmin.exe or in your server directory within the `servers_folder` value defined in the global configuration file 62 | Any configuration files within the directory defined by `servers_folder` will have it's values used for that server over the global configuration file 63 | 64 | Config Option | Value Type | Default Value | Description 65 | --- | :---: | :---: | :------: 66 | config_location | String | **Empty** | The default location for the game to use for storing configuration files (a directory) 67 | appdata_location | String | **Empty** | The location for the game to use for AppData (a directory) 68 | disable_config_validation | Boolean | False | Disable the config validator 69 | share_non_configs | Boolean | True | Makes all files other than the config files store in AppData 70 | multiadmin_log_location | String | logs | The folder that MultiAdmin will store logs in (a directory) 71 | multiadmin_nolog | Boolean | False | Disable logging to file 72 | multiadmin_debug_log | Boolean | True | Enables MultiAdmin debug logging, this logs to a separate file than any other logs 73 | multiadmin_debug_log_blacklist | String List | HandleMessage, StringMatches, MessageListener | Which tags to block for MultiAdmin debug logging 74 | multiadmin_debug_log_whitelist | String List | **Empty** | Which tags to log for MultiAdmin debug logging (Defaults to logging all if none are provided) 75 | use_new_input_system | Boolean | True | **OBSOLETE: Use `console_input_system` instead, this config option may be removed in a future version of MultiAdmin.** Whether to use the new input system, if false, the original input system will be used 76 | console_input_system | [ConsoleInputSystem](#ConsoleInputSystem) | New | Which console input system to use 77 | hide_input | Boolean | False | Whether to hide console input, if true, typed input will not be printed 78 | port | Unsigned Integer | 7777 | The port for the server to use 79 | copy_from_folder_on_reload | String | **Empty** | The location of a folder to copy files from into the folder defined by `config_location` whenever the configuration file is reloaded 80 | folder_copy_whitelist | String List | **Empty** | The list of file names to copy from the folder defined by `copy_from_folder_on_reload` (accepts `*` wildcards) 81 | folder_copy_blacklist | String List | **Empty** | The list of file names to not copy from the folder defined by `copy_from_folder_on_reload` (accepts `*` wildcards) 82 | folder_copy_round_queue | String List | **Empty** | The location of a folder to copy files from into the folder defined by `config_location` after each round, looping through the locations 83 | 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) 84 | 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) 85 | randomize_folder_copy_round_queue | Boolean | False | Whether to randomize the order of entries in `folder_copy_round_queue` 86 | manual_start | Boolean | False | Whether or not to start the server automatically when launching MultiAdmin 87 | max_memory | Decimal | 2048 | The amount of memory in megabytes for MultiAdmin to check against 88 | restart_low_memory | Decimal | 400 | Restart if the game's remaining memory falls below this value in megabytes 89 | restart_low_memory_ticks | Unsigned Integer | 10 | The number of ticks the memory can be over the limit before restarting 90 | 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 91 | restart_low_memory_roundend_ticks | Unsigned Integer | 10 | The number of ticks the memory can be over the limit before restarting at the end of the round 92 | random_input_colors | Boolean | False | Randomize the new input system's colors every time a message is input 93 | restart_every_num_rounds | Integer | -1 | Restart the server every number of rounds 94 | 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 95 | safe_server_shutdown | Boolean | True | When MultiAdmin closes, if this is true, MultiAdmin will attempt to safely shutdown all servers 96 | safe_shutdown_check_delay | Integer | 100 | The time in milliseconds between checking if a server is still running when safely shutting down 97 | safe_shutdown_timeout | Integer | 10000 | The time in milliseconds before MultiAdmin gives up on safely shutting down a server 98 | 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 99 | 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 100 | server_start_retry | Boolean | True | Whether to try to start the server again after crashing 101 | server_start_retry_delay | Integer | 10000 | The time in milliseconds to wait before trying to start the server again after crashing 102 | multiadmin_tick_delay | Integer | 1000 | The time in milliseconds between MultiAdmin ticks (any features that update over time) 103 | servers_folder | String | servers | The location of the `servers` folder for MultiAdmin to load multiple server configurations from 104 | set_title_bar | Boolean | True | Whether to set the console window's titlebar, if false, this feature won't be used 105 | start_config_on_full | String | **Empty** | Start server with this config folder once the server becomes full [Requires Modding] 106 | 107 | ## ConsoleInputSystem 108 | If you are running into issues with the `tmux send-keys` command, switch to the original input system. 109 | 110 | String Value | Integer Value | Description 111 | --- | :---: | :----: 112 | Original | 0 | Represents the original input system. It may prevent MultiAdmin from closing and/or cause ghost game processes. 113 | Old | 1 | Represents the old input system. This input system should operate similarly to the original input system but won't cause issues with MultiAdmin's functionality. 114 | New | 2 | Represents the new input system. The main difference from the original input system is an improved display. 115 | --------------------------------------------------------------------------------