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]+$', [Parameter()] [uri] $OfficialKeysUri = 'https://arma.gsri.team/legacy/keys.zip' ) Begin { Write-Verbose "Removing old keys from $DestinationPath" New-Item $DestinationPath -ItemType Directory -Force | Out-Null Get-ChildItem -Recurse -Filter *.bikey $DestinationPath | Remove-Item -Force } 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 } End { $KeysZip = New-TemporaryFile Write-Verbose "Download official BI keys from $OfficialKeysUri" Invoke-WebRequest -Uri $OfficialKeysUri -OutFile $KeysZip Expand-Archive -Path $KeysZip -DestinationPath $DestinationPath Remove-Item -Force $KeysZip } } 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 # 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]+$' ) Begin { $MasterPath = Convert-Path $MasterPath $WorkshopPath = Convert-Path $WorkshopPath $CommandsFilename = $(New-TemporaryFile) ?? 'New-TemporaryFile' @( '@NoPromptForPassword 1' "force_install_dir ${MasterPath}" "login ${Username}" 'app_update 233780 validate' "force_install_dir ${WorkshopPath}" ) | Add-Content $CommandsFilename } Process { if ($Addon -match $WorkshopPattern) { "workshop_download_item 107410 $Addon validate" | 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-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-StopProcessFromPidFile { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ValueFromPipeline)] [string] $Filename ) Process { If (Test-Path -PathType Leaf $Filename) { Get-Process -Id $(Get-Content $Filename) -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 $Addons | ArmaServer-InvokeDownload -MasterPath $Config.MasterPath -WorkshopPath $Config.WorkshopPath -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 ) Begin { $Config = Import-PowerShellDataFile $ConfigFilename $TranscriptPath = Join-Path $Config.ProfilePath pslogs Start-Transcript -OutputDirectory $TranscriptPath } Process { Stop-ArmaServer -ConfigFilename $ConfigFilename Install-ArmaServer -ConfigFilename $ConfigFilename ArmaServer-InvokeServerProcess -ConfigFilename $ConfigFilename if ($Config.Headless) { ArmaServer-InvokeHeadlessProcess -ConfigFilename $ConfigFilename } } End { Stop-Transcript } } 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 } } |