# Minimal plugin-owned Source RCON client for the local SCUM "Simple RCON" # listener declared by this plugin. It is used by plugin lifecycle scripts that # must speak to the running server (announce, save, shutdown). The listener # password stays on this machine: callers either pass -ConfigPath so the script # reads it from the local UE4SS mod config, or pass -Password directly. param( [string]$ConfigPath = "", [string]$Password = "", [int]$Port = 0, [string]$BindAddress = "", [string]$Command = "", [int]$TimeoutMs = 10000 ) $ErrorActionPreference = "Stop" function Read-RconConfig { param([string]$Path) $values = @{} if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { return $values } $section = "" foreach ($line in Get-Content -LiteralPath $Path) { $trimmed = $line.Trim() if (-not $trimmed -or $trimmed.StartsWith(";") -or $trimmed.StartsWith("#")) { continue } if ($trimmed.StartsWith("[") -and $trimmed.EndsWith("]")) { $section = $trimmed.Substring(1, $trimmed.Length - 2).Trim() continue } $separator = $trimmed.IndexOf("=") if ($separator -lt 1) { continue } $key = $trimmed.Substring(0, $separator).Trim() $value = $trimmed.Substring($separator + 1).Trim().Trim('"') $values[($section + "." + $key).ToLowerInvariant()] = $value } return $values } function Send-RconPacket { param([System.Net.Sockets.NetworkStream]$Stream, [int]$Id, [int]$Type, [string]$Body) $bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($Body) $size = 4 + 4 + $bodyBytes.Length + 2 $buffer = New-Object byte[] (4 + $size) [System.BitConverter]::GetBytes([int]$size).CopyTo($buffer, 0) [System.BitConverter]::GetBytes([int]$Id).CopyTo($buffer, 4) [System.BitConverter]::GetBytes([int]$Type).CopyTo($buffer, 8) $bodyBytes.CopyTo($buffer, 12) $Stream.Write($buffer, 0, $buffer.Length) $Stream.Flush() } function Receive-RconPacket { param([System.Net.Sockets.NetworkStream]$Stream) $header = New-Object byte[] 4 $read = 0 while ($read -lt 4) { $chunk = $Stream.Read($header, $read, 4 - $read) if ($chunk -le 0) { throw "RCON connection closed before a response header arrived" } $read += $chunk } $size = [System.BitConverter]::ToInt32($header, 0) if ($size -lt 10 -or $size -gt 65536) { throw "RCON response size is invalid" } $body = New-Object byte[] $size $read = 0 while ($read -lt $size) { $chunk = $Stream.Read($body, $read, $size - $read) if ($chunk -le 0) { throw "RCON connection closed mid response" } $read += $chunk } $id = [System.BitConverter]::ToInt32($body, 0) $type = [System.BitConverter]::ToInt32($body, 4) $text = [System.Text.Encoding]::UTF8.GetString($body, 8, $size - 10) return @{ Id = $id; Type = $type; Body = $text } } $config = Read-RconConfig -Path $ConfigPath if (-not $Password) { $Password = $config["rcon.password"] } if (-not $Port -or $Port -le 0) { $configuredPort = 0 if ([int]::TryParse([string]$config["rcon.port"], [ref]$configuredPort) -and $configuredPort -ge 1 -and $configuredPort -le 65535) { $Port = $configuredPort } } if (-not $Port -or $Port -le 0) { $Port = 27015 } if (-not $BindAddress) { $BindAddress = $config["rcon.bind_address"] } if (-not $BindAddress) { $BindAddress = "127.0.0.1" } if (-not $Password) { Write-Error "SCUM RCON password is not available for the local listener"; exit 3 } if (-not $Command) { Write-Error "SCUM RCON command text is required"; exit 4 } $client = New-Object System.Net.Sockets.TcpClient try { $connect = $client.BeginConnect($BindAddress, $Port, $null, $null) if (-not $connect.AsyncWaitHandle.WaitOne($TimeoutMs)) { Write-Error "SCUM RCON listener is not reachable on port $Port"; exit 5 } $client.EndConnect($connect) $client.ReceiveTimeout = $TimeoutMs $client.SendTimeout = $TimeoutMs $stream = $client.GetStream() Send-RconPacket -Stream $stream -Id 1 -Type 3 -Body $Password $auth = Receive-RconPacket -Stream $stream if ($auth.Id -ne 1) { Write-Error "SCUM RCON authentication response did not match the request"; exit 6 } Send-RconPacket -Stream $stream -Id 2 -Type 2 -Body $Command $response = Receive-RconPacket -Stream $stream if ($response.Body) { Write-Output $response.Body } exit 0 } catch { Write-Error ("SCUM RCON command failed: " + $_.Exception.Message) exit 7 } finally { $client.Close() }