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
|
||||
|
||||
Reference in New Issue
Block a user