Build/obs--shaders.build.ps1

<#
.SYNOPSIS
    Generates obs-powershell commands for PixelShaders
.DESCRIPTION
    Generates `*-OBS*Shader` commands, based off of pixel shaders.
.NOTES
    Most of the shaders come from the excellent [obs-shaderfilter](https://github.com/Exeldro/obs-shaderfilter) plugin.
    This plugin is required for any of these shader functions to work.

    This file should only build if the commit message matches "shader"
.LINK
    https://github.com/Exeldro/obs-shaderfilter
#>

[ValidatePattern("Shader")]
param()
#region Build Condition
$logOutput = git log -n 1 
$checkIfThisIsValid = $logOutput.CommitMessage ? $logOutput.CommitMessage : $logOutput -join [Environment]::Newline
foreach ($myAttribute in $MyInvocation.MyCommand.ScriptBlock.Attributes)  {
    if ($myAttribute.RegexPattern) {
        
        if ($env:GITHUB_STEP_SUMMARY) {
            @(
                "* $($MyInvocation.MyCommand.Name) has a Build Validation Pattern: (``$($myAttribute.RegexPattern)``)."
                "* $($MyInvocation.MyCommand.Name) Validating Commit: ``$checkIfThisIsValid``"
            ) -join [Environment]::Newline | Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
        }
        $myRegex = [Regex]::new($myAttribute.RegexPattern, $myAttribute.Options, '00:00:00.1')
        if (-not $myRegex.IsMatch($checkIfThisIsValid)) {
            if ($env:GITHUB_STEP_SUMMARY) {
                "* skipping $($MyInvocation.MyCommand.Name) because $checkIfThisIsValid did not match ($($myAttribute.RegexPattern))" | Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
            }
            Write-Warning "Skipping $($MyInvocation.MyCommand) :The last commit did not match $($myRegex)"
            return
        } 
    }
    elseif ($myAttribute -is [ValidateScript]) 
    {
        if ($env:GITHUB_STEP_SUMMARY) {
            "* $($MyInvocation.MyCommand.Name) has a Build Validation Script." | Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
        }
        $validationOutput = . $myAttribute.ScriptBlock $checkIfThisIsValid
        if (-not $validationOutput) {
            if ($env:GITHUB_STEP_SUMMARY) {
                "* Skipping $($MyInvocation.MyCommand.Name) because $($checkIfThisIsValid) did not meet the validation criteria:" | Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
                @(
                    "~~~PowerShell"
                    "$($myAttribute.ScriptBlock)"
                    "~~~"
                ) -join [Environment]::Newline | 
                    Out-File -Path $env:GITHUB_STEP_SUMMARY -Append                                 
            }
            Write-Warning "Skipping $($MyInvocation.MyCommand) : The $CheckIfThisIsValid was not valid"
            return
        }
    }
}
#endregion Build Condition

if ($env:GITHUB_STEP_SUMMARY) {
@"
### Shader build
"@
 | Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
}

#region Sparse cloning https://github.com/Exeldro/obs-shaderfilter.git
$CloneAndGetShaders = {
    @(
        $cloneArgs = @("--sparse","--no-checkout","--filter=tree:0", "https://github.com/Exeldro/obs-shaderfilter.git")
        $cloneOut = git clone @cloneArgs *>&1
        $sparseCheckoutRoot = (Join-Path $pwd "obs-shaderfilter")
        Push-Location $sparseCheckoutRoot
        git sparse-checkout set --no-cone '**.shader' '**.effect'
        $checkoutOut = git checkout
        Pop-Location
        Push-Location ($pwd | Split-Path)
        # Get shaders anywhere in the root repo (this will include the sparsely checked out repo)
        Get-ChildItem -Recurse -File | 
            Where-Object Extension -in '.shader','.effect' | 
            Where-Object { $_.Directory.Name -notin 'internal' }
        Pop-Location        
    )    
}

if ($env:GITHUB_STEP_SUMMARY) {
    "* Cloning Shaders with:
~~~PowerShell
$CloneAndGetShaders
~~~
"
 | Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
}

$SparselyClonedShaders = . $CloneAndGetShaders
#endregion Sparse cloning https://github.com/Exeldro/obs-shaderfilter.git

$parentPath = $PSScriptRoot | Split-Path

$ShaderFiles = $SparselyClonedShaders

if ($env:GITHUB_STEP_SUMMARY) {
"* [x] Found $($shaderFiles.Length) Shaders to Build" |
    Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
}    


$commandsPath = Join-Path $parentPath Commands
$ShaderCommandsPath = Join-Path $commandsPath Shaders

if (-not (Test-Path $ShaderCommandsPath))  {
    $null = New-Item -ItemType Directory -path $ShaderCommandsPath
}

$FindShaderParameters = '[^/]{0,}uniform\s{1,}(?<Type>\S+)\s{1,}(?<ParameterName>[\S-[\<\;]]+)'

$AllShaderParameters = $ShaderFiles | 
    Select-String $FindShaderParameters |
    Where-Object { "$_" -notlike "*//*"}
$ShaderParameters = $AllShaderParameters |
        Group-Object Path

if ($env:GITHUB_STEP_SUMMARY) {
    "* [x] Found $($AllShaderParameters.Length) Shader Parameters in $($ShaderParameters.Count) files" |
        Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
}

$importedModule = Import-Module $parentPath -Global -PassThru

if (-not $importedModule) {
    if ($env:GITHUB_STEP_SUMMARY) {
@"
**Could Not Import Module from $parentPath**
"@
 | Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
    }
        
    Write-Error "Could not import module"
    return
}


$underscoreWord  = "[\w-[_]]+_?"
$capitalizeNames = {
    param($match)
    $matchAsString = "$match"
    $matchAsString.Substring(0,1).ToUpper() + $matchAsString.Substring(1) -replace "_"
}

$FindAnnotations = [Regex]::new("$FindShaderParameters\<[\s\S]+?>",'IgnoreCase')
$generatingJobs  = @()

foreach ($shaderParameterSet in $ShaderParameters) {
    $shaderFileName = @($shaderParameterSet.Name.Split([IO.Path]::DirectorySeparatorChar))[-1]
    $shaderName = $shaderFileName -replace '\.(?>effect|shader)$'
    $ShaderNoun = "OBS" + ([Regex]::Replace($shaderName, $underscoreWord,$capitalizeNames) -replace '[\p{P}_\+]') + "Shader"
    if ($env:GITHUB_STEP_SUMMARY) {
        " * [x] Generating Shader from $shaderFileName ( $ShaderNoun )" |
            Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
    }
    $ShaderContent = [IO.File]::ReadAllText($shaderParameterSet.Name)
    $ShaderAnnotations = [Ordered]@{}
    $foundShaderAnnotations = @($FindAnnotations.Matches($ShaderContent))
    if ($env:GITHUB_STEP_SUMMARY) {
        " * [x] Found $($foundShaderAnnotations.Length) Shader annotations in $shaderFileName ( $ShaderNoun )" |
            Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
    }
    foreach ($shaderAnnotation in $FindAnnotations.Matches($ShaderContent)) {
        $null = $shaderAnnotation -match $FindAnnotations        
        $shaderAnnotations[$matches.'ParameterName'] = [Ordered]@{} + $matches
    }

    $ShaderParameters = [Ordered]@{}
    
    if ($env:GITHUB_STEP_SUMMARY) {
        " * [x] Found $(@($shaderParameterSet.Group).Length) Shader Parameters in $($shaderName)" |
            Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
    }

    foreach ($shaderParameterInSet in $shaderParameterSet.Group) {        
        $shaderMatch = "$(@("$($shaderParameterInSet)") -join '')" -match $FindShaderParameters
        $shaderMatch = [Ordered]@{} + $matches
        $shaderParameterSystemName = $shaderMatch.ParameterName

        if ($shaderParameterSystemName -match '[^\w_]') {
            if ($env:GITHUB_STEP_SUMMARY) {
                " * [ ] Shader Parameter $shaderParameterSystemName will be skipped due to improper naming" |
                    Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
            }
            continue
        }
        # Shader parameters can conflict with automatic parameters (we can sidestep this with a rename)
        $shaderParameterName = 
            switch ($shaderParameterSystemName) {
                debug {
                    "DebugShader"
                }
                default {
                    [Regex]::Replace($shaderParameterSystemName, $underscoreWord,$capitalizeNames)
                }        
            }
        
        if ($env:GITHUB_STEP_SUMMARY) {
            " * [x] Shader Parameter $shaderParameterSystemName will become $ShaderParameterName" |
                Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
        }

        $ShaderParameterHelp = "Set the $shaderParameterSystemName of $ShaderNoun"
        $ShaderParameterAttributes = @()

        # If there were annotations, pick them out.
        if ($shaderParameterInSet -like '*<') {            
            $annotationsForThisShader = $ShaderAnnotations[$shaderParameterSystemName]
            $annotationList = 
                $ShaderAnnotations[$shaderParameterSystemName].0 -split '[\<\>\;]' -notmatch '^\s{0,}$' | 
                Select-Object -Skip 1
            
            $ShaderMin, $ShaderMax = $null, $null

            foreach ($annotationItem in $annotationList) {
                $annotationItems = @($annotationItem -split '\s+' -ne '')
                $afterEquals = @($annotationItems | Select-Object -Skip 3) -join ' ' -replace '"'                        
                switch ($annotationItems[1]) {
                    label { 
                        $null = $null
                        $ShaderParameterHelp = $afterEquals
                    }
                    maximum {
                        $null = $null
                        $ShaderMax = $afterEquals -as [float]
                    }
                    minumum {
                        $ShaderMin = $afterEquals -as [float]
                    }
                }
            }

            if ($null -ne $ShaderMax -and $null -ne $ShaderMin) {
                $ShaderParameterAttributes += "[ValidateRange($ShaderMin, $ShaderMax)]"
            }
        }


        $shaderParameterType = $shaderMatch.Type
        
        $ShaderPowerShellParameterType = 
            switch ($shaderParameterType) {
                int { [int] }
                bool { [switch] }

                string { [string] }
                texture2d { [string] }
                
                float { [float] }                
                float2 { [float[]] }
                float3 { [float[]] }
                float4 { [string] <# float4 is usually a color #>}
                float4x4 { [float[][]]}

                default {                    
                    [PSObject]
                }
            }

        $ShaderParameters[$shaderParameterName] = [Ordered]@{
            ParameterName  = $shaderParameterName
            ParameterType  = $ShaderPowerShellParameterType
            Attribute = "[ComponentModel.DefaultBindingProperty('$ShaderParameterSystemName')]"
            Help = $ShaderParameterHelp                        
        }
        
        if ($shaderParameterSystemName -ne $shaderParameterName -and $shaderParameterSystemName -notin 'debug') {
            $ShaderParameters[$shaderParameterName].Alias = $shaderParameterSystemName
        }
    }

    $ShaderParameters["SourceName"] = [Ordered]@{
        Attribute = 'ValueFromPipelineByPropertyName'
        Alias = 'SceneItemName'
        ParameterType = [string]
        Help = "The name of the source. This must be provided when adding an item for the first time"
    }
    
    $ShaderParameters["FilterName"] = [Ordered]@{
        Attribute = 'ValueFromPipelineByPropertyName'
        ParameterType = [string]
        Help = "The name of the filter. If this is not provided, this will default to the shader name."
    }

    $ShaderParameters["ShaderText"] = [Ordered]@{
        ParameterName = "ShaderText"
        ParameterType = [string]
        Alias = "ShaderContent"
        Help = "The inline value of the shader. This will normally be provided as a default parameter, based off of the name."    
    }

    $ShaderParameters["Force"] = [Ordered]@{
        ParameterName = "Force"
        ParameterType = [switch]        
        Help = "If set, will force the recreation of a shader that already exists"            
    }

    $ShaderParameters["PassThru"] = [Ordered]@{
        ParameterName = "PassThru"
        ParameterType = [switch]        
        Help = "If set, will pass thru the commands that would be sent to OBS (these can be sent at any time with Send-OBS)"            
    }

    $ShaderParameters["NoResponse"] = [Ordered]@{
        ParameterName = "NoResponse"
        ParameterType = [switch]        
        Help = "If set, will not wait for a response from OBS (this will be faster, but will not return anything)"
    }

    $ShaderParameters["UseShaderTime"] = [Ordered]@{
        ParameterName = "UseShaderTime"
        ParameterType = [switch]
        Attributes = "[ComponentModel.DefaultBindingProperty('use_shader_elapsed_time')]"
        Help = "If set, use the shader elapsed time, instead of the OBS system elapsed time"
    }

    $ShaderProcess = [scriptblock]::Create(@"
`$shaderName = '$shaderName'
`$ShaderNoun = '$ShaderNoun'
if (-not `$psBoundParameters['ShaderText']) {
    `$psBoundParameters['ShaderText'] = `$ShaderText = '
$($ShaderContent -replace "'","''")
'
}
"@
 + {
$MyVerb, $myNoun = $MyInvocation.InvocationName -split '-',2
if (-not $myNoun) {
    $myNoun = $myVerb
    $myVerb = 'Get'    
}
switch -regex ($myVerb) {
    Get {
        $FilterNamePattern = "(?>$(
            if ($FilterName) {
                [Regex]::Escape($FilterName)
            }
            else {
                [Regex]::Escape($ShaderNoun -replace '^OBS' -replace 'Shader$'),[Regex]::Escape($shaderName) -join '|'
            }
        ))"

        if ($SourceName) {
            Get-OBSInput | 
                Where-Object InputName -eq $SourceName |
                Get-OBSSourceFilterList |
                Where-Object FilterName -Match $FilterNamePattern
        } else {
            $obs.Inputs |
                Get-OBSSourceFilterList |
                Where-Object FilterName -Match $FilterNamePattern
        }        
    }
    'Remove' {
        if ($SourceName) {
            Get-OBSInput | 
                Where-Object InputName -eq $SourceName |
                Get-OBSSourceFilterList |
                Where-Object FilterName -Match $FilterNamePattern |
                Remove-OBSSourceFilter
        }
    }
    '(?>Add|Set)' {
        $ShaderSettings = [Ordered]@{}
        :nextParameter foreach ($parameterMetadata in $MyInvocation.MyCommand.Parameters[@($psBoundParameters.Keys)]) {
            foreach ($parameterAttribute in $parameterMetadata.Attributes) {
                if ($parameterAttribute -isnot [ComponentModel.DefaultBindingPropertyAttribute]) { continue }
                $ShaderSettings[$parameterAttribute.Name] = $PSBoundParameters[$parameterMetadata.Name]
                if ($ShaderSettings[$parameterAttribute.Name] -is [switch]) {
                    $ShaderSettings[$parameterAttribute.Name] = $ShaderSettings[$parameterAttribute.Name] -as [bool]
                }
                continue nextParameter
            }            
        }

        if (-not $PSBoundParameters['FilterName']) {
            $filterName = $PSBoundParameters['FilterName'] = $shaderName
        }

        $ShaderFilterSplat = [Ordered]@{
            ShaderSetting = $ShaderSettings
            FilterName = $FilterName
            SourceName = $SourceName
        }        

        foreach ($CarryOnParameter in "PassThru", "NoResponse","Force") {
            if ($PSBoundParameters.ContainsKey($CarryOnParameter)) {
                $ShaderFilterSplat[$CarryOnParameter] = $PSBoundParameters[$CarryOnParameter]
            }
        }

        if (-not $script:CachedShaderFilesFromCommand) {
            $script:CachedShaderFilesFromCommand = @{}
        }

        if ($Home -and -not $script:CachedShaderFilesFromCommand[$shaderName]) {
            $MyObsPowerShellPath = Join-Path $home ".obs-powershell"
            $ThisShaderPath = Join-Path $MyObsPowerShellPath "$shaderName.shader"
            $shaderText | Set-Content -LiteralPath $ThisShaderPath
            $script:CachedShaderFilesFromCommand[$shaderName] = Get-Item -LiteralPath $ThisShaderPath
        }
        if ($script:CachedShaderFilesFromCommand[$shaderName]) {
            $ShaderFilterSplat.ShaderFile = $script:CachedShaderFilesFromCommand[$shaderName].FullName
        } else {
            $ShaderFilterSplat.ShaderText = $shaderText
        }        

        if ($myVerb -eq 'Add') {                        
            Add-OBSShaderFilter @ShaderFilterSplat
        } else {
            Set-OBSShaderFilter @ShaderFilterSplat
        }
    }
}
})
    
    

    $NewPipeScriptSplat = [Ordered]@{}
    $NewPipeScriptSplat.FunctionName = "Get-$ShaderNoun"
    $NewPipeScriptSplat.Parameter = $ShaderParameters
    $NewPipeScriptSplat.Alias = "Set-$ShaderNoun", "Add-$ShaderNoun"
    $NewPipeScriptSplat.OutputPath = (Join-Path $ShaderCommandsPath "Get-$ShaderNoun.ps1")
    $NewPipeScriptSplat.Process = $ShaderProcess
    New-PipeScript @NewPipeScriptSplat
}

if (Test-Path "obs-shaderfilter") {
    Remove-Item -Recurse -Force -Path "obs-shaderfilter"
}

$shaderReadme = Join-Path $ShaderCommandsPath "README.md"
New-Item -Path $shaderReadme -Value @'
## obs-powershell Shader Commands

This folder contains the generated commands for the shaders in the [obs-shaderfilter plugin](https://github.com/exeldro/obs-shaderfilter/).
'@
 -Force


trap [Exception] {
if ($env:GITHUB_STEP_SUMMARY) {
@"
❗❗ Trapped Error!

ShaderName: ``$ShaderName``
~~~
$($_ | Out-String)
~~~
"@
 | Out-File -Path $env:GITHUB_STEP_SUMMARY -Append
}        
    continue
}