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'
}

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 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
  $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
}
}