-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
PalworldServerTools.ps1
1370 lines (1363 loc) · 68 KB
/
PalworldServerTools.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<#
Author: Shupershuff
Version: See $ScriptVersion Variable below. See GitHub for latest version.
Usage:
Happy for you to make any modifications this script for your own needs providing:
- Any variants of this script are never sold.
- Any variants of this script published online should always be open source.
- Any variants of this script are never modifed to enable or assist in any malicious behaviour including (but not limited to): Bannable Mods, Cheats, Exploits, Phishing
Purpose:
Script will allow you to launch the server with parameters.
Script Will allow you to Retrieve RCON data or send Server commands via RCON.
See GitHub for full write up.
Instructions: See GitHub readme https://github.com/shupershuff/PalworldServerTools
To Do:
- GUI Front end and/or CLI Front End with a menu
- Auto Update: Add option for this to be called via GUI (Requires GUI/background agent)
- Tidy up for PlayersOnline.txt so it doesn't get massive
- Investigate having most RCON features work when script is run from remote host.
Changes since 1.1.0 (next version edits):
- Fixed minor issues with logging.
- Fixed Setup not working.
#>
##########################################################################################################
# Startup Bits
##########################################################################################################
param(
[switch]$Info,[switch]$Version,[switch]$ServerName,[switch]$ShowPlayers,[switch]$ShowPlayersNoHeader,[switch]$ShowPlayerNames,[switch]$ShowPlayerCount,[switch]$LogPlayers,[switch]$Shutdown,[int]$ShutdownTimer,$ShutdownMessage,[string]$Broadcast,[switch]$DoExit,[switch]$Save,
[string]$KickPlayer,[string]$BanPlayer,$ServerPath,$ThemeSettingsPath,$LaunchParameters,$HostIP,$RCONPort,$RCONPass,[switch]$UpdateOnly,[switch]$UpdateCheck,[switch]$NoUpdate,[switch]$Start,[switch]$StartThemed,[switch]$TodaysTheme,[Switch]$NoLogging,[switch]$Setup,[Switch]$Backup,[switch]$debug
)
$ScriptVersion = "1.1.1"
if ($debug -eq $True){#courtesy of https://thesurlyadmin.com/2015/10/20/transcript-logging-why-you-should-do-it/
$KeepLogsFor = 15
$VerbosePreference = "Continue"
$LogPath = Split-Path $MyInvocation.MyCommand.Path
Get-ChildItem "$LogPath\*.log" | Where LastWriteTime -LT (Get-Date).AddDays(-$KeepLogsFor) | Remove-Item -Confirm:$false
$LogPathName = Join-Path -Path $LogPath -ChildPath "$($MyInvocation.MyCommand.Name)-$(Get-Date -Format 'MM-dd-yyyy').debug.log"
Start-Transcript $LogPathName -Append
}
$ScriptFileName = Split-Path $MyInvocation.MyCommand.Path -Leaf
$WorkingDirectory = ((Get-ChildItem -Path $PSScriptRoot)[0].fullname).substring(0,((Get-ChildItem -Path $PSScriptRoot)[0].fullname).lastindexof('\')) #Set Current Directory path.
$LogsToKeep = 7
#Baseline of acceptable characters for ReadKey functions. Used to prevents receiving inputs from folk who are alt tabbing etc.
$Script:AllowedKeyList = @(48,49,50,51,52,53,54,55,56,57) #0 to 9
$Script:AllowedKeyList += @(96,97,98,99,100,101,102,103,104,105) #0 to 9 on numpad
$Script:AllowedKeyList += @(65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90) # A to Z
$EnterKey = 13
$Script:X = [char]0x1b #escape character for ANSI text colors
##########################################################################################################
# Script Functions
##########################################################################################################
Function Green {#Used for outputting green scucess text
process { Write-Host $_ -ForegroundColor Green }
}
Function Yellow {#Used for outputting yellow warning text
process { Write-Host $_ -ForegroundColor Yellow }
}
Function Red {#Used for outputting red error text
process { Write-Host $_ -ForegroundColor Red }
}
Function WriteLog {
#Determine what kind of text is being written and output to log and console.
#Note: $NoLogging is a script parameter and if true will not output to standard log file. If CustomLogFile param is used, output will continue to be written.
Param ( [string]$LogString,
[switch]$Info, #Standard messages.
[switch]$Verbose, #Only enters into log if $VerbosePreference is set to continue (Default is silentlycontinue). For Debug purposes only.
[switch]$Errorlog, #Can't use $Error as this is a built in PowerShell variable to recall last error. #Red coloured output text in console and sets log message type to [ERROR]
[switch]$Warning, #Cheese coloured output text in console and sets log message type to [WARNING]
[switch]$Success, #Green output text in console and sets log message type to [SUCCESS]
[switch]$NewLine, #used to enter in additional lines without redundantly entering in datetime and message type. Useful for longer messages.
[switch]$NoNewLine, #used to enter in text without creating another line. Useful for text you want added succinctly to log but not outputted to console
[switch]$NoConsole, #Write to log but not to Console
[string]$CustomLogFile #Explicitly specify the output filename.
)
if ($CustomLogFile -eq ""){
$Script:LogFile = ($WorkingDirectory + "\" + $ScriptFileName.replace(".ps1","_") + (("{0:yyyy/MM/dd}" -f (get-date)) -replace "/",".") + "_log.txt")
}
Else {
$Script:LogFile = ($WorkingDirectory + "\" + $CustomLogFile)
}
if ((Test-Path $LogFile) -ne $true){
Add-content $LogFile -value "" #Create empty Logfile
}
if (!(($Info,$Verbose,$Errorlog,$Warning,$Success) -eq $True)) {
$Info = $True #If no parameter has been specified, Set the Default log entry to type: Info
}
$DateTime = "[{0:dd/MM/yy} {0:HH:mm:ss}]" -f (Get-Date)
If ($CheckedLogFile -ne $True){
$fileContent = Get-Content -Path $Script:LogFile
if ($Null -ne $fileContent){
if ($fileContent[2] -match '\[(\d{2}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})\]') {#look at 3rd line of log file and match the date patern.
$firstDate = [datetime]::ParseExact($matches[1], 'dd/MM/yy HH:mm:ss', $null) #convert matched date string to variable
$IsTodaysLogFile = ($firstDate.Date -eq (Get-Date).Date) #compare match against todays date
}
if ($IsTodaysLogFile -eq $False){
Rename-Item $Script:LogFile ($WorkingDirectory + "\" + $ScriptFileName.replace(".ps1","_") + (("{0:yyyy/MM/dd}" -f $firstDate) -replace "/",".") + "_log.txt")
Write-Verbose "Archived Log file."
}
#Check if there's more than 3 logfiles with a date and if so delete the oldest one
$logFiles = Get-ChildItem -Path $WorkingDirectory -Filter "*.txt" | Where-Object { $_.Name -match '\d{2}\.\d{2}\.\d{2}_?\S*log\.txt' }
$logFilesToKeep = $logFiles | Sort-Object name -Descending | Select-Object -First $LogsToKeep #sorting by Name rather than LastWriteTime in case someone looks back and edits it.
$logFilesToDelete = $logFiles | Where-Object { $_ -notin $logFilesToKeep }
foreach ($fileToDelete in $logFilesToDelete) {# Delete log files that exceed the latest three
Remove-Item -Path $fileToDelete.FullName -Force
Write-Verbose ("Deleted " + $fileToDelete.FullName)
}
}
$Script:CheckedLogFile = $True
}
if ($True -eq $Info) {
$LogMessage = "$Datetime [INFO] - $LogString"
if ($False -eq $NoConsole){
write-output $LogString
}
}
if ($True -eq $Verbose) {
if ($VerbosePreference -eq "Continue") {
$LogMessage = "$Datetime [VERBOSE] - $LogString"
if ($False -eq $NoConsole){
write-output $LogString
}
}
}
if ($True -eq $Errorlog) {
$LogMessage = "$Datetime [ERROR] - $LogString"
if ($False -eq $NoConsole){
write-output $LogString | Red
}
}
if ($True -eq $Warning) {
$LogMessage = "$Datetime [WARNING] - $LogString"
if ($False -eq $NoConsole){
write-output $LogString | Yellow
}
}
if ($True -eq $Success) {
$LogMessage = "$Datetime [SUCCESS] - $LogString"
if ($False -eq $NoConsole){
write-output $LogString | Green
}
}
if ($True -eq $NewLine){#Overwrite $LogMessage to remove headers if -newline is enabled
$LogMessage = " $LogString"
}
if (($NoLogging -eq $False -or ($CustomLogFile -ne "" -and $LogPlayers -eq $True)) -and $NoNewLine -eq $True ){#Overwrite $LogMessage to put text immediately after last line if -nonewline is enabled
$LogContent = (Get-Content -Path $LogFile -Raw) # Read the content of the file
if ($logcontent -match ' \r?\n\r?\n$' -or $logcontent -match ' \r?\n$' -or $logcontent -match ' \r?\n$' -or $logcontent[-1] -eq " "){#if the last characters in the file is a space a space with one or two line breaks
$Space = " "
}
$LogContent = $LogContent.trim()
$words = $LogContent -split '\s+' # Split the content into words
$lastWord = $words[-1] # Get the last word
$lastWordPosition = $LogContent.LastIndexOf($lastWord) # Find the last occurrence of the last word in the content
$LogMessage = $lastWord + $Space + $LogString #"$lastLine$LogString"
$newContent = $LogContent.Substring(0, $lastWordPosition) + $LogMessage + $LogContent.Substring($lastWordPosition + $lastWord.Length) # Replace the last occurrence of the last word in the content
$newContent | Set-Content -Path $LogFile # Write the modified content back to the file
}
while ($Complete -ne $True -and $WriteAttempts -ne 3){
try {
if (($NoLogging -eq $False -or ($CustomLogFile -ne "" -and $LogPlayers -eq $True)) -and $NoNewLine -eq $False ){ #if user has disabled logging, eg on sensors that check every minute or so, they may want logging disabled.
Add-content $LogFile -value $LogMessage -ErrorAction Stop
$Complete = $True
}
else {
write-verbose "No Logging specified, didn't write to log."
$Complete = $True
}
}
Catch {#added this in case log file is being written to too fast and file is still locked when trying from previous write when trying to write new line to it.
write-verbose "Unable write to $LogFile. Check permissions on this folder"
$WriteAttempts ++
start-sleep -milliseconds 5
}
}
}
Function ReadKey([string]$message=$Null,[bool]$NoOutput,[bool]$AllowAllKeys) {#used to receive user input
$key = $Null
$Host.UI.RawUI.FlushInputBuffer()
if (![string]::IsNullOrEmpty($message)) {
Write-Host -NoNewLine $message
}
$AllowedKeyList = $Script:AllowedKeyList + @(13,27) #Add Enter & Escape to the allowedkeylist as acceptable inputs.
while ($Null -eq $key) {
if ($Host.UI.RawUI.KeyAvailable) {
$key_ = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown,IncludeKeyUp")
if ($True -ne $AllowAllKeys){
if ($key_.KeyDown -and $key_.VirtualKeyCode -in $AllowedKeyList) {
$key = $key_
}
}
else {
if ($key_.KeyDown) {
$key = $key_
}
}
}
else {
Start-Sleep -m 200 # Milliseconds
}
}
if ($key_.VirtualKeyCode -ne $EnterKey -and -not ($Null -eq $key) -and [bool]$NoOutput -ne $true) {
Write-Host ("$X[38;2;255;165;000;22m" + "$($key.Character)" + "$X[0m") -NoNewLine
}
if (![string]::IsNullOrEmpty($message)) {
Write-Host "" # newline
}
return $(
if ($Null -eq $key -or $key.VirtualKeyCode -eq $EnterKey) {
""
} else {
$key.Character
}
)
}
Function ReadKeyTimeout([string]$message=$Null, [int]$timeOutSeconds=0, [string]$Default=$Null) {#used to receive user input but times out after X amount of time
$key = $Null
$Host.UI.RawUI.FlushInputBuffer()
if (![string]::IsNullOrEmpty($message)) {
Write-Host -NoNewLine $message
}
$Counter = $timeOutSeconds * 1000 / 250
while ($Null -eq $key -and ($timeOutSeconds -eq 0 -or $Counter-- -gt 0)) {
if (($timeOutSeconds -eq 0) -or $Host.UI.RawUI.KeyAvailable) {
$key_ = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown,IncludeKeyUp")
if ($key_.KeyDown -and $key_.VirtualKeyCode -in $AllowedKeyList) {
$key = $key_
}
}
else {
Start-Sleep -m 250 # Milliseconds
}
}
if ($key_.VirtualKeyCode -ne $EnterKey -and -not ($Null -eq $key)) {
Write-Host -NoNewLine ("$X[38;2;255;165;000;22m" + "$($key.Character)" + "$X[0m")
}
if (![string]::IsNullOrEmpty($message)) {
Write-Host "" # newline
}
Write-Host #prevent follow up text from ending up on the same line.
return $(
if ($Null -eq $key -or $key.VirtualKeyCode -eq $EnterKey) {
$Default
} else {
$key.Character
}
)
}
Function PressTheAnyKeyToExit {#Used instead of Pause so folk can hit any key to exit
write-host " Press Any key to exit..." -nonewline
readkey -NoOutput $True -AllowAllKeys $True | out-null
WriteLog -info -noconsole "Script Exited"
Exit
}
Function NoGUIExit {#Used to write to log prior to exit
WriteLog -info -noconsole "Script Exited."
Exit
}
Function ExitCheck {
if ($null -eq $usedParameters){
PressTheAnyKeyToExit
}
Else {
NoGUIExit
}
}
Function SetupServer {
#Set Firewall Rules
#netstat -ano | findstr (get-process -name PalServer-Win64-Test-CMD).id # Using this tells me the query port is 27015. To my knowledge this can't be adjusted.
#I don't think 27015 actually uses tcp, it does for other game servers so left it in just in case. I also don't believe RCON uses UDP but again left it in just in case.
$ruleDisplayNames = @("Palworld Server TCP In","Palworld Server TCP Out","Palworld Server UDP In","Palworld Server UDP Out")
foreach ($ruleDisplayName in $ruleDisplayNames) {
$existingRule = Get-NetFirewallRule -DisplayName $ruleDisplayName -ErrorAction SilentlyContinue
if ($existingRule -eq $null) {
# Rule doesn't exist, so create it
if ($ruleDisplayName -eq "Palworld Server TCP In"){New-NetFirewallRule -DisplayName "Palworld Server TCP In" -Direction Inbound -LocalPort $Config.RCONPort,27015 -Protocol TCP -Action Allow}
if ($ruleDisplayName -eq "Palworld Server TCP Out"){New-NetFirewallRule -DisplayName "Palworld Server TCP Out" -Direction Outbound -LocalPort $Config.RCONPort,27015 -Protocol TCP -Action Allow}
if ($ruleDisplayName -eq "Palworld Server UDP In"){New-NetFirewallRule -DisplayName "Palworld Server UDP In" -Direction Inbound -LocalPort $Config.GamePort,$Config.RCONPort,27015 -Protocol UDP -Action Allow}
if ($ruleDisplayName -eq "Palworld Server UDP Out"){New-NetFirewallRule -DisplayName "Palworld Server UDP Out" -Direction Outbound -LocalPort $Config.GamePort,$Config.RCONPort,27015 -Protocol UDP -Action Allow}
Writelog -info -noconsole "Setup: "
Writelog -info -nonewline "Added firewall rule: $ruleDisplayName"
}
else {
Writelog -info -noconsole "Setup: "
Writelog -info -nonewline "Skipped adding firewall rule '$ruleDisplayName' as it already exists."
}
}
#Check if server is already installed in the ServerPath specified in config.xml, if not install it.
if (-not (Test-Path ($ServerPath + "\palserver.exe"))){
if (-not (Test-Path $ServerPath)){
WriteLog -info -noconsole "Setup: "
WriteLog -info -nonewline "Creating Palworld Server folder in $ServerPath"
New-Item -ItemType Directory -Path $ServerPath -ErrorAction stop | Out-Null
}
WriteLog -info -noconsole "Setup: "
WriteLog -info -nonewline "Downloading Palworld Server to $ServerPath"
Update -install
}
WriteLog -success -noconsole "Setup: "
WriteLog -success -nonewline "Setup Complete."
pause
exit
}
Function UpdateCheck {
# Credit: Some logic pinched from https://superuser.com/questions/1727148/check-if-steam-game-requires-an-update-via-command-line
$AppInfoFile = "$ServerPath\PalServerBuildID.txt"
WriteLog -info -noconsole "Update Check: "
WriteLog -info -noconsole -nonewline "Checking Updates for App ID 2394010"
try {
$AppInfoNew = (Invoke-RestMethod -Uri "https://api.steamcmd.net/v1/info/2394010").data.'2394010'.depots.branches.public.buildid
}
catch {
WriteLog -errorlog -noconsole "Update Check: "
WriteLog -errorlog -noconsole -nonewline "Update Check: Error getting app info for game"
Pause
Exit 1
}
$NeedsUpdate = $true
if (Test-Path $AppInfoFile) {
WriteLog -verbose "Update Check: File PalServerBuildID.txt exists."
$AppInfo = Get-Content $AppInfoFile
$NeedsUpdate = $AppInfo -ne $AppInfoNew
}
else {#if file doesn't exist, force update and export file.
if ($Config.SteamCMDPath -ne ""){#skip update part if steamcmd is not being used.
Update
}
$AppInfoNew | Out-File $AppInfoFile -Force
$NeedsUpdate = $False
return
}
if ($NeedsUpdate) {
WriteLog -Info -noconsole "Update Check: "
WriteLog -Info -nonewline "Update Available!"
if ($False -eq $UpdateCheck -and $Config.SteamCMDPath -ne ""){
if ($Running -eq $True){
WriteLog -info -noconsole "UpdateCheck: Server is currently running"
RCON_ShutdownRestartNotifier -Restart
}
Update -silent
$AppInfoNew | Out-File $AppInfoFile -Force #overwrite file with build ID
WriteLog -Success -noconsole "Update Check: "
WriteLog -success -nonewline "Update Complete!"
}
elseif ($Config.SteamCMDPath -eq ""){
WriteLog -warning -noconsole "Update Check: "
WriteLog -warning -nonewline "Cannot update as steamcmd is not being used"
}
}
else {
WriteLog -Success -noconsole "Update Check: "
WriteLog -Success -nonewline "Version up-to-date"
}
}
Function Update {
param([switch]$Silent,[switch]$install)
try {
if ($Install -eq $True){
WriteLog -info -noconsole "Update: "
WriteLog -info -nonewline "Downloading..."
}
Else {
WriteLog -info -noconsole "Update: "
WriteLog -info -nonewline "Updating..."
}
if ($silent){
Start-Process "$($Config.SteamCMDPath)\steamcmd.exe" -ArgumentList "+force_install_dir `"$ServerPath`" +login anonymous +app_update 2394010 validate +quit" -Wait | out-null
}
Else {
Start-Process "$($Config.SteamCMDPath)\steamcmd.exe" -ArgumentList "+force_install_dir `"$ServerPath`" +login anonymous +app_update 2394010 validate +quit" -Wait
}
if ($Install -eq $True){
WriteLog -success -noconsole "Update: "
WriteLog -success -nonewline "Downloaded!"
}
Else {
WriteLog -success -noconsole "Update: "
WriteLog -success -nonewline "Updated!"
}
}
Catch {
WriteLog -errorlog -noconsole "Update: "
WriteLog -errorlog -nonewline "Couldn't Update :("
}
}
Function LaunchServer {
if ($null -ne (Get-Process | Where-Object {$_.processname -match "palserver"})){
WriteLog -info -noconsole "LaunchServer: Server is currently running"
RCON_ShutdownRestartNotifier -Restart
}
if ($False -eq $UpdateOnly) {
if (-not $Config.NormalSettingsName.EndsWith(".ini")){#add .ini to value if it wasn't specified in config.
$Config.NormalSettingsName = $Config.NormalSettingsName + ".ini"
}
$Config.NormalSettingsName = $Config.NormalSettingsName.tostring()
if ((Test-Path -Path ($ThemeSettingsPath + $Config.NormalSettingsName)) -ne $true){#if file doesn't exist
WriteLog -warning -noconsole "LaunchServer: "
WriteLog -warning -nonewline ($Config.NormalSettingsName + " doesn't exist, copying current config to $ThemeSettingsPath" + $Config.NormalSettingsName)
Copy-Item "$ServerPath\Pal\Saved\Config\WindowsServer\PalWorldSettings.ini" "$ThemeSettingsPath$($Config.NormalSettingsName)" #$ServerPath\Pal\Saved\Config\WindowsServer\CustomSettings\
}
if ($True -ne $Start){
WriteLog -info -noconsole "LaunchServer: "
WriteLog -info -nonewline "Starting Palworld Server with Theme Config"
$iniFiles = Get-ChildItem -Path $ThemeSettingsPath -Filter *.ini
$Script:AllConfigOptions = @{}
foreach ($file in $iniFiles) {
$Script:AllConfigOptions[($file.Name).replace(".txt","")] = $file.FullName
}
$currentDay = (Get-Date -Format "dddd") #;$currentday = (Get-Date).AddDays(-3).ToString("dddd") #for testing theme on previous days
#Validation
$daysOfWeek = @("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
foreach ($day in $daysOfWeek) {
$IniName = $Config.$day
if ($Script:AllConfigOptions.ContainsKey("$IniName.ini")) {
WriteLog -success -noconsole "LaunchServer: "
WriteLog -success -nonewline "The filename specified for $Day is correct: (`"$IniName.ini`")"
}
Else {
WriteLog -errorlog -noconsole "LaunchServer: "
WriteLog -errorlog -nonewline "The filename for $Day is incorrect as it doesn't match config in the xml."
WriteLog -errorlog -newline ("Either edit the config or ensure there's a file called " + $IniName + ".ini")
write-host
$ErrorCount ++
}
}
if ($ErrorCount -ge 1){
$Plural = "these"
if ($ErrorCount -eq 1){
$Plural = "this"
}
WriteLog -errorlog -newline "Correct $Plural and rerun the script. Script will now exit."
ExitCheck
}
$SettingsActual = ($ServerPath +"\Pal\Saved\Config\WindowsServer\PalWorldSettings.ini")
$AllConfigOptionsObject = @()
foreach ($key in $Script:AllConfigOptions.Keys) {# Adding key-value pairs to the variable
$value = $AllConfigOptions[$key]
$entry = [PSCustomObject]@{
Name = $key
Value = $value
}
$AllConfigOptionsObject += $entry
}
$GameSettings = $AllConfigOptionsObject | where-object {$_.Name -match $Config.$currentday}
$TodaysTheme = $GameSettings.Name.replace("_"," ").replace(".ini","")
$TodaysTheme | Out-File -FilePath "$WorkingDirectory\TodaysTheme.txt"
Copy-Item $GameSettings.Value $SettingsActual
WriteLog -success -noconsole "LaunchServer: "
WriteLog -success -nonewline ("Copied `"" + $TodaysTheme + "`" Settings to PalWorldSettings.ini")
}
Else {
Copy-Item ($ThemeSettingsPath + $Config.NormalSettingsName) ($Script:ServerPath + "\Pal\Saved\Config\WindowsServer\PalWorldSettings.ini")
WriteLog -success -noconsole "LaunchServer: "
WriteLog -success -nonewline "Copied $($Config.NormalSettingsName) to PalWorldSettings.ini"
}
If ($True -eq $Config.CommunityServer){
WriteLog -verbose "LaunchServer: Community is enabled."
$Community = "EpicApp=PalServer"
}
Else {
$Community = ""
}
if ($Null -eq $LaunchParameters -or $LaunchParameters -eq ""){
WriteLog -verbose "LaunchServer: Standard Launch Parameters used"
$LaunchParameters = "$Community -log -publicip=$HostIP -publicport=$GamePort -useperfthreads -NoAsyncLoadingThread -UseMultithreadForDS"
}
Start-Process ($Script:ServerPath + "\PalServer.exe") $LaunchParameters
Write-Host
WriteLog -success "Server Started. Exiting..."
ExitCheck
}
}
Function ConfirmPlayer { #Validate if SteamID matches anything for kick/ban.
param ($PlayerDetails,$reason)
$Script:PlayersObject = ((RCON_ShowPlayers -ShowPlayers $True) | ConvertFrom-Csv) | Select-Object Name, SteamID
if ($Script:PlayersObject -eq $null -and $reason -ne "ban"){
WriteLog -verbose "ConfirmPlayer: There is no-one Online."
return
}
$matches = $PlayersObject | Where-Object { $_.name -eq $PlayerDetails } # Check if the string partially is identical to any "name" value in the array. Checking name needs to come first because otherwise someone could rename themselves to someone elses steamID and get the wrong person kicked/banned
if ($matches) {
Writelog -verbose "ConfirmPlayer: Player Name found for $PlayerDetails"
$Script:PlayerDetailsObject = $matches
$Script:ProceedWithKickOrBan = $True
return
}
$matches = $PlayersObject | Where-Object { $_.steamid -eq $PlayerDetails } # Check if the string partially is identical to any "name" value in the array
if ($matches) {
Writelog -verbose "ConfirmPlayer: Player SteamID found for $PlayerDetails"
$Script:PlayerDetailsObject = $matches
$Script:ProceedWithKickOrBan = $True
}
else {
$matches = $PlayersObject | Where-Object { $_.name -like "*$PlayerDetails*" } # Check if the string partially matches any "name" value in the array
if ($matches) {
foreach ($match in $matches) {
Writelog -verbose "ConfirmPlayer: Partial match found for '$PlayerDetails'. Matched name: $($match.name)"
writelog -info -noconsole ("ConfirmPlayer: Checking if user meant player: " + $match.name + "...")
while ($PlayerDetailsCheck -notin $YesValues -and $PlayerDetailsCheck -notin $NoValues){
Write-Host ("Did you mean " + $match.name + "? (Y/N) ") -nonewline -foregroundcolor yellow
$Script:PlayerDetailsActual = $match.name
$PlayerDetailsCheck = ReadKeyTimeout "" 8 "n" #Cancel if no response in 8 seconds. Useful if called from external app to script doesn't stay running if name was typo'd. if no button is pressed, send "n" to decline.
}
writelog -info -noconsole "ConfirmPlayer: "
writelog -info -nonewline -noconsole "User answered: $PlayerDetailsCheck"
if ($PlayerDetailsCheck -eq "y" -or $PlayerDetailsCheck -eq "yes" -or $PlayerDetailsCheck -eq $True){
$Script:PlayerDetailsObject = $match
$Script:ProceedWithKickOrBan = $True
return
}
else {
$PlayerDetailsCheck = $null
$Script:ProceedWithKickOrBan = $False
}
}
}
if ($Reason -eq "Ban" -and $Script:ProceedWithKickOrBan -ne $True){ #if ban command is used and player is offline, search playerdb.csv and manually ban
writelog -warning -noconsole "Confirm Player: "
writelog -warning -nonewline "$PlayerDetails is not online. Checking PlayerDB..."
$Script:PlayerIsOffline = $True
$PlayerDB = import-csv "$ServerPath\Pal\Saved\SaveGames\playerdb.csv"
foreach ($Player in $PlayerDB){
if ($Player.steamid -eq $PlayerDetails ){
writelog -success -noconsole "Confirm Player: "
writelog -success -nonewline "Matched SteamID to $($Player.Name) in PlayerDB."
$Script:PlayerDetailsObject = $Player
$Script:ProceedWithKickOrBan = $True
}
elseif ($Player.name -eq $PlayerDetails ){
writelog -success -noconsole "Confirm Player: "
writelog -success -nonewline "Matched Player Name to Steam ID:$($Player.SteamID) in PlayerDB."
$Script:PlayerDetailsObject = $Player
$Script:ProceedWithKickOrBan = $True
}
Elseif ($Player.name -like "*$PlayerDetails*"){
Writelog -verbose "ConfirmPlayer: Partial match found for '$PlayerDetails'. Matched name: $($Player.name)"
writelog -info -noconsole ("ConfirmPlayer: Checking if user meant player: " + $Player.name + "...")
while ($PlayerDetailsCheck -notin $YesValues -and $PlayerDetailsCheck -notin $NoValues){
Write-Host ("Did you mean " + $Player.name + "? (Y/N) ") -nonewline -foregroundcolor yellow
$Script:PlayerDetailsActual = $Player.name
$PlayerDetailsCheck = ReadKeyTimeout "" 8 "n" #Cancel if no response in 8 seconds. Useful if called from external app to script doesn't stay running if name was typo'd. if no button is pressed, send "n" to decline.
}
writelog -info -noconsole "ConfirmPlayer: "
writelog -info -nonewline -noconsole "User answered: $PlayerDetailsCheck"
if ($PlayerDetailsCheck -eq "y" -or $PlayerDetailsCheck -eq "yes" -or $PlayerDetailsCheck -eq $True){
writelog -success -noconsole "Confirm Player: "
writelog -success -nonewline "Matched $PlayerDetails to Steam ID:$($Player.Name) in PlayerDB."
$Script:PlayerDetailsObject = $Player
$Script:ProceedWithKickOrBan = $True
return
}
else {
$PlayerDetailsCheck = $null
$Script:ProceedWithKickOrBan = $False
}
}
Else {
$Script:ProceedWithKickOrBan = $False
}
}
if ($Script:ProceedWithKickOrBan -eq $False) {
writelog -errorlog -noconsole "ConfirmPlayer: "
writelog -errorlog -nonewline "Couldn't find player online or in PlayerDB."
}
}
Elseif ($Reason -ne "Ban"){
writelog -error -noconsole "Confirm Player: "
writelog -error -nonewline "$PlayerDetails is not online to kick."
$Script:PlayerDetailsActual = $PlayerDetails
$Script:ProceedWithKickOrBan = $False
}
}
}
Function Backup {
if ($Running -ne $True){
Writelog -error -noconsole "Backup: "
Writelog -error -nonewline "Server isn't running. No backup has been taken."
}
Else {
if ($Config.BackupPath -eq ""){
$backupRoot = ($ServerPath + "\Pal\Saved\SaveGames\Backup")#if no path specified in config, use default path.
}
Else {
$backupRoot = $Config.BackupPath
}
$sourcePath = "$ServerPath\Pal\Saved\SaveGames\0\"
# Get the current date and time
$currentDateTime = Get-Date
# Format the date and time components
$year = $currentDateTime.Year
$month = $currentDateTime.ToString("MMMM")
$day = $currentDateTime.ToString("dd")
$hour = $currentDateTime.ToString("HHmm")
# Construct the destination path
$destinationPath = Join-Path -Path $backupRoot -ChildPath "$year\$month\$day\$hour"
# Create the destination directory if it doesn't exist
if (-not (Test-Path $destinationPath)) {
WriteLog -info -noconsole "Backup: Creating Backup Folder in $backupRoot"
New-Item -ItemType Directory -Path $destinationPath -Force | out-null
}
# Save server if it's running. This is to ensure that all files match up before copying. Otherwise if timing is unlucky the server may be halfway through saving files when taking backup.
if ($Running -eq $True){
Try {
RCON_Save
Writelog -success -noconsole "Backup: "
Writelog -success -nonewline "Saved Server prior to backup"
}
Catch {
Writelog -error -noconsole "Backup: "
Writelog -error -nonewline "Couldn't Save Server prior to taking backup. Perhaps you haven't setup RCON."
Writelog -error -newline "Backing up server with files as is."
}
}
# Copy the folder to the destination
Copy-Item -Path $sourcePath -Destination $destinationPath -Recurse -Force | out-null
Writelog -success -noconsole "Backup: "
Writelog -success -nonewline "Backup completed. Save Data copied to: $destinationPath"
#Start Cleanup Tasks
Writelog -info -noconsole "Backup: "
Writelog -info -nonewline "Checking for old backups that can be cleaned up..."
$DirectoryArray = New-Object -TypeName System.Collections.ArrayList
Get-ChildItem -Path "$backupRoot\" -Directory -recurse -Depth 3 | Where-Object {$_.FullName -match '\\\d{4}\\\w+\\\d+\\\d{4}$'} | ForEach-Object {
$DirectoryObject = New-Object -TypeName PSObject
$pathComponents = $_.FullName -split '\\'
$year = $pathComponents[-4]
$month = $pathComponents[-3]
$month = [datetime]::ParseExact($month, 'MMMM', $null).Month # convert month from text to number. EG February to 02
$day = $pathComponents[-2]
$time = $pathComponents[-1]
$hour = $time[0]+$time[1]
$minute = $time[2]+$time[3]
$dateInFolder = Get-Date -Year $year -Month $month -Day $day -Hour $hour -minute $minute -second 00 #$minute can be changed to 00 if we want all the folders to be nicely named.
$ShortFolderDate = (Get-Date -Year $year -Month $month -Day $day).ToString("d")
Add-Member -InputObject $DirectoryObject -MemberType NoteProperty -Name FullPath -Value $_.FullName
Add-Member -InputObject $DirectoryObject -MemberType NoteProperty -Name FolderDate -Value $dateInFolder
Add-Member -InputObject $DirectoryObject -MemberType NoteProperty -Name ShortDate -Value $ShortFolderDate
[VOID]$DirectoryArray.Add($DirectoryObject)
}
$DirectoryArray = $DirectoryArray | Sort-Object {[datetime]$_.FolderDate} -Descending
$HourliesToKeep = $DirectoryArray | Group-Object -Property ShortDate | Select-Object -First 7 | select -expandproperty group #hourlies isn't necessarily hourly, can be taken every few minutes if desired
$DailiesToKeep = $DirectoryArray | Group-Object -Property ShortDate | ForEach-Object { $_.Group[0] } | Select-Object -skip 7 -First 24 #this is actually useful for capturing the last backup of each day
$MonthliesToKeep = $DirectoryArray | Group-Object -Property { ($_.ShortDate -split '/')[1] } | ForEach-Object { $_.Group[0] }
#Perform steps to remove any old backups that aren't needed anymore. Keep all backups within last 7 days (even if last 7 days aren't contiguous). For the last 30 days, keep only the last backup taken on that day (Note that again, 30 days aren't necessarily contiguous). For all older backups, only keep the last backup taken that month.
foreach ($Folder in $DirectoryArray){
if ($MonthliesToKeep.FullPath -notcontains $Folder.FullPath -and $DailiesToKeep.FullPath -notcontains $Folder.FullPath -and $HourliesToKeep.FullPath -notcontains $Folder.FullPath){
$Folder | Add-Member -MemberType NoteProperty -Name KeepFolder -Value "Deleted"
Remove-Item -Path $Folder.FullPath -Recurse -Force
Writelog -warning -noconsole "Backup: "
Writelog -warning -nonewline "Removed $($Folder.FullPath)"
$Cleanup = $True
}
Else {
$Folder | Add-Member -MemberType NoteProperty -Name KeepFolder -Value $True
}
}
#Perform steps to Cleanup any empty directories.
Function IsDirectoryEmpty($directory) { #Function to check each directory and subdirectory to determine if it's actually empty.
$files = Get-ChildItem -Path $directory -File
if ($files.Count -eq 0) { #directory has no files in it, checking subdirectories.
$subdirectories = Get-ChildItem -Path $directory -Directory
foreach ($subdirectory in $subdirectories) {
if (-not (IsDirectoryEmpty $subdirectory.FullName)) {
return $false #subdirectory has files in it
}
}
return $true #directory is empty
}
return $false #directory has files in it.
}
$subdirectories = Get-ChildItem -Path $backupRoot -recurse -Directory
foreach ($subdirectory in $subdirectories) {
if (IsDirectoryEmpty $subdirectory.FullName) { # Check if the subdirectory is empty (no files)
Remove-Item -Path $subdirectory.FullName -Force -Recurse # Remove the subdirectory
Writelog -warning -noconsole "Backup: "
Writelog -warning -nonewline "Deleted empty folder: $($subdirectory.FullName)"
$Cleanup = $True
}
}
Writelog -success -noconsole "Backup: "
if ($Cleanup -eq $True){
Writelog -success -nonewline "Backup cleanup complete."
}
Else {
Writelog -success -nonewline "No cleanup required."
}
}
}
Function RCON_ShutdownRestartNotifier {
param ([int]$ShutdownTimer,$ShutDownMessage,[switch]$Restart)
if ($Restart -eq $True){$RestartOrShutDown = "restart"} Else {$RestartOrShutDown = "shutdown"}
if ($Null -eq $ShutdownTimer -or $ShutdownTimer -eq ""){$ShutdownTimer = $Config.AutoShutdownTimer}
if ($Null -eq $ShutdownMessage -or $ShutdownMessage -eq ""){$shutdownmessage = "Server_is_scheduled_to_$RestartOrShutDown..."}
& ($Config.ARRCONPath + "\ARRCON.exe") --host $HostIP --port $RCONPort --pass $RCONPass "Shutdown $ShutdownTimer $ShutdownMessage"
$Script:TimeUntilServerReset = [int]$ShutdownTimer
WriteLog -info -noconsole "RCON_ShutdownRestartNotifier: "
WriteLog -info -nonewline ("Waiting " + ([int]$ShutdownTimer + [int]$Delay) + " seconds for server to $RestartOrShutDown...")
$MinutePlural = "s"
while ($script:TimeUntilServerReset -gt 0){
if ((Get-Process | Where-Object {$_.processname -match "palserver"}) -and $script:TimeUntilServerReset -ge 10){ #if server process is still running
$script:MinutesRemaining = [math]::floor($Script:TimeUntilServerReset / 60) # Calc Minutes remaining
$script:SecondsRemaining = $Script:TimeUntilServerReset % 60 # Calc Seconds remaining
while ($script:MinutesRemaining -gt 60){#if time is over an hour (it bloody shouldn't be) then wait until a more reasonable time to start sending messages.
start-sleep 60
$script:MinutesRemaining = $script:MinutesRemaining -60
$Script:TimeUntilServerReset = $Script:TimeUntilServerReset -60
}
if ($script:MinutesRemaining -ge 2 -and $script:SecondsRemaining -ne 0){#if there's a large amount of time left, make a nice even round time when broadcasting messages.
start-sleep $script:SecondsRemaining
$script:TimeUntilServerReset = $Script:TimeUntilServerReset - $script:SecondsRemaining
$script:SecondsRemaining = 0
}
Else {
while ($script:MinutesRemaining -eq 0 -and $script:SecondsRemaining -notin @("10","20","30","40","50")){#if time is within 60 seconds, wait until the time left is a nice round number.
start-sleep 1
$script:SecondsRemaining = $script:SecondsRemaining -1
$Script:TimeUntilServerReset = $Script:TimeUntilServerReset -1
}
while ($script:MinutesRemaining -eq 1 -and ($script:SecondsRemaining -ne 0 -and $script:SecondsRemaining -ne 30)){#if time is within 60 seconds, wait until the time left is either 0 or 30
start-sleep 1
$script:SecondsRemaining = $script:SecondsRemaining -1
$Script:TimeUntilServerReset = $Script:TimeUntilServerReset -1
}
$MinutePlural = ""
}
if ($Script:TimeUntilServerReset -ge 3600){#if there's more an hour send a reminder 15 minutes.
WriteLog -verbose ("RCON_ShutdownRestartNotifier: Server_$($RestartOrShutdown)_in_$MinutesRemaining" + "_minute$MinutePlural" + "_and_$SecondsRemaining" + "_seconds.")
RCON_Broadcast -BroadcastMessage ("Server_$($RestartOrShutdown)_in_$minutesRemaining" + "_minute$MinutePlural")
Start-Sleep -Seconds 900 # Wait for 15 minutes
$script:TimeUntilServerReset = $Script:TimeUntilServerReset -900 # Decrement the time remaining
}
elseif ($Script:TimeUntilServerReset -ge 900){#if there's between 15 and 60 mins left, send a reminder every 5 minutes
WriteLog -verbose ("RCON_ShutdownRestartNotifier: Server_$($RestartOrShutdown)_in_$MinutesRemaining" + "_minute$MinutePlural" + "_and_$SecondsRemaining" + "_seconds.")
RCON_Broadcast -BroadcastMessage ("Server_$($RestartOrShutdown)_in_$minutesRemaining" + "_minute$MinutePlural")
Start-Sleep -Seconds 300 # Wait for 300 seconds
$script:TimeUntilServerReset = $Script:TimeUntilServerReset -300 # Decrement the time remaining
}
elseif ($Script:TimeUntilServerReset -ge 300){#if there's between 5 and 15 mins left, send a reminder every minute
WriteLog -verbose ("RCON_ShutdownRestartNotifier: Server_$($RestartOrShutdown)_in_$MinutesRemaining" + "_minute$MinutePlural" + "_and_$SecondsRemaining" + "_seconds.")
RCON_Broadcast -BroadcastMessage ("Server_$($RestartOrShutdown)_in_$minutesRemaining" + "_minute$MinutePlural")
Start-Sleep -Seconds 60 # Wait for 60 seconds
$script:TimeUntilServerReset = $Script:TimeUntilServerReset -60 # Decrement the time remaining
}
Elseif ($Script:TimeUntilServerReset -ge 60){#if there's between 1 and 3 minutes left, send a reminder every 30 seconds.
WriteLog -verbose ("RCON_ShutdownRestartNotifier: Server_$($RestartOrShutdown)_in_$MinutesRemaining" + "_minute$MinutePlural" + "_and_$SecondsRemaining" + "_seconds.")
RCON_Broadcast -BroadcastMessage ("Server_$($RestartOrShutdown)_in_$minutesRemaining" + "_minute$MinutePlural" + "_and_$SecondsRemaining" + "_seconds.")
Start-Sleep -Seconds 30 # Wait for 30 seconds
$script:TimeUntilServerReset = $Script:TimeUntilServerReset -30 # Decrement the time remaining
}
Elseif ($Script:TimeUntilServerReset -ge 10){#if there's between 10 seconds and 1 min left, send a reminder every 10 seconds
WriteLog -verbose ("RCON_ShutdownRestartNotifier: Server_$($RestartOrShutdown)_in_$MinutesRemaining" + "_minute$MinutePlural" + "_and_$SecondsRemaining" + "_seconds.")
RCON_Broadcast -BroadcastMessage ("Server_$($RestartOrShutdown)_in_$SecondsRemaining" + "_seconds.")
if ($Script:TimeUntilServerReset -le 10){#if server is about to restart
Start-Sleep -Milliseconds 4750 # Only wait for 5 ish seconds so the 'shutting down now' warning can be seen and server can be saved.
$script:timeUntilServerReset = 0 # Decrement the time remaining
}
Else {
Start-Sleep -Seconds 10 # Wait for 10 seconds
$script:timeUntilServerReset = $Script:TimeUntilServerReset -10 # Decrement the time remaining
}
}
}
Elseif ((Get-Process | Where-Object {$_.processname -match "palserver"}) -and $script:TimeUntilServerReset -le 10){
$QuickShutdown = $True
}
Else {#if server was shutdown (eg by user manually), cancel messaging.
$SkipRemaining = $True
break
}
if (($script:TimeUntilServerReset -eq 0 -and $SkipRemaining -ne $True) -or $QuickShutdown -eq $True){#if there's less than 10 seconds left, announce immediate shutdown.
if ($Restart -eq $True){$RestartOrShutDown = "restarting"} Else {$RestartOrShutDown = "shutting_down"}
WriteLog -verbose "RCON_ShutdownRestartNotifier: Server_$($RestartOrShutdown)_now!"
if ($QuickShutdown -eq $True){
if ($script:TimeUntilServerReset -ge 5){
RCON_Save #Force save just prior to server shutdown
RCON_Broadcast -BroadcastMessage "Server_$($RestartOrShutdown)_now!"
}
Else {
Write-Host "Server_$($RestartOrShutdown)_now!"
}
start-sleep ($script:TimeUntilServerReset + 5) #wait for remaining shutdown time plus 5 seconds for buffer
}
Else {
RCON_Save #Force save just prior to server shutdown
RCON_Broadcast -BroadcastMessage "Server_$($RestartOrShutdown)_now!"
start-sleep (5 + 5) #5 seconds for remaining shutdown time and 5 seconds for buffer
}
WriteLog -verbose ("RCON_ShutdownRestartNotifier: Waited " + ([int]$ShutdownTimer + 5) + " seconds for server to shutdown.")
}
}
if ($null -ne (Get-Process | Where-Object {$_.processname -match "palserver"})){#if palserver is STILL running, force closure.
WriteLog -warning -noconsole "LaunchServer: "
WriteLog -warning -nonewline "Force killing server processes..."
taskkill /F /IM PalServer.exe | out-null
taskkill /F /IM PalServer-Win64-Test-Cmd.exe | out-null
}
}
Function RCON_Broadcast {#RCON
param ($BroadCastMessage)
$BroadcastResponse = (& ($Config.ARRCONPath + "\ARRCON.exe") --host $HostIP --port $RCONPort --pass $RCONPass "Broadcast $BroadcastMessage")[1]
WriteLog -success -noconsole "RCON_Broadcast: "
WriteLog -success -nonewline "$BroadcastResponse"
}
Function RCON_Save {#RCON
WriteLog -success -noconsole "RCON_Save: "
WriteLog -info -nonewline "Saving..."
Do {
$SaveAttempts ++
$SaveStatus = & ($Config.ARRCONPath + "\ARRCON.exe") --host $HostIP --port $RCONPort --pass $RCONPass Save
} Until ($SaveStatus -eq "Complete Save" -or $SaveAttempts -eq 3)
if ($SaveStatus -eq "Complete Save"){
WriteLog -success -noconsole "RCON_Save: "
WriteLog -success -nonewline "Saved!"
}
}
Function RCON_Info {#RCON
param ($Info,$Version,$ServerName)
try {
WriteLog -info -noconsole "RCON_Info: Getting Info data..."
$InfoText = & ($Config.ARRCONPath + "\ARRCON.exe") --host $HostIP --port $RCONPort --pass $RCONPass info
$InfoText = $InfoText[1]
if ($True -eq $ServerName){
$pattern = 'Pal Server\[v\d+\.\d+\.\d+\.\d+\] (.+)' #response is "Welcome to Pal Server[v0.1.2.0] SERVER NAME. Filter out the preamble so only SERVER NAME is returned.
if ($InfoText -match $pattern) {
$ServerNameText = $matches[1]
}
$ServerNameText
}
if ($Info -eq $True ){
$InfoText
}
if ($True -eq $Version){
$pattern = '\[v(\d+(\.\d+)+)\]' # Define the regular expression pattern to match the version number
# Use the -match operator to find the match in the string
if ($InfoText -match $pattern) {
$versionNumber = "v" + $matches[1] # The matched version number will be in the $matches variable
$versionNumber
}
else {
WriteLog -errorlog -noconsole "RCON_Info: "
WriteLog -errorlog -nonewline "No version number found"
}
}
WriteLog -info -noconsole "RCON_Info: Info data retreived."
}
Catch {
WriteLog -errorlog -noconsole "RCON_Info: Couldn't pull ServerName"
write-output "Couldn't retrieve Server Name" | Red
}
}
Function RCON_ShowPlayers {#RCON
param ($ShowPlayers,$ShowPlayerCount,$ShowPlayerNames,$ShowPlayersNoHeader,$LogPlayers)
Try {
WriteLog -info -noconsole "RCON_ShowPlayers: Getting showplayers data..."
$PlayersOnline = & ($Config.ARRCONPath + "\ARRCON.exe") --host $HostIP --port $RCONPort --pass $RCONPass showplayers
$PlayersOnlineObject = ($PlayersOnline | Select-Object -First ($PlayersOnline.Count - 1)| Select-Object -Skip 1)
$PlayersOnlineObject = $PlayersOnlineObject | convertfrom-csv
$PlayersOnlineCount = $PlayersOnline.count -3
$PlayersOnlineNames = $PlayersOnline[2..($PlayersOnline.Count - 2)] -replace ',.*', ''
$PlayerNamesCommaSeparated = ($PlayersOnlineNames -split "`n" | ForEach-Object { $_.Trim() }) -join ', '
if ($True -eq $ShowPlayerCount){
$PlayersOnlineCount
}
if ($True -eq $ShowPlayers -or $True -eq $ShowPlayersNoHeader -or $True -eq $LogPlayers){
if ($PlayersOnlineCount -ne 0){
Function UpdatePlayerDB {
param([switch]$UpdateCSV,[switch]$addcsv)
try {
if ($UpdateCSV -eq $True){
$PlayerDB | Export-Csv -Path $PlayerCSVFilePath -NoTypeInformation -Force -Encoding utf8 -erroraction stop
Writelog -info -noconsole ("RCON_ShowPlayers: Player '$($PlayerToUpdate.Name)' updated in CSV." -f $OnlinePlayer.Name)
}
If ($AddCSV -eq $True){
$OnlinePlayer | Export-Csv -Path $PlayerCSVFilePath -Append -NoTypeInformation -Force -Encoding utf8 -erroraction stop
Writelog -info -noconsole ("RCON_ShowPlayers: Player '{0}' added to CSV." -f $OnlinePlayer.Name)
}
$Script:CSVLocked = $False
start-sleep -milliseconds 10 # Probably not needed but added this to allow a tiny amount of time for writing to release file.
}
Catch { #if csv is locked, force closure
$Script:CSVLocked = $True
}
}
$PlayerCSVFilePath = ("$ServerPath\Pal\Saved\SaveGames\PlayerDB.csv")
if (-not (Test-Path $PlayerCSVFilePath)) {# Create CSV file with headers if it doesn't exist
Writelog -info -noconsole "RCON_ShowPlayers: PlayerDB.csv didn't exist so created it."
"Name,PlayerUID,SteamID,firstseen,lastseen,previousnames" | Out-File -FilePath $PlayerCSVFilePath -Encoding utf8
}
foreach ($OnlinePlayer in $PlayersOnlineObject){
$PlayerDB = Import-Csv -Path $PlayerCSVFilePath
$OnlinePlayerMatchesPlayerDB = $PlayerDB | Where-Object { $_.SteamID -eq $OnlinePlayer.SteamID } #check if player already exists in the csv
if ($OnlinePlayerMatchesPlayerDB) {#if they ARE in the csv file.
$PlayerToUpdate = $PlayerDB | Where-Object { $_.SteamID -eq $OnlinePlayer.SteamID }
$PlayerToUpdate.lastseen = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
if ($OnlinePlayer.name -ne $PlayerToUpdate.name){
$PlayerToUpdate.PreviousNames = ($PlayerToUpdate.PreviousNames + ", " + $PlayerToUpdate.Name).Trimstart(', ')
$PlayerToUpdate.Name = $OnlinePlayer.name
}
UpdatePlayerDB -UpdateCSV
}
else { #if they aren't in the csv file.
$OnlinePlayer | Add-Member -MemberType NoteProperty -Name 'FirstSeen' -Value (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') -Force #add time properties.
$OnlinePlayer | Add-Member -MemberType NoteProperty -Name 'LastSeen' -Value (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') -Force
UpdatePlayerDB -AddCSV
}
}
if ($Script:CSVLocked -eq $True){
Writelog -errorlog -noconsole "RCON_ShowPlayers: "
Writelog -errorlog -newline "Unable to update PlayerDB.csv."
Writelog -errorlog -nonewline "File is likely locked, make sure to close it if it's open."
Writelog -errorlog -nonewline "Alternatively perhaps the script doesn't have permissions to write to this location."
start-sleep 3
}
$PlayersOnline = ($PlayersOnline | Select-Object -First ($PlayersOnline.Count - 1)| Select-Object -Skip 1) # skip blank last row. skip rcon command header.
if ($LogPlayers -eq $True){
WriteLog -info -noconsole "RCON_ShowPlayers: Players Online: $PlayerNamesCommaSeparated"
WriteLog -info -noconsole "RCON_ShowPlayers: Writing to PlayersOnline.txt"
WriteLog -info -noconsole -CustomLogFile "PlayersOnline.txt" "Players Online details: (PlayerName, PlayerUID, SteamID):"
foreach ($Player in $PlayersOnlineObject){ #skip csv header
WriteLog -newline -noconsole -CustomLogFile "PlayersOnline.txt" ($Player.Name + ", " + $Player.playeruid + ", " + $Player.Steamid)
WriteLog -info -noconsole ("RCON_ShowPlayers: added " + $Player.Name + " to PlayersOnline.txt")
}
WriteLog -info -noconsole "RCON_ShowPlayers: Wrote to PlayersOnline.txt"
}
if ($ShowPlayers -eq $True){
$PlayersOnline
}
Elseif ($ShowPlayersNoHeader -eq $True){
$PlayersOnline = ($PlayersOnline | Select-Object -Skip 1) # skip blank last row. skip rcon command header.
$PlayersOnline
}
}
Else {
WriteLog -info -noconsole "RCON_ShowPlayers: Currently no-one Online"
}
}
if ($True -eq $ShowPlayerNames -and $PlayersOnlineCount -ne 0){
$PlayerNamesCommaSeparated
}
WriteLog -success -noconsole "RCON_ShowPlayers: Showplayers data retreived"
}
Catch {
WriteLog -errorlog -noconsole "RCON_ShowPlayers: "
WriteLog -errorlog -nonewline "Couldn't retrieve Player data"
}
}
Function RCON_DoExit {#RCON
WriteLog -warning -noconsole "RCON: "
WriteLog -warning -nonewline "Shutting down now..."
& ($Config.ARRCONPath + "\ARRCON.exe") --host $HostIP --port $RCONPort --pass $RCONPass DoExit
start-sleep -milliseconds 4850 #takes a short time for server to actually close
}
Function RCON_KickPlayer {#RCON
param ($PlayerDetails)
ConfirmPlayer -PlayerDetails $playerdetails
if ($Script:PlayersObject -eq $null){
WriteLog -error -noconsole "RCON_KickPlayer: "
WriteLog -error -nonewline "There is no-one Online to kick!"
break
}
if ($Script:ProceedWithKickOrBan -ne $False){
WriteLog -warning -noconsole "RCON: "
WriteLog -warning -nonewline "Attempting to Kick $($Script:PlayerDetailsObject.name)..."
$KickResult = (& ($Config.ARRCONPath + "\ARRCON.exe") --host $HostIP --port $RCONPort --pass $RCONPass "KickPlayer $($Script:PlayerDetailsObject.steamid)") | Select-Object -Skip 1
$KickResult = $KickResult -replace '\x1B\[[0-9;]*[a-zA-Z]', '' #remove any ANSI codes sent back by ARRCON eg [39m[49m[22m[24m[27m
if ($KickResult -match "failed to kick") {
WriteLog -errorlog -noconsole "RCON_KickPlayer: "
WriteLog -errorlog -nonewline $KickResult
}
Else {
WriteLog -success -noconsole "RCON_KickPlayer: "
WriteLog -success -nonewline "Kicked $($Script:PlayerDetailsObject.name), SteamID:$($Script:PlayerDetailsObject.steamid)"
}
}
Else {
writelog -errorlog -noconsole "RCON_KickPlayer: "
if ($PlayerDetailsActual -ne $Null){
writelog -errorlog -nonewline "$PlayerDetailsActual was not kicked."
}
Else {
writelog -errorlog -nonewline "$PlayerDetails was not kicked."
}
}
}
Function RCON_BanPlayer {#RCON
param ($PlayerDetails)
ConfirmPlayer -PlayerDetails $playerdetails -reason "Ban"
if ($Script:ProceedWithKickOrBan -ne $False){
if ($Script:PlayerIsOffline -ne $True){ #ARRCON can only ban players who are online.
WriteLog -warning -noconsole "RCON_BanPlayer: "
WriteLog -warning -nonewline "Attempting to ban $($Script:PlayerDetailsObject.name)..."
$BanResult = (& ($Config.ARRCONPath + "\ARRCON.exe") --host $HostIP --port $RCONPort --pass $RCONPass "BanPlayer $($Script:PlayerDetailsObject.steamid)") | Select-Object -Skip 1
if ($BanResult -match "failed to ban") {
WriteLog -warning -noconsole "RCON_BanPlayer: "
WriteLog -warning -nonewline "Unable to ban player SteamID:$($Script:PlayerDetailsObject.steamid)"
}
Else {