├── .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 |
--------------------------------------------------------------------------------