Updating
This commit is contained in:
@@ -128,7 +128,7 @@ public sealed class Cs2ScriptedRenderBackend : IClipRenderBackend
|
||||
return _options.RenderWorker.Cs2.Arguments;
|
||||
}
|
||||
|
||||
return "-ExecutionPolicy Bypass -File \"{wrapperScriptPath}\" -DemoPath \"{demoPath}\" -OutputPath \"{outputPath}\" -StartTick {startTick} -EndTick {endTick} -CameraMode \"{cameraMode}\" -Resolution \"{resolution}\" -Fps {fps} -ArtifactsDirectory \"{artifactsDirectory}\"";
|
||||
return "-ExecutionPolicy Bypass -File \"{wrapperScriptPath}\" -DemoPath \"{demoPath}\" -OutputPath \"{outputPath}\" -StartTick {startTick} -EndTick {endTick} -TickRate {tickRate} -CameraMode \"{cameraMode}\" -Resolution \"{resolution}\" -Fps {fps} -ArtifactsDirectory \"{artifactsDirectory}\"";
|
||||
}
|
||||
|
||||
private string ResolveWorkingDirectory()
|
||||
@@ -171,6 +171,7 @@ public sealed class Cs2ScriptedRenderBackend : IClipRenderBackend
|
||||
.Replace("{outputPath}", outputPath, StringComparison.Ordinal)
|
||||
.Replace("{startTick}", renderJob.StartTick.ToString(), StringComparison.Ordinal)
|
||||
.Replace("{endTick}", renderJob.EndTick.ToString(), StringComparison.Ordinal)
|
||||
.Replace("{tickRate}", renderJob.TickRate.ToString(), StringComparison.Ordinal)
|
||||
.Replace("{cameraMode}", renderJob.CameraMode, StringComparison.Ordinal)
|
||||
.Replace("{resolution}", renderJob.Resolution, StringComparison.Ordinal)
|
||||
.Replace("{fps}", renderJob.Fps, StringComparison.Ordinal)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"RetryDelaySeconds": 30,
|
||||
"Cs2": {
|
||||
"ExecutablePath": "powershell.exe",
|
||||
"Arguments": "-ExecutionPolicy Bypass -File \"{wrapperScriptPath}\" -DemoPath \"{demoPath}\" -OutputPath \"{outputPath}\" -StartTick {startTick} -EndTick {endTick} -CameraMode \"{cameraMode}\" -Resolution \"{resolution}\" -Fps {fps} -ArtifactsDirectory \"{artifactsDirectory}\"",
|
||||
"Arguments": "-ExecutionPolicy Bypass -File \"{wrapperScriptPath}\" -DemoPath \"{demoPath}\" -OutputPath \"{outputPath}\" -StartTick {startTick} -EndTick {endTick} -TickRate {tickRate} -CameraMode \"{cameraMode}\" -Resolution \"{resolution}\" -Fps {fps} -ArtifactsDirectory \"{artifactsDirectory}\"",
|
||||
"WorkingDirectory": "",
|
||||
"TimeoutSeconds": 900,
|
||||
"ArtifactsDirectory": "data/render-artifacts",
|
||||
|
||||
@@ -11,6 +11,9 @@ param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$EndTick,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[int]$TickRate = 64,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CameraMode,
|
||||
|
||||
@@ -34,67 +37,380 @@ param(
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
function Resolve-OptionalPath {
|
||||
param([string]$PathValue)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($PathValue)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PathValue)
|
||||
}
|
||||
|
||||
function Write-Log {
|
||||
param(
|
||||
[string]$Message,
|
||||
[string]$LogPath
|
||||
)
|
||||
|
||||
$timestamp = [DateTime]::UtcNow.ToString("O")
|
||||
Add-Content -LiteralPath $LogPath -Value "[$timestamp] $Message"
|
||||
}
|
||||
|
||||
function Invoke-ExternalCommand {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[string]$Arguments,
|
||||
[string]$WorkingDirectory,
|
||||
[string]$LogPath,
|
||||
[switch]$Optional
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($FilePath)) {
|
||||
if ($Optional) {
|
||||
Write-Log "Skipping optional command because no executable was provided." $LogPath
|
||||
return
|
||||
}
|
||||
|
||||
throw "Command executable path was not provided."
|
||||
}
|
||||
|
||||
$workingDirectoryToUse = if ([string]::IsNullOrWhiteSpace($WorkingDirectory)) {
|
||||
Split-Path -Parent $FilePath
|
||||
}
|
||||
else {
|
||||
$WorkingDirectory
|
||||
}
|
||||
|
||||
$stdoutPath = [System.IO.Path]::GetTempFileName()
|
||||
$stderrPath = [System.IO.Path]::GetTempFileName()
|
||||
|
||||
try {
|
||||
Write-Log "Running: $FilePath $Arguments" $LogPath
|
||||
$process = Start-Process `
|
||||
-FilePath $FilePath `
|
||||
-ArgumentList $Arguments `
|
||||
-WorkingDirectory $workingDirectoryToUse `
|
||||
-RedirectStandardOutput $stdoutPath `
|
||||
-RedirectStandardError $stderrPath `
|
||||
-PassThru `
|
||||
-Wait
|
||||
|
||||
foreach ($line in Get-Content -LiteralPath $stdoutPath -ErrorAction SilentlyContinue) {
|
||||
Write-Log "$line" $LogPath
|
||||
}
|
||||
|
||||
foreach ($line in Get-Content -LiteralPath $stderrPath -ErrorAction SilentlyContinue) {
|
||||
Write-Log "$line" $LogPath
|
||||
}
|
||||
|
||||
if ($process.ExitCode -ne 0 -and -not $Optional) {
|
||||
throw "Command failed with exit code $($process.ExitCode): $FilePath $Arguments"
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-TemplateCommand {
|
||||
param(
|
||||
[string]$Template,
|
||||
[hashtable]$Context,
|
||||
[string]$LogPath,
|
||||
[switch]$Optional
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Template)) {
|
||||
if ($Optional) {
|
||||
Write-Log "Skipping optional templated command because no template was configured." $LogPath
|
||||
return
|
||||
}
|
||||
|
||||
throw "Required templated command was not configured."
|
||||
}
|
||||
|
||||
$expanded = $Template
|
||||
foreach ($key in $Context.Keys) {
|
||||
$expanded = $expanded.Replace("{$key}", [string]$Context[$key], [System.StringComparison]::Ordinal)
|
||||
}
|
||||
|
||||
Write-Log "Executing templated PowerShell command: $expanded" $LogPath
|
||||
Invoke-Expression $expanded 2>&1 | ForEach-Object {
|
||||
Write-Log "$_" $LogPath
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0 -and -not $Optional) {
|
||||
throw "Templated command failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
function Parse-Resolution {
|
||||
param([string]$ResolutionValue)
|
||||
|
||||
if ($ResolutionValue -notmatch '^(?<width>\d+)x(?<height>\d+)$') {
|
||||
throw "Resolution must use WIDTHxHEIGHT format, got '$ResolutionValue'."
|
||||
}
|
||||
|
||||
return @{
|
||||
Width = [int]$Matches.width
|
||||
Height = [int]$Matches.height
|
||||
}
|
||||
}
|
||||
|
||||
function Get-EnvDouble {
|
||||
param(
|
||||
[string]$Name,
|
||||
[double]$Fallback
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace((Get-Item "env:$Name" -ErrorAction SilentlyContinue).Value)) {
|
||||
return $Fallback
|
||||
}
|
||||
|
||||
$value = (Get-Item "env:$Name").Value
|
||||
$parsed = 0.0
|
||||
if ([double]::TryParse($value, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$parsed)) {
|
||||
return $parsed
|
||||
}
|
||||
|
||||
return $Fallback
|
||||
}
|
||||
|
||||
function Write-CfgFile {
|
||||
param(
|
||||
[string]$CfgPath,
|
||||
[string]$DemoAbsolutePath,
|
||||
[int]$StartTickValue,
|
||||
[int]$PrerollTicks,
|
||||
[string]$SafeCameraMode
|
||||
)
|
||||
|
||||
$demoForCfg = $DemoAbsolutePath.Replace('\', '/')
|
||||
$gotoTick = [Math]::Max(0, $StartTickValue - $PrerollTicks)
|
||||
|
||||
@(
|
||||
"echo ""[Fragalytics] render bootstrap started"""
|
||||
"sv_cheats 1"
|
||||
"mirv_deathmsg lifetime 0"
|
||||
"spec_show_xray 0"
|
||||
"demo_pause"
|
||||
"playdemo ""$demoForCfg"""
|
||||
"demo_goto $gotoTick"
|
||||
"spec_mode 4"
|
||||
"echo ""[Fragalytics] camera mode requested: $SafeCameraMode"""
|
||||
"demo_resume"
|
||||
) | Set-Content -LiteralPath $CfgPath -Encoding ASCII
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $DemoPath)) {
|
||||
throw "Demo file was not found: $DemoPath"
|
||||
}
|
||||
|
||||
$outputDirectory = Split-Path -Parent $OutputPath
|
||||
if ($EndTick -le $StartTick) {
|
||||
throw "EndTick must be greater than StartTick."
|
||||
}
|
||||
|
||||
if ($TickRate -le 0) {
|
||||
throw "TickRate must be greater than zero."
|
||||
}
|
||||
|
||||
if ($Fps -le 0) {
|
||||
throw "Fps must be greater than zero."
|
||||
}
|
||||
|
||||
$resolutionParts = Parse-Resolution $Resolution.ToLowerInvariant()
|
||||
$demoAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($DemoPath)
|
||||
$outputAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
|
||||
|
||||
$outputDirectory = Split-Path -Parent $outputAbsolutePath
|
||||
if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) {
|
||||
New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ArtifactsDirectory)) {
|
||||
New-Item -ItemType Directory -Force -Path $ArtifactsDirectory | Out-Null
|
||||
}
|
||||
|
||||
$manifestPath = if ([string]::IsNullOrWhiteSpace($ArtifactsDirectory)) {
|
||||
[System.IO.Path]::ChangeExtension($OutputPath, ".render.json")
|
||||
$artifactsAbsolutePath = if ([string]::IsNullOrWhiteSpace($ArtifactsDirectory)) {
|
||||
Split-Path -Parent $outputAbsolutePath
|
||||
}
|
||||
else {
|
||||
Join-Path $ArtifactsDirectory "render-manifest.json"
|
||||
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ArtifactsDirectory)
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $artifactsAbsolutePath | Out-Null
|
||||
|
||||
$logPath = Join-Path $artifactsAbsolutePath "render-wrapper.log"
|
||||
$manifestPath = Join-Path $artifactsAbsolutePath "render-manifest.json"
|
||||
$cfgPath = Join-Path $artifactsAbsolutePath "fragalytics-render.cfg"
|
||||
$copyCfgPath = Join-Path $artifactsAbsolutePath "copy-render-cfg.cmd"
|
||||
$launchHintPath = Join-Path $artifactsAbsolutePath "launch-cs2.cmd"
|
||||
$steamPath = Resolve-OptionalPath $SteamPath
|
||||
$cs2Path = Resolve-OptionalPath $Cs2Path
|
||||
$obsCliPath = Resolve-OptionalPath $ObsCliPath
|
||||
|
||||
Set-Content -LiteralPath $logPath -Value ""
|
||||
Write-Log "Preparing scripted CS2 render workflow." $logPath
|
||||
|
||||
$tickWindow = $EndTick - $StartTick
|
||||
$clipDurationSeconds = [Math]::Ceiling([double]$tickWindow / [double]$TickRate)
|
||||
$prerollTicks = [Math]::Min([Math]::Max([int]($TickRate * 2), 64), $StartTick)
|
||||
$postrollSeconds = Get-EnvDouble -Name "FRAGALYTICS_RENDER_POSTROLL_SECONDS" -Fallback 1.0
|
||||
$warmupSeconds = Get-EnvDouble -Name "FRAGALYTICS_RENDER_WARMUP_SECONDS" -Fallback 8.0
|
||||
$captureLeadInSeconds = Get-EnvDouble -Name "FRAGALYTICS_RENDER_CAPTURE_LEAD_IN_SECONDS" -Fallback 1.0
|
||||
$shutdownGraceSeconds = Get-EnvDouble -Name "FRAGALYTICS_RENDER_SHUTDOWN_GRACE_SECONDS" -Fallback 5.0
|
||||
$captureDurationSeconds = [Math]::Max(1.0, $clipDurationSeconds + $postrollSeconds)
|
||||
$outputExtension = [System.IO.Path]::GetExtension($outputAbsolutePath)
|
||||
$captureStartTemplate = $env:FRAGALYTICS_RENDER_CAPTURE_START_COMMAND
|
||||
$captureStopTemplate = $env:FRAGALYTICS_RENDER_CAPTURE_STOP_COMMAND
|
||||
$prepareTemplate = $env:FRAGALYTICS_RENDER_PREPARE_COMMAND
|
||||
$postProcessTemplate = $env:FRAGALYTICS_RENDER_POSTPROCESS_COMMAND
|
||||
$launchArgsTemplate = if ([string]::IsNullOrWhiteSpace($env:FRAGALYTICS_CS2_LAUNCH_ARGS)) {
|
||||
'-allow_third_party_software -insecure -novid -console -windowed -w {width} -h {height} +exec "{cfgPath}"'
|
||||
}
|
||||
else {
|
||||
$env:FRAGALYTICS_CS2_LAUNCH_ARGS
|
||||
}
|
||||
|
||||
Write-CfgFile -CfgPath $cfgPath -DemoAbsolutePath $demoAbsolutePath -StartTickValue $StartTick -PrerollTicks $prerollTicks -SafeCameraMode $CameraMode
|
||||
|
||||
$launchContext = @{
|
||||
artifactsDirectory = $artifactsAbsolutePath
|
||||
cfgPath = $cfgPath
|
||||
cs2Path = $cs2Path
|
||||
demoPath = $demoAbsolutePath
|
||||
endTick = $EndTick
|
||||
fps = $Fps
|
||||
height = $resolutionParts.Height
|
||||
logPath = $logPath
|
||||
obsCliPath = $obsCliPath
|
||||
outputExtension = $outputExtension
|
||||
outputPath = $outputAbsolutePath
|
||||
resolution = $Resolution.ToLowerInvariant()
|
||||
startTick = $StartTick
|
||||
steamPath = $steamPath
|
||||
tickRate = $TickRate
|
||||
warmupSeconds = $warmupSeconds
|
||||
captureLeadInSeconds = $captureLeadInSeconds
|
||||
captureDurationSeconds = $captureDurationSeconds
|
||||
shutdownGraceSeconds = $shutdownGraceSeconds
|
||||
width = $resolutionParts.Width
|
||||
}
|
||||
|
||||
$expandedLaunchArgs = $launchArgsTemplate
|
||||
foreach ($key in $launchContext.Keys) {
|
||||
$expandedLaunchArgs = $expandedLaunchArgs.Replace("{$key}", [string]$launchContext[$key], [System.StringComparison]::Ordinal)
|
||||
}
|
||||
|
||||
$manifest = [ordered]@{
|
||||
demoPath = $DemoPath
|
||||
outputPath = $OutputPath
|
||||
demoPath = $demoAbsolutePath
|
||||
outputPath = $outputAbsolutePath
|
||||
startTick = $StartTick
|
||||
endTick = $EndTick
|
||||
tickRate = $TickRate
|
||||
cameraMode = $CameraMode
|
||||
resolution = $Resolution
|
||||
resolution = $Resolution.ToLowerInvariant()
|
||||
fps = $Fps
|
||||
cs2Path = $Cs2Path
|
||||
steamPath = $SteamPath
|
||||
obsCliPath = $ObsCliPath
|
||||
clipDurationSeconds = $clipDurationSeconds
|
||||
captureDurationSeconds = $captureDurationSeconds
|
||||
prerollTicks = $prerollTicks
|
||||
warmupSeconds = $warmupSeconds
|
||||
captureLeadInSeconds = $captureLeadInSeconds
|
||||
shutdownGraceSeconds = $shutdownGraceSeconds
|
||||
cs2Path = $cs2Path
|
||||
steamPath = $steamPath
|
||||
obsCliPath = $obsCliPath
|
||||
launchArguments = $expandedLaunchArgs
|
||||
cfgPath = $cfgPath
|
||||
prepareCommand = $prepareTemplate
|
||||
captureStartCommand = $captureStartTemplate
|
||||
captureStopCommand = $captureStopTemplate
|
||||
postProcessCommand = $postProcessTemplate
|
||||
createdAtUtc = [DateTime]::UtcNow.ToString("O")
|
||||
}
|
||||
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
|
||||
|
||||
$manifest | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
|
||||
@(
|
||||
"@echo off"
|
||||
"copy /Y ""$cfgPath"" ""%USERPROFILE%\game\csgo\cfg\fragalytics-render.cfg"""
|
||||
) | Set-Content -LiteralPath $copyCfgPath -Encoding ASCII
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Cs2Path)) {
|
||||
throw "FRAGALYTICS_CS2_PATH or -Cs2Path is not configured. Review $manifestPath and update render-cs2-clip.ps1 with your capture workflow."
|
||||
@(
|
||||
"@echo off"
|
||||
"""$cs2Path"" $expandedLaunchArgs"
|
||||
) | Set-Content -LiteralPath $launchHintPath -Encoding ASCII
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($cs2Path)) {
|
||||
throw "FRAGALYTICS_CS2_PATH or -Cs2Path is not configured. Review $manifestPath."
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Cs2Path)) {
|
||||
throw "Configured CS2 executable was not found: $Cs2Path"
|
||||
if (-not (Test-Path -LiteralPath $cs2Path)) {
|
||||
throw "Configured CS2 executable was not found: $cs2Path"
|
||||
}
|
||||
|
||||
throw @"
|
||||
CS2 scripted wrapper was invoked successfully, but the actual capture workflow is not implemented yet.
|
||||
if (-not [string]::IsNullOrWhiteSpace($prepareTemplate)) {
|
||||
Invoke-TemplateCommand -Template $prepareTemplate -Context $launchContext -LogPath $logPath
|
||||
}
|
||||
|
||||
Edit:
|
||||
$PSScriptRoot\render-cs2-clip.ps1
|
||||
$captureStarted = $false
|
||||
$cs2Process = $null
|
||||
try {
|
||||
Write-Log "Starting CS2 process." $logPath
|
||||
$cs2Process = Start-Process -FilePath $cs2Path -ArgumentList $expandedLaunchArgs -WorkingDirectory (Split-Path -Parent $cs2Path) -PassThru
|
||||
Write-Log "CS2 process id: $($cs2Process.Id)" $logPath
|
||||
|
||||
Inputs captured in:
|
||||
Start-Sleep -Seconds $warmupSeconds
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($captureStartTemplate)) {
|
||||
Invoke-TemplateCommand -Template $captureStartTemplate -Context $launchContext -LogPath $logPath
|
||||
$captureStarted = $true
|
||||
}
|
||||
elseif (-not [string]::IsNullOrWhiteSpace($obsCliPath)) {
|
||||
Invoke-ExternalCommand -FilePath $obsCliPath -Arguments "recording start" -WorkingDirectory $artifactsAbsolutePath -LogPath $logPath
|
||||
$captureStarted = $true
|
||||
}
|
||||
else {
|
||||
Write-Log "No capture start command was configured. The wrapper will launch CS2 and wait, but capture must be handled externally." $logPath
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $captureLeadInSeconds
|
||||
Write-Log "Waiting $captureDurationSeconds seconds for the clip window to play." $logPath
|
||||
Start-Sleep -Seconds $captureDurationSeconds
|
||||
|
||||
if ($captureStarted) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($captureStopTemplate)) {
|
||||
Invoke-TemplateCommand -Template $captureStopTemplate -Context $launchContext -LogPath $logPath -Optional
|
||||
}
|
||||
elseif (-not [string]::IsNullOrWhiteSpace($obsCliPath)) {
|
||||
Invoke-ExternalCommand -FilePath $obsCliPath -Arguments "recording stop" -WorkingDirectory $artifactsAbsolutePath -LogPath $logPath -Optional
|
||||
}
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($postProcessTemplate)) {
|
||||
Invoke-TemplateCommand -Template $postProcessTemplate -Context $launchContext -LogPath $logPath
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $outputAbsolutePath)) {
|
||||
throw @"
|
||||
No output file was produced at:
|
||||
$outputAbsolutePath
|
||||
|
||||
Review:
|
||||
$manifestPath
|
||||
$logPath
|
||||
$cfgPath
|
||||
|
||||
Expected output:
|
||||
$OutputPath
|
||||
|
||||
Recommended next automation step:
|
||||
1. Launch CS2 with your preferred startup flags
|
||||
2. Load the demo at $DemoPath
|
||||
3. Seek near tick $StartTick and capture until $EndTick
|
||||
4. Encode to $OutputPath
|
||||
5. Exit 0 only if the output file exists
|
||||
Common fixes:
|
||||
- set FRAGALYTICS_RENDER_CAPTURE_START_COMMAND and FRAGALYTICS_RENDER_CAPTURE_STOP_COMMAND
|
||||
- or point FRAGALYTICS_OBS_CLI_PATH at obs-cli
|
||||
- update FRAGALYTICS_CS2_LAUNCH_ARGS so CS2 loads the generated cfg
|
||||
"@
|
||||
}
|
||||
|
||||
Write-Log "Render completed successfully." $logPath
|
||||
}
|
||||
finally {
|
||||
if ($cs2Process -and -not $cs2Process.HasExited) {
|
||||
Write-Log "Stopping CS2 process." $logPath
|
||||
Stop-Process -Id $cs2Process.Id -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds $shutdownGraceSeconds
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ public sealed class RenderJobContract
|
||||
public const string DefaultOutputFormat = "mp4";
|
||||
public const string DefaultResolution = "1920x1080";
|
||||
public const string DefaultFps = "60";
|
||||
public const int DefaultTickRate = 64;
|
||||
|
||||
public string Version { get; set; } = CurrentVersion;
|
||||
public int DemoId { get; set; }
|
||||
@@ -198,13 +199,24 @@ public sealed class RenderJobContract
|
||||
public int SourceEventId { get; set; }
|
||||
public int StartTick { get; set; }
|
||||
public int EndTick { get; set; }
|
||||
public int TickRate { get; set; } = DefaultTickRate;
|
||||
public string CameraMode { get; set; } = DefaultCameraMode;
|
||||
public string OutputFormat { get; set; } = DefaultOutputFormat;
|
||||
public string Resolution { get; set; } = DefaultResolution;
|
||||
public string Fps { get; set; } = DefaultFps;
|
||||
|
||||
public void Normalize()
|
||||
{
|
||||
Version = string.IsNullOrWhiteSpace(Version) ? CurrentVersion : Version.Trim();
|
||||
CameraMode = NormalizeString(CameraMode, DefaultCameraMode);
|
||||
OutputFormat = NormalizeString(OutputFormat, DefaultOutputFormat).ToLowerInvariant();
|
||||
Resolution = NormalizeString(Resolution, DefaultResolution).ToLowerInvariant();
|
||||
Fps = NormalizePositiveNumber(Fps, DefaultFps);
|
||||
}
|
||||
|
||||
public Dictionary<string, string> ToPayload()
|
||||
{
|
||||
Normalize();
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
["version"] = Version,
|
||||
@@ -214,6 +226,7 @@ public sealed class RenderJobContract
|
||||
["sourceEventId"] = SourceEventId.ToString(),
|
||||
["startTick"] = StartTick.ToString(),
|
||||
["endTick"] = EndTick.ToString(),
|
||||
["tickRate"] = TickRate.ToString(),
|
||||
["cameraMode"] = CameraMode,
|
||||
["outputFormat"] = OutputFormat,
|
||||
["resolution"] = Resolution,
|
||||
@@ -223,7 +236,7 @@ public sealed class RenderJobContract
|
||||
|
||||
public static RenderJobContract FromPayload(IReadOnlyDictionary<string, string> payload)
|
||||
{
|
||||
return new RenderJobContract
|
||||
var contract = new RenderJobContract
|
||||
{
|
||||
Version = ReadString(payload, "version", CurrentVersion),
|
||||
DemoId = ReadInt(payload, "demoId"),
|
||||
@@ -232,11 +245,15 @@ public sealed class RenderJobContract
|
||||
SourceEventId = ReadInt(payload, "sourceEventId"),
|
||||
StartTick = ReadInt(payload, "startTick"),
|
||||
EndTick = ReadInt(payload, "endTick"),
|
||||
TickRate = ReadInt(payload, "tickRate", DefaultTickRate),
|
||||
CameraMode = ReadString(payload, "cameraMode", DefaultCameraMode),
|
||||
OutputFormat = ReadString(payload, "outputFormat", DefaultOutputFormat),
|
||||
Resolution = ReadString(payload, "resolution", DefaultResolution),
|
||||
Fps = ReadString(payload, "fps", DefaultFps)
|
||||
};
|
||||
|
||||
contract.Normalize();
|
||||
return contract;
|
||||
}
|
||||
|
||||
public string GetSuggestedFileExtension()
|
||||
@@ -244,15 +261,27 @@ public sealed class RenderJobContract
|
||||
return ClipMediaFormats.GetSuggestedFileExtension(OutputFormat);
|
||||
}
|
||||
|
||||
private static int ReadInt(IReadOnlyDictionary<string, string> payload, string key)
|
||||
private static int ReadInt(IReadOnlyDictionary<string, string> payload, string key, int fallback = 0)
|
||||
{
|
||||
return payload.TryGetValue(key, out var raw) && int.TryParse(raw, out var value) ? value : 0;
|
||||
return payload.TryGetValue(key, out var raw) && int.TryParse(raw, out var value) ? value : fallback;
|
||||
}
|
||||
|
||||
private static string ReadString(IReadOnlyDictionary<string, string> payload, string key, string fallback)
|
||||
{
|
||||
return payload.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value) ? value : fallback;
|
||||
}
|
||||
|
||||
private static string NormalizeString(string? value, string fallback)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
|
||||
}
|
||||
|
||||
private static string NormalizePositiveNumber(string? value, string fallback)
|
||||
{
|
||||
return int.TryParse(value, out var parsed) && parsed > 0
|
||||
? parsed.ToString()
|
||||
: fallback;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ClipAssetRecord
|
||||
|
||||
@@ -50,8 +50,36 @@
|
||||
<strong>@row.ClipJob.AttemptCount</strong>
|
||||
</div>
|
||||
<div class="explanation-row">
|
||||
<span>Payload</span>
|
||||
<code>@string.Join(", ", row.ClipJob.Payload.Select(item => $"{item.Key}={item.Value}"))</code>
|
||||
<span>Highlight</span>
|
||||
<strong>#@row.RenderJob.HighlightId</strong>
|
||||
</div>
|
||||
<div class="explanation-row">
|
||||
<span>Demo / Match</span>
|
||||
<strong>#@row.RenderJob.DemoId / #@row.RenderJob.MatchId</strong>
|
||||
</div>
|
||||
<div class="explanation-row">
|
||||
<span>Camera</span>
|
||||
<code>@row.RenderJob.CameraMode</code>
|
||||
</div>
|
||||
<div class="explanation-row">
|
||||
<span>Output</span>
|
||||
<code>@row.RenderJob.OutputFormat</code>
|
||||
</div>
|
||||
<div class="explanation-row">
|
||||
<span>Resolution</span>
|
||||
<code>@row.RenderJob.Resolution</code>
|
||||
</div>
|
||||
<div class="explanation-row">
|
||||
<span>FPS</span>
|
||||
<code>@row.RenderJob.Fps</code>
|
||||
</div>
|
||||
<div class="explanation-row">
|
||||
<span>Tick Rate</span>
|
||||
<code>@row.RenderJob.TickRate</code>
|
||||
</div>
|
||||
<div class="explanation-row">
|
||||
<span>Contract</span>
|
||||
<code>[email protected]</code>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(row.ClipJob.ErrorMessage))
|
||||
{
|
||||
|
||||
@@ -80,6 +80,7 @@ public sealed class QueryService
|
||||
.Select(job => new ClipJobWithHighlightViewModel
|
||||
{
|
||||
ClipJob = job,
|
||||
RenderJob = RenderJobContract.FromPayload(job.Payload),
|
||||
Highlight = state.Highlights.FirstOrDefault(highlight => highlight.Id == job.HighlightId),
|
||||
ClipAsset = state.ClipAssets.FirstOrDefault(asset => asset.Id == job.ClipAssetId)
|
||||
})
|
||||
@@ -204,6 +205,7 @@ public sealed class HighlightDetailsRow
|
||||
public sealed class ClipJobWithHighlightViewModel
|
||||
{
|
||||
public ClipJobRecord ClipJob { get; set; } = new();
|
||||
public RenderJobContract RenderJob { get; set; } = new();
|
||||
public HighlightRecord? Highlight { get; set; }
|
||||
public ClipAssetRecord? ClipAsset { get; set; }
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ public sealed class MatchScoringService
|
||||
HighlightId = highlight.Id,
|
||||
SourceEventId = candidate.Id,
|
||||
StartTick = highlight.StartTick,
|
||||
EndTick = highlight.EndTick
|
||||
EndTick = highlight.EndTick,
|
||||
TickRate = match.TickRate.GetValueOrDefault(RenderJobContract.DefaultTickRate)
|
||||
};
|
||||
|
||||
var clipJob = new ClipJobRecord
|
||||
|
||||
@@ -214,11 +214,12 @@ static async Task RunFlowTestAsync()
|
||||
Assert(state.ClipJobs[0].Payload.TryGetValue("version", out var version) && version == RenderJobContract.CurrentVersion, "Expected versioned render payload.");
|
||||
Assert(state.ClipJobs[0].Payload.TryGetValue("demoId", out var demoIdPayload) && demoIdPayload == "1", "Expected demo id in render payload.");
|
||||
Assert(state.ClipJobs[0].Payload.ContainsKey("cameraMode"), "Expected render payload defaults on clip job.");
|
||||
Assert(state.ClipJobs[0].Payload.Keys.All(key => key is "version" or "demoId" or "matchId" or "highlightId" or "sourceEventId" or "startTick" or "endTick" or "cameraMode" or "outputFormat" or "resolution" or "fps"), "Expected payload to contain only render contract fields.");
|
||||
Assert(state.ClipJobs[0].Payload.Keys.All(key => key is "version" or "demoId" or "matchId" or "highlightId" or "sourceEventId" or "startTick" or "endTick" or "tickRate" or "cameraMode" or "outputFormat" or "resolution" or "fps"), "Expected payload to contain only render contract fields.");
|
||||
|
||||
var renderJob = RenderJobContract.FromPayload(state.ClipJobs[0].Payload);
|
||||
Assert(renderJob.MatchId == state.Matches[0].Id, "Expected match id in typed render payload.");
|
||||
Assert(renderJob.OutputFormat == RenderJobContract.DefaultOutputFormat, "Expected default output format.");
|
||||
Assert(renderJob.TickRate == state.Matches[0].TickRate, "Expected match tick rate in typed render payload.");
|
||||
|
||||
await ingest.RescoreAllAsync(CancellationToken.None);
|
||||
state = await store.GetSnapshotAsync();
|
||||
@@ -317,10 +318,33 @@ static void RunRenderJobContractDefaultsTest()
|
||||
Assert(contract.OutputFormat == RenderJobContract.DefaultOutputFormat, "Expected default output format.");
|
||||
Assert(contract.Resolution == RenderJobContract.DefaultResolution, "Expected default resolution.");
|
||||
Assert(contract.Fps == RenderJobContract.DefaultFps, "Expected default fps.");
|
||||
Assert(contract.TickRate == RenderJobContract.DefaultTickRate, "Expected default tick rate.");
|
||||
Assert(contract.GetSuggestedFileExtension() == ClipMediaFormats.Mp4Extension, "Expected mp4 extension for default output format.");
|
||||
|
||||
contract.OutputFormat = "webm";
|
||||
Assert(contract.GetSuggestedFileExtension() == ClipMediaFormats.WebMExtension, "Expected webm extension override.");
|
||||
|
||||
var messyPayload = new Dictionary<string, string>
|
||||
{
|
||||
["demoId"] = "9",
|
||||
["matchId"] = "17",
|
||||
["highlightId"] = "23",
|
||||
["sourceEventId"] = "42",
|
||||
["startTick"] = "1500",
|
||||
["endTick"] = "1800",
|
||||
["tickRate"] = "128",
|
||||
["cameraMode"] = " killer_pov ",
|
||||
["outputFormat"] = " WEBM ",
|
||||
["resolution"] = " 1280X720 ",
|
||||
["fps"] = "0"
|
||||
};
|
||||
|
||||
var normalized = RenderJobContract.FromPayload(messyPayload);
|
||||
Assert(normalized.CameraMode == RenderJobContract.DefaultCameraMode, "Expected camera mode to be trimmed.");
|
||||
Assert(normalized.OutputFormat == "webm", "Expected output format to be normalized.");
|
||||
Assert(normalized.Resolution == "1280x720", "Expected resolution to be normalized.");
|
||||
Assert(normalized.Fps == RenderJobContract.DefaultFps, "Expected invalid fps to fall back to the default.");
|
||||
Assert(normalized.TickRate == 128, "Expected tick rate to survive normalization.");
|
||||
}
|
||||
|
||||
static async Task RunRescoreReplacementTestAsync()
|
||||
|
||||
Reference in New Issue
Block a user