ArmaServer.psm1
function ArmaServer-ConvertWorkshopPath { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipeline)] [string] $Path, [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Container }, ErrorMessage = 'Path must be a valid directory')] [string] $WorkshopPath, [Parameter()] [string] $WorkshopPattern = '^[0-9]+$' ) Process { return ($Path -match $WorkshopPattern) ? (Join-Path $WorkshopPath "steamapps/workshop/content/107410/$Path") : $Path } } function ArmaServer-InstallBohemiaKeys { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipeline)] [string] $AddonName, [Parameter(Mandatory)] [string] $DestinationPath, [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Container }, ErrorMessage = 'Path must be a valid directory')] [string] $WorkshopPath, [Parameter()] [string] $WorkshopPattern = '^[0-9]+$' ) Process { $AddonPath = switch ($true) { ($AddonName -match $WorkshopPattern) { Write-Debug "$AddonName is a workshop mod" Join-Path $WorkshopPath "steamapps\workshop\content\107410\$AddonName" } Default { Write-Debug "$AddonName is an absolute path" $AddonName } } Write-Verbose "Copy addon keys from $AddonPath" Get-ChildItem $AddonPath -Recurse -Filter '*.bikey' | Copy-Item -Destination $DestinationPath } } function ArmaServer-InstallConfig { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateScript({ Test-Path -PathType Leaf $_ }, ErrorMessage = 'File not found')] [string] $ConfigFilename ) $Config = Import-PowerShellDataFile $ConfigFilename $TemplatePath = Join-Path $MyInvocation.MyCommand.Module.ModuleBase .templates $BasicFilePath = Join-Path $Config.ConfigPath basic.cfg $ServerFilePath = Join-Path $Config.ConfigPath server.cfg $MissionsPath = Join-Path $Config.MasterPath mpmissions if($null -eq $Config.DefaultMission) { $Config.DefaultMission = (Get-ChildItem $MissionsPath -Filter *.pbo | Select-Object -First 1).BaseName Write-Verbose "Default mission not set, detected: $($Config.DefaultMission)" } # Initiliaze configuration Write-Verbose "Removing old configurations from $($Config.ConfigPath)" New-Item $Config.ConfigPath -ItemType Directory -Force | Out-Null Get-ChildItem $Config.ConfigPath -Filter *.cfg | Remove-Item -Force Write-Verbose "Update configurations from ${TemplatePath}" Copy-Item $TemplatePath/basic.cfg $BasicFilePath $TemplateContent = Get-Content $TemplatePath/server.cfg -Raw $TemplateContent = $ExecutionContext.InvokeCommand.ExpandString($TemplateContent) $TemplateContent | Set-Content $ServerFilePath } function ArmaServer-InstallMission { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipeline)] [string] $Mission, [Parameter(Mandatory)] [string] $DestinationPath, [Parameter()] [string] $GitHubPattern = '^[\w-]+/[\w-]+$' ) Begin { Write-Verbose "Removing old missions from $DestinationPath" New-Item $DestinationPath -ItemType Directory -Force | Out-Null Get-ChildItem $DestinationPath -Filter *.pbo | Remove-Item } Process { switch ($true) { ($Mission -match $GitHubPattern) { Write-Verbose "Download mission from https://github.com/$Mission" if ($PSCmdlet.ShouldProcess($Mission, 'gh release download')) { & gh release download --repo $Mission --pattern *.pbo --dir $DestinationPath } } Default { Write-Verbose "Copy mission from $Mission to $DestinationPath" Get-ChildItem $Mission -Filter *.pbo -Recurse | Copy-Item -Destination $DestinationPath } } } } function ArmaServer-InvokeDownload { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipeline)] [string] $Addon, [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Container }, ErrorMessage = 'Path must be a valid directory')] [string] $MasterPath, [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Container }, ErrorMessage = 'Path must be a valid directory')] [string] $WorkshopPath, [Parameter()] [switch] $Quit, [Parameter()] [string] $Username = $env:STEAM_USERNAME, [Parameter()] [string] $WorkshopPattern = '^[0-9]+$', [Parameter()] [string] $Beta ) Begin { $MasterPath = Convert-Path $MasterPath $WorkshopPath = Convert-Path $WorkshopPath $CommandsFilename = $(New-TemporaryFile) ?? 'New-TemporaryFile' } Process { if ($Addon -match $WorkshopPattern) { "workshop_download_item 107410 $Addon validate" | Add-Content $CommandsFilename } } End { if (Test-Path $CommandsFilename) { Get-Content -Raw $CommandsFilename | Write-Debug } If ($PSCmdlet.ShouldProcess("$CommandsFilename", 'steamcmd runscript')) { "app_update 233780 -beta ${Beta} validate" | ArmaServer-InvokeSteamCmd -Path $MasterPath -Quit -Username $Username Get-Content -Raw $CommandsFilename | ArmaServer-InvokeSteamCmd -Path $WorkshopPath -Quit:$Quit -Username $Username } Remove-Item $CommandsFilename -Force -ErrorAction SilentlyContinue } } function ArmaServer-InvokeHeadlessProcess { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateScript({ If (Test-Path $_ -PathType Leaf) { $true } Else { Throw '-ConfigFilename not found' } })] [string] $ConfigFilename ) # Configuration $Config = Import-PowerShellDataFile $ConfigFilename $Mods = ($config.Mods | ArmaServer-ConvertWorkshopPath -WorkshopPath $Config.WorkshopPath) -Join ';' $ServerMods = ($config.ServerMods | ArmaServer-ConvertWorkshopPath -WorkshopPath $Config.WorkshopPath) -Join ';' $ArmaExe = Join-Path $Config.MasterPath arma3server_x64.exe $Arguments = @( '-client' '-connect=localhost' "-port=$($Config.Port)" """-password=$($Config.Password)""" "-pid=$($Config.ConfigPath)\headless.pid" '-name=HC' "-profiles=$($Config.ProfilePath)" "-mod=${Mods};${ServerMods}" ) # Starting server Write-Verbose 'Starting headless client' $Arguments | ConvertTo-Json | Write-Verbose $Process = Start-Process "${ArmaExe}" -ArgumentList ${Arguments} -PassThru if ($null -ne $Process) { $Process.PriorityClass = 'High' $Process.ProcessorAffinity = $config.HeadlessAffinity } } function ArmaServer-InvokeServerProcess { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateScript({ If (Test-Path $_ -PathType Leaf) { $true } Else { Throw '-ConfigFilename not found' } })] [string] $ConfigFilename ) # Configuration $Config = Import-PowerShellDataFile $ConfigFilename $Mods = ($config.Mods | ArmaServer-ConvertWorkshopPath -WorkshopPath $Config.WorkshopPath) -Join ';' $ServerMods = ($config.ServerMods | ArmaServer-ConvertWorkshopPath -WorkshopPath $Config.WorkshopPath) -Join ';' $ArmaExe = Join-Path $Config.MasterPath arma3server_x64.exe $Arguments = @( "-port=$($Config.Port)" '-cpuCount=2' '-exThreads=7' '-maxMem=8192' '-autoInit' "-pid=$($Config.ConfigPath)\server.pid" '-name=server' "-profiles=$($Config.ProfilePath)" "-config=$($Config.ConfigPath)\server.cfg" "-cfg=$($Config.ConfigPath)\basic.cfg" "-mod=${Mods}" "-serverMod=${ServerMods}" ) # Starting server Write-Verbose 'Starting server' $Arguments | ConvertTo-Json | Write-Verbose $Process = Start-Process "${ArmaExe}" -ArgumentList ${Arguments} -PassThru if ($null -ne $Process) { $Process.PriorityClass = 'High' $Process.ProcessorAffinity = $config.ServerAffinity } } function ArmaServer-InvokeSteamCmd { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipeline)] [string] $Command, [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Container }, ErrorMessage = 'Path must be a valid directory')] [string] $Path, [Parameter()] [switch] $Quit, [Parameter()] [string] $Username = $env:STEAM_USERNAME ) Begin { $Path = Convert-Path $Path $CommandsFilename = $(New-TemporaryFile) ?? 'New-TemporaryFile' @( '@NoPromptForPassword 1' "force_install_dir ${Path}" "login ${Username}" ) | Add-Content $CommandsFilename } Process { $Command | Add-Content $CommandsFilename } End { if ($Quit) { 'quit' | Add-Content $CommandsFilename } if (Test-Path $CommandsFilename) { Get-Content -Raw $CommandsFilename | Write-Debug } If ($PSCmdlet.ShouldProcess("$CommandsFilename", 'steamcmd runscript')) { & steamcmd +runscript $CommandsFilename } Remove-Item $CommandsFilename -Force -ErrorAction SilentlyContinue } } function ArmaServer-StopProcessFromPidFile { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipeline)] [string] $Filename ) Process { If (Test-Path -PathType Leaf $Filename) { $ProcessId = Get-Content $Filename Write-Verbose "$Filename found, attempting to stop process $ProcessId" Get-Process -Id $ProcessId -ErrorAction SilentlyContinue | Stop-Process -Force Remove-Item -Force $Filename } } } function Get-ArmaServerTask { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'File not found')] [string] $ConfigFilename ) $TaskInfo = @{ TaskName = (Get-Item $ConfigFilename).BaseName TaskPath = '\Arma3\' } Get-ScheduledTask @TaskInfo } function Install-ArmaServer { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Filename not found')] [string] $ConfigFilename ) Begin { $Config = Import-PowerShellDataFile $ConfigFilename $KeysPath = Join-Path $Config.MasterPath keys $MissionsPath = Join-Path $Config.MasterPath mpmissions $Addons = $Config.Mods + $Config.ClientMods + $Config.ServerMods | Select-Object -Unique } End { Stop-ArmaServer -ConfigFilename $ConfigFilename New-Item $Config.MasterPath -ItemType Directory -Force | Out-Null New-Item $Config.WorkshopPath -ItemType Directory -Force | Out-Null New-Item $KeysPath -ItemType Directory -Force | Out-Null Get-ChildItem -Recurse -Filter *.bikey $KeysPath | Remove-Item -Force $Addons | ArmaServer-InvokeDownload -MasterPath $Config.MasterPath -WorkshopPath $Config.WorkshopPath -Beta $Config.Beta -Quit $Addons | ArmaServer-InstallBohemiaKeys -DestinationPath $KeysPath -WorkshopPath $Config.WorkshopPath $Config.Missions | ArmaServer-InstallMission -DestinationPath $MissionsPath ArmaServer-InstallConfig -ConfigFilename $ConfigFilename } } function Register-ArmaServerTask { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'File not found')] [string] $ConfigFilename, [Parameter()] [string] $UserId = 'LOCALSERVICE', [Parameter()] [System.DateTime] $At = '5am', [Parameter()] [switch] $Force ) $PwshExe = (Get-Command pwsh).Path $ConfigFilename = Resolve-Path $ConfigFilename $ArgumentString = "-ExecutionPolicy Bypass -NonInteractive -Command Start-ArmaServer -ConfigFilename $ConfigFilename -Verbose" $SchedulerArguments = @{ Action = New-ScheduledTaskAction -Execute """$PwshExe""" -Argument $ArgumentString Principal = New-ScheduledTaskPrincipal -UserId $UserId -LogonType ServiceAccount Trigger = New-ScheduledTaskTrigger -Daily -At $At TaskName = (Get-Item $ConfigFilename).BaseName TaskPath = '\Arma3\' } Register-ScheduledTask -Force:$Force @SchedulerArguments } function Start-ArmaServer { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Filename not found')] [string] $ConfigFilename ) $Config = Import-PowerShellDataFile $ConfigFilename Stop-ArmaServer -ConfigFilename $ConfigFilename ArmaServer-InvokeServerProcess -ConfigFilename $ConfigFilename if ($Config.Headless) { ArmaServer-InvokeHeadlessProcess -ConfigFilename $ConfigFilename } } function Stop-ArmaServer { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [ValidateScript({ Test-Path $_ -PathType Leaf }, ErrorMessage = 'Filename not found')] [string] $ConfigFilename ) End { Write-Verbose 'Stopping server process from PID files' $Config = Import-PowerShellDataFile $ConfigFilename @( $(Join-Path $Config.ConfigPath headless.pid) $(Join-Path $Config.ConfigPath server.pid) ) | ArmaServer-StopProcessFromPidFile } } |