From 2165429724d166bf5b6bba70ec85a0e4f0e9d378 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 13 Oct 2026 18:04:34 +0200 Subject: [PATCH 01/26] plugins: add player lifecycle, configuration and resource contracts --- .github/workflows/plugins.yml | 20 + VisualPinball.Engine.sln | 87 ++-- .../CapabilityFamilyTests.cs | 72 +++ .../CapabilityFamilyTests.cs.meta | 2 + .../ConfigurationTests.cs | 103 +++++ .../ConfigurationTests.cs.meta | 2 + VisualPinball.Plugins.Test/PluginHostTests.cs | 265 +++++++++++ .../PluginHostTests.cs.meta | 2 + VisualPinball.Plugins.Test/ResourceTests.cs | 159 +++++++ .../ResourceTests.cs.meta | 2 + .../VisualPinball.Plugins.Test.asmdef | 10 + .../VisualPinball.Plugins.Test.asmdef.meta | 7 + .../VisualPinball.Plugins.Test.csproj | 16 + .../VisualPinball.Plugins.Test.csproj.meta | 7 + VisualPinball.Plugins/Api.meta | 8 + .../Api/GameIdentityProvider.cs | 21 + .../Api/GameIdentityProvider.cs.meta | 2 + VisualPinball.Plugins/Api/PluginDescriptor.cs | 74 +++ .../Api/PluginDescriptor.cs.meta | 2 + VisualPinball.Plugins/Api/PluginRuntime.cs | 93 ++++ .../Api/PluginRuntime.cs.meta | 2 + VisualPinball.Plugins/Configuration.meta | 8 + .../Configuration/PluginConfigurationStore.cs | 109 +++++ .../PluginConfigurationStore.cs.meta | 2 + .../Configuration/PluginSchema.cs | 178 ++++++++ .../Configuration/PluginSchema.cs.meta | 2 + .../Configuration/PluginSettingsSession.cs | 79 ++++ .../PluginSettingsSession.cs.meta | 2 + VisualPinball.Plugins/LICENSE | 21 + VisualPinball.Plugins/LICENSE.meta | 7 + VisualPinball.Plugins/README.md | 39 ++ VisualPinball.Plugins/README.md.meta | 7 + VisualPinball.Plugins/Resources.meta | 8 + .../Resources/DisplayFormatNegotiator.cs | 30 ++ .../Resources/DisplayFormatNegotiator.cs.meta | 2 + .../Resources/DisplayResourceSource.cs | 57 +++ .../Resources/DisplayResourceSource.cs.meta | 2 + .../Resources/PluginResourceChoices.cs | 27 ++ .../Resources/PluginResourceChoices.cs.meta | 2 + .../Resources/PluginResourceRegistry.cs | 163 +++++++ .../Resources/PluginResourceRegistry.cs.meta | 2 + .../Resources/ResourceContracts.cs | 171 +++++++ .../Resources/ResourceContracts.cs.meta | 2 + .../Resources/ResourceSignal.cs | 68 +++ .../Resources/ResourceSignal.cs.meta | 2 + VisualPinball.Plugins/Runtime.meta | 8 + .../Runtime/OwnerScheduler.cs | 38 ++ .../Runtime/OwnerScheduler.cs.meta | 2 + .../Runtime/PluginCapabilities.cs | 21 + .../Runtime/PluginCapabilities.cs.meta | 2 + VisualPinball.Plugins/Runtime/PluginEvents.cs | 39 ++ .../Runtime/PluginEvents.cs.meta | 2 + VisualPinball.Plugins/Runtime/PluginHost.cs | 420 ++++++++++++++++++ .../Runtime/PluginHost.cs.meta | 2 + .../Runtime/PluginInputPipeline.cs | 33 ++ .../Runtime/PluginInputPipeline.cs.meta | 2 + .../Runtime/PluginOrderResolver.cs | 66 +++ .../Runtime/PluginOrderResolver.cs.meta | 2 + .../Runtime/PluginOutputLeases.cs | 22 + .../Runtime/PluginOutputLeases.cs.meta | 2 + .../Runtime/ResourceScope.cs | 36 ++ .../Runtime/ResourceScope.cs.meta | 2 + .../VisualPinball.Plugins.asmdef | 8 + .../VisualPinball.Plugins.asmdef.meta | 7 + .../VisualPinball.Plugins.csproj | 13 + .../VisualPinball.Plugins.csproj.meta | 7 + .../Packaging/RuntimePackageReader.cs | 12 + .../VisualPinball.Unity/Plugins.meta | 8 + .../Plugins/GamelogicPluginResources.cs | 125 ++++++ .../Plugins/GamelogicPluginResources.cs.meta | 2 + .../Plugins/PackagedGameInformation.cs | 52 +++ .../Plugins/PackagedGameInformation.cs.meta | 2 + .../Plugins/PlayerIntegrationMode.cs | 21 + .../Plugins/PlayerIntegrationMode.cs.meta | 2 + .../Plugins/PluginSettingsViews.cs | 41 ++ .../Plugins/PluginSettingsViews.cs.meta | 2 + .../Plugins/UnityPluginContracts.cs | 32 ++ .../Plugins/UnityPluginContracts.cs.meta | 2 + .../Plugins/UnityTextureResource.cs | 14 + .../Plugins/UnityTextureResource.cs.meta | 2 + .../VisualPinball.Unity.asmdef | 1 + package.json | 2 +- 82 files changed, 2971 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/plugins.yml create mode 100644 VisualPinball.Plugins.Test/CapabilityFamilyTests.cs create mode 100644 VisualPinball.Plugins.Test/CapabilityFamilyTests.cs.meta create mode 100644 VisualPinball.Plugins.Test/ConfigurationTests.cs create mode 100644 VisualPinball.Plugins.Test/ConfigurationTests.cs.meta create mode 100644 VisualPinball.Plugins.Test/PluginHostTests.cs create mode 100644 VisualPinball.Plugins.Test/PluginHostTests.cs.meta create mode 100644 VisualPinball.Plugins.Test/ResourceTests.cs create mode 100644 VisualPinball.Plugins.Test/ResourceTests.cs.meta create mode 100644 VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.asmdef create mode 100644 VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.asmdef.meta create mode 100644 VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj create mode 100644 VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj.meta create mode 100644 VisualPinball.Plugins/Api.meta create mode 100644 VisualPinball.Plugins/Api/GameIdentityProvider.cs create mode 100644 VisualPinball.Plugins/Api/GameIdentityProvider.cs.meta create mode 100644 VisualPinball.Plugins/Api/PluginDescriptor.cs create mode 100644 VisualPinball.Plugins/Api/PluginDescriptor.cs.meta create mode 100644 VisualPinball.Plugins/Api/PluginRuntime.cs create mode 100644 VisualPinball.Plugins/Api/PluginRuntime.cs.meta create mode 100644 VisualPinball.Plugins/Configuration.meta create mode 100644 VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs create mode 100644 VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs.meta create mode 100644 VisualPinball.Plugins/Configuration/PluginSchema.cs create mode 100644 VisualPinball.Plugins/Configuration/PluginSchema.cs.meta create mode 100644 VisualPinball.Plugins/Configuration/PluginSettingsSession.cs create mode 100644 VisualPinball.Plugins/Configuration/PluginSettingsSession.cs.meta create mode 100644 VisualPinball.Plugins/LICENSE create mode 100644 VisualPinball.Plugins/LICENSE.meta create mode 100644 VisualPinball.Plugins/README.md create mode 100644 VisualPinball.Plugins/README.md.meta create mode 100644 VisualPinball.Plugins/Resources.meta create mode 100644 VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs create mode 100644 VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs.meta create mode 100644 VisualPinball.Plugins/Resources/DisplayResourceSource.cs create mode 100644 VisualPinball.Plugins/Resources/DisplayResourceSource.cs.meta create mode 100644 VisualPinball.Plugins/Resources/PluginResourceChoices.cs create mode 100644 VisualPinball.Plugins/Resources/PluginResourceChoices.cs.meta create mode 100644 VisualPinball.Plugins/Resources/PluginResourceRegistry.cs create mode 100644 VisualPinball.Plugins/Resources/PluginResourceRegistry.cs.meta create mode 100644 VisualPinball.Plugins/Resources/ResourceContracts.cs create mode 100644 VisualPinball.Plugins/Resources/ResourceContracts.cs.meta create mode 100644 VisualPinball.Plugins/Resources/ResourceSignal.cs create mode 100644 VisualPinball.Plugins/Resources/ResourceSignal.cs.meta create mode 100644 VisualPinball.Plugins/Runtime.meta create mode 100644 VisualPinball.Plugins/Runtime/OwnerScheduler.cs create mode 100644 VisualPinball.Plugins/Runtime/OwnerScheduler.cs.meta create mode 100644 VisualPinball.Plugins/Runtime/PluginCapabilities.cs create mode 100644 VisualPinball.Plugins/Runtime/PluginCapabilities.cs.meta create mode 100644 VisualPinball.Plugins/Runtime/PluginEvents.cs create mode 100644 VisualPinball.Plugins/Runtime/PluginEvents.cs.meta create mode 100644 VisualPinball.Plugins/Runtime/PluginHost.cs create mode 100644 VisualPinball.Plugins/Runtime/PluginHost.cs.meta create mode 100644 VisualPinball.Plugins/Runtime/PluginInputPipeline.cs create mode 100644 VisualPinball.Plugins/Runtime/PluginInputPipeline.cs.meta create mode 100644 VisualPinball.Plugins/Runtime/PluginOrderResolver.cs create mode 100644 VisualPinball.Plugins/Runtime/PluginOrderResolver.cs.meta create mode 100644 VisualPinball.Plugins/Runtime/PluginOutputLeases.cs create mode 100644 VisualPinball.Plugins/Runtime/PluginOutputLeases.cs.meta create mode 100644 VisualPinball.Plugins/Runtime/ResourceScope.cs create mode 100644 VisualPinball.Plugins/Runtime/ResourceScope.cs.meta create mode 100644 VisualPinball.Plugins/VisualPinball.Plugins.asmdef create mode 100644 VisualPinball.Plugins/VisualPinball.Plugins.asmdef.meta create mode 100644 VisualPinball.Plugins/VisualPinball.Plugins.csproj create mode 100644 VisualPinball.Plugins/VisualPinball.Plugins.csproj.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs.meta diff --git a/.github/workflows/plugins.yml b/.github/workflows/plugins.yml new file mode 100644 index 000000000..73c3264b1 --- /dev/null +++ b/.github/workflows/plugins.yml @@ -0,0 +1,20 @@ +name: Plugin contracts +on: + push: + paths: ['VisualPinball.Plugins/**', 'VisualPinball.Plugins.Test/**', '.github/workflows/plugins.yml'] + pull_request: + paths: ['VisualPinball.Plugins/**', 'VisualPinball.Plugins.Test/**', '.github/workflows/plugins.yml'] +permissions: + contents: read +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + - run: dotnet test VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj -c Release diff --git a/VisualPinball.Engine.sln b/VisualPinball.Engine.sln index 9ddfa4d66..83b11fa12 100644 --- a/VisualPinball.Engine.sln +++ b/VisualPinball.Engine.sln @@ -7,24 +7,28 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VisualPinball.Engine.Test", "VisualPinball.Engine.Test\VisualPinball.Engine.Test.csproj", "{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{E74E1502-B527-475D-BCEC-B8F60699BA5F}" -ProjectSection(SolutionItems) = preProject - LICENSE = LICENSE - README.md = README.md - .editorconfig = .editorconfig - .gitignore = .gitignore - CHANGELOG.md = CHANGELOG.md - CONTRIBUTING.md = CONTRIBUTING.md - package.json = package.json -EndProjectSection + ProjectSection(SolutionItems) = preProject + LICENSE = LICENSE + README.md = README.md + .editorconfig = .editorconfig + .gitignore = .gitignore + CHANGELOG.md = CHANGELOG.md + CONTRIBUTING.md = CONTRIBUTING.md + package.json = package.json + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{E6A28E8F-1DE7-4CF4-ADB7-A55A6E6467AF}" -ProjectSection(SolutionItems) = preProject - .github\workflows\build.yml = .github\workflows\build.yml - .github\workflows\dependents.yml = .github\workflows\dependents.yml - .github\workflows\documentation.yml = .github\workflows\documentation.yml - .github\workflows\publish.yml = .github\workflows\publish.yml - .github\workflows\release.yml = .github\workflows\release.yml -EndProjectSection + ProjectSection(SolutionItems) = preProject + .github\workflows\build.yml = .github\workflows\build.yml + .github\workflows\dependents.yml = .github\workflows\dependents.yml + .github\workflows\documentation.yml = .github\workflows\documentation.yml + .github\workflows\publish.yml = .github\workflows\publish.yml + .github\workflows\release.yml = .github\workflows\release.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisualPinball.Plugins", "VisualPinball.Plugins\VisualPinball.Plugins.csproj", "{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisualPinball.Plugins.Test", "VisualPinball.Plugins.Test\VisualPinball.Plugins.Test.csproj", "{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -36,42 +40,69 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|x64.ActiveCfg = Debug|Any CPU {EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|x64.Build.0 = Debug|Any CPU {EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|x86.ActiveCfg = Debug|Any CPU {EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|x86.Build.0 = Debug|Any CPU - {EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFE73640-F311-4109-9F3E-1573F86990BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFE73640-F311-4109-9F3E-1573F86990BB}.Release|Any CPU.Build.0 = Release|Any CPU {EFE73640-F311-4109-9F3E-1573F86990BB}.Release|x64.ActiveCfg = Release|x64 {EFE73640-F311-4109-9F3E-1573F86990BB}.Release|x64.Build.0 = Release|x64 {EFE73640-F311-4109-9F3E-1573F86990BB}.Release|x86.ActiveCfg = Release|x86 {EFE73640-F311-4109-9F3E-1573F86990BB}.Release|x86.Build.0 = Release|x86 - {EFE73640-F311-4109-9F3E-1573F86990BB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EFE73640-F311-4109-9F3E-1573F86990BB}.Release|Any CPU.Build.0 = Release|Any CPU + {8A259010-3234-4402-BE50-980B4887902C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A259010-3234-4402-BE50-980B4887902C}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A259010-3234-4402-BE50-980B4887902C}.Debug|x64.ActiveCfg = Debug|Any CPU {8A259010-3234-4402-BE50-980B4887902C}.Debug|x64.Build.0 = Debug|Any CPU {8A259010-3234-4402-BE50-980B4887902C}.Debug|x86.ActiveCfg = Debug|Any CPU {8A259010-3234-4402-BE50-980B4887902C}.Debug|x86.Build.0 = Debug|Any CPU - {8A259010-3234-4402-BE50-980B4887902C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8A259010-3234-4402-BE50-980B4887902C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A259010-3234-4402-BE50-980B4887902C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A259010-3234-4402-BE50-980B4887902C}.Release|Any CPU.Build.0 = Release|Any CPU {8A259010-3234-4402-BE50-980B4887902C}.Release|x64.ActiveCfg = Release|Any CPU {8A259010-3234-4402-BE50-980B4887902C}.Release|x64.Build.0 = Release|Any CPU {8A259010-3234-4402-BE50-980B4887902C}.Release|x86.ActiveCfg = Release|Any CPU {8A259010-3234-4402-BE50-980B4887902C}.Release|x86.Build.0 = Release|Any CPU - {8A259010-3234-4402-BE50-980B4887902C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8A259010-3234-4402-BE50-980B4887902C}.Release|Any CPU.Build.0 = Release|Any CPU + {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|x64.ActiveCfg = Debug|Any CPU {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|x64.Build.0 = Debug|Any CPU {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|x86.ActiveCfg = Debug|Any CPU {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|x86.Build.0 = Debug|Any CPU - {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|Any CPU.Build.0 = Release|Any CPU {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|x64.ActiveCfg = Release|Any CPU {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|x64.Build.0 = Release|Any CPU {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|x86.ActiveCfg = Release|Any CPU {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|x86.Build.0 = Release|Any CPU - {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|Any CPU.Build.0 = Release|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|x64.ActiveCfg = Debug|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|x64.Build.0 = Debug|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|x86.ActiveCfg = Debug|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|x86.Build.0 = Debug|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|Any CPU.Build.0 = Release|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|x64.ActiveCfg = Release|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|x64.Build.0 = Release|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|x86.ActiveCfg = Release|Any CPU + {7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|x86.Build.0 = Release|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|x64.ActiveCfg = Debug|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|x64.Build.0 = Debug|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|x86.ActiveCfg = Debug|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|x86.Build.0 = Debug|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|Any CPU.Build.0 = Release|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|x64.ActiveCfg = Release|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|x64.Build.0 = Release|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|x86.ActiveCfg = Release|Any CPU + {CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {E6A28E8F-1DE7-4CF4-ADB7-A55A6E6467AF} = {E74E1502-B527-475D-BCEC-B8F60699BA5F} diff --git a/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs b/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs new file mode 100644 index 000000000..3f783dd06 --- /dev/null +++ b/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using VisualPinball.Plugins; + +namespace VisualPinball.Plugins.Test +{ + [TestFixture] + public sealed class CapabilityFamilyTests + { + [Test] + public void AudioReplacementKeepsOriginalStreamAvailableAndRestoresPlayback() + { + using var source = new AudioResourceSource(); var registry = new PluginResourceRegistry(); + using var publication = registry.Publish(new PluginResourceDescriptor("rom/audio", "rom", "controller", "ROM mix", "rom-mix", ResourceKind.Audio, 1, 1), source); + using var binding = registry.Bind(new PluginResourceSelection(ResourceKind.Audio) { Controller = "controller", Session = 1 }); + int count = 0; using var subscription = binding.Connect(audio => audio.Subscribe(frame => { count++; Assert.That(frame.Channels, Is.EqualTo(2)); })); + using var replacement = ((IPluginAudioPlaybackControl)binding.Source!).AcquirePlayback("ssf"); + Assert.That(source.PlaybackReplaced, Is.True); + source.Publish(new PluginPcmFrame(new byte[16], PcmEncoding.Float32, 48000, 2, 2, 1)); + Assert.That(count, Is.EqualTo(1)); Assert.Throws(() => source.AcquirePlayback("other")); + replacement.Dispose(); Assert.That(source.PlaybackReplaced, Is.False); + } + [Test] + public void InputFiltersHaveDeterministicOrderAndReleaseTheirStage() + { + var pipeline = new PluginInputPipeline(); + using var add = pipeline.Register("add", 20, new InputFilter(value => value + 3)); + using var scale = pipeline.Register("scale", 10, new InputFilter(value => value * 2)); + Assert.That(pipeline.Apply(new PluginInputEvent("nudge-x", 1, 10)).Value, Is.EqualTo(5)); + scale.Dispose(); Assert.That(pipeline.Apply(new PluginInputEvent("nudge-x", 1, 10)).Value, Is.EqualTo(4)); + } + [Test] + public void SegmentAndAuxiliarySurfaceProvidersUseOnlyVpeContracts() + { + var registry = new PluginResourceRegistry(); using var source = new SegmentResourceSource(); + using var publication = registry.Publish(new PluginResourceDescriptor("score", "backglass", "controller", "Score", "score", ResourceKind.Segments, 1, 1), source); + using var surface = registry.Publish(new PluginResourceDescriptor("backglass", "backglass", "controller", "Backglass", "backglass", ResourceKind.Surface, 1, 1), new Surface()); + using var binding = registry.Bind(new PluginResourceSelection(ResourceKind.Segments) { Session = 1 }); + string layout = ""; using var subscription = binding.Connect(segments => segments.Subscribe(frame => layout = frame.Layout)); + source.Publish(new PluginSegmentFrame("vpe.segment.mask16.v1", 4, 16, new float[64])); + Assert.That(layout, Is.EqualTo("vpe.segment.mask16.v1")); Assert.That(registry.Snapshot().Count, Is.EqualTo(2)); + } + [Test] + public void RequiredCapabilityAmbiguityAndOrderingCyclesAreExplicit() + { + var first = new PluginDescriptor("first", "First", "") { Provides = new[] { "cap" } }; + var second = new PluginDescriptor("second", "Second", "") { Provides = new[] { "cap" } }; + var consumer = new PluginDescriptor("consumer", "Consumer", "") { Requires = new[] { "cap" } }; + Assert.Throws(() => PluginOrderResolver.Resolve(new[] { first, second, consumer }, PluginPhase.Start)); + Assert.That(PluginOrderResolver.Resolve(new[] { first, second, consumer }, PluginPhase.Start, preferredProviders: new Dictionary { ["cap"] = "second" }).Select(plugin => plugin.Id).ToList().IndexOf("second"), Is.LessThan(2)); + first.DependsOn = new[] { "second" }; second.DependsOn = new[] { "first" }; + Assert.That(Assert.Throws(() => PluginOrderResolver.Resolve(new[] { first, second }, PluginPhase.Start))!.Message, Does.Contain("first")); + } + [Test] + public void ReleaseRunsEveryActionEvenWhenOneFails() + { + var scope = new ResourceScope(); var calls = new List(); + scope.Own(new Subscription(() => calls.Add(1))); scope.Own(new Subscription(() => throw new InvalidOperationException())); scope.Own(new Subscription(() => calls.Add(3))); + Assert.Throws(scope.Dispose); Assert.That(calls, Is.EqualTo(new[] { 3, 1 })); Assert.DoesNotThrow(scope.Dispose); + } + private sealed class InputFilter : IPluginInputFilter + { + private readonly Func _transform; + internal InputFilter(Func transform) { _transform = transform; } + public PluginInputEvent Filter(in PluginInputEvent input) => new PluginInputEvent(input.Action, _transform(input.Value), input.TimestampUs); + } + private sealed class Surface : IPluginSurface { public ResourceKind Kind => ResourceKind.Surface; public int Width => 1920; public int Height => 1080; public string Backend => "test"; } + } +} diff --git a/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs.meta b/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs.meta new file mode 100644 index 000000000..9032e9744 --- /dev/null +++ b/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: af8831d651125914caaa4a368e290136 \ No newline at end of file diff --git a/VisualPinball.Plugins.Test/ConfigurationTests.cs b/VisualPinball.Plugins.Test/ConfigurationTests.cs new file mode 100644 index 000000000..14d126054 --- /dev/null +++ b/VisualPinball.Plugins.Test/ConfigurationTests.cs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using VisualPinball.Plugins; + +namespace VisualPinball.Plugins.Test +{ + [TestFixture] + public sealed class ConfigurationTests + { + private string _directory = null!; + private readonly PluginDescriptor _plugin = new PluginDescriptor("test.config", "Settings", "Test"); + private readonly GameIdentity _game = new GameIdentity("pinmame", "t2_l8"); + private PluginSettingsSchema Schema() => new PluginSettingsSchema(1, new PluginSettingsGroup("Test", "", + new PluginField("root", "Root", SettingKind.Folder) { Default = "default" }, + new PluginField("variant", "Variant", SettingKind.Choice) { Scope = ConfigurationScope.GlobalAndGame, Default = "auto", Provider = "variants" }, + new PluginField("layout", "Layout", SettingKind.Collection) { Scope = ConfigurationScope.GlobalAndGame, Default = new JArray("default") })); + [SetUp] public void SetUp() => _directory = Path.Combine(Path.GetTempPath(), "vpe-plugin-tests", Guid.NewGuid().ToString("N")); + [TearDown] public void TearDown() { if (Directory.Exists(_directory)) Directory.Delete(_directory, true); } + [Test] + public void GlobalDefaultsAndSparseGameOverridesRemainIndependent() + { + var store = new PluginConfigurationStore(_directory); var schema = Schema(); + store.Save(_plugin, schema, new PluginSettingsSnapshot(1, JObject.Parse("{'root':'global','variant':'one','layout':['a','b']}"))); + store.Save(_plugin, schema, new PluginSettingsSnapshot(1, JObject.Parse("{'variant':'two','layout':['c']}")), _game); + Assert.That(store.Effective(_plugin, schema, _game).String("root"), Is.EqualTo("global")); + Assert.That(store.Effective(_plugin, schema, _game).String("variant"), Is.EqualTo("two")); + Assert.That(store.Effective(_plugin, schema, _game).Value("layout")!.ToString(), Is.EqualTo(new JArray("c").ToString())); + store.Save(_plugin, schema, new PluginSettingsSnapshot(1), _game); + Assert.That(store.Effective(_plugin, schema, _game).String("variant"), Is.EqualTo("one")); + } + [Test] + public void ManuallyStoredValuesCannotOverrideFieldsOutsideTheirDeclaredScope() + { + var schema = Schema(); + var resolved = PluginConfigurationStore.Resolve(schema, + new PluginSettingsSnapshot(1, JObject.Parse("{'root':'global','variant':'one'}")), + new PluginSettingsSnapshot(1, JObject.Parse("{'root':'wrong','variant':'two','unknown':'kept'}"))); + Assert.That(resolved.String("root"), Is.EqualTo("global")); + Assert.That(resolved.String("variant"), Is.EqualTo("two")); + Assert.That(resolved.String("unknown"), Is.EqualTo("kept")); + } + [Test] + public void UnknownFieldsSurviveSaveAndFutureVersionIsNeverOverwritten() + { + var store = new PluginConfigurationStore(_directory); var schema = Schema(); string path = store.SettingsPath(_plugin); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, "{'schemaVersion':1,'futureTop':42,'values':{'unknown':'kept','root':'old'}}"); + store.Save(_plugin, schema, new PluginSettingsSnapshot(1, JObject.Parse("{'root':'new'}"))); + var saved = JObject.Parse(File.ReadAllText(path)); + Assert.That(saved["values"]!["unknown"]!.Value(), Is.EqualTo("kept")); + Assert.That(saved.Value("futureTop"), Is.EqualTo(42)); + saved["schemaVersion"] = 9; File.WriteAllText(path, saved.ToString()); var before = File.ReadAllText(path); + Assert.Throws(() => store.Save(_plugin, schema, new PluginSettingsSnapshot(1))); + Assert.That(File.ReadAllText(path), Is.EqualTo(before)); + } + [Test] + public void MigrationsOperateOnDetachedTreeAndPersistOnlyOnApply() + { + var store = new PluginConfigurationStore(_directory); string path = store.SettingsPath(_plugin); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllText(path, "{'schemaVersion':1,'values':{'old':'value'}}"); + var schema = new PluginSettingsSchema(2, new PluginSettingsGroup("Migrated", "", new PluginField("new", "New", SettingKind.Text))) { + Migrations = new Dictionary> { [1] = tree => { tree["new"] = tree["old"]; tree.Remove("old"); return tree; } } + }; + Assert.That(store.Read(_plugin, schema).String("new"), Is.EqualTo("value")); + Assert.That(JObject.Parse(File.ReadAllText(path)).Value("schemaVersion"), Is.EqualTo(1)); + store.Save(_plugin, schema, store.Read(_plugin, schema)); + Assert.That(JObject.Parse(File.ReadAllText(path)).Value("schemaVersion"), Is.EqualTo(2)); + } + [Test] + public async Task ApplyRechecksAvailabilityAndDoesNotReplaceMissingChoice() + { + var store = new PluginConfigurationStore(_directory); var provider = new CatalogProvider(); + var factory = new ConfigFactory(_plugin, Schema(), provider); + using var session = new PluginSettingsSession(factory, store, "windows", _game); + session.Set("variant", "one"); Assert.That((await session.ApplyAsync()).Success, Is.True); + provider.Available = false; + Assert.That((await session.ApplyAsync()).Success, Is.False); + Assert.That(store.Read(_plugin, Schema(), _game).String("variant"), Is.EqualTo("one")); + session.Set("variant", null); Assert.That(session.Effective.String("variant"), Is.EqualTo("auto")); + } + private sealed class CatalogProvider : IPluginSettingsProvider + { + internal bool Available = true; + public Task DiscoverAsync(string provider, PluginChoiceRequest request, CancellationToken token) => Task.FromResult(new PluginChoiceCatalog(Available.ToString(), new[] { new PluginChoice("auto", "Automatic"), new PluginChoice("one", "One", available: Available, unavailableReason: "Removed") })); + public Task RunActionAsync(string action, PluginChoiceRequest request, CancellationToken token) => Task.FromResult(new PluginActionResult(true, "")); + public IReadOnlyList Validate(PluginChoiceRequest request) => Array.Empty(); + } + private sealed class ConfigFactory : IPlayerPluginFactory + { + public PluginDescriptor Descriptor { get; } + public PluginSettingsSchema Schema { get; } + public IPluginSettingsProvider? SettingsProvider { get; } + internal ConfigFactory(PluginDescriptor descriptor, PluginSettingsSchema schema, IPluginSettingsProvider provider) { Descriptor = descriptor; Schema = schema; SettingsProvider = provider; } + public IPlayerPlugin Create(PluginApplicationContext context) => throw new NotSupportedException(); + } + } +} diff --git a/VisualPinball.Plugins.Test/ConfigurationTests.cs.meta b/VisualPinball.Plugins.Test/ConfigurationTests.cs.meta new file mode 100644 index 000000000..9181d8855 --- /dev/null +++ b/VisualPinball.Plugins.Test/ConfigurationTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a14fe21226595ad4b935330f07654e2c \ No newline at end of file diff --git a/VisualPinball.Plugins.Test/PluginHostTests.cs b/VisualPinball.Plugins.Test/PluginHostTests.cs new file mode 100644 index 000000000..27255100c --- /dev/null +++ b/VisualPinball.Plugins.Test/PluginHostTests.cs @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using VisualPinball.Plugins; + +namespace VisualPinball.Plugins.Test +{ + [TestFixture] + public sealed class PluginHostTests + { + private string _directory = null!; + private readonly GameIdentity _game = new GameIdentity("test", "game"); + [SetUp] public void SetUp() => _directory = Path.Combine(Path.GetTempPath(), "vpe-plugin-tests", Guid.NewGuid().ToString("N")); + [TearDown] public void TearDown() { if (Directory.Exists(_directory)) Directory.Delete(_directory, true); } + + [Test] + public void DisabledGamePolicyPreventsConstruction() + { + var factory = new TestFactory("test.disabled"); + var host = Host(factory); + host.Configuration.SetEnabled(factory.Descriptor, false, _game); + Run(host, host.PrepareAndStartAsync(_game, "test.vpe", "Test", "controller", 1, default)); + Assert.That(factory.Constructions, Is.Zero); + Assert.That(host.Statuses[factory.Descriptor.Id].State, Is.EqualTo(PluginState.Disabled)); + Run(host, host.StopGameAsync()); + Run(host, host.PrepareAndStartAsync(new GameIdentity("test", "other"), "test.vpe", "Other", "controller", 2, default)); + Assert.That(factory.Constructions, Is.EqualTo(1)); + Run(host, host.StopGameAsync()); + } + + [Test] + public void RequiredProviderPrecedesConsumerAndCleanupReversesStart() + { + var calls = new List(); + var provider = new TestFactory("provider", calls); provider.Descriptor.Provides = new[] { "service" }; provider.Descriptor.Order = 20; + var consumer = new TestFactory("consumer", calls); consumer.Descriptor.Requires = new[] { "service" }; consumer.Descriptor.Order = -20; + var host = Host(consumer, provider); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + Assert.That(calls.Where(call => call.EndsWith(":start")), Is.EqualTo(new[] { "provider:start", "consumer:start" })); + Run(host, host.StopGameAsync()); + Assert.That(calls.Where(call => call.EndsWith(":stop")), Is.EqualTo(new[] { "consumer:stop", "provider:stop" })); + Assert.That(calls.Count(call => call.EndsWith(":dispose")), Is.EqualTo(2)); + } + + [TestCase("initialize")] + [TestCase("prepare")] + [TestCase("start")] + public void ProviderFailureBlocksDependentsWhileIndependentPluginRuns(string failAt) + { + var calls = new List(); + var provider = new TestFactory("provider", calls) { FailAt = failAt }; + var consumer = new TestFactory("consumer", calls); consumer.Descriptor.DependsOn = new[] { "provider" }; + var independent = new TestFactory("independent", calls); + var host = Host(consumer, provider, independent); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + Assert.That(host.Statuses["provider"].State, Is.EqualTo(PluginState.Faulted)); + Assert.That(host.Statuses["consumer"].State, Is.EqualTo(PluginState.Blocked)); + Assert.That(host.Statuses["independent"].State, Is.EqualTo(PluginState.Running)); + Assert.That(calls.Count(call => call == "provider:dispose"), Is.EqualTo(1)); + Run(host, host.StopGameAsync()); + Assert.That(calls.Count(call => call == "provider:dispose"), Is.EqualTo(1)); + } + + [Test] + public void ReadyFailureIsContainedAndResourcesAreReleased() + { + var calls = new List(); + var host = Host(new TestFactory("failed", calls) { FailAt = "ready" }, new TestFactory("healthy", calls)); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + Assert.DoesNotThrow(host.GameReady); + Assert.That(calls, Does.Contain("healthy:ready")); + Assert.That(calls, Does.Contain("failed:release")); + Run(host, host.StopGameAsync()); + } + + [Test] + public void WorkerContinuationReturnsToOwnerBeforeNextHook() + { + int owner = Thread.CurrentThread.ManagedThreadId; + var factory = new TestFactory("async") { AsyncInitialize = true }; + var host = Host(factory); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + Assert.That(factory.PrepareThread, Is.EqualTo(owner)); + Run(host, host.StopGameAsync()); + } + + [Test] + public void CancellationUnwindsWithFreshCleanupToken() + { + var calls = new List(); + using var cancel = new CancellationTokenSource(); + var factory = new TestFactory("cancel", calls) { CancelDuringStart = cancel }; + var host = Host(factory); + Assert.Throws(() => Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, cancel.Token))); + Assert.That(calls, Does.Contain("cancel:stop")); + Assert.That(factory.CleanupWasCanceled, Is.False); + } + + private PluginHost Host(params IPlayerPluginFactory[] factories) => new PluginHost(factories, new PluginConfigurationStore(_directory), new OwnerScheduler(), new PluginCapabilities(), new PluginResourceRegistry(), "windows"); + [Test] + public void HealthyPluginKeepsTickingWhileAnotherPluginsCleanupWaits() + { + var calls = new List(); var completion = new TaskCompletionSource(); + var host = Host(new TestFactory("failed", calls) { FailAt = "ready", StopCompletion = completion.Task }, new TestFactory("healthy", calls)); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + host.GameReady(); Assert.That(host.IsTransitioning, Is.True); + host.Tick(.016); Assert.That(calls, Does.Contain("healthy:tick")); + completion.SetResult(true); Run(host, host.StopGameAsync()); + } + [Test] + public void HungLiveApplyDoesNotPersistOrBlockRelaunch() + { + var factory = new TestFactory("live") { ApplyCompletion = new TaskCompletionSource().Task, + Schema = new PluginSettingsSchema(1, new PluginSettingsGroup("Test", "", new PluginField("value", "Value", SettingKind.Integer) { Default = 5, Change = ConfigurationChange.Live })) }; + var host = Host(factory); host.StartupTimeout = TimeSpan.FromMilliseconds(30); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + using var session = new PluginSettingsSession(factory, host.Configuration, "windows"); session.Set("value", 42); + var apply = session.ApplyAsync(host: host); Run(host, apply); + Assert.That(apply.Result.Success, Is.False); Assert.That(File.Exists(host.Configuration.SettingsPath(factory.Descriptor)), Is.False); + Run(host, host.StopGameAsync()); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 2, default)); + Assert.That(host.Statuses["live"].State, Is.EqualTo(PluginState.Unresponsive)); + Assert.That(factory.Constructions, Is.EqualTo(1)); + Run(host, host.StopGameAsync()); + } + [Test] + public void AbandonedStartupIsQuarantinedUntilItsHookAndShutdownFinish() + { + var completion = new TaskCompletionSource(); + var factory = new TestFactory("late") { InitializeCompletion = completion.Task }; + var host = Host(factory); host.StartupTimeout = TimeSpan.FromMilliseconds(30); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + Assert.That(host.Statuses["late"].State, Is.EqualTo(PluginState.Unresponsive)); + Run(host, host.StopGameAsync()); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 2, default)); + Assert.That(factory.Constructions, Is.EqualTo(1)); + completion.SetResult(true); + var until = DateTime.UtcNow.AddSeconds(2); + while (host.Statuses["late"].State == PluginState.Unresponsive && DateTime.UtcNow < until) { host.Scheduler.Pump(); Thread.Sleep(1); } + Assert.That(factory.Calls, Does.Contain("late:dispose")); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 3, default)); + Assert.That(factory.Constructions, Is.EqualTo(2)); Run(host, host.StopGameAsync()); + } + [Test] + public void AResourceAcquiredAfterScopeClosureIsReleasedImmediately() + { + var scope = new ResourceScope(); scope.Dispose(); bool released = false; + Assert.Throws(() => scope.Own(new Subscription(() => released = true))); + Assert.That(released, Is.True); + } + [Test] + public void PauseAndResumeDuringFaultCleanupKeepTheLatestState() + { + var calls = new List(); + var blockedStop = new TaskCompletionSource(); + var factory = new TestFactory("failed", calls) { FailAt = "ready", StopCompletion = blockedStop.Task }; + var host = Host(factory, new TestFactory("healthy", calls)); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + host.GameReady(); + host.PauseChanged(true); host.PauseChanged(false); + blockedStop.SetResult(true); + while (host.IsTransitioning) { host.Scheduler.Pump(); Thread.Sleep(1); } + Assert.That(calls, Does.Contain("healthy:resume")); + Assert.That(calls, Does.Not.Contain("healthy:pause")); + Run(host, host.StopGameAsync()); + } + [Test] + public void FailedPersistenceRollsBackLiveSettingsAndPreservesFutureDocuments() + { + var calls = new List(); + var factory = new TestFactory("live", calls) { Schema = new PluginSettingsSchema(1, new PluginSettingsGroup("Test", "", + new PluginField("value", "Value", SettingKind.Integer) { Default = 5, Change = ConfigurationChange.Live })) }; + var host = Host(factory); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + string path = host.Configuration.SettingsPath(factory.Descriptor); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + const string future = "{'schemaVersion':9,'values':{'value':5,'future':'kept'}}"; + File.WriteAllText(path, future); + using var session = new PluginSettingsSession(factory, host.Configuration, "windows"); + session.Set("value", 42); + Assert.Throws(() => Run(host, session.ApplyAsync(host: host))); + Assert.That(calls, Does.Contain("live:apply:42")); Assert.That(calls, Does.Contain("live:apply:5")); + Assert.That(File.ReadAllText(path), Is.EqualTo(future)); + Run(host, host.StopGameAsync()); + } + [Test] + public void UncooperativeCleanupDoesNotBlockOtherPluginsOrExit() + { + var calls = new List(); + var factory = new TestFactory("stuck", calls) { NeverFinishStop = true }; + var host = Host(factory, new TestFactory("healthy", calls)); + host.CleanupTimeout = TimeSpan.FromMilliseconds(30); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + Run(host, host.StopGameAsync()); + Assert.That(host.Statuses["stuck"].State, Is.EqualTo(PluginState.Unresponsive)); + Assert.That(calls, Does.Contain("healthy:stop")); + Assert.That(calls, Does.Contain("stuck:release")); + Assert.That(host.IsRunning, Is.False); + } + [Test] + public void BadPolicyAndOrderingCycleOnlyBlockAffectedPlugins() + { + var bad = new TestFactory("bad"); var a = new TestFactory("cycle.a"); var b = new TestFactory("cycle.b"); var healthy = new TestFactory("healthy"); + a.Descriptor.After = new[] { b.Descriptor.Id }; b.Descriptor.After = new[] { a.Descriptor.Id }; + var host = Host(bad, a, b, healthy); + Directory.CreateDirectory(host.Configuration.DirectoryFor(bad.Descriptor)); + File.WriteAllText(Path.Combine(host.Configuration.DirectoryFor(bad.Descriptor), "policy.json"), "invalid JSON"); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + Assert.That(host.Statuses["bad"].State, Is.EqualTo(PluginState.Faulted)); + Assert.That(host.Statuses["cycle.a"].State, Is.EqualTo(PluginState.Blocked)); + Assert.That(host.Statuses["cycle.b"].State, Is.EqualTo(PluginState.Blocked)); + Assert.That(host.Statuses["healthy"].State, Is.EqualTo(PluginState.Running)); + Run(host, host.StopGameAsync()); + } + internal static void Run(PluginHost host, Task operation) + { + var deadline = DateTime.UtcNow.AddSeconds(5); + while (!operation.IsCompleted && DateTime.UtcNow < deadline) { host.Scheduler.Pump(); Thread.Sleep(1); } + Assert.That(operation.IsCompleted, Is.True, "The owner scheduler did not complete the operation."); + operation.GetAwaiter().GetResult(); + } + } + + internal sealed class TestFactory : IPlayerPluginFactory + { + public PluginDescriptor Descriptor { get; } + public PluginSettingsSchema Schema { get; set; } = new PluginSettingsSchema(1); + public IPluginSettingsProvider? SettingsProvider { get; set; } + internal readonly List Calls; + internal string FailAt = ""; + internal int Constructions; + internal int PrepareThread; + internal bool AsyncInitialize; + internal bool CleanupWasCanceled; + internal bool NeverFinishStop; + internal Task? StopCompletion; + internal Task? InitializeCompletion; + internal Task? ApplyCompletion; + internal CancellationTokenSource? CancelDuringStart; + internal TestFactory(string id, List? calls = null) { Descriptor = new PluginDescriptor(id, id, "Test") { EnabledByDefault = true }; Calls = calls ?? new List(); } + public IPlayerPlugin Create(PluginApplicationContext context) { Constructions++; context.Lifetime.Own(new Subscription(() => Calls.Add(Descriptor.Id + ":release"))); return new TestPlugin(this); } + private sealed class TestPlugin : PlayerPlugin, IPluginReconfiguration, IPluginTick + { + private readonly TestFactory _factory; + internal TestPlugin(TestFactory factory) { _factory = factory; } + private void Call(string phase) { _factory.Calls.Add(_factory.Descriptor.Id + ":" + phase); if (_factory.FailAt == phase) throw new InvalidOperationException("Test failure"); } + public override async Task InitializeAsync(CancellationToken token) { Call("initialize"); if (_factory.AsyncInitialize) await Task.Run(() => Thread.Sleep(5), token).ConfigureAwait(false); if (_factory.InitializeCompletion != null) await _factory.InitializeCompletion; } + public override Task PrepareGameAsync(PluginGameContext game, CancellationToken token) { _factory.PrepareThread = Thread.CurrentThread.ManagedThreadId; Call("prepare"); return Task.CompletedTask; } + public override Task StartGameAsync(CancellationToken token) { Call("start"); _factory.CancelDuringStart?.Cancel(); token.ThrowIfCancellationRequested(); return Task.CompletedTask; } + public override void GameReady() => Call("ready"); + public override void PauseChanged(bool paused) => Call(paused ? "pause" : "resume"); + public void Tick(double delta) => Call("tick"); + public Task ApplyAsync(PluginSettingsSnapshot settings, CancellationToken cancellation) { Call("apply:" + settings.Integer("value")); return _factory.ApplyCompletion ?? Task.FromResult(new PluginActionResult(true, "")); } + public override Task StopGameAsync(CancellationToken token) { _factory.CleanupWasCanceled = token.IsCancellationRequested; Call("stop"); return _factory.StopCompletion ?? (_factory.NeverFinishStop ? new TaskCompletionSource().Task : Task.CompletedTask); } + public override Task ShutdownAsync(CancellationToken token) { Call("shutdown"); return Task.CompletedTask; } + public override void Dispose() => Call("dispose"); + } + } +} diff --git a/VisualPinball.Plugins.Test/PluginHostTests.cs.meta b/VisualPinball.Plugins.Test/PluginHostTests.cs.meta new file mode 100644 index 000000000..067110357 --- /dev/null +++ b/VisualPinball.Plugins.Test/PluginHostTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6df2c88707f18c342bb587998b0060c9 \ No newline at end of file diff --git a/VisualPinball.Plugins.Test/ResourceTests.cs b/VisualPinball.Plugins.Test/ResourceTests.cs new file mode 100644 index 000000000..81b5ffc16 --- /dev/null +++ b/VisualPinball.Plugins.Test/ResourceTests.cs @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using NUnit.Framework; +using VisualPinball.Plugins; + +namespace VisualPinball.Plugins.Test +{ + [TestFixture] + public sealed class ResourceTests + { + private static PluginResourceDescriptor Display(string id, long instance = 1, string controller = "game", ResourceView view = ResourceView.Original, params string[] parents) + => new PluginResourceDescriptor(id, "test", controller, id, "dmd", ResourceKind.Display, 1, instance, view, DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Rgb24, parents); + private static PluginResourceSelection Selection(ResourceView view = ResourceView.Identification) => new PluginResourceSelection(ResourceKind.Display) { Controller = "game", Session = 1, Intent = view }; + private static PluginDisplayFrame Frame(long instance, DisplayEncoding format = DisplayEncoding.Gray4) => new PluginDisplayFrame(new byte[3 * 2 * (format == DisplayEncoding.Rgb24 ? 3 : 1)], 3, 2, 3 * (format == DisplayEncoding.Rgb24 ? 3 : 1), format, 1, 0, instance); + + [Test] + public void LatePublicationNegotiatesForEachConsumerAndReleaseDoesNotReleaseAnother() + { + int leases = 0; + var registry = new PluginResourceRegistry(); + using var first = registry.Bind(Selection()); + using var second = registry.Bind(Selection()); + Assert.That(first.State, Is.EqualTo(ResourceBindingState.Waiting)); + using var source = new DisplayResourceSource((view, formats) => { leases++; return new Subscription(() => leases--); }); + using var publication = registry.Publish(Display("raw"), source); + Assert.That(leases, Is.EqualTo(2)); + first.Dispose(); Assert.That(leases, Is.EqualTo(1)); + second.Dispose(); Assert.That(leases, Is.Zero); + } + [Test] + public void LateParentFromAnotherSessionCannotMakeDerivedResourceReachable() + { + var registry = new PluginResourceRegistry(); + Assert.Throws(() => registry.Publish(Display("colored", view: ResourceView.Presentation, parents: new[] { "raw" }), new DisplayResourceSource())); + using var raw = registry.Publish(new PluginResourceDescriptor("raw", "test", "game", "raw", "dmd", ResourceKind.Display, 2, 9, + ResourceView.Original, DisplayEncoding.Gray4), new DisplayResourceSource()); + Assert.That(registry.Snapshot(Selection(ResourceView.Presentation)), Is.Empty); + } + [Test] + public void RuntimeSourceChoicesAreControllerRelativeAndDeferredWhenTheGameIsStopped() + { + var game = new GameIdentity("pinmame", "game"); + var idle = PluginResourceChoices.Displays(new PluginChoiceRequest(game, new PluginSettingsSnapshot(1), "windows"), DisplayEncoding.Gray4); + Assert.That(idle.IsComplete, Is.False); + string controller = game.Id + "/controller/0"; + var descriptors = new[] { + new PluginResourceDescriptor(controller, "pinmame", controller, "Controller", "controller", ResourceKind.Controller, 1, 1), + Display(controller + "/display/dmd0", controller: controller) + }; + var live = PluginResourceChoices.Displays(new PluginChoiceRequest(game, new PluginSettingsSnapshot(1), "windows", descriptors), DisplayEncoding.Gray4); + Assert.That(live.IsComplete, Is.True); + Assert.That(live.Find("display/dmd0"), Is.Not.Null); + Assert.That(PluginResourceChoices.Resolve(controller, "display/dmd0"), Is.EqualTo(controller + "/display/dmd0")); + var global = PluginResourceChoices.Displays(new PluginChoiceRequest(null, new PluginSettingsSnapshot(1), "windows", descriptors), DisplayEncoding.Gray4); + Assert.That(global.Choices.Count, Is.EqualTo(1)); + } + [Test] + public void IdentificationConsumersHoldNativePrecisionUntilLastLeaseLeaves() + { + var requests = new List(); + using var negotiation = new DisplayFormatNegotiator(DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8, DisplayEncoding.Luminance8, requests.Add); + using var first = negotiation.Acquire(ResourceView.Identification, DisplayEncoding.Gray2 | DisplayEncoding.Gray4); + using var second = negotiation.Acquire(ResourceView.Identification, DisplayEncoding.Gray2 | DisplayEncoding.Gray4); + first.Dispose(); + Assert.That(requests, Is.EqualTo(new[] { DisplayEncoding.Gray4 })); + second.Dispose(); + Assert.That(requests, Is.EqualTo(new[] { DisplayEncoding.Gray4, DisplayEncoding.Luminance8 })); + } + [Test] + public void AutoBlocksAmbiguityAndManualMissingNeverFallsBack() + { + var registry = new PluginResourceRegistry(); + using var source = new DisplayResourceSource(); + using var raw = registry.Publish(Display("one"), source); + using var auto = registry.Bind(Selection()); + using var other = registry.Publish(Display("two"), new DisplayResourceSource()); + Assert.That(auto.State, Is.EqualTo(ResourceBindingState.Blocked)); + var explicitChoice = Selection(); explicitChoice.ResourceId = "missing"; + using var manual = registry.Bind(explicitChoice); + Assert.That(manual.State, Is.EqualTo(ResourceBindingState.Waiting)); + other.Dispose(); Assert.That(auto.State, Is.EqualTo(ResourceBindingState.Bound)); + } + [Test] + public void PresentationChoosesDerivedResourceWhileIdentificationPreservesRaw() + { + var registry = new PluginResourceRegistry(); + using var raw = registry.Publish(Display("raw"), new DisplayResourceSource()); + using var unrelated = registry.Publish(Display("unrelated", controller: "other"), new DisplayResourceSource()); + using var transformed = registry.Publish(Display("colored", view: ResourceView.Presentation, parents: new[] { "raw" }), new DisplayResourceSource()); + using var identification = registry.Bind(Selection()); + var choice = Selection(ResourceView.Presentation); choice.AllowOriginalFallback = true; + using var presentation = registry.Bind(choice); + Assert.That(identification.Descriptor!.Id, Is.EqualTo("raw")); + Assert.That(presentation.Descriptor!.Id, Is.EqualTo("colored")); + } + [Test] + public void FirstFrameReportsActualDepthAndOldGenerationIsRejected() + { + var registry = new PluginResourceRegistry(); + using var binding = registry.Bind(Selection()); + var encodings = new List(); + using var subscription = binding.SubscribeFrames(frame => encodings.Add(frame.Encoding)); + using var old = new DisplayResourceSource(); + using var first = registry.Publish(Display("raw"), old); + old.Publish(Frame(1)); Assert.That(binding.ActualEncoding, Is.EqualTo(DisplayEncoding.Gray4)); + first.Dispose(); + using var current = new DisplayResourceSource(); + using var second = registry.Publish(Display("raw", 2), current); + old.Publish(Frame(1)); current.Publish(Frame(1)); current.Publish(Frame(2, DisplayEncoding.Gray2)); + Assert.That(encodings, Is.EqualTo(new[] { DisplayEncoding.Gray4, DisplayEncoding.Gray2 })); + } + [Test] + public void IncompatibleConcreteFormatDoesNotReachConsumer() + { + var registry = new PluginResourceRegistry(); var choice = Selection(); choice.Accepts = DisplayEncoding.Gray2; + using var source = new DisplayResourceSource(); using var published = registry.Publish(Display("raw"), source); + using var binding = registry.Bind(choice); + int count = 0; using var subscription = binding.SubscribeFrames(_ => count++); + source.Publish(Frame(1)); source.Publish(Frame(1, DisplayEncoding.Gray2)); + Assert.That(count, Is.EqualTo(1)); + } + [Test] + public void ThrowingObserverCannotLeakPublication() + { + var faults = new List(); var registry = new PluginResourceRegistry(faults.Add); + using var observer = registry.Observe(_ => throw new InvalidOperationException()); + using var source = new DisplayResourceSource(); var lease = registry.Publish(Display("raw"), source); + lease.Dispose(); Assert.That(registry.Snapshot(Selection()), Is.Empty); Assert.That(faults.Count, Is.EqualTo(2)); + } + [Test] + public void OriginalBindingsStayBoundWhenAConsumerPublishesPresentation() + { + var registry = new PluginResourceRegistry(); using var raw = registry.Publish(Display("raw"), new DisplayResourceSource()); + using var binding = registry.Bind(Selection(ResourceView.Original)); + using var derived = registry.Publish(Display("colored", view: ResourceView.Presentation, parents: new[] { "raw" }), new DisplayResourceSource()); + Assert.That(binding.State, Is.EqualTo(ResourceBindingState.Bound)); Assert.That(binding.Descriptor!.Id, Is.EqualTo("raw")); + } + [Test] + public void DerivedResourcesCannotSurviveTheirParentGeneration() + { + var registry = new PluginResourceRegistry(); var raw = registry.Publish(Display("raw"), new DisplayResourceSource()); + using var derived = registry.Publish(Display("colored", view: ResourceView.Presentation, parents: new[] { "raw" }), new DisplayResourceSource()); + using var binding = registry.Bind(Selection(ResourceView.Presentation)); + Assert.That(binding.State, Is.EqualTo(ResourceBindingState.Bound)); raw.Dispose(); + Assert.That(binding.State, Is.EqualTo(ResourceBindingState.Waiting)); + using var replacement = registry.Publish(Display("raw", 2), new DisplayResourceSource()); + Assert.That(binding.State, Is.EqualTo(ResourceBindingState.Waiting)); + } + [Test] + public void PhysicalOutputHasOneOwnerAndCanBeReacquiredAfterCleanup() + { + var outputs = new PluginOutputLeases(); using var first = outputs.Acquire("serial:COM3", "one"); + Assert.Throws(() => outputs.Acquire("serial:COM3", "two")); + first.Dispose(); using var second = outputs.Acquire("serial:COM3", "two"); + Assert.That(outputs.Owner("serial:COM3"), Is.EqualTo("two")); + } + } +} diff --git a/VisualPinball.Plugins.Test/ResourceTests.cs.meta b/VisualPinball.Plugins.Test/ResourceTests.cs.meta new file mode 100644 index 000000000..30a6e99dd --- /dev/null +++ b/VisualPinball.Plugins.Test/ResourceTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 33ed990c98d58ca408a075b29e5858b4 \ No newline at end of file diff --git a/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.asmdef b/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.asmdef new file mode 100644 index 000000000..3c0843135 --- /dev/null +++ b/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.asmdef @@ -0,0 +1,10 @@ +{ + "name": "VisualPinball.Plugins.Test", + "references": ["VisualPinball.Plugins", "Newtonsoft.Json"], + "overrideReferences": true, + "precompiledReferences": ["nunit.framework.dll", "Newtonsoft.Json.dll"], + "includePlatforms": ["Editor"], + "optionalUnityReferences": ["TestAssemblies"], + "autoReferenced": false, + "defineConstraints": ["UNITY_INCLUDE_TESTS"] +} diff --git a/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.asmdef.meta b/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.asmdef.meta new file mode 100644 index 000000000..74d6541a8 --- /dev/null +++ b/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9a9b6d3aca94a0f4c916bc2af7beda90 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj b/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj new file mode 100644 index 000000000..212f6a503 --- /dev/null +++ b/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj @@ -0,0 +1,16 @@ + + + net10.0 + 9.0 + enable + true + false + + + + + + + + + diff --git a/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj.meta b/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj.meta new file mode 100644 index 000000000..540b08821 --- /dev/null +++ b/VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 83cddfc6a256d734c9f6df015b7e1f39 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Plugins/Api.meta b/VisualPinball.Plugins/Api.meta new file mode 100644 index 000000000..4902f15bb --- /dev/null +++ b/VisualPinball.Plugins/Api.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8b1489d83b525a046b98397ef0dc3adf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Plugins/Api/GameIdentityProvider.cs b/VisualPinball.Plugins/Api/GameIdentityProvider.cs new file mode 100644 index 000000000..9bf126a7c --- /dev/null +++ b/VisualPinball.Plugins/Api/GameIdentityProvider.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; + +namespace VisualPinball.Plugins +{ + public interface IGameIdentityProvider { GameIdentity? GameIdentity { get; } } + /// Reads only the provider's inert package payload; it must not initialize its native runtime. + public interface IPackagedGameIdentityReader + { + string ComponentType { get; } + GameIdentity? Read(ReadOnlyMemory payload); + } + public static class PackagedGameIdentityReaders + { + private static readonly Dictionary Readers = new Dictionary(StringComparer.Ordinal); + public static void Reset() => Readers.Clear(); + public static void Register(IPackagedGameIdentityReader reader) => Readers.Add(reader.ComponentType, reader); + public static IPackagedGameIdentityReader? Find(string componentType) => Readers.TryGetValue(componentType, out var reader) ? reader : null; + } +} diff --git a/VisualPinball.Plugins/Api/GameIdentityProvider.cs.meta b/VisualPinball.Plugins/Api/GameIdentityProvider.cs.meta new file mode 100644 index 000000000..3e0664b50 --- /dev/null +++ b/VisualPinball.Plugins/Api/GameIdentityProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3b793c3929bc93f458f85916e7b5ef12 \ No newline at end of file diff --git a/VisualPinball.Plugins/Api/PluginDescriptor.cs b/VisualPinball.Plugins/Api/PluginDescriptor.cs new file mode 100644 index 000000000..6a6781c26 --- /dev/null +++ b/VisualPinball.Plugins/Api/PluginDescriptor.cs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; + +namespace VisualPinball.Plugins +{ + public enum PluginPhase { Initialize, Prepare, Start, Ready, Pause, Resume, Focus, Tick, Render, Stop, Shutdown } + public enum PluginState { Available, Disabled, Unsupported, Blocked, Starting, Running, Stopping, Faulted, Unresponsive } + public enum ConfigurationScope { GlobalOnly, GameOnly, GlobalAndGame } + public enum ConfigurationChange { Live, NextGame, ApplicationRestart } + + public sealed class PluginOrder + { + public int Priority { get; set; } + public IReadOnlyList Before { get; set; } = Array.Empty(); + public IReadOnlyList After { get; set; } = Array.Empty(); + } + + public sealed class PluginDescriptor + { + public const int HostApiVersion = 1; + public string Id { get; } + public string Name { get; } + public string Description { get; } + public string Version { get; set; } = "0.1.0"; + public int ApiVersion { get; set; } = HostApiVersion; + public bool EnabledByDefault { get; set; } + public string Category { get; set; } = "Integrations"; + public int Order { get; set; } + public IReadOnlyList DependsOn { get; set; } = Array.Empty(); + public IReadOnlyList Before { get; set; } = Array.Empty(); + public IReadOnlyList After { get; set; } = Array.Empty(); + public IReadOnlyList Provides { get; set; } = Array.Empty(); + public IReadOnlyList Requires { get; set; } = Array.Empty(); + public IReadOnlyDictionary PhaseOrder { get; set; } = new Dictionary(); + public IReadOnlyList Platforms { get; set; } = Array.Empty(); + + public PluginDescriptor(string id, string name, string description) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("A plugin needs a stable ID.", nameof(id)); + foreach (char character in id) + if (!(character >= 'a' && character <= 'z') && !(character >= '0' && character <= '9') && character != '.' && character != '-') + throw new ArgumentException("Plugin IDs use lowercase letters, digits, dots and hyphens.", nameof(id)); + if (id == "." || id == "..") throw new ArgumentException("Invalid plugin ID.", nameof(id)); + Id = id; + Name = name; + Description = description; + } + } + + public sealed class PluginStatus + { + public PluginState State { get; internal set; } = PluginState.Available; + public string Message { get; internal set; } = ""; + public PluginPhase? Phase { get; internal set; } + } + + public readonly struct GameIdentity : IEquatable + { + public string Provider { get; } + public string Key { get; } + public string Id => Provider + ":" + Key; + public GameIdentity(string provider, string key) + { + if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(key)) throw new ArgumentException("A game identity needs a provider and key."); + Provider = provider.Trim().ToLowerInvariant(); + Key = key.Trim().ToLowerInvariant(); + } + public bool Equals(GameIdentity other) => Provider == other.Provider && Key == other.Key; + public override bool Equals(object? other) => other is GameIdentity identity && Equals(identity); + public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Id); + public override string ToString() => Id; + } +} diff --git a/VisualPinball.Plugins/Api/PluginDescriptor.cs.meta b/VisualPinball.Plugins/Api/PluginDescriptor.cs.meta new file mode 100644 index 000000000..d2bfa712b --- /dev/null +++ b/VisualPinball.Plugins/Api/PluginDescriptor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cab68cf30e70f9d41bf86665c536c321 \ No newline at end of file diff --git a/VisualPinball.Plugins/Api/PluginRuntime.cs b/VisualPinball.Plugins/Api/PluginRuntime.cs new file mode 100644 index 000000000..8bf94ec4c --- /dev/null +++ b/VisualPinball.Plugins/Api/PluginRuntime.cs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace VisualPinball.Plugins +{ + public interface IPlayerPluginFactory + { + PluginDescriptor Descriptor { get; } + PluginSettingsSchema Schema { get; } + IPluginSettingsProvider? SettingsProvider { get; } + IPlayerPlugin Create(PluginApplicationContext context); + } + + public interface IPlayerPlugin : IDisposable + { + Task InitializeAsync(CancellationToken cancellation); + Task PrepareGameAsync(PluginGameContext game, CancellationToken cancellation); + Task StartGameAsync(CancellationToken cancellation); + void GameReady(); + void PauseChanged(bool paused); + void FocusChanged(bool focused); + Task StopGameAsync(CancellationToken cancellation); + Task ShutdownAsync(CancellationToken cancellation); + } + public interface IPluginTick { void Tick(double unscaledSeconds); } + public interface IPluginRender { void BeforeRender(); } + public interface IPluginEmergencyStop { void EmergencyStop(); } + /// After emergency stop, completes only when native workers can no longer use the plugin's resources. + public interface IPluginDeferredCleanup { Task Quiescence { get; } } + /// Reconfiguration is transactional: failure or cancellation must leave the previous runtime state intact. + public interface IPluginReconfiguration { Task ApplyAsync(PluginSettingsSnapshot settings, CancellationToken cancellation); } + + /// Implement only the hooks a plugin needs; the host still controls its lifetime. + public abstract class PlayerPlugin : IPlayerPlugin + { + public virtual Task InitializeAsync(CancellationToken cancellation) => Task.CompletedTask; + public virtual Task PrepareGameAsync(PluginGameContext game, CancellationToken cancellation) => Task.CompletedTask; + public virtual Task StartGameAsync(CancellationToken cancellation) => Task.CompletedTask; + public virtual void GameReady() { } + public virtual void PauseChanged(bool paused) { } + public virtual void FocusChanged(bool focused) { } + public virtual Task StopGameAsync(CancellationToken cancellation) => Task.CompletedTask; + public virtual Task ShutdownAsync(CancellationToken cancellation) => Task.CompletedTask; + public virtual void Dispose() { } + } + + public sealed class PluginApplicationContext + { + public PluginDescriptor Plugin { get; } + public PluginConfigurationStore Configuration { get; } + public OwnerScheduler Scheduler { get; } + public PluginCapabilities Capabilities { get; } + public PluginResourceRegistry Resources { get; } + public ResourceScope Lifetime { get; } + public Action Log { get; } + public PluginApplicationContext(PluginDescriptor plugin, PluginConfigurationStore configuration, OwnerScheduler scheduler, + PluginCapabilities capabilities, PluginResourceRegistry resources, ResourceScope lifetime, Action log) + { Plugin = plugin; Configuration = configuration; Scheduler = scheduler; Capabilities = capabilities; Resources = resources; Lifetime = lifetime; Log = log; } + } + + public sealed class PluginGameContext + { + public GameIdentity Identity { get; } + public string PackagePath { get; } + public string Title { get; } + public string ControllerId { get; } + public long Session { get; } + public PluginSettingsSnapshot Settings { get; } + public PluginCapabilities Capabilities { get; } + public PluginResourceRegistry Resources { get; } + public ResourceScope Lifetime { get; } + public CancellationToken Cancellation { get; } + public PluginGameContext(GameIdentity identity, string packagePath, string title, string controllerId, long session, + PluginSettingsSnapshot settings, PluginCapabilities capabilities, PluginResourceRegistry resources, ResourceScope lifetime, CancellationToken cancellation) + { Identity = identity; PackagePath = packagePath; Title = title; ControllerId = controllerId; Session = session; Settings = settings; Capabilities = capabilities; Resources = resources; Lifetime = lifetime; Cancellation = cancellation; } + } + + public static class PlayerPluginRegistry + { + private static readonly Dictionary Factories = new Dictionary(StringComparer.Ordinal); + public static IReadOnlyCollection Installed => Factories.Values; + public static void Reset() => Factories.Clear(); + public static void Register(IPlayerPluginFactory factory) + { + if (factory.Descriptor.ApiVersion != PluginDescriptor.HostApiVersion) throw new InvalidOperationException("Plugin API version is incompatible: " + factory.Descriptor.Id); + if (Factories.ContainsKey(factory.Descriptor.Id)) throw new InvalidOperationException("Duplicate plugin ID: " + factory.Descriptor.Id); + Factories.Add(factory.Descriptor.Id, factory); + } + } +} diff --git a/VisualPinball.Plugins/Api/PluginRuntime.cs.meta b/VisualPinball.Plugins/Api/PluginRuntime.cs.meta new file mode 100644 index 000000000..c85d62899 --- /dev/null +++ b/VisualPinball.Plugins/Api/PluginRuntime.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ff27c3a59a165c14784ea0e1c17a9c20 \ No newline at end of file diff --git a/VisualPinball.Plugins/Configuration.meta b/VisualPinball.Plugins/Configuration.meta new file mode 100644 index 000000000..692572554 --- /dev/null +++ b/VisualPinball.Plugins/Configuration.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6b4415fc3ad430744adc70ce2443a8f1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs new file mode 100644 index 000000000..173499ffa --- /dev/null +++ b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace VisualPinball.Plugins +{ + /// One plugin owns one folder; game overrides contain only explicitly changed values. + public sealed class PluginConfigurationStore + { + public string Root { get; } + public PluginConfigurationStore(string root) { Root = Path.GetFullPath(root); } + public string DirectoryFor(PluginDescriptor plugin) => Path.Combine(Root, plugin.Id); + public string SettingsPath(PluginDescriptor plugin, GameIdentity? game = null) => game.HasValue + ? Path.Combine(DirectoryFor(plugin), "games", Hash(game.Value.Id) + ".json") : Path.Combine(DirectoryFor(plugin), "settings.json"); + public PluginSettingsSnapshot Read(PluginDescriptor plugin, PluginSettingsSchema schema, GameIdentity? game = null) + { + var document = ReadDocument(SettingsPath(plugin, game)); + if (game.HasValue && document["game"] != null && document.Value("game") != game.Value.Id) + throw new InvalidDataException("The saved profile belongs to another game."); + return schema.Upgrade(new PluginSettingsSnapshot(document.Value("schemaVersion") ?? schema.Version, (JObject?)document["values"]?.DeepClone() ?? new JObject())); + } + public PluginSettingsSnapshot Effective(PluginDescriptor plugin, PluginSettingsSchema schema, GameIdentity? game = null) + { + var global = Read(plugin, schema); + return Resolve(schema, global, game.HasValue ? Read(plugin, schema, game) : null); + } + /// Known fields are restricted to their declared scope; unknown values remain available for compatible upgrades. + public static PluginSettingsSnapshot Resolve(PluginSettingsSchema schema, PluginSettingsSnapshot global, PluginSettingsSnapshot? game = null) + { + PluginSettingsSnapshot Scoped(PluginSettingsSnapshot values, bool gameScope) + { + var scoped = values.Copy(); + foreach (var field in schema.Fields) + if (gameScope ? field.Scope == ConfigurationScope.GlobalOnly : field.Scope == ConfigurationScope.GameOnly) scoped.Values.Remove(field.Id); + return scoped; + } + return game == null ? PluginSettingsSnapshot.Merge(schema.Defaults(), Scoped(global, false)) + : PluginSettingsSnapshot.Merge(schema.Defaults(), Scoped(global, false), Scoped(game, true)); + } + public void Save(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSettingsSnapshot values, GameIdentity? game = null) + { + string path = SettingsPath(plugin, game); + var document = ReadDocument(path); + if ((document.Value("schemaVersion") ?? schema.Version) > schema.Version) + throw new InvalidOperationException("This configuration was written by a newer plugin. It is preserved and cannot be overwritten."); + var stored = Read(plugin, schema, game).Values; + foreach (var field in schema.Fields) { + bool allowed = game.HasValue ? field.Scope != ConfigurationScope.GlobalOnly : field.Scope != ConfigurationScope.GameOnly; + if (!allowed || field.Kind == SettingKind.Action || field.Kind == SettingKind.Status || field.Kind == SettingKind.Custom) continue; + if (values.Values.TryGetValue(field.Id, out var value)) stored[field.Id] = value.DeepClone(); + else stored.Remove(field.Id); + } + document["schemaVersion"] = schema.Version; + document["values"] = stored; + if (game.HasValue) document["game"] = game.Value.Id; + WriteDocument(path, document); + } + public bool Enabled(PluginDescriptor plugin, GameIdentity? game = null) + { + var policy = ReadDocument(Path.Combine(DirectoryFor(plugin), "policy.json")); + if (game.HasValue && policy["games"] is JObject games && games[game.Value.Id]?.Type == JTokenType.Boolean) return games.Value(game.Value.Id); + return policy.Value("enabled") ?? plugin.EnabledByDefault; + } + public void SetEnabled(PluginDescriptor plugin, bool? enabled, GameIdentity? game = null) + { + string path = Path.Combine(DirectoryFor(plugin), "policy.json"); + var policy = ReadDocument(path); + if (game.HasValue) { + var games = policy["games"] as JObject ?? new JObject(); + if (enabled.HasValue) games[game.Value.Id] = enabled.Value; else games.Remove(game.Value.Id); + policy["games"] = games; + } else if (enabled.HasValue) policy["enabled"] = enabled.Value; else policy.Remove("enabled"); + WriteDocument(path, policy); + } + public bool? EnableOverride(PluginDescriptor plugin, GameIdentity game) => ReadDocument(Path.Combine(DirectoryFor(plugin), "policy.json"))["games"]?[game.Id]?.Value(); + + internal static JObject ReadDocument(string path) + { + if (!File.Exists(path)) return new JObject(); + using var stream = File.OpenText(path); + using var reader = new JsonTextReader(stream) { DateParseHandling = DateParseHandling.None, MaxDepth = 64, FloatParseHandling = FloatParseHandling.Double }; + return JObject.Load(reader); + } + internal static void WriteDocument(string path, JObject document) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + string pending = path + "." + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + ".tmp"; + try { + using (var file = new StreamWriter(pending, false, new UTF8Encoding(false))) + using (var writer = new JsonTextWriter(file) { Formatting = Formatting.Indented }) document.WriteTo(writer); + if (File.Exists(path)) File.Replace(pending, path, null); else File.Move(pending, path); + } finally { if (File.Exists(pending)) File.Delete(pending); } + } + public static string Hash(string identity) + { + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(identity)); + var result = new StringBuilder(bytes.Length * 2); + foreach (byte value in bytes) result.Append(value.ToString("x2", CultureInfo.InvariantCulture)); + return result.ToString(); + } + } +} diff --git a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs.meta b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs.meta new file mode 100644 index 000000000..78ded1e00 --- /dev/null +++ b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 15f5d564abf8bd0428332a46d3435ea9 \ No newline at end of file diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs b/VisualPinball.Plugins/Configuration/PluginSchema.cs new file mode 100644 index 000000000..9ac9f58d1 --- /dev/null +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace VisualPinball.Plugins +{ + public enum SettingKind { Boolean, Integer, Number, Text, Folder, File, Choice, Color, Rectangle, Collection, Status, Action, Custom } + + public sealed class PluginField + { + public string Id { get; } + public string Label { get; } + public SettingKind Kind { get; } + public string Description { get; set; } = ""; + public ConfigurationScope Scope { get; set; } = ConfigurationScope.GlobalOnly; + public ConfigurationChange Change { get; set; } = ConfigurationChange.NextGame; + public JToken? Default { get; set; } + public double? Minimum { get; set; } + public double? Maximum { get; set; } + public string Provider { get; set; } = ""; + public string ViewExtension { get; set; } = ""; + public string VisibleWhen { get; set; } = ""; + public JToken? VisibleValue { get; set; } + public IReadOnlyList Children { get; set; } = Array.Empty(); + public Func? Validate { get; set; } + public PluginField(string id, string label, SettingKind kind) { Id = id; Label = label; Kind = kind; } + public bool Visible(PluginSettingsSnapshot snapshot) => VisibleWhen.Length == 0 || JToken.DeepEquals(snapshot.Value(VisibleWhen), VisibleValue ?? new JValue(true)); + } + + public sealed class PluginSettingsGroup + { + public string Title { get; } + public string Description { get; } + public IReadOnlyList Fields { get; } + public PluginSettingsGroup(string title, string description, params PluginField[] fields) { Title = title; Description = description; Fields = fields; } + } + + public readonly struct PluginValidationIssue + { + public string Field { get; } + public string Message { get; } + public PluginValidationIssue(string field, string message) { Field = field; Message = message; } + } + + public sealed class PluginSettingsSchema + { + public int Version { get; } + public IReadOnlyList Groups { get; } + /// Each entry upgrades a detached values tree from its key to the next version. + public IReadOnlyDictionary> Migrations { get; set; } = new Dictionary>(); + public IEnumerable Fields => Groups.SelectMany(group => group.Fields); + public PluginSettingsSchema(int version, params PluginSettingsGroup[] groups) + { + if (version < 1) throw new ArgumentOutOfRangeException(nameof(version)); + Version = version; + Groups = groups; + var seen = new HashSet(StringComparer.Ordinal); + foreach (var field in Fields) if (!seen.Add(field.Id)) throw new ArgumentException("Duplicate plugin field " + field.Id + "."); + } + public PluginSettingsSnapshot Upgrade(PluginSettingsSnapshot snapshot) + { + var values = (JObject)snapshot.Values.DeepClone(); + for (int version = snapshot.SchemaVersion; version < Version; version++) { + if (!Migrations.TryGetValue(version, out var migrate)) throw new InvalidOperationException("No configuration migration from version " + version + "."); + values = migrate(values) ?? throw new InvalidOperationException("Configuration migration returned no values."); + } + return new PluginSettingsSnapshot(Math.Max(snapshot.SchemaVersion, Version), values); + } + public PluginSettingsSnapshot Defaults() + { + var values = new JObject(); + foreach (var field in Fields) if (field.Default != null) values[field.Id] = field.Default.DeepClone(); + return new PluginSettingsSnapshot(Version, values); + } + public IReadOnlyList Validate(PluginSettingsSnapshot snapshot) + { + var issues = new List(); + foreach (var field in Fields) { + if (!field.Visible(snapshot) || field.Kind == SettingKind.Status || field.Kind == SettingKind.Action || field.Kind == SettingKind.Custom) continue; + var value = snapshot.Value(field.Id); + string? error = field.Validate?.Invoke(value); + if (error == null && value != null) { + if (field.Kind == SettingKind.Boolean && value.Type != JTokenType.Boolean) error = "Choose on or off."; + if (field.Kind == SettingKind.Integer && value.Type != JTokenType.Integer) error = "Enter a whole number."; + if (field.Kind == SettingKind.Number || field.Kind == SettingKind.Integer) { + if (value.Type != JTokenType.Float && value.Type != JTokenType.Integer) error = "Enter a number."; + else { + double number = value.Value(); + if (double.IsNaN(number) || double.IsInfinity(number) || field.Minimum.HasValue && number < field.Minimum || field.Maximum.HasValue && number > field.Maximum) + error = "Value is outside the supported range."; + } + } + if (field.Kind == SettingKind.Collection && value.Type != JTokenType.Array) error = "Expected a list."; + if ((field.Kind == SettingKind.Text || field.Kind == SettingKind.File || field.Kind == SettingKind.Folder || field.Kind == SettingKind.Choice || field.Kind == SettingKind.Color) && value.Type != JTokenType.String) error = "Expected text."; + if (field.Kind == SettingKind.Rectangle) { + if (!(value is JObject rectangle)) error = "Expected a rectangle."; + else foreach (string dimension in new[] { "x", "y", "width", "height" }) { + var coordinate = rectangle[dimension]; + if (coordinate == null || coordinate.Type != JTokenType.Integer && coordinate.Type != JTokenType.Float || !double.IsFinite(coordinate.Value()) || (dimension == "width" || dimension == "height") && coordinate.Value() <= 0) error = "Enter valid coordinates and positive dimensions."; + } + } + } + if (error != null) issues.Add(new PluginValidationIssue(field.Id, error)); + } + return issues; + } + } + + public sealed class PluginSettingsSnapshot + { + public int SchemaVersion { get; } + public JObject Values { get; } + public PluginSettingsSnapshot(int schemaVersion, JObject? values = null) { SchemaVersion = schemaVersion; Values = values ?? new JObject(); } + public JToken? Value(string id) => Values[id]; + public string String(string id, string fallback = "") => Value(id)?.Type == JTokenType.String ? Value(id)!.Value() ?? fallback : fallback; + public bool Boolean(string id, bool fallback = false) => Value(id)?.Type == JTokenType.Boolean ? Value(id)!.Value() : fallback; + public int Integer(string id, int fallback = 0) => Value(id)?.Type == JTokenType.Integer ? Value(id)!.Value() : fallback; + public double Number(string id, double fallback = 0) => Value(id)?.Type == JTokenType.Integer || Value(id)?.Type == JTokenType.Float ? Value(id)!.Value() : fallback; + public PluginSettingsSnapshot Copy() => new PluginSettingsSnapshot(SchemaVersion, (JObject)Values.DeepClone()); + public void Set(string id, JToken? value) { if (value == null) Values.Remove(id); else Values[id] = value.DeepClone(); } + public static PluginSettingsSnapshot Merge(PluginSettingsSnapshot defaults, params PluginSettingsSnapshot[] layers) + { + var merged = defaults.Copy(); + foreach (var layer in layers) foreach (var value in layer.Values.Properties()) merged.Values[value.Name] = value.Value.DeepClone(); + return merged; + } + } + + public sealed class PluginChoice + { + public string Id { get; } + public string Label { get; } + public string Description { get; } + public bool Available { get; } + public string UnavailableReason { get; } + public PluginChoice(string id, string label, string description = "", bool available = true, string unavailableReason = "") + { Id = id; Label = label; Description = description; Available = available; UnavailableReason = unavailableReason; } + } + + public sealed class PluginChoiceRequest + { + public GameIdentity? Game { get; } + public PluginSettingsSnapshot Settings { get; } + public string Platform { get; } + public IReadOnlyList Resources { get; } + public PluginChoiceRequest(GameIdentity? game, PluginSettingsSnapshot settings, string platform, IReadOnlyList? resources = null) + { Game = game; Settings = settings; Platform = platform; Resources = resources ?? Array.Empty(); } + } + + public sealed class PluginChoiceCatalog + { + public string Revision { get; } + public IReadOnlyList Choices { get; } + /// False when a runtime-only catalog cannot be inspected while its game is stopped. + public bool IsComplete { get; } + public PluginChoiceCatalog(string revision, IReadOnlyList choices, bool isComplete = true) { Revision = revision; Choices = choices; IsComplete = isComplete; } + public PluginChoice? Find(string id) => Choices.FirstOrDefault(choice => choice.Id == id); + } + + public readonly struct PluginActionResult + { + public bool Success { get; } + public string Message { get; } + public PluginSettingsSnapshot? Changes { get; } + public PluginActionResult(bool success, string message, PluginSettingsSnapshot? changes = null) { Success = success; Message = message; Changes = changes; } + } + + public interface IPluginSettingsProvider + { + Task DiscoverAsync(string provider, PluginChoiceRequest request, CancellationToken cancellation); + Task RunActionAsync(string action, PluginChoiceRequest request, CancellationToken cancellation); + IReadOnlyList Validate(PluginChoiceRequest request); + } +} diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs.meta b/VisualPinball.Plugins/Configuration/PluginSchema.cs.meta new file mode 100644 index 000000000..80062cfcb --- /dev/null +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d54a7d1915fd76d40ab751f55035e8b4 \ No newline at end of file diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs new file mode 100644 index 000000000..4d5faa075 --- /dev/null +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace VisualPinball.Plugins +{ + /// A detached draft. Game drafts contain only overrides, and Apply rechecks dynamic catalogs. + public sealed class PluginSettingsSession : IDisposable + { + private readonly IPlayerPluginFactory _factory; + private readonly PluginConfigurationStore _store; + private readonly string _platform; + private readonly Func> _resources; + private readonly SemaphoreSlim _apply = new SemaphoreSlim(1, 1); + private readonly CancellationTokenSource _lifetime = new CancellationTokenSource(); + public GameIdentity? Game { get; } + public PluginSettingsSnapshot Draft { get; private set; } + public PluginSettingsSnapshot Effective => Game.HasValue + ? PluginConfigurationStore.Resolve(_factory.Schema, _store.Read(_factory.Descriptor, _factory.Schema), Draft) + : PluginConfigurationStore.Resolve(_factory.Schema, Draft); + public PluginSettingsSession(IPlayerPluginFactory factory, PluginConfigurationStore store, string platform, GameIdentity? game = null, Func>? resources = null) + { _factory = factory; _store = store; _platform = platform; Game = game; _resources = resources ?? (() => Array.Empty()); Draft = store.Read(factory.Descriptor, factory.Schema, game); } + public void Set(string fieldId, JToken? value) + { + var field = _factory.Schema.Fields.First(field => field.Id == fieldId); + if (Game.HasValue ? field.Scope == ConfigurationScope.GlobalOnly : field.Scope == ConfigurationScope.GameOnly) throw new InvalidOperationException("This field is not configurable in the selected scope."); + Draft.Set(fieldId, value); + } + public void Revert() => Draft = _store.Read(_factory.Descriptor, _factory.Schema, Game); + public void Reset() => Draft = new PluginSettingsSnapshot(_factory.Schema.Version); + public IReadOnlyList Validate() + { + var effective = Effective; + return _factory.Schema.Validate(effective).Concat(_factory.SettingsProvider?.Validate(new PluginChoiceRequest(Game, effective, _platform)) ?? Array.Empty()).ToArray(); + } + public Task DiscoverAsync(string provider, CancellationToken cancellation) + => _factory.SettingsProvider?.DiscoverAsync(provider, new PluginChoiceRequest(Game, Effective, _platform, _resources()), cancellation) ?? Task.FromResult(new PluginChoiceCatalog("empty", Array.Empty())); + public async Task RunActionAsync(string action, CancellationToken cancellation) + { + if (_factory.SettingsProvider == null) return new PluginActionResult(false, "This action is unavailable."); + var result = await _factory.SettingsProvider.RunActionAsync(action, new PluginChoiceRequest(Game, Effective, _platform), cancellation); + cancellation.ThrowIfCancellationRequested(); + if (result.Success && result.Changes != null) foreach (var field in _factory.Schema.Fields) { + if (Game.HasValue ? field.Scope == ConfigurationScope.GlobalOnly : field.Scope == ConfigurationScope.GameOnly) continue; + if (result.Changes.Values.TryGetValue(field.Id, out var value)) Set(field.Id, value); + } + return result; + } + public async Task ApplyAsync(CancellationToken cancellation = default, PluginHost? host = null) + { + using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellation, _lifetime.Token); + await _apply.WaitAsync(linked.Token).ConfigureAwait(false); + try { + // Freeze the draft before any asynchronous discovery so editing cannot change the validated transaction. + var draft = Draft.Copy(); + var effective = Game.HasValue ? PluginConfigurationStore.Resolve(_factory.Schema, _store.Read(_factory.Descriptor, _factory.Schema), draft) : PluginConfigurationStore.Resolve(_factory.Schema, draft); + var request = new PluginChoiceRequest(Game, effective, _platform, _resources()); + var issues = _factory.Schema.Validate(effective).Concat(_factory.SettingsProvider?.Validate(request) ?? Array.Empty()).ToList(); + foreach (var field in _factory.Schema.Fields.Where(field => field.Kind == SettingKind.Choice && field.Provider.Length > 0 && field.Visible(effective))) { + string selected = effective.String(field.Id); + if (selected.Length == 0 || _factory.SettingsProvider == null) continue; + var catalog = await _factory.SettingsProvider.DiscoverAsync(field.Provider, request, linked.Token).ConfigureAwait(false); + var choice = catalog.Find(selected); + if (choice != null && !choice.Available || choice == null && catalog.IsComplete) issues.Add(new PluginValidationIssue(field.Id, choice?.UnavailableReason ?? "Selected item is missing.")); + } + linked.Token.ThrowIfCancellationRequested(); + if (issues.Count > 0) return new PluginActionResult(false, string.Join("\n", issues.Select(issue => issue.Field + ": " + issue.Message))); + if (host != null) return await host.CommitSettingsAsync(_factory, draft, Game, linked.Token).ConfigureAwait(false); + _store.Save(_factory.Descriptor, _factory.Schema, draft, Game); + return new PluginActionResult(true, "Saved. Changes take effect on the next game."); + } finally { _apply.Release(); } + } + public void Dispose() { _lifetime.Cancel(); } + } +} diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs.meta b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs.meta new file mode 100644 index 000000000..bcad47816 --- /dev/null +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 232fe61a30136af4f980e21193b2d61d \ No newline at end of file diff --git a/VisualPinball.Plugins/LICENSE b/VisualPinball.Plugins/LICENSE new file mode 100644 index 000000000..d0efc58e0 --- /dev/null +++ b/VisualPinball.Plugins/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Visual Pinball Engine contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/VisualPinball.Plugins/LICENSE.meta b/VisualPinball.Plugins/LICENSE.meta new file mode 100644 index 000000000..dd54ac3f2 --- /dev/null +++ b/VisualPinball.Plugins/LICENSE.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 26a7165f639bb1b48a7f9e19cfc501ca +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Plugins/README.md b/VisualPinball.Plugins/README.md new file mode 100644 index 000000000..bfc2008e9 --- /dev/null +++ b/VisualPinball.Plugins/README.md @@ -0,0 +1,39 @@ +# Player plugin contracts + +This MIT-licensed .NET Standard 2.1 library defines the player plugin API without a Unity dependency. Unity compiles the same source through `VisualPinball.Plugins.asmdef`; portable tests run through `dotnet test VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj`. + +An optional integration package registers an `IPlayerPluginFactory`. Its descriptor declares a stable ID, default enablement, platforms, hard dependencies, required/provided capabilities, numeric order, and optional before/after relationships. Its schema and settings provider describe configuration without loading native libraries. The shared player creates the settings UI and constructs runtime instances only for enabled, supported plugins. + +## Lifecycle + +The host resolves dependencies before numeric order, checks every phase for cycles, and blocks affected plugins while independent integrations continue. Hooks start on the owner thread. An asynchronous hook returning from worker code must switch through `PluginApplicationContext.Scheduler` before accessing Unity. Prepare and Start run while the restored table is inactive, before its gamelogic can emit its first event. Ready follows `Player.OnPlayerStarted`; pause, resume, focus, tick and render use their own phase order. + +Instances live for one game. The application context scope covers initialization through shutdown; the game scope covers preparation through stop. Own subscriptions, publications and handles in these scopes. Cleanup unwinds actual startup order in reverse, uses fresh cancellation deadlines, and continues after failures. An unresponsive asynchronous hook cannot hold the host forever. Its plugin ID remains quarantined until the abandoned hook, deferred native work and shutdown finish, preventing overlapping instances. Healthy plugins keep ticking and rendering while an unrelated integration is being reconfigured or cleaned up. `IPluginEmergencyStop` must close event admission immediately and arrange safe deferred native disposal when a worker is still active. Native devices must retain exclusive output leases until their worker has stopped. Implement `IPluginDeferredCleanup.Quiescence` to report that lifetime to the host. + +Static Unity registries reset at SubsystemRegistration. Player applications claim ownership before automatic bridges run, so removing or disabling an adapter cannot resurrect an old watcher. Table authoring projects keep their existing automatic integration behavior. + +## Configuration + +Each plugin owns a directory under the player's `plugins` configuration root. `settings.json` stores global values; `policy.json` stores default and per-game enablement; hashed files under `games` store sparse overrides associated with a provider-qualified `GameIdentity`. This configuration does not enter `player-config.json`. + +Fields declare GlobalOnly, GameOnly or GlobalAndGame scope and Live, NextGame or ApplicationRestart behavior. Known fields stored in the wrong scope cannot affect runtime settings. Unknown values and document properties survive saves. Migrations operate on detached JSON trees; files from a future schema remain readable and cannot be overwritten. No persisted value names a CLR type. + +`PluginSettingsSession` owns a detached draft. Apply freezes and validates it, rechecks dynamic catalogs, and persists atomically. With a host, live reconfiguration and persistence share the lifecycle gate. `IPluginReconfiguration` must implement transactional updates: a failure or cancellation leaves the previous runtime state intact. If persistence fails after a live update, the host requests the previous runtime settings. Other values take effect at the next game or application restart. + +Dynamic choices use stable IDs, readable labels, availability reasons and a catalog revision. Missing explicit asset selections remain visible and require user action. A runtime-only catalog may declare `IsComplete = false` while its game is stopped; this keeps an existing source selection without claiming it is available. Explicit display selections are game-specific and controller-relative. `PluginResourceChoices` supplies the shared discovery and resolution rules. + +The generic player supports standard fields, lists, rectangles, actions, inheritance, Apply/Revert/Reset, and runtime diagnostics. Unity adapters register custom settings views for richer controls such as desktop placement and pack previews; the player knows only the view contract. Custom views own and release preview resources when hidden or detached. + +## Cross-plugin resources + +VPE owns the payload contracts for original and presentation DMD frames, controller events, PCM audio, segment brightness, input, triggers and GPU surfaces. Descriptors identify logical source, provider, controller, session, concrete instance, role, supported formats and derivation parents. Publish a parent before its derived resource. A parent replacement invalidates the old derived lineage; consumers cannot use frames from an earlier instance. + +Bindings follow logical resources across publication/removal. Auto selects the unique compatible source and blocks ambiguity. An explicit missing source waits for that source and never substitutes a different one. Identification always uses a parentless original source. Presentation prefers a derived surface; original fallback is opt-in. Identification leases preserve native 2/4-bit shade indices and hold provider-wide raw mode until the last consumer leaves. Frames report their actual encoding; incompatible concrete frames are rejected with a diagnostic. + +Display pixels, PCM samples and segment buffers are borrowed during synchronous callbacks. Copy only when crossing a thread boundary. Display/controller bridges deliver on the Unity owner thread; native PCM can be delivered on its producer thread, and its source synchronizes subscriptions. Consumers must keep producer callbacks bounded. Native destinations use `PluginOutputLeases` to reserve physical outputs. Input stages use `PluginInputPipeline`; a producer applies its ordered filters before dispatching the resulting input event. + +## Package composition and validation + +`VisualPinball.Plugins` contains contracts, configuration and the host. `VisualPinball.Unity/VisualPinball.Unity/Plugins` bridges scene identities and gamelogic resources. The shared player renders configuration and owns transitions. Optional PinUp and DMD player packages own all product-specific discovery, runtime bindings and custom UI. HDRP/URP shells choose these packages in their manifests. + +The portable suite covers failure isolation, ordering, cancellation/deadlines, sparse profiles, migrations, future versions, missing choices, raw/presentation routing, generation changes, output exclusivity, audio replacement, input ordering and representative resource families. Native desktop and hardware behavior must also be tested on the target platform; portable tests do not validate a Metal compositor, Linux desktop or attached cabinet device. diff --git a/VisualPinball.Plugins/README.md.meta b/VisualPinball.Plugins/README.md.meta new file mode 100644 index 000000000..447c8a893 --- /dev/null +++ b/VisualPinball.Plugins/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6a9b7cfd10bad004fa4d0160a2f14f0a +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Plugins/Resources.meta b/VisualPinball.Plugins/Resources.meta new file mode 100644 index 000000000..60793bf19 --- /dev/null +++ b/VisualPinball.Plugins/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 582b4fdee8188da43828d73b3c2a65a0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs new file mode 100644 index 000000000..7fb17f922 --- /dev/null +++ b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +using System; + +namespace VisualPinball.Plugins +{ + /// Provider-group raw mode is held until the last identification consumer leaves. + public sealed class DisplayFormatNegotiator : IDisposable + { + private readonly DisplayEncoding _default; + private readonly DisplayEncoding _supported; + private readonly Action _request; + private int _identification; + private bool _disposed; + public DisplayFormatNegotiator(DisplayEncoding supported, DisplayEncoding defaultFormat, Action request) + { _supported = supported; _default = defaultFormat; _request = request; } + public IDisposable Acquire(ResourceView intent, DisplayEncoding accepted) + { + if (_disposed) throw new ObjectDisposedException(nameof(DisplayFormatNegotiator)); + if (intent != ResourceView.Identification) return new Subscription(() => { }); + var raw = _supported & accepted & (DisplayEncoding.Gray2 | DisplayEncoding.Gray4); + if (raw == DisplayEncoding.None) throw new InvalidOperationException("This source cannot provide unmodified identification frames."); + if (_identification++ == 0) { + try { _request((_supported & DisplayEncoding.Gray4) != 0 ? DisplayEncoding.Gray4 : DisplayEncoding.Gray2); } + catch { _identification--; throw; } + } + return new Subscription(() => { if (!_disposed && --_identification == 0) _request(_default); }); + } + public void Dispose() { if (_disposed) return; _disposed = true; if (_identification > 0) _request(_default); _identification = 0; } + } +} diff --git a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs.meta b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs.meta new file mode 100644 index 000000000..46c0a103b --- /dev/null +++ b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a1cb27766373724468867a22e2e89211 \ No newline at end of file diff --git a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs new file mode 100644 index 000000000..407637731 --- /dev/null +++ b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; + +namespace VisualPinball.Plugins +{ + /// Cached subscriber arrays make steady-state borrowed frame dispatch allocation-free. + public sealed class DisplayResourceSource : IPluginDisplaySource, IDisposable + { + private readonly List> _subscribers = new List>(); + private Action[] _dispatch = Array.Empty>(); + private readonly Func? _negotiate; + private readonly Action? _fault; + private bool _disposed; + public ResourceKind Kind => ResourceKind.Display; + public DisplayResourceSource(Func? negotiate = null, Action? fault = null) + { _negotiate = negotiate; _fault = fault; } + public IDisposable Subscribe(Action receive) + { + if (_disposed) throw new ObjectDisposedException(nameof(DisplayResourceSource)); + _subscribers.Add(receive); + _dispatch = _subscribers.ToArray(); + return new Subscription(() => { _subscribers.Remove(receive); _dispatch = _subscribers.ToArray(); }); + } + public IDisposable AcquireView(ResourceView intent, DisplayEncoding acceptedFormats) => _negotiate?.Invoke(intent, acceptedFormats) ?? new Subscription(() => { }); + public void Publish(in PluginDisplayFrame frame) + { + if (_disposed) return; + foreach (var receive in _dispatch) + try { receive(frame); } catch (Exception error) { if (_fault == null) throw; _fault(error); } + } + public void Dispose() { _disposed = true; _subscribers.Clear(); _dispatch = Array.Empty>(); } + } + + public sealed class ControllerResourceSource : IPluginControllerSource, IDisposable + { + private readonly Action? _fault; + private readonly List> _subscribers = new List>(); + private Action[] _dispatch = Array.Empty>(); + private bool _disposed; + public ResourceKind Kind => ResourceKind.Controller; + public ControllerResourceSource(Action? fault = null) { _fault = fault; } + public IDisposable Subscribe(Action receive) + { + if (_disposed) throw new ObjectDisposedException(nameof(ControllerResourceSource)); + _subscribers.Add(receive); _dispatch = _subscribers.ToArray(); + return new Subscription(() => { _subscribers.Remove(receive); _dispatch = _subscribers.ToArray(); }); + } + public void Publish(in PluginControllerEvent value) + { + if (_disposed) return; + foreach (var receive in _dispatch) + try { receive(value); } catch (Exception error) { if (_fault == null) throw; _fault(error); } + } + public void Dispose() { _disposed = true; _subscribers.Clear(); _dispatch = Array.Empty>(); } + } +} diff --git a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs.meta b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs.meta new file mode 100644 index 000000000..bf666d5e0 --- /dev/null +++ b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 80a214d041804d64e8ef8230fe0de094 \ No newline at end of file diff --git a/VisualPinball.Plugins/Resources/PluginResourceChoices.cs b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs new file mode 100644 index 000000000..37386d6b7 --- /dev/null +++ b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Linq; + +namespace VisualPinball.Plugins +{ + /// Saved resource choices are relative to a game's controller and can be kept while that game is stopped. + public static class PluginResourceChoices + { + public static PluginChoiceCatalog Displays(PluginChoiceRequest request, DisplayEncoding formats) + { + var automatic = new PluginChoice("auto", "Automatic · this game's DMD"); + if (!request.Game.HasValue) return new PluginChoiceCatalog("global", new[] { automatic }); + string controller = request.Game.Value.Id + "/controller/0"; + var choices = request.Resources.Where(resource => resource.Kind == ResourceKind.Display && resource.View == ResourceView.Original + && resource.Parents.Count == 0 && resource.Controller == controller && resource.Role == "dmd" && (resource.Formats & formats) != 0) + .Select(resource => new PluginChoice(resource.Id.Substring(controller.Length + 1), resource.Label)).Prepend(automatic).ToArray(); + return new PluginChoiceCatalog(string.Join("|", choices.Select(choice => choice.Id)), choices, request.Resources.Any(resource => resource.Kind == ResourceKind.Controller && resource.Controller == controller)); + } + public static string Resolve(string controller, string relative) + { + if (!relative.StartsWith("display/", StringComparison.Ordinal) || relative.Contains("..") || relative.Contains(":") || relative.Contains("\\")) + throw new ArgumentException("Select a display belonging to this game's controller."); + return controller + "/" + relative; + } + } +} diff --git a/VisualPinball.Plugins/Resources/PluginResourceChoices.cs.meta b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs.meta new file mode 100644 index 000000000..b4bf44592 --- /dev/null +++ b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1f1eb7ad8e27f344599ed1486450e5fe \ No newline at end of file diff --git a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs new file mode 100644 index 000000000..e6bd5ab35 --- /dev/null +++ b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Linq; + +namespace VisualPinball.Plugins +{ + public sealed class PluginResourceRegistry + { + internal sealed class Entry + { + internal readonly PluginResourceDescriptor Descriptor; + internal readonly IPluginResourceSource Source; + internal readonly Dictionary ParentInstances; + internal Entry(PluginResourceDescriptor descriptor, IPluginResourceSource source, Dictionary parents) { Descriptor = descriptor; Source = source; ParentInstances = parents; } + } + private readonly Dictionary _entries = new Dictionary(StringComparer.Ordinal); + private readonly List> _observers = new List>(); + private readonly Action _fault; + public PluginResourceRegistry(Action? fault = null) { _fault = fault ?? (_ => { }); } + internal void Report(Exception error) => _fault(error); + public IReadOnlyList Snapshot() => _entries.Values.Where(entry => Reachable(entry, new HashSet())).Select(entry => entry.Descriptor).OrderBy(descriptor => descriptor.Id, StringComparer.Ordinal).ToArray(); + public IReadOnlyList Snapshot(PluginResourceSelection selection) => Candidates(selection).Select(entry => entry.Descriptor).ToArray(); + internal IReadOnlyList Candidates(PluginResourceSelection selection) + { + var found = _entries.Values.Where(entry => selection.Matches(entry.Descriptor) && Reachable(entry, new HashSet())).OrderBy(entry => entry.Descriptor.Id, StringComparer.Ordinal).ToList(); + if (selection.ResourceId.Length == 0 && selection.Intent == ResourceView.Presentation && found.Any(entry => entry.Descriptor.View == ResourceView.Presentation)) + found.RemoveAll(entry => entry.Descriptor.View != ResourceView.Presentation); + return found; + } + public IDisposable Publish(PluginResourceDescriptor descriptor, T source) where T : IPluginResourceSource + { + if (descriptor.Kind != source.Kind) throw new ArgumentException("Resource kind and VPE source interface disagree."); + if (descriptor.Kind == ResourceKind.Display && !(source is IPluginDisplaySource) + || descriptor.Kind == ResourceKind.Controller && !(source is IPluginControllerSource) + || descriptor.Kind == ResourceKind.Audio && !(source is IPluginAudioSource) + || descriptor.Kind == ResourceKind.Segments && !(source is IPluginSegmentSource) + || descriptor.Kind == ResourceKind.Input && !(source is IPluginInputSource) + || descriptor.Kind == ResourceKind.Surface && !(source is IPluginSurface) + || descriptor.Kind == ResourceKind.Triggers && !(source is IPluginTriggerSource)) throw new ArgumentException("The resource must implement its VPE payload contract."); + if (_entries.ContainsKey(descriptor.Id)) throw new InvalidOperationException("Resource already published: " + descriptor.Id); + foreach (string parent in descriptor.Parents) + if (!_entries.ContainsKey(parent)) throw new InvalidOperationException("Publish the source before its derived resource: " + parent); + foreach (string parent in descriptor.Parents) + if (parent == descriptor.Id || Reaches(parent, descriptor.Id, new HashSet())) throw new InvalidOperationException("Resource derivation cycle: " + descriptor.Id); + var parentInstances = descriptor.Parents.ToDictionary(id => id, id => _entries.TryGetValue(id, out var parent) ? parent.Descriptor.Instance : 0L, StringComparer.Ordinal); + var entry = new Entry(descriptor, source, parentInstances); + _entries.Add(descriptor.Id, entry); + Notify(new PluginResourceChange(ResourceChangeKind.Added, descriptor)); + return new Subscription(() => { + if (_entries.TryGetValue(descriptor.Id, out var current) && ReferenceEquals(current, entry)) { + _entries.Remove(descriptor.Id); Notify(new PluginResourceChange(ResourceChangeKind.Removed, descriptor)); + } + }); + } + private bool Reaches(string from, string target, HashSet visited) => visited.Add(from) && _entries.TryGetValue(from, out var entry) + && entry.Descriptor.Parents.Any(parent => parent == target || Reaches(parent, target, visited)); + private bool Reachable(Entry entry, HashSet path) + { + if (!path.Add(entry.Descriptor.Id)) return false; + foreach (string parentId in entry.Descriptor.Parents) { + if (!_entries.TryGetValue(parentId, out var parent) || !Reachable(parent, path)) return false; + if (parent.Descriptor.Session != entry.Descriptor.Session) return false; + long expected = entry.ParentInstances[parentId]; + if (expected != parent.Descriptor.Instance) return false; + path.Remove(parentId); + } + return true; + } + public IDisposable Observe(Action changed) + { + _observers.Add(changed); + return new Subscription(() => _observers.Remove(changed)); + } + private void Notify(PluginResourceChange change) { foreach (var observer in _observers.ToArray()) try { observer(change); } catch (Exception error) { Report(error); } } + public PluginResourceBinding Bind(PluginResourceSelection selection) where T : class, IPluginResourceSource => new PluginResourceBinding(this, selection); + } + + /// A binding follows its selected logical resource and owns view negotiation for the current instance. + public sealed class PluginResourceBinding : IDisposable where T : class, IPluginResourceSource + { + private readonly PluginResourceRegistry _registry; + private readonly PluginResourceSelection _selection; + private readonly IDisposable _watch; + private IDisposable? _view; + private readonly List> _receivers = new List>(); + private readonly List _attached = new List(); + public T? Source { get; private set; } + public PluginResourceDescriptor? Descriptor { get; private set; } + public ResourceBindingState State { get; private set; } + public string Diagnostic { get; private set; } = ""; + public DisplayEncoding ActualEncoding { get; private set; } + public DisplayEncoding AcceptedFormats => _selection.Accepts; + public event Action? Changed; + internal PluginResourceBinding(PluginResourceRegistry registry, PluginResourceSelection selection) + { + _registry = registry; _selection = selection.Copy(); + _watch = registry.Observe(_ => Refresh()); + Refresh(); + } + internal bool AcceptFrame(in PluginDisplayFrame frame) + { + if (State != ResourceBindingState.Bound || Descriptor == null || frame.Instance != Descriptor.Instance) return false; + bool compatible = (frame.Encoding & _selection.Accepts) != 0; + string diagnostic = compatible ? "" : "Source is producing " + frame.Encoding + "; accepted formats: " + _selection.Accepts + "."; + if (ActualEncoding != frame.Encoding || Diagnostic != diagnostic) { + ActualEncoding = frame.Encoding; Diagnostic = diagnostic; + try { Changed?.Invoke(); } catch (Exception error) { _registry.Report(error); } + } + return compatible; + } + public IDisposable Connect(Func subscribe) + { + if (State == ResourceBindingState.Disposed) throw new ObjectDisposedException(nameof(PluginResourceBinding)); + _receivers.Add(subscribe); + RefreshAttachments(); + return new Subscription(() => { _receivers.Remove(subscribe); RefreshAttachments(); }); + } + private void Refresh() + { + if (State == ResourceBindingState.Disposed) return; + var candidates = _registry.Candidates(_selection).Where(entry => entry.Source is T).ToArray(); + if (candidates.Length == 1 && ReferenceEquals(Source, candidates[0].Source) && Descriptor?.Instance == candidates[0].Descriptor.Instance) return; + Detach(); + if (candidates.Length == 0) { State = ResourceBindingState.Waiting; Diagnostic = _selection.ResourceId.Length == 0 ? "Waiting for a compatible resource." : "Missing resource: " + _selection.ResourceId; } + else if (candidates.Length > 1) { State = ResourceBindingState.Blocked; Diagnostic = "Select a resource: " + string.Join(", ", candidates.Select(entry => entry.Descriptor.Id)); } + else { + Source = (T)candidates[0].Source; + Descriptor = candidates[0].Descriptor; + try { + _view = (Source as IPluginDisplaySource)?.AcquireView(_selection.Intent, _selection.Accepts); + State = ResourceBindingState.Bound; Diagnostic = ""; RefreshAttachments(); + } catch (Exception error) { Detach(); State = ResourceBindingState.Blocked; Diagnostic = error.Message; } + } + try { Changed?.Invoke(); } catch (Exception error) { _registry.Report(error); } + } + private void RefreshAttachments() + { + ClearAttachments(); + if (Source != null && State != ResourceBindingState.Disposed) + foreach (var receive in _receivers) _attached.Add(receive(Source)); + } + private void Detach() + { + ClearAttachments(); + try { _view?.Dispose(); } catch (Exception error) { _registry.Report(error); } + _view = null; Source = null; Descriptor = null; ActualEncoding = DisplayEncoding.None; + } + private void ClearAttachments() + { + foreach (var attached in _attached) try { attached.Dispose(); } catch (Exception error) { _registry.Report(error); } + _attached.Clear(); + } + public void Dispose() { if (State == ResourceBindingState.Disposed) return; _watch.Dispose(); Detach(); _receivers.Clear(); State = ResourceBindingState.Disposed; Changed = null; } + } + + public static class DisplayBindingExtensions + { + /// Checks the concrete encoding and generation before delivering borrowed pixels. + public static IDisposable SubscribeFrames(this PluginResourceBinding binding, Action receive) + => binding.Connect(source => source.Subscribe(frame => { if (binding.AcceptFrame(frame)) receive(frame); })); + } +} diff --git a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs.meta b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs.meta new file mode 100644 index 000000000..1ee824411 --- /dev/null +++ b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 264d29b3f89386e47937efa2917ce357 \ No newline at end of file diff --git a/VisualPinball.Plugins/Resources/ResourceContracts.cs b/VisualPinball.Plugins/Resources/ResourceContracts.cs new file mode 100644 index 000000000..d850656c8 --- /dev/null +++ b/VisualPinball.Plugins/Resources/ResourceContracts.cs @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; + +namespace VisualPinball.Plugins +{ + public enum ResourceKind { Controller, Display, Segments, Audio, Input, Surface, Triggers } + public enum ResourceView { Original, Identification, Presentation } + public enum ResourceBindingState { Waiting, Bound, Blocked, Disposed } + public enum ResourceChangeKind { Added, Changed, Removed } + [Flags] public enum DisplayEncoding { None = 0, Gray2 = 1, Gray4 = 2, Luminance8 = 4, Rgb24 = 8, Rgba32 = 16 } + public enum FrameColorSpace { Linear, Srgb } + public enum FrameAlpha { None, Straight, Premultiplied } + public enum PcmEncoding { Int16LittleEndian, Float32 } + public enum ControllerEventKind { Switch, Coil, Lamp, GeneralIllumination, Trigger, State } + + public sealed class PluginResourceDescriptor + { + public string Id { get; } + public string Provider { get; } + public string Controller { get; } + public string Label { get; } + public string Role { get; } + public ResourceKind Kind { get; } + public long Session { get; } + public long Instance { get; } + public int ContractVersion { get; } = 1; + public ResourceView View { get; } + public DisplayEncoding Formats { get; } + public IReadOnlyList Parents { get; } + public PluginResourceDescriptor(string id, string provider, string controller, string label, string role, ResourceKind kind, + long session, long instance, ResourceView view = ResourceView.Original, DisplayEncoding formats = DisplayEncoding.None, params string[] parents) + { + if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("A resource needs a stable logical ID.", nameof(id)); + Id = id; Provider = provider; Controller = controller; Label = label; Role = role; Kind = kind; + Session = session; Instance = instance; View = view; Formats = formats; Parents = Array.AsReadOnly((string[])parents.Clone()); + } + } + + public sealed class PluginResourceSelection + { + public ResourceKind Kind { get; } + public string ResourceId { get; set; } = ""; + public string Controller { get; set; } = ""; + public string Role { get; set; } = ""; + public long Session { get; set; } + public ResourceView Intent { get; set; } = ResourceView.Original; + public DisplayEncoding Accepts { get; set; } = DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8 | DisplayEncoding.Rgb24 | DisplayEncoding.Rgba32; + public bool AllowOriginalFallback { get; set; } + public PluginResourceSelection(ResourceKind kind) { Kind = kind; } + public PluginResourceSelection Copy() => new PluginResourceSelection(Kind) { + ResourceId = ResourceId, Controller = Controller, Role = Role, Session = Session, + Intent = Intent, Accepts = Accepts, AllowOriginalFallback = AllowOriginalFallback + }; + internal bool Matches(PluginResourceDescriptor descriptor) + { + if (descriptor.Kind != Kind || ResourceId.Length > 0 && descriptor.Id != ResourceId || Controller.Length > 0 && descriptor.Controller != Controller + || Role.Length > 0 && descriptor.Role != Role || Session != 0 && descriptor.Session != Session) return false; + if (Kind == ResourceKind.Display && (descriptor.Formats & Accepts) == 0) return false; + if (Intent == ResourceView.Identification) return descriptor.View != ResourceView.Presentation && descriptor.Parents.Count == 0; + if (Intent == ResourceView.Presentation) return descriptor.View == ResourceView.Presentation || AllowOriginalFallback && descriptor.View == ResourceView.Original; + return descriptor.View == ResourceView.Original && descriptor.Parents.Count == 0; + } + } + + public readonly struct PluginResourceChange + { + public ResourceChangeKind Kind { get; } + public PluginResourceDescriptor Descriptor { get; } + public PluginResourceChange(ResourceChangeKind kind, PluginResourceDescriptor descriptor) { Kind = kind; Descriptor = descriptor; } + } + + public interface IPluginResourceSource { ResourceKind Kind { get; } } + + /// Borrowed row-major frame memory is valid only during the synchronous subscriber call. + public readonly struct PluginDisplayFrame + { + public ReadOnlyMemory Pixels { get; } + public int Width { get; } + public int Height { get; } + public int Stride { get; } + public DisplayEncoding Encoding { get; } + public FrameColorSpace ColorSpace { get; } + public FrameAlpha Alpha { get; } + public long Sequence { get; } + public long TimestampUs { get; } + public long Instance { get; } + public PluginDisplayFrame(ReadOnlyMemory pixels, int width, int height, int stride, DisplayEncoding encoding, + long sequence, long timestampUs, long instance, FrameColorSpace colorSpace = FrameColorSpace.Srgb, FrameAlpha alpha = FrameAlpha.None) + { + int bytes = encoding == DisplayEncoding.Rgb24 ? 3 : encoding == DisplayEncoding.Rgba32 ? 4 : 1; + if (encoding != DisplayEncoding.Gray2 && encoding != DisplayEncoding.Gray4 && encoding != DisplayEncoding.Luminance8 + && encoding != DisplayEncoding.Rgb24 && encoding != DisplayEncoding.Rgba32) throw new ArgumentException("A frame needs one concrete VPE encoding."); + if (width <= 0 || height <= 0 || stride < checked(width * bytes) || pixels.Length < checked(stride * height)) throw new ArgumentException("Display frame shape does not match its buffer."); + Pixels = pixels; Width = width; Height = height; Stride = stride; Encoding = encoding; Sequence = sequence; + TimestampUs = timestampUs; Instance = instance; ColorSpace = colorSpace; Alpha = alpha; + } + } + + public interface IPluginDisplaySource : IPluginResourceSource + { + IDisposable Subscribe(Action receive); + /// Acquiring an identification view owns format negotiation until the returned lease is disposed. + IDisposable AcquireView(ResourceView intent, DisplayEncoding acceptedFormats); + } + + public readonly struct PluginControllerEvent + { + public ControllerEventKind Kind { get; } + public string Id { get; } + public double Value { get; } + public long Sequence { get; } + public long TimestampUs { get; } + public PluginControllerEvent(ControllerEventKind kind, string id, double value, long sequence, long timestampUs) + { Kind = kind; Id = id; Value = value; Sequence = sequence; TimestampUs = timestampUs; } + } + public interface IPluginControllerSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } + + public readonly struct PluginPcmFrame + { + public ReadOnlyMemory Samples { get; } + public PcmEncoding Encoding { get; } + public int SampleRate { get; } + public int Channels { get; } + public int Frames { get; } + public long Sequence { get; } + public long TimestampUs { get; } + public string ChannelLayout { get; } + public PluginPcmFrame(ReadOnlyMemory samples, PcmEncoding encoding, int sampleRate, int channels, int frames, long sequence, long timestampUs = 0, string channelLayout = "") + { + if (sampleRate <= 0 || channels <= 0 || frames < 0 || samples.Length != checked(frames * channels * (encoding == PcmEncoding.Float32 ? 4 : 2))) + throw new ArgumentException("PCM frame shape does not match its samples."); + Samples = samples; Encoding = encoding; SampleRate = sampleRate; Channels = channels; Frames = frames; Sequence = sequence; TimestampUs = timestampUs; ChannelLayout = channelLayout; + } + } + public interface IPluginAudioSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } + /// An exclusive lease replaces built-in playback while leaving the original PCM stream available. + public interface IPluginAudioPlaybackControl { IDisposable AcquirePlayback(string pluginId); } + public interface IPluginSegmentSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } + public readonly struct PluginSegmentFrame + { + public string Layout { get; } + public int Elements { get; } + public int SegmentsPerElement { get; } + public ReadOnlyMemory Brightness { get; } + public PluginSegmentFrame(string layout, int elements, int segmentsPerElement, ReadOnlyMemory brightness) + { + if (elements <= 0 || segmentsPerElement <= 0 || brightness.Length != checked(elements * segmentsPerElement)) throw new ArgumentException("Segment layout does not match its state."); + Layout = layout; Elements = elements; SegmentsPerElement = segmentsPerElement; Brightness = brightness; + } + } + public readonly struct PluginInputEvent + { + public string Action { get; } + public float Value { get; } + public long TimestampUs { get; } + public PluginInputEvent(string action, float value, long timestampUs) { Action = action; Value = value; TimestampUs = timestampUs; } + } + public interface IPluginInputSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } + public interface IPluginInputFilter { PluginInputEvent Filter(in PluginInputEvent input); } + public interface IPluginSurface : IPluginResourceSource { int Width { get; } int Height { get; } string Backend { get; } } + public readonly struct PluginTriggerEvent + { + public string Id { get; } + public int Value { get; } + public long Sequence { get; } + public long TimestampUs { get; } + public PluginTriggerEvent(string id, int value, long sequence, long timestampUs) { Id = id; Value = value; Sequence = sequence; TimestampUs = timestampUs; } + } + public interface IPluginTriggerSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } +} diff --git a/VisualPinball.Plugins/Resources/ResourceContracts.cs.meta b/VisualPinball.Plugins/Resources/ResourceContracts.cs.meta new file mode 100644 index 000000000..79217f9d4 --- /dev/null +++ b/VisualPinball.Plugins/Resources/ResourceContracts.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9bba39e37954afe4ab5781464468f07d \ No newline at end of file diff --git a/VisualPinball.Plugins/Resources/ResourceSignal.cs b/VisualPinball.Plugins/Resources/ResourceSignal.cs new file mode 100644 index 000000000..b332c237f --- /dev/null +++ b/VisualPinball.Plugins/Resources/ResourceSignal.cs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Threading; + +namespace VisualPinball.Plugins +{ + /// Subscription changes take a lock; synchronous producer dispatch reads a cached array. + internal sealed class ResourceSignal : IDisposable where T : struct + { + private readonly List> _subscribers = new List>(); + private readonly Action _fault; + private Action[] _dispatch = Array.Empty>(); + private bool _disposed; + internal ResourceSignal(Action? fault = null) { _fault = fault ?? (_ => { }); } + internal bool HasSubscribers => Volatile.Read(ref _dispatch).Length > 0; + internal IDisposable Subscribe(Action receive) + { + lock (_subscribers) { + if (_disposed) throw new ObjectDisposedException(nameof(ResourceSignal)); + _subscribers.Add(receive); Volatile.Write(ref _dispatch, _subscribers.ToArray()); + } + return new Subscription(() => { lock (_subscribers) { _subscribers.Remove(receive); Volatile.Write(ref _dispatch, _subscribers.ToArray()); } }); + } + internal void Publish(in T frame) { foreach (var receive in Volatile.Read(ref _dispatch)) try { receive(frame); } catch (Exception error) { _fault(error); } } + public void Dispose() { lock (_subscribers) { _disposed = true; _subscribers.Clear(); Volatile.Write(ref _dispatch, Array.Empty>()); } } + } + public sealed class AudioResourceSource : IPluginAudioSource, IPluginAudioPlaybackControl, IDisposable + { + private readonly ResourceSignal _signal; + private readonly PluginOutputLeases _output = new PluginOutputLeases(); + public ResourceKind Kind => ResourceKind.Audio; + public bool HasSubscribers => _signal.HasSubscribers; + public bool PlaybackReplaced => _output.Owner("playback") != null; + public AudioResourceSource(Action? fault = null) { _signal = new ResourceSignal(fault); } + public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); + public IDisposable AcquirePlayback(string pluginId) => _output.Acquire("playback", pluginId); + public void Publish(in PluginPcmFrame frame) => _signal.Publish(frame); + public void Dispose() => _signal.Dispose(); + } + public sealed class SegmentResourceSource : IPluginSegmentSource, IDisposable + { + private readonly ResourceSignal _signal; + public ResourceKind Kind => ResourceKind.Segments; + public SegmentResourceSource(Action? fault = null) { _signal = new ResourceSignal(fault); } + public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); + public void Publish(in PluginSegmentFrame frame) => _signal.Publish(frame); + public void Dispose() => _signal.Dispose(); + } + public sealed class InputResourceSource : IPluginInputSource, IDisposable + { + private readonly ResourceSignal _signal; + public ResourceKind Kind => ResourceKind.Input; + public InputResourceSource(Action? fault = null) { _signal = new ResourceSignal(fault); } + public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); + public void Publish(in PluginInputEvent value) => _signal.Publish(value); + public void Dispose() => _signal.Dispose(); + } + public sealed class TriggerResourceSource : IPluginTriggerSource, IDisposable + { + private readonly ResourceSignal _signal; + public ResourceKind Kind => ResourceKind.Triggers; + public TriggerResourceSource(Action? fault = null) { _signal = new ResourceSignal(fault); } + public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); + public void Publish(in PluginTriggerEvent value) => _signal.Publish(value); + public void Dispose() => _signal.Dispose(); + } +} diff --git a/VisualPinball.Plugins/Resources/ResourceSignal.cs.meta b/VisualPinball.Plugins/Resources/ResourceSignal.cs.meta new file mode 100644 index 000000000..47466c4c3 --- /dev/null +++ b/VisualPinball.Plugins/Resources/ResourceSignal.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c22e71bda21f38f4ebc548e93cca3cb6 \ No newline at end of file diff --git a/VisualPinball.Plugins/Runtime.meta b/VisualPinball.Plugins/Runtime.meta new file mode 100644 index 000000000..a372b03c1 --- /dev/null +++ b/VisualPinball.Plugins/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7393f22928505a84eb4c5247093c13b7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Plugins/Runtime/OwnerScheduler.cs b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs new file mode 100644 index 000000000..5c8e3aeb9 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace VisualPinball.Plugins +{ + /// An explicit owner-thread queue. Awaiting Switch returns to this owner without relying on Unity's synchronization context. + public sealed class OwnerScheduler + { + private readonly int _thread = Thread.CurrentThread.ManagedThreadId; + private readonly ConcurrentQueue _pending = new ConcurrentQueue(); + public bool IsOwner => Thread.CurrentThread.ManagedThreadId == _thread; + public void VerifyOwner() { if (!IsOwner) throw new InvalidOperationException("This plugin operation belongs on the owner thread."); } + public void Post(Action action) => _pending.Enqueue(action); + public void Pump(int budget = 4096) + { + VerifyOwner(); + for (int i = 0; i < budget && _pending.TryDequeue(out var action); i++) action(); + } + public OwnerAwaitable Switch() => new OwnerAwaitable(this); + public readonly struct OwnerAwaitable + { + private readonly OwnerScheduler _scheduler; + internal OwnerAwaitable(OwnerScheduler scheduler) { _scheduler = scheduler; } + public Awaiter GetAwaiter() => new Awaiter(_scheduler); + public readonly struct Awaiter : INotifyCompletion + { + private readonly OwnerScheduler _scheduler; + internal Awaiter(OwnerScheduler scheduler) { _scheduler = scheduler; } + public bool IsCompleted => _scheduler.IsOwner; + public void OnCompleted(Action continuation) => _scheduler.Post(continuation); + public void GetResult() => _scheduler.VerifyOwner(); + } + } + } +} diff --git a/VisualPinball.Plugins/Runtime/OwnerScheduler.cs.meta b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs.meta new file mode 100644 index 000000000..a6a732e88 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0a862295931df97488382c4cb1508673 \ No newline at end of file diff --git a/VisualPinball.Plugins/Runtime/PluginCapabilities.cs b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs new file mode 100644 index 000000000..a3cb0fa2f --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; + +namespace VisualPinball.Plugins +{ + /// Versioned service contracts, separate from concrete display/audio resources. + public sealed class PluginCapabilities + { + private readonly Dictionary _services = new Dictionary(StringComparer.Ordinal); + public IEnumerable Ids => _services.Keys; + public IDisposable Publish(string id, T service) where T : class + { + if (_services.ContainsKey(id)) throw new InvalidOperationException("Capability already provided: " + id); + _services.Add(id, service); + return new Subscription(() => { if (_services.TryGetValue(id, out var current) && ReferenceEquals(current, service)) _services.Remove(id); }); + } + public T? Find(string id) where T : class => _services.TryGetValue(id, out var value) ? value as T : null; + public T Require(string id) where T : class => Find(id) ?? throw new InvalidOperationException("Required capability is unavailable: " + id); + } +} diff --git a/VisualPinball.Plugins/Runtime/PluginCapabilities.cs.meta b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs.meta new file mode 100644 index 000000000..c720b8248 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f12964f9611c06d47b0d75e8cf8af734 \ No newline at end of file diff --git a/VisualPinball.Plugins/Runtime/PluginEvents.cs b/VisualPinball.Plugins/Runtime/PluginEvents.cs new file mode 100644 index 000000000..125bd305f --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginEvents.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Threading; + +namespace VisualPinball.Plugins +{ + /// Typed control notifications delivered on the host owner. Borrowed frames use their source contracts instead. + public sealed class PluginEvents + { + private interface IChannel { } + private sealed class Channel : IChannel where T : struct + { + internal readonly List> Subscribers = new List>(); + internal Action[] Dispatch = Array.Empty>(); + } + private readonly Dictionary _channels = new Dictionary(); + private readonly OwnerScheduler _scheduler; + private readonly Action _fault; + public PluginEvents(OwnerScheduler scheduler, Action? fault = null) { _scheduler = scheduler; _fault = fault ?? (_ => { }); } + public IDisposable Subscribe(Action receive) where T : struct + { + _scheduler.VerifyOwner(); + if (!_channels.TryGetValue(typeof(T), out var found)) _channels.Add(typeof(T), found = new Channel()); + var channel = (Channel)found; + channel.Subscribers.Add(receive); channel.Dispatch = channel.Subscribers.ToArray(); + return new Subscription(() => { _scheduler.VerifyOwner(); channel.Subscribers.Remove(receive); channel.Dispatch = channel.Subscribers.ToArray(); }); + } + public void Publish(in T value) where T : struct + { + _scheduler.VerifyOwner(); + if (!_channels.TryGetValue(typeof(T), out var found)) return; + foreach (var receive in ((Channel)found).Dispatch) try { receive(value); } catch (Exception error) { _fault(error); } + } + public void Post(T value, CancellationToken lifetime = default) where T : struct => _scheduler.Post(() => { + if (!lifetime.IsCancellationRequested) Publish(value); + }); + } +} diff --git a/VisualPinball.Plugins/Runtime/PluginEvents.cs.meta b/VisualPinball.Plugins/Runtime/PluginEvents.cs.meta new file mode 100644 index 000000000..cfca6fdd2 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginEvents.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7c77b47fea6f63f4a8ec2c72fa24662a \ No newline at end of file diff --git a/VisualPinball.Plugins/Runtime/PluginHost.cs b/VisualPinball.Plugins/Runtime/PluginHost.cs new file mode 100644 index 000000000..edcc967d7 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginHost.cs @@ -0,0 +1,420 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace VisualPinball.Plugins +{ + /// Serializes game hooks, resolves policy before constructing plugins, and owns reverse cleanup. + public sealed class PluginHost + { + private sealed class HookDeadlineException : TimeoutException + { + internal Task Hook { get; } + internal HookDeadlineException(Task hook) : base("Plugin hook did not finish before its cancellation deadline.") { Hook = hook; } + } + private sealed class Instance + { + internal readonly IPlayerPluginFactory Factory; + internal readonly IPlayerPlugin Plugin; + internal readonly ResourceScope ApplicationScope; + internal readonly ResourceScope GameScope = new ResourceScope(); + internal bool Prepared; + internal bool Cleaned; + internal bool Busy; + internal PluginSettingsSnapshot? Settings; + internal Instance(IPlayerPluginFactory factory, IPlayerPlugin plugin, ResourceScope applicationScope) { Factory = factory; Plugin = plugin; ApplicationScope = applicationScope; } + } + private readonly Dictionary _factories; + private readonly Dictionary _statuses; + private readonly List _entered = new List(); + private readonly List _startOrder = new List(); + private readonly SemaphoreSlim _operation = new SemaphoreSlim(1, 1); + private readonly Dictionary _abandoned = new Dictionary(); + private readonly Action _log; + private CancellationTokenSource? _gameCancellation; + private bool _running; + private bool _ready; + private bool? _paused; + private bool? _focused; + private GameIdentity? _activeGame; + private string[] _hostCapabilities = Array.Empty(); + private readonly Dictionary _dispatch = new Dictionary(); + private Task _faultCleanup = Task.CompletedTask; + public OwnerScheduler Scheduler { get; } + public PluginConfigurationStore Configuration { get; } + public PluginCapabilities Capabilities { get; } + public PluginResourceRegistry Resources { get; } + public string Platform { get; } + public IReadOnlyDictionary Statuses => _statuses; + public IReadOnlyCollection Factories => _factories.Values; + public bool IsRunning => _running; + public bool IsTransitioning => _operation.CurrentCount == 0; + public TimeSpan CleanupTimeout { get; set; } = TimeSpan.FromSeconds(10); + public TimeSpan StartupTimeout { get; set; } = TimeSpan.FromSeconds(30); + public PluginHost(IEnumerable factories, PluginConfigurationStore configuration, OwnerScheduler scheduler, + PluginCapabilities capabilities, PluginResourceRegistry resources, string platform, Action? log = null) + { + _factories = factories.ToDictionary(factory => factory.Descriptor.Id, StringComparer.Ordinal); + _statuses = _factories.Keys.ToDictionary(id => id, _ => new PluginStatus()); + Configuration = configuration; Scheduler = scheduler; Capabilities = capabilities; Resources = resources; Platform = platform; _log = log ?? (_ => { }); + } + + public async Task PrepareAndStartAsync(GameIdentity identity, string packagePath, string title, string controllerId, long session, CancellationToken cancellation) + { + using var gateDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation); + gateDeadline.CancelAfter(StartupTimeout); + try { await _operation.WaitAsync(gateDeadline.Token).ConfigureAwait(false); } + catch (OperationCanceledException) when (!cancellation.IsCancellationRequested) { + await Scheduler.Switch(); + foreach (var factory in _factories.Values) SetStatus(factory.Descriptor.Id, PluginState.Blocked, "Plugin host is still stopping a previous operation."); + return; + } + await Scheduler.Switch(); + try { + await StopCoreAsync(CancellationToken.None).ConfigureAwait(false); + await Scheduler.Switch(); + _gameCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellation); + _activeGame = identity; + _hostCapabilities = Capabilities.Ids.ToArray(); + var gameToken = _gameCancellation.Token; + var selected = Select(identity); + foreach (var descriptor in PluginOrderResolver.Resolve(selected, PluginPhase.Initialize, _hostCapabilities)) { + gameToken.ThrowIfCancellationRequested(); + if (MissingProvider(descriptor, selected) is string missing) { SetStatus(descriptor.Id, PluginState.Blocked, missing); continue; } + var factory = _factories[descriptor.Id]; + var application = new ResourceScope(); + try { + var settings = Configuration.Effective(descriptor, factory.Schema, identity); + var issues = factory.Schema.Validate(settings).Concat(factory.SettingsProvider?.Validate(new PluginChoiceRequest(identity, settings, Platform)) ?? Array.Empty()).ToArray(); + if (issues.Length > 0) throw new InvalidOperationException(string.Join("; ", issues.Select(issue => issue.Field + ": " + issue.Message))); + if (Configuration.Read(descriptor, factory.Schema).SchemaVersion > factory.Schema.Version || Configuration.Read(descriptor, factory.Schema, identity).SchemaVersion > factory.Schema.Version) throw new InvalidOperationException("Configuration requires a newer plugin version."); + var plugin = factory.Create(new PluginApplicationContext(descriptor, Configuration, Scheduler, Capabilities, Resources, application, message => _log(descriptor.Id + ": " + message))); + var entry = new Instance(factory, plugin, application); + _entered.Add(entry); + await InvokeAsync(entry, PluginPhase.Initialize, token => plugin.InitializeAsync(token), gameToken).ConfigureAwait(false); + await Scheduler.Switch(); + } catch (OperationCanceledException) when (gameToken.IsCancellationRequested) { throw; } + catch (Exception error) { + await Scheduler.Switch(); + if (!(error is HookDeadlineException)) Fault(descriptor.Id, PluginPhase.Initialize, error); + var failed = _entered.FirstOrDefault(entry => entry.ApplicationScope == application); + if (failed == null) SafeDispose(application); else await CleanupAsync(failed, CancellationToken.None).ConfigureAwait(false); + await Scheduler.Switch(); + } + } + foreach (var phase in new[] { PluginPhase.Prepare, PluginPhase.Start }) { + await BlockDependentsAsync().ConfigureAwait(false); await Scheduler.Switch(); + var active = _entered.Where(Available).Select(entry => entry.Factory.Descriptor).ToArray(); + foreach (var descriptor in PluginOrderResolver.Resolve(active, phase, _hostCapabilities)) { + gameToken.ThrowIfCancellationRequested(); + var entry = _entered.First(instance => instance.Factory.Descriptor.Id == descriptor.Id); + if (!Available(entry)) continue; + try { + var settings = Configuration.Effective(descriptor, entry.Factory.Schema, identity); + entry.Settings = settings; + var context = new PluginGameContext(identity, packagePath, title, controllerId, session, settings, Capabilities, Resources, entry.GameScope, gameToken); + if (phase == PluginPhase.Prepare) entry.Prepared = true; + if (phase == PluginPhase.Start) _startOrder.Add(entry); + await InvokeAsync(entry, phase, token => phase == PluginPhase.Prepare ? entry.Plugin.PrepareGameAsync(context, token) : entry.Plugin.StartGameAsync(token), gameToken).ConfigureAwait(false); + await Scheduler.Switch(); + if (phase == PluginPhase.Start) SetStatus(descriptor.Id, PluginState.Running, "Running", phase); + } catch (OperationCanceledException) when (gameToken.IsCancellationRequested) { throw; } + catch (Exception error) { + await Scheduler.Switch(); + if (!(error is HookDeadlineException)) Fault(descriptor.Id, phase, error); + await CleanupAsync(entry, CancellationToken.None).ConfigureAwait(false); + await BlockDependentsAsync().ConfigureAwait(false); await Scheduler.Switch(); + } + } + } + _running = true; + RebuildDispatch(); + } catch { + await Scheduler.Switch(); + await StopCoreAsync(CancellationToken.None).ConfigureAwait(false); + await Scheduler.Switch(); + throw; + } finally { _operation.Release(); ReplayState(); } + } + + private IReadOnlyList Select(GameIdentity game) + { + var selected = new List(); + foreach (var factory in _factories.Values) { + var descriptor = factory.Descriptor; + try { + if (_abandoned.ContainsKey(descriptor.Id)) SetStatus(descriptor.Id, PluginState.Unresponsive, "Waiting for the previous instance to finish; restart the application if it remains blocked."); + else if (!Configuration.Enabled(descriptor, game)) SetStatus(descriptor.Id, PluginState.Disabled, "Disabled for this game"); + else if (descriptor.Platforms.Count > 0 && !descriptor.Platforms.Contains(Platform)) SetStatus(descriptor.Id, PluginState.Unsupported, "Unavailable on " + Platform); + else { selected.Add(descriptor); SetStatus(descriptor.Id, PluginState.Available, "Ready"); } + } catch (Exception error) { Fault(descriptor.Id, PluginPhase.Initialize, error); } + } + bool changed; + do { + changed = false; + foreach (var descriptor in selected.ToArray()) { + string? missing = MissingProvider(descriptor, selected); + if (missing != null) { selected.Remove(descriptor); SetStatus(descriptor.Id, PluginState.Blocked, "Missing required provider: " + missing); changed = true; } + } + foreach (var phase in new[] { PluginPhase.Initialize, PluginPhase.Prepare, PluginPhase.Start, PluginPhase.Ready, PluginPhase.Pause, PluginPhase.Resume, PluginPhase.Focus, PluginPhase.Tick, PluginPhase.Render }) { + try { PluginOrderResolver.Resolve(selected, phase, _hostCapabilities); } + catch (PluginOrderException error) { + foreach (string id in error.Plugins) { + selected.RemoveAll(descriptor => descriptor.Id == id); + SetStatus(id, PluginState.Blocked, error.Message, phase); + } + changed = true; + } + } + } while (changed); + return selected; + } + private bool Available(Instance entry) => !entry.Cleaned && _statuses[entry.Factory.Descriptor.Id].State != PluginState.Faulted && _statuses[entry.Factory.Descriptor.Id].State != PluginState.Blocked && _statuses[entry.Factory.Descriptor.Id].State != PluginState.Unresponsive; + private string? MissingProvider(PluginDescriptor descriptor, IEnumerable candidates) + { + var active = candidates.Where(candidate => _statuses[candidate.Id].State != PluginState.Faulted && _statuses[candidate.Id].State != PluginState.Blocked && _statuses[candidate.Id].State != PluginState.Unresponsive).ToArray(); + string? missing = descriptor.DependsOn.FirstOrDefault(id => active.All(candidate => candidate.Id != id)); + return missing ?? descriptor.Requires.FirstOrDefault(id => !_hostCapabilities.Contains(id) && active.All(candidate => !candidate.Provides.Contains(id))); + } + private async Task BlockDependentsAsync() + { + bool changed; + do { + changed = false; + foreach (var entry in _entered.Where(Available).ToArray()) { + string? missing = MissingProvider(entry.Factory.Descriptor, _entered.Where(Available).Select(instance => instance.Factory.Descriptor)); + if (missing == null) continue; + SetStatus(entry.Factory.Descriptor.Id, PluginState.Blocked, "Missing required provider: " + missing); + await CleanupAsync(entry, CancellationToken.None).ConfigureAwait(false); await Scheduler.Switch(); changed = true; + } + } while (changed); + } + private bool IsHealthy(Instance entry) => _statuses[entry.Factory.Descriptor.Id].State == PluginState.Running; + private async Task InvokeAsync(Instance entry, PluginPhase phase, Func invoke, CancellationToken cancellation) + { + SetStatus(entry.Factory.Descriptor.Id, PluginState.Starting, phase.ToString(), phase); + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation); + deadline.CancelAfter(StartupTimeout); + try { await AwaitHookAsync(invoke(deadline.Token), deadline.Token).ConfigureAwait(false); } + catch (HookDeadlineException error) { + await Scheduler.Switch(); Abandon(entry, error.Hook); + if (cancellation.IsCancellationRequested) throw new OperationCanceledException(cancellation); + throw; + } + await Scheduler.Switch(); + } + private async Task AwaitHookAsync(Task hook, CancellationToken deadline) + { + if (await Task.WhenAny(hook, Task.Delay(Timeout.Infinite, deadline)).ConfigureAwait(false) != hook) { + _ = hook.ContinueWith(task => { var ignored = task.Exception; }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); + throw new HookDeadlineException(hook); + } + await hook.ConfigureAwait(false); + } + private void Abandon(Instance entry, Task hook) + { + string id = entry.Factory.Descriptor.Id; + SetStatus(id, PluginState.Unresponsive, "Waiting for an unresponsive plugin; its next instance is blocked."); + Emergency(entry); + entry.Cleaned = true; + SafeDispose(entry.GameScope); SafeDispose(entry.ApplicationScope); + if (_abandoned.ContainsKey(id)) return; + var quiescence = (entry.Plugin as IPluginDeferredCleanup)?.Quiescence ?? Task.CompletedTask; + var pending = Task.WhenAll(hook, quiescence); + _abandoned[id] = pending; + _ = FinishAbandonedAsync(entry, pending); + } + private async Task FinishAbandonedAsync(Instance entry, Task pending) + { + try { await pending.ConfigureAwait(false); } catch (Exception error) { _log("Abandoned hook: " + error.Message); } + await Scheduler.Switch(); + // This old instance is quarantined independently of the active game's lifecycle gate. + using var deadline = new CancellationTokenSource(CleanupTimeout); + try { await entry.Plugin.ShutdownAsync(deadline.Token).ConfigureAwait(false); } + catch (Exception error) { _log("Deferred shutdown: " + error.Message); } + await Scheduler.Switch(); + SafeDispose(entry.Plugin); + string id = entry.Factory.Descriptor.Id; + _abandoned.Remove(id); + if (_statuses[id].State == PluginState.Unresponsive) SetStatus(id, PluginState.Available, "Previous instance stopped."); + } + private void SetStatus(string id, PluginState state, string message, PluginPhase? phase = null) { var status = _statuses[id]; status.State = state; status.Message = message; status.Phase = phase; } + private void Fault(string id, PluginPhase phase, Exception error) { SetStatus(id, PluginState.Faulted, error.Message, phase); _log(id + " / " + phase + ": " + error.Message); } + private void SafeDispose(IDisposable resource) { try { resource.Dispose(); } catch (Exception error) { _log("Plugin cleanup: " + error.Message); } } + + public async Task StopGameAsync(CancellationToken cleanup = default) + { + _gameCancellation?.Cancel(); + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cleanup); + deadline.CancelAfter(CleanupTimeout); + try { await _operation.WaitAsync(deadline.Token).ConfigureAwait(false); } + catch (OperationCanceledException) { await Scheduler.Switch(); EmergencyStop(); return; } + await Scheduler.Switch(); + try { await StopCoreAsync(cleanup).ConfigureAwait(false); await Scheduler.Switch(); } + finally { _operation.Release(); ReplayState(); } + } + private async Task StopCoreAsync(CancellationToken cleanup) + { + _running = false; _dispatch.Clear(); _ready = false; _paused = null; _focused = null; + _gameCancellation?.Cancel(); + // Partially prepared instances unwind first, then started instances in their actual start order. + var reverse = _entered.Where(entry => !_startOrder.Contains(entry)).Reverse().Concat(_startOrder.AsEnumerable().Reverse()).ToArray(); + foreach (var entry in reverse) { + await CleanupAsync(entry, cleanup).ConfigureAwait(false); await Scheduler.Switch(); + } + _startOrder.Clear(); _entered.Clear(); _gameCancellation?.Dispose(); _gameCancellation = null; _activeGame = null; + } + /// Applies live values and persistence within the same lifecycle gate. Plugins must roll back failed reconfiguration. + public async Task CommitSettingsAsync(IPlayerPluginFactory factory, PluginSettingsSnapshot draft, GameIdentity? game, CancellationToken cancellation) + { + if (!_factories.TryGetValue(factory.Descriptor.Id, out var installed) || installed != factory) throw new ArgumentException("The factory is not installed in this host."); + using var operationDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation, _gameCancellation?.Token ?? CancellationToken.None); + operationDeadline.CancelAfter(StartupTimeout); + try { await _operation.WaitAsync(operationDeadline.Token).ConfigureAwait(false); } + catch (OperationCanceledException) { return new PluginActionResult(false, "The plugin host is busy or stopping. Settings were not saved."); } + await Scheduler.Switch(); + Instance? busyEntry = null; + try { + var entry = _entered.FirstOrDefault(candidate => candidate.Factory == factory && IsHealthy(candidate) && Available(candidate)); + PluginSettingsSnapshot? previous = entry?.Settings; + PluginSettingsSnapshot? live = null; + IPluginReconfiguration? receiver = null; + if (entry != null && previous != null && _activeGame.HasValue && (!game.HasValue || game.Value.Equals(_activeGame.Value))) { + var next = game.HasValue ? PluginConfigurationStore.Resolve(factory.Schema, Configuration.Read(factory.Descriptor, factory.Schema), draft) + : PluginConfigurationStore.Resolve(factory.Schema, draft, Configuration.Read(factory.Descriptor, factory.Schema, _activeGame)); + var changed = factory.Schema.Fields.Where(field => field.Change == ConfigurationChange.Live && !JToken.DeepEquals(previous.Value(field.Id), next.Value(field.Id))).ToArray(); + if (changed.Length > 0) { + busyEntry = entry; entry.Busy = true; + receiver = entry.Plugin as IPluginReconfiguration; + if (receiver == null) return new PluginActionResult(false, "This plugin declares live settings but does not implement reconfiguration."); + live = previous.Copy(); + foreach (var field in changed) live.Set(field.Id, next.Value(field.Id)); + var apply = receiver.ApplyAsync(live, operationDeadline.Token); + try { await AwaitHookAsync(apply, operationDeadline.Token).ConfigureAwait(false); } + catch (HookDeadlineException error) { await Scheduler.Switch(); Abandon(entry, error.Hook); return new PluginActionResult(false, "Plugin reconfiguration did not finish. Settings were not saved."); } + var result = await apply.ConfigureAwait(false); + await Scheduler.Switch(); + if (!result.Success) return result; + } + } + try { + cancellation.ThrowIfCancellationRequested(); + Configuration.Save(factory.Descriptor, factory.Schema, draft, game); + } catch { + if (receiver != null && previous != null) { + try { + using var rollbackDeadline = new CancellationTokenSource(StartupTimeout); + var rollback = receiver.ApplyAsync(previous, rollbackDeadline.Token); + await AwaitHookAsync(rollback, rollbackDeadline.Token).ConfigureAwait(false); + await Scheduler.Switch(); + if (!(await rollback).Success) Fault(factory.Descriptor.Id, PluginPhase.Prepare, new InvalidOperationException("Runtime settings rollback failed.")); + } + catch (HookDeadlineException error) { await Scheduler.Switch(); if (entry != null) Abandon(entry, error.Hook); } + catch (Exception error) { await Scheduler.Switch(); Fault(factory.Descriptor.Id, PluginPhase.Prepare, error); } + } + throw; + } + if (live != null && entry != null) entry.Settings = live; + return new PluginActionResult(true, "Saved. Live settings are applied; other changes take effect on the next game or application restart."); + } finally { if (busyEntry != null) busyEntry.Busy = false; _operation.Release(); ReplayState(); } + } + private async Task CleanupAsync(Instance entry, CancellationToken cleanup) + { + if (entry.Cleaned) return; + entry.Cleaned = true; + string id = entry.Factory.Descriptor.Id; + bool keepDiagnostic = _statuses[id].State == PluginState.Faulted || _statuses[id].State == PluginState.Blocked || _statuses[id].State == PluginState.Unresponsive; + if (!keepDiagnostic) SetStatus(id, PluginState.Stopping, "Stopping", PluginPhase.Stop); + if (entry.Prepared) { + using var stopDeadline = CancellationTokenSource.CreateLinkedTokenSource(cleanup); + stopDeadline.CancelAfter(CleanupTimeout); + try { await AwaitHookAsync(entry.Plugin.StopGameAsync(stopDeadline.Token), stopDeadline.Token).ConfigureAwait(false); await Scheduler.Switch(); } + catch (HookDeadlineException error) { await Scheduler.Switch(); Abandon(entry, error.Hook); return; } + catch (OperationCanceledException) when (stopDeadline.IsCancellationRequested) { + await Scheduler.Switch(); Abandon(entry, Task.CompletedTask); return; + } + catch (Exception error) { await Scheduler.Switch(); _log(id + " / Stop: " + error.Message); } + } + SafeDispose(entry.GameScope); + using var shutdownDeadline = CancellationTokenSource.CreateLinkedTokenSource(cleanup); + shutdownDeadline.CancelAfter(CleanupTimeout); + try { await AwaitHookAsync(entry.Plugin.ShutdownAsync(shutdownDeadline.Token), shutdownDeadline.Token).ConfigureAwait(false); await Scheduler.Switch(); } + catch (HookDeadlineException error) { await Scheduler.Switch(); Abandon(entry, error.Hook); return; } + catch (Exception error) { await Scheduler.Switch(); _log(id + " / Shutdown: " + error.Message); } + SafeDispose(entry.ApplicationScope); SafeDispose(entry.Plugin); + if (!keepDiagnostic) SetStatus(id, PluginState.Available, "Stopped"); + } + private void RebuildDispatch() + { + _dispatch.Clear(); + var active = _entered.Where(IsHealthy).Where(Available).ToDictionary(entry => entry.Factory.Descriptor.Id); + foreach (var phase in new[] { PluginPhase.Ready, PluginPhase.Pause, PluginPhase.Resume, PluginPhase.Focus, PluginPhase.Tick, PluginPhase.Render }) + _dispatch[phase] = PluginOrderResolver.Resolve(active.Values.Select(entry => entry.Factory.Descriptor), phase, _hostCapabilities).Select(descriptor => active[descriptor.Id]).ToArray(); + } + private void Dispatch(PluginPhase phase, double delta = 0, bool state = false) + { + Scheduler.VerifyOwner(); + bool continuous = phase == PluginPhase.Tick || phase == PluginPhase.Render; + if (!_running || !continuous && IsTransitioning || !_dispatch.TryGetValue(phase, out var entries)) return; + bool failed = false; + foreach (var entry in entries) { + if (entry.Busy) continue; + if (!Available(entry)) { failed = true; continue; } + if (failed && MissingProvider(entry.Factory.Descriptor, _entered.Where(Available).Select(candidate => candidate.Factory.Descriptor)) is string missing) { + SetStatus(entry.Factory.Descriptor.Id, PluginState.Blocked, "Missing required provider: " + missing); continue; + } + try { + switch (phase) { + case PluginPhase.Ready: entry.Plugin.GameReady(); break; + case PluginPhase.Pause: case PluginPhase.Resume: entry.Plugin.PauseChanged(state); break; + case PluginPhase.Focus: entry.Plugin.FocusChanged(state); break; + case PluginPhase.Tick: (entry.Plugin as IPluginTick)?.Tick(delta); break; + case PluginPhase.Render: (entry.Plugin as IPluginRender)?.BeforeRender(); break; + } + } catch (Exception error) { Fault(entry.Factory.Descriptor.Id, phase, error); failed = true; } + } + if (failed && _faultCleanup.IsCompleted) _faultCleanup = CleanupFaultsAsync(); + } + private async Task CleanupFaultsAsync() + { + await _operation.WaitAsync().ConfigureAwait(false); await Scheduler.Switch(); + try { + await BlockDependentsAsync().ConfigureAwait(false); await Scheduler.Switch(); + while (_entered.Any(entry => !Available(entry) && !entry.Cleaned)) + foreach (var entry in _entered.Where(entry => !Available(entry) && !entry.Cleaned).Reverse().ToArray()) { await CleanupAsync(entry, CancellationToken.None).ConfigureAwait(false); await Scheduler.Switch(); } + RebuildDispatch(); + } catch (Exception error) { _log("Plugin recovery: " + error.Message); } + finally { _operation.Release(); ReplayState(); } + } + private void ReplayState() + { + if (_ready) { _ready = false; Dispatch(PluginPhase.Ready); } + if (_paused.HasValue) { bool paused = _paused.Value; _paused = null; Dispatch(paused ? PluginPhase.Pause : PluginPhase.Resume, state: paused); } + if (_focused.HasValue) { bool focused = _focused.Value; _focused = null; Dispatch(PluginPhase.Focus, state: focused); } + } + public void GameReady() { if (!_running && !IsTransitioning) return; _ready = true; if (!IsTransitioning) ReplayState(); } + public void PauseChanged(bool paused) { if (!_running && !IsTransitioning) return; _paused = paused; if (!IsTransitioning) ReplayState(); } + public void FocusChanged(bool focused) { if (!_running && !IsTransitioning) return; _focused = focused; if (!IsTransitioning) ReplayState(); } + public void Tick(double delta) { Scheduler.Pump(); Dispatch(PluginPhase.Tick, delta); } + public void BeforeRender() => Dispatch(PluginPhase.Render); + public void EmergencyStop() + { + Scheduler.VerifyOwner(); + _running = false; _gameCancellation?.Cancel(); _dispatch.Clear(); + foreach (var entry in _entered.AsEnumerable().Reverse()) { + Emergency(entry); + if (entry.Cleaned) continue; + entry.Cleaned = true; + SafeDispose(entry.GameScope); SafeDispose(entry.ApplicationScope); SafeDispose(entry.Plugin); + } + } + private void Emergency(Instance entry) + { + try { (entry.Plugin as IPluginEmergencyStop)?.EmergencyStop(); } catch (Exception error) { _log(error.Message); } + } + } +} diff --git a/VisualPinball.Plugins/Runtime/PluginHost.cs.meta b/VisualPinball.Plugins/Runtime/PluginHost.cs.meta new file mode 100644 index 000000000..5cfe6ab33 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginHost.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cfc149ebe13e609418c14fdcc83cfa9e \ No newline at end of file diff --git a/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs new file mode 100644 index 000000000..9f818dd1c --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Linq; + +namespace VisualPinball.Plugins +{ + /// Ordered filters transform one input value; observers receive the resulting immutable event. + public sealed class PluginInputPipeline + { + private sealed class Stage + { + internal string Id = ""; + internal int Order; + internal IPluginInputFilter Filter = null!; + } + private readonly List _stages = new List(); + private Stage[] _dispatch = Array.Empty(); + public IDisposable Register(string pluginId, int order, IPluginInputFilter filter) + { + if (_stages.Any(stage => stage.Id == pluginId)) throw new InvalidOperationException("Input filter already registered: " + pluginId); + var entry = new Stage { Id = pluginId, Order = order, Filter = filter }; _stages.Add(entry); Rebuild(); + return new Subscription(() => { _stages.Remove(entry); Rebuild(); }); + } + private void Rebuild() => _dispatch = _stages.OrderBy(stage => stage.Order).ThenBy(stage => stage.Id, StringComparer.Ordinal).ToArray(); + public PluginInputEvent Apply(in PluginInputEvent input) + { + var result = input; + foreach (var stage in _dispatch) result = stage.Filter.Filter(result); + return result; + } + } +} diff --git a/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs.meta b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs.meta new file mode 100644 index 000000000..7d6fae5ca --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 73ede895eba38114994b9ffb19eff75a \ No newline at end of file diff --git a/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs new file mode 100644 index 000000000..47922b2da --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Linq; + +namespace VisualPinball.Plugins +{ + public sealed class PluginOrderException : InvalidOperationException + { + public IReadOnlyList Plugins { get; } + public PluginOrderException(string message, params string[] plugins) : base(message) { Plugins = plugins; } + } + + /// Dependencies and selected capability providers precede consumers; priority breaks ties. + public static class PluginOrderResolver + { + public static IReadOnlyList Resolve(IEnumerable plugins, PluginPhase phase, + IEnumerable? hostCapabilities = null, IReadOnlyDictionary? preferredProviders = null) + { + var nodes = plugins.ToDictionary(plugin => plugin.Id, StringComparer.Ordinal); + var host = new HashSet(hostCapabilities ?? Array.Empty(), StringComparer.Ordinal); + var incoming = nodes.Keys.ToDictionary(id => id, _ => new HashSet(StringComparer.Ordinal)); + foreach (var plugin in nodes.Values) { + foreach (string dependency in plugin.DependsOn) { + if (!nodes.ContainsKey(dependency)) throw new PluginOrderException(plugin.Id + " requires disabled or absent plugin " + dependency + ".", plugin.Id); + incoming[plugin.Id].Add(dependency); + } + foreach (string capability in plugin.Requires) { + if (host.Contains(capability) || plugin.Provides.Contains(capability)) continue; + var candidates = nodes.Values.Where(provider => provider.Id != plugin.Id && provider.Provides.Contains(capability)).ToList(); + if (preferredProviders != null && preferredProviders.TryGetValue(capability, out var selected)) + candidates.RemoveAll(candidate => candidate.Id != selected); + if (candidates.Count != 1) throw new PluginOrderException(plugin.Id + ": " + capability + (candidates.Count == 0 ? " is unavailable." : " has multiple providers; select one."), plugin.Id); + incoming[plugin.Id].Add(candidates[0].Id); + } + plugin.PhaseOrder.TryGetValue(phase, out var order); + foreach (string before in plugin.Before.Concat(order?.Before ?? Array.Empty())) + if (nodes.ContainsKey(before)) incoming[before].Add(plugin.Id); + foreach (string after in plugin.After.Concat(order?.After ?? Array.Empty())) + if (nodes.ContainsKey(after)) incoming[plugin.Id].Add(after); + } + var resolved = new List(nodes.Count); + while (nodes.Count > 0) { + var next = nodes.Values.Where(plugin => incoming[plugin.Id].Count == 0) + .OrderBy(plugin => plugin.PhaseOrder.TryGetValue(phase, out var order) ? order.Priority : plugin.Order) + .ThenBy(plugin => plugin.Id, StringComparer.Ordinal).FirstOrDefault(); + if (next == null) { + var cycle = Cycle(incoming, nodes.Keys.First()); + throw new PluginOrderException("Plugin ordering cycle: " + string.Join(" -> ", cycle.Concat(new[] { cycle[0] })) + ".", cycle); + } + resolved.Add(next); + nodes.Remove(next.Id); + foreach (var dependencies in incoming.Values) dependencies.Remove(next.Id); + } + return resolved; + } + + private static string[] Cycle(Dictionary> incoming, string first) + { + var path = new List(); + string current = first; + while (!path.Contains(current)) { path.Add(current); current = incoming[current].First(); } + return path.Skip(path.IndexOf(current)).ToArray(); + } + } +} diff --git a/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs.meta b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs.meta new file mode 100644 index 000000000..029bc459b --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2fbf1c5d3c2f88a4190c146701374266 \ No newline at end of file diff --git a/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs new file mode 100644 index 000000000..f26df4943 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; + +namespace VisualPinball.Plugins +{ + /// Exclusive destination ownership prevents two plugins from opening the same physical device. + public sealed class PluginOutputLeases + { + private readonly Dictionary _owners = new Dictionary(StringComparer.Ordinal); + public IDisposable Acquire(string destination, string pluginId) + { + if (string.IsNullOrWhiteSpace(destination)) throw new ArgumentException("A destination needs a stable ID.", nameof(destination)); + lock (_owners) { + if (_owners.TryGetValue(destination, out var owner)) throw new InvalidOperationException(destination + " is already owned by " + owner + "."); + _owners.Add(destination, pluginId); + } + return new Subscription(() => { lock (_owners) _owners.Remove(destination); }); + } + public string? Owner(string destination) { lock (_owners) return _owners.TryGetValue(destination, out var owner) ? owner : null; } + } +} diff --git a/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs.meta b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs.meta new file mode 100644 index 000000000..ace3636ab --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9e15f9a2067979a4fb8d4f1660492433 \ No newline at end of file diff --git a/VisualPinball.Plugins/Runtime/ResourceScope.cs b/VisualPinball.Plugins/Runtime/ResourceScope.cs new file mode 100644 index 000000000..086d57de7 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/ResourceScope.cs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; + +namespace VisualPinball.Plugins +{ + /// Releases every owned resource in reverse order, even if one cleanup fails. + public sealed class ResourceScope : IDisposable + { + private readonly List _resources = new List(); + private bool _disposed; + public T Own(T resource) where T : IDisposable + { + if (_disposed) { resource.Dispose(); throw new ObjectDisposedException(nameof(ResourceScope)); } + _resources.Add(resource); + return resource; + } + public void Dispose() + { + if (_disposed) return; + _disposed = true; + List? errors = null; + for (int i = _resources.Count - 1; i >= 0; i--) + try { _resources[i].Dispose(); } catch (Exception error) { (errors ??= new List()).Add(error); } + _resources.Clear(); + if (errors != null) throw new AggregateException("Some plugin resources could not be released.", errors); + } + } + + public sealed class Subscription : IDisposable + { + private Action? _release; + public Subscription(Action release) { _release = release; } + public void Dispose() => System.Threading.Interlocked.Exchange(ref _release, null)?.Invoke(); + } +} diff --git a/VisualPinball.Plugins/Runtime/ResourceScope.cs.meta b/VisualPinball.Plugins/Runtime/ResourceScope.cs.meta new file mode 100644 index 000000000..596564349 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/ResourceScope.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 34133e0d2f52bc74a869e80af211e56a \ No newline at end of file diff --git a/VisualPinball.Plugins/VisualPinball.Plugins.asmdef b/VisualPinball.Plugins/VisualPinball.Plugins.asmdef new file mode 100644 index 000000000..3400ccefc --- /dev/null +++ b/VisualPinball.Plugins/VisualPinball.Plugins.asmdef @@ -0,0 +1,8 @@ +{ + "name": "VisualPinball.Plugins", + "rootNamespace": "VisualPinball.Plugins", + "references": ["Newtonsoft.Json"], + "overrideReferences": false, + "precompiledReferences": [], + "noEngineReferences": true +} diff --git a/VisualPinball.Plugins/VisualPinball.Plugins.asmdef.meta b/VisualPinball.Plugins/VisualPinball.Plugins.asmdef.meta new file mode 100644 index 000000000..baba096aa --- /dev/null +++ b/VisualPinball.Plugins/VisualPinball.Plugins.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 87ac0b1c56dbfee48ab1850ba3068e01 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Plugins/VisualPinball.Plugins.csproj b/VisualPinball.Plugins/VisualPinball.Plugins.csproj new file mode 100644 index 000000000..59299b324 --- /dev/null +++ b/VisualPinball.Plugins/VisualPinball.Plugins.csproj @@ -0,0 +1,13 @@ + + + netstandard2.1 + 9.0 + enable + MIT + true + + + + + + diff --git a/VisualPinball.Plugins/VisualPinball.Plugins.csproj.meta b/VisualPinball.Plugins/VisualPinball.Plugins.csproj.meta new file mode 100644 index 000000000..2f67ab4c8 --- /dev/null +++ b/VisualPinball.Plugins/VisualPinball.Plugins.csproj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 763792b723e118c4ba2b83646d035dc8 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Packaging/RuntimePackageReader.cs b/VisualPinball.Unity/VisualPinball.Unity/Packaging/RuntimePackageReader.cs index 98b9eee4f..62b8f9168 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Packaging/RuntimePackageReader.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Packaging/RuntimePackageReader.cs @@ -72,6 +72,16 @@ public async Task ImportIntoScene( Transform parent, IProgress progress, CancellationToken cancellationToken = default) + { + return await ImportIntoScene(parent, progress, null, cancellationToken); + } + + /// The callback sees a fully restored inactive table and completes before game logic activation. + public async Task ImportIntoScene( + Transform parent, + IProgress progress, + Func beforeActivation, + CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(_vpePath)) { throw new ArgumentException("No .vpe path was provided."); @@ -270,6 +280,8 @@ await ReadPackablesAsync( foreach (var consumer in _table.GetComponentsInChildren(true).OfType()) { consumer.SetPackagedContentResolver(ContentResolver); } + if (beforeActivation != null) await beforeActivation(_table, cancellationToken); + cancellationToken.ThrowIfCancellationRequested(); loadSucceeded = true; } finally { diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins.meta new file mode 100644 index 000000000..2150e2655 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e3c777d0ece34e04397877b907924d04 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs new file mode 100644 index 000000000..bc24df3f2 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using VisualPinball.Plugins; +using VisualPinball.Engine.Game.Engines; +using UnityEngine; + +namespace VisualPinball.Unity +{ + /// Adapts engine output to VPE-owned resources before the inactive table starts. + public sealed class GamelogicPluginResources : IDisposable + { + private sealed class Display : IDisposable + { + internal readonly DisplayResourceSource Source; + internal readonly IDisposable Publication; + internal readonly int Width; + internal readonly int Height; + internal readonly long Instance; + private readonly IDisposable _format; + internal Display(DisplayResourceSource source, IDisposable publication, int width, int height, long instance, IDisposable format) + { Source = source; Publication = publication; Width = width; Height = height; Instance = instance; _format = format; } + public void Dispose() { Publication.Dispose(); Source.Dispose(); _format?.Dispose(); } + } + private static long _instance; + private readonly IGamelogicEngine _engine; + private readonly OwnerScheduler _scheduler; + private readonly PluginResourceRegistry _registry; + private readonly Dictionary _displays = new Dictionary(StringComparer.Ordinal); + private readonly ResourceScope _scope = new ResourceScope(); + private readonly ControllerResourceSource _controller; + private readonly DisplayFormatNegotiator _group; + private readonly string _controllerId; + private readonly string _provider; + private readonly long _session; + private readonly Action _fault; + private readonly DisplayEncoding _formats; + private readonly DisplayEncoding _defaultFormat; + private long _sequence; + private bool _disposed; + public GamelogicPluginResources(GameObject table, IGamelogicEngine engine, GameIdentity identity, long session, + OwnerScheduler scheduler, PluginResourceRegistry registry, Action fault) + { + _engine = engine; _scheduler = scheduler; _registry = registry; _provider = identity.Provider; _controllerId = identity.Id + "/controller/0"; _session = session; _fault = fault; + _controller = new ControllerResourceSource(fault); + var metadata = engine as IDisplayResourceMetadata; + _formats = metadata?.DisplayFormats ?? (engine is IDisplayFrameFormatPreference + ? DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8 | DisplayEncoding.Rgb24 + : DisplayEncoding.Luminance8 | DisplayEncoding.Rgb24); + _defaultFormat = metadata?.DefaultDisplayFormat ?? DisplayEncoding.Luminance8; + if (metadata?.FormatAppliesToAllDisplays == true) _group = _scope.Own(new DisplayFormatNegotiator(_formats, _defaultFormat, + format => (engine as IDisplayFrameFormatPreference)?.RequestDisplayFrameFormat("", ToLegacy(format)))); + _scope.Own(_controller); + _scope.Own(registry.Publish(new PluginResourceDescriptor(_controllerId, _provider, _controllerId, engine.Name, "controller", ResourceKind.Controller, session, Interlocked.Increment(ref _instance)), _controller)); + if (engine is IGamelogicAudioResourceProvider audio) _scope.Own(registry.Publish(new PluginResourceDescriptor(_controllerId + "/audio/rom", _provider, _controllerId, engine.Name + " audio", "rom-mix", ResourceKind.Audio, session, Interlocked.Increment(ref _instance)), audio.AudioSource)); + engine.OnDisplaysRequested += DisplaysRequested; + engine.OnDisplayUpdateFrame += DisplayFrame; + engine.OnCoilChanged += Coil; + engine.OnLampChanged += Lamp; + engine.OnLampsChanged += Lamps; + engine.OnSwitchChanged += Switch; + foreach (var display in table.GetComponentsInChildren(true)) AddDisplay(display.Id, display.Width, display.Height); + } + public string ControllerId => _controllerId; + private void DisplaysRequested(object sender, RequestedDisplays args) + { + _scheduler.VerifyOwner(); + foreach (var display in args.Displays) if (display.Height > 1) AddDisplay(display.Id, display.Width, display.Height); + } + private void AddDisplay(string id, int width, int height) + { + if (_disposed || width <= 0 || height <= 0) return; + if (_displays.TryGetValue(id, out var existing)) { + if (existing.Width == width && existing.Height == height) return; + existing.Dispose(); _displays.Remove(id); + } + var format = _group ?? new DisplayFormatNegotiator(_formats, _defaultFormat, + encoding => (_engine as IDisplayFrameFormatPreference)?.RequestDisplayFrameFormat(id, ToLegacy(encoding))); + var source = new DisplayResourceSource(format.Acquire, _fault); + long instance = Interlocked.Increment(ref _instance); + var descriptor = new PluginResourceDescriptor(_controllerId + "/display/" + id, _provider, _controllerId, id, "dmd", ResourceKind.Display, _session, instance, ResourceView.Original, _formats); + _displays.Add(id, new Display(source, _registry.Publish(descriptor, source), width, height, instance, _group == null ? format : null)); + } + private void DisplayFrame(object sender, DisplayFrameData args) + { + _scheduler.VerifyOwner(); + if (_disposed || !_displays.TryGetValue(args.Id, out var display)) return; + var format = FromLegacy(args.Format); + if (format == DisplayEncoding.None) return; + int stride = display.Width * (format == DisplayEncoding.Rgb24 ? 3 : 1); + if (args.Data == null || args.Data.Length < stride * display.Height) { _fault(new ArgumentException("Invalid frame buffer for " + args.Id)); return; } + display.Source.Publish(new PluginDisplayFrame(args.Data, display.Width, display.Height, stride, format, ++_sequence, Timestamp(), display.Instance)); + } + private void Emit(ControllerEventKind kind, string id, double value) + { + if (_disposed) return; + if (!_scheduler.IsOwner) { _scheduler.Post(() => Emit(kind, id, value)); return; } + _controller.Publish(new PluginControllerEvent(kind, id, value, ++_sequence, Timestamp())); + } + private void Coil(object sender, CoilEventArgs args) => Emit(ControllerEventKind.Coil, args.Id, args.Value); + private void Lamp(object sender, LampEventArgs args) => Emit(args.Source == LampSource.GI ? ControllerEventKind.GeneralIllumination : ControllerEventKind.Lamp, args.Id, args.Value); + private void Lamps(object sender, LampsEventArgs args) { foreach (var lamp in args.LampsChanged) Lamp(sender, lamp); } + private void Switch(object sender, SwitchEventArgs2 args) => Emit(ControllerEventKind.Switch, args.Id, args.IsEnabled ? 1 : 0); + private static long Timestamp() => (long)(Stopwatch.GetTimestamp() * (1000000d / Stopwatch.Frequency)); + public static DisplayEncoding FromLegacy(DisplayFrameFormat format) => format switch { + DisplayFrameFormat.Dmd2 => DisplayEncoding.Gray2, DisplayFrameFormat.Dmd4 => DisplayEncoding.Gray4, + DisplayFrameFormat.Dmd8 => DisplayEncoding.Luminance8, DisplayFrameFormat.Dmd24 => DisplayEncoding.Rgb24, + DisplayFrameFormat.Video24 => DisplayEncoding.Rgb24, _ => DisplayEncoding.None + }; + public static DisplayFrameFormat ToLegacy(DisplayEncoding format) => format switch { + DisplayEncoding.Gray2 => DisplayFrameFormat.Dmd2, DisplayEncoding.Gray4 => DisplayFrameFormat.Dmd4, + DisplayEncoding.Rgb24 => DisplayFrameFormat.Dmd24, _ => DisplayFrameFormat.Dmd8 + }; + public void Dispose() + { + if (_disposed) return; _disposed = true; + _engine.OnDisplaysRequested -= DisplaysRequested; _engine.OnDisplayUpdateFrame -= DisplayFrame; + _engine.OnCoilChanged -= Coil; _engine.OnLampChanged -= Lamp; _engine.OnLampsChanged -= Lamps; _engine.OnSwitchChanged -= Switch; + foreach (var display in _displays.Values) display.Dispose(); _displays.Clear(); _scope.Dispose(); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs.meta new file mode 100644 index 000000000..18d593878 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2a5a7b8cf4efce94f8a0e50242200ba9 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs new file mode 100644 index 000000000..ca80ef17d --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using VisualPinball.Plugins; + +namespace VisualPinball.Unity +{ + /// Reads provider identities without restoring a scene; content hashes keep fallback identities stable across moves. + public static class PackagedGameInformation + { + private sealed class Cached + { + internal long Size; + internal DateTime Modified; + internal GameIdentity Identity; + } + private static readonly Dictionary Cache = new Dictionary(StringComparer.OrdinalIgnoreCase); + public static Task ReadAsync(string path, CancellationToken cancellation = default) => Task.Run(() => Read(path, cancellation), cancellation); + private static GameIdentity Read(string path, CancellationToken cancellation) + { + path = Path.GetFullPath(path); + var file = new FileInfo(path); + lock (Cache) if (Cache.TryGetValue(path, out var cached) && cached.Size == file.Length && cached.Modified == file.LastWriteTimeUtc) return cached.Identity; + var found = new HashSet(); + using (var storage = PackageApi.StorageManager.OpenStorage(path)) { + var table = storage.GetFolder(PackageApi.TableFolder); + if (table.TryGetFolder("items", out var items)) items.VisitFolders(node => node.VisitFolders(component => { + cancellation.ThrowIfCancellationRequested(); + var reader = PackagedGameIdentityReaders.Find(component.Name); + if (reader == null) return; + component.VisitFiles(payload => { var identity = reader.Read(payload.GetData()); if (identity.HasValue) found.Add(identity.Value); }); + })); + } + if (found.Count > 1) throw new InvalidDataException("The table has multiple controller identities. Select a controller profile explicitly."); + GameIdentity result; + if (found.Count == 1) { using var iterator = found.GetEnumerator(); iterator.MoveNext(); result = iterator.Current; } + else { + using var stream = File.OpenRead(path); using var sha = SHA256.Create(); + var buffer = new byte[64 * 1024]; int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) { cancellation.ThrowIfCancellationRequested(); sha.TransformBlock(buffer, 0, read, buffer, 0); } + sha.TransformFinalBlock(Array.Empty(), 0, 0); + result = new GameIdentity("package", BitConverter.ToString(sha.Hash).Replace("-", "").ToLowerInvariant()); + } + lock (Cache) Cache[path] = new Cached { Size = file.Length, Modified = file.LastWriteTimeUtc, Identity = result }; + return result; + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs.meta new file mode 100644 index 000000000..042620e18 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e12aba9f38f9db94b96fa1b09cd49d45 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs new file mode 100644 index 000000000..30ee52aea --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +using UnityEngine; +using VisualPinball.Plugins; + +namespace VisualPinball.Unity +{ + /// Player applications claim integration ownership before automatic native bridges can start. + public static class PlayerIntegrationMode + { + public static bool HostManaged { get; private set; } + public static void Claim() => HostManaged = true; + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void Reset() + { + HostManaged = false; + PlayerPluginRegistry.Reset(); + PackagedGameIdentityReaders.Reset(); + PluginSettingsViews.Reset(); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs.meta new file mode 100644 index 000000000..98b682da5 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6139831b064c6194b8eeb1de5e6044c0 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs new file mode 100644 index 000000000..edf8e337c --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +using System; +using System.Collections.Generic; +using UnityEngine.UIElements; +using VisualPinball.Plugins; + +namespace VisualPinball.Unity +{ + public sealed class PluginSettingsViewContext + { + public PluginSettingsSession Session { get; } + public PluginField Field { get; } + public PluginHost Host { get; } + public Action Changed { get; } + public PluginSettingsViewContext(PluginSettingsSession session, PluginField field, PluginHost host, Action changed) + { Session = session; Field = field; Host = host; Changed = changed; } + } + public interface IPluginSettingsViewFactory + { + string PluginId { get; } + string WidgetId { get; } + IDisposable Build(VisualElement parent, PluginSettingsViewContext context); + } + public readonly struct PluginSettingsNavigationItem + { + public VisualElement Element { get; } + public Action Activate { get; } + public PluginSettingsNavigationItem(VisualElement element, Action activate) { Element = element; Activate = activate; } + } + public interface IPluginSettingsNavigation + { + IReadOnlyList Navigation { get; } + } + public static class PluginSettingsViews + { + private static readonly Dictionary Factories = new Dictionary(StringComparer.Ordinal); + public static void Reset() => Factories.Clear(); + public static void Register(IPluginSettingsViewFactory factory) => Factories.Add(factory.PluginId + "/" + factory.WidgetId, factory); + public static IPluginSettingsViewFactory Find(string pluginId, string widgetId) => Factories.TryGetValue(pluginId + "/" + widgetId, out var factory) ? factory : null; + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs.meta new file mode 100644 index 000000000..1c6e6649e --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c03850435cdac1044b09acea2b4434eb \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs new file mode 100644 index 000000000..adcab61fc --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +using UnityEngine; +using VisualPinball.Plugins; + +namespace VisualPinball.Unity +{ + public interface IUnityGameScene + { + GameObject Table { get; } + IGamelogicEngine Gamelogic { get; } + } + public sealed class UnityGameScene : IUnityGameScene + { + public GameObject Table { get; } + public IGamelogicEngine Gamelogic { get; } + public UnityGameScene(GameObject table, IGamelogicEngine gamelogic) { Table = table; Gamelogic = gamelogic; } + } + /// Native display negotiation may apply to the entire provider, even when the API accepts a display ID. + public interface IDisplayResourceMetadata + { + DisplayEncoding DisplayFormats { get; } + DisplayEncoding DefaultDisplayFormat { get; } + bool FormatAppliesToAllDisplays { get; } + } + public interface IGamelogicAudioResourceProvider { IPluginAudioSource AudioSource { get; } } + public static class UnityPluginCapability + { + public const string GameScene = "vpe.unity.game-scene.v1"; + public const string Events = "vpe.events.v1"; + public const string Outputs = "vpe.outputs.v1"; + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs.meta new file mode 100644 index 000000000..dc63c748d --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 59959f06114aeb7469564553b173ee22 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs new file mode 100644 index 000000000..6d0d49d41 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +using System; +using UnityEngine; +using VisualPinball.Plugins; + +namespace VisualPinball.Unity +{ + /// A borrowed Unity texture. Its producer owns it; consumers may sample it until Changed or resource removal. + public interface IUnityTextureResource : IPluginSurface + { + Texture Texture { get; } + event Action Changed; + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs.meta new file mode 100644 index 000000000..afdeb9f1f --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9715411effe95c44ea6a0b2851a7ab17 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/VisualPinball.Unity.asmdef b/VisualPinball.Unity/VisualPinball.Unity/VisualPinball.Unity.asmdef index 768b51a73..8363e51d6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/VisualPinball.Unity.asmdef +++ b/VisualPinball.Unity/VisualPinball.Unity/VisualPinball.Unity.asmdef @@ -10,6 +10,7 @@ "Unity.Splines", "Unity.InputSystem", "VisualPinball.Engine", + "VisualPinball.Plugins", "com.bartofzo.nativetrees", "MemoryPack.Unity", "Newtonsoft.Json" diff --git a/package.json b/package.json index 2cdbd8347..52a29d107 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "org.visualpinball.engine.unity", - "version": "0.0.1-preview.211", + "version": "0.0.1-preview.212", "displayName": "Visual Pinball Engine", "description": "Main project of the Visual Pinball Engine.", "unity": "6000.5", From 6dfc66497fc21124647d0eabff03c5f1c80e0f06 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 13 Oct 2026 21:58:25 +0200 Subject: [PATCH 02/26] unity: include insert shaders required by standalone player --- .../Assets/Shaders/Srp/Insert.meta | 8 + .../Srp/Insert/Insert Cover.shadergraph | 4613 +++++++++++ .../Srp/Insert/Insert Cover.shadergraph.meta | 20 + .../Srp/Insert/Insert Hybrid.shadergraph | 7354 +++++++++++++++++ .../Srp/Insert/Insert Hybrid.shadergraph.meta | 20 + .../Shaders/Srp/Insert/InsertCover.hlsl | 195 + .../Shaders/Srp/Insert/InsertCover.hlsl.meta | 7 + .../Shaders/Srp/Insert/InsertOptics.hlsl | 701 ++ .../Shaders/Srp/Insert/InsertOptics.hlsl.meta | 7 + 9 files changed, 12925 insertions(+) create mode 100644 VisualPinball.Unity/Assets/Shaders/Srp/Insert.meta create mode 100644 VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Cover.shadergraph create mode 100644 VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Cover.shadergraph.meta create mode 100644 VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Hybrid.shadergraph create mode 100644 VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Hybrid.shadergraph.meta create mode 100644 VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertCover.hlsl create mode 100644 VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertCover.hlsl.meta create mode 100644 VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertOptics.hlsl create mode 100644 VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertOptics.hlsl.meta diff --git a/VisualPinball.Unity/Assets/Shaders/Srp/Insert.meta b/VisualPinball.Unity/Assets/Shaders/Srp/Insert.meta new file mode 100644 index 000000000..b4e0c4305 --- /dev/null +++ b/VisualPinball.Unity/Assets/Shaders/Srp/Insert.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8e9836e19c38a8a439b5155365ab3f99 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Cover.shadergraph b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Cover.shadergraph new file mode 100644 index 000000000..ddf45cbd1 --- /dev/null +++ b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Cover.shadergraph @@ -0,0 +1,4613 @@ +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.GraphData", + "m_ObjectId": "7398bbf56a1159b68fe38f2d28dc845f", + "m_Properties": [ + { + "m_Id": "ad06f3fb3b0c594a95419141a6275219" + }, + { + "m_Id": "fae01e6a006851718b1e68199e602c03" + }, + { + "m_Id": "2097df164c9e5fb1947791cdfa588dd1" + }, + { + "m_Id": "f611e7ae978c57538ba217fb9b8c077a" + }, + { + "m_Id": "5b38cbf426f45f719881c428caf2ff6f" + }, + { + "m_Id": "ca43925be4165b178d1f5bc49e3651ca" + }, + { + "m_Id": "6afc15978a595c2d8813b1230b602de2" + }, + { + "m_Id": "5ee46cc3aef851a28108df86cdbda282" + }, + { + "m_Id": "4e2ee789d2ea5b25b2c2a9618c1b8751" + }, + { + "m_Id": "41b0c9f64d6e57c8bef5ef55302a84e7" + }, + { + "m_Id": "e48982adfc4a5689b1b479e31ba05ead" + }, + { + "m_Id": "beaf92fa297c5f0f8cfffa63a937cc10" + }, + { + "m_Id": "e369cdcd58465c1ea4f76f73dfc2dc40" + }, + { + "m_Id": "73ee4f83fb3f55998eaa8615d946fd00" + }, + { + "m_Id": "a40e81823ec95013b379358b4dc225df" + }, + { + "m_Id": "8a7f5ba32f7354ccae9f44d72abefd82" + }, + { + "m_Id": "bc2becce24e8513a8434c08c2fbf42b9" + }, + { + "m_Id": "597db27618c157c99116431fe62014ac" + }, + { + "m_Id": "e34e3001a11f5de799da15122a2efb3d" + }, + { + "m_Id": "ea281cf402cf56caa413b6c3334f283e" + }, + { + "m_Id": "d3e87f63665b5fcba90acf09dc7ca55b" + } + ], + "m_Keywords": [], + "m_Dropdowns": [], + "m_CategoryData": [ + { + "m_Id": "7b398cb0bb835563b2574124a2b247d5" + } + ], + "m_Nodes": [ + { + "m_Id": "6c3f79c836b45d7ca783db0b58c8a0a2" + }, + { + "m_Id": "1e4ff86c89cf52158fa52f50c68b7622" + }, + { + "m_Id": "293d868c32ac5454bc5baa18d95958d0" + }, + { + "m_Id": "bb541da4cd8f5bce9d7fb6758b047c46" + }, + { + "m_Id": "5cab6e924ebf5fbd98435c85181187d6" + }, + { + "m_Id": "f9847ebe2d1c5ab4a9726ad4fc67938d" + }, + { + "m_Id": "46b690f690045f93a980c40192b8334a" + }, + { + "m_Id": "f3861acedd695d2baffd0f91a051783d" + }, + { + "m_Id": "ac0e9b9ab4275ebf8452ae7fffface7f" + }, + { + "m_Id": "d7e5b5935814588d9e58aa447ec825af" + }, + { + "m_Id": "6eedf44ab7a85b48934617d05064c2c5" + }, + { + "m_Id": "ca03b11231c25e069a7ac8ce3ffec4ad" + }, + { + "m_Id": "203e1b17561857a5884e227dd94e2a70" + }, + { + "m_Id": "34e79bb6392457019e43f53374ac48f9" + }, + { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + { + "m_Id": "c2ba7fad87fc5a7cbddeda15d70be245" + }, + { + "m_Id": "258469db3724556da3ce5aa5103917ab" + }, + { + "m_Id": "93eb8464ca0b54ddb97499e11a6c674f" + }, + { + "m_Id": "2cb25b426b3e5c90836f456a1beda794" + }, + { + "m_Id": "55bdf08e49c45b8eb889aa7ac58378cb" + }, + { + "m_Id": "86439196d3665c5c939ed992d61d38db" + }, + { + "m_Id": "84d0067015ba51dd9ecbac43815457ee" + }, + { + "m_Id": "957e9ed0c7c75a15b6f95d921b90f7c5" + }, + { + "m_Id": "3a23644eca7454818fdf008ee8debf7a" + }, + { + "m_Id": "fc79f245f21e57e691f820bd183097dc" + }, + { + "m_Id": "e39b393c054c5627b0a7dc680763496e" + }, + { + "m_Id": "ae6fbc0cd86a5367a59330cce4ee6483" + }, + { + "m_Id": "a9efd782c94655aeaf5d15772153e39b" + }, + { + "m_Id": "d5cb3ef25c845329974239d959f0af4f" + }, + { + "m_Id": "9eaf027f14a05caaa4525c402bf52b36" + }, + { + "m_Id": "4a4c94b2e0b75a19ab193ec933ea1466" + }, + { + "m_Id": "53fade0153a85229ba074a003b4e5d88" + }, + { + "m_Id": "5b1791466d87517792a0ff4cac505750" + }, + { + "m_Id": "8c8812e0868258fda47f84f843fb6f89" + }, + { + "m_Id": "be026746f05e5466ba9548f5e3cc5d38" + }, + { + "m_Id": "d4705b78c4255faabcebf85cadf8ab7f" + }, + { + "m_Id": "3cb544b2383a52a08d6dfa42b0182fc5" + }, + { + "m_Id": "970b3b62e1625d8784503fc213668716" + }, + { + "m_Id": "c97ff5e9b87140c691321a3e8fc99e68" + }, + { + "m_Id": "b99248f4c5a34783878a33a7e2bc8fb9" + } + ], + "m_GroupDatas": [], + "m_StickyNoteDatas": [], + "m_Edges": [ + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 6 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "5cab6e924ebf5fbd98435c85181187d6" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 7 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "46b690f690045f93a980c40192b8334a" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 8 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "d7e5b5935814588d9e58aa447ec825af" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 10 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "bb541da4cd8f5bce9d7fb6758b047c46" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 27 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "6eedf44ab7a85b48934617d05064c2c5" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 28 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "ca03b11231c25e069a7ac8ce3ffec4ad" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 29 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "203e1b17561857a5884e227dd94e2a70" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 30 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "34e79bb6392457019e43f53374ac48f9" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "c2ba7fad87fc5a7cbddeda15d70be245" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "258469db3724556da3ce5aa5103917ab" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 1 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "93eb8464ca0b54ddb97499e11a6c674f" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 2 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "2cb25b426b3e5c90836f456a1beda794" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 3 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "55bdf08e49c45b8eb889aa7ac58378cb" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 4 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "86439196d3665c5c939ed992d61d38db" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 5 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "84d0067015ba51dd9ecbac43815457ee" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 9 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "957e9ed0c7c75a15b6f95d921b90f7c5" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 23 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "3a23644eca7454818fdf008ee8debf7a" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 24 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "fc79f245f21e57e691f820bd183097dc" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 25 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "e39b393c054c5627b0a7dc680763496e" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 26 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "ae6fbc0cd86a5367a59330cce4ee6483" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 11 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a9efd782c94655aeaf5d15772153e39b" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 14 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "d5cb3ef25c845329974239d959f0af4f" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 15 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "9eaf027f14a05caaa4525c402bf52b36" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 16 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "4a4c94b2e0b75a19ab193ec933ea1466" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 19 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "53fade0153a85229ba074a003b4e5d88" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 17 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "5b1791466d87517792a0ff4cac505750" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 20 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "8c8812e0868258fda47f84f843fb6f89" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 18 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "be026746f05e5466ba9548f5e3cc5d38" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 21 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "d4705b78c4255faabcebf85cadf8ab7f" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 22 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "3cb544b2383a52a08d6dfa42b0182fc5" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 12 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "970b3b62e1625d8784503fc213668716" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 13 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 31 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "ac0e9b9ab4275ebf8452ae7fffface7f" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "c97ff5e9b87140c691321a3e8fc99e68" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 32 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "b99248f4c5a34783878a33a7e2bc8fb9" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "a93f8b0ecc385c40b55e136c1768652f" + }, + "m_SlotId": 33 + } + } + ], + "m_VertexContext": { + "m_Position": { + "x": 800, + "y": 0 + }, + "m_Blocks": [ + { + "m_Id": "6c3f79c836b45d7ca783db0b58c8a0a2" + }, + { + "m_Id": "1e4ff86c89cf52158fa52f50c68b7622" + }, + { + "m_Id": "293d868c32ac5454bc5baa18d95958d0" + } + ] + }, + "m_FragmentContext": { + "m_Position": { + "x": 800, + "y": 220 + }, + "m_Blocks": [ + { + "m_Id": "bb541da4cd8f5bce9d7fb6758b047c46" + }, + { + "m_Id": "5cab6e924ebf5fbd98435c85181187d6" + }, + { + "m_Id": "f9847ebe2d1c5ab4a9726ad4fc67938d" + }, + { + "m_Id": "46b690f690045f93a980c40192b8334a" + }, + { + "m_Id": "f3861acedd695d2baffd0f91a051783d" + }, + { + "m_Id": "ac0e9b9ab4275ebf8452ae7fffface7f" + }, + { + "m_Id": "d7e5b5935814588d9e58aa447ec825af" + }, + { + "m_Id": "6eedf44ab7a85b48934617d05064c2c5" + }, + { + "m_Id": "ca03b11231c25e069a7ac8ce3ffec4ad" + }, + { + "m_Id": "203e1b17561857a5884e227dd94e2a70" + }, + { + "m_Id": "34e79bb6392457019e43f53374ac48f9" + } + ] + }, + "m_PreviewData": { + "serializedMesh": { + "m_SerializedMesh": "{\"mesh\":{\"instanceID\":0}}", + "m_Guid": "" + }, + "preventRotation": false + }, + "m_Path": "Pinball", + "m_GraphPrecision": 1, + "m_PreviewMode": 2, + "m_OutputNode": { + "m_Id": "" + }, + "m_SubDatas": [], + "m_ActiveTargets": [ + { + "m_Id": "bc89bc9885ee5168bdca3b4d6021e8ed" + }, + { + "m_Id": "3c06f88b3ab44722a6d9d8e8794df1db" + } + ] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.HDTarget", + "m_ObjectId": "bc89bc9885ee5168bdca3b4d6021e8ed", + "m_ActiveSubTarget": { + "m_Id": "1634ba568d095374a4ac279291a809e1" + }, + "m_Datas": [ + { + "m_Id": "ba7d7e011193580a94c2da240c2e42f6" + }, + { + "m_Id": "dbe2ed00baa650899b438cb84a5ee443" + }, + { + "m_Id": "78608e610afe598d9c19ac2fce925736" + }, + { + "m_Id": "5efee24d3d145df7b6a2de4cf8200ad0" + } + ], + "m_CustomEditorGUI": "VisualPinball.Engine.Unity.Hdrp.Editor.InsertCoverGUI", + "m_SupportVFX": false, + "m_SupportLineRendering": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.HDLitSubTarget", + "m_ObjectId": "1634ba568d095374a4ac279291a809e1" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.SystemData", + "m_ObjectId": "ba7d7e011193580a94c2da240c2e42f6", + "m_MaterialNeedsUpdateHash": 0, + "m_SurfaceType": 1, + "m_RenderingPass": 4, + "m_BlendMode": 0, + "m_ZTest": 4, + "m_ZWrite": false, + "m_TransparentCullMode": 2, + "m_OpaqueCullMode": 2, + "m_SortPriority": 0, + "m_AlphaTest": false, + "m_ExcludeFromTUAndAA": false, + "m_TransparentDepthPrepass": false, + "m_TransparentDepthPostpass": false, + "m_SupportLodCrossFade": false, + "m_DoubleSidedMode": 0, + "m_DOTSInstancing": false, + "m_CustomVelocity": false, + "m_Tessellation": false, + "m_TessellationMode": 0, + "m_TessellationFactorMinDistance": 20.0, + "m_TessellationFactorMaxDistance": 50.0, + "m_TessellationFactorTriangleSize": 100.0, + "m_TessellationShapeFactor": 0.75, + "m_TessellationBackFaceCullEpsilon": -0.25, + "m_TessellationMaxDisplacement": 0.009999999776482582, + "m_DebugSymbols": false, + "m_Version": 2, + "inspectorFoldoutMask": 9 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.BuiltinData", + "m_ObjectId": "dbe2ed00baa650899b438cb84a5ee443", + "m_Distortion": false, + "m_DistortionMode": 0, + "m_DistortionDepthTest": true, + "m_AddPrecomputedVelocity": false, + "m_TransparentWritesMotionVec": false, + "m_DepthOffset": false, + "m_ConservativeDepthOffset": false, + "m_TransparencyFog": true, + "m_AlphaTestShadow": false, + "m_BackThenFrontRendering": false, + "m_TransparentDepthPrepass": false, + "m_TransparentDepthPostpass": false, + "m_TransparentPerPixelSorting": false, + "m_SupportLodCrossFade": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.LightingData", + "m_ObjectId": "78608e610afe598d9c19ac2fce925736", + "m_NormalDropOffSpace": 0, + "m_BlendPreserveSpecular": true, + "m_ReceiveDecals": true, + "m_ReceiveSSR": true, + "m_ReceiveSSRTransparent": false, + "m_SpecularAA": true, + "m_SpecularOcclusionMode": 1, + "m_OverrideBakedGI": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.HDLitData", + "m_ObjectId": "5efee24d3d145df7b6a2de4cf8200ad0", + "m_RayTracing": false, + "m_MaterialType": 0, + "m_MaterialTypeMask": 2, + "m_RefractionModel": 1, + "m_SSSTransmission": true, + "m_EnergyConservingSpecular": true, + "m_ClearCoat": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "6c3f79c836b45d7ca783db0b58c8a0a2", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Position", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "31b8be0ab2ea5d30830d210d2b9200e1" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Position" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PositionMaterialSlot", + "m_ObjectId": "31b8be0ab2ea5d30830d210d2b9200e1", + "m_Id": 0, + "m_DisplayName": "Position", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Position", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "1e4ff86c89cf52158fa52f50c68b7622", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Normal", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "bb48a1080fa85b1d87db616b9285e870" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Normal" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.NormalMaterialSlot", + "m_ObjectId": "bb48a1080fa85b1d87db616b9285e870", + "m_Id": 0, + "m_DisplayName": "Normal", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Normal", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "293d868c32ac5454bc5baa18d95958d0", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Tangent", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "4295929831725f29beb7f2abb6a036f5" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Tangent" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.TangentMaterialSlot", + "m_ObjectId": "4295929831725f29beb7f2abb6a036f5", + "m_Id": 0, + "m_DisplayName": "Tangent", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Tangent", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "bb541da4cd8f5bce9d7fb6758b047c46", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.BaseColor", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -44.999969482421875, + "y": 155.49998474121094, + "width": 199.99998474121094, + "height": 41.00001525878906 + } + }, + "m_Slots": [ + { + "m_Id": "9481e78455e45a4a88e5a3ea53f3c6fc" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.BaseColor" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.ColorRGBMaterialSlot", + "m_ObjectId": "9481e78455e45a4a88e5a3ea53f3c6fc", + "m_Id": 0, + "m_DisplayName": "Base Color", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "BaseColor", + "m_StageCapability": 2, + "m_Value": { + "x": 0.5, + "y": 0.5, + "z": 0.5 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_ColorMode": 0, + "m_DefaultColor": { + "r": 0.5, + "g": 0.5, + "b": 0.5, + "a": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "5cab6e924ebf5fbd98435c85181187d6", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.NormalTS", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -46.499935150146484, + "y": 466.0, + "width": 199.99990844726562, + "height": 40.99993896484375 + } + }, + "m_Slots": [ + { + "m_Id": "a40600d7e614588ca04d65f6cd2b0536" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.NormalTS" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.NormalMaterialSlot", + "m_ObjectId": "a40600d7e614588ca04d65f6cd2b0536", + "m_Id": 0, + "m_DisplayName": "Normal (Tangent Space)", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "NormalTS", + "m_StageCapability": 2, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 3 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "f9847ebe2d1c5ab4a9726ad4fc67938d", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Metallic", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -43.499900817871094, + "y": 278.5000305175781, + "width": 199.99996948242188, + "height": 41.0 + } + }, + "m_Slots": [ + { + "m_Id": "abfee700dc8a59e8a531f95b6f50074e" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Metallic" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "abfee700dc8a59e8a531f95b6f50074e", + "m_Id": 0, + "m_DisplayName": "Metallic", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Metallic", + "m_StageCapability": 2, + "m_Value": 0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "46b690f690045f93a980c40192b8334a", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Smoothness", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -44.0000114440918, + "y": 317.4999694824219, + "width": 200.0000457763672, + "height": 41.0 + } + }, + "m_Slots": [ + { + "m_Id": "1a74e104ca1d561db59265980ea80129" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Smoothness" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "1a74e104ca1d561db59265980ea80129", + "m_Id": 0, + "m_DisplayName": "Smoothness", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Smoothness", + "m_StageCapability": 2, + "m_Value": 0.5, + "m_DefaultValue": 0.5, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "f3861acedd695d2baffd0f91a051783d", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Occlusion", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -47.499977111816406, + "y": 280.5000305175781, + "width": 199.99996948242188, + "height": 41.0 + } + }, + "m_Slots": [ + { + "m_Id": "0e9e6902c6c35c5aa945368cdbb34e00" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Occlusion" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "0e9e6902c6c35c5aa945368cdbb34e00", + "m_Id": 0, + "m_DisplayName": "Ambient Occlusion", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Occlusion", + "m_StageCapability": 2, + "m_Value": 1, + "m_DefaultValue": 1.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "ac0e9b9ab4275ebf8452ae7fffface7f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Emission", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -44.500038146972656, + "y": 101.99987030029297, + "width": 200.00003051757812, + "height": 41.00000762939453 + } + }, + "m_Slots": [ + { + "m_Id": "e99a9f19d628587796972f50117e5275" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Emission" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.ColorRGBMaterialSlot", + "m_ObjectId": "e99a9f19d628587796972f50117e5275", + "m_Id": 0, + "m_DisplayName": "Emission", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Emission", + "m_StageCapability": 2, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_ColorMode": 1, + "m_DefaultColor": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "d7e5b5935814588d9e58aa447ec825af", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Alpha", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -49.00001525878906, + "y": 100.99995422363281, + "width": 199.99998474121094, + "height": 41.00001525878906 + } + }, + "m_Slots": [ + { + "m_Id": "49677f402c0d512391c56a1e65768ae0" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Alpha" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "49677f402c0d512391c56a1e65768ae0", + "m_Id": 0, + "m_DisplayName": "Alpha", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Alpha", + "m_StageCapability": 2, + "m_Value": 1.0, + "m_DefaultValue": 1.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "6eedf44ab7a85b48934617d05064c2c5", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.RefractionIndex", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -49.00001525878906, + "y": 100.99995422363281, + "width": 199.99998474121094, + "height": 41.00001525878906 + } + }, + "m_Slots": [ + { + "m_Id": "3576fa1886775c80afd25e93b1ffb229" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.RefractionIndex" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "3576fa1886775c80afd25e93b1ffb229", + "m_Id": 0, + "m_DisplayName": "Index Of Refraction", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "RefractionIndex", + "m_StageCapability": 2, + "m_Value": 1.49, + "m_DefaultValue": 1.49, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "ca03b11231c25e069a7ac8ce3ffec4ad", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.RefractionColor", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -44.999969482421875, + "y": 155.49998474121094, + "width": 199.99998474121094, + "height": 41.00001525878906 + } + }, + "m_Slots": [ + { + "m_Id": "49f23d7f40395047a128dadacaaab892" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.RefractionColor" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.ColorRGBMaterialSlot", + "m_ObjectId": "49f23d7f40395047a128dadacaaab892", + "m_Id": 0, + "m_DisplayName": "Transmittance Color", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "RefractionColor", + "m_StageCapability": 2, + "m_Value": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + }, + "m_DefaultValue": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + }, + "m_Labels": [], + "m_ColorMode": 0, + "m_DefaultColor": { + "r": 1.0, + "g": 1.0, + "b": 1.0, + "a": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "203e1b17561857a5884e227dd94e2a70", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.RefractionDistance", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -49.00001525878906, + "y": 100.99995422363281, + "width": 199.99998474121094, + "height": 41.00001525878906 + } + }, + "m_Slots": [ + { + "m_Id": "6f384109d5e8553caa38e0f082105943" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.RefractionDistance" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "6f384109d5e8553caa38e0f082105943", + "m_Id": 0, + "m_DisplayName": "Absorption Distance", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "RefractionDistance", + "m_StageCapability": 2, + "m_Value": 0.003, + "m_DefaultValue": 0.003, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "34e79bb6392457019e43f53374ac48f9", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Thickness", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -49.00001525878906, + "y": 100.99995422363281, + "width": 199.99998474121094, + "height": 41.00001525878906 + } + }, + "m_Slots": [ + { + "m_Id": "7d5e1866e51b58b8a9025ffbac353d66" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Thickness" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "7d5e1866e51b58b8a9025ffbac353d66", + "m_Id": 0, + "m_DisplayName": "Thickness", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Thickness", + "m_StageCapability": 2, + "m_Value": 0.004, + "m_DefaultValue": 0.004, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.CustomFunctionNode", + "m_ObjectId": "a93f8b0ecc385c40b55e136c1768652f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Playfield Normal", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 200, + "y": 120, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "942cd156886f5477ad0c60d7d930133d" + }, + { + "m_Id": "f577f7016b7e50719c77e987245bf314" + }, + { + "m_Id": "3b4b7df63b1754fcb0eb8edf6aadf167" + }, + { + "m_Id": "c243c383b68a593eb2ace677c9636ecb" + }, + { + "m_Id": "0b02084c78405f8f9792f8157485493a" + }, + { + "m_Id": "fd202d99c1d95dd8b021771927ede26f" + }, + { + "m_Id": "5867d7e8b70252e58618057a12ecb96a" + }, + { + "m_Id": "dcf1cd9cfb1c5080818a7163488725ac" + }, + { + "m_Id": "779260f5a50151d1aa78cd531254d0f4" + }, + { + "m_Id": "940c3496569356ddab5164220103f1fd" + }, + { + "m_Id": "361c9eeec046524fb4289b4a0c567e21" + }, + { + "m_Id": "82f91f0f83d15b27810f9cdb5eb076bb" + }, + { + "m_Id": "73ee302899d1508fb3f8221c03c4e859" + }, + { + "m_Id": "7feec6ffee2a564d9a265e239544b3ce" + }, + { + "m_Id": "5e0979d772795d8e98f48bb10f57a0a0" + }, + { + "m_Id": "0c0d768f11f457a7be67ff836afe1257" + }, + { + "m_Id": "a8b71cef2dcc50fbbfca093ef4da42d4" + }, + { + "m_Id": "9165bace26a75858978c8aa955e2a01f" + }, + { + "m_Id": "a0a44d465c905f8bb7d855f78f8937c0" + }, + { + "m_Id": "096a35b00c2452f28f04fabb17d132a8" + }, + { + "m_Id": "433f0a0561f75e86b4fb1e51c9df3a04" + }, + { + "m_Id": "1e892521a4ab56f7860d8a09d38a0665" + }, + { + "m_Id": "1d797c095e245d6b967a06fefeeff9f5" + }, + { + "m_Id": "d5ea220ccb7d5ab38031f80de430bbfb" + }, + { + "m_Id": "1b4e269462095c009d850a9c6b9bd369" + }, + { + "m_Id": "9f6505240c0d52a2bbff0e1f044f4597" + }, + { + "m_Id": "2401b0003ef35ead9766c7718f9ba685" + }, + { + "m_Id": "8caf0367ca0f5b41bc0b2495df74d388" + }, + { + "m_Id": "85d374ad9c0f5c2f935774e79ca5ae27" + }, + { + "m_Id": "5c4157811d9b5d0e95abbc40c2e19208" + }, + { + "m_Id": "55842f485b1a531ba4aa38098affd9c4" + }, + { + "m_Id": "3c5d50ef72f6418bab1e3d025b27d44d" + }, + { + "m_Id": "0a08c13e7bf14b60b5f8a82785e8628d" + }, + { + "m_Id": "d94a3a7c51ea4367b125945816d2e4f0" + } + ], + "synonyms": [ + "code", + "HLSL" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SourceType": 0, + "m_FunctionName": "InsertCover", + "m_FunctionSource": "a279abca59990fb4ca6f814dd1527731", + "m_FunctionBody": "" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector2MaterialSlot", + "m_ObjectId": "942cd156886f5477ad0c60d7d930133d", + "m_Id": 0, + "m_DisplayName": "UV", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "UV", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Texture2DInputMaterialSlot", + "m_ObjectId": "f577f7016b7e50719c77e987245bf314", + "m_Id": 1, + "m_DisplayName": "NormalMap", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "NormalMap", + "m_StageCapability": 3, + "m_BareResource": false, + "m_Texture": { + "m_SerializedTexture": "", + "m_Guid": "" + }, + "m_DefaultType": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "3b4b7df63b1754fcb0eb8edf6aadf167", + "m_Id": 2, + "m_DisplayName": "PlayfieldStrength", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "PlayfieldStrength", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "c243c383b68a593eb2ace677c9636ecb", + "m_Id": 3, + "m_DisplayName": "Strength", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Strength", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "0b02084c78405f8f9792f8157485493a", + "m_Id": 4, + "m_DisplayName": "Smoothness", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Smoothness", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "fd202d99c1d95dd8b021771927ede26f", + "m_Id": 5, + "m_DisplayName": "Opacity", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Opacity", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "5867d7e8b70252e58618057a12ecb96a", + "m_Id": 6, + "m_DisplayName": "NormalTS", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "NormalTS", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "dcf1cd9cfb1c5080818a7163488725ac", + "m_Id": 7, + "m_DisplayName": "SurfaceSmoothness", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "SurfaceSmoothness", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "779260f5a50151d1aa78cd531254d0f4", + "m_Id": 8, + "m_DisplayName": "Alpha", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Alpha", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "940c3496569356ddab5164220103f1fd", + "m_Id": 9, + "m_DisplayName": "HazeColor", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "HazeColor", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "361c9eeec046524fb4289b4a0c567e21", + "m_Id": 10, + "m_DisplayName": "BaseColor", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "BaseColor", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "82f91f0f83d15b27810f9cdb5eb076bb", + "m_Id": 11, + "m_DisplayName": "LampColor", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampColor", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "73ee302899d1508fb3f8221c03c4e859", + "m_Id": 12, + "m_DisplayName": "LampLevel", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampLevel", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "7feec6ffee2a564d9a265e239544b3ce", + "m_Id": 13, + "m_DisplayName": "LampTint", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampTint", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "5e0979d772795d8e98f48bb10f57a0a0", + "m_Id": 14, + "m_DisplayName": "Position", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Position", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "0c0d768f11f457a7be67ff836afe1257", + "m_Id": 15, + "m_DisplayName": "LampA", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampA", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "a8b71cef2dcc50fbbfca093ef4da42d4", + "m_Id": 16, + "m_DisplayName": "LampB", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampB", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "9165bace26a75858978c8aa955e2a01f", + "m_Id": 17, + "m_DisplayName": "LampC", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampC", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "a0a44d465c905f8bb7d855f78f8937c0", + "m_Id": 18, + "m_DisplayName": "LampD", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampD", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "096a35b00c2452f28f04fabb17d132a8", + "m_Id": 19, + "m_DisplayName": "DriveB", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "DriveB", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "433f0a0561f75e86b4fb1e51c9df3a04", + "m_Id": 20, + "m_DisplayName": "DriveC", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "DriveC", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "1e892521a4ab56f7860d8a09d38a0665", + "m_Id": 21, + "m_DisplayName": "DriveD", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "DriveD", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "1d797c095e245d6b967a06fefeeff9f5", + "m_Id": 22, + "m_DisplayName": "Spread", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Spread", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "d5ea220ccb7d5ab38031f80de430bbfb", + "m_Id": 23, + "m_DisplayName": "Ior", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Ior", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "1b4e269462095c009d850a9c6b9bd369", + "m_Id": 24, + "m_DisplayName": "Tint", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Tint", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "9f6505240c0d52a2bbff0e1f044f4597", + "m_Id": 25, + "m_DisplayName": "TintDepth", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "TintDepth", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "2401b0003ef35ead9766c7718f9ba685", + "m_Id": 26, + "m_DisplayName": "Thickness", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Thickness", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "8caf0367ca0f5b41bc0b2495df74d388", + "m_Id": 27, + "m_DisplayName": "RefractionIndex", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "RefractionIndex", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "85d374ad9c0f5c2f935774e79ca5ae27", + "m_Id": 28, + "m_DisplayName": "TransmittanceColor", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "TransmittanceColor", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "5c4157811d9b5d0e95abbc40c2e19208", + "m_Id": 29, + "m_DisplayName": "AbsorptionDistance", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "AbsorptionDistance", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "55842f485b1a531ba4aa38098affd9c4", + "m_Id": 30, + "m_DisplayName": "SlabThickness", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "SlabThickness", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.UVNode", + "m_ObjectId": "c2ba7fad87fc5a7cbddeda15d70be245", + "m_Group": { + "m_Id": "" + }, + "m_Name": "UV", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -100, + "y": 0, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "a0978221bdab5bc0b38ef310df71b821" + } + ], + "synonyms": [ + "texcoords", + "coords", + "coordinates" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_OutputChannel": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "a0978221bdab5bc0b38ef310df71b821", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Internal.Texture2DShaderProperty", + "m_ObjectId": "ad06f3fb3b0c594a95419141a6275219", + "m_Guid": { + "m_GuidSerialized": "ad06f3fb-3b0c-594a-9541-9141a6275219" + }, + "m_Name": "Normal Map", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Playfield Normal", + "m_DefaultReferenceName": "_CoverNormalMap", + "m_OverrideReferenceName": "_CoverNormalMap", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "m_SerializedTexture": "", + "m_Guid": "" + }, + "isMainTexture": false, + "useTilingAndOffset": true, + "useTexelSize": false, + "m_Modifiable": true, + "m_DefaultType": 3 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "258469db3724556da3ce5aa5103917ab", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 80, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "18b8e4a7c2a75c8bb7e736c5423cd6b1" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "ad06f3fb3b0c594a95419141a6275219" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Texture2DMaterialSlot", + "m_ObjectId": "18b8e4a7c2a75c8bb7e736c5423cd6b1", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_BareResource": false +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "fae01e6a006851718b1e68199e602c03", + "m_Guid": { + "m_GuidSerialized": "fae01e6a-0068-5171-8b1e-68199e602c03" + }, + "m_Name": "Playfield Normal Strength", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Playfield Normal Strength", + "m_DefaultReferenceName": "_PlayfieldNormalStrength", + "m_OverrideReferenceName": "_PlayfieldNormalStrength", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": 1, + "m_FloatType": 0, + "m_RangeValues": { + "x": 1.0, + "y": 64.0 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "93eb8464ca0b54ddb97499e11a6c674f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 160, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "e4527a36f8715829af8508c4841d0a9b" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "fae01e6a006851718b1e68199e602c03" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "e4527a36f8715829af8508c4841d0a9b", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "2097df164c9e5fb1947791cdfa588dd1", + "m_Guid": { + "m_GuidSerialized": "2097df16-4c9e-5fb1-9477-91cdfa588dd1" + }, + "m_Name": "Normal Strength", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Normal Strength", + "m_DefaultReferenceName": "_CoverNormalStrength", + "m_OverrideReferenceName": "_CoverNormalStrength", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 1, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 2 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "2cb25b426b3e5c90836f456a1beda794", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 240, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "4f063c67e7e8519196f4c8223897fc72" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "2097df164c9e5fb1947791cdfa588dd1" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "4f063c67e7e8519196f4c8223897fc72", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "f611e7ae978c57538ba217fb9b8c077a", + "m_Guid": { + "m_GuidSerialized": "f611e7ae-978c-5753-8ba2-17fb9b8c077a" + }, + "m_Name": "Smoothness", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Smoothness", + "m_DefaultReferenceName": "_Smoothness", + "m_OverrideReferenceName": "_Smoothness", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.96, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "55bdf08e49c45b8eb889aa7ac58378cb", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 320, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "c9a46acab96a5fbb97b8a5c1f236d181" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "f611e7ae978c57538ba217fb9b8c077a" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "c9a46acab96a5fbb97b8a5c1f236d181", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "5b38cbf426f45f719881c428caf2ff6f", + "m_Guid": { + "m_GuidSerialized": "5b38cbf4-26f4-5f71-9881-c428caf2ff6f" + }, + "m_Name": "Opacity", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Opacity", + "m_DefaultReferenceName": "_CoverOpacity", + "m_OverrideReferenceName": "_CoverOpacity", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.01, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 0.5 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "86439196d3665c5c939ed992d61d38db", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 400, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "9cf77c9759645b3b95e1c3e7f82754f4" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "5b38cbf426f45f719881c428caf2ff6f" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "9cf77c9759645b3b95e1c3e7f82754f4", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.Internal.ColorShaderProperty", + "m_ObjectId": "ca43925be4165b178d1f5bc49e3651ca", + "m_Guid": { + "m_GuidSerialized": "ca43925b-e416-5b17-8d1f-5bc49e3651ca" + }, + "m_Name": "Haze Color", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Haze Color", + "m_DefaultReferenceName": "_HazeColor", + "m_OverrideReferenceName": "_HazeColor", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "r": 0.8, + "g": 0.8, + "b": 0.8, + "a": 1 + }, + "isMainColor": false, + "m_ColorMode": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "84d0067015ba51dd9ecbac43815457ee", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 480, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "938b3d114ddb5b93b607a2293e0ae825" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "ca43925be4165b178d1f5bc49e3651ca" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "938b3d114ddb5b93b607a2293e0ae825", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "6afc15978a595c2d8813b1230b602de2", + "m_Guid": { + "m_GuidSerialized": "6afc1597-8a59-5c2d-8813-b1230b602de2" + }, + "m_Name": "Index of Refraction", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Index of Refraction", + "m_DefaultReferenceName": "_CoverIor", + "m_OverrideReferenceName": "_CoverIor", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 1.49, + "m_FloatType": 1, + "m_RangeValues": { + "x": 1, + "y": 2 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "957e9ed0c7c75a15b6f95d921b90f7c5", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 560, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "387e2958b0695e8a8853a8ae77c58775" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "6afc15978a595c2d8813b1230b602de2" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "387e2958b0695e8a8853a8ae77c58775", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.Internal.ColorShaderProperty", + "m_ObjectId": "5ee46cc3aef851a28108df86cdbda282", + "m_Guid": { + "m_GuidSerialized": "5ee46cc3-aef8-51a2-8108-df86cdbda282" + }, + "m_Name": "Tint (through the plastic)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Tint (through the plastic)", + "m_DefaultReferenceName": "_CoverTint", + "m_OverrideReferenceName": "_CoverTint", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "r": 1, + "g": 1, + "b": 1, + "a": 1 + }, + "isMainColor": false, + "m_ColorMode": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "3a23644eca7454818fdf008ee8debf7a", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 640, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "22da729fd0e059c0a3c52b14d58affab" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "5ee46cc3aef851a28108df86cdbda282" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "22da729fd0e059c0a3c52b14d58affab", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "4e2ee789d2ea5b25b2c2a9618c1b8751", + "m_Guid": { + "m_GuidSerialized": "4e2ee789-d2ea-5b25-b2c2-a9618c1b8751" + }, + "m_Name": "Tint Depth (mm)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Tint Depth (mm)", + "m_DefaultReferenceName": "_CoverTintDepth", + "m_OverrideReferenceName": "_CoverTintDepth", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 3, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0.1, + "y": 20 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "fc79f245f21e57e691f820bd183097dc", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 720, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "848bf7c4287355369ba9a83fc1891f33" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "4e2ee789d2ea5b25b2c2a9618c1b8751" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "848bf7c4287355369ba9a83fc1891f33", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "41b0c9f64d6e57c8bef5ef55302a84e7", + "m_Guid": { + "m_GuidSerialized": "41b0c9f6-4d6e-57c8-bef5-ef55302a84e7" + }, + "m_Name": "Thickness (runtime, mm)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Thickness (runtime, mm)", + "m_DefaultReferenceName": "_CoverThickness", + "m_OverrideReferenceName": "_CoverThickness", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": 4, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0.1, + "y": 20 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "e39b393c054c5627b0a7dc680763496e", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 800, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "e1072f0ff9ae5b2f9fe12dfc85cc9109" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "41b0c9f64d6e57c8bef5ef55302a84e7" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "e1072f0ff9ae5b2f9fe12dfc85cc9109", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "e48982adfc4a5689b1b479e31ba05ead", + "m_Guid": { + "m_GuidSerialized": "e48982ad-fc4a-5689-b1b4-79e31ba05ead" + }, + "m_Name": "Lamp RGB (linear, runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp RGB (linear, runtime)", + "m_DefaultReferenceName": "_InsertLampColor", + "m_OverrideReferenceName": "_InsertLampColor", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 1, + "y": 1, + "z": 1, + "w": 1 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "ae6fbc0cd86a5367a59330cce4ee6483", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 880, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "d5a288ef59cf5879b4e08d4ffcaf52dd" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "e48982adfc4a5689b1b479e31ba05ead" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "d5a288ef59cf5879b4e08d4ffcaf52dd", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.PositionNode", + "m_ObjectId": "a9efd782c94655aeaf5d15772153e39b", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Position", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -100, + "y": 900, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "d4e08638c5ee5c43a31b789c661aa522" + } + ], + "synonyms": [ + "location" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 0, + "m_PositionSource": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "d4e08638c5ee5c43a31b789c661aa522", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "beaf92fa297c5f0f8cfffa63a937cc10", + "m_Guid": { + "m_GuidSerialized": "beaf92fa-297c-5f0f-8cff-fa63a937cc10" + }, + "m_Name": "Virtual Lamp (runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Virtual Lamp (runtime)", + "m_DefaultReferenceName": "_OpticalLamp", + "m_OverrideReferenceName": "_OpticalLamp", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 0, + "y": -0.015, + "z": 0, + "w": 0.008 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "d5cb3ef25c845329974239d959f0af4f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 960, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "2ca2a87a01535897a00f2ca103b16976" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "beaf92fa297c5f0f8cfffa63a937cc10" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "2ca2a87a01535897a00f2ca103b16976", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "e369cdcd58465c1ea4f76f73dfc2dc40", + "m_Guid": { + "m_GuidSerialized": "e369cdcd-5846-5c1e-a4f7-6f73dfc2dc40" + }, + "m_Name": "Virtual Lamp B (runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Virtual Lamp B (runtime)", + "m_DefaultReferenceName": "_OpticalLampB", + "m_OverrideReferenceName": "_OpticalLampB", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 0, + "y": 0, + "z": 0, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "9eaf027f14a05caaa4525c402bf52b36", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1040, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "37ead9122de653408cc5d243743845d5" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "e369cdcd58465c1ea4f76f73dfc2dc40" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "37ead9122de653408cc5d243743845d5", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "73ee4f83fb3f55998eaa8615d946fd00", + "m_Guid": { + "m_GuidSerialized": "73ee4f83-fb3f-5599-8eaa-8615d946fd00" + }, + "m_Name": "Lamp RGB and level (runtime), bulb B", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp RGB and level (runtime), bulb B", + "m_DefaultReferenceName": "_InsertLampColorB", + "m_OverrideReferenceName": "_InsertLampColorB", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 1, + "y": 1, + "z": 1, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "4a4c94b2e0b75a19ab193ec933ea1466", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1120, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "960b1ebc111e522da93b6ce909dfb45f" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "73ee4f83fb3f55998eaa8615d946fd00" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "960b1ebc111e522da93b6ce909dfb45f", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "a40e81823ec95013b379358b4dc225df", + "m_Guid": { + "m_GuidSerialized": "a40e8182-3ec9-5013-b379-358b4dc225df" + }, + "m_Name": "Virtual Lamp C (runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Virtual Lamp C (runtime)", + "m_DefaultReferenceName": "_OpticalLampC", + "m_OverrideReferenceName": "_OpticalLampC", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 0, + "y": 0, + "z": 0, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "53fade0153a85229ba074a003b4e5d88", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1200, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "5c4edaf8dd2d500dbc7ccc231319e9b6" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "a40e81823ec95013b379358b4dc225df" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "5c4edaf8dd2d500dbc7ccc231319e9b6", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "8a7f5ba32f7354ccae9f44d72abefd82", + "m_Guid": { + "m_GuidSerialized": "8a7f5ba3-2f73-54cc-ae9f-44d72abefd82" + }, + "m_Name": "Lamp RGB and level (runtime), bulb C", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp RGB and level (runtime), bulb C", + "m_DefaultReferenceName": "_InsertLampColorC", + "m_OverrideReferenceName": "_InsertLampColorC", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 1, + "y": 1, + "z": 1, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "5b1791466d87517792a0ff4cac505750", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1280, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "81e57c53bd5156a48b933061a5ce149a" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "8a7f5ba32f7354ccae9f44d72abefd82" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "81e57c53bd5156a48b933061a5ce149a", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "bc2becce24e8513a8434c08c2fbf42b9", + "m_Guid": { + "m_GuidSerialized": "bc2becce-24e8-513a-8434-c08c2fbf42b9" + }, + "m_Name": "Virtual Lamp D (runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Virtual Lamp D (runtime)", + "m_DefaultReferenceName": "_OpticalLampD", + "m_OverrideReferenceName": "_OpticalLampD", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 0, + "y": 0, + "z": 0, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "8c8812e0868258fda47f84f843fb6f89", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1360, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "f89014afc4225d548cfb931c58c5bba8" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "bc2becce24e8513a8434c08c2fbf42b9" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "f89014afc4225d548cfb931c58c5bba8", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "597db27618c157c99116431fe62014ac", + "m_Guid": { + "m_GuidSerialized": "597db276-18c1-57c9-9116-431fe62014ac" + }, + "m_Name": "Lamp RGB and level (runtime), bulb D", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp RGB and level (runtime), bulb D", + "m_DefaultReferenceName": "_InsertLampColorD", + "m_OverrideReferenceName": "_InsertLampColorD", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 1, + "y": 1, + "z": 1, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "be026746f05e5466ba9548f5e3cc5d38", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1440, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "1d156b09528b5d7c9904a2223032dd3b" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "597db27618c157c99116431fe62014ac" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "1d156b09528b5d7c9904a2223032dd3b", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "e34e3001a11f5de799da15122a2efb3d", + "m_Guid": { + "m_GuidSerialized": "e34e3001-a11f-5de7-99da-15122a2efb3d" + }, + "m_Name": "Lamp Spread (runtime, mm)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp Spread (runtime, mm)", + "m_DefaultReferenceName": "_OpticalSpread", + "m_OverrideReferenceName": "_OpticalSpread", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 100 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "d4705b78c4255faabcebf85cadf8ab7f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1520, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "db07f42c7bb650e3ac8ea4d83a99afd6" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "e34e3001a11f5de799da15122a2efb3d" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "db07f42c7bb650e3ac8ea4d83a99afd6", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "ea281cf402cf56caa413b6c3334f283e", + "m_Guid": { + "m_GuidSerialized": "ea281cf4-02cf-56ca-a413-b6c3334f283e" + }, + "m_Name": "Intensity (runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Intensity (runtime)", + "m_DefaultReferenceName": "_InsertLampLevel", + "m_OverrideReferenceName": "_InsertLampLevel", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "3cb544b2383a52a08d6dfa42b0182fc5", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1600, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "e747aacbfeeb52ffac3638a80981fafa" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "ea281cf402cf56caa413b6c3334f283e" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "e747aacbfeeb52ffac3638a80981fafa", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "d3e87f63665b5fcba90acf09dc7ca55b", + "m_Guid": { + "m_GuidSerialized": "d3e87f63-665b-5fcb-a90a-cf09dc7ca55b" + }, + "m_Name": "Lamp Tint (runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp Tint (runtime)", + "m_DefaultReferenceName": "_InsertLampTint", + "m_OverrideReferenceName": "_InsertLampTint", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "970b3b62e1625d8784503fc213668716", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1680, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "7c6eb7d2ed8553a58bbe567d204a2f12" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "d3e87f63665b5fcba90acf09dc7ca55b" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "7c6eb7d2ed8553a58bbe567d204a2f12", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.CategoryData", + "m_ObjectId": "7b398cb0bb835563b2574124a2b247d5", + "m_Name": "", + "m_ChildObjectList": [ + { + "m_Id": "ad06f3fb3b0c594a95419141a6275219" + }, + { + "m_Id": "fae01e6a006851718b1e68199e602c03" + }, + { + "m_Id": "2097df164c9e5fb1947791cdfa588dd1" + }, + { + "m_Id": "f611e7ae978c57538ba217fb9b8c077a" + }, + { + "m_Id": "5b38cbf426f45f719881c428caf2ff6f" + }, + { + "m_Id": "ca43925be4165b178d1f5bc49e3651ca" + }, + { + "m_Id": "6afc15978a595c2d8813b1230b602de2" + }, + { + "m_Id": "5ee46cc3aef851a28108df86cdbda282" + }, + { + "m_Id": "4e2ee789d2ea5b25b2c2a9618c1b8751" + }, + { + "m_Id": "41b0c9f64d6e57c8bef5ef55302a84e7" + }, + { + "m_Id": "e48982adfc4a5689b1b479e31ba05ead" + }, + { + "m_Id": "beaf92fa297c5f0f8cfffa63a937cc10" + }, + { + "m_Id": "e369cdcd58465c1ea4f76f73dfc2dc40" + }, + { + "m_Id": "73ee4f83fb3f55998eaa8615d946fd00" + }, + { + "m_Id": "a40e81823ec95013b379358b4dc225df" + }, + { + "m_Id": "8a7f5ba32f7354ccae9f44d72abefd82" + }, + { + "m_Id": "bc2becce24e8513a8434c08c2fbf42b9" + }, + { + "m_Id": "597db27618c157c99116431fe62014ac" + }, + { + "m_Id": "e34e3001a11f5de799da15122a2efb3d" + }, + { + "m_Id": "ea281cf402cf56caa413b6c3334f283e" + }, + { + "m_Id": "d3e87f63665b5fcba90acf09dc7ca55b" + } + ] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalTarget", + "m_ObjectId": "3c06f88b3ab44722a6d9d8e8794df1db", + "m_Datas": [], + "m_ActiveSubTarget": { + "m_Id": "4b4a46faeeab4e6da17eb93882746027" + }, + "m_AllowMaterialOverride": false, + "m_SurfaceType": 1, + "m_ZTestMode": 4, + "m_ZWriteControl": 2, + "m_AlphaMode": 0, + "m_RenderFace": 2, + "m_AlphaClip": false, + "m_CastShadows": false, + "m_ReceiveShadows": true, + "m_CustomEditorGUI": "VisualPinball.Unity.Urp.InsertShaderGUI", + "m_SupportVFX": false +} + +{ + "m_SGVersion": 2, + "m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalLitSubTarget", + "m_ObjectId": "4b4a46faeeab4e6da17eb93882746027", + "m_WorkflowMode": 1, + "m_NormalDropOffSpace": 0, + "m_ClearCoat": false, + "m_BlendModePreserveSpecular": true +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "3c5d50ef72f6418bab1e3d025b27d44d", + "m_Id": 31, + "m_DisplayName": "Emission", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Emission", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.TangentVectorNode", + "m_ObjectId": "c97ff5e9b87140c691321a3e8fc99e68", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Tangent Vector", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -200, + "y": 900, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "5821f697c1b64c319dda20b17c8962d2" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "5821f697c1b64c319dda20b17c8962d2", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "0a08c13e7bf14b60b5f8a82785e8628d", + "m_Id": 32, + "m_DisplayName": "TangentOS", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "TangentOS", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BitangentVectorNode", + "m_ObjectId": "b99248f4c5a34783878a33a7e2bc8fb9", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Bitangent Vector", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -200, + "y": 1050, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "e76f6d80e6cd40aa9db94757c6fe84f0" + } + ], + "synonyms": [ + "binormal" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "e76f6d80e6cd40aa9db94757c6fe84f0", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "d94a3a7c51ea4367b125945816d2e4f0", + "m_Id": 33, + "m_DisplayName": "BitangentOS", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "BitangentOS", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} diff --git a/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Cover.shadergraph.meta b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Cover.shadergraph.meta new file mode 100644 index 000000000..f28cd7a1f --- /dev/null +++ b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Cover.shadergraph.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: e23e1e894fc3bcd47ae3d63d8abc3cd0 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3} + useAsTemplate: 0 + exposeTemplateAsShader: 0 + indexedData: {instanceID: 0} + template: + name: + category: + description: + icon: {instanceID: 0} + thumbnail: {instanceID: 0} + order: 0 diff --git a/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Hybrid.shadergraph b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Hybrid.shadergraph new file mode 100644 index 000000000..c03e7a214 --- /dev/null +++ b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Hybrid.shadergraph @@ -0,0 +1,7354 @@ +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.GraphData", + "m_ObjectId": "db4e2d3aa9a45bc48f43594a6b04696b", + "m_Properties": [ + { + "m_Id": "f5cccd49a27950b79db69a09be9d7a89" + }, + { + "m_Id": "9c58618dfaf852e18f694a70f847b2cc" + }, + { + "m_Id": "80a6c7d4fa675c1f97d62030c40e284b" + }, + { + "m_Id": "cbaacef800f25aed9377b6affe122eb3" + }, + { + "m_Id": "b32e0a9968715785b711d9c950e67e70" + }, + { + "m_Id": "4f866e7a0d8653f1b37d5caa4190d4e5" + }, + { + "m_Id": "fb72f27f05ae538280d3a75b2cd757eb" + }, + { + "m_Id": "87bc8c4be8665adf8fb006ef67285c5d" + }, + { + "m_Id": "cebcb51b748d5b6889bc9956d0d479a3" + }, + { + "m_Id": "f2aa93755f9f5663be6045d6be0c61b9" + }, + { + "m_Id": "e28afaaa8fe053bd91f0e818b549fb77" + }, + { + "m_Id": "c6bcc2966afc5ae488259f75fc7b836d" + }, + { + "m_Id": "48504b8bc994589c80868e82996c56f2" + }, + { + "m_Id": "b032f322f53d557897aa5f0dd7e22c40" + }, + { + "m_Id": "5caa794e9c8856c8a296d48eaaa129e7" + }, + { + "m_Id": "3a765b8511e35e8fb3bd93b9f474c3d6" + }, + { + "m_Id": "79282d2d1f9f59beae1442cb5aec9d4a" + }, + { + "m_Id": "76b742d448275dac96c5a785ed5aa046" + }, + { + "m_Id": "c55299604c8f58ddb1951c3fccf48b0f" + }, + { + "m_Id": "79ce1dea7425576c927c76564474bdaa" + }, + { + "m_Id": "8f2697a2ad035fd283e96674f6099e87" + }, + { + "m_Id": "47031cac033a52d290a3f7d58ebeb0e5" + }, + { + "m_Id": "afd2ca8cba535099b1257c3f27290983" + }, + { + "m_Id": "d8f0b44d8ff45f8f9b7a97b64a7a5db0" + }, + { + "m_Id": "4c5ab3de0c985513bf7247b33879f300" + }, + { + "m_Id": "7550e938d31e5c399795602e29d14c3a" + }, + { + "m_Id": "391f5b4513d65194a4ba42d431bd8c93" + }, + { + "m_Id": "eddfdb9e051d5865834eb933671aaff4" + }, + { + "m_Id": "d1d92de60b5b50679d958d5415cb1bce" + }, + { + "m_Id": "7d4e3081cedd5c919f3bc85431603a72" + }, + { + "m_Id": "d6e78c4734055e67b1986e96afc59726" + }, + { + "m_Id": "975f88b1cbd05c3690f7e57ac7b2eb89" + }, + { + "m_Id": "ab903b8243655d0ea0911669c7c3432b" + }, + { + "m_Id": "d63fb2c8f30356c3aae3606059477dcd" + }, + { + "m_Id": "1e80a80e3298577f8a09c329c878fc93" + }, + { + "m_Id": "0868c71889275f43b8ad9867b67f2b93" + }, + { + "m_Id": "7745a741a46459bc84e174ad710431f3" + }, + { + "m_Id": "b1118c1270965d32a84bc58296cea0d5" + }, + { + "m_Id": "44276acd6332572585d8c5ea51c2b692" + }, + { + "m_Id": "ff36167b8ae75487bb5eccb8dd152070" + }, + { + "m_Id": "e188a4e6b18b54d89908031069759967" + }, + { + "m_Id": "5180ea7940775616b68bd2de94b6fd74" + }, + { + "m_Id": "a144dcb8a49459c19a2080583d6a71d3" + }, + { + "m_Id": "b4462b630ce1577fbdb18f40505fc0b7" + }, + { + "m_Id": "7b66c53699fb4e329b7b4ff523715868" + } + ], + "m_Keywords": [], + "m_Dropdowns": [], + "m_CategoryData": [ + { + "m_Id": "efcb6c45f55553f89edd4456c2a91dad" + } + ], + "m_Nodes": [ + { + "m_Id": "8b17db6110675180842cf69d50d4be33" + }, + { + "m_Id": "1416733beee65259aa05cc935db9583f" + }, + { + "m_Id": "50981c22d63c5fe9be71f306eab3aa8b" + }, + { + "m_Id": "f1c30112c2ac57528b85dd7db4a02b19" + }, + { + "m_Id": "af8e062f44565d54b5f377c2829f4bf3" + }, + { + "m_Id": "67ea0d7b089d573492b036727e05ce99" + }, + { + "m_Id": "90f29fe6214e5becb77e139ba2a1b2a6" + }, + { + "m_Id": "5d1129f3ab125ae794f880badfe3b76f" + }, + { + "m_Id": "8b2ba8d4c746548e8f3a638475ef2a12" + }, + { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + { + "m_Id": "b57bfa9c48e8548695944203dd7264fa" + }, + { + "m_Id": "77adde5a503255bc8730b099ed03c298" + }, + { + "m_Id": "00654fd1b821567b946f725c6879acc6" + }, + { + "m_Id": "5774dd3dc28e51158e12894808bccd68" + }, + { + "m_Id": "181b1e1da978581b981f173bd21e7ed6" + }, + { + "m_Id": "6d910a0a0fbc5419b374f6d76193b271" + }, + { + "m_Id": "a7dea086444d58c09eb5c70fdac63f41" + }, + { + "m_Id": "c74254a56fae556ab408262a4ee977ae" + }, + { + "m_Id": "8e3e2aaf2c8558d9937179b50257456f" + }, + { + "m_Id": "38a0027d064f5a39b3b966d8859ef5f9" + }, + { + "m_Id": "b13762a60757541eb340dac8055f5f35" + }, + { + "m_Id": "9254815eb565514fb64170a48b987901" + }, + { + "m_Id": "93bfa3c99f135970a74b4e3ba8428d7d" + }, + { + "m_Id": "8c40c59aad9650a5bbb772fbe4e38b96" + }, + { + "m_Id": "0c02ad2153675de98a7d0fe17c4d4c4e" + }, + { + "m_Id": "6ddcfef30bf05ecdaf4b8b68419e8db6" + }, + { + "m_Id": "20589895c6be54ea8a175ab190d10bf6" + }, + { + "m_Id": "bfa4def9778953faa67d6315312a64e9" + }, + { + "m_Id": "39c18e46011b582e81b386af722fb317" + }, + { + "m_Id": "5a54035f9b50583e9369005068a9330c" + }, + { + "m_Id": "54220b1a481651f38db03e60c90c40f8" + }, + { + "m_Id": "428c2f754584503890fda944c30ff28f" + }, + { + "m_Id": "d7c11bfbd1bd52608d3a654f476cb993" + }, + { + "m_Id": "c0d964a73077504cbc5b4b2d0f8408c1" + }, + { + "m_Id": "4b32d57f8adf5f11bbc862e34fa6d0c8" + }, + { + "m_Id": "756bb71064fa5a0c8be319a413b19390" + }, + { + "m_Id": "f841f17bc1fb5cf4a403c21690a12576" + }, + { + "m_Id": "af3da35925e55b4682bf83cb054d2db9" + }, + { + "m_Id": "7b2587fa2d6f5838bcc0940478c93467" + }, + { + "m_Id": "72c929a4c9295f2dbfba96ef32f2c945" + }, + { + "m_Id": "648cdf0c4ccd56dab79e0839510f9850" + }, + { + "m_Id": "432b49b887d35d83bf88a081723b160e" + }, + { + "m_Id": "7a917aa723ee5ee4a58985dcfa8fb709" + }, + { + "m_Id": "eb3e21d0b35e5384817d0c6b2ecba57a" + }, + { + "m_Id": "312a376a2c9d5949a63495eb43da788a" + }, + { + "m_Id": "1686d3f1b0615565aa5a86324933c761" + }, + { + "m_Id": "d8f56c5855f75c5a9f96ef67e55b4205" + }, + { + "m_Id": "89a60d00f1fd5348973c297d5401ec5a" + }, + { + "m_Id": "1704bd811b1c5a35bedfb8dda5462598" + }, + { + "m_Id": "bfcc339d0cb456c987457755aabfb04d" + }, + { + "m_Id": "233c9907b34555cd80459f4a0921d1c3" + }, + { + "m_Id": "65db75acf0cc5d4f9d00d1246dc2acff" + }, + { + "m_Id": "9a23f1d133965f8c8ca9b90a7633c2a2" + }, + { + "m_Id": "f46c2964302e54489e6557a72029b007" + }, + { + "m_Id": "925bd03a2d6855e098beab765a34e9e5" + }, + { + "m_Id": "ca9058a29ffb5a5eaa1c0d8a29ef029e" + }, + { + "m_Id": "ec2fa20232095a95b7abe4dfc9a905ec" + }, + { + "m_Id": "b55bcb0aba7950528b1d1b566bd97877" + }, + { + "m_Id": "0229938f88d85671b777b9408b22ed4e" + }, + { + "m_Id": "8f121541bc7a5534828e4315a9d06bd6" + }, + { + "m_Id": "fb36717fa96f4d248390595746a431e8" + } + ], + "m_GroupDatas": [], + "m_StickyNoteDatas": [], + "m_Edges": [ + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 24 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "f1c30112c2ac57528b85dd7db4a02b19" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 25 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "af8e062f44565d54b5f377c2829f4bf3" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 26 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "8b2ba8d4c746548e8f3a638475ef2a12" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "b57bfa9c48e8548695944203dd7264fa" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "77adde5a503255bc8730b099ed03c298" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 1 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "00654fd1b821567b946f725c6879acc6" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 2 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "5774dd3dc28e51158e12894808bccd68" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 27 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "181b1e1da978581b981f173bd21e7ed6" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 28 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "6d910a0a0fbc5419b374f6d76193b271" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 29 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a7dea086444d58c09eb5c70fdac63f41" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 3 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "c74254a56fae556ab408262a4ee977ae" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 4 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "8e3e2aaf2c8558d9937179b50257456f" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 5 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "38a0027d064f5a39b3b966d8859ef5f9" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 6 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "b13762a60757541eb340dac8055f5f35" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 37 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "9254815eb565514fb64170a48b987901" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 38 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "93bfa3c99f135970a74b4e3ba8428d7d" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 39 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "8c40c59aad9650a5bbb772fbe4e38b96" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 7 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "0c02ad2153675de98a7d0fe17c4d4c4e" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 8 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "6ddcfef30bf05ecdaf4b8b68419e8db6" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 9 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "20589895c6be54ea8a175ab190d10bf6" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 10 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "bfa4def9778953faa67d6315312a64e9" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 11 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "39c18e46011b582e81b386af722fb317" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 15 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "5a54035f9b50583e9369005068a9330c" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 17 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "54220b1a481651f38db03e60c90c40f8" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 18 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "428c2f754584503890fda944c30ff28f" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 19 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "d7c11bfbd1bd52608d3a654f476cb993" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 20 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "c0d964a73077504cbc5b4b2d0f8408c1" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 21 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "4b32d57f8adf5f11bbc862e34fa6d0c8" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 22 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "756bb71064fa5a0c8be319a413b19390" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 23 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "f841f17bc1fb5cf4a403c21690a12576" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 30 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "af3da35925e55b4682bf83cb054d2db9" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 31 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "7b2587fa2d6f5838bcc0940478c93467" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 33 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "72c929a4c9295f2dbfba96ef32f2c945" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 34 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "648cdf0c4ccd56dab79e0839510f9850" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 35 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "432b49b887d35d83bf88a081723b160e" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 36 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "7a917aa723ee5ee4a58985dcfa8fb709" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 43 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "eb3e21d0b35e5384817d0c6b2ecba57a" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 44 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "312a376a2c9d5949a63495eb43da788a" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 45 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "1686d3f1b0615565aa5a86324933c761" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 46 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "d8f56c5855f75c5a9f96ef67e55b4205" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 47 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "89a60d00f1fd5348973c297d5401ec5a" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 48 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "1704bd811b1c5a35bedfb8dda5462598" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 49 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "bfcc339d0cb456c987457755aabfb04d" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 50 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "233c9907b34555cd80459f4a0921d1c3" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 51 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "65db75acf0cc5d4f9d00d1246dc2acff" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 12 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "9a23f1d133965f8c8ca9b90a7633c2a2" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 32 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "f46c2964302e54489e6557a72029b007" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 13 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "925bd03a2d6855e098beab765a34e9e5" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 14 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "ca9058a29ffb5a5eaa1c0d8a29ef029e" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 16 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "ec2fa20232095a95b7abe4dfc9a905ec" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 40 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "b55bcb0aba7950528b1d1b566bd97877" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 41 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "0229938f88d85671b777b9408b22ed4e" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 42 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "8f121541bc7a5534828e4315a9d06bd6" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "90f29fe6214e5becb77e139ba2a1b2a6" + }, + "m_SlotId": 0 + } + }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "fb36717fa96f4d248390595746a431e8" + }, + "m_SlotId": 0 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "eb08b67e7dc45dba82c148f72e5dd340" + }, + "m_SlotId": 52 + } + } + ], + "m_VertexContext": { + "m_Position": { + "x": 800, + "y": 0 + }, + "m_Blocks": [ + { + "m_Id": "8b17db6110675180842cf69d50d4be33" + }, + { + "m_Id": "1416733beee65259aa05cc935db9583f" + }, + { + "m_Id": "50981c22d63c5fe9be71f306eab3aa8b" + } + ] + }, + "m_FragmentContext": { + "m_Position": { + "x": 800, + "y": 220 + }, + "m_Blocks": [ + { + "m_Id": "f1c30112c2ac57528b85dd7db4a02b19" + }, + { + "m_Id": "af8e062f44565d54b5f377c2829f4bf3" + }, + { + "m_Id": "67ea0d7b089d573492b036727e05ce99" + }, + { + "m_Id": "90f29fe6214e5becb77e139ba2a1b2a6" + }, + { + "m_Id": "5d1129f3ab125ae794f880badfe3b76f" + }, + { + "m_Id": "8b2ba8d4c746548e8f3a638475ef2a12" + } + ] + }, + "m_PreviewData": { + "serializedMesh": { + "m_SerializedMesh": "{\"mesh\":{\"instanceID\":0}}", + "m_Guid": "" + }, + "preventRotation": false + }, + "m_Path": "Pinball", + "m_GraphPrecision": 1, + "m_PreviewMode": 2, + "m_OutputNode": { + "m_Id": "" + }, + "m_SubDatas": [], + "m_ActiveTargets": [ + { + "m_Id": "63d004576abf5dd38e98f2999bc80c7d" + }, + { + "m_Id": "8474feff245c45cbbf6d350ea7c5b6d6" + } + ] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.HDTarget", + "m_ObjectId": "63d004576abf5dd38e98f2999bc80c7d", + "m_ActiveSubTarget": { + "m_Id": "223cb5dcc04250f88082d38f029d41bd" + }, + "m_Datas": [ + { + "m_Id": "29b58c7f26f65e47888b4747a0943e90" + }, + { + "m_Id": "9551d9a4301d56e0925d60044c340292" + }, + { + "m_Id": "95181d072e69577290beadc7698e1e5f" + }, + { + "m_Id": "82a37421177050b8b4f15561181b130e" + } + ], + "m_CustomEditorGUI": "VisualPinball.Engine.Unity.Hdrp.Editor.InsertHybridGUI", + "m_SupportVFX": false, + "m_SupportLineRendering": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.HDLitSubTarget", + "m_ObjectId": "223cb5dcc04250f88082d38f029d41bd" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.SystemData", + "m_ObjectId": "29b58c7f26f65e47888b4747a0943e90", + "m_MaterialNeedsUpdateHash": 0, + "m_SurfaceType": 0, + "m_RenderingPass": 1, + "m_BlendMode": 0, + "m_ZTest": 4, + "m_ZWrite": false, + "m_TransparentCullMode": 2, + "m_OpaqueCullMode": 2, + "m_SortPriority": 0, + "m_AlphaTest": false, + "m_ExcludeFromTUAndAA": false, + "m_TransparentDepthPrepass": false, + "m_TransparentDepthPostpass": false, + "m_SupportLodCrossFade": false, + "m_DoubleSidedMode": 0, + "m_DOTSInstancing": false, + "m_CustomVelocity": false, + "m_Tessellation": false, + "m_TessellationMode": 0, + "m_TessellationFactorMinDistance": 20.0, + "m_TessellationFactorMaxDistance": 50.0, + "m_TessellationFactorTriangleSize": 100.0, + "m_TessellationShapeFactor": 0.75, + "m_TessellationBackFaceCullEpsilon": -0.25, + "m_TessellationMaxDisplacement": 0.009999999776482582, + "m_DebugSymbols": false, + "m_Version": 2, + "inspectorFoldoutMask": 9 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.BuiltinData", + "m_ObjectId": "9551d9a4301d56e0925d60044c340292", + "m_Distortion": false, + "m_DistortionMode": 0, + "m_DistortionDepthTest": true, + "m_AddPrecomputedVelocity": false, + "m_TransparentWritesMotionVec": false, + "m_DepthOffset": false, + "m_ConservativeDepthOffset": false, + "m_TransparencyFog": true, + "m_AlphaTestShadow": false, + "m_BackThenFrontRendering": false, + "m_TransparentDepthPrepass": false, + "m_TransparentDepthPostpass": false, + "m_TransparentPerPixelSorting": false, + "m_SupportLodCrossFade": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.LightingData", + "m_ObjectId": "95181d072e69577290beadc7698e1e5f", + "m_NormalDropOffSpace": 0, + "m_BlendPreserveSpecular": true, + "m_ReceiveDecals": true, + "m_ReceiveSSR": true, + "m_ReceiveSSRTransparent": false, + "m_SpecularAA": true, + "m_SpecularOcclusionMode": 1, + "m_OverrideBakedGI": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.Rendering.HighDefinition.ShaderGraph.HDLitData", + "m_ObjectId": "82a37421177050b8b4f15561181b130e", + "m_RayTracing": false, + "m_MaterialType": 0, + "m_MaterialTypeMask": 2, + "m_RefractionModel": 0, + "m_SSSTransmission": true, + "m_EnergyConservingSpecular": true, + "m_ClearCoat": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "8b17db6110675180842cf69d50d4be33", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Position", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "c9ace2bb1e7654bab64d83894aad7a58" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Position" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PositionMaterialSlot", + "m_ObjectId": "c9ace2bb1e7654bab64d83894aad7a58", + "m_Id": 0, + "m_DisplayName": "Position", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Position", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "1416733beee65259aa05cc935db9583f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Normal", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "b97acd89ebd25297b33ae7ba020ab828" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Normal" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.NormalMaterialSlot", + "m_ObjectId": "b97acd89ebd25297b33ae7ba020ab828", + "m_Id": 0, + "m_DisplayName": "Normal", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Normal", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "50981c22d63c5fe9be71f306eab3aa8b", + "m_Group": { + "m_Id": "" + }, + "m_Name": "VertexDescription.Tangent", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 0.0, + "y": 0.0, + "width": 0.0, + "height": 0.0 + } + }, + "m_Slots": [ + { + "m_Id": "904e58126e345a33bafe9263d0c27e37" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "VertexDescription.Tangent" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.TangentMaterialSlot", + "m_ObjectId": "904e58126e345a33bafe9263d0c27e37", + "m_Id": 0, + "m_DisplayName": "Tangent", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Tangent", + "m_StageCapability": 1, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "f1c30112c2ac57528b85dd7db4a02b19", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.BaseColor", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -44.999969482421875, + "y": 155.49998474121094, + "width": 199.99998474121094, + "height": 41.00001525878906 + } + }, + "m_Slots": [ + { + "m_Id": "4a335b0d1d3853fd9166fb865d9b13f3" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.BaseColor" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.ColorRGBMaterialSlot", + "m_ObjectId": "4a335b0d1d3853fd9166fb865d9b13f3", + "m_Id": 0, + "m_DisplayName": "Base Color", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "BaseColor", + "m_StageCapability": 2, + "m_Value": { + "x": 0.5, + "y": 0.5, + "z": 0.5 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_ColorMode": 0, + "m_DefaultColor": { + "r": 0.5, + "g": 0.5, + "b": 0.5, + "a": 1.0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "af8e062f44565d54b5f377c2829f4bf3", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.NormalTS", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -46.499935150146484, + "y": 466.0, + "width": 199.99990844726562, + "height": 40.99993896484375 + } + }, + "m_Slots": [ + { + "m_Id": "e8aedce237ae5cb5a3ffd1b2adeab1aa" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.NormalTS" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.NormalMaterialSlot", + "m_ObjectId": "e8aedce237ae5cb5a3ffd1b2adeab1aa", + "m_Id": 0, + "m_DisplayName": "Normal (Tangent Space)", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "NormalTS", + "m_StageCapability": 2, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_Space": 3 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "67ea0d7b089d573492b036727e05ce99", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Metallic", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -43.499900817871094, + "y": 278.5000305175781, + "width": 199.99996948242188, + "height": 41.0 + } + }, + "m_Slots": [ + { + "m_Id": "67442a39fa4f5645b4b268d8a583d568" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Metallic" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "67442a39fa4f5645b4b268d8a583d568", + "m_Id": 0, + "m_DisplayName": "Metallic", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Metallic", + "m_StageCapability": 2, + "m_Value": 0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "90f29fe6214e5becb77e139ba2a1b2a6", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Smoothness", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -44.0000114440918, + "y": 317.4999694824219, + "width": 200.0000457763672, + "height": 41.0 + } + }, + "m_Slots": [ + { + "m_Id": "b0737480c53c51b29470d18b137fbfd3" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Smoothness" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "b0737480c53c51b29470d18b137fbfd3", + "m_Id": 0, + "m_DisplayName": "Smoothness", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Smoothness", + "m_StageCapability": 2, + "m_Value": 0.5, + "m_DefaultValue": 0.5, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "5d1129f3ab125ae794f880badfe3b76f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Occlusion", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -47.499977111816406, + "y": 280.5000305175781, + "width": 199.99996948242188, + "height": 41.0 + } + }, + "m_Slots": [ + { + "m_Id": "9de962b7e65e5b74b848794e12d3c95b" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Occlusion" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "9de962b7e65e5b74b848794e12d3c95b", + "m_Id": 0, + "m_DisplayName": "Ambient Occlusion", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Occlusion", + "m_StageCapability": 2, + "m_Value": 1, + "m_DefaultValue": 1.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BlockNode", + "m_ObjectId": "8b2ba8d4c746548e8f3a638475ef2a12", + "m_Group": { + "m_Id": "" + }, + "m_Name": "SurfaceDescription.Emission", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -44.500038146972656, + "y": 101.99987030029297, + "width": 200.00003051757812, + "height": 41.00000762939453 + } + }, + "m_Slots": [ + { + "m_Id": "dd66ff01568955279b360352bb9849ff" + } + ], + "synonyms": [], + "m_Precision": 0, + "m_PreviewExpanded": true, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SerializedDescriptor": "SurfaceDescription.Emission" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.ColorRGBMaterialSlot", + "m_ObjectId": "dd66ff01568955279b360352bb9849ff", + "m_Id": 0, + "m_DisplayName": "Emission", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Emission", + "m_StageCapability": 2, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [], + "m_ColorMode": 1, + "m_DefaultColor": { + "r": 0.0, + "g": 0.0, + "b": 0.0, + "a": 1.0 + } +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.CustomFunctionNode", + "m_ObjectId": "eb08b67e7dc45dba82c148f72e5dd340", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Insert Optics", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": 220, + "y": 120, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "349cc9eabecd599382ebbd378fd29254" + }, + { + "m_Id": "096ed94099fc537db0950acfee5c159c" + }, + { + "m_Id": "b55f348e6e355bcfb4066fc0d91e43d9" + }, + { + "m_Id": "f040780cb64955a181c7ffc5b38eecb2" + }, + { + "m_Id": "77573de2de1752d68777f9e0c2b26566" + }, + { + "m_Id": "c7236a0a71fe57949a649c4c9497ddff" + }, + { + "m_Id": "6e5d42d4ccce58e5beeacb9b93b18748" + }, + { + "m_Id": "15efee64218d5653af847cefc074d692" + }, + { + "m_Id": "4a5addb296ea5b01a6048162e5217e4d" + }, + { + "m_Id": "4023118b3fcf569fa105156112335b24" + }, + { + "m_Id": "4c64b263fb8754e78d4471667f5e8304" + }, + { + "m_Id": "6cefc178a91a59cab55582de0e4892ac" + }, + { + "m_Id": "16bd98decd925116bc5e4036a06393c3" + }, + { + "m_Id": "0e17e141e860535b81c1296572c12df5" + }, + { + "m_Id": "73ee8fe32066573592f1c3c442857811" + }, + { + "m_Id": "adcb191defa959dd97dea4af352f6026" + }, + { + "m_Id": "18be98d47193584787b1ce14d9eb7114" + }, + { + "m_Id": "1fd93a8c1dcb5c0b960ceca73e3f7f0c" + }, + { + "m_Id": "c3fb31d333dd5e808328175c4f8212c0" + }, + { + "m_Id": "47c0ca35524656509a569372529826b3" + }, + { + "m_Id": "e5dd52e5322e5a5d8ca79c158847b461" + }, + { + "m_Id": "f79854d76c145df2a10d15668a6ca8e0" + }, + { + "m_Id": "4cef7078b6ac55a2982dcc2f99a7e097" + }, + { + "m_Id": "dd0e89a840515532b74099f83591ec6b" + }, + { + "m_Id": "788e350cd48a522dac618260257c6c61" + }, + { + "m_Id": "83eb7fbb69125ceba13c99a182f87d93" + }, + { + "m_Id": "a349eff288675fc787ee780e1dd41801" + }, + { + "m_Id": "3fcc5202d8215989be23e4e51389c428" + }, + { + "m_Id": "b7c11fad70935da7864950676628f85d" + }, + { + "m_Id": "3ad72e845172537ea7d4fabb056d443e" + }, + { + "m_Id": "2e2e8e16613550f0b4f0d0dfd6831c4e" + }, + { + "m_Id": "933ea600871e594e850ab4134cb03c62" + }, + { + "m_Id": "b16effb8ba9952f59c7d35132d74ea6c" + }, + { + "m_Id": "b49d4801febf551c987ad69f01725288" + }, + { + "m_Id": "42b04fc04b4355658365cfbb1e55b879" + }, + { + "m_Id": "084993f5ee235d8daaf5c0270f42b15d" + }, + { + "m_Id": "06fbe54e2af85e9991a3834390f754fe" + }, + { + "m_Id": "accddb20717d5d878f3f50bed0b770c6" + }, + { + "m_Id": "9f2d58bb59665fb2bc695fc7cace5e75" + }, + { + "m_Id": "b14f1734cadd509898bde8cbc5e27073" + }, + { + "m_Id": "a1db37c3ce6f58708f531cf5de57ba59" + }, + { + "m_Id": "67b199d1b7765fd58deab824c473b2e9" + }, + { + "m_Id": "d6daaf0dadaa5c3c828d0fefc3a8702f" + }, + { + "m_Id": "fc726896899e55d4b9012d09d870eaff" + }, + { + "m_Id": "a98ee0b5e24d5158a27aec5d9b491f1c" + }, + { + "m_Id": "faf7421bd7e95ad19e3d01066c85ecd4" + }, + { + "m_Id": "c2396ef7b4ba5cd3956efdc91cdfaa9e" + }, + { + "m_Id": "c95f5cc10bd453cc9048168e06355c6e" + }, + { + "m_Id": "dc667998690b5cfca746930e9b4159de" + }, + { + "m_Id": "a0c74ffb1cec5401a4a0e5e8ae24db9a" + }, + { + "m_Id": "cb40e5e58af55278ac83f6a9b40e8b06" + }, + { + "m_Id": "68eca89dea7d51c5b76294121dfa66bb" + }, + { + "m_Id": "7d8273afe0e04afeb102e010845dea8b" + } + ], + "synonyms": [ + "code", + "HLSL" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_SourceType": 0, + "m_FunctionName": "InsertOptics", + "m_FunctionSource": "a8e1d31204aa95b40a3e523f0b2b266a", + "m_FunctionBody": "" +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "349cc9eabecd599382ebbd378fd29254", + "m_Id": 0, + "m_DisplayName": "Position", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Position", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "096ed94099fc537db0950acfee5c159c", + "m_Id": 1, + "m_DisplayName": "View", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "View", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector2MaterialSlot", + "m_ObjectId": "b55f348e6e355bcfb4066fc0d91e43d9", + "m_Id": 2, + "m_DisplayName": "UV", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "UV", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Texture2DInputMaterialSlot", + "m_ObjectId": "f040780cb64955a181c7ffc5b38eecb2", + "m_Id": 3, + "m_DisplayName": "OpticalNormal", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "OpticalNormal", + "m_StageCapability": 3, + "m_BareResource": false, + "m_Texture": { + "m_SerializedTexture": "", + "m_Guid": "" + }, + "m_DefaultType": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Texture2DInputMaterialSlot", + "m_ObjectId": "77573de2de1752d68777f9e0c2b26566", + "m_Id": 4, + "m_DisplayName": "Response", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Response", + "m_StageCapability": 3, + "m_BareResource": false, + "m_Texture": { + "m_SerializedTexture": "", + "m_Guid": "" + }, + "m_DefaultType": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Texture2DInputMaterialSlot", + "m_ObjectId": "c7236a0a71fe57949a649c4c9497ddff", + "m_Id": 5, + "m_DisplayName": "SurfaceNormal", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "SurfaceNormal", + "m_StageCapability": 3, + "m_BareResource": false, + "m_Texture": { + "m_SerializedTexture": "", + "m_Guid": "" + }, + "m_DefaultType": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "6e5d42d4ccce58e5beeacb9b93b18748", + "m_Id": 6, + "m_DisplayName": "Lamp", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Lamp", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "15efee64218d5653af847cefc074d692", + "m_Id": 7, + "m_DisplayName": "Ior", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Ior", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "4a5addb296ea5b01a6048162e5217e4d", + "m_Id": 8, + "m_DisplayName": "Scatter", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Scatter", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "4023118b3fcf569fa105156112335b24", + "m_Id": 9, + "m_DisplayName": "EdgeGlow", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "EdgeGlow", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "4c64b263fb8754e78d4471667f5e8304", + "m_Id": 10, + "m_DisplayName": "LampGain", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampGain", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "6cefc178a91a59cab55582de0e4892ac", + "m_Id": 11, + "m_DisplayName": "NormalStrength", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "NormalStrength", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "16bd98decd925116bc5e4036a06393c3", + "m_Id": 12, + "m_DisplayName": "Tint", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Tint", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "0e17e141e860535b81c1296572c12df5", + "m_Id": 13, + "m_DisplayName": "LampColor", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampColor", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "73ee8fe32066573592f1c3c442857811", + "m_Id": 14, + "m_DisplayName": "DriveColor", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "DriveColor", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "adcb191defa959dd97dea4af352f6026", + "m_Id": 15, + "m_DisplayName": "Level", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Level", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "18be98d47193584787b1ce14d9eb7114", + "m_Id": 16, + "m_DisplayName": "Starburst", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Starburst", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "1fd93a8c1dcb5c0b960ceca73e3f7f0c", + "m_Id": 17, + "m_DisplayName": "Contrast", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Contrast", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "c3fb31d333dd5e808328175c4f8212c0", + "m_Id": 18, + "m_DisplayName": "Variation", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Variation", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "47c0ca35524656509a569372529826b3", + "m_Id": 19, + "m_DisplayName": "Seed", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Seed", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "e5dd52e5322e5a5d8ca79c158847b461", + "m_Id": 20, + "m_DisplayName": "Brightness", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Brightness", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "f79854d76c145df2a10d15668a6ca8e0", + "m_Id": 21, + "m_DisplayName": "MottleSize", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "MottleSize", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "4cef7078b6ac55a2982dcc2f99a7e097", + "m_Id": 22, + "m_DisplayName": "Grain", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Grain", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "dd0e89a840515532b74099f83591ec6b", + "m_Id": 23, + "m_DisplayName": "MinBrightness", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "MinBrightness", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "788e350cd48a522dac618260257c6c61", + "m_Id": 24, + "m_DisplayName": "BaseColor", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "BaseColor", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "83eb7fbb69125ceba13c99a182f87d93", + "m_Id": 25, + "m_DisplayName": "NormalTS", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "NormalTS", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "a349eff288675fc787ee780e1dd41801", + "m_Id": 26, + "m_DisplayName": "Emission", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Emission", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "3fcc5202d8215989be23e4e51389c428", + "m_Id": 27, + "m_DisplayName": "WorldNormal", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "WorldNormal", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "b7c11fad70935da7864950676628f85d", + "m_Id": 28, + "m_DisplayName": "WorldTangent", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "WorldTangent", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "3ad72e845172537ea7d4fabb056d443e", + "m_Id": 29, + "m_DisplayName": "WorldBitangent", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "WorldBitangent", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "2e2e8e16613550f0b4f0d0dfd6831c4e", + "m_Id": 30, + "m_DisplayName": "Flatten", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Flatten", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "933ea600871e594e850ab4134cb03c62", + "m_Id": 31, + "m_DisplayName": "Clarity", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Clarity", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "b16effb8ba9952f59c7d35132d74ea6c", + "m_Id": 32, + "m_DisplayName": "CavityColor", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "CavityColor", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "b49d4801febf551c987ad69f01725288", + "m_Id": 33, + "m_DisplayName": "CavitySpread", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "CavitySpread", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "42b04fc04b4355658365cfbb1e55b879", + "m_Id": 34, + "m_DisplayName": "CavityContrast", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "CavityContrast", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "084993f5ee235d8daaf5c0270f42b15d", + "m_Id": 35, + "m_DisplayName": "LampTint", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampTint", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "06fbe54e2af85e9991a3834390f754fe", + "m_Id": 36, + "m_DisplayName": "Spread", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Spread", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "accddb20717d5d878f3f50bed0b770c6", + "m_Id": 37, + "m_DisplayName": "LampB", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampB", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "9f2d58bb59665fb2bc695fc7cace5e75", + "m_Id": 38, + "m_DisplayName": "LampC", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampC", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "b14f1734cadd509898bde8cbc5e27073", + "m_Id": 39, + "m_DisplayName": "LampD", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "LampD", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "a1db37c3ce6f58708f531cf5de57ba59", + "m_Id": 40, + "m_DisplayName": "DriveB", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "DriveB", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "67b199d1b7765fd58deab824c473b2e9", + "m_Id": 41, + "m_DisplayName": "DriveC", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "DriveC", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "d6daaf0dadaa5c3c828d0fefc3a8702f", + "m_Id": 42, + "m_DisplayName": "DriveD", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "DriveD", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "fc726896899e55d4b9012d09d870eaff", + "m_Id": 43, + "m_DisplayName": "SurfaceGrain", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "SurfaceGrain", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "a98ee0b5e24d5158a27aec5d9b491f1c", + "m_Id": 44, + "m_DisplayName": "SurfaceGrainSize", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "SurfaceGrainSize", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "faf7421bd7e95ad19e3d01066c85ecd4", + "m_Id": 45, + "m_DisplayName": "Output", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Output", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "c2396ef7b4ba5cd3956efdc91cdfaa9e", + "m_Id": 46, + "m_DisplayName": "Roughness", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Roughness", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "c95f5cc10bd453cc9048168e06355c6e", + "m_Id": 47, + "m_DisplayName": "Flecks", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "Flecks", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "dc667998690b5cfca746930e9b4159de", + "m_Id": 48, + "m_DisplayName": "FleckSize", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "FleckSize", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "a0c74ffb1cec5401a4a0e5e8ae24db9a", + "m_Id": 49, + "m_DisplayName": "RoomReturn", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "RoomReturn", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "cb40e5e58af55278ac83f6a9b40e8b06", + "m_Id": 50, + "m_DisplayName": "RoomBlur", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "RoomBlur", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "68eca89dea7d51c5b76294121dfa66bb", + "m_Id": 51, + "m_DisplayName": "RoomReturnEV", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "RoomReturnEV", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.PositionNode", + "m_ObjectId": "b57bfa9c48e8548695944203dd7264fa", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Position", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -100, + "y": 0, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "58635655d4eb5c8e9e085a4a0d57cfb6" + } + ], + "synonyms": [ + "location" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 0, + "m_PositionSource": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "58635655d4eb5c8e9e085a4a0d57cfb6", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.ViewDirectionNode", + "m_ObjectId": "77adde5a503255bc8730b099ed03c298", + "m_Group": { + "m_Id": "" + }, + "m_Name": "View Direction", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -100, + "y": 160, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "8f7b534471a85ac2adb98e42bec8e9d5" + } + ], + "synonyms": [ + "eye direction" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "8f7b534471a85ac2adb98e42bec8e9d5", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.UVNode", + "m_ObjectId": "00654fd1b821567b946f725c6879acc6", + "m_Group": { + "m_Id": "" + }, + "m_Name": "UV", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -100, + "y": 320, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "f6ab1af3f9955912bbe1a9084aa04c89" + } + ], + "synonyms": [ + "texcoords", + "coords", + "coordinates" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_OutputChannel": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "f6ab1af3f9955912bbe1a9084aa04c89", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.NormalVectorNode", + "m_ObjectId": "5774dd3dc28e51158e12894808bccd68", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Normal Vector", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -100, + "y": 4320, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "c0f1d72859b7571e9556ff4a690cfe3f" + } + ], + "synonyms": [ + "surface direction" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 2 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "c0f1d72859b7571e9556ff4a690cfe3f", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.TangentVectorNode", + "m_ObjectId": "181b1e1da978581b981f173bd21e7ed6", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Tangent Vector", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -100, + "y": 4480, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "55ed95ab62f55e63bb7e9b9de55d3a61" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 2 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "55ed95ab62f55e63bb7e9b9de55d3a61", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.BitangentVectorNode", + "m_ObjectId": "6d910a0a0fbc5419b374f6d76193b271", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Bitangent Vector", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -100, + "y": 4640, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "9bd6a301a28651d989efc3302ced0339" + } + ], + "synonyms": [ + "binormal" + ], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 2, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Space": 2 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector3MaterialSlot", + "m_ObjectId": "9bd6a301a28651d989efc3302ced0339", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Internal.Texture2DShaderProperty", + "m_ObjectId": "f5cccd49a27950b79db69a09be9d7a89", + "m_Guid": { + "m_GuidSerialized": "f5cccd49-a279-50b7-9db6-9a09be9d7a89" + }, + "m_Name": "Optical Normals (baked)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Optical Normals (baked)", + "m_DefaultReferenceName": "_OpticalNormalMap", + "m_OverrideReferenceName": "_OpticalNormalMap", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "m_SerializedTexture": "", + "m_Guid": "" + }, + "isMainTexture": false, + "useTilingAndOffset": false, + "useTexelSize": false, + "m_Modifiable": true, + "m_DefaultType": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "a7dea086444d58c09eb5c70fdac63f41", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 80, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "d245568ceaad54da932104d46e28971f" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "f5cccd49a27950b79db69a09be9d7a89" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Texture2DMaterialSlot", + "m_ObjectId": "d245568ceaad54da932104d46e28971f", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_BareResource": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Internal.Texture2DShaderProperty", + "m_ObjectId": "9c58618dfaf852e18f694a70f847b2cc", + "m_Guid": { + "m_GuidSerialized": "9c58618d-faf8-52e1-8f69-4a70f847b2cc" + }, + "m_Name": "Internal Response (baked)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Internal Response (baked)", + "m_DefaultReferenceName": "_OpticalResponseMap", + "m_OverrideReferenceName": "_OpticalResponseMap", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "m_SerializedTexture": "", + "m_Guid": "" + }, + "isMainTexture": false, + "useTilingAndOffset": false, + "useTexelSize": false, + "m_Modifiable": true, + "m_DefaultType": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "c74254a56fae556ab408262a4ee977ae", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 160, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "cc618f5753ce59e4a058d993d36d0cbd" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "9c58618dfaf852e18f694a70f847b2cc" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Texture2DMaterialSlot", + "m_ObjectId": "cc618f5753ce59e4a058d993d36d0cbd", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_BareResource": false +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Internal.Texture2DShaderProperty", + "m_ObjectId": "80a6c7d4fa675c1f97d62030c40e284b", + "m_Guid": { + "m_GuidSerialized": "80a6c7d4-fa67-5c1f-97d6-2030c40e284b" + }, + "m_Name": "Surface Normal (baked)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Surface Normal (baked)", + "m_DefaultReferenceName": "_NormalMap", + "m_OverrideReferenceName": "_NormalMap", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "m_SerializedTexture": "", + "m_Guid": "" + }, + "isMainTexture": false, + "useTilingAndOffset": false, + "useTexelSize": false, + "m_Modifiable": true, + "m_DefaultType": 3 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "8e3e2aaf2c8558d9937179b50257456f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 240, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "21a6c2332caf5e2ab09966a543798133" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "80a6c7d4fa675c1f97d62030c40e284b" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Texture2DMaterialSlot", + "m_ObjectId": "21a6c2332caf5e2ab09966a543798133", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_BareResource": false +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "cbaacef800f25aed9377b6affe122eb3", + "m_Guid": { + "m_GuidSerialized": "cbaacef8-00f2-5aed-9377-b6affe122eb3" + }, + "m_Name": "Virtual Lamp (local XYZ, radius W, metres)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Virtual Lamp (local XYZ, radius W, metres)", + "m_DefaultReferenceName": "_OpticalLamp", + "m_OverrideReferenceName": "_OpticalLamp", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "x": 0, + "y": -0.015, + "z": 0, + "w": 0.008 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "38a0027d064f5a39b3b966d8859ef5f9", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 320, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "c52d8d8ab335533eb4d41f60457f940d" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "cbaacef800f25aed9377b6affe122eb3" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "c52d8d8ab335533eb4d41f60457f940d", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "b32e0a9968715785b711d9c950e67e70", + "m_Guid": { + "m_GuidSerialized": "b32e0a99-6871-5785-b711-d9c950e67e70" + }, + "m_Name": "Virtual Lamp 2 (local XYZ, radius W, metres)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Virtual Lamp 2 (local XYZ, radius W, metres)", + "m_DefaultReferenceName": "_OpticalLampB", + "m_OverrideReferenceName": "_OpticalLampB", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "x": 0, + "y": 0, + "z": 0, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "b13762a60757541eb340dac8055f5f35", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 400, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "c76d733023185cfa8f25b145998baf66" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "b32e0a9968715785b711d9c950e67e70" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "c76d733023185cfa8f25b145998baf66", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "4f866e7a0d8653f1b37d5caa4190d4e5", + "m_Guid": { + "m_GuidSerialized": "4f866e7a-0d86-53f1-b37d-5caa4190d4e5" + }, + "m_Name": "Virtual Lamp 3 (local XYZ, radius W, metres)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Virtual Lamp 3 (local XYZ, radius W, metres)", + "m_DefaultReferenceName": "_OpticalLampC", + "m_OverrideReferenceName": "_OpticalLampC", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "x": 0, + "y": 0, + "z": 0, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "9254815eb565514fb64170a48b987901", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 480, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "862fe2e5b763553da4c9ed165c00fef7" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "4f866e7a0d8653f1b37d5caa4190d4e5" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "862fe2e5b763553da4c9ed165c00fef7", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "fb72f27f05ae538280d3a75b2cd757eb", + "m_Guid": { + "m_GuidSerialized": "fb72f27f-05ae-5382-80d3-a75b2cd757eb" + }, + "m_Name": "Virtual Lamp 4 (local XYZ, radius W, metres)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Virtual Lamp 4 (local XYZ, radius W, metres)", + "m_DefaultReferenceName": "_OpticalLampD", + "m_OverrideReferenceName": "_OpticalLampD", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "x": 0, + "y": 0, + "z": 0, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "93bfa3c99f135970a74b4e3ba8428d7d", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 560, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "5023b3f3ac4b55b6a907dfff83a7c3db" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "fb72f27f05ae538280d3a75b2cd757eb" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "5023b3f3ac4b55b6a907dfff83a7c3db", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "87bc8c4be8665adf8fb006ef67285c5d", + "m_Guid": { + "m_GuidSerialized": "87bc8c4b-e866-5adf-8fb0-06ef67285c5d" + }, + "m_Name": "Index of Refraction", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Index of Refraction", + "m_DefaultReferenceName": "_OpticalIor", + "m_OverrideReferenceName": "_OpticalIor", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 1.49, + "m_FloatType": 1, + "m_RangeValues": { + "x": 1.01, + "y": 2 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "8c40c59aad9650a5bbb772fbe4e38b96", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 640, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "2cfcc5ffeb725cc5aafd345272e0530d" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "87bc8c4be8665adf8fb006ef67285c5d" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "2cfcc5ffeb725cc5aafd345272e0530d", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "cebcb51b748d5b6889bc9956d0d479a3", + "m_Guid": { + "m_GuidSerialized": "cebcb51b-748d-5b68-89bc-9956d0d479a3" + }, + "m_Name": "Baked Scatter", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Baked Scatter", + "m_DefaultReferenceName": "_OpticalScatter", + "m_OverrideReferenceName": "_OpticalScatter", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.1, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 2 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "0c02ad2153675de98a7d0fe17c4d4c4e", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 720, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "98f8530c68955f2186f1b4eb9f3b3b06" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "cebcb51b748d5b6889bc9956d0d479a3" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "98f8530c68955f2186f1b4eb9f3b3b06", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "f2aa93755f9f5663be6045d6be0c61b9", + "m_Guid": { + "m_GuidSerialized": "f2aa9375-5f9f-5663-be60-45d6be0c61b9" + }, + "m_Name": "Edge Glow", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Edge Glow", + "m_DefaultReferenceName": "_OpticalEdgeGlow", + "m_OverrideReferenceName": "_OpticalEdgeGlow", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.035, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 3 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "6ddcfef30bf05ecdaf4b8b68419e8db6", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 800, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "e4fe6f49b4fe5c95a922bfed0585e734" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "f2aa93755f9f5663be6045d6be0c61b9" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "e4fe6f49b4fe5c95a922bfed0585e734", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "e28afaaa8fe053bd91f0e818b549fb77", + "m_Guid": { + "m_GuidSerialized": "e28afaaa-8fe0-53bd-91f0-e818b549fb77" + }, + "m_Name": "Virtual Lamp Strength", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Virtual Lamp Strength", + "m_DefaultReferenceName": "_OpticalLampGain", + "m_OverrideReferenceName": "_OpticalLampGain", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 1.5, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 5 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "20589895c6be54ea8a175ab190d10bf6", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 880, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "4844225f17825b9f93c82596114b2624" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "e28afaaa8fe053bd91f0e818b549fb77" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "4844225f17825b9f93c82596114b2624", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "c6bcc2966afc5ae488259f75fc7b836d", + "m_Guid": { + "m_GuidSerialized": "c6bcc296-6afc-5ae4-8825-9f75fc7b836d" + }, + "m_Name": "Surface Normal Strength", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Surface Normal Strength", + "m_DefaultReferenceName": "_NormalStrength", + "m_OverrideReferenceName": "_NormalStrength", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.15, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 2 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "bfa4def9778953faa67d6315312a64e9", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 960, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "7ef1a2078e745b4d9b3fd08aac01ebae" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "c6bcc2966afc5ae488259f75fc7b836d" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "7ef1a2078e745b4d9b3fd08aac01ebae", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "48504b8bc994589c80868e82996c56f2", + "m_Guid": { + "m_GuidSerialized": "48504b8b-c994-589c-8086-8e82996c56f2" + }, + "m_Name": "Intensity (runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Intensity (runtime)", + "m_DefaultReferenceName": "_InsertLampLevel", + "m_OverrideReferenceName": "_InsertLampLevel", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": 1, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "39c18e46011b582e81b386af722fb317", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1040, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "33bdeb73b1bc5f1fb4c33e77e09d8c1e" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "48504b8bc994589c80868e82996c56f2" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "33bdeb73b1bc5f1fb4c33e77e09d8c1e", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "b032f322f53d557897aa5f0dd7e22c40", + "m_Guid": { + "m_GuidSerialized": "b032f322-f53d-5578-97aa-5f0dd7e22c40" + }, + "m_Name": "Facet Contrast", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Facet Contrast", + "m_DefaultReferenceName": "_OpticalContrast", + "m_OverrideReferenceName": "_OpticalContrast", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.8, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "5a54035f9b50583e9369005068a9330c", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1120, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "df3daf7e9d135533ac32a50f358f8282" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "b032f322f53d557897aa5f0dd7e22c40" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "df3daf7e9d135533ac32a50f358f8282", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "5caa794e9c8856c8a296d48eaaa129e7", + "m_Guid": { + "m_GuidSerialized": "5caa794e-9c88-56c8-a296-d48eaaa129e7" + }, + "m_Name": "Mottle Amount", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Mottle Amount", + "m_DefaultReferenceName": "_OpticalVariation", + "m_OverrideReferenceName": "_OpticalVariation", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.4, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "54220b1a481651f38db03e60c90c40f8", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1200, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "308708b0ac74527fb220326ccc0661fa" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "5caa794e9c8856c8a296d48eaaa129e7" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "308708b0ac74527fb220326ccc0661fa", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "3a765b8511e35e8fb3bd93b9f474c3d6", + "m_Guid": { + "m_GuidSerialized": "3a765b85-11e3-5e8f-b3bd-93b9f474c3d6" + }, + "m_Name": "Variation Seed", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Variation Seed", + "m_DefaultReferenceName": "_OpticalSeed", + "m_OverrideReferenceName": "_OpticalSeed", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 7, + "m_FloatType": 0, + "m_RangeValues": { + "x": 1.0, + "y": 64.0 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "428c2f754584503890fda944c30ff28f", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1280, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "f44bd3629c5e5cec9c1761fca230a5c2" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "3a765b8511e35e8fb3bd93b9f474c3d6" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "f44bd3629c5e5cec9c1761fca230a5c2", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "79282d2d1f9f59beae1442cb5aec9d4a", + "m_Guid": { + "m_GuidSerialized": "79282d2d-1f9f-59be-ae14-42cb5aec9d4a" + }, + "m_Name": "Max Brightness", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Max Brightness", + "m_DefaultReferenceName": "_InsertLuminosity", + "m_OverrideReferenceName": "_InsertLuminosity", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 1, + "m_FloatType": 0, + "m_RangeValues": { + "x": 1.0, + "y": 64.0 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "d7c11bfbd1bd52608d3a654f476cb993", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1360, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "2bee6001693050ecbc2066679ec51425" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "79282d2d1f9f59beae1442cb5aec9d4a" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "2bee6001693050ecbc2066679ec51425", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "76b742d448275dac96c5a785ed5aa046", + "m_Guid": { + "m_GuidSerialized": "76b742d4-4827-5dac-96c5-a785ed5aa046" + }, + "m_Name": "Mottle Size (mm)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Mottle Size (mm)", + "m_DefaultReferenceName": "_OpticalMottleSize", + "m_OverrideReferenceName": "_OpticalMottleSize", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 1.4, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0.2, + "y": 5 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "c0d964a73077504cbc5b4b2d0f8408c1", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1440, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "22d158e3a3a6564a9326c5fe351982f8" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "76b742d448275dac96c5a785ed5aa046" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "22d158e3a3a6564a9326c5fe351982f8", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "c55299604c8f58ddb1951c3fccf48b0f", + "m_Guid": { + "m_GuidSerialized": "c5529960-4c8f-58dd-b195-1c3fccf48b0f" + }, + "m_Name": "Grain", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Grain", + "m_DefaultReferenceName": "_OpticalGrain", + "m_OverrideReferenceName": "_OpticalGrain", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.25, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "4b32d57f8adf5f11bbc862e34fa6d0c8", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1520, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "6149e17b147c5a4184d467bb5564de3e" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "c55299604c8f58ddb1951c3fccf48b0f" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "6149e17b147c5a4184d467bb5564de3e", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "79ce1dea7425576c927c76564474bdaa", + "m_Guid": { + "m_GuidSerialized": "79ce1dea-7425-576c-927c-76564474bdaa" + }, + "m_Name": "Min Brightness", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Min Brightness", + "m_DefaultReferenceName": "_InsertMinBrightness", + "m_OverrideReferenceName": "_InsertMinBrightness", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.04, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "756bb71064fa5a0c8be319a413b19390", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1600, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "8a45a8e2233c5b989cf76d70c9776590" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "79ce1dea7425576c927c76564474bdaa" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "8a45a8e2233c5b989cf76d70c9776590", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "8f2697a2ad035fd283e96674f6099e87", + "m_Guid": { + "m_GuidSerialized": "8f2697a2-ad03-5fd2-83e9-6674f6099e87" + }, + "m_Name": "Facet Flatten", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Facet Flatten", + "m_DefaultReferenceName": "_InsertFacetFlatten", + "m_OverrideReferenceName": "_InsertFacetFlatten", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "f841f17bc1fb5cf4a403c21690a12576", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1680, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "3b4d66be05cc5400b9c25c64610c0034" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "8f2697a2ad035fd283e96674f6099e87" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "3b4d66be05cc5400b9c25c64610c0034", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "47031cac033a52d290a3f7d58ebeb0e5", + "m_Guid": { + "m_GuidSerialized": "47031cac-033a-52d2-90a3-f7d58ebeb0e5" + }, + "m_Name": "Clarity", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Clarity", + "m_DefaultReferenceName": "_InsertClarity", + "m_OverrideReferenceName": "_InsertClarity", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "af3da35925e55b4682bf83cb054d2db9", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1760, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "46c3773c6bb45b699113239d1bd976aa" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "47031cac033a52d290a3f7d58ebeb0e5" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "46c3773c6bb45b699113239d1bd976aa", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "afd2ca8cba535099b1257c3f27290983", + "m_Guid": { + "m_GuidSerialized": "afd2ca8c-ba53-5099-b125-7c3f27290983" + }, + "m_Name": "Cavity Spread", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Cavity Spread", + "m_DefaultReferenceName": "_InsertCavitySpread", + "m_OverrideReferenceName": "_InsertCavitySpread", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 4, + "m_FloatType": 1, + "m_RangeValues": { + "x": 1, + "y": 8 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "7b2587fa2d6f5838bcc0940478c93467", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1840, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "b3d8ce720f20555f9208d3efbce3f615" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "afd2ca8cba535099b1257c3f27290983" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "b3d8ce720f20555f9208d3efbce3f615", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "d8f0b44d8ff45f8f9b7a97b64a7a5db0", + "m_Guid": { + "m_GuidSerialized": "d8f0b44d-8ff4-5f8f-9b7a-97b64a7a5db0" + }, + "m_Name": "Cavity Contrast", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Cavity Contrast", + "m_DefaultReferenceName": "_InsertCavityContrast", + "m_OverrideReferenceName": "_InsertCavityContrast", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.3, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "72c929a4c9295f2dbfba96ef32f2c945", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 1920, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "d1f410f050055e9ba1de2c6e1f45a2c6" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "d8f0b44d8ff45f8f9b7a97b64a7a5db0" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "d1f410f050055e9ba1de2c6e1f45a2c6", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "4c5ab3de0c985513bf7247b33879f300", + "m_Guid": { + "m_GuidSerialized": "4c5ab3de-0c98-5513-bf72-47b33879f300" + }, + "m_Name": "Lamp Tint (runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp Tint (runtime)", + "m_DefaultReferenceName": "_InsertLampTint", + "m_OverrideReferenceName": "_InsertLampTint", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "648cdf0c4ccd56dab79e0839510f9850", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2000, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "040777c80eb2592294dc0664aeb99af1" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "4c5ab3de0c985513bf7247b33879f300" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "040777c80eb2592294dc0664aeb99af1", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "7550e938d31e5c399795602e29d14c3a", + "m_Guid": { + "m_GuidSerialized": "7550e938-d31e-5c39-9795-602e29d14c3a" + }, + "m_Name": "Lamp Spread (mm)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp Spread (mm)", + "m_DefaultReferenceName": "_OpticalSpread", + "m_OverrideReferenceName": "_OpticalSpread", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 100 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "432b49b887d35d83bf88a081723b160e", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2080, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "ea2cc2cbc7d052d6973f5c750abb8b26" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "7550e938d31e5c399795602e29d14c3a" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "ea2cc2cbc7d052d6973f5c750abb8b26", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "391f5b4513d65194a4ba42d431bd8c93", + "m_Guid": { + "m_GuidSerialized": "391f5b45-13d6-5194-a4ba-42d431bd8c93" + }, + "m_Name": "Surface Grain", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Surface Grain", + "m_DefaultReferenceName": "_SurfaceGrain", + "m_OverrideReferenceName": "_SurfaceGrain", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "7a917aa723ee5ee4a58985dcfa8fb709", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2160, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "dd1c976db49c5fa69e965e6fae64ba96" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "391f5b4513d65194a4ba42d431bd8c93" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "dd1c976db49c5fa69e965e6fae64ba96", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "eddfdb9e051d5865834eb933671aaff4", + "m_Guid": { + "m_GuidSerialized": "eddfdb9e-051d-5865-834e-b933671aaff4" + }, + "m_Name": "Surface Grain Size (mm)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Surface Grain Size (mm)", + "m_DefaultReferenceName": "_SurfaceGrainSize", + "m_OverrideReferenceName": "_SurfaceGrainSize", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.75, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0.1, + "y": 5 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "eb3e21d0b35e5384817d0c6b2ecba57a", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2240, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "c2d25bb1638b5ffab3d45e2a88d67b54" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "eddfdb9e051d5865834eb933671aaff4" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "c2d25bb1638b5ffab3d45e2a88d67b54", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "d1d92de60b5b50679d958d5415cb1bce", + "m_Guid": { + "m_GuidSerialized": "d1d92de6-0b5b-5067-9d95-8d5415cb1bce" + }, + "m_Name": "Lamp Output (runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp Output (runtime)", + "m_DefaultReferenceName": "_InsertLampOutput", + "m_OverrideReferenceName": "_InsertLampOutput", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": 1, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 10 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "312a376a2c9d5949a63495eb43da788a", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2320, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "14e794fdbbaf535fb1098c9909fc5eec" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "d1d92de60b5b50679d958d5415cb1bce" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "14e794fdbbaf535fb1098c9909fc5eec", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "7d4e3081cedd5c919f3bc85431603a72", + "m_Guid": { + "m_GuidSerialized": "7d4e3081-cedd-5c91-9f3b-c85431603a72" + }, + "m_Name": "Roughness", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Roughness", + "m_DefaultReferenceName": "_InsertRoughness", + "m_OverrideReferenceName": "_InsertRoughness", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "1686d3f1b0615565aa5a86324933c761", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2400, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "f09cb08aa32754268eee7015e35d765a" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "7d4e3081cedd5c919f3bc85431603a72" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "f09cb08aa32754268eee7015e35d765a", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "d6e78c4734055e67b1986e96afc59726", + "m_Guid": { + "m_GuidSerialized": "d6e78c47-3405-5e67-b198-6e96afc59726" + }, + "m_Name": "Flecks", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Flecks", + "m_DefaultReferenceName": "_OpticalFlecks", + "m_OverrideReferenceName": "_OpticalFlecks", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "d8f56c5855f75c5a9f96ef67e55b4205", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2480, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "109de4825faa5012b398cef020fe383a" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "d6e78c4734055e67b1986e96afc59726" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "109de4825faa5012b398cef020fe383a", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "975f88b1cbd05c3690f7e57ac7b2eb89", + "m_Guid": { + "m_GuidSerialized": "975f88b1-cbd0-5c36-90f7-e57ac7b2eb89" + }, + "m_Name": "Fleck Size (mm)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Fleck Size (mm)", + "m_DefaultReferenceName": "_OpticalFleckSize", + "m_OverrideReferenceName": "_OpticalFleckSize", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 1, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0.1, + "y": 5 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "89a60d00f1fd5348973c297d5401ec5a", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2560, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "ec39e58529e057f599adde491f16c101" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "975f88b1cbd05c3690f7e57ac7b2eb89" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "ec39e58529e057f599adde491f16c101", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "ab903b8243655d0ea0911669c7c3432b", + "m_Guid": { + "m_GuidSerialized": "ab903b82-4365-5d0e-a091-1669c7c3432b" + }, + "m_Name": "Room Return", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Room Return", + "m_DefaultReferenceName": "_InsertRoomReturn", + "m_OverrideReferenceName": "_InsertRoomReturn", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "1704bd811b1c5a35bedfb8dda5462598", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2640, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "a6fdee34f83d555e9b7e626f540f9f5e" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "ab903b8243655d0ea0911669c7c3432b" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "a6fdee34f83d555e9b7e626f540f9f5e", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "d63fb2c8f30356c3aae3606059477dcd", + "m_Guid": { + "m_GuidSerialized": "d63fb2c8-f303-56c3-aae3-606059477dcd" + }, + "m_Name": "Room Blur", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Room Blur", + "m_DefaultReferenceName": "_InsertRoomBlur", + "m_OverrideReferenceName": "_InsertRoomBlur", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 3, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 6 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "bfcc339d0cb456c987457755aabfb04d", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2720, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "3f6966c2363e5d669c866481c1253bd8" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "d63fb2c8f30356c3aae3606059477dcd" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "3f6966c2363e5d669c866481c1253bd8", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "1e80a80e3298577f8a09c329c878fc93", + "m_Guid": { + "m_GuidSerialized": "1e80a80e-3298-577f-8a09-c329c878fc93" + }, + "m_Name": "Room Return Exposure (EV)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Room Return Exposure (EV)", + "m_DefaultReferenceName": "_InsertRoomReturnEV", + "m_OverrideReferenceName": "_InsertRoomReturnEV", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0, + "m_FloatType": 1, + "m_RangeValues": { + "x": -4, + "y": 6 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "233c9907b34555cd80459f4a0921d1c3", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2800, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "995c1ca53b545700b13ded64ef5c855e" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "1e80a80e3298577f8a09c329c878fc93" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "995c1ca53b545700b13ded64ef5c855e", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.Internal.ColorShaderProperty", + "m_ObjectId": "0868c71889275f43b8ad9867b67f2b93", + "m_Guid": { + "m_GuidSerialized": "0868c718-8927-5f43-b8ad-9867b67f2b93" + }, + "m_Name": "Plastic Tint (off)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Plastic Tint (off)", + "m_DefaultReferenceName": "_BaseColor", + "m_OverrideReferenceName": "_BaseColor", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "r": 0.16, + "g": 0.13, + "b": 0.25, + "a": 1 + }, + "isMainColor": false, + "m_ColorMode": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "65db75acf0cc5d4f9d00d1246dc2acff", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2880, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "af466680550f52cc991288443a538a00" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "0868c71889275f43b8ad9867b67f2b93" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "af466680550f52cc991288443a538a00", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.Internal.ColorShaderProperty", + "m_ObjectId": "7745a741a46459bc84e174ad710431f3", + "m_Guid": { + "m_GuidSerialized": "7745a741-a464-59bc-84e1-74ad710431f3" + }, + "m_Name": "Cavity Color (clear, under the insert)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Cavity Color (clear, under the insert)", + "m_DefaultReferenceName": "_InsertCavityColor", + "m_OverrideReferenceName": "_InsertCavityColor", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "r": 0.9, + "g": 0.9, + "b": 0.9, + "a": 1 + }, + "isMainColor": false, + "m_ColorMode": 0 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "9a23f1d133965f8c8ca9b90a7633c2a2", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2960, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "5dab705379695bae85632b8e8485ad00" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "7745a741a46459bc84e174ad710431f3" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "5dab705379695bae85632b8e8485ad00", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 3, + "m_Type": "UnityEditor.ShaderGraph.Internal.ColorShaderProperty", + "m_ObjectId": "b1118c1270965d32a84bc58296cea0d5", + "m_Guid": { + "m_GuidSerialized": "b1118c12-7096-5d32-a84b-c58296cea0d5" + }, + "m_Name": "Lamp Emission (HDR)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp Emission (HDR)", + "m_DefaultReferenceName": "_EmissiveColor", + "m_OverrideReferenceName": "_EmissiveColor", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": { + "r": 600, + "g": 600, + "b": 600, + "a": 1 + }, + "isMainColor": false, + "m_ColorMode": 1 +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "f46c2964302e54489e6557a72029b007", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 3040, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "4d36a3a292d450a4aa97f1ffe7e532dc" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "b1118c1270965d32a84bc58296cea0d5" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "4d36a3a292d450a4aa97f1ffe7e532dc", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "44276acd6332572585d8c5ea51c2b692", + "m_Guid": { + "m_GuidSerialized": "44276acd-6332-5725-85d8-c5ea51c2b692" + }, + "m_Name": "Lamp RGB (linear, runtime)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp RGB (linear, runtime)", + "m_DefaultReferenceName": "_InsertLampColor", + "m_OverrideReferenceName": "_InsertLampColor", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 1, + "y": 1, + "z": 1, + "w": 1 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "925bd03a2d6855e098beab765a34e9e5", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 3120, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "c0788727c130571c88cb80869663934b" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "44276acd6332572585d8c5ea51c2b692" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "c0788727c130571c88cb80869663934b", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "ff36167b8ae75487bb5eccb8dd152070", + "m_Guid": { + "m_GuidSerialized": "ff36167b-8ae7-5487-bb5e-ccb8dd152070" + }, + "m_Name": "Starburst (baked)", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Starburst (baked)", + "m_DefaultReferenceName": "_OpticalStarburst", + "m_OverrideReferenceName": "_OpticalStarburst", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 0, + "y": 0, + "z": 0, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "ca9058a29ffb5a5eaa1c0d8a29ef029e", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 3200, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "b24f9bbdbdc356b09a92e6f2bc44eeda" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "ff36167b8ae75487bb5eccb8dd152070" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "b24f9bbdbdc356b09a92e6f2bc44eeda", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "e188a4e6b18b54d89908031069759967", + "m_Guid": { + "m_GuidSerialized": "e188a4e6-b18b-54d8-9908-031069759967" + }, + "m_Name": "Lamp RGB and level (runtime), bulb B", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp RGB and level (runtime), bulb B", + "m_DefaultReferenceName": "_InsertLampColorB", + "m_OverrideReferenceName": "_InsertLampColorB", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 1, + "y": 1, + "z": 1, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "ec2fa20232095a95b7abe4dfc9a905ec", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 3280, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "12133ebf88e751c5b496c1d21169298d" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "e188a4e6b18b54d89908031069759967" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "12133ebf88e751c5b496c1d21169298d", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "5180ea7940775616b68bd2de94b6fd74", + "m_Guid": { + "m_GuidSerialized": "5180ea79-4077-5616-b68b-d2de94b6fd74" + }, + "m_Name": "Lamp RGB and level (runtime), bulb C", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp RGB and level (runtime), bulb C", + "m_DefaultReferenceName": "_InsertLampColorC", + "m_OverrideReferenceName": "_InsertLampColorC", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 1, + "y": 1, + "z": 1, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "b55bcb0aba7950528b1d1b566bd97877", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 3360, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "d71c74226c995447871b8a93808b6e76" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "5180ea7940775616b68bd2de94b6fd74" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "d71c74226c995447871b8a93808b6e76", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector4ShaderProperty", + "m_ObjectId": "a144dcb8a49459c19a2080583d6a71d3", + "m_Guid": { + "m_GuidSerialized": "a144dcb8-a494-59c1-9a20-80583d6a71d3" + }, + "m_Name": "Lamp RGB and level (runtime), bulb D", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Lamp RGB and level (runtime), bulb D", + "m_DefaultReferenceName": "_InsertLampColorD", + "m_OverrideReferenceName": "_InsertLampColorD", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": { + "x": 1, + "y": 1, + "z": 1, + "w": 0 + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "0229938f88d85671b777b9408b22ed4e", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 3440, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "c949863c7e955b9a88f457fcca2a1d40" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "a144dcb8a49459c19a2080583d6a71d3" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector4MaterialSlot", + "m_ObjectId": "c949863c7e955b9a88f457fcca2a1d40", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_DefaultValue": { + "x": 0.0, + "y": 0.0, + "z": 0.0, + "w": 0.0 + }, + "m_Labels": [] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "b4462b630ce1577fbdb18f40505fc0b7", + "m_Guid": { + "m_GuidSerialized": "b4462b63-0ce1-577f-bdb1-8f40505fc0b7" + }, + "m_Name": "Smoothness", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "Smoothness", + "m_DefaultReferenceName": "_Smoothness", + "m_OverrideReferenceName": "_Smoothness", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": false, + "m_Value": 0.93, + "m_FloatType": 1, + "m_RangeValues": { + "x": 0, + "y": 1 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "8f121541bc7a5534828e4315a9d06bd6", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 3520, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "c6c0963ecc8e5cc19c427af32c84b57d" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "b4462b630ce1577fbdb18f40505fc0b7" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "c6c0963ecc8e5cc19c427af32c84b57d", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.0, + "m_DefaultValue": 0.0, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.CategoryData", + "m_ObjectId": "efcb6c45f55553f89edd4456c2a91dad", + "m_Name": "", + "m_ChildObjectList": [ + { + "m_Id": "f5cccd49a27950b79db69a09be9d7a89" + }, + { + "m_Id": "9c58618dfaf852e18f694a70f847b2cc" + }, + { + "m_Id": "80a6c7d4fa675c1f97d62030c40e284b" + }, + { + "m_Id": "cbaacef800f25aed9377b6affe122eb3" + }, + { + "m_Id": "b32e0a9968715785b711d9c950e67e70" + }, + { + "m_Id": "4f866e7a0d8653f1b37d5caa4190d4e5" + }, + { + "m_Id": "fb72f27f05ae538280d3a75b2cd757eb" + }, + { + "m_Id": "87bc8c4be8665adf8fb006ef67285c5d" + }, + { + "m_Id": "cebcb51b748d5b6889bc9956d0d479a3" + }, + { + "m_Id": "f2aa93755f9f5663be6045d6be0c61b9" + }, + { + "m_Id": "e28afaaa8fe053bd91f0e818b549fb77" + }, + { + "m_Id": "c6bcc2966afc5ae488259f75fc7b836d" + }, + { + "m_Id": "48504b8bc994589c80868e82996c56f2" + }, + { + "m_Id": "b032f322f53d557897aa5f0dd7e22c40" + }, + { + "m_Id": "5caa794e9c8856c8a296d48eaaa129e7" + }, + { + "m_Id": "3a765b8511e35e8fb3bd93b9f474c3d6" + }, + { + "m_Id": "79282d2d1f9f59beae1442cb5aec9d4a" + }, + { + "m_Id": "76b742d448275dac96c5a785ed5aa046" + }, + { + "m_Id": "c55299604c8f58ddb1951c3fccf48b0f" + }, + { + "m_Id": "79ce1dea7425576c927c76564474bdaa" + }, + { + "m_Id": "8f2697a2ad035fd283e96674f6099e87" + }, + { + "m_Id": "47031cac033a52d290a3f7d58ebeb0e5" + }, + { + "m_Id": "afd2ca8cba535099b1257c3f27290983" + }, + { + "m_Id": "d8f0b44d8ff45f8f9b7a97b64a7a5db0" + }, + { + "m_Id": "4c5ab3de0c985513bf7247b33879f300" + }, + { + "m_Id": "7550e938d31e5c399795602e29d14c3a" + }, + { + "m_Id": "391f5b4513d65194a4ba42d431bd8c93" + }, + { + "m_Id": "eddfdb9e051d5865834eb933671aaff4" + }, + { + "m_Id": "d1d92de60b5b50679d958d5415cb1bce" + }, + { + "m_Id": "7d4e3081cedd5c919f3bc85431603a72" + }, + { + "m_Id": "d6e78c4734055e67b1986e96afc59726" + }, + { + "m_Id": "975f88b1cbd05c3690f7e57ac7b2eb89" + }, + { + "m_Id": "ab903b8243655d0ea0911669c7c3432b" + }, + { + "m_Id": "d63fb2c8f30356c3aae3606059477dcd" + }, + { + "m_Id": "1e80a80e3298577f8a09c329c878fc93" + }, + { + "m_Id": "0868c71889275f43b8ad9867b67f2b93" + }, + { + "m_Id": "7745a741a46459bc84e174ad710431f3" + }, + { + "m_Id": "b1118c1270965d32a84bc58296cea0d5" + }, + { + "m_Id": "44276acd6332572585d8c5ea51c2b692" + }, + { + "m_Id": "ff36167b8ae75487bb5eccb8dd152070" + }, + { + "m_Id": "e188a4e6b18b54d89908031069759967" + }, + { + "m_Id": "5180ea7940775616b68bd2de94b6fd74" + }, + { + "m_Id": "a144dcb8a49459c19a2080583d6a71d3" + }, + { + "m_Id": "b4462b630ce1577fbdb18f40505fc0b7" + } + ] +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalTarget", + "m_ObjectId": "8474feff245c45cbbf6d350ea7c5b6d6", + "m_Datas": [], + "m_ActiveSubTarget": { + "m_Id": "4e2c46213dcd4e63bb84020b96385c6e" + }, + "m_AllowMaterialOverride": false, + "m_SurfaceType": 0, + "m_ZTestMode": 4, + "m_ZWriteControl": 0, + "m_AlphaMode": 0, + "m_RenderFace": 2, + "m_AlphaClip": false, + "m_CastShadows": true, + "m_ReceiveShadows": true, + "m_CustomEditorGUI": "VisualPinball.Unity.Urp.InsertShaderGUI", + "m_SupportVFX": false +} + +{ + "m_SGVersion": 2, + "m_Type": "UnityEditor.Rendering.Universal.ShaderGraph.UniversalLitSubTarget", + "m_ObjectId": "4e2c46213dcd4e63bb84020b96385c6e", + "m_WorkflowMode": 1, + "m_NormalDropOffSpace": 0, + "m_ClearCoat": false, + "m_BlendModePreserveSpecular": true +} + +{ + "m_SGVersion": 1, + "m_Type": "UnityEditor.ShaderGraph.Internal.Vector1ShaderProperty", + "m_ObjectId": "7b66c53699fb4e329b7b4ff523715868", + "m_Guid": { + "m_GuidSerialized": "7b66c536-99fb-4e32-9b7b-4ff523715868" + }, + "m_Name": "URP Emission Scale", + "m_DefaultRefNameVersion": 1, + "m_RefNameGeneratedByDisplayName": "URP Emission Scale", + "m_DefaultReferenceName": "_InsertUrpEmissionScale", + "m_OverrideReferenceName": "_InsertUrpEmissionScale", + "m_GeneratePropertyBlock": true, + "m_UseCustomSlotLabel": false, + "m_CustomSlotLabel": "", + "m_DismissedVersion": 0, + "m_Precision": 0, + "overrideHLSLDeclaration": false, + "hlslDeclarationOverride": 0, + "m_Hidden": true, + "m_Value": 0.013020833333333334, + "m_FloatType": 0, + "m_RangeValues": { + "x": 0, + "y": 6 + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [], + "m_EnumValues": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.PropertyNode", + "m_ObjectId": "fb36717fa96f4d248390595746a431e8", + "m_Group": { + "m_Id": "" + }, + "m_Name": "Property", + "m_DrawState": { + "m_Expanded": true, + "m_Position": { + "serializedVersion": "2", + "x": -500, + "y": 2720, + "width": 210, + "height": 100 + } + }, + "m_Slots": [ + { + "m_Id": "0422f3ca27d9472dbb6f97bd42fbd5f1" + } + ], + "synonyms": [], + "m_Precision": 1, + "m_PreviewExpanded": false, + "m_DismissedVersion": 0, + "m_PreviewMode": 0, + "m_CustomColors": { + "m_SerializableColors": [] + }, + "m_Property": { + "m_Id": "7b66c53699fb4e329b7b4ff523715868" + } +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "0422f3ca27d9472dbb6f97bd42fbd5f1", + "m_Id": 0, + "m_DisplayName": "Out", + "m_SlotType": 1, + "m_Hidden": false, + "m_ShaderOutputName": "Out", + "m_StageCapability": 3, + "m_Value": 0.013020833333333334, + "m_DefaultValue": 0.013020833333333334, + "m_Labels": [] +} + +{ + "m_SGVersion": 0, + "m_Type": "UnityEditor.ShaderGraph.Vector1MaterialSlot", + "m_ObjectId": "7d8273afe0e04afeb102e010845dea8b", + "m_Id": 52, + "m_DisplayName": "UrpEmissionScale", + "m_SlotType": 0, + "m_Hidden": false, + "m_ShaderOutputName": "UrpEmissionScale", + "m_StageCapability": 3, + "m_Value": 0.013020833333333334, + "m_DefaultValue": 0.013020833333333334, + "m_Labels": [] +} diff --git a/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Hybrid.shadergraph.meta b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Hybrid.shadergraph.meta new file mode 100644 index 000000000..24b48934b --- /dev/null +++ b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/Insert Hybrid.shadergraph.meta @@ -0,0 +1,20 @@ +fileFormatVersion: 2 +guid: c5fc2955b016708429b0cb03d5011b07 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3} + useAsTemplate: 0 + exposeTemplateAsShader: 0 + indexedData: {instanceID: 0} + template: + name: + category: + description: + icon: {instanceID: 0} + thumbnail: {instanceID: 0} + order: 0 diff --git a/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertCover.hlsl b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertCover.hlsl new file mode 100644 index 000000000..3a31806bd --- /dev/null +++ b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertCover.hlsl @@ -0,0 +1,195 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef VPE_INSERT_COVER_INCLUDED +#define VPE_INSERT_COVER_INCLUDED + +#if defined(UNIVERSAL_PIPELINE_CORE_INCLUDED) && !defined(SHADERGRAPH_PREVIEW) +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareOpaqueTexture.hlsl" +#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" +float _VpeInsertSceneColorAvailable; +#endif + +// Cover UVs and their tangent frame follow the referenced playfield's XZ +// projection. Its texture and source strength are supplied per material slot. +// The haze is the colour the cover blends in at its opacity: a light veil that +// softens the ribs underneath the way a worn, slightly frosted top does. +// +// The cover is the top of a slab of plastic as thick as the insert is deep, +// and what shows through it is refracted as through that slab: the view ray +// bends at the top by the index, the floor with its ribs is seen where the +// bent ray meets it, a third nearer than it lies, and the light on its way up +// takes the plastic's tint, deeper the longer its path, so a red insert is +// red for being red plastic rather than a red-lit tray, darkest towards its +// edges and at grazing views. Without the slab the body sat at the bottom of +// an open tray under a pane of glass, and looked hollow. +// Hue, saturation and value, hue as a turn of the wheel in 0..1. +float3 InsertCoverRgbToHsv(float3 c) +{ + float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + float4 p = lerp(float4(c.bg, K.wz), float4(c.gb, K.xy), step(c.b, c.g)); + float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.r)); + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +float3 InsertCoverHsvToRgb(float3 c) +{ + float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y); +} + +// Turns the colour's hue towards the lamp's, and its saturation towards the +// lamp's, never below its own, both by the amount; the value is never touched, so a grey haze comes out the lamp's +// colour, as bright as the grey was, and never darker. An RGB lamp carries +// its intensity in its colour, and a dim lamp is still its colour: the amount +// is full from a tenth of full brightness up, and only a lamp that is off, +// its channels all down, leaves the colour untouched. A white lamp has no hue +// to give. +float3 InsertCoverTowardsLampHue(float3 color, float3 lamp, float amount) +{ + float3 hsv = InsertCoverRgbToHsv(max(color, 0.0)); + float3 lampHsv = InsertCoverRgbToHsv(max(lamp, 0.0)); + amount = saturate(amount) * saturate(lampHsv.z * 10.0); + // The hue turns the short way round the wheel, by the lamp's share of the + // saturation the mix ends up with: a grey, which has no hue of its own, + // takes the lamp's hue at any amount, its saturation growing with it, + // instead of a hue part of the way round the wheel, so a grey haze half + // tinted by a blue lamp is a paler blue, never pink; two colours as + // saturated as each other turn by the amount itself. + float turn = frac(lampHsv.x - hsv.x + 0.5) - 0.5; + float share = saturate(amount * lampHsv.y / max(lerp(hsv.y, lampHsv.y, amount), 1e-5)); + hsv.x = frac(hsv.x + turn * share); + hsv.y = lerp(hsv.y, max(hsv.y, lampHsv.y), amount); + return InsertCoverHsvToRgb(hsv); +} + +// The colour the bulbs send to a point, the same the body's glow takes: every +// bulb's light summed, each reaching by the inverse square of the distance +// over its spread, bulbs that are off adding nothing and taking nothing away, +// so a bulb fading in colours every point it reaches in proportion to its +// level; and in w how far that colour reaches, a bulb counting as on from a +// tenth of full up, so a bulb fading in tints its whole reach at once instead +// of creeping outwards with its light. Each lamp is a position in insert +// space with its radius in w, off at zero; each drive a linear colour with +// the level in w, the first split in two. +float4 InsertCoverLampMix(float2 position, float4 lampA, float4 lampB, float4 lampC, float4 lampD, + float3 colorA, float levelA, float4 driveB, float4 driveC, float4 driveD, float spread) +{ + float4 lamps[4] = { lampA, lampB, lampC, lampD }; + float3 drives[4] = { max(colorA, 0.0) * saturate(levelA), max(driveB.rgb, 0.0) * saturate(driveB.w), + max(driveC.rgb, 0.0) * saturate(driveC.w), max(driveD.rgb, 0.0) * saturate(driveD.w) }; + float3 mixed = 0.0; + float weight = 0.0; + float tinted = 0.0; + [unroll] + for (int i = 0; i < 4; i++) { + if (i > 0 && lamps[i].w <= 0.0) continue; + float radius = max(abs(lamps[i].w), 0.0001); + float2 apart = position - lamps[i].xz; + // The body weighs each bulb by its lamp spread as well: the same here, + // or the two would tint a point differently. + float leak = 1.0; + if (spread > 0.0) { + float2 away = apart / (spread * 0.001); + leak = rcp(1.0 + dot(away, away)); + } + float level = max(drives[i].r, max(drives[i].g, drives[i].b)); + mixed += leak * drives[i]; + weight += leak * level; + tinted += saturate(leak * 10.0) * saturate(level * 10.0); + } + return float4(mixed / max(weight, 1e-6), tinted); +} + +void InsertCover_float(float2 UV, UnityTexture2D NormalMap, float PlayfieldStrength, + float Strength, float Smoothness, float Opacity, float3 HazeColor, + float3 LampColor, float LampLevel, float LampTint, + float3 Position, float4 LampA, float4 LampB, float4 LampC, float4 LampD, + float4 DriveB, float4 DriveC, float4 DriveD, float Spread, + float Ior, float3 Tint, float TintDepth, float Thickness, float3 TangentOS, float3 BitangentOS, + out float3 NormalTS, out float SurfaceSmoothness, out float Alpha, out float3 BaseColor, + out float RefractionIndex, out float3 TransmittanceColor, out float AbsorptionDistance, out float SlabThickness, + out float3 Emission) +{ +#if defined(SHADERPASS) && ((defined(SHADERPASS_SHADOWS) && (SHADERPASS == SHADERPASS_SHADOWS)) || (defined(SHADERPASS_SHADOWCASTER) && (SHADERPASS == SHADERPASS_SHADOWCASTER))) + clip(-1); +#endif + Emission = 0.0; + float3 normal = UnpackNormal(SAMPLE_TEXTURE2D(NormalMap.tex, NormalMap.samplerstate, UV * NormalMap.scaleTranslate.xy + NormalMap.scaleTranslate.zw)); + NormalTS = normalize(float3(normal.xy * max(PlayfieldStrength * Strength, 0.0), normal.z)); + SurfaceSmoothness = saturate(Smoothness); + Alpha = saturate(Opacity); + // The haze's hue and saturation turn to the light's, the bulbs' colours + // summed, as far as the insert light asks and their light reaches, its + // value untouched, so a coloured lamp is not milked by a grey veil, a + // banana lit red, green and blue along its length is hazed red, green and + // blue with the mixes between, and lamps that are off leave the haze its + // own colour, as bright as ever. + float4 mixed = InsertCoverLampMix(Position.xz, LampA, LampB, LampC, LampD, LampColor, LampLevel, DriveB, DriveC, DriveD, Spread); + float3 untinted = max(HazeColor, 0.0); + BaseColor = InsertCoverTowardsLampHue(untinted, mixed.rgb, saturate(LampTint) * saturate(mixed.w)); + // Turning a grey haze to a colour keeps its value, not its brightness: a + // red as light as the grey was has a fifth of its luminance, so a veil + // over the body went darker as the lamp came up, where the lamp itself + // had little to add yet. While the lamp is weak the haze keeps enough of + // its grey to hold three fifths of its brightness, pink rather than dark + // red, and gives it up as the lamp's own glow takes the body over. + float3 lumWeights = float3(0.2126, 0.7152, 0.0722); + float lumBefore = dot(untinted, lumWeights); + float lumAfter = dot(BaseColor, lumWeights); + // The bulbs' effective drive, as the body counts it: colour times level. + float litNow = max(max(LampColor.r, max(LampColor.g, LampColor.b)) * saturate(LampLevel), + max(LampB.w > 0.0 ? max(DriveB.r, max(DriveB.g, DriveB.b)) * saturate(DriveB.w) : 0.0, + max(LampC.w > 0.0 ? max(DriveC.r, max(DriveC.g, DriveC.b)) * saturate(DriveC.w) : 0.0, + LampD.w > 0.0 ? max(DriveD.r, max(DriveD.g, DriveD.b)) * saturate(DriveD.w) : 0.0))); + // A clear cover only: a coloured cover is its colour already. + float clearCover = smoothstep(0.7, 0.95, min(Tint.r, min(Tint.g, Tint.b))); + float floorLum = 0.6 * lumBefore * (1.0 - smoothstep(0.1, 0.5, litNow)) * clearCover; + if (lumAfter < floorLum && lumBefore > lumAfter + 1e-5) { + BaseColor = lerp(BaseColor, untinted, saturate((floorLum - lumAfter) / (lumBefore - lumAfter))); + } + // The slab: its index, the colour the light has taken once it has come + // the tint depth through it, and its thickness, both given in millimetres + // and wanted in metres. The insert writes the thickness for its own depth. + RefractionIndex = max(Ior, 1.0); + TransmittanceColor = saturate(Tint); + AbsorptionDistance = max(TintDepth, 0.01) * 0.001; + SlabThickness = max(Thickness, 0.1) * 0.001; +#if defined(UNIVERSAL_PIPELINE_CORE_INCLUDED) && !defined(SHADERGRAPH_PREVIEW) && (!defined(SHADERPASS_META) || (SHADERPASS != SHADERPASS_META)) + if (_VpeInsertSceneColorAvailable > 0.5 && RefractionIndex > 1.0001) { + float3 positionWS = TransformObjectToWorld(Position); + float3 viewOS = normalize(TransformWorldToObjectDir(GetWorldSpaceNormalizeViewDir(positionWS))); + // Use the cover's projected tangent frame, so a rotated insert keeps + // the same normal-map distortion as the surrounding playfield. + float3 normalOS = normalize(TangentOS * NormalTS.x + BitangentOS * NormalTS.y + float3(0.0, NormalTS.z, 0.0)); + float3 rayOS = refract(-viewOS, normalOS, rcp(RefractionIndex)); + float path = SlabThickness / max(-rayOS.y, 0.05); + // Displace relative to the unrefracted ray at the slab floor. This + // makes the relief appear shallower, rather than translating it. + float3 straightOS = -viewOS * (SlabThickness / max(viewOS.y, 0.05)); + float3 offsetWS = TransformObjectToWorld(Position + rayOS * path - straightOS); + float4 screen = ComputeScreenPos(TransformWorldToHClip(positionWS)); + float4 shifted = ComputeScreenPos(TransformWorldToHClip(offsetWS)); + float2 uv = screen.xy / max(screen.w, 1e-5); + float2 refractedUV = shifted.xy / max(shifted.w, 1e-5); + float rawDepth = SampleSceneDepth(saturate(refractedUV)); + float behind = unity_OrthoParams.w > 0.5 ? LinearDepthToEyeDepth(rawDepth) : LinearEyeDepth(rawDepth, _ZBufferParams); + float coverDepth = -TransformWorldToView(positionWS).z; + // A ball in front of the cover, or an off-screen sample, must not + // be pulled into the insert. Use its undisplaced background there. + if (behind < coverDepth - 0.0001 || any(refractedUV < 0.0) || any(refractedUV > 1.0)) { + refractedUV = uv; + } + float3 transmission = pow(max(TransmittanceColor, 1e-5), path / AbsorptionDistance); + // Scene colour already has URP's lighting units. Only the haze's + // diffuse contribution is scaled; surface highlights stay intact. + Emission = SampleSceneColor(saturate(refractedUV)) * transmission * (1.0 - Alpha); + BaseColor *= Alpha; + Alpha = 1.0; + } +#endif +} +#endif diff --git a/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertCover.hlsl.meta b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertCover.hlsl.meta new file mode 100644 index 000000000..91e6171d3 --- /dev/null +++ b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertCover.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a279abca59990fb4ca6f814dd1527731 +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertOptics.hlsl b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertOptics.hlsl new file mode 100644 index 000000000..30097cd4a --- /dev/null +++ b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertOptics.hlsl @@ -0,0 +1,701 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef VPE_INSERT_OPTICS_INCLUDED +#define VPE_INSERT_OPTICS_INCLUDED + +// Read the sky in both pipelines, rather than a local reflection probe. +// Decode URP's environment texture before comparing room brightness. +float3 InsertSampleRoom(float3 direction, float mip) +{ +#if defined(SHADERGRAPH_PREVIEW) + return 1.0; +#elif defined(UNIVERSAL_PIPELINE_CORE_INCLUDED) + float4 encoded = SAMPLE_TEXTURECUBE_LOD(_GlossyEnvironmentCubeMap, sampler_GlossyEnvironmentCubeMap, direction, mip); + return DecodeHDREnvironment(encoded, _GlossyEnvironmentCubeMap_HDR); +#else + return SampleSkyTexture(direction, mip, 0).rgb; +#endif +} + +float3 InsertFacetRandom(float sector, float seed) +{ + return frac(sin((sector + seed * 17.13) * float3(127.1, 311.7, 74.7)) * 43758.5453); +} + +float3 InsertNoiseHash(float3 p) +{ + float3 h = frac(p * float3(0.1031, 0.1030, 0.0973)); + h += dot(h, h.yxz + 33.33); + return frac((h.xxy + h.yxx) * h.zyx); +} + +// Soft nearest-cell values: small cloudy variations without hard Voronoi +// borders. Coordinates are local metres, in all three axes, so atlas seams +// cannot split them and no slope stretches them: a starburst's facets stand +// steep, and a noise laid out over the floor with a share of the height +// mixed in was sheared along one slope of every ridge and squeezed on the +// other, into streaks along the facets seen on every other pleat. Noise in +// three dimensions is the same on any face, walls included. +float InsertCellular(float3 p) +{ + float3 cell = floor(p); + float3 local = frac(p); + float value = 0.0; + float weight = 0.0; + [unroll] for (int z = -1; z <= 1; z++) { + [unroll] for (int y = -1; y <= 1; y++) { + [unroll] for (int x = -1; x <= 1; x++) { + float3 offset = float3(x, y, z); + float3 random = InsertNoiseHash(cell + offset); + float3 delta = offset + 0.15 + 0.7 * random - local; + float w = exp2(-8.0 * dot(delta, delta)); + value += InsertNoiseHash(cell + offset + 17.0).x * w; + weight += w; + } + } + } + return value / max(weight, 1e-5); +} + +float InsertGrain(float3 p) +{ + float3 cell = floor(p); + float3 t = frac(p); + t = t * t * (3.0 - 2.0 * t); + float4 below = float4(InsertNoiseHash(cell).z, InsertNoiseHash(cell + float3(1, 0, 0)).z, + InsertNoiseHash(cell + float3(0, 1, 0)).z, InsertNoiseHash(cell + float3(1, 1, 0)).z); + float4 above = float4(InsertNoiseHash(cell + float3(0, 0, 1)).z, InsertNoiseHash(cell + float3(1, 0, 1)).z, + InsertNoiseHash(cell + float3(0, 1, 1)).z, InsertNoiseHash(cell + 1.0).z); + float4 layer = lerp(below, above, t.z); + return lerp(lerp(layer.x, layer.y, t.x), lerp(layer.z, layer.w, t.x), t.y); +} + +// The height of the surface grain, two octaves of the value noise like the +// bake's, in cells. +float InsertSurfaceGrainHeight(float3 cells) +{ + return InsertGrain(cells) + 0.5 * InsertGrain(cells * 2.07 + 17.3); +} + +// Ray tracing stages have no screen-space derivatives, so the detail stays +// fully visible there. +float InsertDetailVisibility(float3 p) +{ +#if defined(SHADER_STAGE_RAY_TRACING) + return 1.0; +#else + float footprint = max(length(ddx(p)), length(ddy(p))); + return 1.0 - smoothstep(0.35, 1.0, footprint); +#endif +} + +float InsertPixelWidth(float x) +{ +#if defined(SHADER_STAGE_RAY_TRACING) + return 0.0; +#else + return fwidth(x); +#endif +} + +// All positions and directions are in insert local space, in metres. This +// works per eye and at arbitrary camera angles without a scene-colour buffer. +// Hue, saturation and value, hue as a turn of the wheel in 0..1. +float3 InsertOpticsRgbToHsv(float3 c) +{ + float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + float4 p = lerp(float4(c.bg, K.wz), float4(c.gb, K.xy), step(c.b, c.g)); + float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.r)); + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +float3 InsertOpticsHsvToRgb(float3 c) +{ + float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + float3 p = abs(frac(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * lerp(K.xxx, saturate(p - K.xxx), c.y); +} + +// Turns the colour's hue towards the lamp's, and its saturation towards the +// lamp's, never below its own, both by the amount; the value is never touched, so a grey haze comes out the lamp's +// colour, as bright as the grey was, and never darker. An RGB lamp carries +// its intensity in its colour, and a dim lamp is still its colour: the amount +// is full from a tenth of full brightness up, and only a lamp that is off, +// its channels all down, leaves the colour untouched. A white lamp has no hue +// to give. +float3 InsertOpticsTowardsLampHue(float3 color, float3 lamp, float amount) +{ + float3 hsv = InsertOpticsRgbToHsv(max(color, 0.0)); + float3 lampHsv = InsertOpticsRgbToHsv(max(lamp, 0.0)); + amount = saturate(amount) * saturate(lampHsv.z * 10.0); + // The hue turns the short way round the wheel, by the lamp's share of the + // saturation the mix ends up with: a grey, which has no hue of its own, + // takes the lamp's hue at any amount, its saturation growing with it, + // instead of a hue part of the way round the wheel, so a grey haze half + // tinted by a blue lamp is a paler blue, never pink; two colours as + // saturated as each other turn by the amount itself. + float turn = frac(lampHsv.x - hsv.x + 0.5) - 0.5; + float share = saturate(amount * lampHsv.y / max(lerp(hsv.y, lampHsv.y, amount), 1e-5)); + hsv.x = frac(hsv.x + turn * share); + hsv.y = lerp(hsv.y, max(hsv.y, lampHsv.y), amount); + return InsertOpticsHsvToRgb(hsv); +} + +// One virtual bulb's share of a point: the halo its facet shows (x), how much +// of the cavity under it a clear body's face looks onto (y), how far the point +// sits from it for the spread (z, one without a spread), and the tight image +// of the bulb itself (w), kept apart from the halo so it can take a colour of +// its own. Position and the refracted view ray are in insert space, the lamp +// its position in metres with its radius in w. +float4 InsertOpticsBulb(float4 lamp, float3 position, float3 ray, float faceGain, float fresnel, float exit, + float responseB, float cavitySpread, float spread) +{ + float distance = max(position.y - lamp.y, 0.0) / max(-ray.y, 0.12); + float2 hit = position.xz + ray.xz * distance; + float radius = max(abs(lamp.w), 0.0001); + // Keep a common source behind every face. Moving the source independently + // per sector turns the bulb into unrelated gradients and destroys depth. + float2 delta = (hit - lamp.xz) / radius; + float direct = exp2(-2.0 * dot(delta, delta)); + // A tighter image of the bulb inside its halo, as strong as the looks + // always had it. + float bulb = exp2(-12.0 * dot(delta, delta)) * 3.7; + // A wider reflected image stands in for a second internal bounce. + float2 reflectedHit = position.xz - ray.xz * distance * 0.65; + float2 reflectedDelta = (reflectedHit - lamp.xz) / (radius * 1.5); + float bounced = exp2(-2.0 * dot(reflectedDelta, reflectedDelta)); + // The tight source image survives even on the less efficient slopes; + // the broad transmitted component still carries face-to-face contrast. + // Both are what the facet lets through, none at all once it traps the + // ray; the bounce is what it throws back and stays. + float passed = responseB * (1.0 - 0.65 * fresnel); + float transmitted = passed * (direct * exit + 0.17 * bounced) * faceGain; + float image = passed * bulb * exit * lerp(1.0, faceGain, 0.25); + // Each face shows the part of the cavity its refracted view ray lands on, + // spread wide and kept soft, so the ribs shift with the view instead of + // alternating with the lights. + float wide = radius * max(cavitySpread, 0.1); + float2 cavityDelta = (hit - lamp.xz) / wide; + float2 cavityBounce = (reflectedHit - lamp.xz) / (wide * 1.5); + float seen = exit * exp2(-dot(cavityDelta, cavityDelta)) + 0.3 * exp2(-dot(cavityBounce, cavityBounce)); + // Every facet tilted towards the lamp shows its halo, however far from the + // lamp it sits, and the baked glow reaches the whole body alike, so raising + // the brightness floods the body. A real insert is brightest around its + // bulb and dims away from it, the light spreading out from the bulb as + // from a point, by the inverse square of the distance: the spread, in + // millimetres, is the distance from the lamp at which the lit look has + // fallen to half, a fifth at twice that and a tenth at three times, a long + // reach so that one bulb of a banana still shows at its far end and two + // bulbs meet in a soft blend, never a seam. Zero leaves it everywhere alike. + float leak = 1.0; + if (spread > 0.0) { + float2 away = (position.xz - lamp.xz) / (spread * 0.001); + leak = rcp(1.0 + dot(away, away)); + } + return float4(transmitted, seen, leak, image); +} + +// What a facet reflects of the light inside the plastic, seen from the dense +// side: Schlick on the angle in the air beyond it, and all of it once the +// facet traps the ray. +float InsertInternalReflectance(float cosInside, float eta, float reflectance0) +{ + float sinOutside = eta * sqrt(saturate(1.0 - cosInside * cosInside)); + if (sinOutside >= 1.0) { + return 1.0; + } + float cosOutside = sqrt(saturate(1.0 - sinOutside * sinOutside)); + return reflectance0 + (1.0 - reflectance0) * pow(1.0 - cosOutside, 5.0); +} + +// Whether a ray inside the plastic, pointing up, gets out through the flat +// top: not past the critical angle, faded over the last tenth so nothing +// jumps as the eye moves. +float InsertEscapes(float3 ray, float eta) +{ + float sinOutside2 = eta * eta * (1.0 - ray.y * ray.y); + return step(0.0, ray.y) * (1.0 - smoothstep(0.8, 1.0, sinOutside2)); +} + +// The room, as a ray leaving through the top sees it: bent out of the +// plastic, turned to world space, and read from the sky's reflection cubemap +// at a blurred mip, which is what a moulded, slightly rough groove returns. +float3 InsertRoom(float3 ray, float eta, float blur) +{ + float3 outside = refract(ray, float3(0.0, -1.0, 0.0), eta); + float3 world = TransformObjectToWorldDir(outside + float3(0.0, 1e-5, 0.0)); + return InsertSampleRoom(world, blur); +} + +// The sky's average brightness round the insert: its most blurred mip +// straight up and to the four sides, the hemisphere a groove returns. +float InsertSkyAverage() +{ + float3 lum = float3(0.2126, 0.7152, 0.0722); + float sum = dot(InsertSampleRoom(float3(0.0, 1.0, 0.0), 6.0), lum) + + dot(InsertSampleRoom(float3(1.0, 0.0, 0.0), 6.0), lum) + + dot(InsertSampleRoom(float3(-1.0, 0.0, 0.0), 6.0), lum) + + dot(InsertSampleRoom(float3(0.0, 0.0, 1.0), 6.0), lum) + + dot(InsertSampleRoom(float3(0.0, 0.0, -1.0), 6.0), lum); + return max(sum * 0.2, 1e-6); +} + +// How much of the room a ray going down into a groove brings back out +// through the top, and, where the sky may be read, the room it brings. The +// groove is the facet the ray meets first and its mirrored neighbour. After +// each facet the ray goes on to the other one if it heads into it, which is +// when it climbs more slowly than that facet rises; otherwise it goes up to +// the top, where what gets out is the room seen that way and the rest comes +// back down, under total internal reflection all of it, onto whichever +// facet it heads into. A groove near a right angle returns the ray off the +// two facets, a corner reflector; a shallower one throws it up to the top, +// which sends it down onto the other facet, and that one out. Each facet +// takes its Fresnel share; four facets are as far as it goes. Where the ray +// comes down from the top as squarely onto one facet as onto the other, the +// choice between them would flip the rest of its path from one side of the +// room to the other within a facet: what comes back after such a choice is +// faded in as the choice firms up, so nothing steps. The room is +// the sky over its average brightness: how much brighter or darker it is +// that way than all round. Where the sky isn't read, it is the same all round. +float InsertRoomPaths(float3 ray, float3 bent, float3 neighbour, float eta, float reflectance0, float blur, bool readRoom, out float3 room) +{ + room = float3(0.0, 0.0, 0.0); + float back = 0.0; + float carried = 1.0; + // How firmly the path's facet choices were made, fading what follows a near tie. + float firm = 1.0; + float3 d = ray; + bool onBent = true; + for (int i = 0; i < 4; i++) { + float3 facet = onBent ? bent : neighbour; + float3 other = onBent ? neighbour : bent; + carried *= InsertInternalReflectance(saturate(dot(-d, facet)), eta, reflectance0); + d = reflect(d, facet); + if (dot(-d, other) > 0.0) { + onBent = !onBent; + continue; + } + if (d.y <= 0.0) { + break; + } + float escaping = InsertEscapes(d, eta) * (1.0 - InsertInternalReflectance(saturate(d.y), eta, reflectance0)); + float leaving = carried * escaping; + carried -= leaving; + leaving *= firm; + back += leaving; + if (readRoom && leaving > 0.0) { + room += leaving * InsertRoom(d, eta, blur); + } else if (!readRoom) { + room += leaving; + } + d.y = -d.y; + float towardsBent = dot(-d, bent); + float towardsNeighbour = dot(-d, neighbour); + if (max(towardsBent, towardsNeighbour) <= 0.0) { + break; + } + onBent = towardsBent >= towardsNeighbour; + firm *= smoothstep(0.0, 0.05, abs(towardsBent - towardsNeighbour)); + } + if (readRoom && back > 0.0) { + room /= InsertSkyAverage(); + } + return back; +} + +void InsertOptics_float(float3 Position, float3 View, float2 UV, + UnityTexture2D OpticalNormal, UnityTexture2D Response, UnityTexture2D SurfaceNormal, + float4 Lamp, float Ior, float Scatter, float EdgeGlow, float LampGain, + float NormalStrength, float3 Tint, float3 LampColor, float3 DriveColor, float Level, + float4 Starburst, float Contrast, float Variation, float Seed, float Brightness, + float MottleSize, float Grain, float MinBrightness, + float3 WorldNormal, float3 WorldTangent, float3 WorldBitangent, float Flatten, + float Clarity, float3 CavityColor, float CavitySpread, float CavityContrast, + float LampTint, float Spread, float4 LampB, float4 LampC, float4 LampD, + float4 DriveB, float4 DriveC, float4 DriveD, + float SurfaceGrain, float SurfaceGrainSize, float Output, float Roughness, + float Flecks, float FleckSize, float RoomReturn, float RoomBlur, float RoomReturnEV, float UrpEmissionScale, + out float3 BaseColor, out float3 NormalTS, out float3 Emission) +{ + // How rough the structure inside the plastic is: smooth facets and + // channels couple the lamp into crisp ribs and rays, rough ones diffuse + // it, so the ribs soften and the lamp's rays and image go into the glow. + // The top's own finish is the cover's business, not this. + float wear = saturate(Roughness); + Contrast *= 1.0 - 0.7 * wear; + float3 rawNormal = SAMPLE_TEXTURE2D(OpticalNormal.tex, OpticalNormal.samplerstate, UV).rgb * 2.0 - 1.0; + float3 n = normalize(rawNormal + float3(0, 1e-5, 0)); + float3 response = SAMPLE_TEXTURE2D(Response.tex, Response.samplerstate, UV).rgb; + float3 v = normalize(View + float3(0, 1e-5, 0)); + // The facets the bake laid out, found again from the position: a wheel's + // sectors round its centre, ribs' strips across their direction, which + // the bake tells apart by a half pitch made negative in z, or rings' + // strips out from their centre, the half pitch pushed past minus a + // thousand. Strips are counted from far off, so the index stays positive + // and the neighbours either side need no wrapping round. A gem, its sides + // pushed past two thousand, has facets no angle alone finds again, so its + // variation turns round the centre smoothly, one value a side, with no + // step to land in the middle of a facet. + bool ringed = Starburst.z <= -1000.0; + bool ribbed = Starburst.z < 0.0 && !ringed; + bool gem = Starburst.z >= 2000.0; + float sectors = Starburst.z < 0.0 ? 1e6 : gem ? max(Starburst.z - 2000.0, 1.0) : max(Starburst.z, 1.0); + float coordinate; + if (ringed) { + coordinate = length(Position.xz - Starburst.xy) / max(-Starburst.z - 1000.0, 1e-6) + 4096.0; + } else if (ribbed) { + coordinate = dot(Position.xz, Starburst.xy) / -Starburst.z + 4096.0; + } else { + float2 radial = Position.xz - Starburst.xy; + float angle = atan2(radial.y, radial.x); + coordinate = frac(angle * (1.0 / 6.28318530718) + 1.0) * sectors; + } + float sector = floor(coordinate); + float3 random = InsertFacetRandom(sector, Seed); + // Fade the random coefficients across less than one screen pixel at a + // crease. Their identity comes from the fan, never quantized bake noise. + float width = gem ? 0.5 : min(0.45, max(InsertPixelWidth(coordinate), 0.01)); + float part = frac(coordinate); + float previous = fmod(sector + sectors - 1.0, sectors); + float next = fmod(sector + 1.0, sectors); + random = lerp(InsertFacetRandom(previous, Seed), random, smoothstep(-width, width, part)); + random = lerp(random, InsertFacetRandom(next, Seed), smoothstep(1.0 - width, 1.0 + width, part)); + float floorMask = (Starburst.z < 0.0 ? 1.0 : step(4.0, Starburst.z)) * smoothstep(0.35, 0.7, response.b) + * smoothstep(Starburst.w - 0.00015, Starburst.w + 0.00015, Position.y); + float3 cells = Position / max(MottleSize * 0.001, 0.0001) + Seed * float3(7.13, 3.71, 5.29); + float mottle = (InsertCellular(cells) - 0.5) * InsertDetailVisibility(cells); + float3 grainPosition = Position * 4000.0 + Seed * float3(13.1, 17.7, 11.3); + float grain = (InsertGrain(grainPosition) - 0.5) * InsertDetailVisibility(grainPosition); + float finish = 1.0 + grain * Grain; + // The flecks of a moulded insert: the ridges between the facets never + // come out of the mould clean, and under room light every nick and + // every bit of flash along them catches the light as a bright dash, + // stretched along the rib it sits on, while the faces between stay + // plain. Sparse cells of the cloudy noise, drawn long along the ribs + // and gathered to the creases. Each is a small tilted face in the + // plastic: its slope tilts the shading normal, so a fleck lights up + // when a light or the room reflects off it and goes quiet when none + // does, and only a little of it brightens the plastic flat, lit and + // unlit, so the dashes are there in any light. Zero leaves a clean + // moulding. + float2 fleckSlope = float2(0.0, 0.0); + if (Flecks > 0.0) { + float2 radial = Position.xz - Starburst.xy; + radial *= rsqrt(max(dot(radial, radial), 1e-10)); + float2 along = ringed ? float2(-radial.y, radial.x) + : ribbed ? normalize(float2(-Starburst.y, Starburst.x) + float2(1e-6, 0.0)) + : gem ? float2(1.0, 0.0) : radial; + float2 across = float2(-along.y, along.x); + float size = max(FleckSize * 0.001, 1e-5); + // Across a radial rib the position has no component of its own, so + // the facet's index moves each rib to cells of its own. + float3 fleckCells = float3(dot(Position.xz, along) / (size * 4.0), Position.y / size, dot(Position.xz, across) / size + sector * 3.7) + + Seed * float3(5.1, 2.3, 8.7); + float crease = 1.0 - smoothstep(0.0, 0.4, min(part, 1.0 - part)); + float weight = lerp(0.5, 1.0, crease) * floorMask * InsertDetailVisibility(fleckCells); + // The value noise, not the cloudy cellular one: the three samples + // here cost a fraction of the cellular's twenty-seven cells each, + // and thresholded they give the same dashes. + float here = smoothstep(0.42, 0.6, InsertGrain(fleckCells)); + // The slope of the dashes along the tangent and bitangent the normal + // map is read in, a tenth of a cell apart, in the stretched cells. + float3 tangentOS = normalize(TransformWorldToObjectDir(WorldTangent) + float3(1e-6, 0.0, 0.0)); + float3 bitangentOS = normalize(TransformWorldToObjectDir(WorldBitangent) + float3(0.0, 1e-6, 0.0)); + const float stepCells = 0.1; + float3 alongT = stepCells * float3(dot(tangentOS.xz, along) * 0.25, tangentOS.y, dot(tangentOS.xz, across)); + float3 alongB = stepCells * float3(dot(bitangentOS.xz, along) * 0.25, bitangentOS.y, dot(bitangentOS.xz, across)); + float heightT = smoothstep(0.42, 0.6, InsertGrain(fleckCells + alongT)); + float heightB = smoothstep(0.42, 0.6, InsertGrain(fleckCells + alongB)); + fleckSlope = float2(heightT - here, heightB - here) / stepCells * weight; + finish *= 1.0 + 0.35 * here * weight * saturate(Flecks); + } + // The facets bend the ray as the bake found them. Flattening steep walls + // keeps this deliberately artistic ray pointed down. + n = normalize(float3(n.x, max(abs(n.y), 0.25), n.z)); + // The view ray enters the plastic through its flat top, bending towards + // the vertical, and leaves it through the facet underneath into the air + // of the cavity, bending back the other way, the facet a prism, before it + // runs on down to the bulb. Traced so, the bulb sits its full depth below + // the insert: it shifts against the body by the whole of the depth times + // the view's tilt as the eye moves, and each eye of a headset sees it + // where its own ray meets it. One refraction with the facet's normal kept + // the steeper in-plastic ray all the way down, half the parallax, and bent + // it the wrong way round the facet. What the facet lets through falls + // with the exit's Fresnel reflectance. + // + // A facet steep enough traps the ray, total internal reflection, or lets + // it out so near the grazing that it runs off sideways and never meets + // the lamp: a starburst of narrow spokes over a tall ridge has such + // facets everywhere, and seen along its length, where the in-plastic ray + // crosses every facet at its full tilt, the whole bulb went missing while + // it showed from the side. Through a V groove that steep a trapped ray + // leaves by the facet next door, thrown out to the side, so a mirrored + // path would not find the bulb either. What such a facet would trap or + // throw away goes straight on instead, as through a flat bottom, blended + // in as the facet's exit nears the critical angle so nothing jumps: the + // bulb keeps its place and its parallax from every angle, and the facets + // keep their prism wherever they pass the ray. + float eta = max(Ior, 1.001); + float3 inside = refract(-v, float3(0.0, 1.0, 0.0), rcp(eta)); + float reflectance0 = (eta - 1.0) / (eta + 1.0); + reflectance0 *= reflectance0; + // The sine of the facet's exit angle, past one where it traps the ray. + float cosInside = saturate(dot(-inside, n)); + float sinFacet = eta * sqrt(saturate(1.0 - cosInside * cosInside)); + float passing = 1.0 - smoothstep(0.75, 1.0, sinFacet); + float3 facetRay = passing > 0.0 ? refract(inside, n, eta) : -v; + float cosFacet = saturate(dot(facetRay, -n)); + float facetExit = 1.0 - (reflectance0 + (1.0 - reflectance0) * pow(1.0 - cosFacet, 5.0)); + float flatExit = 1.0 - (reflectance0 + (1.0 - reflectance0) * pow(1.0 - saturate(v.y), 5.0)); + float3 ray = normalize(lerp(-v, facetRay, passing)); + float exit = lerp(flatExit, facetExit, passing); + float facing = saturate(dot(v, n)); + float fresnel = 0.04 + 0.96 * pow(1.0 - facing, 5.0); + // Opposite slopes couple very differently to the same internal light. + // This contrast fills the face instead of outlining its rounded edge. + float2 bias = v.xz + float2(0.38, -0.21); + bias *= rsqrt(max(dot(bias, bias), 1e-5)); + float coupling = saturate(0.5 + 1.8 * dot(n.xz, bias)); + float faceGain = lerp(1.0, 0.12 + 1.8 * pow(coupling, 1.3), Contrast * floorMask); + faceGain *= 1.0 + Variation * floorMask * ((random.z - 0.5) * 0.18 + mottle * 0.7); + // An insert has one bulb or a few, a banana its three or four in a row: + // every bulb whose radius is set adds its own halo, each fading with its + // own spread; the baked glow, one for the whole body, fades with the + // nearest bulb, and a clear body's face looks onto whichever cavity its + // ray lands on. + float4 bulbA = InsertOpticsBulb(Lamp, Position, ray, faceGain, fresnel, exit, response.b, CavitySpread, Spread); + float4 bulbB = LampB.w > 0.0 ? InsertOpticsBulb(LampB, Position, ray, faceGain, fresnel, exit, response.b, CavitySpread, Spread) : float4(0.0, 0.0, 0.0, 0.0); + float4 bulbC = LampC.w > 0.0 ? InsertOpticsBulb(LampC, Position, ray, faceGain, fresnel, exit, response.b, CavitySpread, Spread) : float4(0.0, 0.0, 0.0, 0.0); + float4 bulbD = LampD.w > 0.0 ? InsertOpticsBulb(LampD, Position, ray, faceGain, fresnel, exit, response.b, CavitySpread, Spread) : float4(0.0, 0.0, 0.0, 0.0); + // Each bulb is driven on its own, the first by the lamp colour and level, + // the others by a colour with the level in w: its halo carries its own + // colour, and the baked glow, one for the whole body, is the plain sum of + // every bulb's light, each reaching as far as its spread lets it, colour + // and all. So one bulb of a banana lights its end and fades along the + // body, undimmed by the bulbs beside it that are off, two bulbs on add up + // where they meet, and a bulb fading in colours every point it reaches in + // proportion to its level, from the first glimmer on, instead of its + // colour creeping in from the bulb once it is bright enough to win. + // One kind of bulb under the insert: an LED kit gives more light than + // the filaments it replaced, at the same level and colour. + float output = max(Output, 0.0); + float3 driveA = max(DriveColor, 0.0) * saturate(Level) * output; + float3 driveB = max(DriveB.rgb, 0.0) * saturate(DriveB.w) * output; + float3 driveC = max(DriveC.rgb, 0.0) * saturate(DriveC.w) * output; + float3 driveD = max(DriveD.rgb, 0.0) * saturate(DriveD.w) * output; + float3 transmitted = bulbA.x * bulbA.z * driveA + bulbB.x * bulbB.z * driveB + bulbC.x * bulbC.z * driveC + bulbD.x * bulbD.z * driveD; + float3 image = bulbA.w * bulbA.z * driveA + bulbB.w * bulbB.z * driveB + bulbC.w * bulbC.z * driveC + bulbD.w * bulbD.z * driveD; + float4 level = float4(max(driveA.r, max(driveA.g, driveA.b)), max(driveB.r, max(driveB.g, driveB.b)), + max(driveC.r, max(driveC.g, driveC.b)), max(driveD.r, max(driveD.g, driveD.b))); + float4 reach = float4(bulbA.z, bulbB.z, bulbC.z, bulbD.z); + float3 glowDrive = reach.x * driveA + reach.y * driveB + reach.z * driveC + reach.w * driveD; + // The light's colour at unit level, for the tint's hue. + float3 chroma = glowDrive / max(dot(reach, level), 1e-6); + // How far the bulbs' colour reaches for the tint: as the light does, full + // to a tenth of the bulb's own and fading beyond, from a bulb that counts + // as on from a tenth of full up, so a bulb fading in tints its whole + // reach at once and grows brighter within it, never creeping outwards + // from the bulb as the light itself climbs. + float tinted = dot(saturate(reach * 10.0), saturate(level * 10.0)); + float seen = saturate(bulbA.y + bulbB.y + bulbC.y + bulbD.y); + float glow = Scatter * response.r * lerp(1.0, faceGain, floorMask * 0.65) + + EdgeGlow * response.g * (0.25 + 0.75 * response.r); + // The material sets full-intensity brightness and its emission colour; ROM + // events only change each bulb's normalized level and RGB through the + // renderer's property block. + float3 emission = max(LampColor, 0.0); + // A rough structure scatters the rays and the bulb's image into the glow. + float sharp = 1.0 - wear; + Emission = max(Brightness, 0.0) * emission * (glow * glowDrive * (1.0 + 0.5 * wear) + LampGain * (transmitted + image) * sharp) * finish; + // Darken the unlit plastic without keeping the bulb or spill light on. + // Fade back to the authored albedo as the ROM raises lamp intensity. + // Any bulb that is on lights the plastic, not only the first: a multi-bulb + // insert with its first bulb off and another on is as bright as with the + // first on. Only bulbs that exist count. + float bulbsUp = max(saturate(Level), max(LampB.w > 0.0 ? saturate(DriveB.w) : 0.0, + max(LampC.w > 0.0 ? saturate(DriveC.w) : 0.0, LampD.w > 0.0 ? saturate(DriveD.w) : 0.0))); + float bodyBrightness = lerp(saturate(MinBrightness), 1.0, bulbsUp); + float cavity = 0.35 + 0.65 * response.r; + float3 tintedBody = Tint * cavity * bodyBrightness; + // Clear plastic absorbs nothing: an off clear insert shows the bright lamp + // board or reflector under it, lit by room light that came in through the + // top, lamp or no lamp. Each face shows the part of that cavity its + // refracted view ray lands on, as the bulbs found above. The walls and the + // channel get their share by internal reflection: the baked irradiance. + float cavityView = lerp(1.0, seen, saturate(CavityContrast)); + // Room light reaches the whole cavity, so its irradiance shapes the clear + // body less than it darkens the tinted one. + float3 clearBody = CavityColor * (0.6 + 0.4 * response.r) * lerp(0.6, cavityView, floorMask); + BaseColor = lerp(tintedBody, Tint * clearBody, saturate(Clarity)) * finish; + // Moulded plastic is never evenly clear: under room light it is cloudy, + // blotches of it lighter and darker than the rest, up to two fifths + // either way at full Variation. The cellular noise averages its + // neighbours, so it is widened to its full range first. A blotch + // brightens no further than the albedo's brightest channel can go. + float mottleGain = 1.0 + Variation * 0.8 * clamp(mottle * 1.4, -0.5, 0.5); + float bodyPeak = max(BaseColor.r, max(BaseColor.g, BaseColor.b)); + BaseColor *= min(mottleGain, max(1.0, rcp(max(bodyPeak, 1e-5)))); + // Light that came in through the top and went down to a groove does not + // all leave into the cavity: the facet throws part of it, or under total + // internal reflection all of it, across to the facet next door, which + // throws it back up and out through the top, the way a corner reflector + // returns a beam. So a clear insert in a room shows the room above it in + // its ribs, brighter from a low view where the top reflects more, and the + // ribs leaning one way return a different part of the room than those + // leaning the other, exchanging their brightness as the eye moves round: + // the movement under the top that tells clear plastic from paint. The + // path is the view ray refracted in at the top, reflected off the facet + // the bake found, its mirrored neighbour and the top as the groove's + // angle sends it, each reflection weighed by the plastic's own Fresnel, + // then out through the top into the sky's reflection cubemap. What comes + // back takes the place of as much of the diffuse cavity as it weighs, in + // the albedo: the room is only as bright as the light that falls on the + // insert, a directional light, the probes or the sky's own, so the sky + // gives the room its shape, brighter one way and darker another, and the + // scene's lighting its brightness, in a dark room as in a bright one. + float returning = 0.0; + if (RoomReturn > 0.0 && Clarity > 0.0) { + float3 facet = normalize(rawNormal + float3(0.0, 1e-5, 0.0)); + facet.y = abs(facet.y); + // The rib runs radially from the wheel's centre, across its own + // direction for ribs, round the centre for rings; the neighbouring + // facet is this one mirrored across the rib, its lean along the rib + // kept. A moulded facet is never quite flat: a slight bend across it, + // a degree and a half from one crease to the other, lets a broad + // highlight sweep within a stripe instead of switching whole. + float2 radialDir = Position.xz - Starburst.xy; + radialDir *= rsqrt(max(dot(radialDir, radialDir), 1e-10)); + float2 ribDir = ringed ? float2(-radialDir.y, radialDir.x) + : ribbed ? normalize(float2(-Starburst.y, Starburst.x) + float2(1e-6, 0.0)) + : gem ? float2(1.0, 0.0) : radialDir; + float3 along = float3(ribDir.x, 0.0, ribDir.y); + // The bend dies out at the creases so nothing jumps there. + float bend = (part - 0.5) * 0.0524 * smoothstep(0.0, 0.25, min(part, 1.0 - part)); + float3 bent = facet * cos(bend) + cross(along, facet) * sin(bend) + along * dot(along, facet) * (1.0 - cos(bend)); + float3 across = float3(-along.z, 0.0, along.x); + float3 neighbour = normalize(bent - 2.0 * dot(bent, across) * across); + // The lightmap pass doesn't read the sky; to it the room is the same + // all round. +#if defined(SHADERPASS) && ((defined(SHADERPASS_LIGHT_TRANSPORT) && (SHADERPASS == SHADERPASS_LIGHT_TRANSPORT)) || (defined(SHADERPASS_META) && (SHADERPASS == SHADERPASS_META))) + bool readRoom = false; +#else + bool readRoom = true; +#endif + // The return switches from one facet's path to its neighbour's at the + // crease, texel by texel; faded over a couple of screen pixels either + // side, as the facet coefficients are, the switch no longer stairs. + float creaseFade = smoothstep(0.0, 2.5 * width, min(part, 1.0 - part)); + float share = saturate(RoomReturn) * saturate(Clarity) * floorMask * flatExit * creaseFade; + float3 room = float3(0.0, 0.0, 0.0); + float back = 0.0; + if (share > 0.0) { + back = InsertRoomPaths(inside, bent, neighbour, eta, reflectance0, RoomBlur, readRoom, room); + } + // The room comes back whether the lamp is on or off, as it does on + // the real thing: a lit insert still reflects the room above it. But + // inside a body flooded with the lamp's colour a white reflection + // reads wrong, so the returned room takes the colour of the bulbs + // that are on, the more the brighter they are, and a bulb barely on + // leaves it white rather than glowing coloured. Only bulbs that exist + // count: a bulb switched off keeps its last drive. + float lit = max(level.x, max(LampB.w > 0.0 ? level.y : 0.0, max(LampC.w > 0.0 ? level.z : 0.0, LampD.w > 0.0 ? level.w : 0.0))); + // The bulb colour is first lifted to the luminance of white, capped so + // a saturated bulb does not blow out, and the tint then runs from + // white to that: blending first and lifting after kept the white half + // of a half-made tint and snapped to the colour only at the end. + float3 lampTint = chroma * min(rcp(max(dot(chroma, float3(0.2126, 0.7152, 0.0722)), 0.05)), 4.0); + float3 roomTint = lerp(float3(1.0, 1.0, 1.0), lampTint, smoothstep(0.02, 0.3, lit)); + returning = share * back; + // The lamp's own light takes the same paths: what a facet throws back + // of the room it throws back of the lamp below, so the returning + // facets stay lighter than their neighbours however bright the lamp + // gets, instead of drowning in it, and in the tonemapper's shoulder + // they go the lighter, paler colour a brighter lamp does. + Emission *= 1.0 + returning * saturate(lit); + // An albedo can't pass one, however bright the room is that way. + BaseColor = saturate(BaseColor * (1.0 - returning) + share * exp2(RoomReturnEV) * room * roomTint); + } + // A grey body under a coloured lamp milks its colour. Tinted with the lamp, + // the body's hue and saturation turn to the light's, the bulbs' colours + // summed as the glow sums them, as far as their light reaches, its value + // untouched, so nothing of the lamps stays once they are off. + float3 untinted = BaseColor; + BaseColor = InsertOpticsTowardsLampHue(BaseColor, chroma, saturate(LampTint) * saturate(tinted)); + // Turning a grey to a colour keeps its value, not its brightness: a red + // as light as the grey was has a fifth of its luminance, so the plastic + // went darker as the lamp came up, where the lamp itself had little to + // add yet. The room light the plastic scatters does not go away for the + // lamp colouring it, so the luminance the albedo lost comes back in the + // albedo, its hue kept and its value raised, as far as the hue lets it: + // lit by whatever lights the insert, as the returned room is, and lifted + // as the returned room is. + // An albedo can be no brighter than its colour allows, a pure red a + // fifth of a grey, so while the lamp is weak the plastic keeps enough of + // its grey to hold three fifths of its brightness, pink rather than dark + // red, and gives it up as the lamp's own glow takes the body over. + float3 lumWeights = float3(0.2126, 0.7152, 0.0722); + float lumBefore = dot(untinted, lumWeights); + float lumAfter = dot(BaseColor, lumWeights); + float litNow = max(level.x, max(LampB.w > 0.0 ? level.y : 0.0, max(LampC.w > 0.0 ? level.z : 0.0, LampD.w > 0.0 ? level.w : 0.0))); + // Clear plastic only: a tinted body is its colour already, lit or not. + float floorLum = 0.6 * lumBefore * (1.0 - smoothstep(0.1, 0.5, litNow)) * saturate(Clarity); + if (lumAfter < floorLum && lumBefore > lumAfter + 1e-5) { + BaseColor = lerp(BaseColor, untinted, saturate((floorLum - lumAfter) / (lumBefore - lumAfter))); + } + float lost = max(lumBefore - dot(BaseColor, lumWeights), 0.0) * saturate(Clarity) * saturate(RoomReturn); + if (lost > 0.0) { + float lum = max(dot(BaseColor, lumWeights), 1e-5); + float peak = max(BaseColor.r, max(BaseColor.g, BaseColor.b)); + BaseColor *= max(min((lum + lost * exp2(RoomReturnEV)) / lum, rcp(max(peak, 1e-5))), 1.0); + } + float4 packedNormal = SAMPLE_TEXTURE2D(SurfaceNormal.tex, SurfaceNormal.samplerstate, UV); + float3 surface = UnpackNormal(packedNormal); + float3 shading = normalize(float3(surface.xy * NormalStrength, lerp(1.0, surface.z, saturate(NormalStrength)))); + // Room light enters a clear insert through its flat top, and the facets + // underneath only redistribute what is already inside. To the lights and + // reflection probes the surface is that flat top, so the shading normal is + // flattened towards it, written in the frame the tangent-space normal is + // read back in. The optical normal that refracts the lamp stays as baked. + float3 upWS = TransformObjectToWorldNormal(float3(0.0, 1.0, 0.0)); + // The interpolated frame need not be orthonormal, so this is the frame's + // proper inverse, not three dot products. + float3 upTS = TransformWorldToTangent(upWS, float3x3(WorldTangent, WorldBitangent, WorldNormal)); + float3 normalTS = lerp(shading, upTS, saturate(Flatten)); + // A facet that returns the room is a mirror inside the plastic to the + // scene's lights as well: to them it keeps its own relief whatever the + // flattening, so a spot or the sun glints off it where the sky alone, + // which carries none of the table's lights, would not. The diffuse stays. + normalTS = lerp(normalTS, shading, saturate(returning)); + // The fine texture of the moulded plastic, in the reflections: a bump of + // noise in insert space, as the bake used to put into the normal map, + // drawn here instead so that it is as fine at any zoom and on any map + // size, and stays continuous over UV seams. Its slope along the tangent + // and bitangent, taken as differences a tenth of a cell apart, tilts the + // shading normal after the flattening, which is the top it sits on. It + // fades out as the cells shrink below a pixel, before it can shimmer. + if (SurfaceGrain > 0.0) { + float3 cellsPerMetre = 1.0 / max(SurfaceGrainSize * 0.001, 1e-5); + float3 grainCells = Position * cellsPerMetre + Seed * float3(3.7, 9.1, 5.3); + float3 tangentOS = normalize(TransformWorldToObjectDir(WorldTangent) + float3(1e-6, 0.0, 0.0)); + float3 bitangentOS = normalize(TransformWorldToObjectDir(WorldBitangent) + float3(0.0, 1e-6, 0.0)); + const float step = 0.1; + float here = InsertSurfaceGrainHeight(grainCells); + float alongT = InsertSurfaceGrainHeight(grainCells + tangentOS * step); + float alongB = InsertSurfaceGrainHeight(grainCells + bitangentOS * step); + float2 slope = float2(alongT - here, alongB - here) / step; + float visible = InsertDetailVisibility(grainCells); + normalTS.xy -= slope * (0.3 * saturate(SurfaceGrain) * visible); + } + // The flecks' tilt, after the flattening, on the top they are cut into. + normalTS.xy -= fleckSlope * (0.25 * saturate(Flecks)); + NormalTS = normalize(normalTS + float3(0.0, 0.0, 1e-5)); +#if defined(UNIVERSAL_PIPELINE_CORE_INCLUDED) + // Preserve the authored nits and lamp property blocks. Only the final + // emission changes units; the resolver supplies UrpPhysicalUnits' scale. + Emission *= UrpEmissionScale; +#endif +} +#endif diff --git a/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertOptics.hlsl.meta b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertOptics.hlsl.meta new file mode 100644 index 000000000..27d5b5a01 --- /dev/null +++ b/VisualPinball.Unity/Assets/Shaders/Srp/Insert/InsertOptics.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a8e1d31204aa95b40a3e523f0b2b266a +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 73121aa632802aa2dd0164e96ad995534b074d86 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 13 Oct 2026 21:58:26 +0200 Subject: [PATCH 03/26] packaging: use runtime ZIP streams for standalone table reads --- .../Packaging/PackagedContentTests.cs | 42 ++++++++++++ .../Packaging/SharpZipLibStorage.cs | 64 ++++++++++++------- .../Packaging/VpeTableMetadataReader.cs | 16 ++--- 3 files changed, 92 insertions(+), 30 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Test/Packaging/PackagedContentTests.cs b/VisualPinball.Unity/VisualPinball.Unity.Test/Packaging/PackagedContentTests.cs index 2b85f267f..7b7104a07 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Test/Packaging/PackagedContentTests.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Test/Packaging/PackagedContentTests.cs @@ -32,6 +32,47 @@ public void TearDown() } } + [Test] + public void ReadsInterleavedCompressedAndStoredEntries() + { + var package = Path.Combine(_root, "streams.vpe"); + var compressed = Enumerable.Range(0, 65536).Select(index => (byte)(index % 251)).ToArray(); + var stored = Enumerable.Range(0, 65536).Select(index => (byte)(index % 197)).ToArray(); + var manager = new VisualPinball.Unity.Editor.SharpZipStorageManager(); + using (var output = manager.CreateStorage(package)) { + output.AddFile("compressed.bin").SetData(compressed); + output.AddFile("stored.bin").SetData(stored, PackageCompression.Stored); + } + using var input = manager.OpenStorage(package); + Assert.That(input.TryGetFile("compressed.bin", out var compressedFile), Is.True); + Assert.That(input.TryGetFile("stored.bin", out var storedFile), Is.True); + using var first = compressedFile.AsStream(); + using var second = storedFile.AsStream(); + var firstResult = new MemoryStream(); + var secondResult = new MemoryStream(); + var buffer = new byte[113]; + int a, b; + do { + a = first.Read(buffer, 0, buffer.Length); firstResult.Write(buffer, 0, a); + b = second.Read(buffer, 0, buffer.Length); secondResult.Write(buffer, 0, b); + } while (a != 0 || b != 0); + Assert.That(firstResult.ToArray(), Is.EqualTo(compressed)); + Assert.That(secondResult.ToArray(), Is.EqualTo(stored)); + } + + [TestCase(System.IO.Compression.CompressionLevel.Optimal)] + [TestCase(System.IO.Compression.CompressionLevel.NoCompression)] + public void ReadsMetadataFromRuntimeZipArchive(System.IO.Compression.CompressionLevel compression) + { + var package = Path.Combine(_root, "metadata.vpe"); + using (var archive = new System.IO.Compression.ZipArchive(File.Create(package), System.IO.Compression.ZipArchiveMode.Create)) { + using var writer = new StreamWriter(archive.CreateEntry("table/table.json", compression).Open()); + writer.Write("{\"TableName\":\"ZIP fixture\"}"); + } + Assert.That(VpeTableMetadataReader.TryRead(package, out var metadata), Is.True); + Assert.That(metadata.Title, Is.EqualTo("ZIP fixture")); + } + [Test] public async Task RoundTripsNestedTreeInEditorAndRuntimeResolver() { @@ -152,6 +193,7 @@ public async Task StartupRemovesOrphanTempAndUnmarkedFinalIsReplaced() Directory.CreateDirectory(orphan); Directory.CreateDirectory(partial); File.WriteAllText(Path.Combine(orphan, ".complete"), contentRef.ContentHash); + Directory.SetLastWriteTimeUtc(orphan, DateTime.UtcNow.AddDays(-1)); File.WriteAllText(Path.Combine(partial, "partial"), "bad"); var resolver = new PackagedContentResolver(package, CacheOptions(cache)); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Packaging/SharpZipLibStorage.cs b/VisualPinball.Unity/VisualPinball.Unity/Packaging/SharpZipLibStorage.cs index 82be60a3f..aeb4f0e3b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Packaging/SharpZipLibStorage.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Packaging/SharpZipLibStorage.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using ICSharpCode.SharpZipLib.Zip; using NLog; @@ -48,7 +49,8 @@ internal class ZipPackageStorage : IPackageStorage private readonly bool _isWriteMode; private ZipOutputStream _zipOutputStream; - private ZipFile _zipFile; + private ZipArchive _readArchive; + private readonly object _readGate = new object(); // A root folder abstraction to keep track of the top-level. private readonly ZipPackageFolder _rootFolder; @@ -72,11 +74,15 @@ public ZipPackageStorage(string path, bool writeMode) } else { // Open read-only - _zipFile = new ZipFile(File.OpenRead(path)); + // Use the runtime ZIP reader for standalone Mono compatibility. + var input = File.OpenRead(path); + try { _readArchive = new ZipArchive(input, ZipArchiveMode.Read); } + catch { input.Dispose(); throw; } // Build an in-memory tree of folders/files for quick lookup _rootFolder = new ZipPackageFolder("", null, this, false); - BuildFolderTree(); + try { BuildFolderTree(); } + catch { _readArchive.Dispose(); throw; } } } @@ -113,11 +119,7 @@ public void Close() } } else { - // If reading, close the ZipFile - if (_zipFile != null) { - _zipFile.Close(); - _zipFile = null; - } + lock (_readGate) { _readArchive?.Dispose(); _readArchive = null; } } } @@ -131,24 +133,43 @@ public ZipOutputStream GetZipOutputStream() return _zipOutputStream; } - public ZipFile GetZipFile() + internal Stream OpenReadEntry(ZipArchiveEntry entry) { - if (_isWriteMode) { - throw new InvalidOperationException("Cannot get ZipFile in write mode."); + lock (_readGate) { + if (_readArchive == null) throw new ObjectDisposedException(nameof(ZipPackageStorage)); + return new LockedEntryStream(entry.Open(), _readGate); } - return _zipFile; + } + // Entry substreams share the archive's file cursor. Serialize each read/seek operation, not complete file copies. + private sealed class LockedEntryStream : Stream + { + private readonly Stream _inner; + private readonly object _gate; + internal LockedEntryStream(Stream inner, object gate) { _inner = inner; _gate = gate; } + public override bool CanRead => _inner.CanRead; + public override bool CanSeek => _inner.CanSeek; + public override bool CanWrite => false; + public override long Length { get { lock (_gate) return _inner.Length; } } + public override long Position { get { lock (_gate) return _inner.Position; } set { lock (_gate) _inner.Position = value; } } + public override int Read(byte[] buffer, int offset, int count) { lock (_gate) return _inner.Read(buffer, offset, count); } + public override int Read(Span buffer) { lock (_gate) return _inner.Read(buffer); } + public override long Seek(long offset, SeekOrigin origin) { lock (_gate) return _inner.Seek(offset, origin); } + public override void Flush() { } + public override void SetLength(long value) => throw new NotSupportedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + protected override void Dispose(bool disposing) { if (disposing) { lock (_gate) _inner.Dispose(); } base.Dispose(disposing); } } /// - /// In read mode, we build an internal folder/file structure from the existing ZipFile. + /// In read mode, build a folder/file index from the archive directory. /// private void BuildFolderTree() { // Iterate over every entry in the zip and place it into the folder structure - foreach (ZipEntry entry in _zipFile) { + foreach (var entry in _readArchive.Entries) { - var entryName = entry.Name; - var isDirectory = entry.IsDirectory; + var entryName = entry.FullName; + var isDirectory = entryName.EndsWith("/", StringComparison.Ordinal); // Split path by forward slash var parts = entryName.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); @@ -281,7 +302,7 @@ public IPackageFile AddFile(string name, string ext = null) return file; } - internal void AddFileInternal(string name, ZipEntry zipEntry) + internal void AddFileInternal(string name, ZipArchiveEntry zipEntry) { // For read mode, we store a reference to the existing entry if (!_files.TryGetValue(name, out var file)) { @@ -363,7 +384,7 @@ internal class ZipPackageFile : IPackageFile { private readonly ZipPackageFolder _parentFolder; private readonly ZipPackageStorage _storage; - private readonly ZipEntry _existingEntry; // non-null if read-only + private readonly ZipArchiveEntry _existingEntry; // non-null if read-only public ZipPackageFile(string fileName, ZipPackageFolder parentFolder, ZipPackageStorage storage) { @@ -374,7 +395,7 @@ public ZipPackageFile(string fileName, ZipPackageFolder parentFolder, ZipPackage } public ZipPackageFile(string fileName, ZipPackageFolder parentFolder, ZipPackageStorage storage, - ZipEntry existingEntry) + ZipArchiveEntry existingEntry) { Name = fileName; _parentFolder = parentFolder; @@ -387,7 +408,7 @@ public ZipPackageFile(string fileName, ZipPackageFolder parentFolder, ZipPackage public Stream AsStream() { // In write mode, return the ZipOutputStream for this entry. - // In read mode, return the ZipFile's InputStream for this entry. + // In read mode, return a synchronized archive entry stream. if (_storage == null) { throw new InvalidOperationException("Storage reference lost."); } @@ -410,8 +431,7 @@ public Stream AsStream() } // read mode - var zf = _storage.GetZipFile(); - return zf.GetInputStream(_existingEntry); + return _storage.OpenReadEntry(_existingEntry); } public void SetData(byte[] data, PackageCompression compression = PackageCompression.Default) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Packaging/VpeTableMetadataReader.cs b/VisualPinball.Unity/VisualPinball.Unity/Packaging/VpeTableMetadataReader.cs index cd327fb6c..4bba3e6bb 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Packaging/VpeTableMetadataReader.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Packaging/VpeTableMetadataReader.cs @@ -18,9 +18,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using System.Threading.Tasks; -using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; namespace VisualPinball.Unity @@ -78,14 +78,14 @@ public static bool TryRead(string vpePath, out VpeTableMetadataSummary metadata) } try { - using var zip = new ZipFile(File.OpenRead(fileInfo.FullName)); + using var zip = new ZipArchive(File.OpenRead(fileInfo.FullName), ZipArchiveMode.Read); var entry = FindMetadataEntry(zip); if (entry == null) { UpdateCache(fileInfo, false, null); return false; } - using var stream = zip.GetInputStream(entry); + using var stream = entry.Open(); using var reader = new StreamReader(stream); using var jsonReader = new JsonTextReader(reader); var tableMetadata = JsonSerializer.CreateDefault().Deserialize(jsonReader); @@ -186,14 +186,14 @@ private static void UpdateCache(FileInfo fileInfo, bool hasMetadata, VpeTableMet // Reads screenshots/table-bounds.json from the already-open zip and returns the crop aspect // (cropWidth / cropHeight). Null when the entry is missing or unparseable. Cheap — the bounds // file is a handful of bytes, so this adds almost nothing to the metadata read. - private static float? TryReadScreenshotAspect(ZipFile zip) + private static float? TryReadScreenshotAspect(ZipArchive zip) { var entry = zip.GetEntry(ScreenshotBoundsEntryPath); if (entry == null) { return null; } try { - using var stream = zip.GetInputStream(entry); + using var stream = entry.Open(); using var reader = new StreamReader(stream); using var jsonReader = new JsonTextReader(reader); var bounds = JsonSerializer.CreateDefault().Deserialize(jsonReader); @@ -214,15 +214,15 @@ private sealed class ScreenshotBoundsData public int cropHeight; } - private static ZipEntry FindMetadataEntry(ZipFile zip) + private static ZipArchiveEntry FindMetadataEntry(ZipArchive zip) { var exact = zip.GetEntry(TableMetadataEntryPath); if (exact != null) { return exact; } - foreach (ZipEntry entry in zip) { - if (!entry.IsDirectory && string.Equals(entry.Name, TableMetadataEntryPath, StringComparison.OrdinalIgnoreCase)) { + foreach (var entry in zip.Entries) { + if (string.Equals(entry.FullName, TableMetadataEntryPath, StringComparison.OrdinalIgnoreCase)) { return entry; } } From 8f0b3f9038337b3b41739b9a632a0c6084fa1f90 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 13 Oct 2026 21:59:24 +0200 Subject: [PATCH 04/26] plugins: add VPE copyright and MIT source headers --- VisualPinball.Plugins.Test/CapabilityFamilyTests.cs | 7 ++++++- VisualPinball.Plugins.Test/ConfigurationTests.cs | 7 ++++++- VisualPinball.Plugins.Test/PluginHostTests.cs | 7 ++++++- VisualPinball.Plugins.Test/ResourceTests.cs | 7 ++++++- VisualPinball.Plugins/Api/GameIdentityProvider.cs | 7 ++++++- VisualPinball.Plugins/Api/PluginDescriptor.cs | 7 ++++++- VisualPinball.Plugins/Api/PluginRuntime.cs | 7 ++++++- .../Configuration/PluginConfigurationStore.cs | 7 ++++++- VisualPinball.Plugins/Configuration/PluginSchema.cs | 7 ++++++- .../Configuration/PluginSettingsSession.cs | 7 ++++++- VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs | 7 ++++++- VisualPinball.Plugins/Resources/DisplayResourceSource.cs | 7 ++++++- VisualPinball.Plugins/Resources/PluginResourceChoices.cs | 7 ++++++- VisualPinball.Plugins/Resources/PluginResourceRegistry.cs | 7 ++++++- VisualPinball.Plugins/Resources/ResourceContracts.cs | 7 ++++++- VisualPinball.Plugins/Resources/ResourceSignal.cs | 7 ++++++- VisualPinball.Plugins/Runtime/OwnerScheduler.cs | 7 ++++++- VisualPinball.Plugins/Runtime/PluginCapabilities.cs | 7 ++++++- VisualPinball.Plugins/Runtime/PluginEvents.cs | 7 ++++++- VisualPinball.Plugins/Runtime/PluginHost.cs | 7 ++++++- VisualPinball.Plugins/Runtime/PluginInputPipeline.cs | 7 ++++++- VisualPinball.Plugins/Runtime/PluginOrderResolver.cs | 7 ++++++- VisualPinball.Plugins/Runtime/PluginOutputLeases.cs | 7 ++++++- VisualPinball.Plugins/Runtime/ResourceScope.cs | 7 ++++++- .../Plugins/GamelogicPluginResources.cs | 7 ++++++- .../VisualPinball.Unity/Plugins/PackagedGameInformation.cs | 7 ++++++- .../VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs | 7 ++++++- .../VisualPinball.Unity/Plugins/PluginSettingsViews.cs | 7 ++++++- .../VisualPinball.Unity/Plugins/UnityPluginContracts.cs | 7 ++++++- .../VisualPinball.Unity/Plugins/UnityTextureResource.cs | 7 ++++++- 30 files changed, 180 insertions(+), 30 deletions(-) diff --git a/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs b/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs index 3f783dd06..8767833fc 100644 --- a/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs +++ b/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Linq; diff --git a/VisualPinball.Plugins.Test/ConfigurationTests.cs b/VisualPinball.Plugins.Test/ConfigurationTests.cs index 14d126054..082d58387 100644 --- a/VisualPinball.Plugins.Test/ConfigurationTests.cs +++ b/VisualPinball.Plugins.Test/ConfigurationTests.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.IO; diff --git a/VisualPinball.Plugins.Test/PluginHostTests.cs b/VisualPinball.Plugins.Test/PluginHostTests.cs index 27255100c..530064f6e 100644 --- a/VisualPinball.Plugins.Test/PluginHostTests.cs +++ b/VisualPinball.Plugins.Test/PluginHostTests.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.IO; diff --git a/VisualPinball.Plugins.Test/ResourceTests.cs b/VisualPinball.Plugins.Test/ResourceTests.cs index 81b5ffc16..28d204f80 100644 --- a/VisualPinball.Plugins.Test/ResourceTests.cs +++ b/VisualPinball.Plugins.Test/ResourceTests.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using NUnit.Framework; diff --git a/VisualPinball.Plugins/Api/GameIdentityProvider.cs b/VisualPinball.Plugins/Api/GameIdentityProvider.cs index 9bf126a7c..5e838abc5 100644 --- a/VisualPinball.Plugins/Api/GameIdentityProvider.cs +++ b/VisualPinball.Plugins/Api/GameIdentityProvider.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Api/PluginDescriptor.cs b/VisualPinball.Plugins/Api/PluginDescriptor.cs index 6a6781c26..4b45fd603 100644 --- a/VisualPinball.Plugins/Api/PluginDescriptor.cs +++ b/VisualPinball.Plugins/Api/PluginDescriptor.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Api/PluginRuntime.cs b/VisualPinball.Plugins/Api/PluginRuntime.cs index 8bf94ec4c..c471891ad 100644 --- a/VisualPinball.Plugins/Api/PluginRuntime.cs +++ b/VisualPinball.Plugins/Api/PluginRuntime.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Threading; diff --git a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs index 173499ffa..ed2b1fde7 100644 --- a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs +++ b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Globalization; diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs b/VisualPinball.Plugins/Configuration/PluginSchema.cs index 9ac9f58d1..c8dd67b2a 100644 --- a/VisualPinball.Plugins/Configuration/PluginSchema.cs +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Linq; diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs index 4d5faa075..c5efabcc3 100644 --- a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Linq; diff --git a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs index 7fb17f922..3a884fdb7 100644 --- a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs +++ b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; namespace VisualPinball.Plugins diff --git a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs index 407637731..efdb3a781 100644 --- a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs +++ b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Resources/PluginResourceChoices.cs b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs index 37386d6b7..846085ff6 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceChoices.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Linq; diff --git a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs index e6bd5ab35..85cb2f679 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Linq; diff --git a/VisualPinball.Plugins/Resources/ResourceContracts.cs b/VisualPinball.Plugins/Resources/ResourceContracts.cs index d850656c8..12b6afca1 100644 --- a/VisualPinball.Plugins/Resources/ResourceContracts.cs +++ b/VisualPinball.Plugins/Resources/ResourceContracts.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Resources/ResourceSignal.cs b/VisualPinball.Plugins/Resources/ResourceSignal.cs index b332c237f..793524cd9 100644 --- a/VisualPinball.Plugins/Resources/ResourceSignal.cs +++ b/VisualPinball.Plugins/Resources/ResourceSignal.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Threading; diff --git a/VisualPinball.Plugins/Runtime/OwnerScheduler.cs b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs index 5c8e3aeb9..2162493f8 100644 --- a/VisualPinball.Plugins/Runtime/OwnerScheduler.cs +++ b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Concurrent; using System.Runtime.CompilerServices; diff --git a/VisualPinball.Plugins/Runtime/PluginCapabilities.cs b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs index a3cb0fa2f..b39e8b08c 100644 --- a/VisualPinball.Plugins/Runtime/PluginCapabilities.cs +++ b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Runtime/PluginEvents.cs b/VisualPinball.Plugins/Runtime/PluginEvents.cs index 125bd305f..40e65a0fc 100644 --- a/VisualPinball.Plugins/Runtime/PluginEvents.cs +++ b/VisualPinball.Plugins/Runtime/PluginEvents.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Threading; diff --git a/VisualPinball.Plugins/Runtime/PluginHost.cs b/VisualPinball.Plugins/Runtime/PluginHost.cs index edcc967d7..637bcef0c 100644 --- a/VisualPinball.Plugins/Runtime/PluginHost.cs +++ b/VisualPinball.Plugins/Runtime/PluginHost.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Linq; diff --git a/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs index 9f818dd1c..5b2498e29 100644 --- a/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs +++ b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Linq; diff --git a/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs index 47922b2da..b0f471397 100644 --- a/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs +++ b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Linq; diff --git a/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs index f26df4943..128857afb 100644 --- a/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs +++ b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Runtime/ResourceScope.cs b/VisualPinball.Plugins/Runtime/ResourceScope.cs index 086d57de7..d6abef740 100644 --- a/VisualPinball.Plugins/Runtime/ResourceScope.cs +++ b/VisualPinball.Plugins/Runtime/ResourceScope.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs index bc24df3f2..c9913ef5f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs index ca80ef17d..792f7b501 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using System.IO; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs index 30ee52aea..6759c3954 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using UnityEngine; using VisualPinball.Plugins; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs index edf8e337c..93d0393df 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using System.Collections.Generic; using UnityEngine.UIElements; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs index adcab61fc..3fc217790 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using UnityEngine; using VisualPinball.Plugins; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs index 6d0d49d41..b4503b818 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs @@ -1,4 +1,9 @@ -// SPDX-License-Identifier: MIT +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// See LICENSE for the terms of use and distribution. + using System; using UnityEngine; using VisualPinball.Plugins; From 2d6e48608da86675af0201afc2ce4ff6f7c15059 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 13 Oct 2026 22:10:25 +0200 Subject: [PATCH 05/26] plugins: clarify MIT source license references --- VisualPinball.Plugins.Test/CapabilityFamilyTests.cs | 2 +- VisualPinball.Plugins.Test/ConfigurationTests.cs | 2 +- VisualPinball.Plugins.Test/PluginHostTests.cs | 2 +- VisualPinball.Plugins.Test/ResourceTests.cs | 2 +- VisualPinball.Plugins/Api/GameIdentityProvider.cs | 2 +- VisualPinball.Plugins/Api/PluginDescriptor.cs | 2 +- VisualPinball.Plugins/Api/PluginRuntime.cs | 2 +- VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs | 2 +- VisualPinball.Plugins/Configuration/PluginSchema.cs | 2 +- VisualPinball.Plugins/Configuration/PluginSettingsSession.cs | 2 +- VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs | 2 +- VisualPinball.Plugins/Resources/DisplayResourceSource.cs | 2 +- VisualPinball.Plugins/Resources/PluginResourceChoices.cs | 2 +- VisualPinball.Plugins/Resources/PluginResourceRegistry.cs | 2 +- VisualPinball.Plugins/Resources/ResourceContracts.cs | 2 +- VisualPinball.Plugins/Resources/ResourceSignal.cs | 2 +- VisualPinball.Plugins/Runtime/OwnerScheduler.cs | 2 +- VisualPinball.Plugins/Runtime/PluginCapabilities.cs | 2 +- VisualPinball.Plugins/Runtime/PluginEvents.cs | 2 +- VisualPinball.Plugins/Runtime/PluginHost.cs | 2 +- VisualPinball.Plugins/Runtime/PluginInputPipeline.cs | 2 +- VisualPinball.Plugins/Runtime/PluginOrderResolver.cs | 2 +- VisualPinball.Plugins/Runtime/PluginOutputLeases.cs | 2 +- VisualPinball.Plugins/Runtime/ResourceScope.cs | 2 +- .../VisualPinball.Unity/Plugins/GamelogicPluginResources.cs | 2 +- .../VisualPinball.Unity/Plugins/PackagedGameInformation.cs | 2 +- .../VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs | 2 +- .../VisualPinball.Unity/Plugins/PluginSettingsViews.cs | 2 +- .../VisualPinball.Unity/Plugins/UnityPluginContracts.cs | 2 +- .../VisualPinball.Unity/Plugins/UnityTextureResource.cs | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs b/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs index 8767833fc..0214c0fa2 100644 --- a/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs +++ b/VisualPinball.Plugins.Test/CapabilityFamilyTests.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins.Test/ConfigurationTests.cs b/VisualPinball.Plugins.Test/ConfigurationTests.cs index 082d58387..c752e7de3 100644 --- a/VisualPinball.Plugins.Test/ConfigurationTests.cs +++ b/VisualPinball.Plugins.Test/ConfigurationTests.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins.Test/PluginHostTests.cs b/VisualPinball.Plugins.Test/PluginHostTests.cs index 530064f6e..402e411b0 100644 --- a/VisualPinball.Plugins.Test/PluginHostTests.cs +++ b/VisualPinball.Plugins.Test/PluginHostTests.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins.Test/ResourceTests.cs b/VisualPinball.Plugins.Test/ResourceTests.cs index 28d204f80..5f895560e 100644 --- a/VisualPinball.Plugins.Test/ResourceTests.cs +++ b/VisualPinball.Plugins.Test/ResourceTests.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Api/GameIdentityProvider.cs b/VisualPinball.Plugins/Api/GameIdentityProvider.cs index 5e838abc5..95e5c5c9c 100644 --- a/VisualPinball.Plugins/Api/GameIdentityProvider.cs +++ b/VisualPinball.Plugins/Api/GameIdentityProvider.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Api/PluginDescriptor.cs b/VisualPinball.Plugins/Api/PluginDescriptor.cs index 4b45fd603..8e136b2f6 100644 --- a/VisualPinball.Plugins/Api/PluginDescriptor.cs +++ b/VisualPinball.Plugins/Api/PluginDescriptor.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Api/PluginRuntime.cs b/VisualPinball.Plugins/Api/PluginRuntime.cs index c471891ad..2658466dc 100644 --- a/VisualPinball.Plugins/Api/PluginRuntime.cs +++ b/VisualPinball.Plugins/Api/PluginRuntime.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs index ed2b1fde7..1923b039d 100644 --- a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs +++ b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs b/VisualPinball.Plugins/Configuration/PluginSchema.cs index c8dd67b2a..193958b66 100644 --- a/VisualPinball.Plugins/Configuration/PluginSchema.cs +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs index c5efabcc3..73ec0121c 100644 --- a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs index 3a884fdb7..79367ce22 100644 --- a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs +++ b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; diff --git a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs index efdb3a781..1f40905d6 100644 --- a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs +++ b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Resources/PluginResourceChoices.cs b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs index 846085ff6..97fc08724 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceChoices.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Linq; diff --git a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs index 85cb2f679..add1dc228 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Resources/ResourceContracts.cs b/VisualPinball.Plugins/Resources/ResourceContracts.cs index 12b6afca1..4d35fe10b 100644 --- a/VisualPinball.Plugins/Resources/ResourceContracts.cs +++ b/VisualPinball.Plugins/Resources/ResourceContracts.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Resources/ResourceSignal.cs b/VisualPinball.Plugins/Resources/ResourceSignal.cs index 793524cd9..8677fbc48 100644 --- a/VisualPinball.Plugins/Resources/ResourceSignal.cs +++ b/VisualPinball.Plugins/Resources/ResourceSignal.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Runtime/OwnerScheduler.cs b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs index 2162493f8..f715dee5c 100644 --- a/VisualPinball.Plugins/Runtime/OwnerScheduler.cs +++ b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Concurrent; diff --git a/VisualPinball.Plugins/Runtime/PluginCapabilities.cs b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs index b39e8b08c..128b5aa57 100644 --- a/VisualPinball.Plugins/Runtime/PluginCapabilities.cs +++ b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Runtime/PluginEvents.cs b/VisualPinball.Plugins/Runtime/PluginEvents.cs index 40e65a0fc..d98599334 100644 --- a/VisualPinball.Plugins/Runtime/PluginEvents.cs +++ b/VisualPinball.Plugins/Runtime/PluginEvents.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Runtime/PluginHost.cs b/VisualPinball.Plugins/Runtime/PluginHost.cs index 637bcef0c..f57614fac 100644 --- a/VisualPinball.Plugins/Runtime/PluginHost.cs +++ b/VisualPinball.Plugins/Runtime/PluginHost.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs index 5b2498e29..e28f4cb6a 100644 --- a/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs +++ b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs index b0f471397..73a84edac 100644 --- a/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs +++ b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs index 128857afb..fd9908fc6 100644 --- a/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs +++ b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Plugins/Runtime/ResourceScope.cs b/VisualPinball.Plugins/Runtime/ResourceScope.cs index d6abef740..4e7ec9d8f 100644 --- a/VisualPinball.Plugins/Runtime/ResourceScope.cs +++ b/VisualPinball.Plugins/Runtime/ResourceScope.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs index c9913ef5f..fae51778b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs index 792f7b501..8ac086e41 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs index 6759c3954..f26473d4b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using UnityEngine; using VisualPinball.Plugins; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs index 93d0393df..0cfd0888a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using System.Collections.Generic; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs index 3fc217790..bbd9ae1f6 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using UnityEngine; using VisualPinball.Plugins; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs index b4503b818..4cbe9fecb 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs @@ -2,7 +2,7 @@ // Copyright (C) 2026 freezy and VPE Team // // SPDX-License-Identifier: MIT -// See LICENSE for the terms of use and distribution. +// Licensed under MIT. See https://opensource.org/license/mit. using System; using UnityEngine; From 11058ffec1c54c1a1b298411b644826cabd7b802 Mon Sep 17 00:00:00 2001 From: freezy Date: Tue, 13 Oct 2026 23:52:03 +0200 Subject: [PATCH 06/26] plugins: define Player-owned configuration and media contracts --- .../ConfigurationTests.cs | 53 ++++++++++++++++++- .../Configuration/PluginConfigurationStore.cs | 8 +-- .../Configuration/PluginSchema.cs | 47 ++++++++++++++-- .../Configuration/PluginSettingsSession.cs | 35 +++++++++--- VisualPinball.Plugins/README.md | 8 ++- .../Plugins/PlayerIntegrationMode.cs | 1 - .../Plugins/PluginSettingsViews.cs | 46 ---------------- .../Plugins/PluginSettingsViews.cs.meta | 2 - .../Plugins/UnitySettingsMedia.cs | 49 +++++++++++++++++ .../Plugins/UnitySettingsMedia.cs.meta | 2 + 10 files changed, 185 insertions(+), 66 deletions(-) delete mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs delete mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs.meta diff --git a/VisualPinball.Plugins.Test/ConfigurationTests.cs b/VisualPinball.Plugins.Test/ConfigurationTests.cs index c752e7de3..c3fd107cd 100644 --- a/VisualPinball.Plugins.Test/ConfigurationTests.cs +++ b/VisualPinball.Plugins.Test/ConfigurationTests.cs @@ -89,8 +89,57 @@ public async Task ApplyRechecksAvailabilityAndDoesNotReplaceMissingChoice() Assert.That(store.Read(_plugin, Schema(), _game).String("variant"), Is.EqualTo("one")); session.Set("variant", null); Assert.That(session.Effective.String("variant"), Is.EqualTo("auto")); } - private sealed class CatalogProvider : IPluginSettingsProvider - { + [Test] + public void ScreenLayoutsAllowSignedDesktopCoordinatesAndRejectAmbiguousOrInvalidHomes() + { + var schema = new PluginSettingsSchema(1, new PluginSettingsGroup("Displays", "", new PluginField("screens", "Screens", SettingKind.ScreenLayout))); + var valid = JObject.Parse("{'screens':[{'screen':'backglass','x':-1920,'y':0,'width':1920,'height':1080}]}"); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, valid)), Is.Empty); + ((JArray)valid["screens"]!).Add(valid["screens"]![0]!.DeepClone()); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, valid)), Has.Count.EqualTo(1)); + valid["screens"] = JArray.Parse("[{'screen':'backglass','x':0,'y':0,'width':0,'height':1080}]"); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, valid)), Has.Count.EqualTo(1)); + } + [Test] + public void NestedRecordValidationChecksDeclaredTypesAndRanges() + { + var item = new PluginField("devices", "Devices", SettingKind.Collection) { Children = new[] { new PluginField("gain", "Gain", SettingKind.Number) { Minimum = 0, Maximum = 1 } } }; + var schema = new PluginSettingsSchema(1, new PluginSettingsGroup("Output", "", item)); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, JObject.Parse("{'devices':[{'gain':0.5}]}"))), Is.Empty); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, JObject.Parse("{'devices':[{'gain':2.0}]}"))), Has.Count.EqualTo(1)); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, JObject.Parse("{'devices':['invalid']}"))), Has.Count.EqualTo(1)); + } + [Test] + public async Task ScreenApplyRechecksAvailabilityWithoutReplacingAnExistingHome() + { + var field = new PluginField("screens", "Screens", SettingKind.ScreenLayout) { Provider = "variants", Scope = ConfigurationScope.GlobalAndGame }; + var schema = new PluginSettingsSchema(1, new PluginSettingsGroup("Displays", "", field)); + var store = new PluginConfigurationStore(_directory); var provider = new CatalogProvider(); + var factory = new ConfigFactory(_plugin, schema, provider); + using var session = new PluginSettingsSession(factory, store, "windows", _game); + session.Set("screens", JArray.Parse("[{'screen':'one','x':-640,'y':0,'width':640,'height':480}]")); + Assert.That((await session.ApplyAsync()).Success, Is.True); + provider.Available = false; + Assert.That((await session.ApplyAsync()).Success, Is.False); + Assert.That(store.Read(_plugin, schema, _game).Value("screens")![0]!["x"]!.Value(), Is.EqualTo(-640)); + } + [Test] + public async Task NestedChoiceAvailabilityIsRecheckedBeforeSavingRecords() + { + var field = new PluginField("devices", "Devices", SettingKind.Collection) { + Children = new[] { new PluginField("variant", "Variant", SettingKind.Choice) { Provider = "variants", Default = "auto" } } + }; + var schema = new PluginSettingsSchema(1, new PluginSettingsGroup("Output", "", field)); + var store = new PluginConfigurationStore(_directory); var provider = new CatalogProvider(); + using var session = new PluginSettingsSession(new ConfigFactory(_plugin, schema, provider), store, "windows"); + session.Set("devices", JArray.Parse("[{'variant':'one'}]")); + Assert.That((await session.ApplyAsync()).Success, Is.True); + provider.Available = false; + Assert.That((await session.ApplyAsync()).Success, Is.False); + Assert.That(store.Read(_plugin, schema).Value("devices")![0]!["variant"]!.Value(), Is.EqualTo("one")); + } + private sealed class CatalogProvider : IPluginSettingsProvider + { internal bool Available = true; public Task DiscoverAsync(string provider, PluginChoiceRequest request, CancellationToken token) => Task.FromResult(new PluginChoiceCatalog(Available.ToString(), new[] { new PluginChoice("auto", "Automatic"), new PluginChoice("one", "One", available: Available, unavailableReason: "Removed") })); public Task RunActionAsync(string action, PluginChoiceRequest request, CancellationToken token) => Task.FromResult(new PluginActionResult(true, "")); diff --git a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs index 1923b039d..9eb2e31e8 100644 --- a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs +++ b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs @@ -45,8 +45,10 @@ PluginSettingsSnapshot Scoped(PluginSettingsSnapshot values, bool gameScope) if (gameScope ? field.Scope == ConfigurationScope.GlobalOnly : field.Scope == ConfigurationScope.GameOnly) scoped.Values.Remove(field.Id); return scoped; } - return game == null ? PluginSettingsSnapshot.Merge(schema.Defaults(), Scoped(global, false)) - : PluginSettingsSnapshot.Merge(schema.Defaults(), Scoped(global, false), Scoped(game, true)); + var resolved = game == null ? PluginSettingsSnapshot.Merge(schema.Defaults(), Scoped(global, false)) + : PluginSettingsSnapshot.Merge(schema.Defaults(), Scoped(global, false), Scoped(game, true)); + PluginSettingsSchema.FillRecordDefaults(schema.Fields, resolved.Values); + return resolved; } public void Save(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSettingsSnapshot values, GameIdentity? game = null) { @@ -57,7 +59,7 @@ public void Save(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSet var stored = Read(plugin, schema, game).Values; foreach (var field in schema.Fields) { bool allowed = game.HasValue ? field.Scope != ConfigurationScope.GlobalOnly : field.Scope != ConfigurationScope.GameOnly; - if (!allowed || field.Kind == SettingKind.Action || field.Kind == SettingKind.Status || field.Kind == SettingKind.Custom) continue; + if (!allowed || field.Kind == SettingKind.Action || field.Kind == SettingKind.Status || field.Kind == SettingKind.Preview) continue; if (values.Values.TryGetValue(field.Id, out var value)) stored[field.Id] = value.DeepClone(); else stored.Remove(field.Id); } diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs b/VisualPinball.Plugins/Configuration/PluginSchema.cs index 193958b66..7183a94ad 100644 --- a/VisualPinball.Plugins/Configuration/PluginSchema.cs +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs @@ -13,7 +13,7 @@ namespace VisualPinball.Plugins { - public enum SettingKind { Boolean, Integer, Number, Text, Folder, File, Choice, Color, Rectangle, Collection, Status, Action, Custom } + public enum SettingKind { Boolean, Integer, Number, Text, Folder, File, Choice, Color, Rectangle, Collection, Status, Action, Composite, ScreenLayout, Preview } public sealed class PluginField { @@ -27,7 +27,6 @@ public sealed class PluginField public double? Minimum { get; set; } public double? Maximum { get; set; } public string Provider { get; set; } = ""; - public string ViewExtension { get; set; } = ""; public string VisibleWhen { get; set; } = ""; public JToken? VisibleValue { get; set; } public IReadOnlyList Children { get; set; } = Array.Empty(); @@ -79,13 +78,26 @@ public PluginSettingsSnapshot Defaults() { var values = new JObject(); foreach (var field in Fields) if (field.Default != null) values[field.Id] = field.Default.DeepClone(); + FillRecordDefaults(Fields, values); return new PluginSettingsSnapshot(Version, values); } + internal static void FillRecordDefaults(IEnumerable fields, JObject values) + { + foreach (var field in fields) { + if (values[field.Id] == null && field.Default != null) values[field.Id] = field.Default.DeepClone(); + if (field.Kind == SettingKind.Composite) { + if (values[field.Id] == null) values[field.Id] = new JObject(); + if (values[field.Id] is JObject record) FillRecordDefaults(field.Children, record); + } else if (field.Kind == SettingKind.Collection && field.Children.Count > 0 && values[field.Id] is JArray records) { + foreach (var item in records.OfType()) FillRecordDefaults(field.Children, item); + } + } + } public IReadOnlyList Validate(PluginSettingsSnapshot snapshot) { var issues = new List(); foreach (var field in Fields) { - if (!field.Visible(snapshot) || field.Kind == SettingKind.Status || field.Kind == SettingKind.Action || field.Kind == SettingKind.Custom) continue; + if (!field.Visible(snapshot) || field.Kind == SettingKind.Status || field.Kind == SettingKind.Action || field.Kind == SettingKind.Preview) continue; var value = snapshot.Value(field.Id); string? error = field.Validate?.Invoke(value); if (error == null && value != null) { @@ -100,6 +112,20 @@ public IReadOnlyList Validate(PluginSettingsSnapshot snap } } if (field.Kind == SettingKind.Collection && value.Type != JTokenType.Array) error = "Expected a list."; + if (field.Kind == SettingKind.Composite && !(value is JObject)) error = "Expected a settings record."; + if (field.Children.Count > 0 && error == null) { + var nested = new PluginSettingsSchema(Version, new PluginSettingsGroup(field.Label, "", field.Children.ToArray())); + if (field.Kind == SettingKind.Composite && value is JObject record) { + var failures = nested.Validate(new PluginSettingsSnapshot(Version, record)); + if (failures.Count > 0) error = string.Join("; ", failures.Select(issue => issue.Field + ": " + issue.Message)); + } + if (field.Kind == SettingKind.Collection && value is JArray records) foreach (var item in records) { + if (!(item is JObject row)) { error = "Expected a list of settings records."; break; } + var failures = nested.Validate(new PluginSettingsSnapshot(Version, row)); + if (failures.Count > 0) { error = string.Join("; ", failures.Select(issue => issue.Field + ": " + issue.Message)); break; } + } + } + if (field.Kind == SettingKind.ScreenLayout) error = ScreenLayoutError(value); if ((field.Kind == SettingKind.Text || field.Kind == SettingKind.File || field.Kind == SettingKind.Folder || field.Kind == SettingKind.Choice || field.Kind == SettingKind.Color) && value.Type != JTokenType.String) error = "Expected text."; if (field.Kind == SettingKind.Rectangle) { if (!(value is JObject rectangle)) error = "Expected a rectangle."; @@ -113,6 +139,19 @@ public IReadOnlyList Validate(PluginSettingsSnapshot snap } return issues; } + private static string? ScreenLayoutError(JToken value) + { + if (!(value is JArray screens)) return "Expected a screen layout."; + var ids = new HashSet(StringComparer.Ordinal); + foreach (var item in screens) { + if (!(item is JObject screen) || screen["screen"] == null || screen["screen"]!.Type != JTokenType.String && screen["screen"]!.Type != JTokenType.Integer || string.IsNullOrWhiteSpace(screen["screen"]!.ToString()) || !ids.Add(screen["screen"]!.ToString())) return "Each screen needs a unique identifier."; + foreach (string axis in new[] { "x", "y", "width", "height" }) { + var number = screen[axis]; + if (number?.Type != JTokenType.Integer || !int.TryParse(number.ToString(), out int coordinate) || (axis == "width" || axis == "height") && (coordinate < 1 || coordinate > 16384)) return "Use integer coordinates and dimensions from 1 to 16384 pixels."; + } + } + return null; + } } public sealed class PluginSettingsSnapshot @@ -123,7 +162,7 @@ public sealed class PluginSettingsSnapshot public JToken? Value(string id) => Values[id]; public string String(string id, string fallback = "") => Value(id)?.Type == JTokenType.String ? Value(id)!.Value() ?? fallback : fallback; public bool Boolean(string id, bool fallback = false) => Value(id)?.Type == JTokenType.Boolean ? Value(id)!.Value() : fallback; - public int Integer(string id, int fallback = 0) => Value(id)?.Type == JTokenType.Integer ? Value(id)!.Value() : fallback; + public int Integer(string id, int fallback = 0) => Value(id)?.Type == JTokenType.Integer && int.TryParse(Value(id)!.ToString(), System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int number) ? number : fallback; public double Number(string id, double fallback = 0) => Value(id)?.Type == JTokenType.Integer || Value(id)?.Type == JTokenType.Float ? Value(id)!.Value() : fallback; public PluginSettingsSnapshot Copy() => new PluginSettingsSnapshot(SchemaVersion, (JObject)Values.DeepClone()); public void Set(string id, JToken? value) { if (value == null) Values.Remove(id); else Values[id] = value.DeepClone(); } diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs index 73ec0121c..96c6007c9 100644 --- a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs @@ -65,12 +65,20 @@ public async Task ApplyAsync(CancellationToken cancellation var effective = Game.HasValue ? PluginConfigurationStore.Resolve(_factory.Schema, _store.Read(_factory.Descriptor, _factory.Schema), draft) : PluginConfigurationStore.Resolve(_factory.Schema, draft); var request = new PluginChoiceRequest(Game, effective, _platform, _resources()); var issues = _factory.Schema.Validate(effective).Concat(_factory.SettingsProvider?.Validate(request) ?? Array.Empty()).ToList(); - foreach (var field in _factory.Schema.Fields.Where(field => field.Kind == SettingKind.Choice && field.Provider.Length > 0 && field.Visible(effective))) { - string selected = effective.String(field.Id); - if (selected.Length == 0 || _factory.SettingsProvider == null) continue; - var catalog = await _factory.SettingsProvider.DiscoverAsync(field.Provider, request, linked.Token).ConfigureAwait(false); - var choice = catalog.Find(selected); - if (choice != null && !choice.Available || choice == null && catalog.IsComplete) issues.Add(new PluginValidationIssue(field.Id, choice?.UnavailableReason ?? "Selected item is missing.")); + var catalogs = new Dictionary(StringComparer.Ordinal); + foreach (var binding in ChoiceBindings(_factory.Schema.Fields, effective)) { + var field = binding.Field; + var selections = field.Kind == SettingKind.ScreenLayout ? (binding.Value as JArray)?.OfType().Select(screen => screen["screen"]?.ToString() ?? "").ToArray() ?? Array.Empty() : new[] { binding.Value?.ToString() ?? "" }; + if (selections.Length == 0 || _factory.SettingsProvider == null) continue; + if (!catalogs.TryGetValue(field.Provider, out var catalog)) { + catalog = await _factory.SettingsProvider.DiscoverAsync(field.Provider, request, linked.Token).ConfigureAwait(false); + catalogs.Add(field.Provider, catalog); + } + foreach (string selected in selections) { + if (selected.Length == 0) continue; + var choice = catalog.Find(selected); + if (choice != null && !choice.Available || choice == null && catalog.IsComplete) issues.Add(new PluginValidationIssue(binding.Path, choice?.UnavailableReason ?? "Selected item is missing: " + selected)); + } } linked.Token.ThrowIfCancellationRequested(); if (issues.Count > 0) return new PluginActionResult(false, string.Join("\n", issues.Select(issue => issue.Field + ": " + issue.Message))); @@ -79,6 +87,21 @@ public async Task ApplyAsync(CancellationToken cancellation return new PluginActionResult(true, "Saved. Changes take effect on the next game."); } finally { _apply.Release(); } } + private static IEnumerable<(PluginField Field, JToken? Value, string Path)> ChoiceBindings(IEnumerable fields, PluginSettingsSnapshot snapshot, string prefix = "") + { + foreach (var field in fields) { + if (!field.Visible(snapshot)) continue; + var value = snapshot.Value(field.Id); string path = prefix + field.Id; + if ((field.Kind == SettingKind.Choice || field.Kind == SettingKind.ScreenLayout) && field.Provider.Length > 0) yield return (field, value, path); + if (field.Children.Count == 0) continue; + if (field.Kind == SettingKind.Composite && value is JObject record) + foreach (var child in ChoiceBindings(field.Children, new PluginSettingsSnapshot(snapshot.SchemaVersion, record), path + ".")) yield return child; + if (field.Kind == SettingKind.Collection && value is JArray records) for (int index = 0; index < records.Count; index++) { + if (!(records[index] is JObject item)) continue; + foreach (var child in ChoiceBindings(field.Children, new PluginSettingsSnapshot(snapshot.SchemaVersion, item), path + "[" + index + "].")) yield return child; + } + } + } public void Dispose() { _lifetime.Cancel(); } } } diff --git a/VisualPinball.Plugins/README.md b/VisualPinball.Plugins/README.md index bfc2008e9..55f443cdf 100644 --- a/VisualPinball.Plugins/README.md +++ b/VisualPinball.Plugins/README.md @@ -22,7 +22,11 @@ Fields declare GlobalOnly, GameOnly or GlobalAndGame scope and Live, NextGame or Dynamic choices use stable IDs, readable labels, availability reasons and a catalog revision. Missing explicit asset selections remain visible and require user action. A runtime-only catalog may declare `IsComplete = false` while its game is stopped; this keeps an existing source selection without claiming it is available. Explicit display selections are game-specific and controller-relative. `PluginResourceChoices` supplies the shared discovery and resolution rules. -The generic player supports standard fields, lists, rectangles, actions, inheritance, Apply/Revert/Reset, and runtime diagnostics. Unity adapters register custom settings views for richer controls such as desktop placement and pack previews; the player knows only the view contract. Custom views own and release preview resources when hidden or detached. +The Player owns every configuration control, including standard fields, typed records/lists, radio choices, screen placement, previews, inheritance, Apply/Revert/Reset and runtime diagnostics. Plugins declare labels, types, defaults, scope, validation and dynamic providers through `PluginField`; they do not supply UIElements, templates, styles or configuration shaders. + +`SettingKind.ScreenLayout` describes an array of homes with a stable `screen` identifier and integer `x`, `y`, `width`, `height` in physical desktop pixels. Signed coordinates support multiple monitors. Its `Provider` discovers the available screen IDs and names; Apply rechecks their availability. String IDs are preferred; legacy integer IDs remain accepted. Native placement creates a sparse game override when an inherited home is moved. The shared Player renders aspect-ratio guides against the full window bounds. `IUnitySettingsWindowProvider` adapts a plugin's native output transport; the Player owns the placeholder pixels and controls. + +`SettingKind.Preview` declares a preview provider without persisting UI state. `IUnitySettingsPreviewProvider` returns an owned preview with textures, normalized layer rectangles, visibility and canvas dimensions through VPE-owned contracts. The Player owns the viewport, buttons and presentation. Partial startup must be released on cancellation; hiding a settings page disposes its preview. Composite fields and collection records use `Children` to declare nested data controls, reusing the same Player rendering. ## Cross-plugin resources @@ -34,6 +38,6 @@ Display pixels, PCM samples and segment buffers are borrowed during synchronous ## Package composition and validation -`VisualPinball.Plugins` contains contracts, configuration and the host. `VisualPinball.Unity/VisualPinball.Unity/Plugins` bridges scene identities and gamelogic resources. The shared player renders configuration and owns transitions. Optional PinUp and DMD player packages own all product-specific discovery, runtime bindings and custom UI. HDRP/URP shells choose these packages in their manifests. +`VisualPinball.Plugins` contains contracts, configuration and the host. `VisualPinball.Unity/VisualPinball.Unity/Plugins` bridges scene identities, gamelogic resources and settings media transports. The shared Player renders configuration and owns transitions. Each integration's normal Unity package contains its settings schema, discovery providers and runtime plugin assembly. There is no additional Player package for PinUp or DMD. HDRP/URP shells choose optional integration packages in their manifests; shared Player code has no dependency on them. The portable suite covers failure isolation, ordering, cancellation/deadlines, sparse profiles, migrations, future versions, missing choices, raw/presentation routing, generation changes, output exclusivity, audio replacement, input ordering and representative resource families. Native desktop and hardware behavior must also be tested on the target platform; portable tests do not validate a Metal compositor, Linux desktop or attached cabinet device. diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs index f26473d4b..0a20afe5f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs @@ -20,7 +20,6 @@ private static void Reset() HostManaged = false; PlayerPluginRegistry.Reset(); PackagedGameIdentityReaders.Reset(); - PluginSettingsViews.Reset(); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs deleted file mode 100644 index 0cfd0888a..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Visual Pinball Engine -// Copyright (C) 2026 freezy and VPE Team -// -// SPDX-License-Identifier: MIT -// Licensed under MIT. See https://opensource.org/license/mit. - -using System; -using System.Collections.Generic; -using UnityEngine.UIElements; -using VisualPinball.Plugins; - -namespace VisualPinball.Unity -{ - public sealed class PluginSettingsViewContext - { - public PluginSettingsSession Session { get; } - public PluginField Field { get; } - public PluginHost Host { get; } - public Action Changed { get; } - public PluginSettingsViewContext(PluginSettingsSession session, PluginField field, PluginHost host, Action changed) - { Session = session; Field = field; Host = host; Changed = changed; } - } - public interface IPluginSettingsViewFactory - { - string PluginId { get; } - string WidgetId { get; } - IDisposable Build(VisualElement parent, PluginSettingsViewContext context); - } - public readonly struct PluginSettingsNavigationItem - { - public VisualElement Element { get; } - public Action Activate { get; } - public PluginSettingsNavigationItem(VisualElement element, Action activate) { Element = element; Activate = activate; } - } - public interface IPluginSettingsNavigation - { - IReadOnlyList Navigation { get; } - } - public static class PluginSettingsViews - { - private static readonly Dictionary Factories = new Dictionary(StringComparer.Ordinal); - public static void Reset() => Factories.Clear(); - public static void Register(IPluginSettingsViewFactory factory) => Factories.Add(factory.PluginId + "/" + factory.WidgetId, factory); - public static IPluginSettingsViewFactory Find(string pluginId, string widgetId) => Factories.TryGetValue(pluginId + "/" + widgetId, out var factory) ? factory : null; - } -} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs.meta deleted file mode 100644 index 1c6e6649e..000000000 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PluginSettingsViews.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c03850435cdac1044b09acea2b4434eb \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs new file mode 100644 index 000000000..265e33f3d --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs @@ -0,0 +1,49 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using VisualPinball.Plugins; + +namespace VisualPinball.Unity +{ + /// Native output transport. The Player owns placement controls and test-pattern rendering. + public interface IUnitySettingsWindow : IDisposable + { + RectInt Bounds { get; } + void SetBounds(RectInt bounds); + void SetTexture(Texture texture); + void Activate(); + } + public interface IUnitySettingsWindowProvider + { + bool Supported { get; } + IUnitySettingsWindow OpenWindow(RectInt bounds); + } + /// Texture and normalized placement data; no UI objects or styling cross this boundary. + public sealed class UnitySettingsPreviewLayer + { + public string Id { get; } + public Texture Texture { get; set; } + public Rect Rectangle { get; set; } + public bool Visible { get; set; } + public UnitySettingsPreviewLayer(string id) { Id = id; } + } + public interface IUnitySettingsPreview : IDisposable + { + Vector2 CanvasSize { get; } + IReadOnlyList Layers { get; } + void Update(); + } + public interface IUnitySettingsPreviewProvider + { + /// Called on Unity's owning thread. Return an owned preview; cancellation must release partial startup. + Task OpenPreviewAsync(string provider, PluginChoiceRequest request, CancellationToken cancellation); + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs.meta new file mode 100644 index 000000000..5eddbb1e9 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e18b52b309b87e049b69724534e8b760 \ No newline at end of file From 3215077de2759d9a9abf43f65d943e192ee73e29 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 01:08:27 +0200 Subject: [PATCH 07/26] plugins: expose compact configuration layout hints --- VisualPinball.Plugins/Configuration/PluginSchema.cs | 4 ++++ VisualPinball.Plugins/README.md | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs b/VisualPinball.Plugins/Configuration/PluginSchema.cs index 7183a94ad..d7ad14134 100644 --- a/VisualPinball.Plugins/Configuration/PluginSchema.cs +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs @@ -21,6 +21,10 @@ public sealed class PluginField public string Label { get; } public SettingKind Kind { get; } public string Description { get; set; } = ""; + /// Places the label above the editor for long values; ordinary fields render inline. + public bool FullWidth { get; set; } + /// Consecutive fields with the same non-empty hint share a row. The Player owns their sizing and appearance. + public string Row { get; set; } = ""; public ConfigurationScope Scope { get; set; } = ConfigurationScope.GlobalOnly; public ConfigurationChange Change { get; set; } = ConfigurationChange.NextGame; public JToken? Default { get; set; } diff --git a/VisualPinball.Plugins/README.md b/VisualPinball.Plugins/README.md index 55f443cdf..48144c179 100644 --- a/VisualPinball.Plugins/README.md +++ b/VisualPinball.Plugins/README.md @@ -22,7 +22,9 @@ Fields declare GlobalOnly, GameOnly or GlobalAndGame scope and Live, NextGame or Dynamic choices use stable IDs, readable labels, availability reasons and a catalog revision. Missing explicit asset selections remain visible and require user action. A runtime-only catalog may declare `IsComplete = false` while its game is stopped; this keeps an existing source selection without claiming it is available. Explicit display selections are game-specific and controller-relative. `PluginResourceChoices` supplies the shared discovery and resolution rules. -The Player owns every configuration control, including standard fields, typed records/lists, radio choices, screen placement, previews, inheritance, Apply/Revert/Reset and runtime diagnostics. Plugins declare labels, types, defaults, scope, validation and dynamic providers through `PluginField`; they do not supply UIElements, templates, styles or configuration shaders. +The Player owns every configuration control, including standard fields, typed records/lists, choice buttons, screen placement, previews, inheritance, Apply/Revert/Reset and runtime diagnostics. Plugins declare labels, types, defaults, scope, validation and dynamic providers through `PluginField`; they do not supply UIElements, templates, styles or configuration shaders. + +Ordinary fields place their label and editor on one line. `PluginField.FullWidth` requests a label above a wide editor, for example a PAC key or library path. Consecutive fields with the same non-empty `Row` hint share a line, such as left/top or width/height. These are presentation hints; they neither change saved values nor require a schema migration. The Player owns spacing, styling and the concrete controls. `SettingKind.ScreenLayout` describes an array of homes with a stable `screen` identifier and integer `x`, `y`, `width`, `height` in physical desktop pixels. Signed coordinates support multiple monitors. Its `Provider` discovers the available screen IDs and names; Apply rechecks their availability. String IDs are preferred; legacy integer IDs remain accepted. Native placement creates a sparse game override when an inherited home is moved. The shared Player renders aspect-ratio guides against the full window bounds. `IUnitySettingsWindowProvider` adapts a plugin's native output transport; the Player owns the placeholder pixels and controls. From 658694a12f4ade88cadcaba2987f01eb2dae3476 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 02:35:31 +0200 Subject: [PATCH 08/26] plugins: expose shared global settings and mechanical sound gain --- .../GlobalSettingsTests.cs | 106 +++++++++++++++++ .../GlobalSettingsTests.cs.meta | 2 + VisualPinball.Plugins.Test/PluginHostTests.cs | 17 ++- VisualPinball.Plugins/Api/PluginDescriptor.cs | 2 + VisualPinball.Plugins/Api/PluginRuntime.cs | 2 + .../Configuration/PluginConfigurationStore.cs | 35 +++++- .../Configuration/PluginGlobalSetting.cs | 53 +++++++++ .../Configuration/PluginGlobalSetting.cs.meta | 2 + .../Configuration/PluginSchema.cs | 4 + .../Configuration/PluginSettingsSession.cs | 44 +++++-- VisualPinball.Plugins/README.md | 6 + .../Runtime/PluginHost.Settings.cs | 110 ++++++++++++++++++ .../Runtime/PluginHost.Settings.cs.meta | 2 + VisualPinball.Plugins/Runtime/PluginHost.cs | 57 +-------- .../Sound/MechanicalSoundGain.cs | 53 +++++++++ .../Sound/MechanicalSoundGain.cs.meta | 2 + .../Sound/SoundEffectAsset.cs | 14 ++- 17 files changed, 439 insertions(+), 72 deletions(-) create mode 100644 VisualPinball.Plugins.Test/GlobalSettingsTests.cs create mode 100644 VisualPinball.Plugins.Test/GlobalSettingsTests.cs.meta create mode 100644 VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs create mode 100644 VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs.meta create mode 100644 VisualPinball.Plugins/Runtime/PluginHost.Settings.cs create mode 100644 VisualPinball.Plugins/Runtime/PluginHost.Settings.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Sound/MechanicalSoundGain.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Sound/MechanicalSoundGain.cs.meta diff --git a/VisualPinball.Plugins.Test/GlobalSettingsTests.cs b/VisualPinball.Plugins.Test/GlobalSettingsTests.cs new file mode 100644 index 000000000..ff7a76d3e --- /dev/null +++ b/VisualPinball.Plugins.Test/GlobalSettingsTests.cs @@ -0,0 +1,106 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +namespace VisualPinball.Plugins.Test +{ + [TestFixture] + public sealed class GlobalSettingsTests + { + private string _directory = null!; + private PluginConfigurationStore _store = null!; + [SetUp] public void SetUp() { _directory = Path.Combine(Path.GetTempPath(), "vpe-mixer-tests", Guid.NewGuid().ToString("N")); _store = new PluginConfigurationStore(_directory); } + [TearDown] public void TearDown() { if (Directory.Exists(_directory)) Directory.Delete(_directory, true); } + private static PluginField Volume(string id = "value") => new PluginField(id, "Volume", SettingKind.Integer) { + Default = 100, Minimum = 0, Maximum = 100, Change = ConfigurationChange.Live, GlobalSetting = new PluginGlobalSetting(PluginGlobalSections.VolumeMixer) + }; + private static TestFactory Factory(string id = "sound") => new TestFactory(id) { + Schema = new PluginSettingsSchema(1, new PluginSettingsGroup("Sound", "", Volume(), new PluginField("path", "Library", SettingKind.Text) { Default = "old" })) + }; + private PluginHost Host(params IPlayerPluginFactory[] factories) => new PluginHost(factories, _store, new OwnerScheduler(), new PluginCapabilities(), new PluginResourceRegistry(), "windows"); + [Test] + public void OnlyExplicitSupportedVisibleAndCompatibleFieldsAppear() + { + var first = Factory("first"); first.Schema.Fields.First().GlobalSetting!.Order = 20; + var second = Factory("second"); second.Schema.Fields.First().GlobalSetting!.Order = 10; + var unsupported = Factory("unsupported"); unsupported.Descriptor.Platforms = new[] { "linux" }; + var game = Factory("game"); game.Schema.Fields.First().Scope = ConfigurationScope.GameOnly; + var unbounded = Factory("unbounded"); unbounded.Schema.Fields.First().Maximum = null; + var absent = new TestFactory("absent"); var broken = Factory("broken"); + Directory.CreateDirectory(_store.DirectoryFor(broken.Descriptor)); File.WriteAllText(_store.SettingsPath(broken.Descriptor), "bad json"); + var diagnostics = new List(); + var found = PluginGlobalSettingBinding.Discover(new[] { first, second, unsupported, game, unbounded, absent, broken }, _store, "windows", PluginGlobalSections.VolumeMixer, diagnostics.Add); + Assert.That(found.Select(item => item.Id), Is.EqualTo(new[] { "second/value", "first/value" })); + Assert.That(diagnostics.Count, Is.EqualTo(3)); + Assert.That(PluginGlobalSettingBinding.Discover(new[] { first }, _store, "windows", "unknown"), Is.Empty); + } + [Test] + public void OrdinaryDraftCannotOverwriteANewerMixerValueAndResetIsExplicit() + { + var factory = Factory(); var host = Host(factory); + using var session = new PluginSettingsSession(factory, _store, "windows"); + session.Set("path", "new"); + PluginHostTests.Run(host, host.CommitGlobalSettingAsync(factory, "value", new JValue(37))); + var apply = session.ApplyAsync(host: host); PluginHostTests.Run(host, apply); + Assert.That(apply.Result.Success, Is.True); + Assert.That(_store.Effective(factory.Descriptor, factory.Schema).Integer("value"), Is.EqualTo(37)); + Assert.That(_store.Effective(factory.Descriptor, factory.Schema).String("path"), Is.EqualTo("new")); + session.Reset(); PluginHostTests.Run(host, session.ApplyAsync(host: host)); + Assert.That(_store.Read(factory.Descriptor, factory.Schema).Values, Is.Empty); + Assert.That(_store.Effective(factory.Descriptor, factory.Schema).Integer("value"), Is.EqualTo(100)); + } + [Test] + public void AliasRefreshKeepsDirtyFieldsAndInvalidValuesAreNeverPersisted() + { + var factory = Factory(); var host = Host(factory); + using var session = new PluginSettingsSession(factory, _store, "windows"); + session.Set("path", "draft"); + var latest = new PluginSettingsSnapshot(1); latest.Set("value", 30); latest.Set("path", "external"); + session.RefreshUnedited(latest, new[] { "value", "path" }); + Assert.That(session.Effective.Integer("value"), Is.EqualTo(30)); + Assert.That(session.HasEdits("value"), Is.False); Assert.That(session.Draft.String("path"), Is.EqualTo("draft")); + var invalid = host.CommitGlobalSettingAsync(factory, "value", new JValue(101)); PluginHostTests.Run(host, invalid); + Assert.That(invalid.Result.Success, Is.False); Assert.That(File.Exists(_store.SettingsPath(factory.Descriptor)), Is.False); + } + [Test] + public void MandatorySettingsServiceIgnoresLegacyDisablePolicies() + { + var factory = Factory(); factory.Descriptor.CanDisable = false; + Directory.CreateDirectory(_store.DirectoryFor(factory.Descriptor)); File.WriteAllText(Path.Combine(_store.DirectoryFor(factory.Descriptor), "policy.json"), "{'enabled':false}"); + Assert.That(_store.Enabled(factory.Descriptor), Is.True); + Assert.Throws(() => _store.SetEnabled(factory.Descriptor, false)); + var host = Host(factory); PluginHostTests.Run(host, host.PrepareAndStartAsync(new GameIdentity("test", "game"), "test", "Test", "controller", 1, default)); + Assert.That(factory.Constructions, Is.EqualTo(1)); PluginHostTests.Run(host, host.StopGameAsync()); + } + [Test] + public void FailedDiskReplaceRollsBackGainWithoutSuppressingTicks() + { + if (!System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) Assert.Ignore("Windows file-sharing semantics are required for this persistence fault injection."); + var factory = Factory(); factory.SynchronousApply = true; + var initial = new PluginSettingsSnapshot(1); initial.Set("value", 5); _store.Save(factory.Descriptor, factory.Schema, initial); + var host = Host(factory); PluginHostTests.Run(host, host.PrepareAndStartAsync(new GameIdentity("test", "game"), "test", "Test", "controller", 1, default)); + FileStream? held = null; + factory.ApplyAction = level => { if (level == 42) held = new FileStream(_store.SettingsPath(factory.Descriptor), FileMode.Open, FileAccess.Read, FileShare.Read); }; + var apply = host.CommitGlobalSettingAsync(factory, "value", new JValue(42)); + try { + var deadline = DateTime.UtcNow.AddSeconds(5); + while (!apply.IsCompleted && DateTime.UtcNow < deadline) { host.Scheduler.Pump(); if (held != null) host.Tick(.016); Thread.Sleep(1); } + PluginHostTests.Run(host, apply); + Assert.That(apply.Result.Success, Is.False); Assert.That(factory.Calls, Does.Contain("sound:apply:5")); + Assert.That(factory.Calls, Does.Contain("sound:tick")); + Assert.That(_store.Effective(factory.Descriptor, factory.Schema).Integer("value"), Is.EqualTo(5)); + } finally { held?.Dispose(); PluginHostTests.Run(host, host.StopGameAsync()); } + } + } +} diff --git a/VisualPinball.Plugins.Test/GlobalSettingsTests.cs.meta b/VisualPinball.Plugins.Test/GlobalSettingsTests.cs.meta new file mode 100644 index 000000000..dcdd925f8 --- /dev/null +++ b/VisualPinball.Plugins.Test/GlobalSettingsTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4bb5bd368acbd12459ffd17b38e8fc2b \ No newline at end of file diff --git a/VisualPinball.Plugins.Test/PluginHostTests.cs b/VisualPinball.Plugins.Test/PluginHostTests.cs index 402e411b0..4f66f23fd 100644 --- a/VisualPinball.Plugins.Test/PluginHostTests.cs +++ b/VisualPinball.Plugins.Test/PluginHostTests.cs @@ -176,7 +176,7 @@ public void PauseAndResumeDuringFaultCleanupKeepTheLatestState() Run(host, host.StopGameAsync()); } [Test] - public void FailedPersistenceRollsBackLiveSettingsAndPreservesFutureDocuments() + public void FutureDocumentsAreRejectedBeforeTouchingLiveSettings() { var calls = new List(); var factory = new TestFactory("live", calls) { Schema = new PluginSettingsSchema(1, new PluginSettingsGroup("Test", "", @@ -190,7 +190,7 @@ public void FailedPersistenceRollsBackLiveSettingsAndPreservesFutureDocuments() using var session = new PluginSettingsSession(factory, host.Configuration, "windows"); session.Set("value", 42); Assert.Throws(() => Run(host, session.ApplyAsync(host: host))); - Assert.That(calls, Does.Contain("live:apply:42")); Assert.That(calls, Does.Contain("live:apply:5")); + Assert.That(calls, Does.Not.Contain("live:apply:42")); Assert.That(File.ReadAllText(path), Is.EqualTo(future)); Run(host, host.StopGameAsync()); } @@ -247,10 +247,17 @@ internal sealed class TestFactory : IPlayerPluginFactory internal Task? StopCompletion; internal Task? InitializeCompletion; internal Task? ApplyCompletion; + internal Action? ApplyAction; + internal bool SynchronousApply; internal CancellationTokenSource? CancelDuringStart; internal TestFactory(string id, List? calls = null) { Descriptor = new PluginDescriptor(id, id, "Test") { EnabledByDefault = true }; Calls = calls ?? new List(); } - public IPlayerPlugin Create(PluginApplicationContext context) { Constructions++; context.Lifetime.Own(new Subscription(() => Calls.Add(Descriptor.Id + ":release"))); return new TestPlugin(this); } - private sealed class TestPlugin : PlayerPlugin, IPluginReconfiguration, IPluginTick + public IPlayerPlugin Create(PluginApplicationContext context) { Constructions++; context.Lifetime.Own(new Subscription(() => Calls.Add(Descriptor.Id + ":release"))); return SynchronousApply ? new SynchronousTestPlugin(this) : new TestPlugin(this); } + private sealed class SynchronousTestPlugin : TestPlugin, IPluginSynchronousReconfiguration + { + internal SynchronousTestPlugin(TestFactory factory) : base(factory) { } + public PluginActionResult Apply(PluginSettingsSnapshot settings) => ApplyAsync(settings, default).GetAwaiter().GetResult(); + } + private class TestPlugin : PlayerPlugin, IPluginReconfiguration, IPluginTick { private readonly TestFactory _factory; internal TestPlugin(TestFactory factory) { _factory = factory; } @@ -261,7 +268,7 @@ private sealed class TestPlugin : PlayerPlugin, IPluginReconfiguration, IPluginT public override void GameReady() => Call("ready"); public override void PauseChanged(bool paused) => Call(paused ? "pause" : "resume"); public void Tick(double delta) => Call("tick"); - public Task ApplyAsync(PluginSettingsSnapshot settings, CancellationToken cancellation) { Call("apply:" + settings.Integer("value")); return _factory.ApplyCompletion ?? Task.FromResult(new PluginActionResult(true, "")); } + public Task ApplyAsync(PluginSettingsSnapshot settings, CancellationToken cancellation) { Call("apply:" + settings.Integer("value")); _factory.ApplyAction?.Invoke(settings.Integer("value")); return _factory.ApplyCompletion ?? Task.FromResult(new PluginActionResult(true, "")); } public override Task StopGameAsync(CancellationToken token) { _factory.CleanupWasCanceled = token.IsCancellationRequested; Call("stop"); return _factory.StopCompletion ?? (_factory.NeverFinishStop ? new TaskCompletionSource().Task : Task.CompletedTask); } public override Task ShutdownAsync(CancellationToken token) { Call("shutdown"); return Task.CompletedTask; } public override void Dispose() => Call("dispose"); diff --git a/VisualPinball.Plugins/Api/PluginDescriptor.cs b/VisualPinball.Plugins/Api/PluginDescriptor.cs index 8e136b2f6..ef6c95bed 100644 --- a/VisualPinball.Plugins/Api/PluginDescriptor.cs +++ b/VisualPinball.Plugins/Api/PluginDescriptor.cs @@ -30,6 +30,8 @@ public sealed class PluginDescriptor public string Version { get; set; } = "0.1.0"; public int ApiVersion { get; set; } = HostApiVersion; public bool EnabledByDefault { get; set; } + /// False for configuration services required by an application-owned engine. + public bool CanDisable { get; set; } = true; public string Category { get; set; } = "Integrations"; public int Order { get; set; } public IReadOnlyList DependsOn { get; set; } = Array.Empty(); diff --git a/VisualPinball.Plugins/Api/PluginRuntime.cs b/VisualPinball.Plugins/Api/PluginRuntime.cs index 2658466dc..9f4879603 100644 --- a/VisualPinball.Plugins/Api/PluginRuntime.cs +++ b/VisualPinball.Plugins/Api/PluginRuntime.cs @@ -37,6 +37,8 @@ public interface IPluginEmergencyStop { void EmergencyStop(); } public interface IPluginDeferredCleanup { Task Quiescence { get; } } /// Reconfiguration is transactional: failure or cancellation must leave the previous runtime state intact. public interface IPluginReconfiguration { Task ApplyAsync(PluginSettingsSnapshot settings, CancellationToken cancellation); } + /// Fast owner-thread updates. Failure must leave the previous runtime state intact. + public interface IPluginSynchronousReconfiguration { PluginActionResult Apply(PluginSettingsSnapshot settings); } /// Implement only the hooks a plugin needs; the host still controls its lifetime. public abstract class PlayerPlugin : IPlayerPlugin diff --git a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs index 9eb2e31e8..c31dc943d 100644 --- a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs +++ b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs @@ -8,8 +8,10 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Security.Cryptography; using System.Text; +using System.Threading; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -18,6 +20,7 @@ namespace VisualPinball.Plugins /// One plugin owns one folder; game overrides contain only explicitly changed values. public sealed class PluginConfigurationStore { + private readonly object _writes = new object(); public string Root { get; } public PluginConfigurationStore(string root) { Root = Path.GetFullPath(root); } public string DirectoryFor(PluginDescriptor plugin) => Path.Combine(Root, plugin.Id); @@ -51,6 +54,10 @@ PluginSettingsSnapshot Scoped(PluginSettingsSnapshot values, bool gameScope) return resolved; } public void Save(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSettingsSnapshot values, GameIdentity? game = null) + { + lock (_writes) SaveCore(plugin, schema, values, game); + } + private void SaveCore(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSettingsSnapshot values, GameIdentity? game) { string path = SettingsPath(plugin, game); var document = ReadDocument(path); @@ -68,14 +75,35 @@ public void Save(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSet if (game.HasValue) document["game"] = game.Value.Id; WriteDocument(path, document); } + /// Merges a scoped edit into the latest profile without replacing unrelated fields. + public PluginSettingsSnapshot MergePatch(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSettingsSnapshot values, IReadOnlyCollection changed, GameIdentity? game = null) + { + var latest = Read(plugin, schema, game); + if (latest.SchemaVersion > schema.Version) throw new InvalidOperationException("This configuration was written by a newer plugin and cannot be overwritten."); + foreach (var field in schema.Fields) { + if (!changed.Contains(field.Id) || (game.HasValue ? field.Scope == ConfigurationScope.GlobalOnly : field.Scope == ConfigurationScope.GameOnly) + || field.Kind == SettingKind.Action || field.Kind == SettingKind.Status || field.Kind == SettingKind.Preview) continue; + latest.Set(field.Id, values.Value(field.Id)); + } + return latest; + } + public void SavePatch(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSettingsSnapshot values, IReadOnlyCollection changed, GameIdentity? game = null) + { + lock (_writes) SaveCore(plugin, schema, MergePatch(plugin, schema, values, changed, game), game); + } public bool Enabled(PluginDescriptor plugin, GameIdentity? game = null) { + if (!plugin.CanDisable) return true; var policy = ReadDocument(Path.Combine(DirectoryFor(plugin), "policy.json")); if (game.HasValue && policy["games"] is JObject games && games[game.Value.Id]?.Type == JTokenType.Boolean) return games.Value(game.Value.Id); return policy.Value("enabled") ?? plugin.EnabledByDefault; } public void SetEnabled(PluginDescriptor plugin, bool? enabled, GameIdentity? game = null) { + if (!plugin.CanDisable) { + if (enabled == false) throw new InvalidOperationException("This configuration service cannot be disabled."); + return; + } string path = Path.Combine(DirectoryFor(plugin), "policy.json"); var policy = ReadDocument(path); if (game.HasValue) { @@ -90,7 +118,7 @@ public void SetEnabled(PluginDescriptor plugin, bool? enabled, GameIdentity? gam internal static JObject ReadDocument(string path) { if (!File.Exists(path)) return new JObject(); - using var stream = File.OpenText(path); + using var stream = new StreamReader(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)); using var reader = new JsonTextReader(stream) { DateParseHandling = DateParseHandling.None, MaxDepth = 64, FloatParseHandling = FloatParseHandling.Double }; return JObject.Load(reader); } @@ -101,7 +129,10 @@ internal static void WriteDocument(string path, JObject document) try { using (var file = new StreamWriter(pending, false, new UTF8Encoding(false))) using (var writer = new JsonTextWriter(file) { Formatting = Formatting.Indented }) document.WriteTo(writer); - if (File.Exists(path)) File.Replace(pending, path, null); else File.Move(pending, path); + for (int attempt = 0; ; attempt++) { + try { if (File.Exists(path)) File.Replace(pending, path, null); else File.Move(pending, path); break; } + catch (IOException error) when (attempt < 3 && ((error.HResult & 0xffff) == 32 || (error.HResult & 0xffff) == 33)) { Thread.Sleep(20 * (attempt + 1)); } + } } finally { if (File.Exists(pending)) File.Delete(pending); } } public static string Hash(string identity) diff --git a/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs b/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs new file mode 100644 index 000000000..65b305cbc --- /dev/null +++ b/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs @@ -0,0 +1,53 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace VisualPinball.Plugins +{ + public enum PluginFieldPresentation { Input, Slider } + public static class PluginGlobalSections { public const string VolumeMixer = "audio.mixer"; } + public sealed class PluginGlobalSetting + { + public string Section { get; } + public string Label { get; set; } = ""; + public int Order { get; set; } + public PluginGlobalSetting(string section) => Section = section ?? throw new ArgumentNullException(nameof(section)); + } + /// An alias identifies a field in its owning factory, never a second copy of its value. + public sealed class PluginGlobalSettingBinding + { + public IPlayerPluginFactory Factory { get; } + public PluginField Field { get; } + public string Id => Factory.Descriptor.Id + "/" + Field.Id; + public string Label => Field.GlobalSetting!.Label.Length == 0 ? Factory.Descriptor.Name : Field.GlobalSetting.Label; + internal PluginGlobalSettingBinding(IPlayerPluginFactory factory, PluginField field) { Factory = factory; Field = field; } + public static IReadOnlyList Discover(IEnumerable factories, PluginConfigurationStore store, string platform, string section, Action? diagnostic = null) + { + var bindings = new List(); + foreach (var factory in factories) { + if (factory.Descriptor.Platforms.Count > 0 && !factory.Descriptor.Platforms.Contains(platform)) continue; + var fields = factory.Schema.Fields.Where(field => field.GlobalSetting?.Section == section).ToArray(); + if (fields.Length == 0) continue; + PluginSettingsSnapshot effective; + try { effective = store.Effective(factory.Descriptor, factory.Schema); } + catch (Exception error) { diagnostic?.Invoke(factory.Descriptor.Name + ": " + error.Message); continue; } + foreach (var field in fields) { + if (!field.Visible(effective)) continue; + if (field.Scope == ConfigurationScope.GameOnly || section == PluginGlobalSections.VolumeMixer && !BoundedNumber(field)) { + diagnostic?.Invoke("Invalid global setting exposure: " + factory.Descriptor.Id + "/" + field.Id); continue; + } + bindings.Add(new PluginGlobalSettingBinding(factory, field)); + } + } + return bindings.OrderBy(binding => binding.Field.GlobalSetting!.Order).ThenBy(binding => binding.Id, StringComparer.Ordinal).ToArray(); + } + private static bool BoundedNumber(PluginField field) => (field.Kind == SettingKind.Integer || field.Kind == SettingKind.Number) + && field.Minimum.HasValue && field.Maximum.HasValue && double.IsFinite(field.Minimum.Value) && double.IsFinite(field.Maximum.Value) && field.Minimum < field.Maximum; + } +} diff --git a/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs.meta b/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs.meta new file mode 100644 index 000000000..9eaabc950 --- /dev/null +++ b/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2b444ea492aa2e146943444dafa74ead \ No newline at end of file diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs b/VisualPinball.Plugins/Configuration/PluginSchema.cs index d7ad14134..21d8fe46e 100644 --- a/VisualPinball.Plugins/Configuration/PluginSchema.cs +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs @@ -25,6 +25,10 @@ public sealed class PluginField public bool FullWidth { get; set; } /// Consecutive fields with the same non-empty hint share a row. The Player owns their sizing and appearance. public string Row { get; set; } = ""; + public PluginFieldPresentation Presentation { get; set; } + public string Unit { get; set; } = ""; + /// Opt-in appearance on a shared Player page; storage still belongs to this plugin. + public PluginGlobalSetting? GlobalSetting { get; set; } public ConfigurationScope Scope { get; set; } = ConfigurationScope.GlobalOnly; public ConfigurationChange Change { get; set; } = ConfigurationChange.NextGame; public JToken? Default { get; set; } diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs index 96c6007c9..dff17e1fd 100644 --- a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs @@ -22,21 +22,39 @@ public sealed class PluginSettingsSession : IDisposable private readonly Func> _resources; private readonly SemaphoreSlim _apply = new SemaphoreSlim(1, 1); private readonly CancellationTokenSource _lifetime = new CancellationTokenSource(); + private PluginSettingsSnapshot _baseline; + private readonly HashSet _resetFields = new HashSet(StringComparer.Ordinal); + public bool HasEdits(string field) => _resetFields.Contains(field) || !JToken.DeepEquals(_baseline.Value(field), Draft.Value(field)); + private string[] ChangedFields() => _factory.Schema.Fields.Where(field => HasEdits(field.Id)).Select(field => field.Id).ToArray(); public GameIdentity? Game { get; } public PluginSettingsSnapshot Draft { get; private set; } public PluginSettingsSnapshot Effective => Game.HasValue ? PluginConfigurationStore.Resolve(_factory.Schema, _store.Read(_factory.Descriptor, _factory.Schema), Draft) : PluginConfigurationStore.Resolve(_factory.Schema, Draft); public PluginSettingsSession(IPlayerPluginFactory factory, PluginConfigurationStore store, string platform, GameIdentity? game = null, Func>? resources = null) - { _factory = factory; _store = store; _platform = platform; Game = game; _resources = resources ?? (() => Array.Empty()); Draft = store.Read(factory.Descriptor, factory.Schema, game); } + { _factory = factory; _store = store; _platform = platform; Game = game; _resources = resources ?? (() => Array.Empty()); Draft = store.Read(factory.Descriptor, factory.Schema, game); _baseline = Draft.Copy(); } public void Set(string fieldId, JToken? value) { var field = _factory.Schema.Fields.First(field => field.Id == fieldId); if (Game.HasValue ? field.Scope == ConfigurationScope.GlobalOnly : field.Scope == ConfigurationScope.GameOnly) throw new InvalidOperationException("This field is not configurable in the selected scope."); Draft.Set(fieldId, value); + _resetFields.Remove(fieldId); + } + public void Revert() { Draft = _store.Read(_factory.Descriptor, _factory.Schema, Game); _baseline = Draft.Copy(); _resetFields.Clear(); } + /// Refreshes aliases without discarding edits already made in this draft. + public void RefreshUnedited(PluginSettingsSnapshot saved, IEnumerable fields) + { + foreach (string id in fields) { + if (HasEdits(id)) continue; + Draft.Set(id, saved.Value(id)); _baseline.Set(id, saved.Value(id)); + } + } + public void Reset() + { + Draft = new PluginSettingsSnapshot(_factory.Schema.Version); + foreach (var field in _factory.Schema.Fields) + if (Game.HasValue ? field.Scope != ConfigurationScope.GlobalOnly : field.Scope != ConfigurationScope.GameOnly) _resetFields.Add(field.Id); } - public void Revert() => Draft = _store.Read(_factory.Descriptor, _factory.Schema, Game); - public void Reset() => Draft = new PluginSettingsSnapshot(_factory.Schema.Version); public IReadOnlyList Validate() { var effective = Effective; @@ -62,8 +80,14 @@ public async Task ApplyAsync(CancellationToken cancellation try { // Freeze the draft before any asynchronous discovery so editing cannot change the validated transaction. var draft = Draft.Copy(); - var effective = Game.HasValue ? PluginConfigurationStore.Resolve(_factory.Schema, _store.Read(_factory.Descriptor, _factory.Schema), draft) : PluginConfigurationStore.Resolve(_factory.Schema, draft); - var request = new PluginChoiceRequest(Game, effective, _platform, _resources()); + var changed = ChangedFields(); + var resources = _resources(); + var effective = await Task.Run(() => { + var latest = _store.MergePatch(_factory.Descriptor, _factory.Schema, draft, changed, Game); + return Game.HasValue ? PluginConfigurationStore.Resolve(_factory.Schema, _store.Read(_factory.Descriptor, _factory.Schema), latest) : PluginConfigurationStore.Resolve(_factory.Schema, latest); + }, linked.Token).ConfigureAwait(false); + if (host != null) await host.Scheduler.Switch(); + var request = new PluginChoiceRequest(Game, effective, _platform, resources); var issues = _factory.Schema.Validate(effective).Concat(_factory.SettingsProvider?.Validate(request) ?? Array.Empty()).ToList(); var catalogs = new Dictionary(StringComparer.Ordinal); foreach (var binding in ChoiceBindings(_factory.Schema.Fields, effective)) { @@ -82,8 +106,14 @@ public async Task ApplyAsync(CancellationToken cancellation } linked.Token.ThrowIfCancellationRequested(); if (issues.Count > 0) return new PluginActionResult(false, string.Join("\n", issues.Select(issue => issue.Field + ": " + issue.Message))); - if (host != null) return await host.CommitSettingsAsync(_factory, draft, Game, linked.Token).ConfigureAwait(false); - _store.Save(_factory.Descriptor, _factory.Schema, draft, Game); + if (host != null) { + var result = await host.CommitSettingsAsync(_factory, draft, Game, linked.Token, changed).ConfigureAwait(false); + await host.Scheduler.Switch(); + if (result.Success) Revert(); + return result; + } + await Task.Run(() => _store.SavePatch(_factory.Descriptor, _factory.Schema, draft, changed, Game)).ConfigureAwait(false); + Revert(); return new PluginActionResult(true, "Saved. Changes take effect on the next game."); } finally { _apply.Release(); } } diff --git a/VisualPinball.Plugins/README.md b/VisualPinball.Plugins/README.md index 48144c179..1846f8d12 100644 --- a/VisualPinball.Plugins/README.md +++ b/VisualPinball.Plugins/README.md @@ -26,6 +26,12 @@ The Player owns every configuration control, including standard fields, typed re Ordinary fields place their label and editor on one line. `PluginField.FullWidth` requests a label above a wide editor, for example a PAC key or library path. Consecutive fields with the same non-empty `Row` hint share a line, such as left/top or width/height. These are presentation hints; they neither change saved values nor require a schema migration. The Player owns spacing, styling and the concrete controls. +An optional `PluginField.GlobalSetting` exposes the same canonical field on a shared Player page. This is independent of its storage scope: `GlobalOnly` alone does not expose a field. The first section, `PluginGlobalSections.VolumeMixer` (`audio.mixer`), accepts bounded numeric fields with a global scope. The Player discovers installed, supported integrations, orders their entries by the exposure's `Order`, and supplies the controls. `Presentation = PluginFieldPresentation.Slider` and `Unit = "%"` request the same numeric slider in the ordinary plugin form. A `GameOnly` field cannot be globally exposed. Unknown sections are ignored; invalid exposures and unreadable profiles are reported without removing other integrations from the mixer. + +The mixer commits individual field patches through the host. Ordinary plugin drafts also save only fields edited since opening, merged into the latest profile before validation, so an open form cannot overwrite a newer mixer value accidentally. Reset explicitly removes the scoped values. `SettingsCommitted` lets a view refresh unedited fields while preserving its draft. The application owns and coalesces mixer writes; leaving a page flushes them, and game preparation waits for that flush. There is no second copy of plugin volume in Player preferences. + +Implement `IPluginSynchronousReconfiguration` for fast owner-thread updates such as gain changes; the host applies these without suspending tick/render callbacks. The asynchronous interface remains available for operations that need it, suspending only the target integration during its live hook. Persistence runs on a worker, preserves tick/render, and cannot be cancelled after its atomic write begins. A failed save restores the previous live settings. `PluginDescriptor.CanDisable = false` is intended for settings adapters attached to an existing gamelogic engine: legacy disable policies are ignored and the Player hides disable controls. It does not move ownership of the engine into the plugin host. + `SettingKind.ScreenLayout` describes an array of homes with a stable `screen` identifier and integer `x`, `y`, `width`, `height` in physical desktop pixels. Signed coordinates support multiple monitors. Its `Provider` discovers the available screen IDs and names; Apply rechecks their availability. String IDs are preferred; legacy integer IDs remain accepted. Native placement creates a sparse game override when an inherited home is moved. The shared Player renders aspect-ratio guides against the full window bounds. `IUnitySettingsWindowProvider` adapts a plugin's native output transport; the Player owns the placeholder pixels and controls. `SettingKind.Preview` declares a preview provider without persisting UI state. `IUnitySettingsPreviewProvider` returns an owned preview with textures, normalized layer rectangles, visibility and canvas dimensions through VPE-owned contracts. The Player owns the viewport, buttons and presentation. Partial startup must be released on cancellation; hiding a settings page disposes its preview. Composite fields and collection records use `Children` to declare nested data controls, reusing the same Player rendering. diff --git a/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs b/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs new file mode 100644 index 000000000..c34452688 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs @@ -0,0 +1,110 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace VisualPinball.Plugins +{ + public sealed partial class PluginHost + { + public event Action, GameIdentity?>? SettingsCommitted; + /// Edits one canonical global field. Waiting for game startup does not discard a mixer edit. + public Task CommitGlobalSettingAsync(IPlayerPluginFactory factory, string fieldId, JToken value, CancellationToken cancellation = default) + { + var field = factory.Schema.Fields.FirstOrDefault(item => item.Id == fieldId); + if (field?.GlobalSetting == null || field.Scope == ConfigurationScope.GameOnly) throw new ArgumentException("The field is not exposed as a global setting."); + var patch = new PluginSettingsSnapshot(factory.Schema.Version); patch.Set(fieldId, value); + return CommitSettingsCoreAsync(factory, patch, null, cancellation, new[] { fieldId }, field); + } + private async Task CommitSettingsCoreAsync(IPlayerPluginFactory factory, PluginSettingsSnapshot draft, GameIdentity? game, CancellationToken cancellation, IReadOnlyCollection? edited, PluginField? single = null) + { + if (!_factories.TryGetValue(factory.Descriptor.Id, out var installed) || installed != factory) throw new ArgumentException("The factory is not installed in this host."); + var patch = draft.Copy(); var fields = (edited ?? factory.Schema.Fields.Select(field => field.Id).ToArray()).ToArray(); + await _operation.WaitAsync(cancellation).ConfigureAwait(false); + await Scheduler.Switch(); + Instance? entry = null; + PluginSettingsSnapshot? previous = null; + IPluginSynchronousReconfiguration? synchronous = null; + IPluginReconfiguration? asynchronous = null; + bool applied = false; + try { + entry = _entered.FirstOrDefault(candidate => candidate.Factory == factory && IsHealthy(candidate) && Available(candidate)); + GameIdentity? active = _activeGame; + var prepared = await Task.Run(() => { + var latest = Configuration.MergePatch(factory.Descriptor, factory.Schema, patch, fields, game); + var global = game.HasValue ? Configuration.Read(factory.Descriptor, factory.Schema) : latest; + var effective = game.HasValue ? PluginConfigurationStore.Resolve(factory.Schema, global, latest) : PluginConfigurationStore.Resolve(factory.Schema, global); + var runtime = active.HasValue && (!game.HasValue || game.Value.Equals(active.Value)) + ? PluginConfigurationStore.Resolve(factory.Schema, global, game.HasValue ? latest : Configuration.Read(factory.Descriptor, factory.Schema, active)) : effective; + return (Effective: effective, Runtime: runtime); + }, cancellation).ConfigureAwait(false); + await Scheduler.Switch(); cancellation.ThrowIfCancellationRequested(); + var validation = single == null ? factory.Schema : new PluginSettingsSchema(factory.Schema.Version, new PluginSettingsGroup("", "", single)); + var issues = validation.Validate(prepared.Effective); + if (issues.Count > 0) return new PluginActionResult(false, string.Join("\n", issues.Select(issue => issue.Field + ": " + issue.Message))); + if (single != null && !single.Visible(prepared.Effective)) return new PluginActionResult(false, "This setting is currently unavailable."); + previous = entry?.Settings; + PluginSettingsSnapshot? live = null; + if (entry != null && previous != null && active.HasValue && (!game.HasValue || game.Value.Equals(active.Value))) { + var changed = factory.Schema.Fields.Where(field => fields.Contains(field.Id) && field.Change == ConfigurationChange.Live && !JToken.DeepEquals(previous.Value(field.Id), prepared.Runtime.Value(field.Id))).ToArray(); + if (changed.Length > 0) { + live = previous.Copy(); foreach (var field in changed) live.Set(field.Id, prepared.Runtime.Value(field.Id)); + synchronous = entry.Plugin as IPluginSynchronousReconfiguration; + asynchronous = entry.Plugin as IPluginReconfiguration; + PluginActionResult result; + if (synchronous != null) result = synchronous.Apply(live); + else if (asynchronous != null) { + entry.Busy = true; + try { + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation); deadline.CancelAfter(StartupTimeout); + var hook = asynchronous.ApplyAsync(live, deadline.Token); + await AwaitHookAsync(hook, deadline.Token).ConfigureAwait(false); await Scheduler.Switch(); result = await hook.ConfigureAwait(false); + } catch (HookDeadlineException error) { await Scheduler.Switch(); Abandon(entry, error.Hook); return new PluginActionResult(false, "Live configuration did not finish; settings were not saved."); } + finally { await Scheduler.Switch(); entry.Busy = false; } + } else return new PluginActionResult(false, "The plugin does not implement live configuration."); + if (!result.Success) return result; + applied = true; entry.Settings = live; + } + } + cancellation.ThrowIfCancellationRequested(); + // Once persistence starts, join it. Cancelling the waiter could otherwise allow a late write after rollback. + await Task.Run(() => Configuration.SavePatch(factory.Descriptor, factory.Schema, patch, fields, game)).ConfigureAwait(false); + await Scheduler.Switch(); + NotifySettings(factory.Descriptor.Id, fields, game); + return new PluginActionResult(true, "Saved."); + } catch (Exception error) { + await Scheduler.Switch(); + if (applied && previous != null && entry != null && !entry.Cleaned) { + try { + PluginActionResult rollback; + if (synchronous != null) rollback = synchronous.Apply(previous); + else { + entry.Busy = true; using var deadline = new CancellationTokenSource(StartupTimeout); + var hook = asynchronous!.ApplyAsync(previous, deadline.Token); + await AwaitHookAsync(hook, deadline.Token).ConfigureAwait(false); await Scheduler.Switch(); rollback = await hook.ConfigureAwait(false); + } + if (!rollback.Success) Fault(factory.Descriptor.Id, PluginPhase.Prepare, new InvalidOperationException("Runtime settings rollback failed.")); + else entry.Settings = previous; + } catch (HookDeadlineException failure) { Abandon(entry, failure.Hook); } + catch (Exception failure) { Fault(factory.Descriptor.Id, PluginPhase.Prepare, failure); } + finally { await Scheduler.Switch(); entry.Busy = false; } + } + return new PluginActionResult(false, error is OperationCanceledException ? "Settings change cancelled." : error.Message); + } finally { _operation.Release(); ReplayState(); } + } + private void NotifySettings(string id, IReadOnlyCollection fields, GameIdentity? game) + { + if (SettingsCommitted == null) return; + foreach (Action, GameIdentity?> listener in SettingsCommitted.GetInvocationList()) + try { listener(id, fields, game); } catch (Exception error) { _log("Settings observer: " + error.Message); } + } + } +} diff --git a/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs.meta b/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs.meta new file mode 100644 index 000000000..ebb8ac265 --- /dev/null +++ b/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c6ededed44c0a584a8067e1a51a15331 \ No newline at end of file diff --git a/VisualPinball.Plugins/Runtime/PluginHost.cs b/VisualPinball.Plugins/Runtime/PluginHost.cs index f57614fac..61422a906 100644 --- a/VisualPinball.Plugins/Runtime/PluginHost.cs +++ b/VisualPinball.Plugins/Runtime/PluginHost.cs @@ -14,7 +14,7 @@ namespace VisualPinball.Plugins { /// Serializes game hooks, resolves policy before constructing plugins, and owns reverse cleanup. - public sealed class PluginHost + public sealed partial class PluginHost { private sealed class HookDeadlineException : TimeoutException { @@ -274,59 +274,8 @@ private async Task StopCoreAsync(CancellationToken cleanup) _startOrder.Clear(); _entered.Clear(); _gameCancellation?.Dispose(); _gameCancellation = null; _activeGame = null; } /// Applies live values and persistence within the same lifecycle gate. Plugins must roll back failed reconfiguration. - public async Task CommitSettingsAsync(IPlayerPluginFactory factory, PluginSettingsSnapshot draft, GameIdentity? game, CancellationToken cancellation) - { - if (!_factories.TryGetValue(factory.Descriptor.Id, out var installed) || installed != factory) throw new ArgumentException("The factory is not installed in this host."); - using var operationDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation, _gameCancellation?.Token ?? CancellationToken.None); - operationDeadline.CancelAfter(StartupTimeout); - try { await _operation.WaitAsync(operationDeadline.Token).ConfigureAwait(false); } - catch (OperationCanceledException) { return new PluginActionResult(false, "The plugin host is busy or stopping. Settings were not saved."); } - await Scheduler.Switch(); - Instance? busyEntry = null; - try { - var entry = _entered.FirstOrDefault(candidate => candidate.Factory == factory && IsHealthy(candidate) && Available(candidate)); - PluginSettingsSnapshot? previous = entry?.Settings; - PluginSettingsSnapshot? live = null; - IPluginReconfiguration? receiver = null; - if (entry != null && previous != null && _activeGame.HasValue && (!game.HasValue || game.Value.Equals(_activeGame.Value))) { - var next = game.HasValue ? PluginConfigurationStore.Resolve(factory.Schema, Configuration.Read(factory.Descriptor, factory.Schema), draft) - : PluginConfigurationStore.Resolve(factory.Schema, draft, Configuration.Read(factory.Descriptor, factory.Schema, _activeGame)); - var changed = factory.Schema.Fields.Where(field => field.Change == ConfigurationChange.Live && !JToken.DeepEquals(previous.Value(field.Id), next.Value(field.Id))).ToArray(); - if (changed.Length > 0) { - busyEntry = entry; entry.Busy = true; - receiver = entry.Plugin as IPluginReconfiguration; - if (receiver == null) return new PluginActionResult(false, "This plugin declares live settings but does not implement reconfiguration."); - live = previous.Copy(); - foreach (var field in changed) live.Set(field.Id, next.Value(field.Id)); - var apply = receiver.ApplyAsync(live, operationDeadline.Token); - try { await AwaitHookAsync(apply, operationDeadline.Token).ConfigureAwait(false); } - catch (HookDeadlineException error) { await Scheduler.Switch(); Abandon(entry, error.Hook); return new PluginActionResult(false, "Plugin reconfiguration did not finish. Settings were not saved."); } - var result = await apply.ConfigureAwait(false); - await Scheduler.Switch(); - if (!result.Success) return result; - } - } - try { - cancellation.ThrowIfCancellationRequested(); - Configuration.Save(factory.Descriptor, factory.Schema, draft, game); - } catch { - if (receiver != null && previous != null) { - try { - using var rollbackDeadline = new CancellationTokenSource(StartupTimeout); - var rollback = receiver.ApplyAsync(previous, rollbackDeadline.Token); - await AwaitHookAsync(rollback, rollbackDeadline.Token).ConfigureAwait(false); - await Scheduler.Switch(); - if (!(await rollback).Success) Fault(factory.Descriptor.Id, PluginPhase.Prepare, new InvalidOperationException("Runtime settings rollback failed.")); - } - catch (HookDeadlineException error) { await Scheduler.Switch(); if (entry != null) Abandon(entry, error.Hook); } - catch (Exception error) { await Scheduler.Switch(); Fault(factory.Descriptor.Id, PluginPhase.Prepare, error); } - } - throw; - } - if (live != null && entry != null) entry.Settings = live; - return new PluginActionResult(true, "Saved. Live settings are applied; other changes take effect on the next game or application restart."); - } finally { if (busyEntry != null) busyEntry.Busy = false; _operation.Release(); ReplayState(); } - } + public Task CommitSettingsAsync(IPlayerPluginFactory factory, PluginSettingsSnapshot draft, GameIdentity? game, CancellationToken cancellation, IReadOnlyCollection? edited = null) + => CommitSettingsCoreAsync(factory, draft, game, cancellation, edited); private async Task CleanupAsync(Instance entry, CancellationToken cleanup) { if (entry.Cleaned) return; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechanicalSoundGain.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechanicalSoundGain.cs new file mode 100644 index 000000000..87feb2bad --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechanicalSoundGain.cs @@ -0,0 +1,53 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace VisualPinball.Unity +{ + /// Applies a user gain while keeping mechanical requests and fades in their authored units. + public static class MechanicalSoundGain + { + private static readonly Dictionary Levels = new(); + private static readonly List Destroyed = new(); + private static float _gain = 1; + public static float Gain { + get => _gain; + set { + if (float.IsNaN(value) || float.IsInfinity(value)) throw new ArgumentOutOfRangeException(nameof(value)); + _gain = Mathf.Clamp01(value); + Destroyed.Clear(); + foreach (var pair in Levels) { + if (pair.Key == null) Destroyed.Add(pair.Key); + else pair.Key.volume = pair.Value * _gain; + } + foreach (var source in Destroyed) Levels.Remove(source); + Destroyed.Clear(); + } + } + internal static void Register(AudioSource source) + { + Levels[source] = source.volume; + source.volume *= _gain; + } + internal static void Unregister(AudioSource source) + { + // Unity's destroyed-object null check must not prevent removing its managed key. + if (!ReferenceEquals(source, null)) Levels.Remove(source); + } + public static float AuthoredLevel(AudioSource source) => Levels.TryGetValue(source, out float level) ? level : source.volume; + public static void SetAuthoredLevel(AudioSource source, float level) + { + level = Mathf.Clamp01(level); + if (Levels.ContainsKey(source)) { Levels[source] = level; source.volume = level * _gain; } + else source.volume = level; + } + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void Reset() { Levels.Clear(); Destroyed.Clear(); _gain = 1; } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/MechanicalSoundGain.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechanicalSoundGain.cs.meta new file mode 100644 index 000000000..483c34546 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/MechanicalSoundGain.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 617582555a436984182cb2d9ae48a14f \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundEffectAsset.cs b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundEffectAsset.cs index 518fb1c39..510a49bed 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundEffectAsset.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Sound/SoundEffectAsset.cs @@ -112,7 +112,12 @@ public async Task Play( try { ConfigureAudioSource(audioSource); - audioSource.volume *= volume; + bool mechanical = Application.isPlaying && _type == SoundEffectType.Mechanical; +#if UNITY_EDITOR + mechanical &= !EditorSceneManager.IsPreviewScene(audioObj.scene); +#endif + if (mechanical) MechanicalSoundGain.Register(audioSource); + MechanicalSoundGain.SetAuthoredLevel(audioSource, MechanicalSoundGain.AuthoredLevel(audioSource) * volume); audioSource.Play(); using var eitherCts = CancellationTokenSource.CreateLinkedTokenSource( allowFadeOutCt, @@ -127,7 +132,7 @@ public async Task Play( await FadeOrFinish( audioSource, 0f, - audioSource.volume, + MechanicalSoundGain.AuthoredLevel(audioSource), _fadeInTime, eitherCts.Token ); @@ -151,7 +156,7 @@ await FadeOrFinish( { await FadeOrFinish( audioSource, - audioSource.volume, + MechanicalSoundGain.AuthoredLevel(audioSource), 0f, _fadeOutTime, instantCt @@ -162,6 +167,7 @@ await FadeOrFinish( } finally { + MechanicalSoundGain.Unregister(audioSource); if (audioSource != null) { if (Application.isPlaying) @@ -213,7 +219,7 @@ CancellationToken ct while (progress < 1f && audioSource != null) { ct.ThrowIfCancellationRequested(); - audioSource.volume = Mathf.Lerp(fromVolume, toVolume, progress); + MechanicalSoundGain.SetAuthoredLevel(audioSource, Mathf.Lerp(fromVolume, toVolume, progress)); #if UNITY_EDITOR var deltaTime = (float)(EditorApplication.timeSinceStartup - lastTime); lastTime = EditorApplication.timeSinceStartup; From a37c566d004f1d05f5258a648e99ac8fb5c5bf5d Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 02:52:28 +0200 Subject: [PATCH 09/26] plugins: publish native controller outputs and segment displays - New ResourceKind.Outputs carries every lamp, solenoid, GI string and mech by the controller's own number, with the value's semantics (binary, levels, physical, position) and maximum, plus a poll-end marker. It is separate from the mapped Controller resource, so role-less controller bindings stay unambiguous. - Gamelogic engines opt in through IGamelogicNativeOutputs; GamelogicPluginResources publishes the outputs resource. - Height-one displays are published as Segments resources with layout and hardware hints. IPluginSegmentSource announces the display's order and element count before the first frame. - Choice, action and validation requests carry the table package (PluginPackageReference), for catalogs that look next to the table. --- .../OutputResourceTests.cs | 52 +++++++++++++ .../OutputResourceTests.cs.meta | 2 + .../Configuration/PluginSchema.cs | 11 +++ .../Configuration/PluginSettingsSession.cs | 12 ++- .../Resources/DisplayResourceSource.cs | 10 ++- .../Resources/PluginResourceRegistry.cs | 2 +- .../Resources/ResourceContracts.cs | 65 ++++++++++++++-- .../Resources/ResourceSignal.cs | 9 ++- VisualPinball.Plugins/Runtime/PluginHost.cs | 2 +- .../Plugins/GamelogicPluginResources.cs | 77 ++++++++++++++++++- .../Plugins/UnityPluginContracts.cs | 26 +++++++ 11 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 VisualPinball.Plugins.Test/OutputResourceTests.cs create mode 100644 VisualPinball.Plugins.Test/OutputResourceTests.cs.meta diff --git a/VisualPinball.Plugins.Test/OutputResourceTests.cs b/VisualPinball.Plugins.Test/OutputResourceTests.cs new file mode 100644 index 000000000..9e806b52b --- /dev/null +++ b/VisualPinball.Plugins.Test/OutputResourceTests.cs @@ -0,0 +1,52 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using NUnit.Framework; +using VisualPinball.Plugins; + +namespace VisualPinball.Plugins.Test +{ + [TestFixture] + public sealed class OutputResourceTests + { + [Test] + public void NativeOutputsLeaveRolelessControllerBindingsUnambiguous() + { + var registry = new PluginResourceRegistry(); + using var mapped = new ControllerResourceSource(); + using var native = new ControllerResourceSource(ResourceKind.Outputs); + using var mappedPublication = registry.Publish(new PluginResourceDescriptor("pinmame", "pinmame", "pinmame", "PinMAME", "controller", ResourceKind.Controller, 1, 1), mapped); + using var nativePublication = registry.Publish(new PluginResourceDescriptor("pinmame/outputs", "pinmame", "pinmame", "PinMAME outputs", "outputs", ResourceKind.Outputs, 1, 2), native); + using var controller = registry.Bind(new PluginResourceSelection(ResourceKind.Controller) { Controller = "pinmame", Session = 1 }); + using var outputs = registry.Bind(new PluginResourceSelection(ResourceKind.Outputs) { Controller = "pinmame", Session = 1 }); + Assert.That(controller.State, Is.EqualTo(ResourceBindingState.Bound)); + Assert.That(controller.Source, Is.SameAs(mapped)); + Assert.That(outputs.State, Is.EqualTo(ResourceBindingState.Bound)); + Assert.That(outputs.Source, Is.SameAs(native)); + int number = 0; + using var subscription = outputs.Connect(source => source.Subscribe(change => number = change.Number)); + native.Publish(new PluginControllerEvent(ControllerEventKind.Lamp, "81", 128, 1, 0, 81, ControllerValueSemantics.Physical, 255)); + Assert.That(number, Is.EqualTo(81)); + } + + [Test] + public void SegmentDisplaysAnnounceTheirPlaceBeforeTheFirstFrame() + { + var registry = new PluginResourceRegistry(); + using var source = new SegmentResourceSource(1, 7); + using var publication = registry.Publish(new PluginResourceDescriptor("pinmame/segments/display1", "pinmame", "pinmame", "display1", "segments", ResourceKind.Segments, 1, 1), source); + using var binding = registry.Bind(new PluginResourceSelection(ResourceKind.Segments) { Session = 1 }); + Assert.That(binding.Source!.Order, Is.EqualTo(1)); + Assert.That(binding.Source.Elements, Is.EqualTo(7)); + } + + [Test] + public void ControllerSourcesCarryOnlyControllerKinds() + { + Assert.Throws(() => new ControllerResourceSource(ResourceKind.Display)); + } + } +} diff --git a/VisualPinball.Plugins.Test/OutputResourceTests.cs.meta b/VisualPinball.Plugins.Test/OutputResourceTests.cs.meta new file mode 100644 index 000000000..ec3cad58a --- /dev/null +++ b/VisualPinball.Plugins.Test/OutputResourceTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 36dd34cbc22a45f0bb32aa153c713f65 diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs b/VisualPinball.Plugins/Configuration/PluginSchema.cs index 21d8fe46e..6e6266268 100644 --- a/VisualPinball.Plugins/Configuration/PluginSchema.cs +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs @@ -193,9 +193,20 @@ public PluginChoice(string id, string label, string description = "", bool avail { Id = id; Label = label; Description = description; Available = available; UnavailableReason = unavailableReason; } } + /// The table package a game-scoped request is about, for catalogs that look next to it. + public sealed class PluginPackageReference + { + /// Full path of the table package. + public string Path { get; } + public string Title { get; } + public PluginPackageReference(string path, string title) { Path = path ?? ""; Title = title ?? ""; } + } + public sealed class PluginChoiceRequest { public GameIdentity? Game { get; } + /// The selected table package, when the request concerns one; null for global settings. + public PluginPackageReference? Package { get; set; } public PluginSettingsSnapshot Settings { get; } public string Platform { get; } public IReadOnlyList Resources { get; } diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs index dff17e1fd..f08b8fdc9 100644 --- a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs @@ -27,6 +27,8 @@ public sealed class PluginSettingsSession : IDisposable public bool HasEdits(string field) => _resetFields.Contains(field) || !JToken.DeepEquals(_baseline.Value(field), Draft.Value(field)); private string[] ChangedFields() => _factory.Schema.Fields.Where(field => HasEdits(field.Id)).Select(field => field.Id).ToArray(); public GameIdentity? Game { get; } + /// The table package being configured, passed to catalogs, actions and validation. + public PluginPackageReference? Package { get; set; } public PluginSettingsSnapshot Draft { get; private set; } public PluginSettingsSnapshot Effective => Game.HasValue ? PluginConfigurationStore.Resolve(_factory.Schema, _store.Read(_factory.Descriptor, _factory.Schema), Draft) @@ -58,14 +60,16 @@ public void Reset() public IReadOnlyList Validate() { var effective = Effective; - return _factory.Schema.Validate(effective).Concat(_factory.SettingsProvider?.Validate(new PluginChoiceRequest(Game, effective, _platform)) ?? Array.Empty()).ToArray(); + return _factory.Schema.Validate(effective).Concat(_factory.SettingsProvider?.Validate(Request(effective)) ?? Array.Empty()).ToArray(); } + private PluginChoiceRequest Request(PluginSettingsSnapshot settings, IReadOnlyList? resources = null) + => new PluginChoiceRequest(Game, settings, _platform, resources) { Package = Package }; public Task DiscoverAsync(string provider, CancellationToken cancellation) - => _factory.SettingsProvider?.DiscoverAsync(provider, new PluginChoiceRequest(Game, Effective, _platform, _resources()), cancellation) ?? Task.FromResult(new PluginChoiceCatalog("empty", Array.Empty())); + => _factory.SettingsProvider?.DiscoverAsync(provider, Request(Effective, _resources()), cancellation) ?? Task.FromResult(new PluginChoiceCatalog("empty", Array.Empty())); public async Task RunActionAsync(string action, CancellationToken cancellation) { if (_factory.SettingsProvider == null) return new PluginActionResult(false, "This action is unavailable."); - var result = await _factory.SettingsProvider.RunActionAsync(action, new PluginChoiceRequest(Game, Effective, _platform), cancellation); + var result = await _factory.SettingsProvider.RunActionAsync(action, Request(Effective), cancellation); cancellation.ThrowIfCancellationRequested(); if (result.Success && result.Changes != null) foreach (var field in _factory.Schema.Fields) { if (Game.HasValue ? field.Scope == ConfigurationScope.GlobalOnly : field.Scope == ConfigurationScope.GameOnly) continue; @@ -87,7 +91,7 @@ public async Task ApplyAsync(CancellationToken cancellation return Game.HasValue ? PluginConfigurationStore.Resolve(_factory.Schema, _store.Read(_factory.Descriptor, _factory.Schema), latest) : PluginConfigurationStore.Resolve(_factory.Schema, latest); }, linked.Token).ConfigureAwait(false); if (host != null) await host.Scheduler.Switch(); - var request = new PluginChoiceRequest(Game, effective, _platform, resources); + var request = Request(effective, resources); var issues = _factory.Schema.Validate(effective).Concat(_factory.SettingsProvider?.Validate(request) ?? Array.Empty()).ToList(); var catalogs = new Dictionary(StringComparer.Ordinal); foreach (var binding in ChoiceBindings(_factory.Schema.Fields, effective)) { diff --git a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs index 1f40905d6..c8c481a5d 100644 --- a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs +++ b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs @@ -43,8 +43,14 @@ public sealed class ControllerResourceSource : IPluginControllerSource, IDisposa private readonly List> _subscribers = new List>(); private Action[] _dispatch = Array.Empty>(); private bool _disposed; - public ResourceKind Kind => ResourceKind.Controller; - public ControllerResourceSource(Action? fault = null) { _fault = fault; } + public ResourceKind Kind { get; } + public ControllerResourceSource(Action? fault = null) : this(ResourceKind.Controller, fault) { } + /// for mapped outputs, for native ones. + public ControllerResourceSource(ResourceKind kind, Action? fault = null) + { + if (kind != ResourceKind.Controller && kind != ResourceKind.Outputs) throw new ArgumentException("A controller source carries controller or output events.", nameof(kind)); + Kind = kind; _fault = fault; + } public IDisposable Subscribe(Action receive) { if (_disposed) throw new ObjectDisposedException(nameof(ControllerResourceSource)); diff --git a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs index add1dc228..e677d805a 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs @@ -37,7 +37,7 @@ public IDisposable Publish(PluginResourceDescriptor descriptor, T source) whe { if (descriptor.Kind != source.Kind) throw new ArgumentException("Resource kind and VPE source interface disagree."); if (descriptor.Kind == ResourceKind.Display && !(source is IPluginDisplaySource) - || descriptor.Kind == ResourceKind.Controller && !(source is IPluginControllerSource) + || (descriptor.Kind == ResourceKind.Controller || descriptor.Kind == ResourceKind.Outputs) && !(source is IPluginControllerSource) || descriptor.Kind == ResourceKind.Audio && !(source is IPluginAudioSource) || descriptor.Kind == ResourceKind.Segments && !(source is IPluginSegmentSource) || descriptor.Kind == ResourceKind.Input && !(source is IPluginInputSource) diff --git a/VisualPinball.Plugins/Resources/ResourceContracts.cs b/VisualPinball.Plugins/Resources/ResourceContracts.cs index 4d35fe10b..abbdefbf4 100644 --- a/VisualPinball.Plugins/Resources/ResourceContracts.cs +++ b/VisualPinball.Plugins/Resources/ResourceContracts.cs @@ -9,7 +9,12 @@ namespace VisualPinball.Plugins { - public enum ResourceKind { Controller, Display, Segments, Audio, Input, Surface, Triggers } + /// + /// carries every controller output by its native number through . + /// It is separate from , the outputs as the table maps them, so a controller binding without a + /// role stays unambiguous. + /// + public enum ResourceKind { Controller, Display, Segments, Audio, Input, Surface, Triggers, Outputs } public enum ResourceView { Original, Identification, Presentation } public enum ResourceBindingState { Waiting, Bound, Blocked, Disposed } public enum ResourceChangeKind { Added, Changed, Removed } @@ -17,7 +22,26 @@ public enum ResourceChangeKind { Added, Changed, Removed } public enum FrameColorSpace { Linear, Srgb } public enum FrameAlpha { None, Straight, Premultiplied } public enum PcmEncoding { Int16LittleEndian, Float32 } - public enum ControllerEventKind { Switch, Coil, Lamp, GeneralIllumination, Trigger, State } + public enum ControllerEventKind + { + Switch, Coil, Lamp, GeneralIllumination, Trigger, State, + /// Raw position of an emulated mech, as the controller reports it. + Mech, + /// Ends one controller poll; consumers that batch per poll apply what they collected. + PollEnd + } + /// What a controller event's raw value means. + public enum ControllerValueSemantics + { + /// 0 or 1. + Binary, + /// Discrete levels from 0 to the event's maximum, such as WPC GI 0..8. + Levels, + /// Already faded physical brightness from 0 to the event's maximum. + Physical, + /// A position, such as a mech step; not a brightness. + Position + } public sealed class PluginResourceDescriptor { @@ -109,6 +133,11 @@ public interface IPluginDisplaySource : IPluginResourceSource IDisposable AcquireView(ResourceView intent, DisplayEncoding acceptedFormats); } + /// + /// Events of a controller-native resource address outputs by the controller's own numbering + /// () and describe their raw value per address, because one controller can mix binary and + /// physically faded outputs of the same kind. + /// public readonly struct PluginControllerEvent { public ControllerEventKind Kind { get; } @@ -116,8 +145,15 @@ public readonly struct PluginControllerEvent public double Value { get; } public long Sequence { get; } public long TimestampUs { get; } + /// Controller-native output number, or -1 when the event is addressed by only. + public int Number { get; } + public ControllerValueSemantics Semantics { get; } + /// Raw value of a fully on output (or the largest level); 1 for binary outputs. + public double Maximum { get; } public PluginControllerEvent(ControllerEventKind kind, string id, double value, long sequence, long timestampUs) - { Kind = kind; Id = id; Value = value; Sequence = sequence; TimestampUs = timestampUs; } + : this(kind, id, value, sequence, timestampUs, -1, ControllerValueSemantics.Binary, 1) { } + public PluginControllerEvent(ControllerEventKind kind, string id, double value, long sequence, long timestampUs, int number, ControllerValueSemantics semantics, double maximum) + { Kind = kind; Id = id; Value = value; Sequence = sequence; TimestampUs = timestampUs; Number = number; Semantics = semantics; Maximum = maximum > 0 ? maximum : 1; } } public interface IPluginControllerSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } @@ -141,17 +177,34 @@ public PluginPcmFrame(ReadOnlyMemory samples, PcmEncoding encoding, int sa public interface IPluginAudioSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } /// An exclusive lease replaces built-in playback while leaving the original PCM stream available. public interface IPluginAudioPlaybackControl { IDisposable AcquirePlayback(string pluginId); } - public interface IPluginSegmentSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } + /// A segment display. Its place and size are known when it is published, before any frame arrives. + public interface IPluginSegmentSource : IPluginResourceSource + { + /// Position among the controller's segment displays, in the order the controller announced them. + int Order { get; } + /// Number of elements (digits) the controller announced; frames carry the same count. + int Elements { get; } + IDisposable Subscribe(Action receive); + } + /// + /// names the segment arrangement of the elements: seg7 (a-g and a comma at bit 7), + /// seg9 (seven segments plus split verticals at bits 8 and 9), seg14 (a-f, g1, comma, h, i, j, g2, + /// k, l, m, dot) or seg16. hints at the display technology: plasma, + /// vfd-green, vfd-blue, led, optionally followed by +gottlieb. Brightness is borrowed + /// for the synchronous callback only. + /// public readonly struct PluginSegmentFrame { public string Layout { get; } public int Elements { get; } public int SegmentsPerElement { get; } public ReadOnlyMemory Brightness { get; } - public PluginSegmentFrame(string layout, int elements, int segmentsPerElement, ReadOnlyMemory brightness) + public string Hardware { get; } + public PluginSegmentFrame(string layout, int elements, int segmentsPerElement, ReadOnlyMemory brightness) : this(layout, elements, segmentsPerElement, brightness, "") { } + public PluginSegmentFrame(string layout, int elements, int segmentsPerElement, ReadOnlyMemory brightness, string hardware) { if (elements <= 0 || segmentsPerElement <= 0 || brightness.Length != checked(elements * segmentsPerElement)) throw new ArgumentException("Segment layout does not match its state."); - Layout = layout; Elements = elements; SegmentsPerElement = segmentsPerElement; Brightness = brightness; + Layout = layout; Elements = elements; SegmentsPerElement = segmentsPerElement; Brightness = brightness; Hardware = hardware ?? ""; } } public readonly struct PluginInputEvent diff --git a/VisualPinball.Plugins/Resources/ResourceSignal.cs b/VisualPinball.Plugins/Resources/ResourceSignal.cs index 8677fbc48..124087f8e 100644 --- a/VisualPinball.Plugins/Resources/ResourceSignal.cs +++ b/VisualPinball.Plugins/Resources/ResourceSignal.cs @@ -47,7 +47,14 @@ public sealed class SegmentResourceSource : IPluginSegmentSource, IDisposable { private readonly ResourceSignal _signal; public ResourceKind Kind => ResourceKind.Segments; - public SegmentResourceSource(Action? fault = null) { _signal = new ResourceSignal(fault); } + public int Order { get; } + public int Elements { get; } + public SegmentResourceSource(Action? fault = null) : this(0, 0, fault) { } + public SegmentResourceSource(int order, int elements, Action? fault = null) + { + Order = order; Elements = elements; + _signal = new ResourceSignal(fault); + } public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); public void Publish(in PluginSegmentFrame frame) => _signal.Publish(frame); public void Dispose() => _signal.Dispose(); diff --git a/VisualPinball.Plugins/Runtime/PluginHost.cs b/VisualPinball.Plugins/Runtime/PluginHost.cs index 61422a906..3c81f62be 100644 --- a/VisualPinball.Plugins/Runtime/PluginHost.cs +++ b/VisualPinball.Plugins/Runtime/PluginHost.cs @@ -94,7 +94,7 @@ public async Task PrepareAndStartAsync(GameIdentity identity, string packagePath var application = new ResourceScope(); try { var settings = Configuration.Effective(descriptor, factory.Schema, identity); - var issues = factory.Schema.Validate(settings).Concat(factory.SettingsProvider?.Validate(new PluginChoiceRequest(identity, settings, Platform)) ?? Array.Empty()).ToArray(); + var issues = factory.Schema.Validate(settings).Concat(factory.SettingsProvider?.Validate(new PluginChoiceRequest(identity, settings, Platform) { Package = new PluginPackageReference(packagePath, title) }) ?? Array.Empty()).ToArray(); if (issues.Length > 0) throw new InvalidOperationException(string.Join("; ", issues.Select(issue => issue.Field + ": " + issue.Message))); if (Configuration.Read(descriptor, factory.Schema).SchemaVersion > factory.Schema.Version || Configuration.Read(descriptor, factory.Schema, identity).SchemaVersion > factory.Schema.Version) throw new InvalidOperationException("Configuration requires a newer plugin version."); var plugin = factory.Create(new PluginApplicationContext(descriptor, Configuration, Scheduler, Capabilities, Resources, application, message => _log(descriptor.Id + ": " + message))); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs index fae51778b..dbd38426e 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs @@ -30,8 +30,31 @@ internal Display(DisplayResourceSource source, IDisposable publication, int widt { Source = source; Publication = publication; Width = width; Height = height; Instance = instance; _format = format; } public void Dispose() { Publication.Dispose(); Source.Dispose(); _format?.Dispose(); } } + /// A segment display published as . + private sealed class Segments : IDisposable + { + internal readonly SegmentResourceSource Source; + internal readonly IDisposable Publication; + internal readonly string Layout; + internal readonly string Hardware; + internal float[] Brightness = Array.Empty(); + internal Segments(SegmentResourceSource source, IDisposable publication, string layout, string hardware) + { Source = source; Publication = publication; Layout = layout; Hardware = hardware; } + public void Dispose() { Publication.Dispose(); Source.Dispose(); } + } + /// Forwards native outputs without exposing the listener on the public type. + private sealed class NativeOutputs : INativeOutputListener + { + private readonly GamelogicPluginResources _owner; + internal NativeOutputs(GamelogicPluginResources owner) { _owner = owner; } + public void NativeOutputChanged(ControllerEventKind kind, int number, double value, ControllerValueSemantics semantics, double maximum) => _owner.EmitNative(kind, number, value, semantics, maximum); + public void NativeOutputPollEnd() => _owner.EmitNative(ControllerEventKind.PollEnd, -1, 0, ControllerValueSemantics.Binary, 1); + } private static long _instance; private readonly IGamelogicEngine _engine; + private readonly Dictionary _segments = new Dictionary(StringComparer.Ordinal); + private readonly ControllerResourceSource _native; + private readonly string[] _numberNames = new string[1024]; private readonly OwnerScheduler _scheduler; private readonly PluginResourceRegistry _registry; private readonly Dictionary _displays = new Dictionary(StringComparer.Ordinal); @@ -60,6 +83,13 @@ public GamelogicPluginResources(GameObject table, IGamelogicEngine engine, GameI format => (engine as IDisplayFrameFormatPreference)?.RequestDisplayFrameFormat("", ToLegacy(format)))); _scope.Own(_controller); _scope.Own(registry.Publish(new PluginResourceDescriptor(_controllerId, _provider, _controllerId, engine.Name, "controller", ResourceKind.Controller, session, Interlocked.Increment(ref _instance)), _controller)); + if (engine is IGamelogicNativeOutputs native) { + // Every output by native number, for consumers such as backglasses that address ROM lamps directly. + _native = _scope.Own(new ControllerResourceSource(ResourceKind.Outputs, fault)); + _scope.Own(registry.Publish(new PluginResourceDescriptor(_controllerId + "/outputs", _provider, _controllerId, engine.Name + " outputs", "outputs", ResourceKind.Outputs, session, Interlocked.Increment(ref _instance)), _native)); + native.SetNativeOutputListener(new NativeOutputs(this)); + _scope.Own(new Subscription(() => native.SetNativeOutputListener(null))); + } if (engine is IGamelogicAudioResourceProvider audio) _scope.Own(registry.Publish(new PluginResourceDescriptor(_controllerId + "/audio/rom", _provider, _controllerId, engine.Name + " audio", "rom-mix", ResourceKind.Audio, session, Interlocked.Increment(ref _instance)), audio.AudioSource)); engine.OnDisplaysRequested += DisplaysRequested; engine.OnDisplayUpdateFrame += DisplayFrame; @@ -73,7 +103,33 @@ public GamelogicPluginResources(GameObject table, IGamelogicEngine engine, GameI private void DisplaysRequested(object sender, RequestedDisplays args) { _scheduler.VerifyOwner(); - foreach (var display in args.Displays) if (display.Height > 1) AddDisplay(display.Id, display.Width, display.Height); + foreach (var display in args.Displays) { + if (display.Height > 1) AddDisplay(display.Id, display.Width, display.Height); + else if (display.Width > 0) AddSegments(display.Id, display.Width); + } + } + private void AddSegments(string id, int elements) + { + if (_disposed || _segments.ContainsKey(id)) return; + string layout = "seg16", hardware = ""; + if (_engine is ISegmentDisplayMetadata metadata && metadata.TryGetSegmentLayout(id, out var declared, out var declaredHardware)) { layout = declared; hardware = declaredHardware; } + // Displays are numbered in announcement order, so consumers can lay out digits before any frame arrives. + var source = new SegmentResourceSource(_segments.Count, elements, _fault); + var descriptor = new PluginResourceDescriptor(_controllerId + "/segments/" + id, _provider, _controllerId, id, "segments", ResourceKind.Segments, _session, Interlocked.Increment(ref _instance)); + _segments.Add(id, new Segments(source, _registry.Publish(descriptor, source), layout, hardware)); + } + private void SegmentFrame(Segments display, DisplayFrameData args) + { + // One 16-bit segment mask per element, as PinMAME reports it. + if (args.Data == null || args.Data.Length < 2) return; + int elements = args.Data.Length / 2; + if (display.Brightness.Length != elements * 16) display.Brightness = new float[elements * 16]; + var brightness = display.Brightness; + for (int element = 0; element < elements; element++) { + int mask = args.Data[element * 2] | args.Data[element * 2 + 1] << 8; + for (int bit = 0; bit < 16; bit++) brightness[element * 16 + bit] = (mask >> bit & 1) != 0 ? 1f : 0f; + } + display.Source.Publish(new PluginSegmentFrame(display.Layout, elements, 16, brightness, display.Hardware)); } private void AddDisplay(string id, int width, int height) { @@ -92,7 +148,9 @@ private void AddDisplay(string id, int width, int height) private void DisplayFrame(object sender, DisplayFrameData args) { _scheduler.VerifyOwner(); - if (_disposed || !_displays.TryGetValue(args.Id, out var display)) return; + if (_disposed) return; + if (args.Format == DisplayFrameFormat.Segment && _segments.TryGetValue(args.Id, out var segments)) { SegmentFrame(segments, args); return; } + if (!_displays.TryGetValue(args.Id, out var display)) return; var format = FromLegacy(args.Format); if (format == DisplayEncoding.None) return; int stride = display.Width * (format == DisplayEncoding.Rgb24 ? 3 : 1); @@ -105,6 +163,18 @@ private void Emit(ControllerEventKind kind, string id, double value) if (!_scheduler.IsOwner) { _scheduler.Post(() => Emit(kind, id, value)); return; } _controller.Publish(new PluginControllerEvent(kind, id, value, ++_sequence, Timestamp())); } + private void EmitNative(ControllerEventKind kind, int number, double value, ControllerValueSemantics semantics, double maximum) + { + if (_disposed || _native == null) return; + if (!_scheduler.IsOwner) { _scheduler.Post(() => EmitNative(kind, number, value, semantics, maximum)); return; } + _native.Publish(new PluginControllerEvent(kind, NumberName(number), value, ++_sequence, Timestamp(), number, semantics, maximum)); + } + private string NumberName(int number) + { + if (number < 0) return ""; + if (number >= _numberNames.Length) return number.ToString(System.Globalization.CultureInfo.InvariantCulture); + return _numberNames[number] ??= number.ToString(System.Globalization.CultureInfo.InvariantCulture); + } private void Coil(object sender, CoilEventArgs args) => Emit(ControllerEventKind.Coil, args.Id, args.Value); private void Lamp(object sender, LampEventArgs args) => Emit(args.Source == LampSource.GI ? ControllerEventKind.GeneralIllumination : ControllerEventKind.Lamp, args.Id, args.Value); private void Lamps(object sender, LampsEventArgs args) { foreach (var lamp in args.LampsChanged) Lamp(sender, lamp); } @@ -124,7 +194,8 @@ public void Dispose() if (_disposed) return; _disposed = true; _engine.OnDisplaysRequested -= DisplaysRequested; _engine.OnDisplayUpdateFrame -= DisplayFrame; _engine.OnCoilChanged -= Coil; _engine.OnLampChanged -= Lamp; _engine.OnLampsChanged -= Lamps; _engine.OnSwitchChanged -= Switch; - foreach (var display in _displays.Values) display.Dispose(); _displays.Clear(); _scope.Dispose(); + foreach (var display in _displays.Values) display.Dispose(); _displays.Clear(); + foreach (var segments in _segments.Values) segments.Dispose(); _segments.Clear(); _scope.Dispose(); } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs index bbd9ae1f6..d95ff5d8d 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs @@ -28,6 +28,32 @@ public interface IDisplayResourceMetadata bool FormatAppliesToAllDisplays { get; } } public interface IGamelogicAudioResourceProvider { IPluginAudioSource AudioSource { get; } } + + /// Receives every controller output in the controller's own numbering, on Unity's main thread. + public interface INativeOutputListener + { + void NativeOutputChanged(ControllerEventKind kind, int number, double value, ControllerValueSemantics semantics, double maximum); + /// The controller finished one poll. + void NativeOutputPollEnd(); + } + + /// + /// A gamelogic engine that reports all lamps, solenoids, GI strings and mechs by native number, independent of the + /// table's mapping. Backglass-only outputs (match, tilt, ball in play) are usually not mapped to anything on the + /// playfield, so the mapped events alone miss them. + /// + public interface IGamelogicNativeOutputs + { + /// Sets the single listener, or null. Changes are delivered from the engine's main-thread poll. + void SetNativeOutputListener(INativeOutputListener listener); + } + + /// Layout and technology of a segment display, as published in . + public interface ISegmentDisplayMetadata + { + /// Returns the layout (seg7, seg9, seg14, seg16) and hardware hint of a display. + bool TryGetSegmentLayout(string displayId, out string layout, out string hardware); + } public static class UnityPluginCapability { public const string GameScene = "vpe.unity.game-scene.v1"; From 1cc1a8003da89fed03c50b284f3f63ba0ea1a424 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 02:55:31 +0200 Subject: [PATCH 10/26] native windows: shared borderless GPU windows for plugins One native library, VisualPinballNativeWindows (VPNW_ API), extracted from PinUp's desktop output so every plugin can present a Unity render texture in a borderless desktop window: Direct3D11/12 with DirectComposition on Windows, Metal on macOS, Vulkan/X11 on Linux. Sources and fixtures live in Native~/NativeWindows. Binaries are built locally and ignored, like NativeInput. Managed side in VisualPinball.Unity: NativeWindowSurface (placement, z-order, placement mode, diagnostics; reports unsupported instead of throwing when the library is missing) and NativeWindowHost, one shared presenter issuing a draw/present event pair per frame and draining deferred GPU releases. --- Native~/NativeWindows/Desktop/CMakeLists.txt | 64 ++ Native~/NativeWindows/Desktop/DesktopLayout.h | 49 + .../NativeWindows/Desktop/DesktopTests.cpp | 279 ++++++ .../Desktop/Linux/VulkanWindows.cpp | 367 ++++++++ .../Desktop/Linux/X11Windows.cpp | 164 ++++ .../NativeWindows/Desktop/Linux/X11Windows.h | 22 + .../Desktop/Mac/MacCoordinates.h | 63 ++ .../NativeWindows/Desktop/Mac/MetalWindows.mm | 209 +++++ Native~/NativeWindows/Desktop/WindowBackend.h | 41 + .../NativeWindows/Desktop/WindowRegistry.cpp | 168 ++++ Native~/NativeWindows/README.md | 107 +++ .../VisualPinballNativeWindows.h | 86 ++ Native~/NativeWindows/Windows/Build.ps1 | 51 ++ .../NativeWindows/Windows/D3D12CopyBridge.cpp | 204 +++++ .../NativeWindows/Windows/D3D12CopyBridge.h | 70 ++ .../Windows/NativeWindowTests.cpp | 323 +++++++ .../Windows/VisualPinballNativeWindows.cpp | 855 ++++++++++++++++++ .../libVisualPinballNativeWindows.so.meta | 33 + .../VisualPinballNativeWindows.dll.meta | 50 + .../VisualPinball.Unity/NativeWindows.meta | 8 + .../NativeWindows/NativeWindowCapabilities.cs | 27 + .../NativeWindowCapabilities.cs.meta | 11 + .../NativeWindows/NativeWindowDiagnostics.cs | 40 + .../NativeWindowDiagnostics.cs.meta | 11 + .../NativeWindows/NativeWindowHost.cs | 106 +++ .../NativeWindows/NativeWindowHost.cs.meta | 11 + .../NativeWindows/NativeWindowSurface.cs | 305 +++++++ .../NativeWindows/NativeWindowSurface.cs.meta | 11 + 28 files changed, 3735 insertions(+) create mode 100644 Native~/NativeWindows/Desktop/CMakeLists.txt create mode 100644 Native~/NativeWindows/Desktop/DesktopLayout.h create mode 100644 Native~/NativeWindows/Desktop/DesktopTests.cpp create mode 100644 Native~/NativeWindows/Desktop/Linux/VulkanWindows.cpp create mode 100644 Native~/NativeWindows/Desktop/Linux/X11Windows.cpp create mode 100644 Native~/NativeWindows/Desktop/Linux/X11Windows.h create mode 100644 Native~/NativeWindows/Desktop/Mac/MacCoordinates.h create mode 100644 Native~/NativeWindows/Desktop/Mac/MetalWindows.mm create mode 100644 Native~/NativeWindows/Desktop/WindowBackend.h create mode 100644 Native~/NativeWindows/Desktop/WindowRegistry.cpp create mode 100644 Native~/NativeWindows/README.md create mode 100644 Native~/NativeWindows/VisualPinballNativeWindows.h create mode 100644 Native~/NativeWindows/Windows/Build.ps1 create mode 100644 Native~/NativeWindows/Windows/D3D12CopyBridge.cpp create mode 100644 Native~/NativeWindows/Windows/D3D12CopyBridge.h create mode 100644 Native~/NativeWindows/Windows/NativeWindowTests.cpp create mode 100644 Native~/NativeWindows/Windows/VisualPinballNativeWindows.cpp create mode 100644 VisualPinball.Unity/Plugins/linux-x64/libVisualPinballNativeWindows.so.meta create mode 100644 VisualPinball.Unity/Plugins/win-x64/VisualPinballNativeWindows.dll.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/NativeWindows.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs.meta diff --git a/Native~/NativeWindows/Desktop/CMakeLists.txt b/Native~/NativeWindows/Desktop/CMakeLists.txt new file mode 100644 index 000000000..62382b876 --- /dev/null +++ b/Native~/NativeWindows/Desktop/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.22) +if(APPLE) + set(CMAKE_OSX_DEPLOYMENT_TARGET "14.0" CACHE STRING "Minimum macOS version") + set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures (universal binary by default)") +endif() +project(VisualPinballNativeWindows LANGUAGES C CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() +set(UNITY_PLUGIN_API "" CACHE PATH "Installed Unity Editor Data/PluginAPI directory") +option(VPNW_BUILD_TESTS "Build native window fixtures" ON) +option(VPNW_DEPLOY "Copy the built library into VisualPinball.Unity/Plugins" ON) +if(NOT EXISTS "${UNITY_PLUGIN_API}/IUnityInterface.h") + message(FATAL_ERROR "Set UNITY_PLUGIN_API to the installed Unity PluginAPI directory.") +endif() +get_filename_component(VPNW_API_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE) +get_filename_component(VPNW_PACKAGE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) +set(SOURCES WindowRegistry.cpp) +if(APPLE) + enable_language(OBJCXX) + list(APPEND SOURCES Mac/MetalWindows.mm) + set(PLATFORM_LIBRARIES "-framework Cocoa" "-framework QuartzCore" "-framework Metal" "-framework ApplicationServices") + set(VPNW_PLUGIN_DIRECTORY "${VPNW_PACKAGE_ROOT}/VisualPinball.Unity/Plugins/osx" CACHE PATH "Deployment directory") +elseif(UNIX) + find_package(Vulkan REQUIRED) + find_package(X11 REQUIRED) + if(NOT X11_Xrender_FOUND) + message(FATAL_ERROR "The X Rendering Extension development files (libxrender-dev) are required.") + endif() + list(APPEND SOURCES Linux/X11Windows.cpp Linux/VulkanWindows.cpp) + set(PLATFORM_LIBRARIES Vulkan::Vulkan X11::X11 X11::Xrender dl pthread m) + set(VPNW_PLUGIN_DIRECTORY "${VPNW_PACKAGE_ROOT}/VisualPinball.Unity/Plugins/linux-x64" CACHE PATH "Deployment directory") +else() + message(FATAL_ERROR "This CMake project builds the macOS and Linux library; Windows uses Native~/NativeWindows/Windows/Build.ps1.") +endif() +add_library(VisualPinballNativeWindows SHARED ${SOURCES}) +target_include_directories(VisualPinballNativeWindows PRIVATE "${UNITY_PLUGIN_API}" "${VPNW_API_DIRECTORY}" "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(VisualPinballNativeWindows PRIVATE ${PLATFORM_LIBRARIES}) +target_compile_options(VisualPinballNativeWindows PRIVATE -Wall -Wextra -Werror -Wno-missing-field-initializers) +if(APPLE) + target_compile_options(VisualPinballNativeWindows PRIVATE -fobjc-arc) + set_target_properties(VisualPinballNativeWindows PROPERTIES PREFIX "lib" SUFFIX ".dylib") +endif() +if(VPNW_DEPLOY) + add_custom_command(TARGET VisualPinballNativeWindows POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E make_directory "${VPNW_PLUGIN_DIRECTORY}" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "${VPNW_PLUGIN_DIRECTORY}/" + COMMENT "Deploying VisualPinballNativeWindows to ${VPNW_PLUGIN_DIRECTORY}") +endif() +if(VPNW_BUILD_TESTS) + enable_testing() + add_executable(NativeWindowTests DesktopTests.cpp) + if(APPLE) + set_source_files_properties(DesktopTests.cpp PROPERTIES LANGUAGE OBJCXX) + target_compile_options(NativeWindowTests PRIVATE -fobjc-arc) + endif() + target_include_directories(NativeWindowTests PRIVATE "${UNITY_PLUGIN_API}" "${VPNW_API_DIRECTORY}" "${CMAKE_CURRENT_SOURCE_DIR}") + target_link_libraries(NativeWindowTests PRIVATE VisualPinballNativeWindows ${PLATFORM_LIBRARIES}) + add_test(NAME NativeWindows COMMAND NativeWindowTests) +endif() diff --git a/Native~/NativeWindows/Desktop/DesktopLayout.h b/Native~/NativeWindows/Desktop/DesktopLayout.h new file mode 100644 index 000000000..165190174 --- /dev/null +++ b/Native~/NativeWindows/Desktop/DesktopLayout.h @@ -0,0 +1,49 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#pragma once +#include +#include +#include +#include + +namespace vpnw +{ + struct DesktopRect { double x, y, width, height; }; + struct Monitor { DesktopRect logical; double pixelWidth, pixelHeight; }; + // Anchor the primary display at (0,0), then preserve monitor adjacency in physical pixels. + // Scaling every global origin by the primary DPI creates gaps/overlaps on mixed-DPI desktops. + inline std::vector PhysicalLayout(const std::vector& monitors) + { + std::vector result(monitors.size()); if (monitors.empty()) return result; + std::vector placed(monitors.size(), false); placed[0] = true; + result[0] = { 0, 0, monitors[0].pixelWidth, monitors[0].pixelHeight }; + for (std::size_t count = 1; count < monitors.size(); count++) + { + std::size_t target = 0, anchor = 0; double nearest = std::numeric_limits::infinity(), shared = -1; + for (std::size_t i = 0; i < monitors.size(); i++) if (!placed[i]) + for (std::size_t j = 0; j < monitors.size(); j++) if (placed[j]) + { + auto a = monitors[j].logical, b = monitors[i].logical; + double dx = std::max({ a.x - b.x - b.width, b.x - a.x - a.width, 0.0 }); + double dy = std::max({ a.y - b.y - b.height, b.y - a.y - a.height, 0.0 }); + double distance = dx * dx + dy * dy; + double overlap = std::max(0.0, std::min(a.x + a.width, b.x + b.width) - std::max(a.x, b.x)) + + std::max(0.0, std::min(a.y + a.height, b.y + b.height) - std::max(a.y, b.y)); + if (distance < nearest || (distance == nearest && overlap > shared)) { nearest = distance; shared = overlap; target = i; anchor = j; } + } + auto parent = monitors[anchor].logical, child = monitors[target].logical, pixels = result[anchor]; + double sx = monitors[target].pixelWidth / child.width, sy = monitors[target].pixelHeight / child.height; + DesktopRect rectangle{ pixels.x + (child.x - parent.x) * sx, pixels.y + (child.y - parent.y) * sy, monitors[target].pixelWidth, monitors[target].pixelHeight }; + if (child.x >= parent.x + parent.width) rectangle.x = pixels.x + pixels.width + (child.x - parent.x - parent.width) * sx; + else if (child.x + child.width <= parent.x) rectangle.x = pixels.x - (parent.x - child.x - child.width) * sx - rectangle.width; + else if (child.y >= parent.y + parent.height) rectangle.y = pixels.y + pixels.height + (child.y - parent.y - parent.height) * sy; + else if (child.y + child.height <= parent.y) rectangle.y = pixels.y - (parent.y - child.y - child.height) * sy - rectangle.height; + result[target] = rectangle; placed[target] = true; + } + return result; + } +} diff --git a/Native~/NativeWindows/Desktop/DesktopTests.cpp b/Native~/NativeWindows/Desktop/DesktopTests.cpp new file mode 100644 index 000000000..1f6cbc732 --- /dev/null +++ b/Native~/NativeWindows/Desktop/DesktopTests.cpp @@ -0,0 +1,279 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#include "IUnityGraphics.h" +#if !defined(__APPLE__) +#define VK_USE_PLATFORM_XLIB_KHR +#include "IUnityGraphicsVulkan.h" +#include +#include +#else +#import +#import +#include "IUnityGraphicsMetal.h" +#endif +#include "VisualPinballNativeWindows.h" +#include "DesktopLayout.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + std::string Error() + { + std::vector text(static_cast(VPNW_GetLastError(nullptr, 0))); + VPNW_GetLastError(text.data(), static_cast(text.size())); return text.data(); + } + void Require(bool value, const char* message) { if (!value) throw std::runtime_error(std::string(message) + ": " + Error()); } + IUnityGraphics graphics{}; + IUnityInterfaces interfaces{}; + bool loaded = false; + bool gpuTested = false; + void UNITY_INTERFACE_API Register(IUnityGraphicsDeviceEventCallback) {} + void UNITY_INTERFACE_API Unregister(IUnityGraphicsDeviceEventCallback) {} + int UNITY_INTERFACE_API Reserve(int) { return 49000; } + +#if !defined(__APPLE__) + IUnityGraphicsVulkan graphicsVulkan{}; + UnityVulkanInstance native{}; + UnityVulkanImage image{}; + UnityGfxRenderer UNITY_INTERFACE_API Renderer() { return kUnityGfxRendererVulkan; } + UnityVulkanInstance UNITY_INTERFACE_API Instance() { return native; } + void UNITY_INTERFACE_API Configure(int event, const UnityVulkanPluginEventConfig* config) + { + if (event == VPNW_GetPresentEventId()) Require(config->graphicsQueueAccess == kUnityVulkanGraphicsQueueAccess_Allow && (config->flags & kUnityVulkanEventConfigFlag_FlushCommandBuffers), "Presentation requires flushed queue access"); + } + bool UNITY_INTERFACE_API Access(void* texture, const VkImageSubresource*, VkImageLayout layout, VkPipelineStageFlags, VkAccessFlags, UnityVulkanResourceAccessMode, UnityVulkanImage* output) + { + if (texture != &image) return false; + *output = image; output->layout = layout; return true; + } + void Check(VkResult result) { if (result != VK_SUCCESS) throw std::runtime_error("Vulkan fixture: " + std::to_string(result)); } + void StartGraphics() + { + const char* extensions[] = { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_XLIB_SURFACE_EXTENSION_NAME }; + VkApplicationInfo application{ VK_STRUCTURE_TYPE_APPLICATION_INFO }; application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo create{ VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO }; create.pApplicationInfo = &application; + create.enabledExtensionCount = 2; create.ppEnabledExtensionNames = extensions; + Check(vkCreateInstance(&create, nullptr, &native.instance)); std::uint32_t count = 0; + Check(vkEnumeratePhysicalDevices(native.instance, &count, nullptr)); Require(count > 0, "A Vulkan device is required"); + std::vector devices(count); Check(vkEnumeratePhysicalDevices(native.instance, &count, devices.data())); native.physicalDevice = devices.front(); + vkGetPhysicalDeviceQueueFamilyProperties(native.physicalDevice, &count, nullptr); std::vector families(count); + vkGetPhysicalDeviceQueueFamilyProperties(native.physicalDevice, &count, families.data()); + for (std::uint32_t i = 0; i < count; i++) if (families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { native.queueFamilyIndex = i; break; } + float priority = 1; VkDeviceQueueCreateInfo queue{ VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO }; + queue.queueFamilyIndex = native.queueFamilyIndex; queue.queueCount = 1; queue.pQueuePriorities = &priority; + const char* deviceExtensions[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + VkDeviceCreateInfo device{ VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO }; device.queueCreateInfoCount = 1; device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = 1; device.ppEnabledExtensionNames = deviceExtensions; + Check(vkCreateDevice(native.physicalDevice, &device, nullptr, &native.device)); vkGetDeviceQueue(native.device, native.queueFamilyIndex, 0, &native.graphicsQueue); + native.getInstanceProcAddr = vkGetInstanceProcAddr; + graphicsVulkan.ConfigureEvent = Configure; graphicsVulkan.Instance = Instance; graphicsVulkan.AccessTexture = Access; + } + VkDeviceMemory memory = VK_NULL_HANDLE; + void* Source(bool srgb = false) + { + UnityVulkanImage previous = image; VkDeviceMemory previousMemory = memory; + VkImageCreateInfo create{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + create.imageType = VK_IMAGE_TYPE_2D; create.format = srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM; create.extent = { 64, 32, 1 }; + create.mipLevels = create.arrayLayers = 1; create.samples = VK_SAMPLE_COUNT_1_BIT; create.tiling = VK_IMAGE_TILING_OPTIMAL; + create.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; create.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + Check(vkCreateImage(native.device, &create, nullptr, &image.image)); VkMemoryRequirements requirement{}; + vkGetImageMemoryRequirements(native.device, image.image, &requirement); VkPhysicalDeviceMemoryProperties properties{}; + vkGetPhysicalDeviceMemoryProperties(native.physicalDevice, &properties); VkMemoryAllocateInfo allocation{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; + allocation.allocationSize = requirement.size; + for (std::uint32_t i = 0; i < properties.memoryTypeCount; i++) if (requirement.memoryTypeBits & (1u << i)) { allocation.memoryTypeIndex = i; break; } + Check(vkAllocateMemory(native.device, &allocation, nullptr, &memory)); Check(vkBindImageMemory(native.device, image.image, memory, 0)); + VkCommandPool pool = VK_NULL_HANDLE; VkCommandPoolCreateInfo poolInfo{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; poolInfo.queueFamilyIndex = native.queueFamilyIndex; + Check(vkCreateCommandPool(native.device, &poolInfo, nullptr, &pool)); VkCommandBuffer command; + VkCommandBufferAllocateInfo buffers{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; buffers.commandPool = pool; buffers.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; buffers.commandBufferCount = 1; + Check(vkAllocateCommandBuffers(native.device, &buffers, &command)); VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; Check(vkBeginCommandBuffer(command, &begin)); + VkImageMemoryBarrier barrier{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; barrier.image = image.image; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkClearColorValue green{ { 0, 128.0f / 255, 0, 128.0f / 255 } }; + if (previous.image) + { + VkImageCopy copy{}; copy.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.dstSubresource = copy.srcSubresource; copy.extent = create.extent; + vkCmdCopyImage(command, previous.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + } + else vkCmdClearColorImage(command, image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &green, 1, &barrier.subresourceRange); + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + Check(vkEndCommandBuffer(command)); VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; submit.commandBufferCount = 1; submit.pCommandBuffers = &command; + Check(vkQueueSubmit(native.graphicsQueue, 1, &submit, VK_NULL_HANDLE)); Check(vkQueueWaitIdle(native.graphicsQueue)); vkDestroyCommandPool(native.device, pool, nullptr); + if (previous.image) { vkDestroyImage(native.device, previous.image, nullptr); vkFreeMemory(native.device, previousMemory, nullptr); } + image.layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; image.extent = create.extent; image.format = create.format; image.samples = VK_SAMPLE_COUNT_1_BIT; + image.aspect = VK_IMAGE_ASPECT_COLOR_BIT; image.usage = create.usage; image.layers = image.mipCount = 1; return ℑ + } + void Flush() { Check(vkQueueWaitIdle(native.graphicsQueue)); } + void StopGraphics() { vkDestroyImage(native.device, image.image, nullptr); vkFreeMemory(native.device, memory, nullptr); vkDestroyDevice(native.device, nullptr); vkDestroyInstance(native.instance, nullptr); } +#else + IUnityGraphicsMetal graphicsMetal{}; + id device = nil; id queue = nil; id command = nil; id texture = nil; + UnityGfxRenderer UNITY_INTERFACE_API Renderer() { return kUnityGfxRendererMetal; } + id UNITY_INTERFACE_API Device() { return device; } + id UNITY_INTERFACE_API Queue() { return queue; } + id UNITY_INTERFACE_API Command() { return command; } + void UNITY_INTERFACE_API EndEncoder() {} + void StartGraphics() + { + [NSApplication sharedApplication]; device = MTLCreateSystemDefaultDevice(); queue = [device newCommandQueue]; + graphicsMetal.MetalDevice = Device; graphicsMetal.CurrentCommandBuffer = Command; graphicsMetal.EndCurrentCommandEncoder = EndEncoder; + } + void* Source(bool srgb = false) + { + MTLTextureDescriptor* description = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:srgb ? MTLPixelFormatRGBA8Unorm_sRGB : MTLPixelFormatRGBA8Unorm width:64 height:32 mipmapped:NO]; + description.storageMode = MTLStorageModeShared; description.usage = MTLTextureUsageShaderRead; + texture = [device newTextureWithDescriptor:description]; std::vector bytes(64 * 32 * 4); + for (std::size_t i = 0; i < bytes.size(); i += 4) { bytes[i + 1] = bytes[i + 3] = 128; } + [texture replaceRegion:MTLRegionMake2D(0, 0, 64, 32) mipmapLevel:0 withBytes:bytes.data() bytesPerRow:64 * 4]; return (__bridge void*)texture; + } + void Flush() { [command commit]; [command waitUntilCompleted]; Require(command.status != MTLCommandBufferStatusError, "Metal GPU work completed without an error"); command = nil; } + void StopGraphics() { texture = nil; queue = nil; device = nil; } +#endif + IUnityInterface* UNITY_INTERFACE_API Interface(UnityInterfaceGUID guid) + { + auto same = [&](UnityInterfaceGUID other) { return guid.m_GUIDHigh == other.m_GUIDHigh && guid.m_GUIDLow == other.m_GUIDLow; }; + if (same(GetUnityInterfaceGUID())) return &graphics; +#if !defined(__APPLE__) + if (same(GetUnityInterfaceGUID())) return &graphicsVulkan; +#else + if (same(GetUnityInterfaceGUID())) return &graphicsMetal; +#endif + return nullptr; + } + void Coordinates() + { + auto layout = vpnw::PhysicalLayout({ {{0, 0, 1920, 1080}, 3840, 2160}, {{-1920, 0, 1920, 1080}, 1920, 1080}, {{1920, 0, 1920, 1080}, 1920, 1080}, {{0, -1080, 1920, 1080}, 1920, 1080} }); + Require(layout[1].x == -1920 && layout[1].width == 1920, "Standard-DPI monitor left of Retina primary"); + Require(layout[2].x == 3840 && layout[2].width == 1920, "Standard-DPI monitor right of Retina primary"); + Require(layout[3].y == -1080 && layout[3].height == 1080, "Monitor above primary uses negative physical origin"); + } +#if !defined(__APPLE__) + // Test-only lookup and readback of the plugin's own window; production code never reads pixels back. + std::string Title(Display* connection, Window window) + { + char* name = nullptr; std::string result; + if (XFetchName(connection, window, &name) && name) { result = name; XFree(name); } + return result; + } + Window FindTitled(Display* connection, Window parent, const std::string& wanted, int depth) + { + if (depth > 16) return None; + Window root = None, actualParent = None, *children = nullptr; unsigned int count = 0; + if (!XQueryTree(connection, parent, &root, &actualParent, &children, &count)) return None; + Window found = None; + for (unsigned int i = 0; i < count && !found; i++) + found = Title(connection, children[i]) == wanted ? children[i] : FindTitled(connection, children[i], wanted, depth + 1); + if (children) XFree(children); + return found; + } + unsigned char Channel(unsigned long pixel, unsigned long mask) + { + if (!mask) return 0; + unsigned int shift = 0; while ((mask & 1) == 0) { mask >>= 1; shift++; } + return static_cast(((pixel >> shift) & mask) * 255 / mask); + } + // Reads the BGRA centre pixel of the plugin's window through a separate X11 connection. + bool CenterPixel(unsigned char (&output)[4]) + { + Display* connection = XOpenDisplay(nullptr); Require(connection != nullptr, "Open fixture X11 connection"); + bool captured = false; + Window window = FindTitled(connection, DefaultRootWindow(connection), "Visual Pinball Window", 0); + XWindowAttributes attributes{}; + if (window && XGetWindowAttributes(connection, window, &attributes) && attributes.map_state == IsViewable) + { + XImage* image = XGetImage(connection, window, attributes.width / 2, attributes.height / 2, 1, 1, AllPlanes, ZPixmap); + if (image) + { + unsigned long pixel = XGetPixel(image, 0, 0); + output[0] = Channel(pixel, image->blue_mask); output[1] = Channel(pixel, image->green_mask); output[2] = Channel(pixel, image->red_mask); + output[3] = attributes.depth == 32 ? static_cast(pixel >> 24) : 255; + XDestroyImage(image); captured = true; + } + } + XCloseDisplay(connection); return captured; + } +#endif + void Pixels() + { +#if !defined(__APPLE__) + // Some virtual X servers (Xvfb, WSLg) do not expose Vulkan-presented contents to XGetImage. + if (const char* skip = std::getenv("VPNW_SKIP_PIXEL_CHECK"); skip && *skip == '1') { std::cout << "Pixel readback skipped (VPNW_SKIP_PIXEL_CHECK=1).\n"; return; } + unsigned char pixel[4]{}; + for (int attempt = 0; attempt < 100; attempt++) + { + Require(CenterPixel(pixel), "Find the native window by title and capture its presented GPU pixels"); + if (pixel[1] != 0) break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + const std::string observed = "GPU output preserves encoded green and alpha bytes (BGRA " + std::to_string(pixel[0]) + "," + + std::to_string(pixel[1]) + "," + std::to_string(pixel[2]) + "," + std::to_string(pixel[3]) + ")"; + Require(pixel[0] == 0 && pixel[1] == 128 && pixel[2] == 0 && pixel[3] == 128, observed.c_str()); +#endif + } + void Windows() + { + StartGraphics(); +#if defined(__APPLE__) + if (!device) { std::cout << "Metal GPU unavailable on this host; coordinate fixtures still execute.\n"; return; } +#endif + graphics.GetRenderer = Renderer; graphics.RegisterDeviceEventCallback = Register; graphics.UnregisterDeviceEventCallback = Unregister; graphics.ReserveEventIDRange = Reserve; + interfaces.GetInterface = Interface; UnityPluginLoad(&interfaces); loaded = true; + const std::uint32_t capabilities = VPNW_CapabilityWindows | VPNW_CapabilityPlacement | VPNW_CapabilityTopmost | VPNW_CapabilityTransparency; + Require(VPNW_GetCapabilities() == capabilities, "Window, placement, topmost and transparency capabilities"); + int id = VPNW_CreateWindow(-80, 24, 64, 32); Require(id > 0, "Create borderless window at negative origin"); + VPNW_Diagnostics diagnostics{}; diagnostics.structBytes = sizeof(diagnostics); + Require(VPNW_GetDiagnostics(id, &diagnostics) && diagnostics.left == -80 && diagnostics.top == 24 && diagnostics.visible == 0, "Physical geometry and initially hidden window"); + Require(VPNW_ConfigureWindow(id, 20, 24, 96, 48, 1, 1), "Position, resize, visibility, and topmost"); + auto render = reinterpret_cast(VPNW_GetRenderEventFunc()); + for (bool srgb : { false, true }) + { + Require(VPNW_SetTexture(id, Source(srgb), 0), "Bind native GPU texture"); + for (int frame = 0; frame < 8; frame++) + { +#if defined(__APPLE__) + command = [queue commandBuffer]; +#endif + render(VPNW_GetDrawEventId()); render(VPNW_GetPresentEventId()); Flush(); VPNW_Pump(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Require(VPNW_SetPlacementMode(id, 1) && VPNW_SetPlacementMode(id, 0), "Toggle native placement interaction"); + Require(VPNW_GetDiagnostics(id, &diagnostics), "Read presentation diagnostics"); + std::cout << "srgb=" << srgb << " renderer=" << diagnostics.graphicsRenderer << " visible=" << diagnostics.visible << " dpi=" << diagnostics.dpi << + " rect=" << diagnostics.left << ',' << diagnostics.top << ' ' << diagnostics.windowWidth << 'x' << diagnostics.windowHeight << + " presents=" << diagnostics.presentedFrames << " skips=" << diagnostics.skippedFrames << " result=" << diagnostics.lastResult << " error='" << Error() << "'" << std::endl; + Pixels(); + } + Require(VPNW_GetDiagnostics(id, &diagnostics) && diagnostics.presentedFrames > 0 && diagnostics.premultipliedAlpha == 1, "GPU submission and premultiplied transparency"); + Require(diagnostics.sourceWidth == 64 && diagnostics.sourceHeight == 32, "Native source dimensions"); + Require(VPNW_SetZOrder(id, 0) && VPNW_SetZOrder(id, 1) && VPNW_SetZOrder(id, 3), "Back/front/topmost operations"); + Require(VPNW_SetTexture(id, nullptr, 0), "Detach native texture"); Require(VPNW_DestroyWindow(id) && VPNW_DestroyWindow(id), "Idempotent destruction"); + render(VPNW_GetPresentEventId()); Require(VPNW_GetPendingResourceCount() == 0, "Deferred GPU retirement"); + UnityPluginUnload(); loaded = false; StopGraphics(); + gpuTested = true; + } +} + +int main() +{ + try + { + Coordinates(); Windows(); + std::cout << "Native coordinate fixtures passed. " << (gpuTested ? "GPU window fixtures passed.\n" : "GPU window fixtures skipped.\n"); return 0; + } + catch (const std::exception& error) { std::cerr << error.what() << '\n'; if (loaded) { UnityPluginUnload(); StopGraphics(); } return 1; } +} diff --git a/Native~/NativeWindows/Desktop/Linux/VulkanWindows.cpp b/Native~/NativeWindows/Desktop/Linux/VulkanWindows.cpp new file mode 100644 index 000000000..30c343a24 --- /dev/null +++ b/Native~/NativeWindows/Desktop/Linux/VulkanWindows.cpp @@ -0,0 +1,367 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#define VK_USE_PLATFORM_XLIB_KHR +#include "X11Windows.h" +#include "IUnityGraphicsVulkan.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + IUnityGraphicsVulkan* vulkan = nullptr; + UnityVulkanInstance instance{}; + PFN_vkGetInstanceProcAddr loader = nullptr; + VKAPI_ATTR VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo* requested, const VkAllocationCallbacks* allocator, VkInstance* result) + { + try + { + VkInstanceCreateInfo create = *requested; + std::vector extensions(requested->ppEnabledExtensionNames, requested->ppEnabledExtensionNames + requested->enabledExtensionCount); + for (const char* name : { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_XLIB_SURFACE_EXTENSION_NAME }) + if (std::none_of(extensions.begin(), extensions.end(), [&](const char* value) { return std::strcmp(value, name) == 0; })) extensions.push_back(name); + create.enabledExtensionCount = static_cast(extensions.size()); create.ppEnabledExtensionNames = extensions.data(); + return reinterpret_cast(loader(VK_NULL_HANDLE, "vkCreateInstance"))(&create, allocator, result); + } + catch (...) { return VK_ERROR_OUT_OF_HOST_MEMORY; } + } + VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL GetInstanceProcAddr(VkInstance value, const char* name) + { + if (!value && std::strcmp(name, "vkCreateInstance") == 0) return reinterpret_cast(CreateInstance); + return loader(value, name); + } + PFN_vkGetInstanceProcAddr UNITY_INTERFACE_API Intercept(PFN_vkGetInstanceProcAddr original, void*) { loader = original; return GetInstanceProcAddr; } + void Check(VkResult result, const char* operation) + { + if (result != VK_SUCCESS) throw std::runtime_error(std::string(operation) + ": Vulkan result " + std::to_string(result)); + } + struct Frame + { + VkCommandBuffer command = VK_NULL_HANDLE; + VkFence fence = VK_NULL_HANDLE; + VkSemaphore acquired = VK_NULL_HANDLE; + VkImage raw = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkExtent2D size{}; + VkFormat format = VK_FORMAT_UNDEFINED; + bool initialized = false; + }; + class VulkanWindow final : public vpnw::WindowBackend + { + std::mutex mutex; + Window window = None; + VkSurfaceKHR surface = VK_NULL_HANDLE; + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + VkCommandPool pool = VK_NULL_HANDLE; + std::array frames{}; + std::vector images; + // Present semaphores are owned by swapchain images, so reacquisition proves their prior wait has finished. + std::vector presented; + std::vector initialized; + VkExtent2D extent{}; + VkFormat format = VK_FORMAT_UNDEFINED; + void* texture = nullptr; + UnityVulkanImage source{}; + bool sourceReady = false, flip = false, alive = true, visible = false, topmost = false, rebuild = true, placement = false; + int left = 0, top = 0, width = 1, height = 1; + std::size_t nextFrame = 0; + std::uint64_t submitted = 0, skipped = 0; + int lastResult = 0; + + bool FramesReady() const + { + for (const auto& frame : frames) if (frame.fence && vkGetFenceStatus(instance.device, frame.fence) == VK_NOT_READY) return false; + return true; + } + void ReleaseSwapchain() + { + if (!swapchain) return; + // This runs only on resize, retirement, or graphics shutdown, after copy fences finish. + // Queue idling also retires presentation waits; ordinary frames never wait for GPU completion. + VkResult idle = vkQueueWaitIdle(instance.graphicsQueue); + if (idle != VK_ERROR_DEVICE_LOST) Check(idle, "Retire desktop presentation"); + for (auto semaphore : presented) if (semaphore) vkDestroySemaphore(instance.device, semaphore, nullptr); + presented.clear(); images.clear(); initialized.clear(); + vkDestroySwapchainKHR(instance.device, swapchain, nullptr); swapchain = VK_NULL_HANDLE; + } + void BuildSwapchain() + { + VkSurfaceCapabilitiesKHR capabilities{}; + Check(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(instance.physicalDevice, surface, &capabilities), "Query desktop surface"); + if (!(capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT)) throw std::runtime_error("The desktop surface cannot receive GPU texture copies."); + std::uint32_t count = 0; + Check(vkGetPhysicalDeviceSurfaceFormatsKHR(instance.physicalDevice, surface, &count, nullptr), "Query desktop formats"); + std::vector formats(count); + Check(vkGetPhysicalDeviceSurfaceFormatsKHR(instance.physicalDevice, surface, &count, formats.data()), "Query desktop formats"); + VkSurfaceFormatKHR selected{ VK_FORMAT_UNDEFINED, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }; + for (const auto& candidate : formats) + if ((candidate.format == VK_FORMAT_B8G8R8A8_UNORM || candidate.format == VK_FORMAT_R8G8B8A8_UNORM) && candidate.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { selected = candidate; break; } + if (selected.format == VK_FORMAT_UNDEFINED) throw std::runtime_error("The desktop surface has no encoded SDR UNORM format."); + VkCompositeAlphaFlagBitsKHR alpha = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR; + if (!(capabilities.supportedCompositeAlpha & alpha)) + { + if (capabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR) alpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR; + else throw std::runtime_error("The desktop compositor cannot preserve premultiplied transparency."); + } + VkExtent2D size = capabilities.currentExtent; + if (size.width == UINT32_MAX) + { + size.width = std::clamp(static_cast(width), capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + size.height = std::clamp(static_cast(height), capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + } + std::uint32_t imageCount = capabilities.minImageCount + 1; + if (capabilities.maxImageCount && imageCount > capabilities.maxImageCount) imageCount = capabilities.maxImageCount; + VkPresentModeKHR mode = VK_PRESENT_MODE_FIFO_KHR; + Check(vkGetPhysicalDeviceSurfacePresentModesKHR(instance.physicalDevice, surface, &count, nullptr), "Query presentation modes"); + std::vector modes(count); + Check(vkGetPhysicalDeviceSurfacePresentModesKHR(instance.physicalDevice, surface, &count, modes.data()), "Query presentation modes"); + for (auto candidate : modes) if (candidate == VK_PRESENT_MODE_IMMEDIATE_KHR) mode = candidate; + VkSwapchainCreateInfoKHR create{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR }; + create.surface = surface; create.minImageCount = imageCount; create.imageFormat = selected.format; create.imageColorSpace = selected.colorSpace; + create.imageExtent = size; create.imageArrayLayers = 1; create.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT; + create.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; create.preTransform = capabilities.currentTransform; + create.compositeAlpha = alpha; create.presentMode = mode; create.clipped = VK_TRUE; + Check(vkCreateSwapchainKHR(instance.device, &create, nullptr, &swapchain), "Create desktop swapchain"); + Check(vkGetSwapchainImagesKHR(instance.device, swapchain, &count, nullptr), "Query swapchain images"); + images.resize(count); presented.resize(count); initialized.assign(count, false); + Check(vkGetSwapchainImagesKHR(instance.device, swapchain, &count, images.data()), "Query swapchain images"); + VkSemaphoreCreateInfo semaphore{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; + for (auto& ready : presented) Check(vkCreateSemaphore(instance.device, &semaphore, nullptr, &ready), "Create presentation semaphore"); + extent = size; format = selected.format; rebuild = false; + } + VkImage EncodedSource(Frame& frame) + { + if (source.format != VK_FORMAT_R8G8B8A8_SRGB && source.format != VK_FORMAT_B8G8R8A8_SRGB) return source.image; + VkFormat rawFormat = source.format == VK_FORMAT_R8G8B8A8_SRGB ? VK_FORMAT_R8G8B8A8_UNORM : VK_FORMAT_B8G8R8A8_UNORM; + if (!frame.raw || frame.size.width != source.extent.width || frame.size.height != source.extent.height || frame.format != rawFormat) + { + if (frame.raw) vkDestroyImage(instance.device, frame.raw, nullptr); + if (frame.memory) vkFreeMemory(instance.device, frame.memory, nullptr); + frame.raw = VK_NULL_HANDLE; frame.memory = VK_NULL_HANDLE; frame.initialized = false; + VkImageCreateInfo create{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO }; + create.imageType = VK_IMAGE_TYPE_2D; create.format = rawFormat; create.extent = source.extent; + create.mipLevels = create.arrayLayers = 1; create.samples = VK_SAMPLE_COUNT_1_BIT; create.tiling = VK_IMAGE_TILING_OPTIMAL; + create.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; create.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + Check(vkCreateImage(instance.device, &create, nullptr, &frame.raw), "Create encoded desktop snapshot"); + VkMemoryRequirements requirements{}; vkGetImageMemoryRequirements(instance.device, frame.raw, &requirements); + VkPhysicalDeviceMemoryProperties properties{}; vkGetPhysicalDeviceMemoryProperties(instance.physicalDevice, &properties); + VkMemoryAllocateInfo allocation{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO }; allocation.allocationSize = requirements.size; + bool selected = false; + for (std::uint32_t i = 0; i < properties.memoryTypeCount; i++) + if (requirements.memoryTypeBits & (1u << i)) + { + if (!selected || (properties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) allocation.memoryTypeIndex = i; + selected = true; if (properties.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) break; + } + if (!selected) throw std::runtime_error("No device memory for encoded desktop snapshot."); + Check(vkAllocateMemory(instance.device, &allocation, nullptr, &frame.memory), "Allocate encoded desktop snapshot"); + Check(vkBindImageMemory(instance.device, frame.raw, frame.memory, 0), "Bind encoded desktop snapshot"); + frame.size = { source.extent.width, source.extent.height }; frame.format = rawFormat; + } + VkImageMemoryBarrier barrier{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; barrier.image = frame.raw; + barrier.oldLayout = frame.initialized ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; barrier.srcAccessMask = frame.initialized ? VK_ACCESS_TRANSFER_READ_BIT : 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vkCmdPipelineBarrier(frame.command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkImageCopy copy{}; copy.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.dstSubresource = copy.srcSubresource; copy.extent = source.extent; + // Copy preserves raw sRGB bytes; a direct sRGB blit would decode them into linear color. + vkCmdCopyImage(frame.command, source.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, frame.raw, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + vkCmdPipelineBarrier(frame.command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + frame.initialized = true; return frame.raw; + } + public: + VulkanWindow(int x, int y, int w, int h) : left(x), top(y), width(w), height(h) + { + try + { + if (!vulkan || !instance.device) throw std::runtime_error("Unity Vulkan is required for Linux desktop output."); + window = vpnw::XCreateBorderless(x, y, w, h); + VkXlibSurfaceCreateInfoKHR create{ VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR }; + create.dpy = vpnw::XDisplay(); create.window = window; + Check(vkCreateXlibSurfaceKHR(instance.instance, &create, nullptr, &surface), "Create X11 Vulkan surface"); + VkBool32 supported = VK_FALSE; + Check(vkGetPhysicalDeviceSurfaceSupportKHR(instance.physicalDevice, instance.queueFamilyIndex, surface, &supported), "Query presentation queue"); + if (!supported) throw std::runtime_error("Unity's Vulkan queue cannot present to this X11 display."); + VkCommandPoolCreateInfo poolInfo{ VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO }; + poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; poolInfo.queueFamilyIndex = instance.queueFamilyIndex; + Check(vkCreateCommandPool(instance.device, &poolInfo, nullptr, &pool), "Create desktop command pool"); + std::array buffers{}; + VkCommandBufferAllocateInfo allocation{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO }; + allocation.commandPool = pool; allocation.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocation.commandBufferCount = 3; + Check(vkAllocateCommandBuffers(instance.device, &allocation, buffers.data()), "Allocate desktop commands"); + VkFenceCreateInfo fence{ VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; fence.flags = VK_FENCE_CREATE_SIGNALED_BIT; + VkSemaphoreCreateInfo semaphore{ VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO }; + for (std::size_t i = 0; i < frames.size(); i++) + { + frames[i].command = buffers[i]; + Check(vkCreateFence(instance.device, &fence, nullptr, &frames[i].fence), "Create desktop frame fence"); + Check(vkCreateSemaphore(instance.device, &semaphore, nullptr, &frames[i].acquired), "Create acquisition semaphore"); + } + } + catch (...) { Cleanup(); throw; } + } + ~VulkanWindow() override + { + Cleanup(); + } + void Cleanup() noexcept + { + if (swapchain) { vkQueueWaitIdle(instance.graphicsQueue); for (auto semaphore : presented) if (semaphore) vkDestroySemaphore(instance.device, semaphore, nullptr); vkDestroySwapchainKHR(instance.device, swapchain, nullptr); } + for (const auto& frame : frames) + { + if (frame.fence) vkDestroyFence(instance.device, frame.fence, nullptr); + if (frame.acquired) vkDestroySemaphore(instance.device, frame.acquired, nullptr); + if (frame.raw) vkDestroyImage(instance.device, frame.raw, nullptr); + if (frame.memory) vkFreeMemory(instance.device, frame.memory, nullptr); + } + if (pool) vkDestroyCommandPool(instance.device, pool, nullptr); + if (surface) vkDestroySurfaceKHR(instance.instance, surface, nullptr); + if (window) { try { vpnw::XDestroyBorderless(window); } catch (...) {} } + } + bool Configure(int x, int y, int w, int h, bool show, bool stayOnTop) override + { + std::lock_guard lock(mutex); + rebuild |= width != w || height != h; left = x; top = y; width = w; height = h; visible = show; topmost = stayOnTop; + return vpnw::XConfigure(window, x, y, w, h, show, stayOnTop); + } + bool SetOrder(int order) override + { + std::lock_guard lock(mutex); if (order >= 2) topmost = order == 2; return vpnw::XOrder(window, order); + } + bool SetTexture(void* value, bool flipY) override + { + std::lock_guard lock(mutex); texture = value; flip = flipY; sourceReady = false; return true; + } + bool SetPlacementMode(bool enabled) override { std::lock_guard lock(mutex); placement = enabled; return vpnw::XSetPlacementMode(window, enabled); } + void Retire() override + { + std::lock_guard lock(mutex); alive = visible = false; texture = nullptr; sourceReady = false; + vpnw::XConfigure(window, left, top, width, height, false, topmost); + } + bool Collect() override + { + std::lock_guard lock(mutex); if (lastResult == 0 && !FramesReady()) return false; ReleaseSwapchain(); return true; + } + void Draw() override + { + std::lock_guard lock(mutex); sourceReady = false; + if (!alive || !visible || !texture || !vulkan) return; + sourceReady = vulkan->AccessTexture(texture, UnityVulkanWholeImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_READ_BIT, kUnityVulkanResourceAccess_PipelineBarrier, &source); + if (sourceReady && (source.samples != VK_SAMPLE_COUNT_1_BIT || source.layers != 1 || source.extent.depth != 1 || + (source.format != VK_FORMAT_R8G8B8A8_UNORM && source.format != VK_FORMAT_B8G8R8A8_UNORM && source.format != VK_FORMAT_R8G8B8A8_SRGB && source.format != VK_FORMAT_B8G8R8A8_SRGB))) + { + sourceReady = false; vpnw::SetError("Linux desktop output requires a single-layer, single-sample RGBA8/BGRA8 texture."); + } + } + void Present() override + { + std::lock_guard lock(mutex); + VPNW_Diagnostics bounds{}; + if (alive && placement && vpnw::XDiagnostics(window, bounds)) { + rebuild |= width != bounds.windowWidth || height != bounds.windowHeight; + left = bounds.left; top = bounds.top; width = bounds.windowWidth; height = bounds.windowHeight; + } + if (!alive || !visible || !sourceReady) { skipped++; return; } + try + { + if (rebuild) { if (!FramesReady()) { skipped++; return; } ReleaseSwapchain(); BuildSwapchain(); } + auto& frame = frames[nextFrame]; + if (vkGetFenceStatus(instance.device, frame.fence) == VK_NOT_READY) { skipped++; return; } + std::uint32_t index = 0; + VkResult result = vkAcquireNextImageKHR(instance.device, swapchain, 0, frame.acquired, VK_NULL_HANDLE, &index); + if (result == VK_ERROR_OUT_OF_DATE_KHR) { rebuild = true; skipped++; return; } + if (result == VK_NOT_READY || result == VK_TIMEOUT) { skipped++; return; } + if (result != VK_SUBOPTIMAL_KHR) Check(result, "Acquire desktop image"); + Check(vkResetCommandBuffer(frame.command, 0), "Reset desktop command"); + VkCommandBufferBeginInfo begin{ VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO }; begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + Check(vkBeginCommandBuffer(frame.command, &begin), "Begin desktop copy"); + VkImageMemoryBarrier barrier{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER }; + barrier.oldLayout = initialized[index] ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = images[index]; + barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 }; + vkCmdPipelineBarrier(frame.command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + VkImageBlit copy{}; copy.srcSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 }; copy.dstSubresource = copy.srcSubresource; + copy.srcOffsets[0] = { 0, flip ? static_cast(source.extent.height) : 0, 0 }; + copy.srcOffsets[1] = { static_cast(source.extent.width), flip ? 0 : static_cast(source.extent.height), 1 }; + copy.dstOffsets[1] = { static_cast(extent.width), static_cast(extent.height), 1 }; + VkImage encoded = EncodedSource(frame); + vkCmdBlitImage(frame.command, encoded, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, images[index], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©, VK_FILTER_LINEAR); + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = 0; + vkCmdPipelineBarrier(frame.command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + Check(vkEndCommandBuffer(frame.command), "End desktop copy"); + Check(vkResetFences(instance.device, 1, &frame.fence), "Reset desktop fence"); + VkPipelineStageFlags stage = VK_PIPELINE_STAGE_TRANSFER_BIT; + VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO }; + submit.waitSemaphoreCount = 1; submit.pWaitSemaphores = &frame.acquired; submit.pWaitDstStageMask = &stage; + submit.commandBufferCount = 1; submit.pCommandBuffers = &frame.command; submit.signalSemaphoreCount = 1; submit.pSignalSemaphores = &presented[index]; + Check(vkQueueSubmit(instance.graphicsQueue, 1, &submit, frame.fence), "Submit desktop copy"); + VkPresentInfoKHR present{ VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; + present.waitSemaphoreCount = 1; present.pWaitSemaphores = &presented[index]; present.swapchainCount = 1; present.pSwapchains = &swapchain; present.pImageIndices = &index; + result = vkQueuePresentKHR(instance.graphicsQueue, &present); + if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) rebuild = true; else Check(result, "Present desktop image"); + initialized[index] = true; nextFrame = (nextFrame + 1) % frames.size(); submitted++; lastResult = 0; + } + catch (const std::exception& error) { lastResult = -1; alive = false; skipped++; vpnw::SetError(error.what()); } + } + VPNW_Diagnostics Diagnostics() override + { + std::lock_guard lock(mutex); + VPNW_Diagnostics result{}; result.structBytes = sizeof(result); result.graphicsRenderer = kUnityGfxRendererVulkan; + result.deviceReady = instance.device ? 1 : 0; result.alive = alive ? 1 : 0; result.topmost = topmost ? 1 : 0; + result.left = left; result.top = top; result.windowWidth = width; result.windowHeight = height; result.visible = visible ? 1 : 0; + vpnw::XDiagnostics(window, result); result.sourceWidth = source.extent.width; result.sourceHeight = source.extent.height; + result.sourceFormat = static_cast(source.format); result.presentedFrames = submitted; result.skippedFrames = skipped; result.lastResult = lastResult; result.premultipliedAlpha = 1; return result; + } + }; +} + +void vpnw::PreloadGraphics(IUnityInterfaces* interfaces) +{ + // Retain XWayland presentation when Unity itself selects a Wayland surface. + const char* display = std::getenv("DISPLAY"); + auto* api = interfaces->Get(); + if (!display || !*display) return; + // The hook only works when the library is preloaded, before Unity creates its Vulkan instance (see the .so importer). + if (!api || !api->AddInterceptInitialization(Intercept, nullptr, 100)) + SetError("Could not register the Vulkan initialization hook; native windows need the library preloaded at startup."); +} +void vpnw::UnloadGraphics(IUnityInterfaces* interfaces) +{ + auto* api = interfaces->Get(); + if (api) api->RemoveInterceptInitialization(Intercept); +} +void vpnw::InitializeGraphics(IUnityInterfaces* interfaces, UnityGfxRenderer renderer) +{ + if (renderer != kUnityGfxRendererVulkan) { SetError("Linux native windows require Unity Vulkan."); return; } + vulkan = interfaces->Get(); + if (!vulkan) throw std::runtime_error("Unity's Vulkan native interface is unavailable."); + instance = vulkan->Instance(); + UnityVulkanPluginEventConfig draw{}; + draw.renderPassPrecondition = kUnityVulkanRenderPass_EnsureOutside; + draw.graphicsQueueAccess = kUnityVulkanGraphicsQueueAccess_DontCare; + draw.flags = kUnityVulkanEventConfigFlag_EnsurePreviousFrameSubmission; + vulkan->ConfigureEvent(DrawEvent, &draw); + UnityVulkanPluginEventConfig present{}; + present.renderPassPrecondition = kUnityVulkanRenderPass_DontCare; + present.graphicsQueueAccess = kUnityVulkanGraphicsQueueAccess_Allow; + present.flags = kUnityVulkanEventConfigFlag_EnsurePreviousFrameSubmission | kUnityVulkanEventConfigFlag_FlushCommandBuffers | kUnityVulkanEventConfigFlag_SyncWorkerThreads; + vulkan->ConfigureEvent(PresentEvent, &present); +} +void vpnw::ShutdownGraphics() { if (instance.device) vkDeviceWaitIdle(instance.device); vulkan = nullptr; } +std::shared_ptr vpnw::CreateWindowBackend(int x, int y, int width, int height) +{ + return std::make_shared(x, y, width, height); +} diff --git a/Native~/NativeWindows/Desktop/Linux/X11Windows.cpp b/Native~/NativeWindows/Desktop/Linux/X11Windows.cpp new file mode 100644 index 000000000..d0d943c32 --- /dev/null +++ b/Native~/NativeWindows/Desktop/Linux/X11Windows.cpp @@ -0,0 +1,164 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#include "X11Windows.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + std::mutex displayMutex; + Display* display = nullptr; + std::mutex initializationMutex; + std::unordered_map colormaps; + std::unordered_set placementWindows; + XErrorHandler previousError = nullptr; + int dpi = 0; + int Error(Display* connection, XErrorEvent* event) + { + if (connection != display) return previousError ? previousError(connection, event) : 0; + char message[256]{}; XGetErrorText(connection, event->error_code, message, sizeof(message)); + vpnw::SetError(std::string("X11: ") + message); return 0; + } + Atom Property(const char* name) { return XInternAtom(vpnw::XDisplay(), name, False); } + void State(Window window, int action, Atom first, Atom second = None) + { + XEvent event{}; event.xclient.type = ClientMessage; event.xclient.window = window; + event.xclient.message_type = Property("_NET_WM_STATE"); event.xclient.format = 32; + event.xclient.data.l[0] = action; event.xclient.data.l[1] = static_cast(first); + event.xclient.data.l[2] = static_cast(second); event.xclient.data.l[3] = 1; + XSendEvent(vpnw::XDisplay(), DefaultRootWindow(vpnw::XDisplay()), False, SubstructureRedirectMask | SubstructureNotifyMask, &event); + } + // Desktop scale as published by the session (Xft.dpi in RESOURCE_MANAGER); X11 itself has no per-window DPI. + int DesktopDpi(Display* connection) + { + if (dpi) return dpi; + dpi = 96; + if (const char* resources = XResourceManagerString(connection)) + { + XrmInitialize(); + if (XrmDatabase database = XrmGetStringDatabase(resources)) + { + char* type = nullptr; XrmValue value{}; + if (XrmGetResource(database, "Xft.dpi", "Xft.Dpi", &type, &value) && value.addr) + { + const double parsed = std::atof(value.addr); + if (parsed > 0 && parsed <= 960) dpi = static_cast(std::lround(parsed)); + } + XrmDestroyDatabase(database); + } + } + return dpi; + } +} + +Display* vpnw::XDisplay() +{ + std::lock_guard lock(initializationMutex); + if (!display) + { + XInitThreads(); display = XOpenDisplay(nullptr); + if (!display) throw std::runtime_error("An X11/XWayland DISPLAY is required for native desktop windows. Wayland toplevel placement is compositor controlled."); + previousError = XSetErrorHandler(Error); + } + return display; +} +std::mutex& vpnw::XMutex() { return displayMutex; } +Window vpnw::XCreateBorderless(int x, int y, int width, int height) +{ + std::lock_guard lock(displayMutex); Display* connection = XDisplay(); + XVisualInfo query{}; query.screen = DefaultScreen(connection); query.depth = 32; query.c_class = TrueColor; + int count = 0; XVisualInfo* visuals = XGetVisualInfo(connection, VisualScreenMask | VisualDepthMask | VisualClassMask, &query, &count); + Visual* visual = nullptr; + for (int i = 0; i < count; i++) { auto* format = XRenderFindVisualFormat(connection, visuals[i].visual); if (format && format->direct.alphaMask) { visual = visuals[i].visual; break; } } + if (visuals) XFree(visuals); + if (!visual) throw std::runtime_error("The X11 display has no 32-bit ARGB visual for transparent windows."); + XSetWindowAttributes attributes{}; attributes.colormap = XCreateColormap(connection, DefaultRootWindow(connection), visual, AllocNone); + attributes.border_pixel = 0; attributes.background_pixel = 0; attributes.event_mask = StructureNotifyMask | ExposureMask | ButtonPressMask; + Window window = XCreateWindow(connection, DefaultRootWindow(connection), x, y, static_cast(width), static_cast(height), 0, 32, InputOutput, visual, + CWColormap | CWBorderPixel | CWBackPixel | CWEventMask, &attributes); + colormaps.emplace(window, attributes.colormap); + unsigned long motif[] = { 2, 0, 0, 0, 0 }; + XChangeProperty(connection, window, Property("_MOTIF_WM_HINTS"), Property("_MOTIF_WM_HINTS"), 32, PropModeReplace, reinterpret_cast(motif), 5); + XStoreName(connection, window, "Visual Pinball Window"); + XClassHint classHint{}; char className[] = "VisualPinball.NativeWindow"; classHint.res_name = className; classHint.res_class = className; + XSetClassHint(connection, window, &classHint); + XWMHints hints{}; hints.flags = InputHint; hints.input = False; XSetWMHints(connection, window, &hints); + Atom states[] = { Property("_NET_WM_STATE_SKIP_TASKBAR"), Property("_NET_WM_STATE_SKIP_PAGER") }; + XChangeProperty(connection, window, Property("_NET_WM_STATE"), XA_ATOM, 32, PropModeReplace, reinterpret_cast(states), 2); + XFlush(connection); return window; +} +void vpnw::XDestroyBorderless(Window window) +{ + std::lock_guard lock(displayMutex); + XDestroyWindow(XDisplay(), window); + placementWindows.erase(window); + auto found = colormaps.find(window); + if (found != colormaps.end()) { XFreeColormap(XDisplay(), found->second); colormaps.erase(found); } + XFlush(XDisplay()); +} +bool vpnw::XConfigure(Window window, int x, int y, int width, int height, bool visible, bool topmost) +{ + std::lock_guard lock(displayMutex); + XMoveResizeWindow(XDisplay(), window, x, y, static_cast(width), static_cast(height)); + State(window, topmost ? 1 : 0, Property("_NET_WM_STATE_ABOVE")); + if (visible) XMapWindow(XDisplay(), window); else XUnmapWindow(XDisplay(), window); + XFlush(XDisplay()); return true; +} +bool vpnw::XOrder(Window window, int order) +{ + std::lock_guard lock(displayMutex); + if (order == 0) XLowerWindow(XDisplay(), window); else if (order == 1) XRaiseWindow(XDisplay(), window); + else State(window, order == 2 ? 1 : 0, Property("_NET_WM_STATE_ABOVE")); + XFlush(XDisplay()); return true; +} +bool vpnw::XSetPlacementMode(Window window, bool enabled) +{ + std::lock_guard lock(displayMutex); + if (enabled) placementWindows.insert(window); else placementWindows.erase(window); + return true; +} +bool vpnw::XDiagnostics(Window window, VPNW_Diagnostics& output) +{ + std::lock_guard lock(displayMutex); XWindowAttributes attributes{}; + if (!XGetWindowAttributes(XDisplay(), window, &attributes)) return false; + Window child = None; XTranslateCoordinates(XDisplay(), window, DefaultRootWindow(XDisplay()), 0, 0, &output.left, &output.top, &child); + output.windowWidth = attributes.width; output.windowHeight = attributes.height; output.visible = attributes.map_state == IsViewable ? 1 : 0; + output.dpi = static_cast(DesktopDpi(XDisplay())); return true; +} +void vpnw::PumpPlatform() +{ + std::lock_guard lock(displayMutex); + if (!display) return; + while (XPending(display)) { + XEvent event{}; XNextEvent(display, &event); + if (event.type != ButtonPress || event.xbutton.button != Button1 || !placementWindows.count(event.xbutton.window)) continue; + XWindowAttributes bounds{}; if (!XGetWindowAttributes(display, event.xbutton.window, &bounds)) continue; + bool left = event.xbutton.x < 10, right = event.xbutton.x >= bounds.width - 10; + bool top = event.xbutton.y < 10, bottom = event.xbutton.y >= bounds.height - 10; + int direction = top ? (left ? 0 : right ? 2 : 1) : bottom ? (left ? 6 : right ? 4 : 5) : left ? 7 : right ? 3 : 8; + XUngrabPointer(display, CurrentTime); + XEvent request{}; request.xclient.type = ClientMessage; request.xclient.window = event.xbutton.window; + request.xclient.message_type = Property("_NET_WM_MOVERESIZE"); request.xclient.format = 32; + request.xclient.data.l[0] = event.xbutton.x_root; request.xclient.data.l[1] = event.xbutton.y_root; + request.xclient.data.l[2] = direction; request.xclient.data.l[3] = Button1; request.xclient.data.l[4] = 1; + XSendEvent(display, DefaultRootWindow(display), False, SubstructureRedirectMask | SubstructureNotifyMask, &request); + XFlush(display); + } +} +std::uint32_t vpnw::PlatformCapabilities() +{ + const char* setting = std::getenv("DISPLAY"); + return setting && *setting ? VPNW_CapabilityWindows | VPNW_CapabilityPlacement | VPNW_CapabilityTopmost | VPNW_CapabilityTransparency : 0u; +} diff --git a/Native~/NativeWindows/Desktop/Linux/X11Windows.h b/Native~/NativeWindows/Desktop/Linux/X11Windows.h new file mode 100644 index 000000000..d5d675b4a --- /dev/null +++ b/Native~/NativeWindows/Desktop/Linux/X11Windows.h @@ -0,0 +1,22 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#pragma once +#include "WindowBackend.h" +#include +#include + +namespace vpnw +{ + Display* XDisplay(); + std::mutex& XMutex(); + Window XCreateBorderless(int x, int y, int width, int height); + void XDestroyBorderless(Window window); + bool XConfigure(Window window, int x, int y, int width, int height, bool visible, bool topmost); + bool XOrder(Window window, int order); + bool XSetPlacementMode(Window window, bool enabled); + bool XDiagnostics(Window window, VPNW_Diagnostics& output); +} diff --git a/Native~/NativeWindows/Desktop/Mac/MacCoordinates.h b/Native~/NativeWindows/Desktop/Mac/MacCoordinates.h new file mode 100644 index 000000000..43e05c478 --- /dev/null +++ b/Native~/NativeWindows/Desktop/Mac/MacCoordinates.h @@ -0,0 +1,63 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#pragma once +#import +#import +#include +#include +#include "DesktopLayout.h" + +namespace vpnw +{ + // Desktop coordinates use top-left origins and physical pixels; Cocoa uses bottom-left points. + inline NSRect PhysicalScreen(NSScreen* screen) + { + NSArray* screens = NSScreen.screens; std::vector monitors; + for (NSScreen* entry in screens) + { + CGDirectDisplayID display = [entry.deviceDescription[@"NSScreenNumber"] unsignedIntValue]; CGRect bounds = CGDisplayBounds(display); + monitors.push_back({ { bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height }, static_cast(CGDisplayPixelsWide(display)), static_cast(CGDisplayPixelsHigh(display)) }); + } + auto layout = PhysicalLayout(monitors); NSUInteger index = [screens indexOfObject:screen]; + if (index == NSNotFound) return NSZeroRect; + auto rectangle = layout[index]; return NSMakeRect(rectangle.x, rectangle.y, rectangle.width, rectangle.height); + } + inline NSScreen* ScreenForPixels(int x, int y, int width, int height) + { + NSScreen* selected = NSScreen.screens.firstObject; double largest = -1; + NSRect requested = NSMakeRect(x, y, width, height); + for (NSScreen* screen in NSScreen.screens) + { + NSRect intersection = NSIntersectionRect(requested, PhysicalScreen(screen)); + double area = intersection.size.width * intersection.size.height; + if (area > largest) { largest = area; selected = screen; } + } + return selected; + } + inline NSRect CocoaFrame(int x, int y, int width, int height) + { + NSScreen* screen = ScreenForPixels(x, y, width, height); + if (!screen) return NSMakeRect(x, y, width, height); + NSRect physical = PhysicalScreen(screen); NSRect logical = screen.frame; + double sx = physical.size.width / logical.size.width, sy = physical.size.height / logical.size.height; + return NSMakeRect(logical.origin.x + (x - physical.origin.x) / sx, + NSMaxY(logical) - (y - physical.origin.y + height) / sy, width / sx, height / sy); + } + inline NSRect PixelFrame(NSWindow* window) + { + NSScreen* screen = window.screen ?: NSScreen.screens.firstObject; + if (!screen) return window.frame; + NSRect physical = PhysicalScreen(screen), frame = window.frame, logical = screen.frame; + double sx = physical.size.width / logical.size.width, sy = physical.size.height / logical.size.height; + return NSMakeRect(physical.origin.x + (frame.origin.x - logical.origin.x) * sx, + physical.origin.y + (NSMaxY(logical) - NSMaxY(frame)) * sy, frame.size.width * sx, frame.size.height * sy); + } + inline void MainThread(dispatch_block_t action) + { + if (NSThread.isMainThread) action(); else dispatch_sync(dispatch_get_main_queue(), action); + } +} diff --git a/Native~/NativeWindows/Desktop/Mac/MetalWindows.mm b/Native~/NativeWindows/Desktop/Mac/MetalWindows.mm new file mode 100644 index 000000000..fb3316a70 --- /dev/null +++ b/Native~/NativeWindows/Desktop/Mac/MetalWindows.mm @@ -0,0 +1,209 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#include "WindowBackend.h" +#include "MacCoordinates.h" +#include "IUnityGraphicsMetal.h" +#import +#import +#include +#include +#include + +namespace +{ + IUnityGraphicsMetal* metal = nullptr; + id device = nil; + id pipeline = nil; + constexpr const char* Shader = R"( +#include +using namespace metal; +struct Varying { float4 position [[position]]; float2 uv; }; +vertex Varying vpnw_vertex(uint id [[vertex_id]], constant uint& flip [[buffer(0)]]) { + float2 uv = float2((id << 1) & 2, id & 2); + Varying output; output.position = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1); + output.uv = float2(uv.x, flip ? 1 - uv.y : uv.y); return output; +} +fragment float4 vpnw_fragment(Varying input [[stage_in]], texture2d source [[texture(0)]]) { + constexpr sampler sample(filter::linear, address::clamp_to_edge); + return source.sample(sample, input.uv); +})"; + void Pipeline() + { + if (pipeline) return; + NSError* error = nil; + id library = [device newLibraryWithSource:[NSString stringWithUTF8String:Shader] options:nil error:&error]; + if (!library) throw std::runtime_error(error.localizedDescription.UTF8String ?: "Could not compile desktop Metal shaders."); + MTLRenderPipelineDescriptor* description = [[MTLRenderPipelineDescriptor alloc] init]; + description.vertexFunction = [library newFunctionWithName:@"vpnw_vertex"]; + description.fragmentFunction = [library newFunctionWithName:@"vpnw_fragment"]; + description.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm; + description.colorAttachments[0].blendingEnabled = NO; + pipeline = [device newRenderPipelineStateWithDescriptor:description error:&error]; + if (!pipeline) throw std::runtime_error(error.localizedDescription.UTF8String ?: "Could not create desktop Metal pipeline."); + } + class MetalWindow final : public vpnw::WindowBackend, public std::enable_shared_from_this + { + std::mutex mutex; + NSWindow* window = nil; + CAMetalLayer* layer = nil; + id texture = nil; + id rawSnapshot = nil; + std::atomic inFlight{ 0 }; + bool alive = true, visible = false, topmost = false, flip = false, closing = false; + int left, top, width, height; + std::uint64_t presented = 0, skipped = 0; + public: + MetalWindow(int x, int y, int w, int h) : left(x), top(y), width(w), height(h) + { + if (!metal || !device) throw std::runtime_error("Unity Metal is required for macOS desktop output."); + vpnw::MainThread(^{ + window = [[NSWindow alloc] initWithContentRect:vpnw::CocoaFrame(x, y, w, h) styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]; + window.releasedWhenClosed = NO; window.opaque = NO; window.backgroundColor = NSColor.clearColor; + window.hasShadow = NO; window.title = @"Visual Pinball Window"; + window.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorFullScreenAuxiliary | NSWindowCollectionBehaviorStationary; + layer = [CAMetalLayer layer]; layer.device = device; layer.pixelFormat = MTLPixelFormatBGRA8Unorm; + CGColorSpaceRef sdr = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + layer.colorspace = sdr; CGColorSpaceRelease(sdr); + layer.opaque = NO; layer.framebufferOnly = YES; layer.maximumDrawableCount = 3; + layer.allowsNextDrawableTimeout = YES; layer.displaySyncEnabled = NO; + layer.drawableSize = CGSizeMake(w, h); layer.contentsScale = window.backingScaleFactor; + window.contentView.wantsLayer = YES; window.contentView.layer = layer; + }); + } + ~MetalWindow() override + { + NSWindow* owned = window; + if (owned) dispatch_async(dispatch_get_main_queue(), ^{ [owned orderOut:nil]; [owned close]; }); + } + bool Configure(int x, int y, int w, int h, bool show, bool stayOnTop) override + { + std::lock_guard lock(mutex); + left = x; top = y; width = w; height = h; visible = show; topmost = stayOnTop; + vpnw::MainThread(^{ + [window setFrame:vpnw::CocoaFrame(x, y, w, h) display:NO]; + window.level = stayOnTop ? NSFloatingWindowLevel : NSNormalWindowLevel; + layer.contentsScale = window.backingScaleFactor; layer.drawableSize = CGSizeMake(w, h); + if (show) [window orderFrontRegardless]; else [window orderOut:nil]; + }); + return true; + } + bool SetOrder(int order) override + { + std::lock_guard lock(mutex); + if (order >= 2) topmost = order == 2; + vpnw::MainThread(^{ + if (order == 0) [window orderBack:nil]; else if (order == 1) [window orderFrontRegardless]; + else window.level = order == 2 ? NSFloatingWindowLevel : NSNormalWindowLevel; + }); return true; + } + bool SetTexture(void* value, bool flipY) override + { + std::lock_guard lock(mutex); texture = (__bridge id)value; rawSnapshot = nil; flip = flipY; return true; + } + bool SetPlacementMode(bool enabled) override + { + vpnw::MainThread(^{ + window.movableByWindowBackground = enabled; + window.styleMask = NSWindowStyleMaskBorderless | (enabled ? NSWindowStyleMaskResizable : 0); + }); + return true; + } + void Retire() override + { + std::lock_guard lock(mutex); alive = visible = false; texture = nil; + NSWindow* owned = window; dispatch_async(dispatch_get_main_queue(), ^{ [owned orderOut:nil]; }); + } + bool Collect() override + { + std::lock_guard lock(mutex); + if (inFlight.load() != 0) return false; + if (!closing) + { + closing = true; NSWindow* owned = window; window = nil; layer = nil; + dispatch_async(dispatch_get_main_queue(), ^{ [owned close]; }); + } + return true; + } + void Draw() override {} + void Present() override + { + std::lock_guard lock(mutex); + if (!alive || !visible || !texture || !metal || inFlight.load() >= 2) { skipped++; return; } + @autoreleasepool + { + Pipeline(); id drawable = [layer nextDrawable]; + if (!drawable) { skipped++; return; } + metal->EndCurrentCommandEncoder(); id command = metal->CurrentCommandBuffer(); + if (!command) { skipped++; return; } + id source = texture; + // A raw GPU copy avoids sampling the sRGB view and preserves encoded SDR bytes. + if (texture.pixelFormat == MTLPixelFormatBGRA8Unorm_sRGB || texture.pixelFormat == MTLPixelFormatRGBA8Unorm_sRGB) + { + MTLPixelFormat format = texture.pixelFormat == MTLPixelFormatBGRA8Unorm_sRGB ? MTLPixelFormatBGRA8Unorm : MTLPixelFormatRGBA8Unorm; + if (!rawSnapshot || rawSnapshot.width != texture.width || rawSnapshot.height != texture.height) + { + MTLTextureDescriptor* description = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format width:texture.width height:texture.height mipmapped:NO]; + description.storageMode = MTLStorageModePrivate; description.usage = MTLTextureUsageShaderRead; + rawSnapshot = [device newTextureWithDescriptor:description]; + } + id copy = [command blitCommandEncoder]; + [copy copyFromTexture:texture sourceSlice:0 sourceLevel:0 sourceOrigin:MTLOriginMake(0, 0, 0) sourceSize:MTLSizeMake(texture.width, texture.height, 1) + toTexture:rawSnapshot destinationSlice:0 destinationLevel:0 destinationOrigin:MTLOriginMake(0, 0, 0)]; [copy endEncoding]; source = rawSnapshot; + } + MTLRenderPassDescriptor* pass = [MTLRenderPassDescriptor renderPassDescriptor]; + pass.colorAttachments[0].texture = drawable.texture; pass.colorAttachments[0].loadAction = MTLLoadActionDontCare; pass.colorAttachments[0].storeAction = MTLStoreActionStore; + id encoder = [command renderCommandEncoderWithDescriptor:pass]; + [encoder setRenderPipelineState:pipeline]; [encoder setFragmentTexture:source atIndex:0]; + std::uint32_t inversion = flip ? 1 : 0; [encoder setVertexBytes:&inversion length:sizeof(inversion) atIndex:0]; + [encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; [encoder endEncoding]; + [command presentDrawable:drawable]; inFlight++; + auto owner = shared_from_this(); id retainedSource = source, retainedInput = texture; + [command addCompletedHandler:^(id completed) { + (void)retainedSource; (void)retainedInput; + if (completed.status == MTLCommandBufferStatusError) vpnw::SetError(completed.error.localizedDescription.UTF8String ?: "Desktop Metal command failed."); + owner->inFlight--; + }]; + presented++; + } + } + VPNW_Diagnostics Diagnostics() override + { + std::lock_guard lock(mutex); VPNW_Diagnostics result{}; + result.structBytes = sizeof(result); result.graphicsRenderer = kUnityGfxRendererMetal; result.deviceReady = device ? 1 : 0; + result.alive = alive ? 1 : 0; result.visible = visible ? 1 : 0; result.topmost = topmost ? 1 : 0; + vpnw::MainThread(^{ + NSRect bounds = vpnw::PixelFrame(window); + left = static_cast(std::round(bounds.origin.x)); top = static_cast(std::round(bounds.origin.y)); + width = static_cast(std::round(bounds.size.width)); height = static_cast(std::round(bounds.size.height)); + layer.contentsScale = window.backingScaleFactor; layer.drawableSize = CGSizeMake(width, height); + }); + result.left = left; result.top = top; result.windowWidth = width; result.windowHeight = height; result.dpi = 96; + result.sourceWidth = static_cast(texture.width); result.sourceHeight = static_cast(texture.height); + result.sourceFormat = static_cast(texture.pixelFormat); result.presentedFrames = presented; result.skippedFrames = skipped; result.premultipliedAlpha = 1; return result; + } + }; +} + +void vpnw::PreloadGraphics(IUnityInterfaces*) {} +void vpnw::UnloadGraphics(IUnityInterfaces*) {} +void vpnw::InitializeGraphics(IUnityInterfaces* interfaces, UnityGfxRenderer renderer) +{ + if (renderer != kUnityGfxRendererMetal) { SetError("macOS native windows require Unity Metal."); return; } + metal = interfaces->Get(); + if (!metal) throw std::runtime_error("Unity's Metal native interface is unavailable."); + device = metal->MetalDevice(); +} +void vpnw::ShutdownGraphics() { pipeline = nil; device = nil; metal = nullptr; } +void vpnw::PumpPlatform() {} +std::uint32_t vpnw::PlatformCapabilities() +{ + return VPNW_CapabilityWindows | VPNW_CapabilityPlacement | VPNW_CapabilityTopmost | VPNW_CapabilityTransparency; +} +std::shared_ptr vpnw::CreateWindowBackend(int x, int y, int width, int height) +{ + return std::make_shared(x, y, width, height); +} diff --git a/Native~/NativeWindows/Desktop/WindowBackend.h b/Native~/NativeWindows/Desktop/WindowBackend.h new file mode 100644 index 000000000..577f94cdb --- /dev/null +++ b/Native~/NativeWindows/Desktop/WindowBackend.h @@ -0,0 +1,41 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#pragma once +#include "VisualPinballNativeWindows.h" +#include "IUnityGraphics.h" +#include "IUnityInterface.h" +#include +#include + +namespace vpnw +{ + extern int DrawEvent; + extern int PresentEvent; + void SetError(const std::string& message); + void PreloadGraphics(IUnityInterfaces* interfaces); + void UnloadGraphics(IUnityInterfaces* interfaces); + void InitializeGraphics(IUnityInterfaces* interfaces, UnityGfxRenderer renderer); + void ShutdownGraphics(); + void PumpPlatform(); + // VPNW_Capability bits. + std::uint32_t PlatformCapabilities(); + class WindowBackend + { + public: + virtual ~WindowBackend() = default; + virtual bool Configure(int x, int y, int width, int height, bool visible, bool topmost) = 0; + virtual bool SetOrder(int order) = 0; + virtual bool SetPlacementMode(bool enabled) = 0; + virtual bool SetTexture(void* texture, bool flipY) = 0; + virtual void Retire() = 0; + virtual bool Collect() = 0; + virtual void Draw() = 0; + virtual void Present() = 0; + virtual VPNW_Diagnostics Diagnostics() = 0; + }; + std::shared_ptr CreateWindowBackend(int x, int y, int width, int height); +} diff --git a/Native~/NativeWindows/Desktop/WindowRegistry.cpp b/Native~/NativeWindows/Desktop/WindowRegistry.cpp new file mode 100644 index 000000000..437c6490a --- /dev/null +++ b/Native~/NativeWindows/Desktop/WindowRegistry.cpp @@ -0,0 +1,168 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#include "WindowBackend.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + std::mutex registryMutex, errorMutex; + std::unordered_map> windows; + std::vector> retired; + std::string lastError; + int nextId = 1; + IUnityInterfaces* unity = nullptr; + IUnityGraphics* graphics = nullptr; + std::atomic renderer{ kUnityGfxRendererNull }; + + std::shared_ptr Find(int id) + { + std::lock_guard lock(registryMutex); + auto found = windows.find(id); + return found == windows.end() ? nullptr : found->second; + } + void UNITY_INTERFACE_API OnDevice(UnityGfxDeviceEventType event) + { + try + { + if (event == kUnityGfxDeviceEventInitialize) + { + renderer = graphics->GetRenderer(); + vpnw::InitializeGraphics(unity, static_cast(renderer.load())); + } + else if (event == kUnityGfxDeviceEventShutdown) + { + VPNW_DestroyAll(); + vpnw::ShutdownGraphics(); + std::lock_guard lock(registryMutex); + retired.clear(); renderer = kUnityGfxRendererNull; + } + } + catch (const std::exception& error) { vpnw::SetError(error.what()); } + } + void UNITY_INTERFACE_API OnRender(int event) + { + try + { + std::vector> active, pending; + { + std::lock_guard lock(registryMutex); + for (auto& pair : windows) active.push_back(pair.second); + pending = retired; + } + for (auto& window : active) + if (event == vpnw::DrawEvent) window->Draw(); else if (event == vpnw::PresentEvent) window->Present(); + if (event == vpnw::PresentEvent) for (auto& window : pending) + if (window->Collect()) + { + std::lock_guard lock(registryMutex); + retired.erase(std::remove(retired.begin(), retired.end(), window), retired.end()); + } + } + catch (const std::exception& error) { vpnw::SetError(error.what()); } + } + template int Attempt(Function function) + { + try { return function() ? 1 : 0; } + catch (const std::exception& error) { vpnw::SetError(error.what()); return 0; } + } +} + +int vpnw::DrawEvent = 0; +int vpnw::PresentEvent = 1; + +void vpnw::SetError(const std::string& message) +{ + std::lock_guard lock(errorMutex); lastError = message; +} +extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* interfaces) +{ + unity = interfaces; graphics = unity->Get(); + if (!graphics) { vpnw::SetError("Unity graphics interface is unavailable."); return; } + vpnw::DrawEvent = graphics->ReserveEventIDRange(2); + vpnw::PresentEvent = vpnw::DrawEvent + 1; + vpnw::PreloadGraphics(unity); + graphics->RegisterDeviceEventCallback(OnDevice); OnDevice(kUnityGfxDeviceEventInitialize); +} +extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginUnload() +{ + OnDevice(kUnityGfxDeviceEventShutdown); + if (graphics) graphics->UnregisterDeviceEventCallback(OnDevice); + if (unity) vpnw::UnloadGraphics(unity); + unity = nullptr; graphics = nullptr; +} + +int VPNW_CreateWindow(int x, int y, int width, int height) +{ + int id = 0; + Attempt([&] { + if (width <= 0 || height <= 0 || width > 32768 || height > 32768) return false; + auto window = vpnw::CreateWindowBackend(x, y, width, height); + if (!window) return false; + std::lock_guard lock(registryMutex); + if (nextId == std::numeric_limits::max()) return false; + id = nextId++; windows.emplace(id, window); return true; + }); + return id; +} +int VPNW_ConfigureWindow(int id, int x, int y, int width, int height, int visible, int topmost) +{ + return Attempt([&] { auto window = Find(id); return window && width > 0 && height > 0 && width <= 32768 && height <= 32768 && window->Configure(x, y, width, height, visible != 0, topmost != 0); }); +} +int VPNW_SetZOrder(int id, int order) +{ + return Attempt([&] { auto window = Find(id); return window && order >= 0 && order <= 3 && window->SetOrder(order); }); +} +int VPNW_SetPlacementMode(int id, int enabled) +{ + return Attempt([&] { auto window = Find(id); return window && window->SetPlacementMode(enabled != 0); }); +} +int VPNW_SetTexture(int id, void* texture, int flipY) +{ + return Attempt([&] { auto window = Find(id); return window && window->SetTexture(texture, flipY != 0); }); +} +int VPNW_DestroyWindow(int id) +{ + return Attempt([&] { + std::shared_ptr window; + { + std::lock_guard lock(registryMutex); + auto found = windows.find(id); if (found == windows.end()) return true; + window = found->second; windows.erase(found); retired.push_back(window); + } + window->Retire(); return true; + }); +} +void VPNW_DestroyAll() +{ + std::vector ids; + { std::lock_guard lock(registryMutex); for (auto& pair : windows) ids.push_back(pair.first); } + for (int id : ids) VPNW_DestroyWindow(id); +} +void VPNW_Pump() { vpnw::PumpPlatform(); } +void* VPNW_GetRenderEventFunc() { return reinterpret_cast(OnRender); } +int VPNW_GetGraphicsRenderer() { return renderer.load(); } +int VPNW_GetDrawEventId() { return vpnw::DrawEvent; } +int VPNW_GetPresentEventId() { return vpnw::PresentEvent; } +int VPNW_GetPendingResourceCount() { std::lock_guard lock(registryMutex); return static_cast(retired.size()); } +int VPNW_GetDiagnostics(int id, VPNW_Diagnostics* output) +{ + return Attempt([&] { auto window = Find(id); if (!window || !output || output->structBytes != sizeof(VPNW_Diagnostics)) return false; *output = window->Diagnostics(); return true; }); +} +int VPNW_GetLastError(char* output, int capacity) +{ + std::lock_guard lock(errorMutex); + if (output && capacity > 0) { int count = std::min(capacity - 1, static_cast(lastError.size())); std::memcpy(output, lastError.data(), count); output[count] = 0; } + return static_cast(lastError.size()) + 1; +} +std::uint32_t VPNW_GetCapabilities() { return vpnw::PlatformCapabilities(); } diff --git a/Native~/NativeWindows/README.md b/Native~/NativeWindows/README.md new file mode 100644 index 000000000..00e491a6e --- /dev/null +++ b/Native~/NativeWindows/README.md @@ -0,0 +1,107 @@ +# Native windows + +`VisualPinballNativeWindows` presents a Unity texture in independent borderless desktop windows, entirely on the GPU. Player plugins such as PinUP and the B2S backglass use it to put a premultiplied RGBA RenderTexture on a second or third monitor, with transparent areas showing the desktop behind. Production code never reads texture pixels back to the CPU. + +| Platform | Library | Unity graphics | Presentation | +| --- | --- | --- | --- | +| Windows x64 | `VisualPinballNativeWindows.dll` | Direct3D11, Direct3D12 (v7 interface) | DirectComposition, premultiplied-alpha flip-model swapchain | +| macOS (arm64 + x86_64) | `libVisualPinballNativeWindows.dylib` | Metal | Borderless `NSWindow` with a `CAMetalLayer` | +| Linux x64 | `libVisualPinballNativeWindows.so` | Vulkan | X11/XWayland window with a 32-bit ARGB visual and a Vulkan swapchain | + +All platforms export the same C API (`VisualPinballNativeWindows.h`, prefix `VPNW_`), so the managed side is a single P/Invoke class: `NativeWindowSurface` in the `VisualPinball.Unity` assembly. + +## Layout + +| Path | Content | +| --- | --- | +| `VisualPinballNativeWindows.h` | Public C API shared by all platforms. | +| `Windows/VisualPinballNativeWindows.cpp` | Dedicated Win32 message thread, registry IDs, DirectComposition windows, GPU scaling and flip, Direct3D11 state isolation. | +| `Windows/D3D12CopyBridge.cpp` | Unity resource-state declarations, private GPU snapshots, same-queue D3D11On12 presentation and frame-fence retirement. | +| `Windows/NativeWindowTests.cpp` | Standalone real-device fixture with a minimal Unity graphics-interface host. | +| `Desktop/WindowRegistry.cpp` | Shared macOS/Linux registry, Unity plugin entry points and render events. | +| `Desktop/Mac/MetalWindows.mm` | Metal presentation; `MacCoordinates.h` and `DesktopLayout.h` map physical pixels to Cocoa points on mixed-DPI desktops. | +| `Desktop/Linux/VulkanWindows.cpp` | Vulkan swapchain presentation; `X11Windows.cpp` creates, places, orders and pumps the X11 windows. | +| `Desktop/DesktopTests.cpp` | Standalone fixture for macOS and Linux. | + +## Design + +Unity calls `UnityPluginLoad`, where the plugin reserves two event IDs: a draw event and a present event. Both operate on all registered windows, so a host submits one pair per frame after its GPU work, not one pair per window. Window handles are registry IDs that are never reused while the plugin is loaded, so queued render events can outlive a destroyed window safely. + +On Windows the D3D11 path swaps the entire immediate-context state through `ID3D11DeviceContext1` and restores Unity's state afterwards. Typeless resources use typed views; typed sRGB resources get a cached compatible UNORM GPU copy, which avoids an illegal view and preserves the stored SDR values. The D3D12 path submits a snapshot copy through `IUnityGraphicsD3D12v7.ExecuteCommandList`, so Unity owns the source's barriers and ordering. The present event runs on Unity's submission thread after worker flushes and draws the snapshot into DirectComposition swapchain buffers through D3D11On12 on Unity's own queue. Retired snapshots and destroyed presentation resources stay alive until their frame fence completes; `PendingResourceCount` reports them, and the host keeps issuing event pairs until it is zero. + +The HWNDs live on a plugin-owned message thread with per-monitor DPI awareness; Unity's process DPI mode is not changed. On macOS, window operations are dispatched to the main thread. On Linux, X11 events (placement drags) are processed by `VPNW_Pump`, which the host calls once per frame on the main thread. The Vulkan backend intercepts Unity's instance creation to add the Xlib surface extensions and keeps XWayland presentation when Unity itself runs on Wayland. + +Present never waits for the display: Windows uses `DXGI_PRESENT_DO_NOT_WAIT`, Metal skips a frame when two are in flight, and Vulkan acquires with a zero timeout and prefers immediate mode. Skipped frames are counted in the diagnostics. + +## C API + +| Function | Purpose | +| --- | --- | +| `VPNW_CreateWindow(x, y, width, height)` | Creates a hidden borderless window; returns its ID or 0. | +| `VPNW_ConfigureWindow(id, x, y, width, height, visible, topmost)` | Sets rectangle, visibility and topmost state. | +| `VPNW_SetZOrder(id, order)` | 0 back, 1 front, 2 topmost, 3 non-topmost, without moving or activating. | +| `VPNW_SetPlacementMode(id, enabled)` | Lets the user drag the body and resize at the edges. | +| `VPNW_SetTexture(id, texture, flipY)` | Assigns Unity's native texture pointer; null detaches. | +| `VPNW_DestroyWindow(id)` / `VPNW_DestroyAll()` | Idempotent destruction; GPU resources retire through later events. | +| `VPNW_Pump()` | Processes platform window events (Linux); no-op on Windows and macOS. | +| `VPNW_GetRenderEventFunc()`, `VPNW_GetDrawEventId()`, `VPNW_GetPresentEventId()` | Unity render callback and its two event IDs. | +| `VPNW_GetPendingResourceCount()` | Retired GPU objects waiting for later events. | +| `VPNW_GetGraphicsRenderer()` | Unity's `UnityGfxRenderer` as seen by the plugin. | +| `VPNW_GetDiagnostics(id, diagnostics)` | Geometry, DPI, source, frame counters and last result (80-byte struct). | +| `VPNW_GetLastError(buffer, capacity)` | Last error as UTF-8; returns the required size. | +| `VPNW_GetCapabilities()` | Bits: 1 windows, 2 placement, 4 topmost, 8 transparency. | + +Functions returning `int` return nonzero on success. Coordinates and sizes are signed physical desktop pixels; monitors left of or above the primary monitor have negative origins. + +## Using it from a plugin + +Create a `NativeWindowSurface`, give it the native pointer of the RenderTexture you render into, configure it, and register it with the shared `NativeWindowHost`. The host is a single hidden `DontDestroyOnLoad` object shared by all plugins; it issues one draw/present event pair per frame in `LateUpdate`, pumps platform events, and keeps running until deferred GPU releases have drained. + +```csharp +if (!NativeWindowSurface.IsSupported) { + return; // Missing library or unsupported graphics backend; never throws. +} +var texture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32); +texture.Create(); +var surface = new NativeWindowSurface(x, y, width, height); // hidden +surface.SetTexture(texture.GetNativeTexturePtr()); +surface.Configure(x, y, width, height, visible: true, topmost: false); +var lease = NativeWindowHost.Register(surface); + +// Shutdown: unregister, destroy the window, then release the texture. +lease.Dispose(); +surface.Dispose(); +texture.Release(); +``` + +Render premultiplied RGBA in the SDR encoding you want on screen. `Configure` caches its last arguments, so calling it every frame is cheap. Resubmit `GetNativeTexturePtr()` whenever the RenderTexture is recreated or resized, and dispose the surface before releasing its texture. `SendToBack`, `BringToFront` and `SetTopmost` change stacking without moving or activating the window. `SetPlacementMode(true)` lets the user move and resize the window while a settings screen is open; read the result from `Diagnostics`. `Diagnostics` crosses into the window system, so query it for troubleshooting, not every frame. `NativeWindowSurface.DestroyAll()` is available for host shutdown or domain-reload cleanup. + +## Build and test + +### Windows + +Run `Windows/Build.ps1` from PowerShell with the Visual Studio C++ x64 toolchain and a Windows SDK installed. `-UnityEditor` accepts the installed Editor directory containing `Data/PluginAPI` (default: 6000.5.0f1). The script compiles with C++17, `/W4 /WX`, writes `VisualPinball.Unity/Plugins/win-x64/VisualPinballNativeWindows.dll` and keeps intermediates in `Windows/obj`. Use `-OutputPath` to stage a build elsewhere when Unity has loaded and locked the DLL. + +`./Build.ps1 -Test` builds and runs the fixture. It checks real D3D11 and D3D12 presentation, exact signed coordinates, DPI, hidden creation, placement hit-testing, z-order commands (including that `Configure` can undo a topmost state set through z-order), resize, source detach, idempotent destruction, nested modal message loops, pending D3D12 retirement before the frame fence, four-corner orientation with and without flip, and a test-only GPU readback of an exact premultiplied green128/alpha128 pixel for BGRA UNORM, RGBA/BGRA typeless and RGBA/BGRA sRGB sources. + +### macOS and Linux + +The `Desktop` folder is a CMake project (3.22+). Point `UNITY_PLUGIN_API` at the Unity Editor's `PluginAPI` directory. With `VPNW_DEPLOY` (default on), the library is copied to `VisualPinball.Unity/Plugins/osx` or `VisualPinball.Unity/Plugins/linux-x64`. + +```sh +cmake -S Native~/NativeWindows/Desktop -B Native~/NativeWindows/Desktop/obj -DUNITY_PLUGIN_API="/Applications/Unity/Hub/Editor/6000.5.0f1/Unity.app/Contents/PluginAPI" +cmake --build Native~/NativeWindows/Desktop/obj --config Release +Native~/NativeWindows/Desktop/obj/NativeWindowTests +``` + +On macOS the default `CMAKE_OSX_ARCHITECTURES` is `arm64;x86_64`, producing a universal `libVisualPinballNativeWindows.dylib` (deployment target 14.0). Create its Unity `.meta` (Editor OSX and Standalone OSXUniversal enabled) when the binary is first added. + +On Linux install `cmake g++ libvulkan-dev libx11-dev libxrender-dev`. The fixture needs an X11 display (`DISPLAY`) and a Vulkan driver; it creates a window at a negative origin, checks geometry, capabilities, presentation through Unity's event pair for UNORM and sRGB sources, placement, z-order, detach, idempotent destruction and deferred retirement. It then reads back the presented centre pixel through a separate X11 connection. Virtual X servers such as Xvfb or WSLg may not expose Vulkan-presented contents to `XGetImage`; set `VPNW_SKIP_PIXEL_CHECK=1` to skip only that readback there. The window reports DPI from the session's `Xft.dpi` resource, or 96. + +## Limits + +Windows requires Windows 10 or later and Direct3D feature level 11.0; Direct3D12 also requires Unity's v7 native graphics interface and D3D11On12. macOS requires Unity Metal; Linux requires Unity Vulkan and an X11 or XWayland display, since Wayland toplevel placement is compositor controlled. Texture arrays, XR surfaces and arbitrary MSAA sources are not covered. Output is SDR 8-bit BGRA without tone mapping or color management. A desktop compositor or another topmost application can still affect visible stacking. + +## References + +The plugin uses the installed Unity SDK interfaces and the [official Unity native rendering sample](https://github.com/Unity-Technologies/NativeRenderingPlugin). Microsoft documents [full D3D11 context-state swapping](https://learn.microsoft.com/en-us/windows/win32/api/d3d11_1/nf-d3d11_1-id3d11devicecontext1-swapdevicecontextstate), [D3D11On12 wrapped resources](https://learn.microsoft.com/en-us/windows/win32/api/d3d11on12/nf-d3d11on12-id3d11on12device-createwrappedresource), and [DirectComposition swapchain creation](https://learn.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgifactory2-createswapchainforcomposition). diff --git a/Native~/NativeWindows/VisualPinballNativeWindows.h b/Native~/NativeWindows/VisualPinballNativeWindows.h new file mode 100644 index 000000000..c0532f5a5 --- /dev/null +++ b/Native~/NativeWindows/VisualPinballNativeWindows.h @@ -0,0 +1,86 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +// Public C API of VisualPinballNativeWindows. Identical on Windows (D3D11/D3D12), macOS (Metal) and Linux (Vulkan/X11). +#pragma once +#include + +#if defined(_WIN32) +#define VPNW_CALL __cdecl +#if defined(VPNW_BUILD) +#define VPNW_API extern "C" __declspec(dllexport) +#else +#define VPNW_API extern "C" __declspec(dllimport) +#endif +#else +#define VPNW_CALL +#define VPNW_API extern "C" __attribute__((visibility("default"))) +#endif + +// All coordinates and sizes are physical desktop pixels, including negative monitor origins. +// Handles are registry IDs, never HWNDs or pointers. IDs are never reused during a plugin load. +struct VPNW_Diagnostics +{ + std::uint32_t structBytes; + std::int32_t graphicsRenderer; + std::int32_t deviceReady; + std::int32_t alive; + std::int32_t visible; + std::int32_t topmost; + std::int32_t left; + std::int32_t top; + std::int32_t windowWidth; + std::int32_t windowHeight; + std::uint32_t dpi; + std::uint32_t sourceWidth; + std::uint32_t sourceHeight; + std::uint32_t sourceFormat; + std::uint64_t presentedFrames; + std::uint64_t skippedFrames; + // Windows: last presentation HRESULT. macOS/Linux: 0 on success, negative after a presentation failure. + std::int32_t lastResult; + std::int32_t premultipliedAlpha; +}; +static_assert(sizeof(VPNW_Diagnostics) == 80, "Managed diagnostics ABI must remain stable."); + +// Bits returned by VPNW_GetCapabilities. +enum VPNW_Capability : std::uint32_t +{ + VPNW_CapabilityWindows = 1, // Borderless GPU output windows can be created. + VPNW_CapabilityPlacement = 2, // VPNW_SetPlacementMode is functional. + VPNW_CapabilityTopmost = 4, // Topmost/non-topmost requests are honoured. + VPNW_CapabilityTransparency = 8 // Premultiplied alpha reaches the desktop compositor. +}; + +// Creates an initially hidden borderless window. Returns 0 on failure (see VPNW_GetLastError). +VPNW_API int VPNW_CALL VPNW_CreateWindow(int x, int y, int width, int height); +// Identical repeated calls do not reposition or reshow the window. +VPNW_API int VPNW_CALL VPNW_ConfigureWindow(int id, int x, int y, int width, int height, int visible, int topmost); +// order: 0 back, 1 front, 2 topmost, 3 non-topmost. Geometry and activation remain unchanged. +VPNW_API int VPNW_CALL VPNW_SetZOrder(int id, int order); +// Settings-only interaction: drag the body and resize at the edges. Playback windows remain fixed. +VPNW_API int VPNW_CALL VPNW_SetPlacementMode(int id, int enabled); +// Texture is Unity's native texture pointer (ID3D11Texture2D, ID3D12Resource, MTLTexture or VkImage*), +// with premultiplied RGBA in the desired SDR encoding. nullptr detaches the source. +VPNW_API int VPNW_CALL VPNW_SetTexture(int id, void* texture, int flipY); +// Idempotent. GPU resources are retired and collected by later render events. +VPNW_API int VPNW_CALL VPNW_DestroyWindow(int id); +VPNW_API void VPNW_CALL VPNW_DestroyAll(); +// Processes platform window events (X11 placement drags). No-op on Windows and macOS. Call from the main thread. +VPNW_API void VPNW_CALL VPNW_Pump(); +VPNW_API void* VPNW_CALL VPNW_GetRenderEventFunc(); +VPNW_API int VPNW_CALL VPNW_GetGraphicsRenderer(); +// Issue the draw event and then the present event once per frame; both operate on all registered windows. +VPNW_API int VPNW_CALL VPNW_GetDrawEventId(); +VPNW_API int VPNW_CALL VPNW_GetPresentEventId(); +// Pending retired source/presentation objects. Issue later event pairs until this reaches zero. +VPNW_API int VPNW_CALL VPNW_GetPendingResourceCount(); +// diagnostics->structBytes must be sizeof(VPNW_Diagnostics). +VPNW_API int VPNW_CALL VPNW_GetDiagnostics(int id, VPNW_Diagnostics* diagnostics); +// UTF-8, null terminated when capacity > 0. Return value is the required byte count, including the terminator. +VPNW_API int VPNW_CALL VPNW_GetLastError(char* destination, int capacity); +// VPNW_Capability bits for the current platform and session. +VPNW_API std::uint32_t VPNW_CALL VPNW_GetCapabilities(); diff --git a/Native~/NativeWindows/Windows/Build.ps1 b/Native~/NativeWindows/Windows/Build.ps1 new file mode 100644 index 000000000..99f63eb73 --- /dev/null +++ b/Native~/NativeWindows/Windows/Build.ps1 @@ -0,0 +1,51 @@ +param( + [string] $UnityEditor = 'C:\Program Files\Unity\Hub\Editor\6000.5.0f1\Editor', + [string] $OutputPath, + [switch] $Test +) +$ErrorActionPreference = 'Stop' +$nativeRoot = $PSScriptRoot +$apiRoot = [System.IO.Path]::GetFullPath((Join-Path $nativeRoot '..')) +$packageRoot = [System.IO.Path]::GetFullPath((Join-Path $nativeRoot '..\..\..')) +$sdkHeaders = Join-Path $UnityEditor 'Data\PluginAPI' +if (!(Test-Path -LiteralPath (Join-Path $sdkHeaders 'IUnityGraphicsD3D11.h'))) { + throw "Unity native plugin SDK headers were not found under $sdkHeaders. Supply -UnityEditor with the installed Editor directory." +} +$vswherePath = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' +$visualStudioRoot = & $vswherePath -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +if (!$visualStudioRoot) { throw 'Install the Visual Studio C++ x64 toolchain before building.' } +$msvcRoot = Get-ChildItem -LiteralPath (Join-Path $visualStudioRoot 'VC\Tools\MSVC') -Directory | Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty FullName +$windowsSdkRoot = (Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots').KitsRoot10 +$sdkVersion = Get-ChildItem -LiteralPath (Join-Path $windowsSdkRoot 'Include') -Directory | Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName 'um\dcomp.h') } | Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name +$compiler = Join-Path $msvcRoot 'bin\Hostx64\x64\cl.exe' +$linker = Join-Path $msvcRoot 'bin\Hostx64\x64\link.exe' +# Intermediates go to obj/, which the repository ignores. +$buildDirectory = Join-Path $nativeRoot 'obj' +$pluginDirectory = Join-Path $packageRoot 'VisualPinball.Unity\Plugins\win-x64' +New-Item -ItemType Directory -Force -Path $buildDirectory, $pluginDirectory | Out-Null +$includePaths = @((Join-Path $msvcRoot 'include'), $sdkHeaders, $apiRoot) +foreach ($part in @('um', 'shared', 'ucrt', 'winrt')) { $includePaths += Join-Path $windowsSdkRoot "Include\$sdkVersion\$part" } +$libraryPaths = @((Join-Path $msvcRoot 'lib\x64'), (Join-Path $windowsSdkRoot "Lib\$sdkVersion\um\x64"), (Join-Path $windowsSdkRoot "Lib\$sdkVersion\ucrt\x64")) +$source = Join-Path $nativeRoot $(if ($Test) { 'NativeWindowTests.cpp' } else { 'VisualPinballNativeWindows.cpp' }) +$object = Join-Path $buildDirectory $(if ($Test) { 'NativeWindowTests.obj' } else { 'VisualPinballNativeWindows.obj' }) +$output = $(if ($OutputPath) { [System.IO.Path]::GetFullPath($OutputPath) } elseif ($Test) { Join-Path $buildDirectory 'NativeWindowTests.exe' } else { Join-Path $pluginDirectory 'VisualPinballNativeWindows.dll' }) +New-Item -ItemType Directory -Force -Path (Split-Path -Parent $output) | Out-Null +$compileArguments = @('/nologo', '/c', '/std:c++17', '/permissive-', '/EHsc', '/W4', '/WX', '/O2', '/MD', '/utf-8', '/DUNICODE', '/D_UNICODE', '/D_WIN32_WINNT=0x0A00', "/Fo$object") +foreach ($path in $includePaths) { $compileArguments += "/I$path" } +& $compiler @compileArguments $source +if (!$Test -and $LASTEXITCODE -eq 0) { + $bridgeObject = Join-Path $buildDirectory 'D3D12CopyBridge.obj' + $bridgeArguments = $compileArguments | Where-Object { !($_ -like '/Fo*') } + & $compiler @bridgeArguments "/Fo$bridgeObject" (Join-Path $nativeRoot 'D3D12CopyBridge.cpp') +} +if ($LASTEXITCODE -ne 0) { throw "C++ compilation failed with exit code $LASTEXITCODE." } +$linkArguments = @('/NOLOGO', '/MACHINE:X64', '/INCREMENTAL:NO', '/OPT:REF', '/OPT:ICF', "/OUT:$output", "/IMPLIB:$(Join-Path $buildDirectory 'VisualPinballNativeWindows.lib')", $object, 'd3d11.lib', 'd3d12.lib', 'dxgi.lib', 'dcomp.lib', 'd3dcompiler.lib', 'user32.lib') +foreach ($path in $libraryPaths) { $linkArguments += "/LIBPATH:$path" } +if (!$Test) { $linkArguments += @('/DLL', $bridgeObject) } +& $linker @linkArguments +if ($LASTEXITCODE -ne 0) { throw "C++ linking failed with exit code $LASTEXITCODE." } +Write-Host "Built $output" +if ($Test) { + & $output + if ($LASTEXITCODE -ne 0) { throw "Native integration tests failed with exit code $LASTEXITCODE." } +} diff --git a/Native~/NativeWindows/Windows/D3D12CopyBridge.cpp b/Native~/NativeWindows/Windows/D3D12CopyBridge.cpp new file mode 100644 index 000000000..28623caf2 --- /dev/null +++ b/Native~/NativeWindows/Windows/D3D12CopyBridge.cpp @@ -0,0 +1,204 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#include "D3D12CopyBridge.h" +#include +#include +#include + +using Microsoft::WRL::ComPtr; + +namespace vpnw +{ + namespace + { + void Check12(HRESULT result, const char* operation) + { + if (FAILED(result)) { + std::ostringstream message; + message << operation << " failed (HRESULT 0x" << std::hex << static_cast(result) << ")."; + throw std::runtime_error(message.str()); + } + } + DXGI_FORMAT TypedFormat(DXGI_FORMAT format) + { + switch (format) { + case DXGI_FORMAT_R8G8B8A8_TYPELESS: case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; + case DXGI_FORMAT_B8G8R8A8_TYPELESS: case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; + case DXGI_FORMAT_R16G16B16A16_TYPELESS: return DXGI_FORMAT_R16G16B16A16_FLOAT; + case DXGI_FORMAT_R32G32B32A32_TYPELESS: return DXGI_FORMAT_R32G32B32A32_FLOAT; + default: return format; + } + } + D3D12_RESOURCE_BARRIER Barrier(ID3D12Resource* resource, D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES after) + { + D3D12_RESOURCE_BARRIER barrier{}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Transition.pResource = resource; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrier.Transition.StateBefore = before; + barrier.Transition.StateAfter = after; + return barrier; + } + } + + void D3D12CopyBridge::InitializeDevice() + { + if (_device) return; + if (!_unity || !_unity->GetDevice()) throw std::runtime_error("Unity D3D12 v7 device is unavailable."); + _device = _unity->GetDevice(); + } + + void D3D12CopyBridge::InitializePresentation(ID3D11Device** device, ID3D11DeviceContext** context) + { + InitializeDevice(); + ID3D12CommandQueue* queue = _unity->GetCommandQueue(); + if (!queue) throw std::runtime_error("D3D12 presentation must execute in the configured submission-thread event."); + IUnknown* queues[] = {queue}; + const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_0}; + Check12(D3D11On12CreateDevice(_device.Get(), D3D11_CREATE_DEVICE_BGRA_SUPPORT, + levels, 1, queues, 1, 0, device, context, nullptr), "Create same-queue D3D11On12 presenter"); + Check12((*device)->QueryInterface(IID_PPV_ARGS(&_on12)), "Query D3D11On12 interface"); + } + + std::shared_ptr D3D12CopyBridge::Source(void* nativeTexture) + { + if (!nativeTexture) return nullptr; + auto result = std::make_shared(); + Check12(static_cast(nativeTexture)->QueryInterface(IID_PPV_ARGS(&result->source)), "Acquire Unity D3D12 texture"); + const auto description = result->source->GetDesc(); + if (description.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D || description.DepthOrArraySize != 1 || + description.Width > 16384 || description.Height > 16384) + throw std::invalid_argument("D3D12 output requires a single 2D texture no larger than16384 pixels."); + result->width = static_cast(description.Width); result->height = description.Height; + result->format = description.Format; + return result; + } + + void D3D12CopyBridge::Prepare(D3D12CopySource& source) + { + if (source.snapshot) return; + ComPtr owner; + Check12(source.source->GetDevice(IID_PPV_ARGS(&owner)), "Read Unity texture device"); + if (owner.Get() != _device.Get()) throw std::invalid_argument("Texture belongs to a different D3D12 device."); + auto description = source.source->GetDesc(); + description.SampleDesc.Count = 1; description.SampleDesc.Quality = 0; + description.MipLevels = 1; description.Flags = D3D12_RESOURCE_FLAG_NONE; + // Compatible format-family copies retain the stored bytes. A typed UNORM snapshot + // avoids illegal UNORM views on typed sRGB resources, including Unity ARGB32 targets. + description.Format = TypedFormat(description.Format); + D3D12_HEAP_PROPERTIES heap{}; + heap.Type = D3D12_HEAP_TYPE_DEFAULT; heap.CreationNodeMask = 1; heap.VisibleNodeMask = 1; + Check12(_device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &description, + D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&source.snapshot)), "Create private GPU window snapshot"); + std::array slots; + for (auto& slot : slots) { + Check12(_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&slot.allocator)), "Create GPU copy allocator"); + Check12(_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, slot.allocator.Get(), nullptr, IID_PPV_ARGS(&slot.commands)), "Create GPU copy command list"); + Check12(slot.commands->Close(), "Close initial copy command list"); + } + source.slots = std::move(slots); + source.unityState.resource = source.source.Get(); + source.unityState.expected = source.unityState.current = source.source->GetDesc().SampleDesc.Count > 1 + ? D3D12_RESOURCE_STATE_RESOLVE_SOURCE : D3D12_RESOURCE_STATE_COPY_SOURCE; + } + + bool D3D12CopyBridge::RecordCopy(D3D12CopySource& source) + { + InitializeDevice(); + try { Prepare(source); } catch (...) { source.snapshot.Reset(); throw; } + const UINT64 completed = _unity->GetFrameFence()->GetCompletedValue(); + D3D12CopySource::CommandSlot* available = nullptr; + for (auto& slot : source.slots) if (slot.fence <= completed) { available = &slot; break; } + if (!available) return false; // Never block Unity waiting for a busy copy allocator. + Check12(available->allocator->Reset(), "Reset GPU copy allocator"); + Check12(available->commands->Reset(available->allocator.Get(), nullptr), "Reset GPU copy command list"); + const bool multisampled = source.source->GetDesc().SampleDesc.Count > 1; + const auto destinationState = multisampled ? D3D12_RESOURCE_STATE_RESOLVE_DEST : D3D12_RESOURCE_STATE_COPY_DEST; + auto before = Barrier(source.snapshot.Get(), D3D12_RESOURCE_STATE_COMMON, destinationState); + available->commands->ResourceBarrier(1, &before); + if (multisampled) { + available->commands->ResolveSubresource(source.snapshot.Get(), 0, source.source.Get(), 0, TypedFormat(source.format)); + } + else { + D3D12_TEXTURE_COPY_LOCATION destination{}, input{}; + destination.pResource = source.snapshot.Get(); destination.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + input.pResource = source.source.Get(); input.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + available->commands->CopyTextureRegion(&destination, 0, 0, 0, &input, nullptr); + } + auto after = Barrier(source.snapshot.Get(), destinationState, D3D12_RESOURCE_STATE_COMMON); + available->commands->ResourceBarrier(1, &after); + Check12(available->commands->Close(), "Close GPU copy command list"); + // Source state declaration is immutable after preparation; queued Unity workers may still read it. + // Unity orders this after preceding compositor work and inserts/tracks the source barriers. + available->fence = _unity->ExecuteCommandList(available->commands.Get(), 1, &source.unityState); + source.lastFence = (std::max)(source.lastFence, available->fence); + source.copied = true; + return true; + } + + ID3D11Resource* D3D12CopyBridge::Acquire(D3D12CopySource& source) + { + if (!_on12 || !source.copied) return nullptr; + source.lastFence = (std::max)(source.lastFence, FrameFenceValue()); + if (!source.wrapped) { + D3D11_RESOURCE_FLAGS flags{}; flags.BindFlags = D3D11_BIND_SHADER_RESOURCE; + Check12(_on12->CreateWrappedResource(source.snapshot.Get(), &flags, + D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COMMON, IID_PPV_ARGS(&source.wrapped)), "Wrap private GPU snapshot"); + } + ID3D11Resource* resource = source.wrapped.Get(); + _on12->AcquireWrappedResources(&resource, 1); + return resource; + } + + void D3D12CopyBridge::Release(D3D12CopySource& source) + { + if (!_on12 || !source.wrapped) return; + ID3D11Resource* resource = source.wrapped.Get(); + _on12->ReleaseWrappedResources(&resource, 1); + } + + void D3D12CopyBridge::Retire(std::shared_ptr source) + { + if (!source) return; + std::lock_guard lock(_retiredMutex); + _retired.push_back(std::move(source)); + } + + void D3D12CopyBridge::Collect() + { + if (!_unity) return; + const UINT64 completed = _unity->GetFrameFence()->GetCompletedValue(); + std::lock_guard lock(_retiredMutex); + for (auto item = _retired.begin(); item != _retired.end();) { + if ((*item)->lastFence <= completed) item = _retired.erase(item); + else ++item; + } + } + + int D3D12CopyBridge::PendingResourceCount() + { + std::lock_guard lock(_retiredMutex); + return static_cast(_retired.size()); + } + + void D3D12CopyBridge::Shutdown() + { + std::lock_guard lock(_retiredMutex); + _retired.clear(); _on12.Reset(); _device.Reset(); + } + + ComPtr D3D12CopyBridge::WrapTarget(ID3D12Resource* resource) + { + ComPtr result; + D3D11_RESOURCE_FLAGS flags{}; flags.BindFlags = D3D11_BIND_RENDER_TARGET; + Check12(_on12->CreateWrappedResource(resource, &flags, D3D12_RESOURCE_STATE_COMMON, + D3D12_RESOURCE_STATE_COMMON, IID_PPV_ARGS(&result)), "Wrap composition backbuffer"); + return result; + } + void D3D12CopyBridge::AcquireTarget(ID3D11Resource* resource) { _on12->AcquireWrappedResources(&resource, 1); } + void D3D12CopyBridge::ReleaseTarget(ID3D11Resource* resource) { _on12->ReleaseWrappedResources(&resource, 1); } +} diff --git a/Native~/NativeWindows/Windows/D3D12CopyBridge.h b/Native~/NativeWindows/Windows/D3D12CopyBridge.h new file mode 100644 index 000000000..4240259f7 --- /dev/null +++ b/Native~/NativeWindows/Windows/D3D12CopyBridge.h @@ -0,0 +1,70 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#pragma once +#include +#include +#include +#include +#include "IUnityGraphicsD3D12.h" +#include +#include +#include +#include + +namespace vpnw +{ + // A source and its private GPU snapshot. Copies are submitted through Unity's state tracker. + // The snapshot is then read by D3D11On12 on the same queue after Unity flushes its workers. + struct D3D12CopySource + { + Microsoft::WRL::ComPtr source; + Microsoft::WRL::ComPtr snapshot; + Microsoft::WRL::ComPtr wrapped; + struct CommandSlot + { + Microsoft::WRL::ComPtr allocator; + Microsoft::WRL::ComPtr commands; + UINT64 fence = 0; + }; + std::array slots; + UINT64 lastFence = 0; + UINT width = 0, height = 0; + DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN; + bool copied = false; + UnityGraphicsD3D12ResourceState unityState{}; + }; + + class D3D12CopyBridge + { + IUnityGraphicsD3D12v7* _unity = nullptr; + Microsoft::WRL::ComPtr _device; + Microsoft::WRL::ComPtr _on12; + std::mutex _retiredMutex; + std::vector> _retired; + void Prepare(D3D12CopySource& source); + public: + void SetUnity(IUnityGraphicsD3D12v7* unity) { _unity = unity; } + bool Supported() const { return _unity != nullptr; } + bool DeviceReady() const { return _device != nullptr; } + void InitializeDevice(); // Render thread: no direct queue access. + void InitializePresentation(ID3D11Device** device, ID3D11DeviceContext** context); // Submission thread only. + std::shared_ptr Source(void* nativeTexture); + bool RecordCopy(D3D12CopySource& source); + ID3D11Resource* Acquire(D3D12CopySource& source); + void Release(D3D12CopySource& source); + IUnknown* Queue() { return _unity->GetCommandQueue(); } // Submission-thread event only. + Microsoft::WRL::ComPtr WrapTarget(ID3D12Resource* resource); + void AcquireTarget(ID3D11Resource* resource); + void ReleaseTarget(ID3D11Resource* resource); + UINT64 FrameFenceValue() { return _unity->GetNextFrameFenceValue(); } + bool Completed(UINT64 value) { return _unity->GetFrameFence()->GetCompletedValue() >= value; } + void Retire(std::shared_ptr source); + int PendingResourceCount(); + void Collect(); // Rendering/submission thread only. + void Shutdown(); + }; +} diff --git a/Native~/NativeWindows/Windows/NativeWindowTests.cpp b/Native~/NativeWindows/Windows/NativeWindowTests.cpp new file mode 100644 index 000000000..2496ad24a --- /dev/null +++ b/Native~/NativeWindows/Windows/NativeWindowTests.cpp @@ -0,0 +1,323 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +// Standalone integration checks against the same production source, with a small Unity-interface fixture. +// Owns temporary output windows and GPU devices only; it does not alter display settings. +#include "VisualPinballNativeWindows.cpp" +#include "D3D12CopyBridge.cpp" +#include + +using namespace vpnw; + +namespace Test +{ + UnityGfxRenderer renderer = kUnityGfxRendererD3D11; + ComPtr device11; + ComPtr device12; + ComPtr queue12; + ComPtr fence12; + UINT64 nextFence = 1; + std::vector> transitionAllocators; + std::vector> transitionLists; + std::unordered_map states; + + UnityGfxRenderer UNITY_INTERFACE_API GetRenderer() { return renderer; } + void UNITY_INTERFACE_API Register(IUnityGraphicsDeviceEventCallback) {} + void UNITY_INTERFACE_API Unregister(IUnityGraphicsDeviceEventCallback) {} + int UNITY_INTERFACE_API Reserve(int) { return 10000; } + ID3D11Device* UNITY_INTERFACE_API Device11() { return device11.Get(); } + ID3D12Device* UNITY_INTERFACE_API Device12() { return device12.Get(); } + ID3D12CommandQueue* UNITY_INTERFACE_API Queue12() { return queue12.Get(); } + ID3D12Fence* UNITY_INTERFACE_API Fence12() { return fence12.Get(); } + UINT64 UNITY_INTERFACE_API NextFence() { return nextFence; } + void UNITY_INTERFACE_API Configure(int, const UnityD3D12PluginEventConfig*) {} + UINT64 UNITY_INTERFACE_API Execute(ID3D12GraphicsCommandList* commands, int count, UnityGraphicsD3D12ResourceState* requested) + { + ComPtr allocator; + ComPtr preparation; + Check12(device12->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&allocator)), "Create fixture state allocator"); + Check12(device12->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.Get(), nullptr, IID_PPV_ARGS(&preparation)), "Create fixture state command list"); + for (int index = 0; index < count; index++) { + auto& state = requested[index]; + if (states[state.resource] != state.expected) { + auto barrier = Barrier(state.resource, states[state.resource], state.expected); + preparation->ResourceBarrier(1, &barrier); + } + states[state.resource] = state.current; + } + Check12(preparation->Close(), "Close fixture preparation"); + ID3D12CommandList* lists[] = {preparation.Get(), commands}; + queue12->ExecuteCommandLists(2, lists); + transitionAllocators.push_back(allocator); transitionLists.push_back(preparation); + return nextFence; + } + IUnityGraphics graphics{}; + IUnityGraphicsD3D11 graphics11{}; + IUnityGraphicsD3D12v7 graphics12{}; + IUnityInterface* UNITY_INTERFACE_API Interface(UnityInterfaceGUID guid) + { + auto matches = [guid](UnityInterfaceGUID candidate) { return guid.m_GUIDHigh == candidate.m_GUIDHigh && guid.m_GUIDLow == candidate.m_GUIDLow; }; + if (matches(GetUnityInterfaceGUID())) return &graphics; + if (renderer == kUnityGfxRendererD3D11 && matches(GetUnityInterfaceGUID())) return &graphics11; + if (renderer == kUnityGfxRendererD3D12 && matches(GetUnityInterfaceGUID())) return &graphics12; + return nullptr; + } + void Assert(bool condition, const char* description) + { + if (!condition) throw std::runtime_error(std::string(description) + ": " + LastError); + } + void Wait12() + { + Check12(queue12->Signal(fence12.Get(), nextFence), "Signal fixture frame fence"); + HANDLE completed = CreateEventW(nullptr, FALSE, FALSE, nullptr); + Check12(fence12->SetEventOnCompletion(nextFence++, completed), "Wait for fixture GPU work"); + Assert(WaitForSingleObject(completed, 10000) == WAIT_OBJECT_0, "GPU fixture completion"); + CloseHandle(completed); + transitionAllocators.clear(); transitionLists.clear(); + } + ComPtr Texture11(DXGI_FORMAT format = DXGI_FORMAT_B8G8R8A8_UNORM, bool corners = false) + { + const std::uint32_t pixels[4] = {corners ? 0xffff0000u : 0x80008000u, corners ? 0xff00ff00u : 0x80008000u, corners ? 0xff0000ffu : 0x80008000u, corners ? 0xffffff00u : 0x80008000u}; + D3D11_TEXTURE2D_DESC description{}; + description.Width = description.Height = 2; description.ArraySize = description.MipLevels = 1; + description.Format = format; description.SampleDesc.Count = 1; + description.Usage = D3D11_USAGE_IMMUTABLE; description.BindFlags = D3D11_BIND_SHADER_RESOURCE; + D3D11_SUBRESOURCE_DATA data{pixels, 8, 0}; + ComPtr texture; + Check(device11->CreateTexture2D(&description, &data, &texture), "Create fixture RGBA texture"); + return texture; + } + ComPtr Texture12(DXGI_FORMAT format = DXGI_FORMAT_B8G8R8A8_UNORM, bool corners = false) + { + D3D12_RESOURCE_DESC description{}; + description.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; description.Width = description.Height = 2; + description.DepthOrArraySize = description.MipLevels = 1; + description.Format = format; description.SampleDesc.Count = 1; + D3D12_HEAP_PROPERTIES heap{}; heap.Type = D3D12_HEAP_TYPE_DEFAULT; + ComPtr texture; + Check12(device12->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &description, + D3D12_RESOURCE_STATE_COPY_DEST, nullptr, IID_PPV_ARGS(&texture)), "Create D3D12 fixture texture"); + D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint{}; UINT64 bytes; + device12->GetCopyableFootprints(&description, 0, 1, 0, &footprint, nullptr, nullptr, &bytes); + D3D12_RESOURCE_DESC uploadDescription{}; + uploadDescription.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; uploadDescription.Width = bytes; + uploadDescription.Height = uploadDescription.DepthOrArraySize = uploadDescription.MipLevels = 1; + uploadDescription.SampleDesc.Count = 1; uploadDescription.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + heap.Type = D3D12_HEAP_TYPE_UPLOAD; + ComPtr upload; + Check12(device12->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &uploadDescription, + D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&upload)), "Create fixture upload"); + void* pointer; Check12(upload->Map(0, nullptr, &pointer), "Map fixture upload"); + const std::uint32_t pixels[4] = {0xffff0000u, 0xff00ff00u, 0xff0000ffu, 0xffffff00u}; + for (UINT y = 0; y < 2; y++) for (UINT x = 0; x < 2; x++) + *reinterpret_cast(static_cast(pointer) + y * footprint.Footprint.RowPitch + x * 4) = corners ? pixels[y * 2 + x] : 0x80008000; + upload->Unmap(0, nullptr); + ComPtr allocator; ComPtr commands; + Check12(device12->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&allocator)), "Create fixture upload allocator"); + Check12(device12->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, allocator.Get(), nullptr, IID_PPV_ARGS(&commands)), "Create fixture upload commands"); + D3D12_TEXTURE_COPY_LOCATION output{}, input{}; + output.pResource = texture.Get(); input.pResource = upload.Get(); input.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + input.PlacedFootprint = footprint; + commands->CopyTextureRegion(&output, 0, 0, 0, &input, nullptr); + Check12(commands->Close(), "Close fixture upload"); + ID3D12CommandList* list = commands.Get(); queue12->ExecuteCommandLists(1, &list); + states[texture.Get()] = D3D12_RESOURCE_STATE_COPY_DEST; + Wait12(); + return texture; + } + void Frame() + { + RenderEvent(VPNW_GetDrawEventId()); + RenderEvent(VPNW_GetPresentEventId()); + if (renderer == kUnityGfxRendererD3D12) Wait12(); + } + void NestedWindowWork(HWND ownWindow, int id) + { + std::promise entered; + auto started = entered.get_future(); + std::atomic finish{false}; + auto nested = std::async(std::launch::async, [&] { + GetWindowThread()->Invoke([&] { + entered.set_value(); + while (!finish.load()) { + MSG message{}; + if (GetMessageW(&message, nullptr, 0, 0) > 0) DispatchMessageW(&message); + } + }); + }); + started.get(); + auto queued = std::async(std::launch::async, [=] { return VPNW_SetZOrder(id, 1); }); + const bool responsive = queued.wait_for(std::chrono::seconds(2)) == std::future_status::ready; + finish.store(true); PostMessageW(ownWindow, WM_NULL, 0, 0); + nested.get(); + Assert(responsive && queued.get() != 0, "Window work completes inside a nested modal message loop"); + } + void CheckPresentedPixels(int id, bool corners = false, bool flipped = false) + { + // One-off fixture readback proves GPU shader/color/alpha output. Production never reads pixels. + auto surface = Require(id); + RenderSurface(surface, false); + if (renderer == kUnityGfxRendererD3D12) Wait12(); + ComPtr rendered; + surface->targetView->GetResource(&rendered); + ComPtr texture; + Check(rendered.As(&texture), "Read fixture backbuffer type"); + D3D11_TEXTURE2D_DESC description{}; texture->GetDesc(&description); + description.Usage = D3D11_USAGE_STAGING; description.BindFlags = 0; + description.CPUAccessFlags = D3D11_CPU_ACCESS_READ; description.MiscFlags = 0; + ComPtr staging; + Check(Device->CreateTexture2D(&description, nullptr, &staging), "Create test-only pixel readback"); + if (renderer == kUnityGfxRendererD3D12) CopyBridge.AcquireTarget(rendered.Get()); + Context->CopyResource(staging.Get(), rendered.Get()); + if (renderer == kUnityGfxRendererD3D12) { + CopyBridge.ReleaseTarget(rendered.Get()); Context->Flush(); Wait12(); + } + D3D11_MAPPED_SUBRESOURCE mapped{}; + Check(Context->Map(staging.Get(), 0, D3D11_MAP_READ, 0, &mapped), "Map test-only pixel readback"); + const auto pixel = *reinterpret_cast(static_cast(mapped.pData) + + (description.Height / 2) * mapped.RowPitch + (description.Width / 2) * 4); + bool correct = pixel == 0x80008000; + if (corners) { + const std::uint32_t expected[4] = {0xffff0000u, 0xff00ff00u, 0xff0000ffu, 0xffffff00u}; + correct = true; + for (UINT y = 0; y < 2; y++) for (UINT x = 0; x < 2; x++) { + const auto observed = *reinterpret_cast(static_cast(mapped.pData) + + (y ? description.Height - 8 : 8) * mapped.RowPitch + (x ? description.Width - 8 : 8) * 4); + correct &= observed == expected[(flipped ? 1 - y : y) * 2 + x]; + } + } + Context->Unmap(staging.Get(), 0); + Assert(correct, corners ? "Native output preserves four-corner orientation and explicit vertical flip" : "GPU output retains exact stored SDR green128/alpha128 premultiplied bytes"); + } + void Roundtrip(IUnityInterfaces& interfaces, IUnknown* texture) + { + UnityPluginLoad(&interfaces); + const int id = VPNW_CreateWindow(-160, -96, 320, 240); + Assert(id != 0, "Create borderless window"); + Assert(VPNW_ConfigureWindow(id, -160, -96, 320, 240, 0, 1) != 0, "Configure signed desktop rectangle"); + VPNW_Diagnostics diagnostics{}; diagnostics.structBytes = sizeof(diagnostics); + Assert(VPNW_GetDiagnostics(id, &diagnostics) != 0, "Read hidden window diagnostics"); + Assert(diagnostics.left == -160 && diagnostics.top == -96 && diagnostics.windowWidth == 320 && diagnostics.windowHeight == 240, + "Exact physical signed window rectangle"); + std::cout << "hidden dpi=" << diagnostics.dpi << " visible=" << diagnostics.visible << " topmost=" << diagnostics.topmost << "\n"; + Assert(diagnostics.dpi >= 96 && diagnostics.visible == 0, "DPI/visibility/z-order"); + Assert(VPNW_SetPlacementMode(id, 1) != 0, "Enable native placement interaction"); + const HWND ownWindow = Require(id)->window.load(); + GetWindowThread()->Invoke([&] { + RECT bounds{}; GetWindowRect(ownWindow, &bounds); + Assert(SendMessageW(ownWindow, WM_NCHITTEST, 0, MAKELPARAM((bounds.left + bounds.right) / 2, (bounds.top + bounds.bottom) / 2)) == HTCAPTION, "Drag the placement window body"); + Assert(SendMessageW(ownWindow, WM_NCHITTEST, 0, MAKELPARAM(bounds.left + 2, bounds.top + 2)) == HTTOPLEFT, "Resize the placement window corner"); + }); + Assert(VPNW_SetPlacementMode(id, 0) != 0, "Disable placement during normal playback"); + Assert(VPNW_SetTexture(id, texture, 0) != 0, "Retain GPU source texture"); + Assert(VPNW_ConfigureWindow(id, 20, 20, 64, 64, 1, 0) != 0, "Show temporary own output window"); + Assert(VPNW_SetPlacementMode(id, 1) && VPNW_SetPlacementMode(id, 0) && IsWindowVisible(ownWindow), "Placement toggles preserve visible window style"); + NestedWindowWork(ownWindow, id); + Assert(VPNW_SetZOrder(id, 2) != 0 && VPNW_GetDiagnostics(id, &diagnostics) != 0 && diagnostics.topmost == 1, "Topmost visible output"); + Assert(VPNW_ConfigureWindow(id, 20, 20, 64, 64, 1, 0) != 0 && VPNW_GetDiagnostics(id, &diagnostics) != 0 && diagnostics.topmost == 0, + "Configure clears a topmost state set through z-order"); + Assert(VPNW_SetZOrder(id, 2) != 0 && VPNW_GetDiagnostics(id, &diagnostics) != 0 && diagnostics.topmost == 1, "Topmost visible output again"); + Assert(VPNW_SetZOrder(id, 3) != 0 && VPNW_GetDiagnostics(id, &diagnostics) != 0 && diagnostics.topmost == 0, "Clear topmost visible output"); + Assert(VPNW_SetZOrder(id, 0) != 0 && VPNW_SetZOrder(id, 1) != 0, "Send back and bring front"); + for (int attempt = 0; attempt < 20; attempt++) { + Frame(); + Assert(VPNW_GetDiagnostics(id, &diagnostics) != 0, "Read nonblocking presentation attempt"); + if (diagnostics.presentedFrames > 0) break; + Sleep(10); // A busy DWM may reject an initial DO_NOT_WAIT present; bounded fixture retry only. + } + Assert(VPNW_GetDiagnostics(id, &diagnostics) != 0, "Read presented output diagnostics"); + std::cout << "presentation renderer=" << diagnostics.graphicsRenderer << " ready=" << diagnostics.deviceReady << + " visible=" << diagnostics.visible << " source=" << diagnostics.sourceWidth << 'x' << diagnostics.sourceHeight << + " presents=" << diagnostics.presentedFrames << " skips=" << diagnostics.skippedFrames << " hr=" << diagnostics.lastResult << "\n"; + Assert((diagnostics.presentedFrames > 0 || diagnostics.lastResult == DXGI_STATUS_OCCLUDED) && + diagnostics.sourceWidth == 2 && diagnostics.sourceHeight == 2, "GPU texture presented or desktop explicitly occluded"); + Assert(diagnostics.premultipliedAlpha == 1 && diagnostics.lastResult >= 0, "DirectComposition premultiplied-alpha output"); + CheckPresentedPixels(id); + Assert(VPNW_ConfigureWindow(id, 20, 20, 96, 48, 1, 0) != 0, "Resize own output"); + Frame(); Frame(); + Assert(VPNW_GetDiagnostics(id, &diagnostics) != 0 && diagnostics.windowWidth == 96 && diagnostics.windowHeight == 48, "Resize presentation path"); + const auto before = diagnostics.presentedFrames; + Assert(VPNW_SetTexture(id, nullptr, 0) != 0, "Detach source"); + Frame(); + Assert(VPNW_GetDiagnostics(id, &diagnostics) != 0 && diagnostics.presentedFrames == before, "No texture means no presentation"); + Assert(VPNW_DestroyWindow(id) != 0 && VPNW_DestroyWindow(id) != 0, "Idempotent destruction"); + Frame(); // Queued events can outlive the surface; no recycled ID/pointer is dereferenced. + if (renderer == kUnityGfxRendererD3D12) { + const int pending = VPNW_CreateWindow(20, 20, 32, 32); + Assert(pending != 0 && VPNW_SetTexture(pending, texture, 0) != 0 && + VPNW_ConfigureWindow(pending, 20, 20, 32, 32, 1, 0) != 0, "Create pending-frame destruction fixture"); + RenderEvent(VPNW_GetDrawEventId()); RenderEvent(VPNW_GetPresentEventId()); + Assert(VPNW_SetTexture(pending, nullptr, 0) != 0 && VPNW_DestroyWindow(pending) != 0, "Detach and destroy before GPU frame fence"); + Assert(!RetiredSurfaces.empty(), "Detached presentation resources retained until fence"); + Assert(VPNW_GetPendingResourceCount() >= 2, "Pending source and presentation resource diagnostics"); + Wait12(); Frame(); + Assert(RetiredSurfaces.empty(), "Completed presentation resources collected"); + Assert(VPNW_GetPendingResourceCount() == 0, "Cleanup events drain pending resource diagnostics"); + } + UnityPluginUnload(); + std::cout << "Passed native backend " << static_cast(renderer) << " roundtrip/pixel output; presents=" << before << ".\n"; + } + void Orientation(IUnityInterfaces& interfaces, IUnknown* texture) + { + UnityPluginLoad(&interfaces); + const int id = VPNW_CreateWindow(20, 20, 64, 64); + Assert(id != 0 && VPNW_ConfigureWindow(id, 20, 20, 64, 64, 1, 0), "Create orientation fixture"); + for (int flip : {0, 1}) { + Assert(VPNW_SetTexture(id, texture, flip) != 0, "Set corner texture"); + Frame(); CheckPresentedPixels(id, true, flip != 0); + } + VPNW_DestroyWindow(id); Frame(); UnityPluginUnload(); + std::cout << "Passed four-corner orientation for backend " << static_cast(renderer) << ".\n"; + } +} + +int main() +{ + try { + Test::graphics.GetRenderer = Test::GetRenderer; Test::graphics.RegisterDeviceEventCallback = Test::Register; + Test::graphics.UnregisterDeviceEventCallback = Test::Unregister; Test::graphics.ReserveEventIDRange = Test::Reserve; + Test::graphics11.GetDevice = Test::Device11; + Test::graphics12.GetDevice = Test::Device12; Test::graphics12.GetCommandQueue = Test::Queue12; + Test::graphics12.GetFrameFence = Test::Fence12; Test::graphics12.GetNextFrameFenceValue = Test::NextFence; + Test::graphics12.ExecuteCommandList = Test::Execute; Test::graphics12.ConfigureEvent = Test::Configure; + IUnityInterfaces interfaces{}; interfaces.GetInterface = Test::Interface; + const std::uint32_t capabilities = VPNW_CapabilityWindows | VPNW_CapabilityPlacement | VPNW_CapabilityTopmost | VPNW_CapabilityTransparency; + Test::Assert(VPNW_GetCapabilities() == capabilities, "Windows reports window, placement, topmost and transparency capabilities"); + VPNW_Pump(); // No-op on Windows; must be safe to call before any window exists. + Check(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT, + nullptr, 0, D3D11_SDK_VERSION, &Test::device11, nullptr, nullptr), "Create D3D11 test device"); + auto texture11 = Test::Texture11(); + Test::Roundtrip(interfaces, texture11.Get()); + auto corners11 = Test::Texture11(DXGI_FORMAT_B8G8R8A8_UNORM, true); + Test::Orientation(interfaces, corners11.Get()); + for (auto format : {DXGI_FORMAT_R8G8B8A8_TYPELESS, DXGI_FORMAT_B8G8R8A8_TYPELESS, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB}) { + std::cout << "D3D11 source format=" << static_cast(format) << "\n"; + auto typedFixture = Test::Texture11(format); + Test::Roundtrip(interfaces, typedFixture.Get()); + } + Test::renderer = kUnityGfxRendererD3D12; + Check12(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&Test::device12)), "Create D3D12 test device"); + D3D12_COMMAND_QUEUE_DESC queue{}; queue.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + Check12(Test::device12->CreateCommandQueue(&queue, IID_PPV_ARGS(&Test::queue12)), "Create D3D12 test queue"); + Check12(Test::device12->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&Test::fence12)), "Create D3D12 test fence"); + auto texture12 = Test::Texture12(); + Test::Roundtrip(interfaces, texture12.Get()); + auto corners12 = Test::Texture12(DXGI_FORMAT_B8G8R8A8_UNORM, true); + Test::Orientation(interfaces, corners12.Get()); + for (auto format : {DXGI_FORMAT_R8G8B8A8_TYPELESS, DXGI_FORMAT_B8G8R8A8_TYPELESS, DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB}) { + std::cout << "D3D12 source format=" << static_cast(format) << "\n"; + auto typedFixture = Test::Texture12(format); + Test::Roundtrip(interfaces, typedFixture.Get()); + } + return 0; + } + catch (const std::exception& error) { + std::cerr << error.what() << "\n"; + VPNW_DestroyAll(); + UnityPluginUnload(); + return 1; + } +} diff --git a/Native~/NativeWindows/Windows/VisualPinballNativeWindows.cpp b/Native~/NativeWindows/Windows/VisualPinballNativeWindows.cpp new file mode 100644 index 000000000..fb9ff9dfe --- /dev/null +++ b/Native~/NativeWindows/Windows/VisualPinballNativeWindows.cpp @@ -0,0 +1,855 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +#define VPNW_BUILD +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include "VisualPinballNativeWindows.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "IUnityInterface.h" +#include "IUnityGraphics.h" +#include "IUnityGraphicsD3D11.h" +#include "D3D12CopyBridge.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Microsoft::WRL::ComPtr; +extern "C" IMAGE_DOS_HEADER __ImageBase; + +namespace vpnw +{ + constexpr wchar_t WindowClass[] = L"VisualPinball.NativeWindow"; + constexpr wchar_t WindowTitle[] = L"Visual Pinball Window"; + constexpr wchar_t PlacementProperty[] = L"VisualPinball.NativeWindow.Placement"; + std::mutex ErrorMutex; + std::string LastError; + IUnityInterfaces* UnityInterfaces = nullptr; + IUnityGraphics* UnityGraphics = nullptr; + std::atomic Renderer{-1}; + D3D12CopyBridge CopyBridge; + int DrawEventId = 0, PresentEventId = 0; + + void RecordError(const std::string& message) + { + std::lock_guard lock(ErrorMutex); + LastError = message; + OutputDebugStringA(("VisualPinballNativeWindows: " + message + "\n").c_str()); + } + + void Check(HRESULT result, const char* operation) + { + if (FAILED(result)) { + std::ostringstream text; + text << operation << " failed (HRESULT 0x" << std::hex << static_cast(result) << ")."; + throw std::runtime_error(text.str()); + } + } + + void CheckWindow(bool success, const char* operation) + { + if (!success) + throw std::runtime_error(std::string(operation) + " failed (Win32 " + std::to_string(GetLastError()) + ")."); + } + + template int Guard(Function operation) + { + try { return operation(); } + catch (const std::exception& error) { RecordError(error.what()); } + catch (...) { RecordError("Unexpected native exception."); } + return 0; + } + + void ValidateRectangle(int x, int y, int width, int height) + { + if (width < 1 || height < 1 || width > D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION || height > D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION) + throw std::invalid_argument("Window dimensions must be between 1 and 16384 physical pixels."); + if (static_cast(x) + width > INT_MAX || static_cast(y) + height > INT_MAX) + throw std::invalid_argument("Desktop rectangle exceeds signed Win32 coordinate range."); + } + + constexpr UINT WindowTasksMessage = WM_APP + 1; + void DrainWindowTasks(void* thread); + LRESULT CALLBACK WindowProcedure(HWND window, UINT message, WPARAM wParam, LPARAM lParam) + { + if (message == WM_NCCREATE) SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast(reinterpret_cast(lParam)->lpCreateParams)); + if (message == WindowTasksMessage) { + if (auto thread = reinterpret_cast(GetWindowLongPtrW(window, GWLP_USERDATA))) DrainWindowTasks(thread); + return 0; + } + if (GetPropW(window, PlacementProperty)) { + if (message == WM_NCCALCSIZE) return 0; + if (message == WM_NCHITTEST) { + RECT bounds{}; GetWindowRect(window, &bounds); + const int x = GET_X_LPARAM(lParam), y = GET_Y_LPARAM(lParam); + const bool left = x < bounds.left + 10, right = x >= bounds.right - 10; + const bool top = y < bounds.top + 10, bottom = y >= bounds.bottom - 10; + if (top) return left ? HTTOPLEFT : right ? HTTOPRIGHT : HTTOP; + if (bottom) return left ? HTBOTTOMLEFT : right ? HTBOTTOMRIGHT : HTBOTTOM; + return left ? HTLEFT : right ? HTRIGHT : HTCAPTION; + } + if (message == WM_GETMINMAXINFO) { + auto* limits = reinterpret_cast(lParam); + limits->ptMinTrackSize = { 64, 32 }; return 0; + } + } + switch (message) { + case WM_PAINT: ValidateRect(window, nullptr); return 0; + case WM_ERASEBKGND: return 1; + case WM_MOUSEACTIVATE: return MA_NOACTIVATE; + case WM_CLOSE: ShowWindow(window, SW_HIDE); return 0; + // The application owns physical-pixel rectangles. Do not substitute Windows' suggested logical size. + case WM_DPICHANGED: return 0; + default: return DefWindowProcW(window, message, wParam, lParam); + } + } + + // HWND creation, positioning and destruction stay on one message-pumping thread. + // This thread's DPI context changes; the Unity process-wide DPI policy does not. + class WindowThread + { + HANDLE _wake = nullptr; + std::thread _thread; + std::atomic _stop{false}; + DWORD _threadId = 0; + HWND _dispatcher = nullptr; + std::mutex _mutex; + std::deque> _tasks; + public: + WindowThread() + { + _wake = CreateEventW(nullptr, FALSE, FALSE, nullptr); + CheckWindow(_wake != nullptr, "CreateEvent"); + std::promise started; + auto future = started.get_future(); + _thread = std::thread([this, ready = std::move(started)]() mutable { + _threadId = GetCurrentThreadId(); + try { + CheckWindow(SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) != nullptr, "SetThreadDpiAwarenessContext"); + WNDCLASSEXW description{}; + description.cbSize = sizeof(description); + description.lpfnWndProc = WindowProcedure; + description.hInstance = reinterpret_cast(&__ImageBase); + description.hCursor = LoadCursorW(nullptr, IDC_ARROW); + description.lpszClassName = WindowClass; + CheckWindow(RegisterClassExW(&description) != 0, "RegisterClassEx"); + _dispatcher = CreateWindowExW(0, WindowClass, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, description.hInstance, this); + CheckWindow(_dispatcher != nullptr, "Create window-task dispatcher"); + ready.set_value(); + } + catch (...) { ready.set_exception(std::current_exception()); return; } + while (!_stop.load()) { + MsgWaitForMultipleObjects(1, &_wake, FALSE, INFINITE, QS_ALLINPUT); + DrainTasks(); + MSG message{}; + while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { + TranslateMessage(&message); + DispatchMessageW(&message); + } + } + DestroyWindow(_dispatcher); + UnregisterClassW(WindowClass, reinterpret_cast(&__ImageBase)); + }); + try { future.get(); } + catch (...) { _thread.join(); CloseHandle(_wake); throw; } + } + ~WindowThread() + { + _stop.store(true); + SetEvent(_wake); + _thread.join(); + CloseHandle(_wake); + } + template auto Invoke(Function operation) -> decltype(operation()) + { + if (GetCurrentThreadId() == _threadId) return operation(); + using Result = decltype(operation()); + auto task = std::make_shared>(std::move(operation)); + auto future = task->get_future(); + { std::lock_guard lock(_mutex); _tasks.emplace_back([task] { (*task)(); }); } + SetEvent(_wake); + // Windows' nested move/resize loop dispatches window messages, but does not + // wait on our wake event. A message-only HWND keeps queued calls responsive there. + PostMessageW(_dispatcher, WindowTasksMessage, 0, 0); + return future.get(); + } + void DrainTasks() + { + std::deque> tasks; + { std::lock_guard lock(_mutex); tasks.swap(_tasks); } + for (auto& task : tasks) task(); + } + }; + void DrainWindowTasks(void* thread) { static_cast(thread)->DrainTasks(); } + + struct Surface + { + std::mutex mutex; + std::atomic alive{true}; + std::atomic window{nullptr}; + ComPtr source; + std::shared_ptr source12; + ComPtr resolved; + ComPtr sourceView; + bool copyStoredValues = false; + ComPtr swapChain; + ComPtr targetView; + std::array, 2> targetViews12; + std::array, 2> targetResources12; + ID3D11Resource* acquiredTarget12 = nullptr; + UINT64 lastFence12 = 0; + ComPtr composition; + ComPtr compositionTarget; + ComPtr visual; + UINT width = 0, height = 0; + bool flipY = false; + // Configuration cache is owned exclusively by the HWND message thread. + bool configured = false, requestedVisible = false, requestedTopmost = false; + int requestedX = 0, requestedY = 0, requestedWidth = 0, requestedHeight = 0; + std::uint64_t presented = 0, skipped = 0; + HRESULT lastResult = S_OK; + + void ReleasePresentation() + { + if (compositionTarget) compositionTarget->SetRoot(nullptr); + if (visual) visual->SetContent(nullptr); + if (composition) composition->Commit(); + visual.Reset(); compositionTarget.Reset(); composition.Reset(); + targetView.Reset(); + for (auto& view : targetViews12) view.Reset(); + for (auto& resource : targetResources12) resource.Reset(); + swapChain.Reset(); + width = height = 0; + } + }; + + std::mutex RegistryMutex; + std::unordered_map> Surfaces; + std::uint64_t NextId = 1; + std::mutex RetiredSurfaceMutex; + std::vector> RetiredSurfaces; + std::mutex WindowServiceMutex; + std::shared_ptr WindowService; + + std::shared_ptr GetWindowThread() + { + std::lock_guard lock(WindowServiceMutex); + if (!WindowService) WindowService = std::make_shared(); + return WindowService; + } + + std::shared_ptr Find(int id) + { + std::lock_guard lock(RegistryMutex); + auto item = Surfaces.find(id); + return item == Surfaces.end() ? nullptr : item->second; + } + + std::shared_ptr Require(int id) + { + auto surface = Find(id); + if (!surface) throw std::invalid_argument("Unknown or destroyed native window ID."); + return surface; + } + + std::mutex DeviceMutex; + ComPtr Device; + ComPtr Context; + ComPtr Context1; + ComPtr PluginContextState; + ComPtr VertexShader; + ComPtr PixelShader; + ComPtr Sampler; + ComPtr Rasterizer; + ComPtr Blend; + ComPtr Depth; + ComPtr Parameters; + + constexpr char ShaderSource[] = R"( +cbuffer Parameters : register(b0) { float flipY; float3 padding; }; +struct Vertex { float4 position : SV_Position; float2 uv : TEXCOORD0; }; +Vertex VertexMain(uint id : SV_VertexID) { + Vertex result; + result.uv = float2((id << 1) & 2, id & 2); + result.position = float4(result.uv * float2(2, -2) + float2(-1, 1), 0, 1); + return result; +} +Texture2D scene : register(t0); +SamplerState sceneSampler : register(s0); +float4 PixelMain(Vertex input) : SV_Target { + float2 uv = input.uv; + if (flipY != 0) uv.y = 1 - uv.y; + return scene.Sample(sceneSampler, uv); +})"; + + void CreatePipeline() + { + ComPtr vertex, pixel, errors; + Check(D3DCompile(ShaderSource, sizeof(ShaderSource) - 1, "VisualPinballNativeWindow", nullptr, nullptr, + "VertexMain", "vs_5_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &vertex, &errors), "Compile native vertex shader"); + Check(D3DCompile(ShaderSource, sizeof(ShaderSource) - 1, "VisualPinballNativeWindow", nullptr, nullptr, + "PixelMain", "ps_5_0", D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, &pixel, &errors), "Compile native pixel shader"); + Check(Device->CreateVertexShader(vertex->GetBufferPointer(), vertex->GetBufferSize(), nullptr, &VertexShader), "CreateVertexShader"); + Check(Device->CreatePixelShader(pixel->GetBufferPointer(), pixel->GetBufferSize(), nullptr, &PixelShader), "CreatePixelShader"); + D3D11_SAMPLER_DESC sampler{}; + sampler.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sampler.AddressU = sampler.AddressV = sampler.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + sampler.MaxLOD = D3D11_FLOAT32_MAX; + Check(Device->CreateSamplerState(&sampler, &Sampler), "CreateSamplerState"); + D3D11_RASTERIZER_DESC rasterizer{}; + rasterizer.FillMode = D3D11_FILL_SOLID; rasterizer.CullMode = D3D11_CULL_NONE; rasterizer.DepthClipEnable = TRUE; + Check(Device->CreateRasterizerState(&rasterizer, &Rasterizer), "CreateRasterizerState"); + D3D11_BLEND_DESC blend{}; + blend.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + blend.RenderTarget[0].SrcBlend = blend.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + blend.RenderTarget[0].DestBlend = blend.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO; + blend.RenderTarget[0].BlendOp = blend.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + Check(Device->CreateBlendState(&blend, &Blend), "CreateBlendState"); + D3D11_DEPTH_STENCIL_DESC depth{}; + depth.DepthFunc = D3D11_COMPARISON_ALWAYS; + depth.StencilReadMask = depth.StencilWriteMask = 255; + depth.FrontFace.StencilFailOp = depth.FrontFace.StencilDepthFailOp = depth.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP; + depth.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; + depth.BackFace = depth.FrontFace; + Check(Device->CreateDepthStencilState(&depth, &Depth), "CreateDepthStencilState"); + D3D11_BUFFER_DESC buffer{}; + buffer.ByteWidth = 16; buffer.Usage = D3D11_USAGE_DEFAULT; buffer.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + Check(Device->CreateBuffer(&buffer, nullptr, &Parameters), "CreateBuffer"); + } + + // D3D11.1 swaps the entire context state, including UAVs and shader resources which + // resource hazards could implicitly unbind. This avoids partial-state restoration bugs. + class SavedContext + { + ID3D11DeviceContext1* _context; + ComPtr _previous; + public: + explicit SavedContext(ID3D11DeviceContext1* context) : _context(context) + { + _context->SwapDeviceContextState(PluginContextState.Get(), &_previous); + } + ~SavedContext() { _context->SwapDeviceContextState(_previous.Get(), nullptr); } + }; + + DXGI_FORMAT SamplingFormat(DXGI_FORMAT format) + { + // Present the stored SDR RGB values without an implicit sRGB decoding step. + switch (format) { + case DXGI_FORMAT_R8G8B8A8_TYPELESS: case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; + case DXGI_FORMAT_B8G8R8A8_TYPELESS: case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; + case DXGI_FORMAT_R16G16B16A16_TYPELESS: return DXGI_FORMAT_R16G16B16A16_FLOAT; + case DXGI_FORMAT_R32G32B32A32_TYPELESS: return DXGI_FORMAT_R32G32B32A32_FLOAT; + default: return format; + } + } + + void EnsureSourceView(Surface& surface) + { + if (surface.sourceView) return; + D3D11_TEXTURE2D_DESC description{}; + surface.source->GetDesc(&description); + if (description.ArraySize != 1) throw std::invalid_argument("Native output requires a single 2D texture, not an array or XR surface."); + ID3D11Texture2D* texture = surface.source.Get(); + const bool typedSrgb = description.Format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB || description.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB; + if (description.SampleDesc.Count > 1 || typedSrgb) { + auto resolved = description; + resolved.SampleDesc.Count = 1; resolved.SampleDesc.Quality = 0; + resolved.MipLevels = 1; + resolved.Usage = D3D11_USAGE_DEFAULT; resolved.CPUAccessFlags = 0; + resolved.BindFlags = D3D11_BIND_SHADER_RESOURCE; resolved.MiscFlags = 0; + resolved.Format = SamplingFormat(description.Format); + Check(Device->CreateTexture2D(&resolved, nullptr, &surface.resolved), "Create typed presentation source texture"); + surface.copyStoredValues = description.SampleDesc.Count == 1; + texture = surface.resolved.Get(); + } + D3D11_SHADER_RESOURCE_VIEW_DESC view{}; + view.Format = SamplingFormat(description.Format); + view.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + view.Texture2D.MipLevels = 1; + Check(Device->CreateShaderResourceView(texture, &view, &surface.sourceView), "Create source texture view"); + } + + bool EnsurePresentation(Surface& surface, UINT width, UINT height) + { + if (!surface.swapChain) { + ComPtr dxgiDevice; Check(Device.As(&dxgiDevice), "Query DXGI device"); + ComPtr factory; Check(CreateDXGIFactory1(IID_PPV_ARGS(&factory)), "Create DXGI factory"); + DXGI_SWAP_CHAIN_DESC1 description{}; + description.Width = width; description.Height = height; + description.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + description.SampleDesc.Count = 1; description.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + description.BufferCount = 2; description.Scaling = DXGI_SCALING_STRETCH; + description.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; + description.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED; + Check(factory->CreateSwapChainForComposition(Renderer.load() == kUnityGfxRendererD3D12 ? CopyBridge.Queue() : Device.Get(), &description, nullptr, &surface.swapChain), "Create composition swapchain"); + Check(DCompositionCreateDevice(dxgiDevice.Get(), IID_PPV_ARGS(&surface.composition)), "Create DirectComposition device"); + Check(surface.composition->CreateTargetForHwnd(surface.window.load(), TRUE, &surface.compositionTarget), "Create composition window target"); + Check(surface.composition->CreateVisual(&surface.visual), "Create composition visual"); + Check(surface.visual->SetContent(surface.swapChain.Get()), "Attach composition swapchain"); + Check(surface.compositionTarget->SetRoot(surface.visual.Get()), "Attach composition root"); + Check(surface.composition->Commit(), "Commit window composition"); + } + else if (surface.width != width || surface.height != height) { + if (Renderer.load() == kUnityGfxRendererD3D12 && !CopyBridge.Completed(surface.lastFence12)) return false; + surface.targetView.Reset(); + for (auto& view : surface.targetViews12) view.Reset(); + for (auto& resource : surface.targetResources12) resource.Reset(); + Check(surface.swapChain->ResizeBuffers(2, width, height, DXGI_FORMAT_B8G8R8A8_UNORM, 0), "Resize composition swapchain"); + } + if (Renderer.load() == kUnityGfxRendererD3D12) { + for (UINT index = 0; index < 2; index++) if (!surface.targetViews12[index]) { + ComPtr buffer; + Check(surface.swapChain->GetBuffer(index, IID_PPV_ARGS(&buffer)), "Get D3D12 composition backbuffer"); + surface.targetResources12[index] = CopyBridge.WrapTarget(buffer.Get()); + Check(Device->CreateRenderTargetView(surface.targetResources12[index].Get(), nullptr, + &surface.targetViews12[index]), "Create wrapped composition render target"); + } + ComPtr swap3; Check(surface.swapChain.As(&swap3), "Query composition buffer index"); + UINT index = swap3->GetCurrentBackBufferIndex(); + surface.targetView = surface.targetViews12[index]; + surface.acquiredTarget12 = surface.targetResources12[index].Get(); + CopyBridge.AcquireTarget(surface.acquiredTarget12); + } + else if (!surface.targetView) { + ComPtr buffer; + Check(surface.swapChain->GetBuffer(0, IID_PPV_ARGS(&buffer)), "Get composition backbuffer"); + Check(Device->CreateRenderTargetView(buffer.Get(), nullptr, &surface.targetView), "Create composition render target"); + } + surface.width = width; surface.height = height; + return true; + } + + void RenderSurface(const std::shared_ptr& surface, bool present = true) + { + std::lock_guard lock(surface->mutex); + HWND window = surface->window.load(); + if (!surface->alive.load() || !window || (!surface->source && !surface->source12) || !IsWindowVisible(window)) return; + RECT client{}; + if (!GetClientRect(window, &client) || client.right < 1 || client.bottom < 1) return; + bool acquiredSource = false; + try { + if (surface->source12) { + auto wrapped = CopyBridge.Acquire(*surface->source12); + if (!wrapped) return; + acquiredSource = true; + ComPtr texture; Check(wrapped->QueryInterface(IID_PPV_ARGS(&texture)), "Query wrapped source texture"); + if (surface->source.Get() != texture.Get()) { + surface->source = texture; surface->sourceView.Reset(); surface->resolved.Reset(); + } + } + EnsureSourceView(*surface); + if (!EnsurePresentation(*surface, static_cast(client.right), static_cast(client.bottom))) { + surface->skipped++; + if (acquiredSource) CopyBridge.Release(*surface->source12); + return; + } + if (surface->resolved) { + D3D11_TEXTURE2D_DESC description{}; surface->resolved->GetDesc(&description); + if (surface->copyStoredValues) Context->CopySubresourceRegion(surface->resolved.Get(), 0, 0, 0, 0, surface->source.Get(), 0, nullptr); + else Context->ResolveSubresource(surface->resolved.Get(), 0, surface->source.Get(), 0, description.Format); + } + ID3D11RenderTargetView* target = surface->targetView.Get(); + Context->OMSetRenderTargets(1, &target, nullptr); + Context->OMSetBlendState(Blend.Get(), nullptr, 0xffffffff); + Context->OMSetDepthStencilState(Depth.Get(), 0); + Context->RSSetState(Rasterizer.Get()); + D3D11_VIEWPORT viewport{0, 0, static_cast(surface->width), static_cast(surface->height), 0, 1}; + Context->RSSetViewports(1, &viewport); + Context->IASetInputLayout(nullptr); Context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + Context->VSSetShader(VertexShader.Get(), nullptr, 0); Context->PSSetShader(PixelShader.Get(), nullptr, 0); + Context->GSSetShader(nullptr, nullptr, 0); Context->HSSetShader(nullptr, nullptr, 0); Context->DSSetShader(nullptr, nullptr, 0); + Context->PSSetShaderResources(0, 1, surface->sourceView.GetAddressOf()); + Context->PSSetSamplers(0, 1, Sampler.GetAddressOf()); + const float parameters[4] = {surface->flipY ? 1.f : 0.f, 0, 0, 0}; + Context->UpdateSubresource(Parameters.Get(), 0, nullptr, parameters, 0, 0); + Context->PSSetConstantBuffers(0, 1, Parameters.GetAddressOf()); + Context->Draw(3, 0); + ID3D11RenderTargetView* noTarget = nullptr; + Context->OMSetRenderTargets(1, &noTarget, nullptr); + ID3D11ShaderResourceView* noSource = nullptr; + Context->PSSetShaderResources(0, 1, &noSource); + if (surface->acquiredTarget12) { + CopyBridge.ReleaseTarget(surface->acquiredTarget12); surface->acquiredTarget12 = nullptr; + } + if (acquiredSource) { CopyBridge.Release(*surface->source12); acquiredSource = false; } + if (Renderer.load() == kUnityGfxRendererD3D12) { + Context->Flush(); // Same queue, after Unity's configured flush/submission event. + surface->lastFence12 = CopyBridge.FrameFenceValue(); + } + // A fixture can inspect the rendered buffer before a flip discards its contents. + if (!present) return; + // Never stall Unity's render thread waiting for a desktop display's refresh rate. + surface->lastResult = surface->swapChain->Present(0, DXGI_PRESENT_DO_NOT_WAIT); + if (surface->lastResult == DXGI_ERROR_WAS_STILL_DRAWING || surface->lastResult == DXGI_STATUS_OCCLUDED) surface->skipped++; + else { Check(surface->lastResult, "Present composition swapchain"); surface->presented++; } + } + catch (const std::exception& error) { + if (surface->acquiredTarget12) { + CopyBridge.ReleaseTarget(surface->acquiredTarget12); surface->acquiredTarget12 = nullptr; + } + if (acquiredSource) CopyBridge.Release(*surface->source12); + surface->skipped++; + if (SUCCEEDED(surface->lastResult)) surface->lastResult = E_FAIL; + surface->ReleasePresentation(); + RecordError(error.what()); + } + } + + void ReleaseDevice() + { + std::lock_guard registry(RegistryMutex); + for (auto& entry : Surfaces) { + std::lock_guard surface(entry.second->mutex); + entry.second->ReleasePresentation(); + entry.second->sourceView.Reset(); entry.second->resolved.Reset(); entry.second->source.Reset(); entry.second->source12.reset(); + } + { std::lock_guard retired(RetiredSurfaceMutex); RetiredSurfaces.clear(); } + CopyBridge.Shutdown(); + Parameters.Reset(); Depth.Reset(); Blend.Reset(); Rasterizer.Reset(); Sampler.Reset(); + PixelShader.Reset(); VertexShader.Reset(); PluginContextState.Reset(); Context1.Reset(); Context.Reset(); Device.Reset(); + } + + void InitializeDevice() + { + if (Device || !UnityInterfaces) return; + if (Renderer.load() == kUnityGfxRendererD3D12) { CopyBridge.InitializeDevice(); return; } + if (Renderer.load() != kUnityGfxRendererD3D11) return; + auto graphics = UnityInterfaces->Get(); + if (!graphics || !graphics->GetDevice()) throw std::runtime_error("Unity's Direct3D11 device is not ready."); + Device = graphics->GetDevice(); + Device->GetImmediateContext(&Context); + Check(Context.As(&Context1), "Query D3D11.1 immediate context"); + ComPtr device1; Check(Device.As(&device1), "Query D3D11.1 device"); + D3D_FEATURE_LEVEL level = Device->GetFeatureLevel(); + if (level < D3D_FEATURE_LEVEL_11_0) throw std::runtime_error("Native output requires Direct3D feature level11_0 or newer."); + UINT flags = (Device->GetCreationFlags() & D3D11_CREATE_DEVICE_SINGLETHREADED) != 0 + ? D3D11_1_CREATE_DEVICE_CONTEXT_STATE_SINGLETHREADED : 0; + Check(device1->CreateDeviceContextState(flags, &level, 1, D3D11_SDK_VERSION, __uuidof(ID3D11Device), nullptr, &PluginContextState), "Create isolated presentation context state"); + CreatePipeline(); + } + + void UNITY_INTERFACE_API GraphicsEvent(UnityGfxDeviceEventType event) + { + std::lock_guard lock(DeviceMutex); + try { + if (event == kUnityGfxDeviceEventShutdown || event == kUnityGfxDeviceEventBeforeReset) ReleaseDevice(); + else if (event == kUnityGfxDeviceEventInitialize || event == kUnityGfxDeviceEventAfterReset) { + Renderer.store(UnityGraphics ? UnityGraphics->GetRenderer() : -1); + InitializeDevice(); + } + } + catch (const std::exception& error) { ReleaseDevice(); RecordError(error.what()); } + } + + void UNITY_INTERFACE_API RenderEvent(int id) + { + std::lock_guard lock(DeviceMutex); + try { + InitializeDevice(); + thread_local std::vector> surfaces; + surfaces.clear(); + { std::lock_guard registry(RegistryMutex); for (auto& entry : Surfaces) surfaces.push_back(entry.second); } + if (Renderer.load() == kUnityGfxRendererD3D12) { + CopyBridge.Collect(); + { std::lock_guard retired(RetiredSurfaceMutex); + for (auto item = RetiredSurfaces.begin(); item != RetiredSurfaces.end();) { + if (CopyBridge.Completed((*item)->lastFence12)) { (*item)->ReleasePresentation(); item = RetiredSurfaces.erase(item); } + else ++item; + } + } + if (id == DrawEventId) { + for (auto& surface : surfaces) { + std::lock_guard state(surface->mutex); + if (surface->alive.load() && surface->source12 && IsWindowVisible(surface->window.load())) + if (!CopyBridge.RecordCopy(*surface->source12)) surface->skipped++; + } + } + else if (id == PresentEventId) { + if (!Device) { CopyBridge.InitializePresentation(&Device, &Context); CreatePipeline(); } + for (auto& surface : surfaces) RenderSurface(surface); + Context->Flush(); + } + } + else if (Device && id != PresentEventId) { + SavedContext saved(Context1.Get()); + if (id == DrawEventId || id == 0) for (auto& surface : surfaces) RenderSurface(surface); + else if (auto surface = Find(id)) RenderSurface(surface); + } + else if (!Device) RecordError("Native output requires a supported Windows Direct3D11 or Direct3D12 backend."); + surfaces.clear(); + } + catch (const std::exception& error) { RecordError(error.what()); } + catch (...) { RecordError("Unexpected native rendering exception."); } + } + +} + +using namespace vpnw; + +VPNW_API int VPNW_CALL VPNW_CreateWindow(int x, int y, int width, int height) +{ + return Guard([=] { + ValidateRectangle(x, y, width, height); + if (Renderer.load() != kUnityGfxRendererD3D11 && (Renderer.load() != kUnityGfxRendererD3D12 || !CopyBridge.Supported())) + throw std::runtime_error("Native windows require Unity Direct3D11 or Direct3D12 with the v7 native graphics interface."); + auto surface = std::make_shared(); + int id; + { std::lock_guard lock(RegistryMutex); + if (NextId > INT_MAX) throw std::runtime_error("Native window ID range exhausted."); + id = static_cast(NextId++); + } + GetWindowThread()->Invoke([=] { + HWND window = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_NOREDIRECTIONBITMAP, + WindowClass, WindowTitle, WS_POPUP, x, y, width, height, nullptr, nullptr, + reinterpret_cast(&__ImageBase), nullptr); + CheckWindow(window != nullptr, "Create borderless output window"); + surface->window.store(window); + }); + { std::lock_guard lock(RegistryMutex); Surfaces.emplace(id, surface); } + return id; + }); +} + +VPNW_API int VPNW_CALL VPNW_ConfigureWindow(int id, int x, int y, int width, int height, int visible, int topmost) +{ + return Guard([=] { + ValidateRectangle(x, y, width, height); + auto surface = Require(id); + GetWindowThread()->Invoke([=] { + if (!surface->alive.load()) throw std::runtime_error("Window was destroyed before configuration."); + HWND window = surface->window.load(); + bool changedRectangle = !surface->configured || surface->requestedX != x || surface->requestedY != y || + surface->requestedWidth != width || surface->requestedHeight != height; + bool changedVisibility = !surface->configured || surface->requestedVisible != (visible != 0); + bool changedTopmost = !surface->configured || surface->requestedTopmost != (topmost != 0); + if (changedVisibility) ShowWindow(window, visible ? SW_SHOWNOACTIVATE : SW_HIDE); + // Apply z-order after showing: Windows need not mark a hidden window topmost. + if (changedRectangle || changedTopmost || (changedVisibility && visible)) { + UINT flags = SWP_NOACTIVATE; + if (!changedRectangle) flags |= SWP_NOMOVE | SWP_NOSIZE; + CheckWindow(SetWindowPos(window, topmost ? HWND_TOPMOST : HWND_NOTOPMOST, x, y, width, height, flags) != FALSE, "Set physical desktop rectangle"); + } + surface->configured = true; surface->requestedX = x; surface->requestedY = y; + surface->requestedWidth = width; surface->requestedHeight = height; + surface->requestedVisible = visible != 0; surface->requestedTopmost = topmost != 0; + }); + return 1; + }); +} + +VPNW_API int VPNW_CALL VPNW_SetPlacementMode(int id, int enabled) +{ + return Guard([=] { + auto surface = Require(id); + GetWindowThread()->Invoke([=] { + HWND window = surface->window.load(); + if (enabled) CheckWindow(SetPropW(window, PlacementProperty, reinterpret_cast(1)) != FALSE, "Enable placement interaction"); + else RemovePropW(window, PlacementProperty); + const auto style = GetWindowLongPtrW(window, GWL_STYLE); + SetWindowLongPtrW(window, GWL_STYLE, enabled ? style | WS_THICKFRAME : style & ~WS_THICKFRAME); + CheckWindow(SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER) != FALSE, "Update placement frame"); + }); + return 1; + }); +} + +VPNW_API int VPNW_CALL VPNW_SetZOrder(int id, int order) +{ + return Guard([=] { + if (order < 0 || order > 3) throw std::invalid_argument("Z-order must be Back0, Front1, Topmost2 or NonTopmost3."); + auto surface = Require(id); + GetWindowThread()->Invoke([=] { + if (!surface->alive.load()) throw std::runtime_error("Window was destroyed before z-order update."); + const HWND positions[] = {HWND_BOTTOM, HWND_TOP, HWND_TOPMOST, HWND_NOTOPMOST}; + CheckWindow(SetWindowPos(surface->window.load(), positions[order], 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE) != FALSE, "Update window z-order"); + // Keep the configuration cache truthful, so a later Configure can restore the other topmost state. + if (order >= 2) surface->requestedTopmost = order == 2; + }); + return 1; + }); +} + +VPNW_API int VPNW_CALL VPNW_SetTexture(int id, void* texture, int flipY) +{ + return Guard([=] { + auto surface = Require(id); + std::lock_guard device(DeviceMutex); + std::lock_guard lock(surface->mutex); + if (!surface->alive.load()) throw std::runtime_error("Window was destroyed before texture assignment."); + if (Renderer.load() == kUnityGfxRendererD3D12) { + if (!surface->source12 || surface->source12->source.Get() != texture) { + auto replacement = CopyBridge.Source(texture); + CopyBridge.Retire(std::move(surface->source12)); surface->source12 = std::move(replacement); + surface->source.Reset(); surface->sourceView.Reset(); surface->resolved.Reset(); + } + } + else { + ComPtr source; + if (texture) Check(static_cast(texture)->QueryInterface(IID_PPV_ARGS(&source)), "Acquire Unity texture"); + if (source && Device) { + ComPtr owner; source->GetDevice(&owner); + if (owner.Get() != Device.Get()) throw std::invalid_argument("Texture belongs to a different Direct3D11 device."); + } + if (surface->source.Get() != source.Get()) { + surface->source = source; surface->sourceView.Reset(); surface->resolved.Reset(); + } + } + surface->flipY = flipY != 0; + return 1; + }); +} + +VPNW_API int VPNW_CALL VPNW_DestroyWindow(int id) +{ + return Guard([=] { + std::shared_ptr surface; + { std::lock_guard lock(RegistryMutex); + auto item = Surfaces.find(id); + if (item == Surfaces.end()) return 1; // Idempotent cleanup, including after Unity device shutdown. + surface = item->second; Surfaces.erase(item); + } + HWND window; + { std::lock_guard lock(surface->mutex); + surface->alive.store(false); window = surface->window.exchange(nullptr); + if (Renderer.load() == kUnityGfxRendererD3D12) { + if (surface->source12) { + surface->lastFence12 = (std::max)(surface->lastFence12, surface->source12->lastFence); + CopyBridge.Retire(std::move(surface->source12)); + } + // Detaching the source does not imply that submitted backbuffer work has completed. + if (surface->compositionTarget) surface->compositionTarget->SetRoot(nullptr); + if (surface->composition) surface->composition->Commit(); + std::lock_guard retired(RetiredSurfaceMutex); RetiredSurfaces.push_back(surface); + } + else surface->ReleasePresentation(); + surface->sourceView.Reset(); surface->resolved.Reset(); surface->source.Reset(); + } + GetWindowThread()->Invoke([=] { if (window) CheckWindow(DestroyWindow(window) != FALSE, "Destroy output window"); }); + return 1; + }); +} + +VPNW_API void VPNW_CALL VPNW_DestroyAll() +{ + std::vector ids; + { std::lock_guard lock(RegistryMutex); for (auto& entry : Surfaces) ids.push_back(entry.first); } + for (int id : ids) VPNW_DestroyWindow(id); +} + +// The HWNDs live on the plugin's own message thread; Unity's main thread has nothing to pump. +VPNW_API void VPNW_CALL VPNW_Pump() {} +VPNW_API std::uint32_t VPNW_CALL VPNW_GetCapabilities() +{ + return VPNW_CapabilityWindows | VPNW_CapabilityPlacement | VPNW_CapabilityTopmost | VPNW_CapabilityTransparency; +} +VPNW_API void* VPNW_CALL VPNW_GetRenderEventFunc() { return reinterpret_cast(RenderEvent); } +VPNW_API int VPNW_CALL VPNW_GetGraphicsRenderer() { return Renderer.load(); } +VPNW_API int VPNW_CALL VPNW_GetDrawEventId() { return DrawEventId; } +VPNW_API int VPNW_CALL VPNW_GetPresentEventId() { return PresentEventId; } + +VPNW_API int VPNW_CALL VPNW_GetPendingResourceCount() +{ + std::lock_guard device(DeviceMutex); + const int sources = CopyBridge.PendingResourceCount(); + std::lock_guard retired(RetiredSurfaceMutex); + return sources + static_cast(RetiredSurfaces.size()); +} + +VPNW_API int VPNW_CALL VPNW_GetDiagnostics(int id, VPNW_Diagnostics* diagnostics) +{ + return Guard([=] { + if (!diagnostics || diagnostics->structBytes != sizeof(VPNW_Diagnostics)) throw std::invalid_argument("Unexpected native diagnostics structure size."); + auto surface = Require(id); + std::lock_guard device(DeviceMutex); + std::lock_guard lock(surface->mutex); + HWND window = surface->window.load(); + VPNW_Diagnostics result{}; + result.structBytes = sizeof(result); result.graphicsRenderer = Renderer.load(); result.deviceReady = Device ? 1 : 0; + result.alive = surface->alive.load() ? 1 : 0; result.visible = IsWindowVisible(window) ? 1 : 0; + result.topmost = (GetWindowLongPtrW(window, GWL_EXSTYLE) & WS_EX_TOPMOST) != 0 ? 1 : 0; + // The HWND thread can be inside Windows' modal drag/resize loop. Read geometry + // directly with a temporary thread DPI context so diagnostics never wait for that loop. + const auto previousDpi = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + RECT rectangle{}; + const bool readRectangle = GetWindowRect(window, &rectangle) != FALSE; + if (previousDpi) SetThreadDpiAwarenessContext(previousDpi); + CheckWindow(readRectangle, "Read physical output window rectangle"); + result.left = rectangle.left; result.top = rectangle.top; + result.windowWidth = rectangle.right - rectangle.left; result.windowHeight = rectangle.bottom - rectangle.top; + result.dpi = GetDpiForWindow(window); + if (surface->source12) { + result.sourceWidth = surface->source12->width; result.sourceHeight = surface->source12->height; result.sourceFormat = surface->source12->format; + } + else if (surface->source) { + D3D11_TEXTURE2D_DESC description{}; surface->source->GetDesc(&description); + result.sourceWidth = description.Width; result.sourceHeight = description.Height; result.sourceFormat = description.Format; + } + result.presentedFrames = surface->presented; result.skippedFrames = surface->skipped; + result.lastResult = surface->lastResult; result.premultipliedAlpha = 1; + *diagnostics = result; + return 1; + }); +} + +VPNW_API int VPNW_CALL VPNW_GetLastError(char* destination, int capacity) +{ + std::lock_guard lock(ErrorMutex); + if (destination && capacity > 0) strncpy_s(destination, static_cast(capacity), LastError.c_str(), _TRUNCATE); + return static_cast(LastError.size() + 1); +} + +extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* interfaces) +{ + UnityInterfaces = interfaces; + UnityGraphics = interfaces ? interfaces->Get() : nullptr; + if (UnityGraphics) { + Renderer.store(UnityGraphics->GetRenderer()); + UnityGraphics->RegisterDeviceEventCallback(GraphicsEvent); + DrawEventId = UnityGraphics->ReserveEventIDRange(2); PresentEventId = DrawEventId + 1; + auto graphics12 = interfaces->Get(); + CopyBridge.SetUnity(graphics12); + if (graphics12 && Renderer.load() == kUnityGfxRendererD3D12) { + UnityD3D12PluginEventConfig draw{}; + draw.graphicsQueueAccess = kUnityD3D12GraphicsQueueAccess_DontCare; + draw.flags = kUnityD3D12EventConfigFlag_SyncWorkerThreads; + graphics12->ConfigureEvent(DrawEventId, &draw); + UnityD3D12PluginEventConfig present{}; + present.graphicsQueueAccess = kUnityD3D12GraphicsQueueAccess_Allow; + present.flags = kUnityD3D12EventConfigFlag_FlushCommandBuffers | kUnityD3D12EventConfigFlag_SyncWorkerThreads; + graphics12->ConfigureEvent(PresentEventId, &present); + } + } + // Device access is deferred to the graphics event/render callback, both on Unity's rendering thread. +} + +extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginUnload() +{ + if (UnityGraphics) UnityGraphics->UnregisterDeviceEventCallback(GraphicsEvent); + VPNW_DestroyAll(); + { std::lock_guard device(DeviceMutex); ReleaseDevice(); } + { std::lock_guard windows(WindowServiceMutex); WindowService.reset(); } + UnityGraphics = nullptr; UnityInterfaces = nullptr; Renderer.store(-1); +} diff --git a/VisualPinball.Unity/Plugins/linux-x64/libVisualPinballNativeWindows.so.meta b/VisualPinball.Unity/Plugins/linux-x64/libVisualPinballNativeWindows.so.meta new file mode 100644 index 000000000..60ff2f34a --- /dev/null +++ b/VisualPinball.Unity/Plugins/linux-x64/libVisualPinballNativeWindows.so.meta @@ -0,0 +1,33 @@ +fileFormatVersion: 2 +guid: 2553238478f6451cb296432662f93032 +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 1 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Android: + enabled: 0 + settings: + Is16KbAligned: false + Any: + enabled: 0 + settings: {} + Editor: + enabled: 1 + settings: + CPU: x86_64 + DefaultValueInitialized: true + OS: Linux + Linux64: + enabled: 1 + settings: + CPU: x86_64 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/Plugins/win-x64/VisualPinballNativeWindows.dll.meta b/VisualPinball.Unity/Plugins/win-x64/VisualPinballNativeWindows.dll.meta new file mode 100644 index 000000000..47c16d857 --- /dev/null +++ b/VisualPinball.Unity/Plugins/win-x64/VisualPinballNativeWindows.dll.meta @@ -0,0 +1,50 @@ +fileFormatVersion: 2 +guid: f47f7ca3675a47fbae9454acef61d63c +PluginImporter: + externalObjects: {} + serializedVersion: 3 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + Any: + enabled: 0 + settings: + Exclude Editor: 0 + Exclude Linux64: 1 + Exclude OSXUniversal: 1 + Exclude Win: 1 + Exclude Win64: 0 + Editor: + enabled: 1 + settings: + CPU: x86_64 + DefaultValueInitialized: true + OS: Windows + Linux64: + enabled: 0 + settings: + CPU: x86_64 + OSXUniversal: + enabled: 0 + settings: + CPU: None + Win: + enabled: 0 + settings: + CPU: None + Win64: + enabled: 1 + settings: + CPU: x86_64 + WindowsStoreApps: + enabled: 0 + settings: + CPU: x64 + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows.meta b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows.meta new file mode 100644 index 000000000..2ce75ddfc --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 220b67a7c6d84202bb866fa284052954 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs new file mode 100644 index 000000000..299b84723 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs @@ -0,0 +1,27 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; + +namespace VisualPinball.Unity +{ + /// + /// Window features reported by the native library, matching the native VPNW_Capability bits. + /// + [Flags] + public enum NativeWindowCapabilities : uint + { + None = 0, + /// Borderless GPU output windows can be created. + Windows = 1, + /// Placement mode (mouse dragging and edge resizing) is functional. + Placement = 2, + /// Topmost and non-topmost requests are honoured. + Topmost = 4, + /// Premultiplied alpha reaches the desktop compositor. + Transparency = 8, + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs.meta new file mode 100644 index 000000000..fba932e3d --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f39c01809b8a402182a0059d5584a900 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs new file mode 100644 index 000000000..910290a69 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs @@ -0,0 +1,40 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System.Runtime.InteropServices; + +namespace VisualPinball.Unity +{ + /// + /// Snapshot of a native window's state, matching the native VPNW_Diagnostics layout (80 bytes). + /// + /// Coordinates and sizes are physical desktop pixels. Boolean fields are 0 or 1. + [StructLayout(LayoutKind.Sequential)] + public struct NativeWindowDiagnostics + { + public uint StructBytes; + /// UnityGfxRenderer value seen by the native plugin. + public int GraphicsRenderer; + public int DeviceReady; + public int Alive; + public int Visible; + public int Topmost; + public int Left; + public int Top; + public int WindowWidth; + public int WindowHeight; + public uint Dpi; + public uint SourceWidth; + public uint SourceHeight; + /// Platform texture format: DXGI_FORMAT, MTLPixelFormat or VkFormat. + public uint SourceFormat; + public ulong PresentedFrames; + public ulong SkippedFrames; + /// Windows: last presentation HRESULT. macOS/Linux: 0, or negative after a presentation failure. + public int LastResult; + public int PremultipliedAlpha; + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs.meta new file mode 100644 index 000000000..7a35b659c --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6175021f04f4bf2add7104325ff17e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs new file mode 100644 index 000000000..27a7ae7ca --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs @@ -0,0 +1,106 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; +using System.Collections.Generic; +using NLog; +using UnityEngine; +using Logger = NLog.Logger; + +namespace VisualPinball.Unity +{ + /// + /// Submits one pair of native rendering events per frame after all rendering, for every registered + /// , and keeps doing so until deferred GPU releases have drained. + /// + /// + /// A single hidden instance is shared by all users (PinUP, B2S, ...). It is created on the first + /// and removes itself once no surface is registered and no GPU resource is pending. + /// + [DefaultExecutionOrder(10000)] + public sealed class NativeWindowHost : MonoBehaviour + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private static NativeWindowHost _instance; + private readonly HashSet _surfaces = new HashSet(); + + /// + /// Registers a surface for render-thread presentation. Dispose the returned lease before disposing the surface. + /// + public static IDisposable Register(NativeWindowSurface surface) + { + if (surface == null) { + throw new ArgumentNullException(nameof(surface)); + } + if (_instance == null) { + var owner = new GameObject("Native Window Presenter") { hideFlags = HideFlags.HideAndDontSave }; + if (Application.isPlaying) { + DontDestroyOnLoad(owner); + } + _instance = owner.AddComponent(); + } + _instance._surfaces.Add(surface); + return new PresentationLease(surface); + } + + private static void Unregister(NativeWindowSurface surface) + { + if (_instance != null) { + _instance._surfaces.Remove(surface); + } + } + + private void LateUpdate() + { + NativeWindowSurface.Pump(); + if (_surfaces.Count == 0 && NativeWindowSurface.PendingResourceCount == 0) { + // Detach first, so a Register() before the deferred destruction creates a fresh presenter. + if (_instance == this) { + _instance = null; + } + Destroy(gameObject); + return; + } + var callback = NativeWindowSurface.RenderEventFunction; + GL.IssuePluginEvent(callback, NativeWindowSurface.DrawEventId); + GL.IssuePluginEvent(callback, NativeWindowSurface.PresentEventId); + } + + private void OnDestroy() + { + foreach (var surface in _surfaces) { + try { + surface.Dispose(); + } catch (Exception e) { + Logger.Error(e, "Could not destroy native window."); + } + } + _surfaces.Clear(); + if (_instance == this) { + _instance = null; + } + } + + private sealed class PresentationLease : IDisposable + { + private NativeWindowSurface _surface; + + internal PresentationLease(NativeWindowSurface surface) + { + _surface = surface; + } + + public void Dispose() + { + if (_surface != null) { + Unregister(_surface); + } + _surface = null; + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs.meta new file mode 100644 index 000000000..cef512123 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aa47f2ca77a940c0acecec29922e8c3d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs new file mode 100644 index 000000000..c31b352b2 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs @@ -0,0 +1,305 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace VisualPinball.Unity +{ + /// + /// A borderless desktop window presenting a Unity texture on the GPU: Direct3D11/12 through DirectComposition on + /// Windows, Metal on macOS, and Vulkan on X11/XWayland on Linux. + /// + /// + /// Coordinates are signed physical desktop pixels; monitors left of or above the primary monitor have negative + /// origins. A new window is hidden until shows it. Register the surface with + /// , which issues and once + /// per frame for all surfaces. Dispose the surface before releasing its RenderTexture. The wrapper has explicit + /// ownership and no finalizer. + /// + public sealed class NativeWindowSurface : IDisposable + { + internal const string LibraryName = "VisualPinballNativeWindows"; + + // UnityGfxRenderer values of the supported graphics backends. + private const int RendererD3D11 = 2; + private const int RendererMetal = 16; + private const int RendererD3D12 = 18; + private const int RendererVulkan = 21; + + /// Native registry ID; zero after disposal. IDs are never reused while the plugin is loaded. + public int Id { get; private set; } + + private bool _configured, _visible, _topmost; + private int _x, _y, _width, _height; + + private static bool? _libraryAvailable; + + #region Static API + + /// + /// True when the native library is present and Unity runs a graphics backend the platform's native windows + /// support: Direct3D11/12 on Windows, Metal on macOS, Vulkan with an X11 display on Linux. + /// + /// Never throws; a missing native library reports false. + public static bool IsSupported + { + get { + if (IntPtr.Size != 8 || !LibraryAvailable || (Capabilities & NativeWindowCapabilities.Windows) == 0) { + return false; + } + var renderer = GraphicsRenderer; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + return renderer == RendererD3D11 || renderer == RendererD3D12; + } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { + return renderer == RendererMetal; + } + return RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && renderer == RendererVulkan; + } + } + + /// Unity's UnityGfxRenderer as seen by the native plugin, or -1 when the library is unavailable. + public static int GraphicsRenderer => LibraryAvailable ? Native.GetGraphicsRenderer() : -1; + + /// Window features of the current platform and session. + public static NativeWindowCapabilities Capabilities => LibraryAvailable + ? (NativeWindowCapabilities)Native.GetCapabilities() + : NativeWindowCapabilities.None; + + /// Unity rendering callback. Issue and then with it. + public static IntPtr RenderEventFunction => LibraryAvailable ? Native.GetRenderEventFunc() : IntPtr.Zero; + + /// Submit after the frame's GPU work, followed immediately by . Operates on all surfaces. + public static int DrawEventId => LibraryAvailable ? Native.GetDrawEventId() : 0; + + /// Presents all surfaces; submit right after , once per frame. + public static int PresentEventId => LibraryAvailable ? Native.GetPresentEventId() : 0; + + /// Retired GPU source and presentation objects awaiting later render events. Keep issuing event pairs until zero. + public static int PendingResourceCount => LibraryAvailable ? Native.GetPendingResourceCount() : 0; + + /// Processes platform window events (Linux placement dragging). Call once per frame from the main thread. + public static void Pump() + { + if (LibraryAvailable) { + Native.Pump(); + } + } + + /// Destroys all native windows, for shutdown or domain-reload cleanup by the host. + public static void DestroyAll() + { + if (LibraryAvailable) { + Native.DestroyAll(); + } + } + + /// The last native error message (UTF-8), for diagnostics. + public static string LastError + { + get { + if (!LibraryAvailable) { + return $"The native library {LibraryName} is not available."; + } + var bytes = new byte[System.Math.Max(1, Native.GetLastError(null, 0))]; + Native.GetLastError(bytes, bytes.Length); + var length = Array.IndexOf(bytes, (byte)0); + return Encoding.UTF8.GetString(bytes, 0, length < 0 ? bytes.Length : length); + } + } + + private static bool LibraryAvailable + { + get { + if (_libraryAvailable.HasValue) { + return _libraryAvailable.Value; + } + try { + Native.GetCapabilities(); + _libraryAvailable = true; + + } catch (DllNotFoundException) { + _libraryAvailable = false; + + } catch (EntryPointNotFoundException) { + _libraryAvailable = false; + + } catch (BadImageFormatException) { + _libraryAvailable = false; + } + return _libraryAvailable.Value; + } + } + + #endregion + + /// Creates an initially hidden window, using signed physical desktop pixel coordinates. + /// The native library is missing or the player is not 64-bit. + /// The native window could not be created; see . + public NativeWindowSurface(int x, int y, int width, int height) + { + if (IntPtr.Size != 8) { + throw new PlatformNotSupportedException("Native windows require a 64-bit player."); + } + if (!LibraryAvailable) { + throw new PlatformNotSupportedException($"The native library {LibraryName} is not available on this platform."); + } + Id = Native.CreateWindow(x, y, width, height); + if (Id == 0) { + throw Failure("Create native window"); + } + } + + /// Sets the physical desktop rectangle, visibility and topmost state. Identical calls are cached and skipped. + public void Configure(int x, int y, int width, int height, bool visible, bool topmost) + { + RequireAlive(); + if (_configured && x == _x && y == _y && width == _width && height == _height && visible == _visible && topmost == _topmost) { + return; + } + if (Native.ConfigureWindow(Id, x, y, width, height, visible ? 1 : 0, topmost ? 1 : 0) == 0) { + throw Failure("Configure native window"); + } + _configured = true; + _x = x; + _y = y; + _width = width; + _height = height; + _visible = visible; + _topmost = topmost; + } + + /// + /// Assigns the source texture, typically RenderTexture.GetNativeTexturePtr(), containing premultiplied RGBA + /// in the desired SDR encoding. + /// + /// IntPtr.Zero detaches the source. Resubmit the pointer whenever Unity recreates or resizes the texture. + public void SetTexture(IntPtr texture, bool flipY = false) + { + RequireAlive(); + if (Native.SetTexture(Id, texture, flipY ? 1 : 0) == 0) { + throw Failure("Assign native window texture"); + } + } + + /// Allows mouse dragging and edge resizing while the user places the window. Playback windows stay fixed. + public void SetPlacementMode(bool enabled) + { + RequireAlive(); + if (Native.SetPlacementMode(Id, enabled ? 1 : 0) == 0) { + throw Failure("Set native placement mode"); + } + } + + /// Moves the window behind other windows without moving, resizing or activating it. + public void SendToBack() => SetZOrder(0); + + /// Moves the window in front of other windows without moving, resizing or activating it. + public void BringToFront() => SetZOrder(1); + + /// Sets or clears the topmost state without moving, resizing or activating the window. + public void SetTopmost(bool topmost) => SetZOrder(topmost ? 2 : 3); + + /// Actual window geometry, DPI, source and presentation counters. Query for troubleshooting, not every frame. + public NativeWindowDiagnostics Diagnostics + { + get { + RequireAlive(); + var result = new NativeWindowDiagnostics { StructBytes = (uint)Marshal.SizeOf() }; + if (Native.GetDiagnostics(Id, ref result) == 0) { + throw Failure("Read native window diagnostics"); + } + return result; + } + } + + /// Destroys the window. GPU resources are released by later render events; see . + public void Dispose() + { + if (Id == 0) { + return; + } + var id = Id; + Id = 0; + if (Native.DestroyWindow(id) == 0) { + throw Failure("Destroy native window"); + } + } + + private void SetZOrder(int order) + { + RequireAlive(); + if (Native.SetZOrder(Id, order) == 0) { + throw Failure("Set native window z-order"); + } + if (order >= 2) { + _topmost = order == 2; + } + } + + private void RequireAlive() + { + if (Id == 0) { + throw new ObjectDisposedException(nameof(NativeWindowSurface)); + } + } + + private static InvalidOperationException Failure(string operation) => new InvalidOperationException(operation + ": " + LastError); + + private static class Native + { + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_CreateWindow")] + internal static extern int CreateWindow(int x, int y, int width, int height); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_ConfigureWindow")] + internal static extern int ConfigureWindow(int id, int x, int y, int width, int height, int visible, int topmost); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_SetZOrder")] + internal static extern int SetZOrder(int id, int order); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_SetPlacementMode")] + internal static extern int SetPlacementMode(int id, int enabled); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_SetTexture")] + internal static extern int SetTexture(int id, IntPtr texture, int flipY); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_DestroyWindow")] + internal static extern int DestroyWindow(int id); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_DestroyAll")] + internal static extern void DestroyAll(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_Pump")] + internal static extern void Pump(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_GetRenderEventFunc")] + internal static extern IntPtr GetRenderEventFunc(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_GetGraphicsRenderer")] + internal static extern int GetGraphicsRenderer(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_GetDrawEventId")] + internal static extern int GetDrawEventId(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_GetPresentEventId")] + internal static extern int GetPresentEventId(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_GetPendingResourceCount")] + internal static extern int GetPendingResourceCount(); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_GetDiagnostics")] + internal static extern int GetDiagnostics(int id, ref NativeWindowDiagnostics diagnostics); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_GetLastError")] + internal static extern int GetLastError([Out] byte[] destination, int capacity); + + [DllImport(LibraryName, CallingConvention = CallingConvention.Cdecl, EntryPoint = "VPNW_GetCapabilities")] + internal static extern uint GetCapabilities(); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs.meta new file mode 100644 index 000000000..b2d3692aa --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a84c7df2a3274902aa080cad39fd27a3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 0e6507d9d68cd1d8d1d6dfaa415d798e9a20e323 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 09:47:31 +0200 Subject: [PATCH 11/26] native windows: move sources to VisualPinball.Native~ Groups native sources under a VisualPinball-prefixed folder that Unity still ignores, since the package root is the repository root. --- .../NativeWindows/Desktop/CMakeLists.txt | 2 +- .../NativeWindows/Desktop/DesktopLayout.h | 0 .../NativeWindows/Desktop/DesktopTests.cpp | 0 .../NativeWindows/Desktop/Linux/VulkanWindows.cpp | 0 .../NativeWindows/Desktop/Linux/X11Windows.cpp | 0 .../NativeWindows/Desktop/Linux/X11Windows.h | 0 .../NativeWindows/Desktop/Mac/MacCoordinates.h | 0 .../NativeWindows/Desktop/Mac/MetalWindows.mm | 0 .../NativeWindows/Desktop/WindowBackend.h | 0 .../NativeWindows/Desktop/WindowRegistry.cpp | 0 {Native~ => VisualPinball.Native~}/NativeWindows/README.md | 6 +++--- .../NativeWindows/VisualPinballNativeWindows.h | 0 .../NativeWindows/Windows/Build.ps1 | 0 .../NativeWindows/Windows/D3D12CopyBridge.cpp | 0 .../NativeWindows/Windows/D3D12CopyBridge.h | 0 .../NativeWindows/Windows/NativeWindowTests.cpp | 0 .../NativeWindows/Windows/VisualPinballNativeWindows.cpp | 0 17 files changed, 4 insertions(+), 4 deletions(-) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/CMakeLists.txt (95%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/DesktopLayout.h (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/DesktopTests.cpp (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/Linux/VulkanWindows.cpp (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/Linux/X11Windows.cpp (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/Linux/X11Windows.h (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/Mac/MacCoordinates.h (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/Mac/MetalWindows.mm (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/WindowBackend.h (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Desktop/WindowRegistry.cpp (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/README.md (96%) rename {Native~ => VisualPinball.Native~}/NativeWindows/VisualPinballNativeWindows.h (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Windows/Build.ps1 (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Windows/D3D12CopyBridge.cpp (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Windows/D3D12CopyBridge.h (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Windows/NativeWindowTests.cpp (100%) rename {Native~ => VisualPinball.Native~}/NativeWindows/Windows/VisualPinballNativeWindows.cpp (100%) diff --git a/Native~/NativeWindows/Desktop/CMakeLists.txt b/VisualPinball.Native~/NativeWindows/Desktop/CMakeLists.txt similarity index 95% rename from Native~/NativeWindows/Desktop/CMakeLists.txt rename to VisualPinball.Native~/NativeWindows/Desktop/CMakeLists.txt index 62382b876..a82d79deb 100644 --- a/Native~/NativeWindows/Desktop/CMakeLists.txt +++ b/VisualPinball.Native~/NativeWindows/Desktop/CMakeLists.txt @@ -35,7 +35,7 @@ elseif(UNIX) set(PLATFORM_LIBRARIES Vulkan::Vulkan X11::X11 X11::Xrender dl pthread m) set(VPNW_PLUGIN_DIRECTORY "${VPNW_PACKAGE_ROOT}/VisualPinball.Unity/Plugins/linux-x64" CACHE PATH "Deployment directory") else() - message(FATAL_ERROR "This CMake project builds the macOS and Linux library; Windows uses Native~/NativeWindows/Windows/Build.ps1.") + message(FATAL_ERROR "This CMake project builds the macOS and Linux library; Windows uses VisualPinball.Native~/NativeWindows/Windows/Build.ps1.") endif() add_library(VisualPinballNativeWindows SHARED ${SOURCES}) target_include_directories(VisualPinballNativeWindows PRIVATE "${UNITY_PLUGIN_API}" "${VPNW_API_DIRECTORY}" "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/Native~/NativeWindows/Desktop/DesktopLayout.h b/VisualPinball.Native~/NativeWindows/Desktop/DesktopLayout.h similarity index 100% rename from Native~/NativeWindows/Desktop/DesktopLayout.h rename to VisualPinball.Native~/NativeWindows/Desktop/DesktopLayout.h diff --git a/Native~/NativeWindows/Desktop/DesktopTests.cpp b/VisualPinball.Native~/NativeWindows/Desktop/DesktopTests.cpp similarity index 100% rename from Native~/NativeWindows/Desktop/DesktopTests.cpp rename to VisualPinball.Native~/NativeWindows/Desktop/DesktopTests.cpp diff --git a/Native~/NativeWindows/Desktop/Linux/VulkanWindows.cpp b/VisualPinball.Native~/NativeWindows/Desktop/Linux/VulkanWindows.cpp similarity index 100% rename from Native~/NativeWindows/Desktop/Linux/VulkanWindows.cpp rename to VisualPinball.Native~/NativeWindows/Desktop/Linux/VulkanWindows.cpp diff --git a/Native~/NativeWindows/Desktop/Linux/X11Windows.cpp b/VisualPinball.Native~/NativeWindows/Desktop/Linux/X11Windows.cpp similarity index 100% rename from Native~/NativeWindows/Desktop/Linux/X11Windows.cpp rename to VisualPinball.Native~/NativeWindows/Desktop/Linux/X11Windows.cpp diff --git a/Native~/NativeWindows/Desktop/Linux/X11Windows.h b/VisualPinball.Native~/NativeWindows/Desktop/Linux/X11Windows.h similarity index 100% rename from Native~/NativeWindows/Desktop/Linux/X11Windows.h rename to VisualPinball.Native~/NativeWindows/Desktop/Linux/X11Windows.h diff --git a/Native~/NativeWindows/Desktop/Mac/MacCoordinates.h b/VisualPinball.Native~/NativeWindows/Desktop/Mac/MacCoordinates.h similarity index 100% rename from Native~/NativeWindows/Desktop/Mac/MacCoordinates.h rename to VisualPinball.Native~/NativeWindows/Desktop/Mac/MacCoordinates.h diff --git a/Native~/NativeWindows/Desktop/Mac/MetalWindows.mm b/VisualPinball.Native~/NativeWindows/Desktop/Mac/MetalWindows.mm similarity index 100% rename from Native~/NativeWindows/Desktop/Mac/MetalWindows.mm rename to VisualPinball.Native~/NativeWindows/Desktop/Mac/MetalWindows.mm diff --git a/Native~/NativeWindows/Desktop/WindowBackend.h b/VisualPinball.Native~/NativeWindows/Desktop/WindowBackend.h similarity index 100% rename from Native~/NativeWindows/Desktop/WindowBackend.h rename to VisualPinball.Native~/NativeWindows/Desktop/WindowBackend.h diff --git a/Native~/NativeWindows/Desktop/WindowRegistry.cpp b/VisualPinball.Native~/NativeWindows/Desktop/WindowRegistry.cpp similarity index 100% rename from Native~/NativeWindows/Desktop/WindowRegistry.cpp rename to VisualPinball.Native~/NativeWindows/Desktop/WindowRegistry.cpp diff --git a/Native~/NativeWindows/README.md b/VisualPinball.Native~/NativeWindows/README.md similarity index 96% rename from Native~/NativeWindows/README.md rename to VisualPinball.Native~/NativeWindows/README.md index 00e491a6e..2d651e637 100644 --- a/Native~/NativeWindows/README.md +++ b/VisualPinball.Native~/NativeWindows/README.md @@ -89,9 +89,9 @@ Run `Windows/Build.ps1` from PowerShell with the Visual Studio C++ x64 toolchain The `Desktop` folder is a CMake project (3.22+). Point `UNITY_PLUGIN_API` at the Unity Editor's `PluginAPI` directory. With `VPNW_DEPLOY` (default on), the library is copied to `VisualPinball.Unity/Plugins/osx` or `VisualPinball.Unity/Plugins/linux-x64`. ```sh -cmake -S Native~/NativeWindows/Desktop -B Native~/NativeWindows/Desktop/obj -DUNITY_PLUGIN_API="/Applications/Unity/Hub/Editor/6000.5.0f1/Unity.app/Contents/PluginAPI" -cmake --build Native~/NativeWindows/Desktop/obj --config Release -Native~/NativeWindows/Desktop/obj/NativeWindowTests +cmake -S VisualPinball.Native~/NativeWindows/Desktop -B VisualPinball.Native~/NativeWindows/Desktop/obj -DUNITY_PLUGIN_API="/Applications/Unity/Hub/Editor/6000.5.0f1/Unity.app/Contents/PluginAPI" +cmake --build VisualPinball.Native~/NativeWindows/Desktop/obj --config Release +VisualPinball.Native~/NativeWindows/Desktop/obj/NativeWindowTests ``` On macOS the default `CMAKE_OSX_ARCHITECTURES` is `arm64;x86_64`, producing a universal `libVisualPinballNativeWindows.dylib` (deployment target 14.0). Create its Unity `.meta` (Editor OSX and Standalone OSXUniversal enabled) when the binary is first added. diff --git a/Native~/NativeWindows/VisualPinballNativeWindows.h b/VisualPinball.Native~/NativeWindows/VisualPinballNativeWindows.h similarity index 100% rename from Native~/NativeWindows/VisualPinballNativeWindows.h rename to VisualPinball.Native~/NativeWindows/VisualPinballNativeWindows.h diff --git a/Native~/NativeWindows/Windows/Build.ps1 b/VisualPinball.Native~/NativeWindows/Windows/Build.ps1 similarity index 100% rename from Native~/NativeWindows/Windows/Build.ps1 rename to VisualPinball.Native~/NativeWindows/Windows/Build.ps1 diff --git a/Native~/NativeWindows/Windows/D3D12CopyBridge.cpp b/VisualPinball.Native~/NativeWindows/Windows/D3D12CopyBridge.cpp similarity index 100% rename from Native~/NativeWindows/Windows/D3D12CopyBridge.cpp rename to VisualPinball.Native~/NativeWindows/Windows/D3D12CopyBridge.cpp diff --git a/Native~/NativeWindows/Windows/D3D12CopyBridge.h b/VisualPinball.Native~/NativeWindows/Windows/D3D12CopyBridge.h similarity index 100% rename from Native~/NativeWindows/Windows/D3D12CopyBridge.h rename to VisualPinball.Native~/NativeWindows/Windows/D3D12CopyBridge.h diff --git a/Native~/NativeWindows/Windows/NativeWindowTests.cpp b/VisualPinball.Native~/NativeWindows/Windows/NativeWindowTests.cpp similarity index 100% rename from Native~/NativeWindows/Windows/NativeWindowTests.cpp rename to VisualPinball.Native~/NativeWindows/Windows/NativeWindowTests.cpp diff --git a/Native~/NativeWindows/Windows/VisualPinballNativeWindows.cpp b/VisualPinball.Native~/NativeWindows/Windows/VisualPinballNativeWindows.cpp similarity index 100% rename from Native~/NativeWindows/Windows/VisualPinballNativeWindows.cpp rename to VisualPinball.Native~/NativeWindows/Windows/VisualPinballNativeWindows.cpp From 635b5a2d49f7a444f38bbb687940094651614f87 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 10:41:58 +0200 Subject: [PATCH 12/26] plugins: address review findings on cleanup, formats, drafts and segments - An unresponsive or emergency-stopped plugin keeps its scopes (native handles, device leases) until its running hooks and Quiescence have finished, instead of losing them while its worker still runs. Such instances are quarantined and block a new instance of the plugin until they have drained. The host tracks every running lifecycle hook per instance for this. - The identification display encoding is chosen from what the consumer accepts. A later consumer must accept the encoding already delivered. - A successful settings apply keeps edits made while it was running, instead of reverting them to the saved profile. - Segment sources announce their layout and hardware with their order and size. - The packaged game identity cache compares paths ordinally, so case-distinct files on Linux no longer share an entry. - The macOS native window keeps its drawable at the window's pixel size after a resize or display change. --- .../NativeWindows/Desktop/Mac/MetalWindows.mm | 25 +++++- .../OutputResourceTests.cs | 4 +- VisualPinball.Plugins.Test/PluginHostTests.cs | 60 ++++++++++++- .../SettingsAndNegotiationTests.cs | 90 +++++++++++++++++++ .../SettingsAndNegotiationTests.cs.meta | 2 + .../Configuration/PluginSettingsSession.cs | 22 ++++- .../Resources/DisplayFormatNegotiator.cs | 15 +++- .../Resources/ResourceContracts.cs | 4 + .../Resources/ResourceSignal.cs | 8 +- .../Runtime/PluginHost.Settings.cs | 4 +- VisualPinball.Plugins/Runtime/PluginHost.cs | 68 ++++++++++---- .../Plugins/GamelogicPluginResources.cs | 2 +- .../Plugins/PackagedGameInformation.cs | 3 +- 13 files changed, 270 insertions(+), 37 deletions(-) create mode 100644 VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs create mode 100644 VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs.meta diff --git a/VisualPinball.Native~/NativeWindows/Desktop/Mac/MetalWindows.mm b/VisualPinball.Native~/NativeWindows/Desktop/Mac/MetalWindows.mm index fb3316a70..b22261619 100644 --- a/VisualPinball.Native~/NativeWindows/Desktop/Mac/MetalWindows.mm +++ b/VisualPinball.Native~/NativeWindows/Desktop/Mac/MetalWindows.mm @@ -52,6 +52,8 @@ void Pipeline() CAMetalLayer* layer = nil; id texture = nil; id rawSnapshot = nil; + // Keep the drawable at the window's pixel size when the user resizes it or it moves to another display. + id resizeObserver = nil, scaleObserver = nil; std::atomic inFlight{ 0 }; bool alive = true, visible = false, topmost = false, flip = false, closing = false; int left, top, width, height; @@ -72,10 +74,31 @@ void Pipeline() layer.allowsNextDrawableTimeout = YES; layer.displaySyncEnabled = NO; layer.drawableSize = CGSizeMake(w, h); layer.contentsScale = window.backingScaleFactor; window.contentView.wantsLayer = YES; window.contentView.layer = layer; + // Runs on the main thread without the window mutex: Diagnostics holds it while waiting for the main thread. + __weak NSWindow* weakWindow = window; __weak CAMetalLayer* weakLayer = layer; + void (^refresh)(NSNotification*) = ^(NSNotification*) { + NSWindow* resized = weakWindow; CAMetalLayer* target = weakLayer; + if (!resized || !target) return; + NSRect pixels = vpnw::PixelFrame(resized); + target.contentsScale = resized.backingScaleFactor; + target.drawableSize = CGSizeMake(std::round(pixels.size.width), std::round(pixels.size.height)); + }; + NSNotificationCenter* center = NSNotificationCenter.defaultCenter; + resizeObserver = [center addObserverForName:NSWindowDidResizeNotification object:window queue:NSOperationQueue.mainQueue usingBlock:refresh]; + scaleObserver = [center addObserverForName:NSWindowDidChangeBackingPropertiesNotification object:window queue:NSOperationQueue.mainQueue usingBlock:refresh]; + }); + } + void StopObserving() + { + id resize = resizeObserver, scale = scaleObserver; resizeObserver = scaleObserver = nil; + if (resize || scale) dispatch_async(dispatch_get_main_queue(), ^{ + if (resize) [NSNotificationCenter.defaultCenter removeObserver:resize]; + if (scale) [NSNotificationCenter.defaultCenter removeObserver:scale]; }); } ~MetalWindow() override { + StopObserving(); NSWindow* owned = window; if (owned) dispatch_async(dispatch_get_main_queue(), ^{ [owned orderOut:nil]; [owned close]; }); } @@ -123,7 +146,7 @@ bool Collect() override if (inFlight.load() != 0) return false; if (!closing) { - closing = true; NSWindow* owned = window; window = nil; layer = nil; + closing = true; StopObserving(); NSWindow* owned = window; window = nil; layer = nil; dispatch_async(dispatch_get_main_queue(), ^{ [owned close]; }); } return true; diff --git a/VisualPinball.Plugins.Test/OutputResourceTests.cs b/VisualPinball.Plugins.Test/OutputResourceTests.cs index 9e806b52b..5bc8ba077 100644 --- a/VisualPinball.Plugins.Test/OutputResourceTests.cs +++ b/VisualPinball.Plugins.Test/OutputResourceTests.cs @@ -36,11 +36,13 @@ public void NativeOutputsLeaveRolelessControllerBindingsUnambiguous() public void SegmentDisplaysAnnounceTheirPlaceBeforeTheFirstFrame() { var registry = new PluginResourceRegistry(); - using var source = new SegmentResourceSource(1, 7); + using var source = new SegmentResourceSource(1, 7, "seg14", "vfd-green"); using var publication = registry.Publish(new PluginResourceDescriptor("pinmame/segments/display1", "pinmame", "pinmame", "display1", "segments", ResourceKind.Segments, 1, 1), source); using var binding = registry.Bind(new PluginResourceSelection(ResourceKind.Segments) { Session = 1 }); Assert.That(binding.Source!.Order, Is.EqualTo(1)); Assert.That(binding.Source.Elements, Is.EqualTo(7)); + Assert.That(binding.Source.Layout, Is.EqualTo("seg14")); + Assert.That(binding.Source.Hardware, Is.EqualTo("vfd-green")); } [Test] diff --git a/VisualPinball.Plugins.Test/PluginHostTests.cs b/VisualPinball.Plugins.Test/PluginHostTests.cs index 4f66f23fd..6e86f2a9a 100644 --- a/VisualPinball.Plugins.Test/PluginHostTests.cs +++ b/VisualPinball.Plugins.Test/PluginHostTests.cs @@ -198,15 +198,64 @@ public void FutureDocumentsAreRejectedBeforeTouchingLiveSettings() public void UncooperativeCleanupDoesNotBlockOtherPluginsOrExit() { var calls = new List(); - var factory = new TestFactory("stuck", calls) { NeverFinishStop = true }; + var stop = new TaskCompletionSource(); + var factory = new TestFactory("stuck", calls) { StopCompletion = stop.Task }; var host = Host(factory, new TestFactory("healthy", calls)); host.CleanupTimeout = TimeSpan.FromMilliseconds(30); Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); Run(host, host.StopGameAsync()); Assert.That(host.Statuses["stuck"].State, Is.EqualTo(PluginState.Unresponsive)); Assert.That(calls, Does.Contain("healthy:stop")); - Assert.That(calls, Does.Contain("stuck:release")); Assert.That(host.IsRunning, Is.False); + // The hung hook may still use its resources, so they stay owned until it returns. + Assert.That(calls, Does.Not.Contain("stuck:release")); + stop.SetResult(true); + Run(host, Task.Run(async () => { while (!calls.Contains("stuck:release")) await Task.Delay(5); })); + Assert.That(calls, Does.Contain("stuck:dispose")); + Assert.That(host.Statuses["stuck"].State, Is.EqualTo(PluginState.Available)); + } + [Test] + public void EmergencyStoppedWorkersKeepTheirResourcesAndBlockTheNextInstance() + { + var calls = new List(); + var quiet = new TaskCompletionSource(); + var factory = new TestFactory("busy", calls) { QuiescenceTask = quiet.Task }; + var host = Host(factory); + host.CleanupTimeout = TimeSpan.FromMilliseconds(30); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + host.EmergencyStop(); + // Even well past the cleanup timeout, a running worker keeps its resources and blocks a replacement. + Run(host, Task.Delay(100)); + Assert.That(calls, Does.Not.Contain("busy:release")); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + Assert.That(factory.Constructions, Is.EqualTo(1)); + Assert.That(host.Statuses["busy"].State, Is.EqualTo(PluginState.Unresponsive)); + Run(host, host.StopGameAsync()); + quiet.SetResult(true); + Run(host, Task.Run(async () => { while (!calls.Contains("busy:release")) await Task.Delay(5); })); + Run(host, Task.Delay(20)); + Assert.That(host.Statuses["busy"].State, Is.EqualTo(PluginState.Available)); + Run(host, host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default)); + Assert.That(factory.Constructions, Is.EqualTo(2)); + Run(host, host.StopGameAsync()); + } + [Test] + public void EmergencyStopDuringAHungHookKeepsResourcesUntilTheHookReturns() + { + var calls = new List(); + var initialize = new TaskCompletionSource(); + var factory = new TestFactory("hung", calls) { InitializeCompletion = initialize.Task }; + var host = Host(factory); + host.StartupTimeout = TimeSpan.FromSeconds(30); + var start = host.PrepareAndStartAsync(_game, "test", "Test", "controller", 1, default); + Run(host, Task.Run(async () => { while (!calls.Contains("hung:initialize")) await Task.Delay(5); })); + // Quiescence is complete; only the initialization hook is still running. + host.EmergencyStop(); + Run(host, Task.Delay(50)); + Assert.That(calls, Does.Not.Contain("hung:release")); + initialize.SetResult(true); + Run(host, Task.Run(async () => { while (!calls.Contains("hung:release")) await Task.Delay(5); })); + Run(host, Task.WhenAny(start, Task.Delay(1000))); } [Test] public void BadPolicyAndOrderingCycleOnlyBlockAffectedPlugins() @@ -240,6 +289,8 @@ internal sealed class TestFactory : IPlayerPluginFactory internal readonly List Calls; internal string FailAt = ""; internal int Constructions; + /// Work the plugin reports as still running after its hooks returned. + internal Task? QuiescenceTask; internal int PrepareThread; internal bool AsyncInitialize; internal bool CleanupWasCanceled; @@ -257,8 +308,9 @@ private sealed class SynchronousTestPlugin : TestPlugin, IPluginSynchronousRecon internal SynchronousTestPlugin(TestFactory factory) : base(factory) { } public PluginActionResult Apply(PluginSettingsSnapshot settings) => ApplyAsync(settings, default).GetAwaiter().GetResult(); } - private class TestPlugin : PlayerPlugin, IPluginReconfiguration, IPluginTick - { + private class TestPlugin : PlayerPlugin, IPluginReconfiguration, IPluginTick, IPluginDeferredCleanup + { + public Task Quiescence => _factory.QuiescenceTask ?? Task.CompletedTask; private readonly TestFactory _factory; internal TestPlugin(TestFactory factory) { _factory = factory; } private void Call(string phase) { _factory.Calls.Add(_factory.Descriptor.Id + ":" + phase); if (_factory.FailAt == phase) throw new InvalidOperationException("Test failure"); } diff --git a/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs b/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs new file mode 100644 index 000000000..e657c64f8 --- /dev/null +++ b/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs @@ -0,0 +1,90 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using NUnit.Framework; +using VisualPinball.Plugins; + +namespace VisualPinball.Plugins.Test +{ + [TestFixture] + public sealed class SettingsAndNegotiationTests + { + private string _directory = null!; + private readonly PluginDescriptor _plugin = new PluginDescriptor("test.apply", "Apply", "Test"); + + [SetUp] public void SetUp() => _directory = Path.Combine(Path.GetTempPath(), "vpe-plugin-tests", Guid.NewGuid().ToString("N")); + [TearDown] public void TearDown() { if (Directory.Exists(_directory)) Directory.Delete(_directory, true); } + + [Test] + public async Task EditsMadeWhileApplyingStayPending() + { + var schema = new PluginSettingsSchema(1, new PluginSettingsGroup("General", "", + new PluginField("a", "A", SettingKind.Text) { Default = "" }, + new PluginField("b", "B", SettingKind.Text) { Default = "" })); + var store = new PluginConfigurationStore(_directory); + using var session = new PluginSettingsSession(new Factory(_plugin, schema), store, "windows"); + session.Set("a", "one"); + // The draft is frozen synchronously when Apply starts; these edits come after. + var apply = session.ApplyAsync(); + session.Set("a", "two"); + session.Set("b", "later"); + Assert.That((await apply).Success, Is.True); + + var saved = store.Read(_plugin, schema); + Assert.That(saved.String("a"), Is.EqualTo("one")); + Assert.That(saved.String("b"), Is.EqualTo("")); + Assert.That(session.Draft.String("a"), Is.EqualTo("two")); + Assert.That(session.Draft.String("b"), Is.EqualTo("later")); + Assert.That(session.HasEdits("a"), Is.True); + Assert.That(session.HasEdits("b"), Is.True); + } + + [Test] + public async Task UnchangedFieldsFollowTheSavedProfileAfterApply() + { + var schema = new PluginSettingsSchema(1, new PluginSettingsGroup("General", "", + new PluginField("a", "A", SettingKind.Text) { Default = "" })); + var store = new PluginConfigurationStore(_directory); + using var session = new PluginSettingsSession(new Factory(_plugin, schema), store, "windows"); + session.Set("a", "one"); + Assert.That((await session.ApplyAsync()).Success, Is.True); + Assert.That(session.Draft.String("a"), Is.EqualTo("one")); + Assert.That(session.HasEdits("a"), Is.False); + } + + [Test] + public void IdentificationEncodingIsOneTheConsumerAccepts() + { + var requested = new List(); + using var negotiator = new DisplayFormatNegotiator(DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8, DisplayEncoding.Luminance8, requested.Add); + var gray2Only = negotiator.Acquire(ResourceView.Identification, DisplayEncoding.Gray2); + Assert.That(requested, Is.EqualTo(new[] { DisplayEncoding.Gray2 })); + // A second consumer must be able to read the encoding already being delivered. + Assert.Throws(() => negotiator.Acquire(ResourceView.Identification, DisplayEncoding.Gray4)); + var both = negotiator.Acquire(ResourceView.Identification, DisplayEncoding.Gray2 | DisplayEncoding.Gray4); + Assert.That(requested, Has.Count.EqualTo(1)); + gray2Only.Dispose(); + both.Dispose(); + Assert.That(requested[requested.Count - 1], Is.EqualTo(DisplayEncoding.Luminance8)); + // With the group idle again, a Gray4 consumer gets Gray4. + using var gray4 = negotiator.Acquire(ResourceView.Identification, DisplayEncoding.Gray4); + Assert.That(requested[requested.Count - 1], Is.EqualTo(DisplayEncoding.Gray4)); + } + + private sealed class Factory : IPlayerPluginFactory + { + public PluginDescriptor Descriptor { get; } + public PluginSettingsSchema Schema { get; } + public IPluginSettingsProvider? SettingsProvider => null; + internal Factory(PluginDescriptor descriptor, PluginSettingsSchema schema) { Descriptor = descriptor; Schema = schema; } + public IPlayerPlugin Create(PluginApplicationContext context) => throw new NotSupportedException(); + } + } +} diff --git a/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs.meta b/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs.meta new file mode 100644 index 000000000..6c9f03eb8 --- /dev/null +++ b/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 15a7104bc7914bcfb617d9aeb94373fd diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs index f08b8fdc9..1bd472db1 100644 --- a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs @@ -84,6 +84,7 @@ public async Task ApplyAsync(CancellationToken cancellation try { // Freeze the draft before any asynchronous discovery so editing cannot change the validated transaction. var draft = Draft.Copy(); + var frozenResets = new HashSet(_resetFields, StringComparer.Ordinal); var changed = ChangedFields(); var resources = _resources(); var effective = await Task.Run(() => { @@ -113,14 +114,31 @@ public async Task ApplyAsync(CancellationToken cancellation if (host != null) { var result = await host.CommitSettingsAsync(_factory, draft, Game, linked.Token, changed).ConfigureAwait(false); await host.Scheduler.Switch(); - if (result.Success) Revert(); + if (result.Success) Rebase(draft, frozenResets); return result; } await Task.Run(() => _store.SavePatch(_factory.Descriptor, _factory.Schema, draft, changed, Game)).ConfigureAwait(false); - Revert(); + Rebase(draft, frozenResets); return new PluginActionResult(true, "Saved. Changes take effect on the next game."); } finally { _apply.Release(); } } + /// + /// Reloads the saved profile after a successful apply, then re-applies fields edited after + /// was taken, so changes made while the apply ran stay pending instead of being discarded. + /// + private void Rebase(PluginSettingsSnapshot frozen, HashSet frozenResets) + { + var current = Draft; + var currentResets = new HashSet(_resetFields, StringComparer.Ordinal); + Revert(); + foreach (var field in _factory.Schema.Fields) { + string id = field.Id; + bool reset = currentResets.Contains(id); + if (JToken.DeepEquals(frozen.Value(id), current.Value(id)) && reset == frozenResets.Contains(id)) continue; + Draft.Set(id, current.Value(id)); + if (reset) _resetFields.Add(id); + } + } private static IEnumerable<(PluginField Field, JToken? Value, string Path)> ChoiceBindings(IEnumerable fields, PluginSettingsSnapshot snapshot, string prefix = "") { foreach (var field in fields) { diff --git a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs index 79367ce22..e603fc8dc 100644 --- a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs +++ b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs @@ -15,6 +15,7 @@ public sealed class DisplayFormatNegotiator : IDisposable private readonly DisplayEncoding _supported; private readonly Action _request; private int _identification; + private DisplayEncoding _active; private bool _disposed; public DisplayFormatNegotiator(DisplayEncoding supported, DisplayEncoding defaultFormat, Action request) { _supported = supported; _default = defaultFormat; _request = request; } @@ -24,11 +25,17 @@ public IDisposable Acquire(ResourceView intent, DisplayEncoding accepted) if (intent != ResourceView.Identification) return new Subscription(() => { }); var raw = _supported & accepted & (DisplayEncoding.Gray2 | DisplayEncoding.Gray4); if (raw == DisplayEncoding.None) throw new InvalidOperationException("This source cannot provide unmodified identification frames."); - if (_identification++ == 0) { - try { _request((_supported & DisplayEncoding.Gray4) != 0 ? DisplayEncoding.Gray4 : DisplayEncoding.Gray2); } - catch { _identification--; throw; } + if (_identification > 0) { + // The provider group delivers one raw encoding at a time; a consumer that cannot read it cannot join. + if ((raw & _active) == DisplayEncoding.None) throw new InvalidOperationException("Identification frames are already delivered as " + _active + ", which this consumer does not accept."); + _identification++; + } else { + var encoding = (raw & DisplayEncoding.Gray4) != 0 ? DisplayEncoding.Gray4 : DisplayEncoding.Gray2; + _request(encoding); + _active = encoding; + _identification = 1; } - return new Subscription(() => { if (!_disposed && --_identification == 0) _request(_default); }); + return new Subscription(() => { if (!_disposed && --_identification == 0) { _active = DisplayEncoding.None; _request(_default); } }); } public void Dispose() { if (_disposed) return; _disposed = true; if (_identification > 0) _request(_default); _identification = 0; } } diff --git a/VisualPinball.Plugins/Resources/ResourceContracts.cs b/VisualPinball.Plugins/Resources/ResourceContracts.cs index abbdefbf4..ba3eef40d 100644 --- a/VisualPinball.Plugins/Resources/ResourceContracts.cs +++ b/VisualPinball.Plugins/Resources/ResourceContracts.cs @@ -184,6 +184,10 @@ public interface IPluginSegmentSource : IPluginResourceSource int Order { get; } /// Number of elements (digits) the controller announced; frames carry the same count. int Elements { get; } + /// Segment arrangement of the elements, as in . + string Layout { get; } + /// Display technology hint, as in . + string Hardware { get; } IDisposable Subscribe(Action receive); } /// diff --git a/VisualPinball.Plugins/Resources/ResourceSignal.cs b/VisualPinball.Plugins/Resources/ResourceSignal.cs index 124087f8e..1c519e8b2 100644 --- a/VisualPinball.Plugins/Resources/ResourceSignal.cs +++ b/VisualPinball.Plugins/Resources/ResourceSignal.cs @@ -49,10 +49,12 @@ public sealed class SegmentResourceSource : IPluginSegmentSource, IDisposable public ResourceKind Kind => ResourceKind.Segments; public int Order { get; } public int Elements { get; } - public SegmentResourceSource(Action? fault = null) : this(0, 0, fault) { } - public SegmentResourceSource(int order, int elements, Action? fault = null) + public string Layout { get; } + public string Hardware { get; } + public SegmentResourceSource(Action? fault = null) : this(0, 0, "", "", fault) { } + public SegmentResourceSource(int order, int elements, string layout, string hardware, Action? fault = null) { - Order = order; Elements = elements; + Order = order; Elements = elements; Layout = layout ?? ""; Hardware = hardware ?? ""; _signal = new ResourceSignal(fault); } public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); diff --git a/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs b/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs index c34452688..d42637d03 100644 --- a/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs +++ b/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs @@ -66,7 +66,7 @@ private async Task CommitSettingsCoreAsync(IPlayerPluginFact try { using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation); deadline.CancelAfter(StartupTimeout); var hook = asynchronous.ApplyAsync(live, deadline.Token); - await AwaitHookAsync(hook, deadline.Token).ConfigureAwait(false); await Scheduler.Switch(); result = await hook.ConfigureAwait(false); + await AwaitHookAsync(entry, hook, deadline.Token).ConfigureAwait(false); await Scheduler.Switch(); result = await hook.ConfigureAwait(false); } catch (HookDeadlineException error) { await Scheduler.Switch(); Abandon(entry, error.Hook); return new PluginActionResult(false, "Live configuration did not finish; settings were not saved."); } finally { await Scheduler.Switch(); entry.Busy = false; } } else return new PluginActionResult(false, "The plugin does not implement live configuration."); @@ -89,7 +89,7 @@ private async Task CommitSettingsCoreAsync(IPlayerPluginFact else { entry.Busy = true; using var deadline = new CancellationTokenSource(StartupTimeout); var hook = asynchronous!.ApplyAsync(previous, deadline.Token); - await AwaitHookAsync(hook, deadline.Token).ConfigureAwait(false); await Scheduler.Switch(); rollback = await hook.ConfigureAwait(false); + await AwaitHookAsync(entry, hook, deadline.Token).ConfigureAwait(false); await Scheduler.Switch(); rollback = await hook.ConfigureAwait(false); } if (!rollback.Success) Fault(factory.Descriptor.Id, PluginPhase.Prepare, new InvalidOperationException("Runtime settings rollback failed.")); else entry.Settings = previous; diff --git a/VisualPinball.Plugins/Runtime/PluginHost.cs b/VisualPinball.Plugins/Runtime/PluginHost.cs index 3c81f62be..539818975 100644 --- a/VisualPinball.Plugins/Runtime/PluginHost.cs +++ b/VisualPinball.Plugins/Runtime/PluginHost.cs @@ -29,7 +29,18 @@ private sealed class Instance internal readonly ResourceScope GameScope = new ResourceScope(); internal bool Prepared; internal bool Cleaned; + internal bool Abandoned; internal bool Busy; + private readonly List _hooks = new List(); + /// Records a lifecycle hook until it completes, so a quarantine can wait for it even after its deadline. + internal void Track(Task hook) + { + if (hook.IsCompleted) return; + lock (_hooks) _hooks.Add(hook); + hook.ContinueWith(done => { lock (_hooks) _hooks.Remove(done); }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + /// Completes when every hook running now has finished. + internal Task RunningHooks() { lock (_hooks) return _hooks.Count == 0 ? Task.CompletedTask : Task.WhenAll(_hooks.ToArray()); } internal PluginSettingsSnapshot? Settings; internal Instance(IPlayerPluginFactory factory, IPlayerPlugin plugin, ResourceScope applicationScope) { Factory = factory; Plugin = plugin; ApplicationScope = applicationScope; } } @@ -204,7 +215,7 @@ private async Task InvokeAsync(Instance entry, PluginPhase phase, Func { var ignored = task.Exception; }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default); throw new HookDeadlineException(hook); @@ -224,26 +236,43 @@ private void Abandon(Instance entry, Task hook) { string id = entry.Factory.Descriptor.Id; SetStatus(id, PluginState.Unresponsive, "Waiting for an unresponsive plugin; its next instance is blocked."); + // Stop the plugin now, but keep its scopes (native handles, device leases) until its hook and workers are done. Emergency(entry); - entry.Cleaned = true; - SafeDispose(entry.GameScope); SafeDispose(entry.ApplicationScope); - if (_abandoned.ContainsKey(id)) return; - var quiescence = (entry.Plugin as IPluginDeferredCleanup)?.Quiescence ?? Task.CompletedTask; - var pending = Task.WhenAll(hook, quiescence); - _abandoned[id] = pending; - _ = FinishAbandonedAsync(entry, pending); + if (entry.Abandoned) return; + Quarantine(entry, Task.WhenAll(hook, entry.RunningHooks(), Quiescence(entry)), shutdown: true); + } + private static Task Quiescence(Instance entry) => (entry.Plugin as IPluginDeferredCleanup)?.Quiescence ?? Task.CompletedTask; + /// + /// Keeps a stopped instance and its scopes (native handles, device leases) until its pending work finishes, however + /// long that takes, and blocks new instances of the plugin meanwhile. The host itself never waits for it. + /// + private void Quarantine(Instance entry, Task pending, bool shutdown) + { + string id = entry.Factory.Descriptor.Id; + entry.Abandoned = entry.Cleaned = true; + SetStatus(id, PluginState.Unresponsive, "Waiting for an unresponsive plugin; its next instance is blocked."); + // Another instance of the same plugin may still be draining; the next one waits for both. + _abandoned[id] = _abandoned.TryGetValue(id, out var earlier) ? Task.WhenAll(earlier, pending) : pending; + _ = FinishQuarantineAsync(entry, pending, shutdown); } - private async Task FinishAbandonedAsync(Instance entry, Task pending) + private async Task FinishQuarantineAsync(Instance entry, Task pending, bool shutdown) { try { await pending.ConfigureAwait(false); } catch (Exception error) { _log("Abandoned hook: " + error.Message); } await Scheduler.Switch(); - // This old instance is quarantined independently of the active game's lifecycle gate. - using var deadline = new CancellationTokenSource(CleanupTimeout); - try { await entry.Plugin.ShutdownAsync(deadline.Token).ConfigureAwait(false); } - catch (Exception error) { _log("Deferred shutdown: " + error.Message); } - await Scheduler.Switch(); - SafeDispose(entry.Plugin); + if (shutdown) { + // This old instance is quarantined independently of the active game's lifecycle gate. + using var deadline = new CancellationTokenSource(CleanupTimeout); + try { await entry.Plugin.ShutdownAsync(deadline.Token).ConfigureAwait(false); } + catch (Exception error) { _log("Deferred shutdown: " + error.Message); } + await Scheduler.Switch(); + } + SafeDispose(entry.GameScope); SafeDispose(entry.ApplicationScope); SafeDispose(entry.Plugin); string id = entry.Factory.Descriptor.Id; + if (!_abandoned.TryGetValue(id, out var remaining)) return; + // Unblock the plugin only when every abandoned instance has drained, not just this one. + try { await remaining.ConfigureAwait(false); } catch { /* logged by each instance */ } + await Scheduler.Switch(); + if (!_abandoned.TryGetValue(id, out var current) || current != remaining) return; _abandoned.Remove(id); if (_statuses[id].State == PluginState.Unresponsive) SetStatus(id, PluginState.Available, "Previous instance stopped."); } @@ -286,7 +315,7 @@ private async Task CleanupAsync(Instance entry, CancellationToken cleanup) if (entry.Prepared) { using var stopDeadline = CancellationTokenSource.CreateLinkedTokenSource(cleanup); stopDeadline.CancelAfter(CleanupTimeout); - try { await AwaitHookAsync(entry.Plugin.StopGameAsync(stopDeadline.Token), stopDeadline.Token).ConfigureAwait(false); await Scheduler.Switch(); } + try { await AwaitHookAsync(entry, entry.Plugin.StopGameAsync(stopDeadline.Token), stopDeadline.Token).ConfigureAwait(false); await Scheduler.Switch(); } catch (HookDeadlineException error) { await Scheduler.Switch(); Abandon(entry, error.Hook); return; } catch (OperationCanceledException) when (stopDeadline.IsCancellationRequested) { await Scheduler.Switch(); Abandon(entry, Task.CompletedTask); return; @@ -296,7 +325,7 @@ private async Task CleanupAsync(Instance entry, CancellationToken cleanup) SafeDispose(entry.GameScope); using var shutdownDeadline = CancellationTokenSource.CreateLinkedTokenSource(cleanup); shutdownDeadline.CancelAfter(CleanupTimeout); - try { await AwaitHookAsync(entry.Plugin.ShutdownAsync(shutdownDeadline.Token), shutdownDeadline.Token).ConfigureAwait(false); await Scheduler.Switch(); } + try { await AwaitHookAsync(entry, entry.Plugin.ShutdownAsync(shutdownDeadline.Token), shutdownDeadline.Token).ConfigureAwait(false); await Scheduler.Switch(); } catch (HookDeadlineException error) { await Scheduler.Switch(); Abandon(entry, error.Hook); return; } catch (Exception error) { await Scheduler.Switch(); _log(id + " / Shutdown: " + error.Message); } SafeDispose(entry.ApplicationScope); SafeDispose(entry.Plugin); @@ -363,6 +392,9 @@ public void EmergencyStop() Emergency(entry); if (entry.Cleaned) continue; entry.Cleaned = true; + // Hooks and workers still running keep their resources until they are done; everything else goes now. + var pending = Task.WhenAll(entry.RunningHooks(), Quiescence(entry)); + if (!pending.IsCompleted) { Quarantine(entry, pending, shutdown: false); continue; } SafeDispose(entry.GameScope); SafeDispose(entry.ApplicationScope); SafeDispose(entry.Plugin); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs index dbd38426e..33c8e9824 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs @@ -114,7 +114,7 @@ private void AddSegments(string id, int elements) string layout = "seg16", hardware = ""; if (_engine is ISegmentDisplayMetadata metadata && metadata.TryGetSegmentLayout(id, out var declared, out var declaredHardware)) { layout = declared; hardware = declaredHardware; } // Displays are numbered in announcement order, so consumers can lay out digits before any frame arrives. - var source = new SegmentResourceSource(_segments.Count, elements, _fault); + var source = new SegmentResourceSource(_segments.Count, elements, layout, hardware, _fault); var descriptor = new PluginResourceDescriptor(_controllerId + "/segments/" + id, _provider, _controllerId, id, "segments", ResourceKind.Segments, _session, Interlocked.Increment(ref _instance)); _segments.Add(id, new Segments(source, _registry.Publish(descriptor, source), layout, hardware)); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs index 8ac086e41..e7b72d12c 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs @@ -23,7 +23,8 @@ private sealed class Cached internal DateTime Modified; internal GameIdentity Identity; } - private static readonly Dictionary Cache = new Dictionary(StringComparer.OrdinalIgnoreCase); + // Ordinal on every platform: Linux paths are case-sensitive, and elsewhere a differently cased path only costs a second read. + private static readonly Dictionary Cache = new Dictionary(StringComparer.Ordinal); public static Task ReadAsync(string path, CancellationToken cancellation = default) => Task.Run(() => Read(path, cancellation), cancellation); private static GameIdentity Read(string path, CancellationToken cancellation) { From 63e443d4c7983a0d4725f70c7564dedae03ea357 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 10:53:35 +0200 Subject: [PATCH 13/26] plugins: document the plugin contract APIs Every public type and member of the plugin API now has XML documentation saying what it is for, with a use case from the Player, PinUP, B2S, DMD or PinMAME integrations, examples for what plugins implement or call, and the threading, lifetime and ownership rules. This covers the descriptors and runtime hooks, the configuration schema, store and global settings, the runtime helpers, the resource registry and display sources, the Unity plugin contracts and the native window service. One-line enums and interfaces are split over several lines so that each member can carry its own comment; no code changed. --- .../Api/GameIdentityProvider.cs | 136 ++++- VisualPinball.Plugins/Api/PluginDescriptor.cs | 323 ++++++++++- VisualPinball.Plugins/Api/PluginRuntime.cs | 433 ++++++++++++++- .../Configuration/PluginConfigurationStore.cs | 165 ++++++ .../Configuration/PluginGlobalSetting.cs | 107 +++- .../Configuration/PluginSchema.cs | 514 +++++++++++++++++- .../Resources/DisplayResourceSource.cs | 125 ++++- .../Resources/PluginResourceChoices.cs | 47 +- .../Runtime/OwnerScheduler.cs | 61 ++- .../Runtime/PluginCapabilities.cs | 58 +- VisualPinball.Plugins/Runtime/PluginEvents.cs | 52 +- .../Runtime/PluginHost.Settings.cs | 48 +- .../Runtime/PluginInputPipeline.cs | 35 +- .../Runtime/PluginOrderResolver.cs | 72 ++- .../Runtime/PluginOutputLeases.cs | 29 +- .../Runtime/ResourceScope.cs | 52 +- .../NativeWindows/NativeWindowCapabilities.cs | 22 +- .../NativeWindows/NativeWindowDiagnostics.cs | 38 +- .../NativeWindows/NativeWindowHost.cs | 33 ++ .../NativeWindows/NativeWindowSurface.cs | 133 ++++- .../Plugins/PlayerIntegrationMode.cs | 49 +- .../Plugins/UnityPluginContracts.cs | 211 ++++++- .../Plugins/UnitySettingsMedia.cs | 166 +++++- .../Plugins/UnityTextureResource.cs | 40 +- 24 files changed, 2876 insertions(+), 73 deletions(-) diff --git a/VisualPinball.Plugins/Api/GameIdentityProvider.cs b/VisualPinball.Plugins/Api/GameIdentityProvider.cs index 95e5c5c9c..95f59418b 100644 --- a/VisualPinball.Plugins/Api/GameIdentityProvider.cs +++ b/VisualPinball.Plugins/Api/GameIdentityProvider.cs @@ -4,23 +4,119 @@ // SPDX-License-Identifier: MIT // Licensed under MIT. See https://opensource.org/license/mit. -using System; -using System.Collections.Generic; - -namespace VisualPinball.Plugins -{ - public interface IGameIdentityProvider { GameIdentity? GameIdentity { get; } } - /// Reads only the provider's inert package payload; it must not initialize its native runtime. - public interface IPackagedGameIdentityReader - { - string ComponentType { get; } - GameIdentity? Read(ReadOnlyMemory payload); - } - public static class PackagedGameIdentityReaders - { - private static readonly Dictionary Readers = new Dictionary(StringComparer.Ordinal); - public static void Reset() => Readers.Clear(); - public static void Register(IPackagedGameIdentityReader reader) => Readers.Add(reader.ComponentType, reader); - public static IPackagedGameIdentityReader? Find(string componentType) => Readers.TryGetValue(componentType, out var reader) ? reader : null; - } -} +using System; +using System.Collections.Generic; + +namespace VisualPinball.Plugins +{ + /// + /// Implemented by a gamelogic engine that knows which game it runs, so the Player can tell plugins which game is being + /// played without opening the table package. + /// + /// + /// The identity selects per-game plugin settings and lets plugins find matching media, such as a PinUP pack or a + /// backglass for the ROM. The Player asks the loaded table's engine first and only falls back to reading the package + /// (see ) when the engine cannot answer. + /// + /// A ROM-based engine reports its ROM name: + /// + /// public class MyRomEngine : MonoBehaviour, IGamelogicEngine, IGameIdentityProvider + /// { + /// public string romId = "t2_l8"; + /// GameIdentity? IGameIdentityProvider.GameIdentity => string.IsNullOrWhiteSpace(romId) ? null : new GameIdentity("myrom", romId); + /// } + /// + /// + /// + public interface IGameIdentityProvider + { + /// + /// The game this engine is configured to run, or null when it cannot tell yet (for example before a ROM is + /// selected). Must be cheap and side-effect free: the Player reads it while preparing a game, before the engine starts. + /// + GameIdentity? GameIdentity { get; } + } + + /// + /// Reads a game identity from one component's serialized data inside a table package, without loading the table. + /// + /// + /// The Player uses readers to identify a game from the package alone, for instance to show per-game plugin settings in + /// the table browser before anything is loaded. Each reader handles one component type; the Player walks the package's + /// components, hands every payload of a matching type to its reader, and uses the single identity found. Without any + /// identity the package's content hash identifies the game. + /// + /// A reader must only parse the payload. It must not initialize the component's native runtime (no emulator, no + /// device), since it runs on a worker thread for packages that may never be played. + /// + /// + /// The PinMAME integration reads the ROM name from its engine component: + /// + /// internal sealed class PinMamePackageIdentityReader : IPackagedGameIdentityReader + /// { + /// public string ComponentType => "PinMameGamelogicEngine"; + /// public GameIdentity? Read(ReadOnlyMemory<byte> payload) + /// { + /// var values = JObject.Parse(Encoding.UTF8.GetString(payload.Span)); + /// var rom = values.Value<string>("RomId"); + /// return string.IsNullOrWhiteSpace(rom) ? null : new GameIdentity("pinmame", rom); + /// } + /// } + /// + /// + /// + public interface IPackagedGameIdentityReader + { + /// + /// Name of the component type whose payloads this reader understands, as stored in the package (the component's + /// type name, such as PinMameGamelogicEngine). Used as the registry key, so it must be unique. + /// + string ComponentType { get; } + + /// + /// Extracts the game identity from one serialized component payload, or returns null when the payload does not name + /// a game (for example an engine with no ROM selected). Called on a worker thread; must not touch Unity. + /// + /// The component's raw serialized data, borrowed for the duration of the call. + GameIdentity? Read(ReadOnlyMemory payload); + } + + /// + /// Registry of s, keyed by component type, that the Player consults when it + /// identifies a table package. + /// + /// + /// Integration packages register their reader once at startup, typically from a + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] method. The registry is not + /// thread-safe: register on the main thread before packages are read. + /// + public static class PackagedGameIdentityReaders + { + private static readonly Dictionary Readers = new Dictionary(StringComparer.Ordinal); + + /// + /// Removes all registered readers. The host calls this when the scripting domain starts, so readers registered by a + /// previous play session in the editor do not linger; plugins have no reason to call it. + /// + public static void Reset() => Readers.Clear(); + + /// Adds a reader for its . + /// + /// + /// + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + /// private static void Register() => PackagedGameIdentityReaders.Register(new PinMamePackageIdentityReader()); + /// + /// + /// + /// A reader for the same component type is already registered. + public static void Register(IPackagedGameIdentityReader reader) => Readers.Add(reader.ComponentType, reader); + + /// + /// Returns the reader for a component type, or null when no integration understands that component. The Player calls + /// this for every component in a package and skips the ones without a reader. + /// + /// The component type name as stored in the package. + public static IPackagedGameIdentityReader? Find(string componentType) => Readers.TryGetValue(componentType, out var reader) ? reader : null; + } +} diff --git a/VisualPinball.Plugins/Api/PluginDescriptor.cs b/VisualPinball.Plugins/Api/PluginDescriptor.cs index ef6c95bed..6bacf6551 100644 --- a/VisualPinball.Plugins/Api/PluginDescriptor.cs +++ b/VisualPinball.Plugins/Api/PluginDescriptor.cs @@ -9,39 +9,304 @@ namespace VisualPinball.Plugins { - public enum PluginPhase { Initialize, Prepare, Start, Ready, Pause, Resume, Focus, Tick, Render, Stop, Shutdown } - public enum PluginState { Available, Disabled, Unsupported, Blocked, Starting, Running, Stopping, Faulted, Unresponsive } - public enum ConfigurationScope { GlobalOnly, GameOnly, GlobalAndGame } - public enum ConfigurationChange { Live, NextGame, ApplicationRestart } + /// + /// The lifecycle steps the drives a plugin through. Each value names one hook of + /// or of an optional hook interface. + /// + /// + /// Phases serve two purposes. They select the ordering rules in , so a plugin + /// can, for example, render after another plugin without also starting after it. They are also reported in + /// , so the Player can say in which step a plugin failed. The host orders the startup + /// and dispatch phases ( to ); cleanup does not use ordering rules, it runs + /// in reverse start order. + /// + public enum PluginPhase + { + /// , right after the host created the instance. + Initialize, + /// , where a plugin sets up everything it needs for the game. + Prepare, + /// , after every plugin has been prepared. + Start, + /// , when the table's player has started. + Ready, + /// with true, when the game is paused. + Pause, + /// with false, when the game resumes. + Resume, + /// , when the application gains or loses focus. + Focus, + /// , once per frame. + Tick, + /// , once per frame after all frame updates. + Render, + /// + /// . Reported in while a plugin stops; not + /// used for ordering, since cleanup runs in reverse start order. + /// + Stop, + /// + /// . Not used for ordering, since cleanup runs in reverse start order, and + /// the host does not currently report it in . + /// + Shutdown + } + + /// The runtime state of one installed plugin, as kept by the . + /// + /// The Player's plugin settings page reads it from and shows the state and + /// when a plugin is , , + /// or . A plugin cannot set its own state; the host derives it from + /// configuration and from how the plugin's hooks complete. + /// + public enum PluginState + { + /// + /// Installed and selected, or cleanly stopped. This is the initial state and the state after a game ends normally. + /// + Available, + /// Disabled by the user, globally or for the current game, so the host did not create an instance. + Disabled, + /// + /// Not created because the current platform is not listed in . + /// + Unsupported, + /// + /// Not started or stopped early because a dependency or required capability is missing or ambiguous, its ordering + /// rules form a cycle, or the host was still busy with a previous game. says which. + /// + Blocked, + /// + /// A startup hook (initialize, prepare or start) is running. says which one. + /// + Starting, + /// + /// completed. Only running plugins receive ready, pause, focus, tick and + /// render calls. + /// + Running, + /// The host is running the plugin's stop and shutdown hooks. + Stopping, + /// + /// A hook threw, or the plugin's settings were invalid at startup. holds the error. + /// The instance has been cleaned up and plugins that depend on it are blocked. The next game selects it again. + /// + Faulted, + /// + /// A hook did not finish before its deadline, or the plugin's workers have not yet quiesced after an emergency stop + /// (see ). The old instance keeps its resources until it drains, and new + /// instances of the plugin are blocked until then; the state returns to afterwards. + /// + Unresponsive + } + + /// Where a setting is stored and where the user can edit it. + /// + /// Set per field through . The keeps one global + /// settings.json per plugin and one profile per game that holds only explicitly changed values; the scope decides + /// which of the two a field may live in. For example the B2S plugin makes its window placement + /// so a backglass can sit on a different monitor for one table. + /// + public enum ConfigurationScope + { + /// + /// Stored only in the global settings. Game profiles never hold or override it, and it is not shown on a game's + /// settings page. This is the default for . + /// + GlobalOnly, + /// + /// Stored only in game profiles. It is not shown on the global settings page and cannot be exposed as a global + /// setting such as a volume mixer slider. + /// + GameOnly, + /// + /// A global value that a game profile may override. On a game's settings page the Player shows an override toggle + /// for it; the game value wins when present. + /// + GlobalAndGame + } + + /// When a changed setting reaches a running plugin. + /// + /// Set per field through . When the user commits settings while a game runs, the host + /// pushes only fields to the running plugin, through + /// or . Everything else is saved and read when the next game is prepared. PinMAME + /// and PinUP, for example, make their volume so the volume mixer takes effect immediately. + /// + public enum ConfigurationChange + { + /// + /// Applied to the running plugin as soon as it is committed. The plugin must implement + /// or ; otherwise the host refuses + /// to save a changed live field that affects the running game. + /// + Live, + /// + /// Saved immediately and used from the next game on. This is the default for . + /// + NextGame, + /// + /// Only takes effect after the application restarts. The host currently treats it like : the + /// value is saved and nothing is pushed to the running plugin. + /// + ApplicationRestart + } + /// Ordering rules for one , overriding or extending a descriptor's general rules. + /// + /// Stored in . Use it when a plugin must run in a different position in one + /// phase only, for example to render after another plugin that it otherwise starts before. + /// + /// + /// descriptor.PhaseOrder = new Dictionary<PluginPhase, PluginOrder> { + /// [PluginPhase.Render] = new PluginOrder { Priority = 50, After = new[] { "org.visualpinball.dmd" } } + /// }; + /// + /// + /// public sealed class PluginOrder { + /// + /// Replaces for this phase. Lower values run earlier among plugins that no + /// ordering rule constrains; ties are broken by plugin ID. Defaults to 0, so set it explicitly when the plugin's + /// general should still apply in this phase. + /// public int Priority { get; set; } + /// + /// IDs of plugins this plugin must run before in this phase, in addition to . + /// IDs of plugins that are not selected are ignored. + /// public IReadOnlyList Before { get; set; } = Array.Empty(); + /// + /// IDs of plugins this plugin must run after in this phase, in addition to . + /// IDs of plugins that are not selected are ignored. + /// public IReadOnlyList After { get; set; } = Array.Empty(); } + /// + /// Static description of a plugin: its identity, how the Player lists it, whether it is enabled by default, and how it + /// is ordered relative to other plugins. + /// + /// + /// Every exposes one descriptor. The host reads it before any plugin instance exists: + /// to decide whether the plugin is enabled and supported for the game, to check its dependencies and required + /// capabilities, and to order its hooks against other plugins. The Player's plugin page shows its + /// , , and . Set all properties + /// when the descriptor is created and do not change them afterwards; the host reads them at every game start. + /// + /// The PinUP integration describes itself like this: + /// + /// public PluginDescriptor Descriptor { get; } = new PluginDescriptor("org.visualpinball.pinup", "PinUp", + /// "PUP packs, layered video and native screens, configured here instead of inside your tables.") + /// { Category = "Media", Order = 10 }; + /// + /// A plugin that needs another plugin's service and only works on Windows: + /// + /// new PluginDescriptor("org.example.cabinet-lights", "Cabinet lights", "Drives cabinet LEDs from the game's lamps.") { + /// Category = "Hardware", Order = 40, Requires = new[] { "vpe.outputs.v1" }, Platforms = new[] { "windows" } + /// }; + /// + /// + /// public sealed class PluginDescriptor { + /// + /// The plugin API version this build of the host implements. rejects + /// plugins whose differs. + /// public const int HostApiVersion = 1; + /// + /// Stable, unique plugin ID, such as org.visualpinball.pinup. Only lowercase letters, digits, dots and + /// hyphens. It keys the registry, the plugin's statuses and ordering references, and names the plugin's configuration + /// folder, so it must never change between releases. + /// public string Id { get; } + /// Display name shown in the Player's plugin list, which is sorted by it. public string Name { get; } + /// One or two sentences shown on the plugin's settings page, telling the user what the plugin does. public string Description { get; } + /// + /// The plugin's own version, shown as v followed by this value on its card in the Player. Informational only; + /// configuration compatibility is handled by . Defaults to 0.1.0. + /// public string Version { get; set; } = "0.1.0"; + /// + /// The plugin API version the plugin was built against. Leave the default: it is as + /// compiled into the plugin, so a plugin built against another API version is rejected at registration. + /// public int ApiVersion { get; set; } = HostApiVersion; + /// + /// Whether the plugin runs when the user has not enabled or disabled it, globally or for the game. Defaults to + /// false, so a new plugin stays off until the user enables it. + /// public bool EnabledByDefault { get; set; } - /// False for configuration services required by an application-owned engine. + /// + /// Whether the user can disable the plugin. Defaults to true. Set it to false for configuration services that an + /// application-owned engine relies on, such as the PinMAME plugin that adjusts ROM audio: such a plugin is always + /// enabled, the Player hides its enable toggle, and + /// refuses to disable it. + /// public bool CanDisable { get; set; } = true; + /// + /// Group label shown on the plugin's card in the Player, such as Media, Displays or Gamelogic. + /// Defaults to Integrations. + /// public string Category { get; set; } = "Integrations"; + /// + /// Default ordering priority in every phase. Lower values run earlier among plugins that no dependency or + /// / rule constrains; ties are broken by . A + /// entry replaces it for that phase. The bundled plugins use PinMAME 5, PinUP 10, DMD 20 and + /// B2S 30. + /// public int Order { get; set; } + /// + /// IDs of plugins that must be selected and healthy for this one to run. They run before this plugin in every + /// phase. If one is disabled, absent or fails, this plugin is . + /// public IReadOnlyList DependsOn { get; set; } = Array.Empty(); + /// + /// IDs of plugins this plugin runs before in every phase. Unlike it does not require them; + /// IDs of plugins that are not selected are ignored. + /// public IReadOnlyList Before { get; set; } = Array.Empty(); + /// + /// IDs of plugins this plugin runs after in every phase. Unlike it does not require them; + /// IDs of plugins that are not selected are ignored. + /// public IReadOnlyList After { get; set; } = Array.Empty(); + /// + /// Capability IDs this plugin offers to others. Plugins that list one in are ordered after this + /// plugin. Declaring a capability does not publish it: the plugin publishes its service itself through + /// . + /// public IReadOnlyList Provides { get; set; } = Array.Empty(); + /// + /// Capability IDs this plugin needs, such as vpe.outputs.v1. Each must be published by the host when the game + /// starts or provided by exactly one selected plugin, which then runs first. When none or several selected plugins + /// provide it, this plugin is . + /// public IReadOnlyList Requires { get; set; } = Array.Empty(); + /// + /// Per-phase ordering overrides. An entry's replaces for that + /// phase, and its before/after lists are added to and . Empty by default. + /// public IReadOnlyDictionary PhaseOrder { get; set; } = new Dictionary(); + /// + /// Platforms the plugin supports, compared with . The Player uses windows, + /// macos and linux, and Unity's lowercase platform name elsewhere. Empty, the default, means every + /// platform; on an unlisted platform the plugin is . + /// public IReadOnlyList Platforms { get; set; } = Array.Empty(); + /// Creates a descriptor with the plugin's identity and the texts the Player shows. + /// The stable plugin ID; see . + /// The display name; see . + /// The short description; see . + /// + /// is empty, contains characters other than lowercase letters, digits, dots and hyphens, or is + /// . or ... + /// public PluginDescriptor(string id, string name, string description) { if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("A plugin needs a stable ID.", nameof(id)); @@ -55,27 +320,75 @@ public PluginDescriptor(string id, string name, string description) } } + /// The current state of one installed plugin, updated by the . + /// + /// Read it from . The host changes it on the owner thread, so read it there too. The + /// Player's plugin settings page uses it to explain why a plugin is not running. + /// public sealed class PluginStatus { + /// The plugin's lifecycle state. Starts as . public PluginState State { get; internal set; } = PluginState.Available; + /// + /// Human-readable detail for , such as the error of a faulted hook or the missing provider of a + /// blocked plugin. Never null; empty until the host sets it. + /// public string Message { get; internal set; } = ""; + /// The phase the state refers to, such as the hook that failed, or null when it is not tied to one. public PluginPhase? Phase { get; internal set; } } + /// + /// Identifies the game being played, independently of the table package, as a provider namespace plus a key within it. + /// + /// + /// The Player gets it from the table's gamelogic engine (see ) or, failing that, + /// from the package (see ), and hands it to plugins in + /// . Per-game plugin settings and enablement are stored under its + /// , and plugins use it to find matching media: the DMD plugin picks a colorization by + /// , and the B2S plugin matches backglasses by ROM name when is + /// pinmame. Provider and key are trimmed and lowercased, so identities compare case-insensitively. + /// + /// + /// var identity = new GameIdentity("pinmame", "T2_L8"); + /// // identity.Key == "t2_l8", identity.Id == "pinmame:t2_l8" + /// string rom = identity.Provider == "pinmame" ? identity.Key : ""; + /// + /// + /// public readonly struct GameIdentity : IEquatable { + /// + /// Namespace of , lowercased: pinmame for ROM names, package for the content hash the + /// Player falls back to when nothing else identifies the game. + /// public string Provider { get; } + /// The game within , lowercased, such as the ROM name t2_l8. public string Key { get; } + /// + /// and joined by a colon, such as pinmame:t2_l8. Used as the stable + /// key for per-game settings and as the prefix of the game's controller ID. + /// public string Id => Provider + ":" + Key; + /// Creates an identity, trimming and lowercasing both parts. + /// The namespace of the key, such as pinmame. + /// The game within that namespace, such as a ROM name. + /// or is null or blank. public GameIdentity(string provider, string key) { if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(key)) throw new ArgumentException("A game identity needs a provider and key."); Provider = provider.Trim().ToLowerInvariant(); Key = key.Trim().ToLowerInvariant(); } + /// Whether both identities have the same and . + /// The identity to compare with. public bool Equals(GameIdentity other) => Provider == other.Provider && Key == other.Key; + /// Whether is a with the same provider and key. + /// The object to compare with. public override bool Equals(object? other) => other is GameIdentity identity && Equals(identity); + /// Returns a hash of , consistent with . public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(Id); + /// Returns , such as pinmame:t2_l8. public override string ToString() => Id; } } diff --git a/VisualPinball.Plugins/Api/PluginRuntime.cs b/VisualPinball.Plugins/Api/PluginRuntime.cs index 9f4879603..b142a48f2 100644 --- a/VisualPinball.Plugins/Api/PluginRuntime.cs +++ b/VisualPinball.Plugins/Api/PluginRuntime.cs @@ -11,85 +11,498 @@ namespace VisualPinball.Plugins { + /// + /// Entry point of a Player plugin: describes the plugin and its settings, and creates plugin instances when a game + /// starts. + /// + /// + /// An integration package registers one factory with at startup. The + /// reads and without creating the plugin, for + /// example to show the Player's plugin settings page while no game runs. When a game starts and the plugin is enabled, + /// supported and has its dependencies, the host validates the settings and calls . PinUP, the B2S + /// backglass, DMD and PinMAME packages each ship one factory. + /// + /// A minimal factory with one live setting, registered before the first scene loads: + /// + /// internal sealed class VolumePluginFactory : IPlayerPluginFactory + /// { + /// public PluginDescriptor Descriptor { get; } = + /// new PluginDescriptor("org.example.volume", "Volume", "Sets the ROM volume.") { EnabledByDefault = true }; + /// public PluginSettingsSchema Schema { get; } = new PluginSettingsSchema(1, new PluginSettingsGroup("Audio", "", + /// new PluginField("Volume", "Volume", SettingKind.Integer) { + /// Default = 100, Minimum = 0, Maximum = 100, Change = ConfigurationChange.Live + /// })); + /// public IPluginSettingsProvider? SettingsProvider => null; + /// public IPlayerPlugin Create(PluginApplicationContext context) => new VolumePlugin(context); + /// + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + /// private static void Register() => PlayerPluginRegistry.Register(new VolumePluginFactory()); + /// } + /// + /// + /// public interface IPlayerPluginFactory { + /// + /// The plugin's identity, presentation and ordering rules. Must return the same instance every time; the host reads it + /// whenever it selects or orders plugins. + /// PluginDescriptor Descriptor { get; } + /// + /// The plugin's settings: fields, defaults, scopes and migrations. The Player builds the settings page from it, the + /// stores values by it, and the host validates the effective settings against + /// it before creating the plugin. Must return the same instance every time. + /// PluginSettingsSchema Schema { get; } + /// + /// Optional source of dynamic choices, actions and extra validation for the settings page, or null when the schema is + /// self-contained. The host also calls its before creating the plugin; + /// any issue faults the plugin for that game. + /// IPluginSettingsProvider? SettingsProvider { get; } + /// + /// Creates a plugin instance. The host calls it on the owner thread once per game, for each enabled and selected + /// plugin, and then drives the instance through its lifecycle. Keep it cheap and do + /// asynchronous work in . An exception faults the plugin for that game. + /// + /// + /// Host services for the instance, valid until it is disposed. Keep it; the game hooks do not receive it again. + /// IPlayerPlugin Create(PluginApplicationContext context); } + /// A plugin instance, driven by the through the lifecycle of one game. + /// + /// The host creates a new instance for every game through and calls, in + /// order: , , , then + /// , , and the optional + /// and hooks while the game runs, and finally + /// , and . Plugins are ordered by + /// their in each startup and dispatch phase; cleanup runs in reverse start order. + /// + /// Every hook is called on the owner thread (Unity's main thread in the Player). An asynchronous hook that awaits + /// worker tasks must switch back with await context.Scheduler.Switch() before touching Unity or other + /// owner-thread state. Startup hooks get a token that is cancelled when the game is cancelled or after + /// ; cleanup hooks get one that is cancelled after + /// . A hook that misses its deadline makes the plugin + /// : the host calls and keeps the + /// instance's resources until the hook finishes. An exception from any hook except the cleanup hooks makes the plugin + /// ; the host cleans it up and blocks plugins that depend on it, while other plugins + /// keep running. + /// + /// Derive from to implement only the hooks you need. + /// public interface IPlayerPlugin : IDisposable { + /// + /// First hook after , for setup that does not depend on the game, such as + /// loading native libraries. Called once per instance, in initialize order, before any plugin is prepared. + /// + /// Cancelled when the game is cancelled or the startup deadline expires. Task InitializeAsync(CancellationToken cancellation); + /// + /// Sets up everything the plugin needs for the game: reads , finds the table + /// through a capability, binds or publishes resources, opens windows. Register everything that must be released + /// when the game ends with . Called once, in prepare order; PinUP loads the + /// matching PUP pack and binds the DMD here, and B2S loads the backglass and creates its windows. + /// + /// The game being started and the services scoped to it. + /// Cancelled when the game is cancelled or the startup deadline expires. + /// is called even when this hook throws or is cancelled. Task PrepareGameAsync(PluginGameContext game, CancellationToken cancellation); + /// + /// Starts the plugin's game activity after every plugin has been prepared, so resources published by other plugins + /// during preparation are available. When it completes the plugin is , and cleanup + /// later runs in reverse of the order in which plugins were started. + /// + /// Cancelled when the game is cancelled or the startup deadline expires. Task StartGameAsync(CancellationToken cancellation); + /// + /// Called once when the table's player has started and the game is playable. If that happens while the host is still + /// starting plugins, the call is delivered once startup finishes. Only running plugins receive it. + /// void GameReady(); + /// + /// Called when the game is paused or resumed, for example when the Player opens or closes its in-game menu. If the + /// state changes several times while the host is busy, only the latest state is delivered. Only running plugins + /// receive it. + /// + /// True when the game was paused, false when it resumed. void PauseChanged(bool paused); + /// + /// Called when the application window gains or loses focus. If focus changes several times while the host is busy, + /// only the latest state is delivered. Only running plugins receive it. + /// + /// True when the application has focus. void FocusChanged(bool focused); + /// + /// Ends the plugin's game activity: stop workers and playback and wait until they no longer use game resources. + /// Called when the game stops, or earlier when the plugin faults or is blocked, but only if + /// was called. Afterwards the host disposes . + /// Exceptions are logged and cleanup continues. + /// + /// Cancelled after . Task StopGameAsync(CancellationToken cancellation); + /// + /// Releases what set up. Called after for every created + /// instance, even when initialization failed. Afterwards the host disposes + /// and then the plugin itself. Exceptions are logged and cleanup + /// continues. + /// + /// Cancelled after . Task ShutdownAsync(CancellationToken cancellation); } - public interface IPluginTick { void Tick(double unscaledSeconds); } - public interface IPluginRender { void BeforeRender(); } - public interface IPluginEmergencyStop { void EmergencyStop(); } - /// After emergency stop, completes only when native workers can no longer use the plugin's resources. - public interface IPluginDeferredCleanup { Task Quiescence { get; } } - /// Reconfiguration is transactional: failure or cancellation must leave the previous runtime state intact. - public interface IPluginReconfiguration { Task ApplyAsync(PluginSettingsSnapshot settings, CancellationToken cancellation); } - /// Fast owner-thread updates. Failure must leave the previous runtime state intact. - public interface IPluginSynchronousReconfiguration { PluginActionResult Apply(PluginSettingsSnapshot settings); } - /// Implement only the hooks a plugin needs; the host still controls its lifetime. + /// Optional hook for plugins that need a per-frame update on the owner thread. + /// + /// The Player calls it from its frame update, so it keeps running while the game is paused. B2S advances lamp and reel + /// animations here, and DMD pumps its presentation destinations. Only running plugins are ticked, and a plugin is + /// skipped while its asynchronous live reconfiguration is in progress. + /// + public interface IPluginTick + { + /// Advances the plugin by one frame. + /// Real time since the previous frame in seconds, unaffected by Unity's time scale. + void Tick(double unscaledSeconds); + } + + /// Optional hook for plugins that must update their output after all frame updates and before rendering. + /// + /// The Player calls it from LateUpdate, once per frame. B2S composes the backglass and updates its windows and + /// surfaces here, and PinUP renders its screen outputs. Only running plugins receive it. + /// + public interface IPluginRender + { + /// Updates the plugin's visual output for the coming frame. Called on the owner thread. + void BeforeRender(); + } + + /// Optional hook for stopping a plugin immediately, without awaiting anything. + /// + /// The host calls it when there is no time for an orderly stop: when the application quits, when stopping the game + /// cannot wait for a pending operation within , and when one of the plugin's + /// hooks misses its deadline. Normal and + /// may not run afterwards, but still does. + /// + public interface IPluginEmergencyStop + { + /// + /// Stops playback, workers and device output synchronously on the owner thread. Must not block or throw, and must be + /// safe to call more than once and after the plugin has already stopped. Pair it with + /// when native workers need time to finish. + /// + void EmergencyStop(); + } + + /// + /// Optional hook for plugins whose native workers may still use the plugin's resources after an emergency stop. + /// + /// + /// After , or after abandoning an unresponsive hook, the host reads + /// . Until it completes the host keeps the instance's resource scopes (native handles, device + /// leases) alive and marks the plugin , so no new instance can start against + /// the same hardware. The DMD plugin returns the completion of its native bridge shutdown here, PinUP that of its + /// media player. + /// + public interface IPluginDeferredCleanup + { + /// + /// Completes when native workers can no longer use the plugin's resources. Read after an emergency stop; return a + /// completed task when nothing is pending. A fault is logged and treated as completion. + /// + Task Quiescence { get; } + } + + /// + /// Optional hook for applying settings to a running plugin asynchronously. + /// + /// + /// When the user commits settings that change a live field for the running game, the host calls this hook before saving. + /// If the plugin also implements , that one is used instead. While the + /// call runs the plugin is skipped for tick, render and other dispatched calls. If saving fails afterwards, the host + /// calls it again with the previous settings to roll back. + /// + public interface IPluginReconfiguration + { + /// + /// Applies new settings. Reconfiguration is transactional: on failure or cancellation the previous runtime state must + /// remain intact. Called on the owner thread. + /// + /// + /// The plugin's current runtime settings with the changed live fields replaced. Fields that are not live keep their + /// values from game start. Keep this snapshot as the plugin's current settings. + /// + /// Cancelled by the caller or after . + /// + /// Success, or a failure whose the Player shows; on failure nothing is saved. + /// + Task ApplyAsync(PluginSettingsSnapshot settings, CancellationToken cancellation); + } + + /// + /// Optional hook for applying settings to a running plugin synchronously. + /// + /// + /// Preferred over when a plugin implements both. Use it for fast updates such as a + /// volume change: PinMAME sets the engine's output gain here, so the Player's volume mixer takes effect immediately. + /// + /// + /// public PluginActionResult Apply(PluginSettingsSnapshot settings) + /// { + /// _application.Scheduler.VerifyOwner(); + /// if (_engine != null) _engine.OutputGain = Mathf.Clamp(settings.Integer("Volume", 100), 0, 100) / 100f; + /// return new PluginActionResult(true, ""); + /// } + /// + /// + /// + public interface IPluginSynchronousReconfiguration + { + /// + /// Applies new settings on the owner thread. On failure the previous runtime state must remain intact. If saving fails + /// afterwards, the host calls it again with the previous settings to roll back. + /// + /// + /// The plugin's current runtime settings with the changed live fields replaced. Keep this snapshot as the plugin's + /// current settings. + /// + /// + /// Success, or a failure whose the Player shows; on failure nothing is saved. + /// + PluginActionResult Apply(PluginSettingsSnapshot settings); + } + + /// + /// Base class for plugins with no-op implementations of every hook, so a plugin overrides + /// only what it needs. The host still controls its lifetime. + /// + /// + /// All bundled plugins derive from it and add optional interfaces such as or + /// . + /// + /// A plugin that applies a volume setting to the table's PinMAME engine: + /// + /// internal sealed class VolumePlugin : PlayerPlugin, IPluginSynchronousReconfiguration + /// { + /// private readonly PluginApplicationContext _application; + /// private PinMameGamelogicEngine? _engine; + /// public VolumePlugin(PluginApplicationContext application) => _application = application; + /// + /// public override Task PrepareGameAsync(PluginGameContext game, CancellationToken cancellation) + /// { + /// var scene = game.Capabilities.Require<IUnityGameScene>(UnityPluginCapability.GameScene); + /// _engine = scene.Table.GetComponentInChildren<PinMameGamelogicEngine>(true); + /// Apply(game.Settings); + /// return Task.CompletedTask; + /// } + /// + /// public PluginActionResult Apply(PluginSettingsSnapshot settings) + /// { + /// if (_engine != null) _engine.OutputGain = settings.Integer("Volume", 100) / 100f; + /// return new PluginActionResult(true, ""); + /// } + /// } + /// + /// + /// public abstract class PlayerPlugin : IPlayerPlugin { + /// + /// Does nothing by default. public virtual Task InitializeAsync(CancellationToken cancellation) => Task.CompletedTask; + /// + /// Does nothing by default. public virtual Task PrepareGameAsync(PluginGameContext game, CancellationToken cancellation) => Task.CompletedTask; + /// + /// Does nothing by default. public virtual Task StartGameAsync(CancellationToken cancellation) => Task.CompletedTask; + /// + /// Does nothing by default. public virtual void GameReady() { } + /// + /// Does nothing by default. public virtual void PauseChanged(bool paused) { } + /// + /// Does nothing by default. public virtual void FocusChanged(bool focused) { } + /// + /// Does nothing by default. public virtual Task StopGameAsync(CancellationToken cancellation) => Task.CompletedTask; + /// + /// Does nothing by default. public virtual Task ShutdownAsync(CancellationToken cancellation) => Task.CompletedTask; + /// + /// Releases whatever the plugin still holds. The host calls it last, on the owner thread, also after an emergency + /// stop where and did not run, so it must release + /// everything and tolerate an already stopped plugin. Does nothing by default. + /// public virtual void Dispose() { } } + /// Host services handed to a plugin instance when it is created, valid for the instance's whole life. + /// + /// The host builds it and passes it to ; plugins keep it in a field. Since the + /// host creates a new instance for every game, "application" here means the instance's life, from creation until + /// completes; game-specific services come with + /// . + /// public sealed class PluginApplicationContext { + /// This plugin's descriptor, for example to pass as a resource owner. public PluginDescriptor Plugin { get; } + /// + /// The settings store. Plugins can use for their own files; the + /// DMD plugin stages colorization caches there. + /// public PluginConfigurationStore Configuration { get; } + /// + /// The owner-thread scheduler. Await to return to the owner thread after worker + /// code, to queue work there, and to assert + /// that a call is on it. The host pumps it at the start of every , which the Player calls + /// every frame. + /// public OwnerScheduler Scheduler { get; } + /// + /// Versioned services published by the host and by other plugins. Same instance as + /// . + /// public PluginCapabilities Capabilities { get; } + /// + /// The registry for binding and publishing displays, controllers, surfaces and other resources. Same instance as + /// . + /// public PluginResourceRegistry Resources { get; } + /// + /// Resources owned for the instance's whole life. The host disposes it after + /// and before the plugin itself, in reverse order of registration; for game-specific resources use + /// . + /// public ResourceScope Lifetime { get; } + /// + /// Writes a diagnostic line, prefixed with the plugin ID. The Player sends it to the Unity log; use it for + /// conditions the user may need to see, such as a missing PUP pack. + /// public Action Log { get; } + /// Creates the context. Called by the ; plugins only receive it. + /// The descriptor of the plugin being created. + /// The settings store. + /// The owner-thread scheduler. + /// The shared capability registry. + /// The shared resource registry. + /// The scope disposed after the instance shuts down. + /// The diagnostic sink for this plugin. public PluginApplicationContext(PluginDescriptor plugin, PluginConfigurationStore configuration, OwnerScheduler scheduler, PluginCapabilities capabilities, PluginResourceRegistry resources, ResourceScope lifetime, Action log) { Plugin = plugin; Configuration = configuration; Scheduler = scheduler; Capabilities = capabilities; Resources = resources; Lifetime = lifetime; Log = log; } } + /// The game being started and the services scoped to it, passed to . + /// + /// It tells the plugin which game runs (, , ), how + /// the plugin is configured for it (), and how to find the game's resources + /// (, ). Keep it in a field if later hooks need it; it stays valid until + /// completes. + /// + /// Binding the game's DMD, as PinUP does: + /// + /// var selection = new PluginResourceSelection(ResourceKind.Display) { + /// Controller = game.ControllerId, Role = "dmd", Session = game.Session + /// }; + /// var dmd = game.Lifetime.Own(game.Resources.Bind<IPluginDisplaySource>(selection)); + /// + /// + /// public sealed class PluginGameContext { + /// The game being played, used to find matching media such as a PUP pack, backglass or colorization. public GameIdentity Identity { get; } + /// Full path of the table package being played. The B2S plugin uses it, with the title, to match a backglass. public string PackagePath { get; } + /// The table's title as shown in the Player. public string Title { get; } + /// + /// ID of the game's controller, such as pinmame:t2_l8/controller/0. Select resources of this controller with + /// it, and use it as the prefix of IDs of resources the plugin publishes for this game. + /// public string ControllerId { get; } + /// + /// Number of the game session, increasing with every game the Player starts. Pass it in resource selections and + /// descriptors so they never match resources of a previous game. + /// public long Session { get; } + /// + /// The plugin's effective settings for this game when it was prepared: defaults, global values and the game's + /// overrides merged. It does not change afterwards; live changes arrive through + /// or . + /// public PluginSettingsSnapshot Settings { get; } + /// + /// Versioned services, including those the Player publishes for the game such as the table scene. Same instance as + /// . + /// public PluginCapabilities Capabilities { get; } + /// + /// The registry for binding and publishing resources. Same instance as + /// . + /// public PluginResourceRegistry Resources { get; } + /// + /// Resources owned for this game, such as bindings, subscriptions and created game objects. The host disposes it + /// right after , in reverse order of registration. + /// public ResourceScope Lifetime { get; } + /// + /// Cancelled when the game stops or its startup is cancelled. Use it for background work tied to the game; it is + /// not the per-hook deadline token. + /// public CancellationToken Cancellation { get; } + /// Creates the context. Called by the ; plugins only receive it. + /// The game being played. + /// Full path of the table package. + /// The table's title. + /// ID of the game's controller. + /// The game session number. + /// The plugin's effective settings for the game. + /// The shared capability registry. + /// The shared resource registry. + /// The scope disposed after the game stops. + /// The game's cancellation token. public PluginGameContext(GameIdentity identity, string packagePath, string title, string controllerId, long session, PluginSettingsSnapshot settings, PluginCapabilities capabilities, PluginResourceRegistry resources, ResourceScope lifetime, CancellationToken cancellation) { Identity = identity; PackagePath = packagePath; Title = title; ControllerId = controllerId; Session = session; Settings = settings; Capabilities = capabilities; Resources = resources; Lifetime = lifetime; Cancellation = cancellation; } } + /// Global list of installed plugin factories, which the Player hands to its . + /// + /// Integration packages register their factory once at startup, from a + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] method. The Player creates its + /// host from afterwards, and the host copies the list, so later registrations are not seen by + /// an existing host. The registry is not thread-safe: register on the main thread. + /// public static class PlayerPluginRegistry { private static readonly Dictionary Factories = new Dictionary(StringComparer.Ordinal); + /// The registered factories, as a live view in no particular order. public static IReadOnlyCollection Installed => Factories.Values; + /// + /// Removes all registered factories. VPE calls this when the scripting domain starts, so factories registered + /// by a previous play session in the editor do not linger; plugins have no reason to call it. + /// public static void Reset() => Factories.Clear(); + /// Adds a factory under its . + /// The factory to install. + /// + /// + /// + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + /// private static void Register() => PlayerPluginRegistry.Register(new PinUpPluginFactory()); + /// + /// + /// + /// + /// The factory's differs from , + /// or a factory with the same ID is already registered. + /// public static void Register(IPlayerPluginFactory factory) { if (factory.Descriptor.ApiVersion != PluginDescriptor.HostApiVersion) throw new InvalidOperationException("Plugin API version is incompatible: " + factory.Descriptor.Id); diff --git a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs index c31dc943d..ad12f0acb 100644 --- a/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs +++ b/VisualPinball.Plugins/Configuration/PluginConfigurationStore.cs @@ -18,14 +18,72 @@ namespace VisualPinball.Plugins { /// One plugin owns one folder; game overrides contain only explicitly changed values. + /// + /// The store keeps every plugin's saved settings and enablement as JSON files below : + /// + /// <plugin id>/settings.json: the global values. + /// <plugin id>/games/<hash>.json: one game's overrides, named by of the + /// . + /// <plugin id>/policy.json: whether the plugin is enabled, globally and per game. + /// + /// Each settings file holds schemaVersion, values and, for game files, the game it belongs to. Values the + /// current schema does not know are kept, so a newer plugin version can still find them. + /// + /// The Player creates one store at <user config directory>/plugins and hands it to every plugin as + /// . The host and the settings page read the effective values from it before a + /// game starts and write edits back through it. Plugins rarely need more than : the B2S plugin and the DMD + /// plugin keep their decoded caches under their own folder. + /// + /// + /// Reading is lock-free and may happen on any thread. Writes are atomic (a temporary file replaces the old one), and + /// and are serialized per store instance. Because each call touches the file system, the + /// host and the Player call these methods on worker threads. + /// + /// + /// A plugin keeping a cache next to its settings: + /// + /// string cache = Path.Combine(context.Configuration.DirectoryFor(context.Plugin), "cache", "pictures"); + /// + /// + /// public sealed class PluginConfigurationStore { private readonly object _writes = new object(); + /// The absolute directory that holds one folder per plugin. public string Root { get; } + /// Creates a store rooted at a directory. Nothing is created on disk until the first write. + /// The configuration directory, made absolute with . public PluginConfigurationStore(string root) { Root = Path.GetFullPath(root); } + /// + /// The folder that belongs to a plugin, Root/<plugin id>. It may not exist yet. Plugins may keep their own files in + /// it, such as caches, as long as they avoid settings.json, policy.json and games/. + /// + /// The plugin whose folder to return. public string DirectoryFor(PluginDescriptor plugin) => Path.Combine(Root, plugin.Id); + /// + /// The path of a plugin's global settings file, or of one game's override file when is given. The file may + /// not exist yet. + /// + /// The plugin whose settings file to return. + /// The game whose overrides to locate, or null for the global settings. public string SettingsPath(PluginDescriptor plugin, GameIdentity? game = null) => game.HasValue ? Path.Combine(DirectoryFor(plugin), "games", Hash(game.Value.Id) + ".json") : Path.Combine(DirectoryFor(plugin), "settings.json"); + /// + /// Reads one stored layer as it is saved: the global values, or only the overrides of one game. Defaults and the other layer are + /// not applied; use for the values a plugin runs with. + /// + /// + /// A missing file returns an empty snapshot at the schema's version. Stored values are upgraded with + /// ; a file written by a newer schema is returned unchanged with its newer + /// , which callers can compare with . + /// + /// The plugin whose settings to read. + /// The plugin's schema, used to upgrade older files. + /// The game whose overrides to read, or null for the global values. + /// A new, detached snapshot the caller may modify. + /// The game file names a different game. + /// The file is not valid JSON. + /// An upgrade step is missing or failed. public PluginSettingsSnapshot Read(PluginDescriptor plugin, PluginSettingsSchema schema, GameIdentity? game = null) { var document = ReadDocument(SettingsPath(plugin, game)); @@ -33,12 +91,37 @@ public PluginSettingsSnapshot Read(PluginDescriptor plugin, PluginSettingsSchema throw new InvalidDataException("The saved profile belongs to another game."); return schema.Upgrade(new PluginSettingsSnapshot(document.Value("schemaVersion") ?? schema.Version, (JObject?)document["values"]?.DeepClone() ?? new JObject())); } + /// + /// Reads the values a plugin runs with: schema defaults, then the saved global values, then the game's overrides when + /// is given (see ). + /// + /// + /// The host calls this when it prepares a game and passes the result to the plugin as . + /// The volume mixer calls it without a game to show global values. + /// + /// The plugin whose settings to read. + /// The plugin's schema. + /// The game being played or configured, or null for the global values only. + /// A new, detached snapshot. + /// The game file names a different game. + /// A file is not valid JSON. public PluginSettingsSnapshot Effective(PluginDescriptor plugin, PluginSettingsSchema schema, GameIdentity? game = null) { var global = Read(plugin, schema); return Resolve(schema, global, game.HasValue ? Read(plugin, schema, game) : null); } /// Known fields are restricted to their declared scope; unknown values remain available for compatible upgrades. + /// + /// Combines schema defaults, the global layer and the optional game layer into effective values, without touching the disk. Before + /// merging, fields are removed from the global layer and + /// fields from the game layer, so a stray value in the wrong file has no effect. Later + /// layers replace whole top-level values; missing defaults inside composite and collection records are filled in afterwards. + /// The settings session uses this to preview a draft that is not saved yet. + /// + /// The plugin's schema, which supplies defaults and scopes. + /// The global values, as returned by without a game. + /// The game's overrides, or null to resolve global values only. + /// A new snapshot at . public static PluginSettingsSnapshot Resolve(PluginSettingsSchema schema, PluginSettingsSnapshot global, PluginSettingsSnapshot? game = null) { PluginSettingsSnapshot Scoped(PluginSettingsSnapshot values, bool gameScope) @@ -53,6 +136,23 @@ PluginSettingsSnapshot Scoped(PluginSettingsSnapshot values, bool gameScope) PluginSettingsSchema.FillRecordDefaults(schema.Fields, resolved.Values); return resolved; } + /// + /// Replaces one stored layer with : every field allowed in that layer takes the value from the snapshot, + /// or is removed from the file when the snapshot has none (for a game layer, removing means "inherit the global value"). + /// + /// + /// Fields outside the layer's scope, and , and + /// fields, are never written. Stored values the schema does not know are kept. The values are + /// not validated here; the host and the settings session validate before they save. To change only some fields without + /// overwriting concurrent edits to others, use . + /// + /// The plugin whose settings to write. + /// The plugin's schema. + /// The complete layer to store. + /// The game whose overrides to write, or null for the global values. + /// The file was written by a newer schema version and is left untouched. + /// The game file names a different game. + /// The file could not be written. public void Save(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSettingsSnapshot values, GameIdentity? game = null) { lock (_writes) SaveCore(plugin, schema, values, game); @@ -76,6 +176,19 @@ private void SaveCore(PluginDescriptor plugin, PluginSettingsSchema schema, Plug WriteDocument(path, document); } /// Merges a scoped edit into the latest profile without replacing unrelated fields. + /// + /// Reads the stored layer and copies over only the fields named in that are allowed in that layer and + /// hold a value (not , or ). A + /// changed field that does not contain is removed, which resets it to the inherited value. Nothing is + /// written; the host uses the result to validate an edit before stores it. + /// + /// The plugin whose settings to read. + /// The plugin's schema. + /// The edited values; only the fields in are taken from it. + /// IDs of the top-level fields the user changed. + /// The game whose overrides to merge into, or null for the global values. + /// The stored layer with the edit applied, as a new snapshot. + /// The stored layer was written by a newer schema version. public PluginSettingsSnapshot MergePatch(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSettingsSnapshot values, IReadOnlyCollection changed, GameIdentity? game = null) { var latest = Read(plugin, schema, game); @@ -87,10 +200,37 @@ public PluginSettingsSnapshot MergePatch(PluginDescriptor plugin, PluginSettings } return latest; } + /// + /// Saves only the fields named in into the latest stored layer, leaving every other stored value as it + /// is on disk. The read and the write happen under the store's write lock, so two edits of different fields do not overwrite each + /// other. + /// + /// + /// This is how the Player saves: the settings page saves the fields the user edited, and the volume mixer saves one field. See + /// for which fields are taken. + /// + /// The plugin whose settings to write. + /// The plugin's schema. + /// The edited values. + /// IDs of the top-level fields to save. + /// The game whose overrides to write, or null for the global values. + /// The stored layer was written by a newer schema version. + /// The file could not be written. public void SavePatch(PluginDescriptor plugin, PluginSettingsSchema schema, PluginSettingsSnapshot values, IReadOnlyCollection changed, GameIdentity? game = null) { lock (_writes) SaveCore(plugin, schema, MergePatch(plugin, schema, values, changed, game), game); } + /// + /// Whether the plugin should run, globally or for one game. A plugin with false is always + /// enabled. Otherwise a per-game choice wins, then the global choice, then . + /// + /// + /// The host checks this for every plugin when it selects the plugins for a game; the Player's settings page shows it as the + /// "Enabled by default" or "Enabled for this game" toggle. + /// + /// The plugin to check. + /// The game to check, or null for the global choice. + /// policy.json is not valid JSON. public bool Enabled(PluginDescriptor plugin, GameIdentity? game = null) { if (!plugin.CanDisable) return true; @@ -98,6 +238,18 @@ public bool Enabled(PluginDescriptor plugin, GameIdentity? game = null) if (game.HasValue && policy["games"] is JObject games && games[game.Value.Id]?.Type == JTokenType.Boolean) return games.Value(game.Value.Id); return policy.Value("enabled") ?? plugin.EnabledByDefault; } + /// + /// Saves whether a plugin is enabled, globally or for one game, in its policy.json. The change takes effect on the next game. + /// + /// + /// For a plugin with false nothing is written, and asking to disable it throws. + /// Unlike , this does not take the store's write lock. + /// + /// The plugin to change. + /// True or false to enable or disable; null removes the choice, so the game follows the global choice and the + /// global choice follows . + /// The game to change, or null for the global choice. + /// is false for a plugin that cannot be disabled. public void SetEnabled(PluginDescriptor plugin, bool? enabled, GameIdentity? game = null) { if (!plugin.CanDisable) { @@ -113,6 +265,13 @@ public void SetEnabled(PluginDescriptor plugin, bool? enabled, GameIdentity? gam } else if (enabled.HasValue) policy["enabled"] = enabled.Value; else policy.Remove("enabled"); WriteDocument(path, policy); } + /// + /// The per-game enablement saved for , or null when the game follows the global choice. Use it to tell + /// whether a game overrides the global policy; gives the resulting decision. Does not consider + /// . + /// + /// The plugin to check. + /// The game to check. public bool? EnableOverride(PluginDescriptor plugin, GameIdentity game) => ReadDocument(Path.Combine(DirectoryFor(plugin), "policy.json"))["games"]?[game.Id]?.Value(); internal static JObject ReadDocument(string path) @@ -135,6 +294,12 @@ internal static void WriteDocument(string path, JObject document) } } finally { if (File.Exists(pending)) File.Delete(pending); } } + /// + /// Turns a game identity into a file name: the lowercase hexadecimal SHA-256 of its UTF-8 bytes. Used for the per-game settings + /// files, so any identity yields a safe, fixed-length name. + /// + /// The text to hash, normally a such as pinmame:t2_l8. + /// 64 lowercase hexadecimal characters. public static string Hash(string identity) { using var sha = SHA256.Create(); diff --git a/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs b/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs index 65b305cbc..c5c536558 100644 --- a/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs +++ b/VisualPinball.Plugins/Configuration/PluginGlobalSetting.cs @@ -10,23 +10,126 @@ namespace VisualPinball.Plugins { - public enum PluginFieldPresentation { Input, Slider } - public static class PluginGlobalSections { public const string VolumeMixer = "audio.mixer"; } + /// + /// How the Player draws the editor of a numeric field ( or ). + /// + /// + /// Set through . Other kinds ignore it. Volume fields shown on the global volume mixer use + /// , as the PinMAME and PinUP volume settings do. + /// + public enum PluginFieldPresentation + { + /// A plain number box. This is the default. + Input, + /// + /// A slider with step buttons between and , with the value and + /// printed next to it. Give the field both bounds when you use it. + /// + Slider + } + /// + /// Identifiers of the shared Player pages that plugin fields can appear on through . + /// + /// + /// A shared page collects one kind of setting from every installed plugin, so the user can adjust them in one place instead of + /// visiting each plugin's settings page. + /// + public static class PluginGlobalSections + { + /// + /// The Player's global volume mixer, which lists the table's mechanical sounds next to every plugin's volume. Fields exposed here + /// must be or with finite and + /// (minimum below maximum); skips any other field + /// with a diagnostic. PinMAME's ROM volume and PinUP's media volume appear here. + /// + public const string VolumeMixer = "audio.mixer"; + } + /// + /// Asks the Player to also show a plugin field on a shared page, such as the global volume mixer, in addition to the plugin's own + /// settings page. + /// + /// + /// The value is not copied: the shared page reads and writes the same global value of the same field in the plugin's own + /// configuration (see ). Only the global value is shown there, so the field must not use + /// . Combine it with so that moving the mixer slider + /// is heard immediately in a running game. + /// + /// The PinMAME integration puts its ROM volume on the mixer: + /// + /// new PluginField("Volume", "Volume", SettingKind.Integer) { + /// Default = 100, Minimum = 0, Maximum = 100, Unit = "%", + /// Change = ConfigurationChange.Live, Presentation = PluginFieldPresentation.Slider, + /// GlobalSetting = new PluginGlobalSetting(PluginGlobalSections.VolumeMixer) { Order = 20 } + /// } + /// + /// + /// public sealed class PluginGlobalSetting { + /// The shared page the field appears on, one of the constants. public string Section { get; } + /// + /// Label shown on the shared page. Empty (the default) uses the plugin's , which reads better + /// on the mixer than a generic field label such as "Volume". + /// public string Label { get; set; } = ""; + /// + /// Sort key on the shared page; lower values come first, ties are sorted by . Defaults + /// to 0. PinMAME uses 20 and PinUP 30. + /// public int Order { get; set; } + /// Creates an exposure on the given shared page. + /// One of the constants. + /// is null. public PluginGlobalSetting(string section) => Section = section ?? throw new ArgumentNullException(nameof(section)); } /// An alias identifies a field in its owning factory, never a second copy of its value. + /// + /// One binding connects an entry on a shared Player page to the plugin field that declared a . + /// The Player's volume mixer builds its sliders from , reads each value from the plugin's effective global + /// settings, and writes edits back with , which saves the plugin's global value + /// and, for fields, applies it to the running plugin. Plugins never create bindings; they only + /// declare . + /// public sealed class PluginGlobalSettingBinding { + /// The plugin that owns and stores the field. public IPlayerPluginFactory Factory { get; } + /// The exposed field. Its is never null. public PluginField Field { get; } + /// + /// Stable key of this entry, pluginId/fieldId (for example org.visualpinball.pinup/Volume). Unique across plugins; + /// the Player keys its pending writes and controls by it. + /// public string Id => Factory.Descriptor.Id + "/" + Field.Id; + /// + /// Text shown on the shared page: , or the plugin's + /// when that is empty. + /// public string Label => Field.GlobalSetting!.Label.Length == 0 ? Factory.Descriptor.Name : Field.GlobalSetting.Label; internal PluginGlobalSettingBinding(IPlayerPluginFactory factory, PluginField field) { Factory = factory; Field = field; } + /// + /// Finds every field the given plugins expose on a shared page, in display order. + /// + /// + /// Plugins that do not run on are skipped. A field is included only when it is visible under the + /// plugin's effective global settings (see ). A field with , + /// or a field on that is not a bounded number, is reported to + /// and skipped. A plugin whose saved settings cannot be read is reported and skipped as well, so one + /// broken file does not empty the page. The result is sorted by , then by + /// (ordinal). + /// + /// This reads configuration files, so the Player runs it on a worker thread. + /// + /// + /// The installed plugin factories, for example . + /// The store the plugins' settings are read from. + /// + /// The current platform ID (windows, macos, linux), matched against . + /// + /// The shared page, one of the constants. + /// Optional receiver of messages about skipped plugins and invalid exposures. + /// The bindings to show, possibly empty. public static IReadOnlyList Discover(IEnumerable factories, PluginConfigurationStore store, string platform, string section, Action? diagnostic = null) { var bindings = new List(); diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs b/VisualPinball.Plugins/Configuration/PluginSchema.cs index 6e6266268..8ff15c5a8 100644 --- a/VisualPinball.Plugins/Configuration/PluginSchema.cs +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs @@ -13,58 +13,319 @@ namespace VisualPinball.Plugins { - public enum SettingKind { Boolean, Integer, Number, Text, Folder, File, Choice, Color, Rectangle, Collection, Status, Action, Composite, ScreenLayout, Preview } + /// + /// The type of a : which JSON value it stores, how checks it, and + /// which editor the Player shows for it. + /// + /// + /// , and store nothing: they are never validated or saved. + /// + public enum SettingKind + { + /// A JSON true or false, shown as a toggle. + Boolean, + /// + /// A JSON integer, shown as a number box or, with , a slider. Checked against + /// and . + /// + Integer, + /// + /// A JSON number (integer or floating point), shown like and checked against the same bounds. NaN and + /// infinity are rejected. + /// + Number, + /// A JSON string, shown as a text box. + Text, + /// A directory path stored as a JSON string, shown as a text box with a folder picker. + Folder, + /// A file path stored as a JSON string, shown as a text box with a file picker. + File, + /// + /// The of one item from the catalog named by , stored as a JSON + /// string. The Player asks for the catalog, shows up to five choices as buttons + /// and more as a dropdown, and refuses to apply a selection that the catalog marks unavailable or, when the catalog is + /// , no longer lists. + /// + Choice, + /// + /// A color stored as a JSON string, such as #ff8000 as used by the DMD plugin. Only checked to be text; the Player edits it + /// as text. + /// + Color, + /// + /// A JSON object with numeric x, y, width and height, where width and height are positive. The B2S + /// plugin uses it for the DMD area on a backglass. + /// + Rectangle, + /// + /// A JSON array. With , each item is a record (JSON object) of those fields, validated like a + /// ; without children, the Player edits the items as text. The B2S plugin uses it for its list of + /// backglass folders. + /// + Collection, + /// Read-only text showing the plugin's current runtime status message. Stores nothing. + Status, + /// + /// A button that runs with as the action + /// name, such as PinUP's "Find installed media libraries". Stores nothing; the action may return values for the draft. + /// + Action, + /// A JSON object whose properties are the fields, edited and validated as one record. + Composite, + /// + /// A JSON array of window placements, each an object with a unique screen identifier (string or integer) and integer + /// x, y, width and height in desktop pixels (sizes from 1 to 16384). The Player shows a placement + /// editor that opens a window per screen; the screen identifiers are checked against the catalog named by + /// . PinUP and B2S use it for their output windows. + /// + ScreenLayout, + /// + /// A live preview area. The Player opens it from the plugin's Unity preview provider with as the + /// preview name, such as PinUP's pack preview. Stores nothing. + /// + Preview + } + /// + /// One setting a plugin declares: its stored key, its type, how the Player presents it, where it can be configured and when a + /// change takes effect. + /// + /// + /// Fields are declared once, in the plugin factory's . The Player builds the plugin's + /// settings page from them, the store saves their values under , and the plugin reads the effective values from + /// when a game is prepared. Set the optional properties in an object initializer and do + /// not change a field after the schema is handed to the host. + /// + /// Fields from the B2S plugin: a game-only choice, a folder list, and two integers that share a row and only show for one mode: + /// + /// new PluginField("Backglass", "Backglass", SettingKind.Choice) { + /// Default = "auto", Scope = ConfigurationScope.GameOnly, Provider = "backglasses" + /// }, + /// new PluginField("Folders", "Backglass folders", SettingKind.Collection) { + /// Default = new JArray(), FullWidth = true, + /// Children = new[] { new PluginField("Path", "Folder", SettingKind.Folder) { Default = "" } } + /// }, + /// new PluginField("FadeOn", "Fade in (ms)", SettingKind.Integer) { + /// Default = 20, Minimum = 0, Maximum = 1000, Row = "fade", VisibleWhen = "LampResponse", VisibleValue = "smooth" + /// }, + /// new PluginField("FadeOff", "Fade out (ms)", SettingKind.Integer) { + /// Default = 40, Minimum = 0, Maximum = 1000, Row = "fade", VisibleWhen = "LampResponse", VisibleValue = "smooth" + /// }, + /// + /// + /// public sealed class PluginField { + /// + /// The key the value is stored under. Unique among a schema's top-level fields, or among the of one field. + /// Keep it stable: renaming it loses saved values unless a step moves them. + /// public string Id { get; } + /// The name shown next to the editor; for an field, the button text. public string Label { get; } + /// The value type and editor of the field. public SettingKind Kind { get; } + /// Help text shown below the editor. Empty (the default) shows none. public string Description { get; set; } = ""; /// Places the label above the editor for long values; ordinary fields render inline. + /// + /// Collection, composite, screen layout and preview fields always use the full width. Paths are a typical use, such as PinUP's + /// PUPVideos library folder. + /// public bool FullWidth { get; set; } /// Consecutive fields with the same non-empty hint share a row. The Player owns their sizing and appearance. + /// The B2S plugin puts its fade-in and fade-out times on one row with Row = "fade". public string Row { get; set; } = ""; + /// + /// The editor style for and fields. Defaults to a number box. + /// public PluginFieldPresentation Presentation { get; set; } + /// + /// Unit text appended to the value shown by a , such as %. Empty by default. + /// Put units of other fields in the . + /// public string Unit { get; set; } = ""; /// Opt-in appearance on a shared Player page; storage still belongs to this plugin. + /// Null (the default) shows the field only on the plugin's own page. See . public PluginGlobalSetting? GlobalSetting { get; set; } + /// + /// Where the field can be set: only globally (the default), only per game, or globally with optional per-game overrides. The store + /// ignores a value saved in a layer the scope does not allow. + /// public ConfigurationScope Scope { get; set; } = ConfigurationScope.GlobalOnly; + /// + /// When an applied edit reaches the plugin. Defaults to : the edit is saved and the + /// plugin sees it in when the next game is prepared. With + /// , when the plugin is running the game the edit applies to, the host first passes the new + /// values to it through or and only saves them + /// if that succeeds. + /// public ConfigurationChange Change { get; set; } = ConfigurationChange.NextGame; + /// + /// The value used when nothing is saved, in the JSON shape of . Null means no default: the value is absent and + /// the typed accessors return their fallback. Defaults of fill missing + /// properties inside composite and collection records. + /// public JToken? Default { get; set; } + /// + /// Inclusive lower bound for and values, or null for none. Also + /// the low end of a slider. + /// public double? Minimum { get; set; } + /// + /// Inclusive upper bound for and values, or null for none. Also + /// the high end of a slider. + /// public double? Maximum { get; set; } + /// + /// The name the Player passes to the plugin's providers for this field: the catalog name for + /// ( and ), + /// the action name for (), or the preview name + /// (). Empty for other kinds. The names are private to the plugin, such as PinUP's + /// packs and presets catalogs. + /// public string Provider { get; set; } = ""; + /// + /// The of another field that controls whether this one is shown and validated; empty (the default) always shows + /// it. Inside a composite or collection record it names a sibling in the same record. A hidden field keeps its saved value. + /// See . + /// public string VisibleWhen { get; set; } = ""; + /// + /// The value must hold for this field to show, compared with . + /// Null (the default) means true, which suits a boolean toggle. + /// public JToken? VisibleValue { get; set; } + /// + /// The fields of one record, for and fields. Empty by + /// default. Inside records the Player uses plain editors (no sliders, screen layouts or previews), and , + /// and only apply to top-level fields. + /// public IReadOnlyList Children { get; set; } = Array.Empty(); + /// + /// Optional extra check of this field's value. It receives the raw value (null when absent) and returns an error message to show, + /// or null when the value is valid. When it returns a message, the built-in checks for are skipped. + /// + /// + /// It runs during every , including on the Player's main thread, so keep it fast and + /// free of side effects. Checks that need other fields or the file system belong in . + /// + /// + /// new PluginField("Name", "Name", SettingKind.Text) { + /// Validate = value => value?.ToString().Length > 32 ? "Use at most 32 characters." : null + /// } + /// + /// + /// public Func? Validate { get; set; } + /// Creates a field. Set the optional properties with an object initializer. + /// The stable storage key; see . + /// The name shown to the user. + /// The value type and editor. public PluginField(string id, string label, SettingKind kind) { Id = id; Label = label; Kind = kind; } + /// + /// Whether the field is shown and validated under the given values: true when is empty or the controlling + /// field's value equals (or true). A controlling field without a value hides the field. + /// + /// The values to test, normally the effective settings or, inside a record, that record's values. public bool Visible(PluginSettingsSnapshot snapshot) => VisibleWhen.Length == 0 || JToken.DeepEquals(snapshot.Value(VisibleWhen), VisibleValue ?? new JValue(true)); } + /// A titled section of a plugin's settings page that groups related fields. + /// + /// Groups only affect presentation; field IDs must be unique across all groups of a schema. The Player hides a group when none of + /// its fields is visible in the scope being edited, for example a group of global-only fields while a game is selected. + /// public sealed class PluginSettingsGroup { + /// The section heading. public string Title { get; } + /// Help text shown below the heading, or empty for none. public string Description { get; } + /// The fields in display order. public IReadOnlyList Fields { get; } + /// Creates a group. + /// The section heading. + /// Help text below the heading; pass an empty string for none. + /// The fields in display order. The array is used as is, not copied. public PluginSettingsGroup(string title, string description, params PluginField[] fields) { Title = title; Description = description; Fields = fields; } } + /// A problem found in a settings value, reported to the user before settings are applied or a game starts. + /// + /// Returned by and . When the user applies + /// settings, issues block saving and are shown as field: message; when the host prepares a game, issues keep the plugin from + /// starting for that game. + /// public readonly struct PluginValidationIssue { + /// + /// The of the offending field. Choice checks inside records use a path such as + /// AnimationSlowdowns[0].Animation. + /// public string Field { get; } + /// A short message for the user that says what to fix. public string Message { get; } + /// Creates an issue. + /// The ID of the offending field. + /// The message shown to the user. public PluginValidationIssue(string field, string message) { Field = field; Message = message; } } + /// + /// The complete description of a plugin's settings: its fields, grouped for the settings page, and the version of their stored + /// layout. + /// + /// + /// Every plugin factory exposes one schema as . The Player builds the plugin's settings + /// page from it, uses it to resolve defaults and scopes and to upgrade old files, and the host + /// validates the effective values with it before the plugin starts a game. Build it once, typically in a static method of the + /// factory, and do not change it afterwards. + /// + /// When the stored layout changes (a field is renamed, split or changes its JSON shape), increase and add a + /// step from the previous version. Adding a field with a default needs no new version. + /// + /// + /// A small schema, as the PinMAME integration declares it: + /// + /// public PluginSettingsSchema Schema { get; } = new PluginSettingsSchema(1, + /// new PluginSettingsGroup("ROM audio", "Controls PinMAME playback without changing ROM settings.", + /// new PluginField("Volume", "Volume", SettingKind.Integer) { + /// Default = 100, Minimum = 0, Maximum = 100, Unit = "%", Change = ConfigurationChange.Live, + /// Presentation = PluginFieldPresentation.Slider, + /// GlobalSetting = new PluginGlobalSetting(PluginGlobalSections.VolumeMixer) { Order = 20 } + /// })); + /// + /// + /// public sealed class PluginSettingsSchema { + /// The version of the stored layout, 1 or higher. Saved with every settings file and used to pick migrations. public int Version { get; } + /// The groups of the settings page, in display order. public IReadOnlyList Groups { get; } /// Each entry upgrades a detached values tree from its key to the next version. + /// + /// The entry for key n turns values saved by version n into values for version n + 1. It may modify and + /// return the tree it receives, which is a copy, and must not return null. Upgrades happen when settings are read; the upgraded + /// values are only written back when the user next saves. Empty by default. + /// + /// Renaming a field in version 2: + /// + /// Migrations = new Dictionary<int, Func<JObject, JObject>> { + /// [1] = values => { values["new"] = values["old"]; values.Remove("old"); return values; } + /// } + /// + /// + /// public IReadOnlyDictionary> Migrations { get; set; } = new Dictionary>(); + /// All top-level fields of all groups, in display order. Children of composite and collection fields are not included. public IEnumerable Fields => Groups.SelectMany(group => group.Fields); + /// Creates a schema. + /// The version of the stored layout; start at 1. + /// The groups of the settings page, in display order. The array is used as is, not copied. + /// is less than 1. + /// Two top-level fields share an . public PluginSettingsSchema(int version, params PluginSettingsGroup[] groups) { if (version < 1) throw new ArgumentOutOfRangeException(nameof(version)); @@ -73,6 +334,17 @@ public PluginSettingsSchema(int version, params PluginSettingsGroup[] groups) var seen = new HashSet(StringComparer.Ordinal); foreach (var field in Fields) if (!seen.Add(field.Id)) throw new ArgumentException("Duplicate plugin field " + field.Id + "."); } + /// + /// Brings values saved by an older version up to by running each step in turn. + /// + /// + /// The input snapshot is not modified. Values from the current version are returned as a copy. Values from a newer version are + /// returned unchanged with their newer ; the store then refuses to overwrite + /// them and the host refuses to start the plugin with them. + /// + /// The saved values and the version they were written with. + /// A new snapshot. + /// A step needed for the upgrade is missing or returned null. public PluginSettingsSnapshot Upgrade(PluginSettingsSnapshot snapshot) { var values = (JObject)snapshot.Values.DeepClone(); @@ -82,6 +354,11 @@ public PluginSettingsSnapshot Upgrade(PluginSettingsSnapshot snapshot) } return new PluginSettingsSnapshot(Math.Max(snapshot.SchemaVersion, Version), values); } + /// + /// Returns the default values: every field's , plus a record of child defaults for each + /// field. Fields without a default are absent. + /// + /// A new snapshot at . public PluginSettingsSnapshot Defaults() { var values = new JObject(); @@ -101,6 +378,24 @@ internal static void FillRecordDefaults(IEnumerable fields, JObject } } } + /// + /// Checks values against the declared fields and returns every problem found. + /// + /// + /// Only visible fields are checked (see ), and , + /// and fields are skipped. For each field, its + /// runs first; if it reports nothing and a value is present, the value is checked against + /// and, for numbers, and . An + /// absent value is valid. Records of composite and collection fields are checked against their , + /// and their problems are reported as one issue on the parent field. + /// + /// Pass effective values (defaults and layers resolved), as the host does before a game starts and the settings session does on + /// apply. Plugin-specific checks of choices, files and combinations of fields belong in + /// , which callers run in addition. + /// + /// + /// The values to check. + /// The issues found; empty when the values are valid. public IReadOnlyList Validate(PluginSettingsSnapshot snapshot) { var issues = new List(); @@ -162,18 +457,85 @@ public IReadOnlyList Validate(PluginSettingsSnapshot snap } } + /// + /// A set of settings values keyed by , together with the schema version they follow. + /// + /// + /// Snapshots are what plugins read their configuration from: holds the effective values for + /// the game being prepared, and and receive + /// new ones for live changes. The same type also carries a single stored layer, a draft on the settings page, or the values an + /// action returns. Read values with the typed accessors, which never throw and return the fallback when the value is absent or of + /// another JSON type. + /// + /// A snapshot is a mutable JSON tree and is not thread-safe. Treat snapshots you are handed as read-only and use + /// before changing one. + /// + /// + /// Reading settings in a plugin, with keys from PinUP's schema: + /// + /// public override Task PrepareGameAsync(PluginGameContext game, CancellationToken cancellation) + /// { + /// string root = game.Settings.String("VideosRoot"); + /// int volume = game.Settings.Integer("Volume", 100); + /// bool hardware = game.Settings.Boolean("HardwareDecoding", true); + /// ... + /// } + /// + /// + /// public sealed class PluginSettingsSnapshot { + /// The the values follow. public int SchemaVersion { get; } + /// + /// The values as a JSON object, one property per field ID. This is the live tree, not a copy. Values the current schema does not + /// know may be present. + /// public JObject Values { get; } + /// Creates a snapshot. + /// The schema version the values follow. + /// The values, used as is (not copied); null starts empty. public PluginSettingsSnapshot(int schemaVersion, JObject? values = null) { SchemaVersion = schemaVersion; Values = values ?? new JObject(); } + /// + /// Returns the raw value of a field, or null when it is absent. The token is part of ; do not modify it. + /// + /// The field ID. public JToken? Value(string id) => Values[id]; + /// Returns a field's value if it is a JSON string, otherwise . + /// The field ID. + /// The value to return when the field is absent or not a string. public string String(string id, string fallback = "") => Value(id)?.Type == JTokenType.String ? Value(id)!.Value() ?? fallback : fallback; + /// Returns a field's value if it is a JSON boolean, otherwise . + /// The field ID. + /// The value to return when the field is absent or not a boolean. public bool Boolean(string id, bool fallback = false) => Value(id)?.Type == JTokenType.Boolean ? Value(id)!.Value() : fallback; + /// + /// Returns a field's value if it is a JSON integer that fits in an , otherwise . + /// Floating-point values are not converted. + /// + /// The field ID. + /// The value to return when the field is absent, not an integer or out of range. public int Integer(string id, int fallback = 0) => Value(id)?.Type == JTokenType.Integer && int.TryParse(Value(id)!.ToString(), System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int number) ? number : fallback; + /// Returns a field's value if it is a JSON integer or floating-point number, otherwise . + /// The field ID. + /// The value to return when the field is absent or not a number. public double Number(string id, double fallback = 0) => Value(id)?.Type == JTokenType.Integer || Value(id)?.Type == JTokenType.Float ? Value(id)!.Value() : fallback; + /// Returns a deep copy that can be changed without affecting this snapshot. public PluginSettingsSnapshot Copy() => new PluginSettingsSnapshot(SchemaVersion, (JObject)Values.DeepClone()); + /// + /// Stores a copy of for a field, or removes the field when is null. In a game + /// layer, removing a field means it inherits the global value; in a global layer, the default. + /// + /// The field ID. + /// The new value, or null to remove it. public void Set(string id, JToken? value) { if (value == null) Values.Remove(id); else Values[id] = value.DeepClone(); } + /// + /// Combines layers into a new snapshot: a copy of , with each top-level value of each layer replacing + /// the one before it. Values are replaced whole, not merged property by property. + /// + /// The base values; its becomes the result's. + /// The layers to apply in order, such as the global values and then a game's overrides. + /// A new snapshot; the inputs are not modified. public static PluginSettingsSnapshot Merge(PluginSettingsSnapshot defaults, params PluginSettingsSnapshot[] layers) { var merged = defaults.Copy(); @@ -182,60 +544,210 @@ public static PluginSettingsSnapshot Merge(PluginSettingsSnapshot defaults, para } } + /// One item a field can select, returned in a . + /// + /// Only the is saved, so it must stay stable while labels may change. PinUP lists one choice per pack folder, B2S + /// one per .directb2s file it found, and one per display of the running game. + /// public sealed class PluginChoice { + /// The value stored in the field when this item is selected. public string Id { get; } + /// The text shown in the picker. When several choices share a label, the Player appends their IDs. public string Label { get; } + /// Optional longer text, shown as a tooltip. public string Description { get; } + /// + /// False for an item that exists but cannot be used now. The Player does not offer unavailable items, and applying settings that + /// select one fails with . + /// public bool Available { get; } + /// Why the item cannot be used, shown to the user when a saved selection points to it. Empty for available items. public string UnavailableReason { get; } + /// Creates a choice. + /// The stable value to store. + /// The text shown in the picker. + /// Optional tooltip text. + /// False when the item cannot be selected now. + /// Why it cannot be selected, when is false. public PluginChoice(string id, string label, string description = "", bool available = true, string unavailableReason = "") { Id = id; Label = label; Description = description; Available = available; UnavailableReason = unavailableReason; } } /// The table package a game-scoped request is about, for catalogs that look next to it. + /// + /// The B2S plugin, for example, looks for a .directb2s file next to the package and named after the table file or its + /// folder, and compares the title with file names. + /// public sealed class PluginPackageReference { /// Full path of the table package. public string Path { get; } + /// The table's display title, or empty when unknown. public string Title { get; } + /// Creates a reference. Null arguments become empty strings. + /// Full path of the table package. + /// The table's display title. public PluginPackageReference(string path, string title) { Path = path ?? ""; Title = title ?? ""; } } + /// + /// The context passed to an : which game and table are being configured, the settings to + /// work with, the platform and the resources that are currently published. + /// + /// + /// The settings session builds a request for every catalog lookup, action and validation; the host builds one to validate a + /// plugin's settings before a game starts. Providers only read it. + /// public sealed class PluginChoiceRequest { + /// The game being configured, or null when global settings are edited. public GameIdentity? Game { get; } /// The selected table package, when the request concerns one; null for global settings. public PluginPackageReference? Package { get; set; } + /// + /// The effective settings to work with, including unsaved edits from the settings page. Use these, not the saved values, so that + /// catalogs follow what the user just changed (for example PinUP lists the packs of the library folder in the draft). + /// public PluginSettingsSnapshot Settings { get; } + /// The platform ID: windows, macos or linux in the Player. public string Platform { get; } + /// + /// The resources published in the plugin resource registry at the time of the request, such as the displays of a running game. + /// Filled for catalog lookups and the check made when settings are applied; empty for actions, for validation and when nothing + /// runs. Never null. + /// public IReadOnlyList Resources { get; } + /// Creates a request. + /// The game being configured, or null for global settings. + /// The effective settings to work with. + /// The platform ID. + /// The currently published resources, or null for none. public PluginChoiceRequest(GameIdentity? game, PluginSettingsSnapshot settings, string platform, IReadOnlyList? resources = null) { Game = game; Settings = settings; Platform = platform; Resources = resources ?? Array.Empty(); } } + /// The items a or field can select right now. + /// + /// Returned by . The Player fills the picker from it, and the settings session + /// checks each saved selection against it before applying settings. + /// public sealed class PluginChoiceCatalog { + /// + /// An opaque string that changes when the list changes. Providers in this codebase join the choice IDs with |. The host does + /// not interpret it. + /// public string Revision { get; } + /// The items in display order, including unavailable ones. public IReadOnlyList Choices { get; } /// False when a runtime-only catalog cannot be inspected while its game is stopped. + /// + /// A saved selection missing from an incomplete catalog is kept and not reported as an error. + /// returns an incomplete catalog when the game's controller is not running, because its displays are unknown then. + /// public bool IsComplete { get; } + /// Creates a catalog. + /// An opaque string that identifies this version of the list. + /// The items in display order. + /// False when the list may lack items that exist only while the game runs. public PluginChoiceCatalog(string revision, IReadOnlyList choices, bool isComplete = true) { Revision = revision; Choices = choices; IsComplete = isComplete; } + /// Returns the first item with the given ID, or null when the catalog has none. + /// The to look for. public PluginChoice? Find(string id) => Choices.FirstOrDefault(choice => choice.Id == id); } + /// The outcome of a settings action, a reconfiguration or a settings commit. + /// + /// Plugins return it from , and + /// . The host and the settings session return it when they save settings. + /// public readonly struct PluginActionResult { + /// Whether the operation succeeded. A failed reconfiguration must leave the plugin as it was. public bool Success { get; } + /// A short message for the user, shown on the settings page in both cases. public string Message { get; } + /// + /// For an action: values to put into the settings draft when is true, or null for none. Only fields + /// editable in the current scope are taken, and the user still has to apply them. Ignored in other results. + /// public PluginSettingsSnapshot? Changes { get; } + /// Creates a result. + /// Whether the operation succeeded. + /// A short message for the user. + /// For an action, values to put into the draft; otherwise null. public PluginActionResult(bool success, string message, PluginSettingsSnapshot? changes = null) { Success = success; Message = message; Changes = changes; } } + /// + /// Implemented by plugins whose settings need dynamic content: catalogs for choice fields, buttons that run actions, and checks + /// that the schema alone cannot express. + /// + /// + /// A factory returns it from , or null when the schema is enough (PinMAME's + /// factory returns null). The PinUP and B2S factories implement it themselves and return this. Fields name what they need + /// through ; for example PinUP's Pack field asks for the packs catalog, which lists + /// the pack folders of the configured library. + /// + /// The provider is used while no plugin instance exists, so it must not rely on runtime state other than what the request carries. + /// Calls can arrive on the main thread or on a worker thread: do not touch Unity objects, and move file system work off the + /// calling thread, as PinUP does with Task.Run. + /// + /// + /// + /// public Task<PluginChoiceCatalog> DiscoverAsync(string provider, PluginChoiceRequest request, CancellationToken cancellation) + /// => Task.Run(() => { + /// var choices = new List<PluginChoice> { new PluginChoice("auto", "Automatic") }; + /// string root = request.Settings.String("Library"); + /// if (provider == "items" && Directory.Exists(root)) + /// foreach (string name in Directory.EnumerateDirectories(root).Select(Path.GetFileName)) + /// choices.Add(new PluginChoice(name, name)); + /// return new PluginChoiceCatalog(string.Join("|", choices.Select(choice => choice.Id)), choices); + /// }, cancellation); + /// + /// public Task<PluginActionResult> RunActionAsync(string action, PluginChoiceRequest request, CancellationToken cancellation) + /// => Task.FromResult(new PluginActionResult(false, "Unknown action.")); + /// + /// public IReadOnlyList<PluginValidationIssue> Validate(PluginChoiceRequest request) + /// => Directory.Exists(request.Settings.String("Library")) ? Array.Empty<PluginValidationIssue>() + /// : new[] { new PluginValidationIssue("Library", "This folder does not exist.") }; + /// + /// + /// public interface IPluginSettingsProvider { + /// + /// Lists the items for a or field. Called when the settings + /// page shows the field, and again when settings are applied to check the saved selections. + /// + /// The field's . + /// The game, table, draft settings and published resources to list items for. + /// + /// Cancelled when the page is refreshed or closed or the apply is cancelled; stop work and throw + /// . + /// + /// The catalog; mark it incomplete when items may exist that cannot be seen now. Task DiscoverAsync(string provider, PluginChoiceRequest request, CancellationToken cancellation); + /// + /// Runs the action of a field when the user presses its button, such as PinUP locating an + /// installed LibVLC or B2S rescanning its folders. + /// + /// The field's . + /// The game, table and draft settings; is empty. + /// Cancelled when the page is refreshed or closed. + /// + /// The result to show; on success, may carry values for the draft, which the user then + /// applies. + /// Task RunActionAsync(string action, PluginChoiceRequest request, CancellationToken cancellation); + /// + /// Checks settings beyond what the schema checks, such as folders that must exist or fields that depend on each other. Called + /// with the effective settings when the user applies them and when the host prepares a game; issues block saving and keep the + /// plugin from starting that game. In the Player it runs synchronously on the main thread, so keep it fast. + /// + /// The game, table and effective settings to check. + /// The issues found; empty when the settings are valid. IReadOnlyList Validate(PluginChoiceRequest request); } } diff --git a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs index c8c481a5d..717b1a684 100644 --- a/VisualPinball.Plugins/Resources/DisplayResourceSource.cs +++ b/VisualPinball.Plugins/Resources/DisplayResourceSource.cs @@ -9,7 +9,34 @@ namespace VisualPinball.Plugins { - /// Cached subscriber arrays make steady-state borrowed frame dispatch allocation-free. + /// + /// Ready-made for code that produces display frames: publish it in the + /// , then call for every frame. + /// + /// + /// + /// The Unity integration wraps every DMD of a running gamelogic engine, such as PinMAME, in one of these, and the DMD + /// plugin publishes its colorized output through one as a resource derived from + /// the original. Consumers such as the B2S and PinUP plugins bind to it through . + /// + /// + /// Delivery is synchronous and reuses a cached subscriber array, so publishing a frame allocates nothing once + /// subscriptions are stable. The class does no locking: subscribe, publish and dispose on the owner thread. Stamp every + /// frame with the the source was published with, because + /// drops frames of any other instance. + /// + /// + /// The DMD plugin's presentation output: + /// + /// _source = new DisplayResourceSource(fault: error => application.Log(error.Message)); + /// _publication = application.Resources.Publish(new PluginResourceDescriptor(original.Id + "/presentation/" + application.Plugin.Id, + /// application.Plugin.Id, original.Controller, original.Label + " · DMD presentation", original.Role, ResourceKind.Display, + /// original.Session, _instance, ResourceView.Presentation, DisplayEncoding.Rgb24, original.Id), _source); + /// // each frame, on the owner thread: + /// _source.Publish(new PluginDisplayFrame(pixels, width, height, width * 3, DisplayEncoding.Rgb24, ++_sequence, timestampUs, _instance)); + /// + /// + /// public sealed class DisplayResourceSource : IPluginDisplaySource, IDisposable { private readonly List> _subscribers = new List>(); @@ -17,9 +44,28 @@ public sealed class DisplayResourceSource : IPluginDisplaySource, IDisposable private readonly Func? _negotiate; private readonly Action? _fault; private bool _disposed; + /// Always . public ResourceKind Kind => ResourceKind.Display; + /// Creates a display source. + /// + /// Implements : called with the consumer's intent and accepted formats, returns a lease that + /// holds the producer's output format until disposed, and throws when the producer cannot satisfy the request. The + /// Unity integration passes , which switches the engine to raw Gray2/Gray4 + /// frames while identification consumers are bound. When null, every view request succeeds with a lease that does nothing. + /// + /// + /// Receives exceptions thrown by subscribers, and delivery continues with the next subscriber. When null, a subscriber's + /// exception propagates out of and the remaining subscribers miss that frame. + /// public DisplayResourceSource(Func? negotiate = null, Action? fault = null) { _negotiate = negotiate; _fault = fault; } + /// + /// Adds a frame receiver until the returned handle is disposed. Consumers normally go through a + /// and instead of calling this. + /// + /// Called synchronously for every published frame; the pixels are only valid during the call. + /// A handle that removes the receiver. + /// The source has been disposed. public IDisposable Subscribe(Action receive) { if (_disposed) throw new ObjectDisposedException(nameof(DisplayResourceSource)); @@ -27,42 +73,117 @@ public IDisposable Subscribe(Action receive) _dispatch = _subscribers.ToArray(); return new Subscription(() => { _subscribers.Remove(receive); _dispatch = _subscribers.ToArray(); }); } + /// + /// Asks the producer for a view of the display, delegating to the negotiate function given to the constructor. + /// A binding calls it when it binds and disposes the lease when it unbinds. + /// + /// What the consumer wants, such as for unmodified raw frames. + /// The encodings the consumer can read. + /// A lease that keeps the negotiated format until disposed. + /// Thrown by the negotiator when the producer cannot provide the requested view. public IDisposable AcquireView(ResourceView intent, DisplayEncoding acceptedFormats) => _negotiate?.Invoke(intent, acceptedFormats) ?? new Subscription(() => { }); + /// + /// Delivers a frame synchronously to every subscriber, in subscription order. The frame's pixels may be reused as soon + /// as the call returns. Does nothing after . + /// + /// The frame; its must match the published descriptor. public void Publish(in PluginDisplayFrame frame) { if (_disposed) return; foreach (var receive in _dispatch) try { receive(frame); } catch (Exception error) { if (_fault == null) throw; _fault(error); } } + /// + /// Drops all subscribers and stops delivery; later calls throw. Does not withdraw the + /// publication: dispose the handle returned by as well. + /// public void Dispose() { _disposed = true; _subscribers.Clear(); _dispatch = Array.Empty>(); } } + /// + /// Ready-made for code that produces controller events (switches, coils, lamps, GI): + /// publish it in the , then call for every change. + /// + /// + /// + /// The Unity integration publishes two per running gamelogic engine: a source with + /// the outputs the table maps, which PinUP forwards to its PUP pack, and, when the engine reports them, a + /// source with every output by native number, which the B2S plugin prefers for + /// backglass lamps. + /// + /// + /// Delivery is synchronous and allocation-free once subscriptions are stable. The class does no locking: subscribe, + /// publish and dispose on the owner thread, and post events from other threads with . + /// + /// + /// + /// var source = scope.Own(new ControllerResourceSource(fault)); + /// scope.Own(registry.Publish(new PluginResourceDescriptor(controllerId, provider, controllerId, engine.Name, "controller", + /// ResourceKind.Controller, session, instance), source)); + /// source.Publish(new PluginControllerEvent(ControllerEventKind.Coil, "28", 1, ++sequence, timestampUs)); + /// + /// + /// public sealed class ControllerResourceSource : IPluginControllerSource, IDisposable { private readonly Action? _fault; private readonly List> _subscribers = new List>(); private Action[] _dispatch = Array.Empty>(); private bool _disposed; + /// + /// or , as chosen at construction. Must match the + /// descriptor this source is published with. + /// public ResourceKind Kind { get; } + /// Creates a source of mapped controller events (). + /// + /// Receives exceptions thrown by subscribers, and delivery continues. When null, a subscriber's exception propagates out + /// of . + /// public ControllerResourceSource(Action? fault = null) : this(ResourceKind.Controller, fault) { } - /// for mapped outputs, for native ones. + /// Creates a controller event source of the given kind. + /// + /// for mapped outputs, for native ones. + /// + /// + /// Receives exceptions thrown by subscribers, and delivery continues. When null, a subscriber's exception propagates out + /// of . + /// + /// + /// is neither nor . + /// public ControllerResourceSource(ResourceKind kind, Action? fault = null) { if (kind != ResourceKind.Controller && kind != ResourceKind.Outputs) throw new ArgumentException("A controller source carries controller or output events.", nameof(kind)); Kind = kind; _fault = fault; } + /// + /// Adds an event receiver until the returned handle is disposed. Consumers normally connect through a + /// instead of calling this directly. + /// + /// Called synchronously on the publishing (owner) thread for every event. + /// A handle that removes the receiver. + /// The source has been disposed. public IDisposable Subscribe(Action receive) { if (_disposed) throw new ObjectDisposedException(nameof(ControllerResourceSource)); _subscribers.Add(receive); _dispatch = _subscribers.ToArray(); return new Subscription(() => { _subscribers.Remove(receive); _dispatch = _subscribers.ToArray(); }); } + /// + /// Delivers an event synchronously to every subscriber, in subscription order. Does nothing after . + /// + /// The event. public void Publish(in PluginControllerEvent value) { if (_disposed) return; foreach (var receive in _dispatch) try { receive(value); } catch (Exception error) { if (_fault == null) throw; _fault(error); } } + /// + /// Drops all subscribers and stops delivery; later calls throw. Does not withdraw the + /// publication: dispose the handle returned by as well. + /// public void Dispose() { _disposed = true; _subscribers.Clear(); _dispatch = Array.Empty>(); } } } diff --git a/VisualPinball.Plugins/Resources/PluginResourceChoices.cs b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs index 97fc08724..bd96ebb42 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceChoices.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceChoices.cs @@ -9,9 +9,43 @@ namespace VisualPinball.Plugins { - /// Saved resource choices are relative to a game's controller and can be kept while that game is stopped. + /// + /// Helpers for a settings choice that picks one of the game's displays as a plugin's input, such as "DMD source". The + /// saved value is relative to the game's controller, so the same choice keeps working across sessions and can be kept + /// while the game is stopped. + /// + /// + /// The DMD, PinUP and B2S plugins each declare a choice field whose provider is sources. Their + /// answers that provider with , and their + /// prepare hook turns the saved value into a resource ID with . + /// + /// + /// // settings provider, may run on a worker: + /// if (provider == "sources") return PluginResourceChoices.Displays(request, DisplayEncoding.Gray2 | DisplayEncoding.Gray4); + /// + /// // PrepareGameAsync, owner thread: + /// string source = game.Settings.String("Source", "auto"); + /// if (source != "auto") selection.ResourceId = PluginResourceChoices.Resolve(game.ControllerId, source); + /// + /// + /// public static class PluginResourceChoices { + /// + /// Builds the choice catalog of displays a plugin can read: auto ("Automatic · this game's DMD") followed by every + /// original DMD of the request's game that offers at least one of . + /// + /// + /// Only root resources (no parents) with role dmd, view and the game's + /// controller <game id>/controller/0 are listed; each choice ID is the resource ID relative to that + /// controller, such as display/dmd0. Without a game (global settings) only auto is offered. The catalog is + /// marked incomplete ( false) unless the game's controller is currently + /// published, so a saved display is not rejected as missing while the game is stopped. Pure function of the request; + /// safe on any thread. + /// + /// The settings request; its snapshot supplies the displays. + /// The encodings the plugin can consume; displays offering none of them are left out. + /// The catalog; its revision changes whenever the listed IDs change. public static PluginChoiceCatalog Displays(PluginChoiceRequest request, DisplayEncoding formats) { var automatic = new PluginChoice("auto", "Automatic · this game's DMD"); @@ -22,6 +56,17 @@ public static PluginChoiceCatalog Displays(PluginChoiceRequest request, DisplayE .Select(resource => new PluginChoice(resource.Id.Substring(controller.Length + 1), resource.Label)).Prepend(automatic).ToArray(); return new PluginChoiceCatalog(string.Join("|", choices.Select(choice => choice.Id)), choices, request.Resources.Any(resource => resource.Kind == ResourceKind.Controller && resource.Controller == controller)); } + /// + /// Turns a saved relative display choice into the absolute resource ID to put in + /// . Do not pass auto; leave the selection's ID empty instead. + /// + /// The running game's controller ID, . + /// The saved choice, such as display/dmd0. + /// and joined by a slash. + /// + /// does not start with display/ or contains .., : or a backslash, so a + /// tampered setting cannot point at another game's or another kind of resource. + /// public static string Resolve(string controller, string relative) { if (!relative.StartsWith("display/", StringComparison.Ordinal) || relative.Contains("..") || relative.Contains(":") || relative.Contains("\\")) diff --git a/VisualPinball.Plugins/Runtime/OwnerScheduler.cs b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs index f715dee5c..68729a870 100644 --- a/VisualPinball.Plugins/Runtime/OwnerScheduler.cs +++ b/VisualPinball.Plugins/Runtime/OwnerScheduler.cs @@ -11,31 +11,90 @@ namespace VisualPinball.Plugins { - /// An explicit owner-thread queue. Awaiting Switch returns to this owner without relying on Unity's synchronization context. + /// + /// The plugin host's owner thread: a queue of work that runs on the thread that created the scheduler, which plugins use + /// to get back to that thread after background work. + /// + /// + /// The Player creates one scheduler on Unity's main thread and hands it to every plugin through + /// . Plugin hooks, resource events and Unity calls all belong on this + /// thread. Queued work runs when the host pumps the queue, which does once per frame, so + /// await Switch() resumes on the next frame at the latest. The scheduler does not depend on Unity's + /// synchronization context, so it also works after ConfigureAwait(false) and in tests without Unity. + /// + /// The B2S plugin reads the backglass file on a worker and returns to the owner thread before touching Unity: + /// + /// var backglass = await Task.Run(() => DirectB2SReader.Read(path), cancellation); + /// await _application.Scheduler.Switch(); + /// var view = new B2SBackglassView(backglass, options); // owner thread again + /// + /// + /// public sealed class OwnerScheduler { private readonly int _thread = Thread.CurrentThread.ManagedThreadId; private readonly ConcurrentQueue _pending = new ConcurrentQueue(); + /// + /// True when the calling thread is the owner thread, the thread that constructed this scheduler. Safe to read from any thread. + /// public bool IsOwner => Thread.CurrentThread.ManagedThreadId == _thread; + /// + /// Throws unless called on the owner thread. Plugins call it at the top of callbacks that must not run on a worker, for + /// example the DMD plugin's presentation factory. + /// + /// The calling thread is not the owner thread. public void VerifyOwner() { if (!IsOwner) throw new InvalidOperationException("This plugin operation belongs on the owner thread."); } + /// + /// Queues an action to run on the owner thread during the next . Thread-safe; actions run in the order + /// they were posted. Unlike , the action is always queued, even when called on the owner thread. + /// + /// + /// An exception thrown by the action escapes (and so ), so catch inside + /// the action. GamelogicPluginResources posts controller events raised off the owner thread this way; the DMD plugin + /// posts the release of its native resources once its worker has stopped. + /// + /// The work to run on the owner thread. public void Post(Action action) => _pending.Enqueue(action); + /// + /// Runs queued actions on the owner thread, at most of them, so a flood of posts cannot stall a + /// frame. Actions left over run on the next call. The host calls this from ; plugins do not + /// need to call it. + /// + /// The maximum number of actions to run in this call. + /// The calling thread is not the owner thread. public void Pump(int budget = 4096) { VerifyOwner(); for (int i = 0; i < budget && _pending.TryDequeue(out var action); i++) action(); } + /// + /// Returns an awaitable that continues on the owner thread. On the owner thread it completes immediately; elsewhere the + /// continuation is posted and runs during the next . + /// + /// + /// Use it after every await that may resume on a worker (Task.Run, ConfigureAwait(false)) before + /// touching plugin state, resources or Unity. If nothing pumps the scheduler, the continuation never runs. + /// public OwnerAwaitable Switch() => new OwnerAwaitable(this); + /// The awaitable returned by . Use it only with await. public readonly struct OwnerAwaitable { private readonly OwnerScheduler _scheduler; internal OwnerAwaitable(OwnerScheduler scheduler) { _scheduler = scheduler; } + /// Returns the awaiter the compiler uses for await. public Awaiter GetAwaiter() => new Awaiter(_scheduler); + /// Awaiter that resumes on the scheduler's owner thread. Called by compiler-generated code, not directly. public readonly struct Awaiter : INotifyCompletion { private readonly OwnerScheduler _scheduler; internal Awaiter(OwnerScheduler scheduler) { _scheduler = scheduler; } + /// True on the owner thread, in which case the await continues synchronously without queueing. public bool IsCompleted => _scheduler.IsOwner; + /// Posts the continuation to the owner queue; it runs during the next . + /// The rest of the awaiting method. public void OnCompleted(Action continuation) => _scheduler.Post(continuation); + /// Ends the await, checking that execution really is on the owner thread. + /// The continuation was resumed on another thread. public void GetResult() => _scheduler.VerifyOwner(); } } diff --git a/VisualPinball.Plugins/Runtime/PluginCapabilities.cs b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs index 128b5aa57..3ebca4376 100644 --- a/VisualPinball.Plugins/Runtime/PluginCapabilities.cs +++ b/VisualPinball.Plugins/Runtime/PluginCapabilities.cs @@ -9,18 +9,74 @@ namespace VisualPinball.Plugins { - /// Versioned service contracts, separate from concrete display/audio resources. + /// + /// Directory of shared services, each published under a versioned string ID, through which the Player and plugins hand + /// each other objects. Concrete display, audio and controller streams go through + /// instead. + /// + /// + /// A capability ID names a contract and its version, such as vpe.unity.game-scene.v1, so a provider can publish a + /// new version beside an old one. The Player publishes vpe.events.v1 () and + /// vpe.outputs.v1 () for its lifetime, and the Unity game scene for each game + /// before plugins are prepared. Plugins declare what they consume in and what they + /// offer in ; the host uses those lists and the IDs published here to order and + /// block plugins. + /// Not thread-safe: publish and look up on the owner thread. + /// + /// The DMD plugin takes the output leases while preparing a game: + /// + /// var scene = game.Capabilities.Require<IUnityGameScene>(UnityPluginCapability.GameScene); + /// var outputs = game.Capabilities.Require<PluginOutputLeases>(UnityPluginCapability.Outputs); + /// + /// + /// public sealed class PluginCapabilities { private readonly Dictionary _services = new Dictionary(StringComparer.Ordinal); + /// + /// IDs of the currently published capabilities. The host reads them when a game starts to tell which + /// entries the application already satisfies. + /// public IEnumerable Ids => _services.Keys; + /// + /// Publishes a service under an ID until the returned handle is disposed. Disposing removes the service only if it is + /// still the one registered under that ID; disposing twice does nothing. + /// + /// The contract type consumers look the service up by. + /// Versioned capability ID, compared ordinally. + /// The service instance shared with every consumer. + /// A handle that withdraws the service; put it in a tied to the service's lifetime. + /// A service is already published under . + /// + /// + /// The Player publishes the game scene for one game: + /// + /// _gameScope.Own(Host.Capabilities.Publish<IUnityGameScene>(UnityPluginCapability.GameScene, new UnityGameScene(table, engine))); + /// + /// + /// public IDisposable Publish(string id, T service) where T : class { if (_services.ContainsKey(id)) throw new InvalidOperationException("Capability already provided: " + id); _services.Add(id, service); return new Subscription(() => { if (_services.TryGetValue(id, out var current) && ReferenceEquals(current, service)) _services.Remove(id); }); } + /// + /// Returns the service published under , or null when nothing is published there or the service + /// does not implement . Use it for optional capabilities. + /// + /// The contract type to cast the service to. + /// Versioned capability ID. public T? Find(string id) where T : class => _services.TryGetValue(id, out var value) ? value as T : null; + /// + /// Returns the service published under , for capabilities a plugin cannot work without. List the + /// ID in as well, so the host blocks the plugin up front when nothing provides it. + /// + /// The contract type to cast the service to. + /// Versioned capability ID. + /// + /// Nothing is published under , or the service does not implement . + /// public T Require(string id) where T : class => Find(id) ?? throw new InvalidOperationException("Required capability is unavailable: " + id); } } diff --git a/VisualPinball.Plugins/Runtime/PluginEvents.cs b/VisualPinball.Plugins/Runtime/PluginEvents.cs index d98599334..5dc493479 100644 --- a/VisualPinball.Plugins/Runtime/PluginEvents.cs +++ b/VisualPinball.Plugins/Runtime/PluginEvents.cs @@ -10,7 +10,26 @@ namespace VisualPinball.Plugins { - /// Typed control notifications delivered on the host owner. Borrowed frames use their source contracts instead. + /// + /// Typed publish/subscribe bus for small control notifications between plugins, delivered on the host's owner thread. + /// One channel exists per struct type: subscribers of T receive every published T. + /// + /// + /// The Player publishes one instance as the vpe.events.v1 capability (UnityPluginCapability.Events), with + /// subscriber exceptions going to Unity's log. Plugins define their own notification structs and agree on them; none of the + /// bundled plugins uses the bus yet. Streams of frames or samples do not belong here: they are borrowed buffers and go + /// through the resource contracts in . + /// + /// A plugin-defined notification: + /// + /// public readonly struct AttractModeChanged { public bool Active { get; } public AttractModeChanged(bool active) => Active = active; } + /// + /// var events = game.Capabilities.Require<PluginEvents>(UnityPluginCapability.Events); + /// game.Lifetime.Own(events.Subscribe<AttractModeChanged>(change => _attract = change.Active)); + /// events.Post(new AttractModeChanged(true), game.Cancellation); // from any thread + /// + /// + /// public sealed class PluginEvents { private interface IChannel { } @@ -22,7 +41,23 @@ private sealed class Channel : IChannel where T : struct private readonly Dictionary _channels = new Dictionary(); private readonly OwnerScheduler _scheduler; private readonly Action _fault; + /// Creates an event bus bound to an owner thread. Created by the Player; plugins get it as a capability. + /// The owner thread that subscriptions, publishing and delivery belong to. + /// + /// Receives exceptions thrown by subscribers, so one failing subscriber does not stop delivery to the others. When null, + /// such exceptions are silently dropped. + /// public PluginEvents(OwnerScheduler scheduler, Action? fault = null) { _scheduler = scheduler; _fault = fault ?? (_ => { }); } + /// + /// Starts receiving every published until the returned handle is disposed. Owner thread only. A + /// subscriber added while an event is being delivered receives the next one, not the current one. + /// + /// The notification type; a struct, so each subscriber gets its own copy. + /// Called on the owner thread for each event. + /// + /// A handle that unsubscribes. Dispose it on the owner thread, typically through . + /// + /// Called off the owner thread. public IDisposable Subscribe(Action receive) where T : struct { _scheduler.VerifyOwner(); @@ -31,12 +66,27 @@ public IDisposable Subscribe(Action receive) where T : struct channel.Subscribers.Add(receive); channel.Dispatch = channel.Subscribers.ToArray(); return new Subscription(() => { _scheduler.VerifyOwner(); channel.Subscribers.Remove(receive); channel.Dispatch = channel.Subscribers.ToArray(); }); } + /// + /// Delivers an event synchronously to the current subscribers of , in subscription order. Owner + /// thread only; use from other threads. Subscriber exceptions go to the fault handler. + /// + /// The notification type. + /// The event to deliver. + /// Called off the owner thread. public void Publish(in T value) where T : struct { _scheduler.VerifyOwner(); if (!_channels.TryGetValue(typeof(T), out var found)) return; foreach (var receive in ((Channel)found).Dispatch) try { receive(value); } catch (Exception error) { _fault(error); } } + /// + /// Queues an event for delivery on the owner thread during the next scheduler pump. Thread-safe. The event is dropped if + /// is cancelled by the time it would be delivered, so pass the game's cancellation token to + /// keep a stopped game's events from reaching the next one. + /// + /// The notification type. + /// The event to deliver. + /// Cancels delivery if it has not happened yet; the default never cancels. public void Post(T value, CancellationToken lifetime = default) where T : struct => _scheduler.Post(() => { if (!lifetime.IsCancellationRequested) Publish(value); }); diff --git a/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs b/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs index d42637d03..d95b17857 100644 --- a/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs +++ b/VisualPinball.Plugins/Runtime/PluginHost.Settings.cs @@ -15,8 +15,54 @@ namespace VisualPinball.Plugins { public sealed partial class PluginHost { + /// + /// Raised on the owner thread after settings were saved through or + /// . Arguments: the plugin ID, the IDs of the saved fields, and the game whose + /// per-game settings changed, or null for global settings. + /// + /// + /// Lets one settings view follow edits made elsewhere: the Player's plugin settings page refreshes the fields the user + /// has not touched when the volume mixer saves a value. Exceptions thrown by a handler are logged and do not reach other + /// handlers or the committing caller. Not raised when the commit fails or is cancelled. + /// public event Action, GameIdentity?>? SettingsCommitted; - /// Edits one canonical global field. Waiting for game startup does not discard a mixer edit. + /// + /// Saves one field's global value, applying it to the running plugin first when the field is + /// . This is the write path for fields exposed on shared Player pages through + /// , such as the volume mixer. + /// + /// + /// + /// The call waits behind any game start, stop or other settings commit instead of failing, so an edit made while a game + /// is loading is applied once loading finishes rather than discarded. Only is validated and + /// written; other saved fields are left as they are. + /// + /// + /// When a game is running, the field is and its runtime value changes, the host + /// passes the plugin its current settings with the new value through or + /// and saves only if that succeeds. If the plugin implements neither, the commit + /// fails and nothing is saved. If the running game overrides the field per game, the global edit is saved but the + /// runtime value, and so the plugin, stays on the game's value. Other fields take effect on the next game. + /// + /// + /// The Player queues these calls from its volume mixer and sends only the latest value per field. + /// + /// + /// The installed plugin whose setting changes. + /// ID of a field that declares and is not game-only. + /// The new value, in the field's JSON form. + /// + /// Cancels the commit. While waiting for the lifecycle gate it cancels the returned task; afterwards it yields a failed + /// result and any live change is rolled back. Once saving has started the save completes. + /// + /// + /// Completes on the owner thread with success and Saved., or failure and a message to show next to the control + /// (validation issues, a rejected live change, or Settings change cancelled.). + /// + /// + /// Thrown immediately when the field does not exist, is not exposed as a global setting or is game-only; the task faults + /// with it when is not installed in this host. + /// public Task CommitGlobalSettingAsync(IPlayerPluginFactory factory, string fieldId, JToken value, CancellationToken cancellation = default) { var field = factory.Schema.Fields.FirstOrDefault(item => item.Id == fieldId); diff --git a/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs index e28f4cb6a..cec17ff6e 100644 --- a/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs +++ b/VisualPinball.Plugins/Runtime/PluginInputPipeline.cs @@ -10,7 +10,28 @@ namespace VisualPinball.Plugins { - /// Ordered filters transform one input value; observers receive the resulting immutable event. + /// + /// A chain of plugin input filters: each passes through every registered + /// in a deterministic order, and the owner of the pipeline forwards the final value. + /// + /// + /// Filters let plugins adjust input before the game sees it, for example to scale or dead-zone an analog nudge axis. The + /// order is order ascending, then plugin ID, so the result does not depend on registration order. Each plugin can + /// register one filter per pipeline. The pipeline itself does no threading, so register, unregister and apply on the + /// same thread. The Player does not create a pipeline yet; the type is a building block for an input owner. + /// + /// + /// sealed class NudgeScale : IPluginInputFilter + /// { + /// public PluginInputEvent Filter(in PluginInputEvent input) => input.Action == "nudge-x" + /// ? new PluginInputEvent(input.Action, input.Value * 2, input.TimestampUs) : input; + /// } + /// + /// using var stage = pipeline.Register("my.nudge", 10, new NudgeScale()); + /// var filtered = pipeline.Apply(new PluginInputEvent("nudge-x", 0.25f, timestampUs)); + /// + /// + /// public sealed class PluginInputPipeline { private sealed class Stage @@ -21,6 +42,12 @@ private sealed class Stage } private readonly List _stages = new List(); private Stage[] _dispatch = Array.Empty(); + /// Adds a plugin's filter to the chain until the returned handle is disposed. + /// The registering plugin's ID; also the tie-breaker between equal orders. + /// Position in the chain; lower values run first. + /// The filter to run on every event. + /// A handle that removes the filter; dispose it with the plugin's game or application scope. + /// A filter is already registered for . public IDisposable Register(string pluginId, int order, IPluginInputFilter filter) { if (_stages.Any(stage => stage.Id == pluginId)) throw new InvalidOperationException("Input filter already registered: " + pluginId); @@ -28,6 +55,12 @@ public IDisposable Register(string pluginId, int order, IPluginInputFilter filte return new Subscription(() => { _stages.Remove(entry); Rebuild(); }); } private void Rebuild() => _dispatch = _stages.OrderBy(stage => stage.Order).ThenBy(stage => stage.Id, StringComparer.Ordinal).ToArray(); + /// + /// Runs an event through every filter in order, each receiving the previous filter's output, and returns the result. + /// With no filters registered the input is returned unchanged. A filter's exception propagates to the caller. + /// + /// The raw input event. + /// The filtered event to deliver to the game. public PluginInputEvent Apply(in PluginInputEvent input) { var result = input; diff --git a/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs index 73a84edac..3e072fe66 100644 --- a/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs +++ b/VisualPinball.Plugins/Runtime/PluginOrderResolver.cs @@ -10,15 +10,85 @@ namespace VisualPinball.Plugins { + /// + /// Thrown by when a set of plugins cannot be ordered: a dependency is missing, a + /// required capability has no provider or more than one, or the ordering constraints form a cycle. + /// + /// + /// The host catches it while selecting plugins for a game and marks every plugin in as + /// with the exception's message, then resolves the rest again. Plugin authors see the + /// message in the Player's plugin status, such as my.plugin: vpe.example.v1 has multiple providers; select one. + /// public sealed class PluginOrderException : InvalidOperationException { + /// + /// IDs of the plugins at fault: the consumer whose dependency or capability is unsatisfied, or every plugin in the + /// cycle, in cycle order. + /// public IReadOnlyList Plugins { get; } + /// Creates the exception with a user-facing message and the plugins to block. + /// Explanation shown in the plugin's status. + /// IDs of the plugins at fault. public PluginOrderException(string message, params string[] plugins) : base(message) { Plugins = plugins; } } - /// Dependencies and selected capability providers precede consumers; priority breaks ties. + /// + /// Computes the order in which plugins run one lifecycle phase: dependencies and capability providers run before the + /// plugins that need them, explicit before/after constraints are honored, and priority then plugin ID break ties. + /// + /// + /// The host resolves an order for every phase from the descriptors' declarations, so plugin authors influence ordering + /// only through : , + /// /, , + /// , and per-phase + /// . Stopping runs in reverse start order. + /// + /// A plugin that runs after the DMD plugin in every phase, and early in the render phase: + /// + /// new PluginDescriptor("my.overlay", "Overlay", "Draws on top of the DMD") { + /// After = new[] { "org.visualpinball.dmd" }, + /// PhaseOrder = new Dictionary<PluginPhase, PluginOrder> { [PluginPhase.Render] = new PluginOrder { Priority = -10 } } + /// }; + /// + /// + /// public static class PluginOrderResolver { + /// + /// Orders for one and returns them in execution order. + /// + /// + /// Rules, applied together: + /// + /// Every entry must be in the set and runs first. + /// + /// Each entry not offered by the host and not provided by the plugin itself needs + /// exactly one other plugin in the set that provides it, which then runs first. + /// narrows several providers down to the selected one. + /// + /// + /// and , plus the phase's + /// and , add constraints; IDs not in the set are ignored. + /// + /// + /// Among plugins free to run, the lowest of the phase goes first, or + /// when the plugin has no entry for the phase; equal values go by ID (ordinal). + /// + /// + /// + /// The enabled plugins to order. IDs must be unique. + /// The phase whose entry applies. + /// + /// Capability IDs the application already offers (see ); null for none. + /// + /// + /// Capability ID to the ID of the plugin chosen to provide it, when several could; null for none. + /// + /// The same descriptors, in the order the phase should run them. + /// + /// A dependency or required capability is unsatisfied or ambiguous, or the constraints form a cycle. + /// + /// Two descriptors share an ID. public static IReadOnlyList Resolve(IEnumerable plugins, PluginPhase phase, IEnumerable? hostCapabilities = null, IReadOnlyDictionary? preferredProviders = null) { diff --git a/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs index fd9908fc6..c80957636 100644 --- a/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs +++ b/VisualPinball.Plugins/Runtime/PluginOutputLeases.cs @@ -9,10 +9,35 @@ namespace VisualPinball.Plugins { - /// Exclusive destination ownership prevents two plugins from opening the same physical device. + /// + /// Exclusive leases on physical output destinations, so two plugins never open the same device, such as a real DMD on a + /// USB or serial port. + /// + /// + /// The Player publishes one instance as the vpe.outputs.v1 capability (UnityPluginCapability.Outputs). A + /// plugin acquires a lease for each device before opening it and keeps the lease until the device is closed. Leases + /// compare destination IDs as plain strings, so agree on stable IDs: the DMD plugin uses usb:zedmd:auto, + /// usb:pin2dmd:auto or serial:COM3 (port upper-cased). Thread-safe. + /// + /// The DMD plugin leases its configured devices while preparing a game and releases them once its native worker stopped: + /// + /// var outputs = game.Capabilities.Require<PluginOutputLeases>(UnityPluginCapability.Outputs); + /// foreach (var output in OutputIds(settings)) _nativeResources.Own(outputs.Acquire(output, _application.Plugin.Id)); + /// + /// + /// public sealed class PluginOutputLeases { private readonly Dictionary _owners = new Dictionary(StringComparer.Ordinal); + /// + /// Takes exclusive ownership of a destination until the returned lease is disposed. Fails instead of waiting when + /// another plugin holds it; a failed acquisition in a prepare hook faults the plugin with the message naming the owner. + /// + /// Stable device ID, compared ordinally. + /// ID of the acquiring plugin, reported by and in conflict messages. + /// The lease. Dispose it after the device is closed, from any thread; disposing twice does nothing. + /// is null, empty or whitespace. + /// The destination is already leased. public IDisposable Acquire(string destination, string pluginId) { if (string.IsNullOrWhiteSpace(destination)) throw new ArgumentException("A destination needs a stable ID.", nameof(destination)); @@ -22,6 +47,8 @@ public IDisposable Acquire(string destination, string pluginId) } return new Subscription(() => { lock (_owners) _owners.Remove(destination); }); } + /// Returns the ID of the plugin holding a destination, or null when it is free. + /// Stable device ID, compared ordinally. public string? Owner(string destination) { lock (_owners) return _owners.TryGetValue(destination, out var owner) ? owner : null; } } } diff --git a/VisualPinball.Plugins/Runtime/ResourceScope.cs b/VisualPinball.Plugins/Runtime/ResourceScope.cs index 4e7ec9d8f..7450e2905 100644 --- a/VisualPinball.Plugins/Runtime/ResourceScope.cs +++ b/VisualPinball.Plugins/Runtime/ResourceScope.cs @@ -9,17 +9,49 @@ namespace VisualPinball.Plugins { - /// Releases every owned resource in reverse order, even if one cleanup fails. + /// + /// Owns a set of disposables and releases them together, in reverse order of acquisition, continuing past failures. + /// + /// + /// The host gives each plugin instance two scopes: , released right after + /// , and , released after + /// . A plugin that stops responding keeps both until its pending work finishes. + /// Handing subscriptions, bindings, publications and GameObjects to a scope instead of tracking them by hand means a + /// failed or abandoned plugin still cleans up. Plugins and the Player also create + /// their own scopes for narrower lifetimes, such as PinUP's scope per player screen or the DMD plugin's scope for native + /// device leases. Not thread-safe: use one scope from one thread. + /// + /// + /// var dmd = game.Lifetime.Own(game.Resources.Bind<IPluginDisplaySource>(selection)); + /// game.Lifetime.Own(dmd.SubscribeFrames(frame => Render(frame))); + /// game.Lifetime.Own(new Subscription(() => { if (_root != null) UnityEngine.Object.Destroy(_root); })); + /// + /// + /// public sealed class ResourceScope : IDisposable { private readonly List _resources = new List(); private bool _disposed; + /// + /// Adds a resource to release when the scope is disposed, and returns it so the call can wrap a constructor or + /// Publish call inline. If the scope is already disposed, the resource is disposed at once and the call throws, + /// so late work cannot leak. + /// + /// The resource type, kept so the result needs no cast. + /// The resource to own. + /// . + /// The scope has already been disposed. public T Own(T resource) where T : IDisposable { if (_disposed) { resource.Dispose(); throw new ObjectDisposedException(nameof(ResourceScope)); } _resources.Add(resource); return resource; } + /// + /// Disposes every owned resource, last owned first. Every resource is disposed even when one throws; afterwards the + /// collected failures are rethrown together. Later calls do nothing. + /// + /// One or more resources threw while being disposed. public void Dispose() { if (_disposed) return; @@ -32,10 +64,28 @@ public void Dispose() } } + /// + /// An that runs a release action once. Returned by most plugin APIs as the handle that undoes a + /// subscription, publication or lease, and handy for turning any cleanup into something a can own. + /// + /// + /// + /// PinUP unhooks its screen events when the game ends: + /// + /// game.Lifetime.Own(new Subscription(() => { _player.ScreenCreated -= ScreenCreated; _player.ScreenDestroyed -= ScreenDestroyed; })); + /// + /// + /// public sealed class Subscription : IDisposable { private Action? _release; + /// Wraps a release action. + /// Runs on the first , on the disposing thread. public Subscription(Action release) { _release = release; } + /// + /// Runs the release action on the first call; later calls, including concurrent ones, do nothing. An exception from the + /// action propagates to the caller and the action is not retried. + /// public void Dispose() => System.Threading.Interlocked.Exchange(ref _release, null)?.Invoke(); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs index 299b84723..0895389d1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowCapabilities.cs @@ -11,17 +11,35 @@ namespace VisualPinball.Unity /// /// Window features reported by the native library, matching the native VPNW_Capability bits. /// + /// + /// Read the current set from . The bits describe the platform and the + /// desktop session, not the graphics backend; combines + /// with the backend check and is what a plugin should test before creating a window. The other + /// bits let a plugin explain degraded behaviour, for example that a compositor ignores topmost requests. + /// + /// + /// var capabilities = NativeWindowSurface.Capabilities; + /// if ((capabilities & NativeWindowCapabilities.Transparency) == 0) { + /// log("Transparent areas of the backglass will not show the desktop."); + /// } + /// + /// + /// [Flags] public enum NativeWindowCapabilities : uint { + /// No feature is available, or the native library could not be loaded. None = 0, /// Borderless GPU output windows can be created. Windows = 1, - /// Placement mode (mouse dragging and edge resizing) is functional. + /// + /// Placement mode (mouse dragging and edge resizing, see ) is + /// functional. + /// Placement = 2, /// Topmost and non-topmost requests are honoured. Topmost = 4, - /// Premultiplied alpha reaches the desktop compositor. + /// Premultiplied alpha reaches the desktop compositor, so transparent pixels show the desktop behind. Transparency = 8, } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs index 910290a69..0a06070f4 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowDiagnostics.cs @@ -11,30 +11,66 @@ namespace VisualPinball.Unity /// /// Snapshot of a native window's state, matching the native VPNW_Diagnostics layout (80 bytes). /// - /// Coordinates and sizes are physical desktop pixels. Boolean fields are 0 or 1. + /// + /// Coordinates and sizes are physical desktop pixels. Boolean fields are 0 or 1. + /// + /// Returned by , which crosses into the window system, so query it for + /// troubleshooting or occasional polling, not every frame. Besides logging, the settings windows of PinUP and the B2S + /// backglass read the actual position and size from it after the user dragged or resized a window in placement mode. + /// + /// + /// + /// var state = surface.Diagnostics; + /// var bounds = new RectInt(state.Left, state.Top, state.WindowWidth, state.WindowHeight); + /// Debug.Log($"{state.PresentedFrames} frames presented, {state.SkippedFrames} skipped, last result 0x{state.LastResult:x8}"); + /// + /// + /// [StructLayout(LayoutKind.Sequential)] public struct NativeWindowDiagnostics { + /// + /// Size of this structure in bytes. fills it in before the native call, + /// which rejects a size other than 80. + /// public uint StructBytes; /// UnityGfxRenderer value seen by the native plugin. public int GraphicsRenderer; + /// 1 when the native plugin has a graphics device to present with, otherwise 0. public int DeviceReady; + /// 1 while the native window can present; 0 on Linux after a presentation failure stopped it. public int Alive; + /// 1 when the window is shown, otherwise 0. public int Visible; + /// 1 when the window stays above normal windows, otherwise 0. public int Topmost; + /// Left edge of the window in physical desktop pixels; negative left of the primary monitor. public int Left; + /// Top edge of the window in physical desktop pixels; negative above the primary monitor. public int Top; + /// Actual window width in physical pixels. public int WindowWidth; + /// Actual window height in physical pixels. public int WindowHeight; + /// DPI of the monitor the window is on (96 is 100 % scaling). public uint Dpi; + /// Width of the source texture in pixels, or 0 when no texture is assigned. public uint SourceWidth; + /// Height of the source texture in pixels, or 0 when no texture is assigned. public uint SourceHeight; /// Platform texture format: DXGI_FORMAT, MTLPixelFormat or VkFormat. public uint SourceFormat; + /// Number of frames presented to the window since it was created. public ulong PresentedFrames; + /// + /// Number of present events that did not reach the window, for example because the display was not ready yet. + /// Presentation never waits for the display, so a count that keeps rising while the window is visible and has a source + /// means it cannot keep up with Unity's frame rate. + /// public ulong SkippedFrames; /// Windows: last presentation HRESULT. macOS/Linux: 0, or negative after a presentation failure. public int LastResult; + /// 1 when the window composes its content with premultiplied alpha, otherwise 0. public int PremultipliedAlpha; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs index 27a7ae7ca..411508a9a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowHost.cs @@ -19,6 +19,30 @@ namespace VisualPinball.Unity /// /// A single hidden instance is shared by all users (PinUP, B2S, ...). It is created on the first /// and removes itself once no surface is registered and no GPU resource is pending. + /// + /// Native windows draw and present on Unity's render thread, triggered by + /// and . Both events operate on all windows, so they must be issued once + /// per frame in total, not once per window; this component does that from LateUpdate (execution order 10000) + /// and also calls . A plugin only registers its surface and keeps the returned + /// lease. In play mode the presenter survives scene loads; it runs LateUpdate only while the application is + /// playing. + /// + /// + /// When the presenter is destroyed (for example when play mode ends), it disposes every surface still registered. + /// + /// + /// + /// var surface = new NativeWindowSurface(x, y, width, height); // hidden + /// surface.SetTexture(texture.GetNativeTexturePtr()); + /// surface.Configure(x, y, width, height, visible: true, topmost: false); + /// var lease = NativeWindowHost.Register(surface); + /// + /// // Shutdown: unregister, destroy the window, then release the texture. + /// lease.Dispose(); + /// surface.Dispose(); + /// texture.Release(); + /// + /// /// [DefaultExecutionOrder(10000)] public sealed class NativeWindowHost : MonoBehaviour @@ -31,6 +55,15 @@ public sealed class NativeWindowHost : MonoBehaviour /// /// Registers a surface for render-thread presentation. Dispose the returned lease before disposing the surface. /// + /// + /// Call on the main thread; the first call creates the hidden presenter object. Registering the same surface twice + /// has no further effect, and one lease removes it. Disposing the lease more than once is harmless. After the last + /// lease ends the presenter keeps issuing events until is zero, + /// then removes itself. + /// + /// The window to present. The caller keeps ownership and disposes it after the lease. + /// A lease that unregisters the surface when disposed. + /// is null. public static IDisposable Register(NativeWindowSurface surface) { if (surface == null) { diff --git a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs index c31b352b2..29c766ad0 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/NativeWindows/NativeWindowSurface.cs @@ -20,6 +20,32 @@ namespace VisualPinball.Unity /// , which issues and once /// per frame for all surfaces. Dispose the surface before releasing its RenderTexture. The wrapper has explicit /// ownership and no finalizer. + /// + /// Plugins use it to put a premultiplied RGBA RenderTexture on a second or third monitor, with transparent areas + /// showing the desktop behind: PinUP opens one per PUP screen, the B2S plugin one for the backglass and one for the + /// B2S DMD, and both open one in placement mode for the Player's screen layout editor (see + /// ). The texture never leaves the GPU. Use the instance from the main thread, as + /// all VPE users do. Instance members throw after , and + /// carrying when the native call fails. + /// + /// + /// + /// if (!NativeWindowSurface.IsSupported) { + /// return; // Missing library or unsupported graphics backend; never throws. + /// } + /// var texture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32); + /// texture.Create(); + /// var surface = new NativeWindowSurface(x, y, width, height); // hidden + /// surface.SetTexture(texture.GetNativeTexturePtr()); + /// surface.Configure(x, y, width, height, visible: true, topmost: false); + /// var lease = NativeWindowHost.Register(surface); + /// + /// // Shutdown: unregister, destroy the window, then release the texture. + /// lease.Dispose(); + /// surface.Dispose(); + /// texture.Release(); + /// + /// /// public sealed class NativeWindowSurface : IDisposable { @@ -63,27 +89,50 @@ public static bool IsSupported } } - /// Unity's UnityGfxRenderer as seen by the native plugin, or -1 when the library is unavailable. + /// + /// Unity's UnityGfxRenderer as seen by the native plugin, or -1 when the library is unavailable. The supported values + /// are 2 (Direct3D11), 16 (Metal), 18 (Direct3D12) and 21 (Vulkan). Useful in log messages when + /// is false. + /// public static int GraphicsRenderer => LibraryAvailable ? Native.GetGraphicsRenderer() : -1; - /// Window features of the current platform and session. + /// + /// Window features of the current platform and session, or when the + /// library is unavailable. + /// public static NativeWindowCapabilities Capabilities => LibraryAvailable ? (NativeWindowCapabilities)Native.GetCapabilities() : NativeWindowCapabilities.None; - /// Unity rendering callback. Issue and then with it. + /// + /// Unity rendering callback for GL.IssuePluginEvent, or when the library is + /// unavailable. Issue and then with it. Plugins do not need it: + /// issues the events. + /// public static IntPtr RenderEventFunction => LibraryAvailable ? Native.GetRenderEventFunc() : IntPtr.Zero; - /// Submit after the frame's GPU work, followed immediately by . Operates on all surfaces. + /// + /// Event ID of the per-frame draw step for all windows. Submit after the frame's GPU work, followed immediately by + /// . Operates on all surfaces. 0 when the library is unavailable. + /// public static int DrawEventId => LibraryAvailable ? Native.GetDrawEventId() : 0; - /// Presents all surfaces; submit right after , once per frame. + /// + /// Event ID that presents all surfaces; submit right after , once per frame. 0 when the + /// library is unavailable. + /// public static int PresentEventId => LibraryAvailable ? Native.GetPresentEventId() : 0; - /// Retired GPU source and presentation objects awaiting later render events. Keep issuing event pairs until zero. + /// + /// Retired GPU source and presentation objects awaiting later render events. Keep issuing event pairs until zero. + /// does this after its last surface is unregistered. 0 when the library is unavailable. + /// public static int PendingResourceCount => LibraryAvailable ? Native.GetPendingResourceCount() : 0; - /// Processes platform window events (Linux placement dragging). Call once per frame from the main thread. + /// + /// Processes platform window events (Linux placement dragging). Call once per frame from the main thread. A no-op on + /// Windows and macOS and when the library is unavailable. calls it every frame. + /// public static void Pump() { if (LibraryAvailable) { @@ -92,6 +141,12 @@ public static void Pump() } /// Destroys all native windows, for shutdown or domain-reload cleanup by the host. + /// + /// This affects the windows of every plugin. Existing instances keep their + /// , so their other members then fail; stays safe because native destruction is + /// idempotent. GPU resources still retire through later render events. Plugins should dispose their own surfaces + /// instead. + /// public static void DestroyAll() { if (LibraryAvailable) { @@ -99,7 +154,10 @@ public static void DestroyAll() } } - /// The last native error message (UTF-8), for diagnostics. + /// + /// The last native error message (UTF-8), for diagnostics, or a message naming the missing library when it is + /// unavailable. The native library keeps one message for all windows, so read it right after a failure. + /// public static string LastError { get { @@ -139,6 +197,15 @@ private static bool LibraryAvailable #endregion /// Creates an initially hidden window, using signed physical desktop pixel coordinates. + /// + /// Check first: the constructor only verifies that the library loads, not that Unity's + /// graphics backend can present. Then assign a texture, call to show the window and register + /// it with . + /// + /// Left edge in physical desktop pixels; negative left of the primary monitor. + /// Top edge in physical desktop pixels; negative above the primary monitor. + /// Width in physical pixels. + /// Height in physical pixels. /// The native library is missing or the player is not 64-bit. /// The native window could not be created; see . public NativeWindowSurface(int x, int y, int width, int height) @@ -156,6 +223,19 @@ public NativeWindowSurface(int x, int y, int width, int height) } /// Sets the physical desktop rectangle, visibility and topmost state. Identical calls are cached and skipped. + /// + /// The cache makes calling it every frame cheap. It compares with the last values passed here (and the topmost state + /// set by ), not with the window's current geometry, so repeating the previous call does not + /// undo a move the user made in placement mode. + /// + /// Left edge in physical desktop pixels; negative left of the primary monitor. + /// Top edge in physical desktop pixels; negative above the primary monitor. + /// Width in physical pixels. + /// Height in physical pixels. + /// True shows the window, false hides it. + /// True keeps the window above normal windows. + /// The surface was disposed. + /// The native call failed; the message includes . public void Configure(int x, int y, int width, int height, bool visible, bool topmost) { RequireAlive(); @@ -178,7 +258,16 @@ public void Configure(int x, int y, int width, int height, bool visible, bool to /// Assigns the source texture, typically RenderTexture.GetNativeTexturePtr(), containing premultiplied RGBA /// in the desired SDR encoding. /// - /// IntPtr.Zero detaches the source. Resubmit the pointer whenever Unity recreates or resizes the texture. + /// + /// IntPtr.Zero detaches the source. Resubmit the pointer whenever Unity recreates or resizes the texture. The pointer + /// is an ID3D11Texture2D, ID3D12Resource, MTLTexture or VkImage* depending on the + /// backend; the texture must be created and stay alive until it is detached or the surface is disposed. It is scaled + /// to the window size on the GPU. + /// + /// The native texture pointer, or to show nothing. + /// True flips the image vertically when it is presented. VPE's own callers pass false. + /// The surface was disposed. + /// The native call failed; the message includes . public void SetTexture(IntPtr texture, bool flipY = false) { RequireAlive(); @@ -188,6 +277,13 @@ public void SetTexture(IntPtr texture, bool flipY = false) } /// Allows mouse dragging and edge resizing while the user places the window. Playback windows stay fixed. + /// + /// Read the result from . Only functional when includes + /// ; on Linux the drags are processed by . + /// + /// True lets the user move and resize the window; false fixes it again. + /// The surface was disposed. + /// The native call failed; the message includes . public void SetPlacementMode(bool enabled) { RequireAlive(); @@ -197,15 +293,29 @@ public void SetPlacementMode(bool enabled) } /// Moves the window behind other windows without moving, resizing or activating it. + /// The surface was disposed. + /// The native call failed; the message includes . public void SendToBack() => SetZOrder(0); /// Moves the window in front of other windows without moving, resizing or activating it. + /// The surface was disposed. + /// The native call failed; the message includes . public void BringToFront() => SetZOrder(1); /// Sets or clears the topmost state without moving, resizing or activating the window. + /// The new state also updates the cache of . + /// True keeps the window above normal windows; false makes it a normal window again. + /// The surface was disposed. + /// The native call failed; the message includes . public void SetTopmost(bool topmost) => SetZOrder(topmost ? 2 : 3); /// Actual window geometry, DPI, source and presentation counters. Query for troubleshooting, not every frame. + /// + /// Each read crosses into the window system. The settings windows of PinUP and the B2S plugin also poll it to pick up + /// the rectangle after the user moved or resized the window in placement mode. + /// + /// The surface was disposed. + /// The native call failed; the message includes . public NativeWindowDiagnostics Diagnostics { get { @@ -219,6 +329,11 @@ public NativeWindowDiagnostics Diagnostics } /// Destroys the window. GPU resources are released by later render events; see . + /// + /// Dispose the lease first and release the source texture afterwards. Calling it again + /// does nothing. is zero afterwards even when the native call fails. + /// + /// The native window could not be destroyed; see . public void Dispose() { if (Id == 0) { diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs index 0a20afe5f..ee36c407a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PlayerIntegrationMode.cs @@ -9,11 +9,58 @@ namespace VisualPinball.Unity { - /// Player applications claim integration ownership before automatic native bridges can start. + /// + /// Tells integration packages whether a player application manages them through the plugin host. Player applications + /// claim integration ownership before automatic native bridges can start. + /// + /// + /// Some integrations start themselves when VPE runs a table in the editor or in a standalone build. The DMD package, + /// for example, adds a bridge to every table it finds so DMD frames reach physical displays. In + /// the VPE Player the same work is done by plugins under the user's settings, so the Player calls + /// and the automatic bridges stand down by checking . + /// + /// Unity's load order makes this work: the state is reset at SubsystemRegistration, the Player claims at + /// AfterAssembliesLoaded, and bridges start at BeforeSceneLoad. + /// + /// + /// An integration that starts itself checks the mode first: + /// + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + /// private static void StartWatcher() + /// { + /// if (PlayerIntegrationMode.HostManaged) return; // the Player's plugin does this job + /// var gameObject = new GameObject("My bridge"); + /// Object.DontDestroyOnLoad(gameObject); + /// gameObject.AddComponent<MyBridgeWatcher>(); + /// } + /// + /// + /// public static class PlayerIntegrationMode { + /// + /// True when a player application has called in this run. Integrations must then not start + /// their own bridges. False by default, in the editor and in builds without a plugin host. + /// public static bool HostManaged { get; private set; } + /// + /// Marks integrations as managed by the host for the rest of the run. Call it on the main thread from a + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] method, so it runs after + /// the reset and before integrations start. Only player applications call this; plugins have no reason to. + /// + /// + /// + /// + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] + /// private static void ClaimIntegrations() => PlayerIntegrationMode.Claim(); + /// + /// + /// public static void Claim() => HostManaged = true; + /// + /// Clears the claim and the plugin and identity-reader registries at the start of every run, including each play mode + /// entry in the editor with domain reload disabled, so state from a previous play session does not linger. + /// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void Reset() { diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs index d95ff5d8d..0d9d76df2 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityPluginContracts.cs @@ -9,31 +9,159 @@ namespace VisualPinball.Unity { + /// + /// The Unity scene of the game being played: the table's root object and its gamelogic engine. Plugins that need to + /// add objects to the table or talk to a specific engine get it from the game's capabilities. + /// + /// + /// The Player publishes one instance under for each game, before plugins + /// receive , and withdraws it when the game stops. The PinMAME plugin uses + /// it to find the PinMameGamelogicEngine under the table, the DMD plugin parents its bridge object to + /// , and the B2S plugin attaches its in-scene backglass to it. All access belongs on the owner + /// (main) thread. + /// + /// + /// public override Task PrepareGameAsync(PluginGameContext game, CancellationToken cancellation) + /// { + /// var scene = game.Capabilities.Require<IUnityGameScene>(UnityPluginCapability.GameScene); + /// _root = new GameObject("My plugin"); + /// _root.transform.SetParent(scene.Table.transform, false); + /// game.Lifetime.Own(new Subscription(() => { if (_root != null) UnityEngine.Object.Destroy(_root); })); + /// return Task.CompletedTask; + /// } + /// + /// + /// public interface IUnityGameScene { + /// + /// Root object of the loaded table. Valid for the duration of the game; objects a plugin adds under it are destroyed + /// with the table unless the plugin removes them first. + /// GameObject Table { get; } + /// + /// The table's gamelogic engine, found on the same object as its component, or null when the + /// table has no Player or no engine. Cast it to an optional interface (such as ) + /// or a concrete engine type to reach engine-specific features. + /// IGamelogicEngine Gamelogic { get; } } + /// + /// Default immutable , created by the Player for each game it prepares. + /// + /// + /// Plugins consume and have no reason to create this type; a host or test that publishes + /// itself can use it. + /// + /// How the Player publishes the scene for the game's lifetime: + /// + /// _gameScope.Own(Host.Capabilities.Publish<IUnityGameScene>(UnityPluginCapability.GameScene, new UnityGameScene(table, engine))); + /// + /// + /// public sealed class UnityGameScene : IUnityGameScene { + /// public GameObject Table { get; } + /// public IGamelogicEngine Gamelogic { get; } + /// Creates a scene description. Neither argument is validated. + /// Root object of the loaded table. + /// The table's gamelogic engine, or null when it has none. public UnityGameScene(GameObject table, IGamelogicEngine gamelogic) { Table = table; Gamelogic = gamelogic; } } - /// Native display negotiation may apply to the entire provider, even when the API accepts a display ID. + /// + /// Implemented by a gamelogic engine to declare which display frame encodings it can produce, so VPE publishes its + /// display resources with the right formats and negotiates raw frames correctly. + /// + /// + /// reads this when a game is prepared. The formats are advertised in each display's + /// . When a consumer binds a display for + /// (for example the DMD plugin matching colorization frames), VPE requests raw + /// or frames through + /// and returns to when the last such + /// consumer leaves. Native display negotiation may apply to the entire provider, even when the API accepts a display ID; + /// declares that. + /// + /// Without this interface VPE assumes Gray2 | Gray4 | Luminance8 | Rgb24 for engines that implement + /// and Luminance8 | Rgb24 otherwise, with + /// as default and per-display negotiation. + /// + /// + /// PinMAME switches the frame format of all its displays at once: + /// + /// DisplayEncoding IDisplayResourceMetadata.DisplayFormats => DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8; + /// DisplayEncoding IDisplayResourceMetadata.DefaultDisplayFormat => DisplayEncoding.Luminance8; + /// bool IDisplayResourceMetadata.FormatAppliesToAllDisplays => true; + /// + /// + /// public interface IDisplayResourceMetadata { + /// All encodings the engine can deliver for its dot-matrix displays, as flags. DisplayEncoding DisplayFormats { get; } + /// + /// The single encoding the engine delivers when no consumer asks for raw frames. VPE requests it again when the last + /// identification consumer leaves, or when the game's resources are disposed while raw frames were requested. + /// DisplayEncoding DefaultDisplayFormat { get; } + /// + /// True when a format request changes every display of the engine, not just the one named. VPE then negotiates once + /// for all displays and calls with an empty + /// display ID. False negotiates each display separately. + /// bool FormatAppliesToAllDisplays { get; } } - public interface IGamelogicAudioResourceProvider { IPluginAudioSource AudioSource { get; } } + /// + /// Implemented by a gamelogic engine that produces its own sound (such as ROM audio), so VPE can publish it as a plugin + /// audio resource. + /// + /// + /// When a game is prepared, publishes + /// as a resource with the + /// role rom-mix and the ID <controller>/audio/rom, where plugins can bind it. PinMAME exposes its emulated sound board this way. + /// + /// + /// private readonly AudioResourceSource _pluginAudio = new AudioResourceSource(error => Debug.LogException(error)); + /// IPluginAudioSource IGamelogicAudioResourceProvider.AudioSource => _pluginAudio; + /// + /// + /// + public interface IGamelogicAudioResourceProvider + { + /// + /// The engine's audio stream. Read once when the game's resources are created; must not be null. The engine owns the + /// source and publishes frames to it. + /// + IPluginAudioSource AudioSource { get; } + } - /// Receives every controller output in the controller's own numbering, on Unity's main thread. + /// + /// Receives every controller output in the controller's own numbering, on Unity's main thread. + /// + /// + /// VPE implements this interface and hands it to engines through + /// ; engine authors call it, plugins do not implement it. + /// VPE republishes each call as a on the game's + /// resource, where the B2S backglass reads ROM lamps and solenoids by number. Calls made from another thread are posted + /// to the owner thread. + /// public interface INativeOutputListener { + /// Reports the new value of one output. + /// + /// The output type: , , + /// or . + /// + /// The output's number as the controller defines it, such as a PinMAME lamp number. + /// The raw value, interpreted through and . + /// What means (on/off, levels, faded brightness or position). + /// The value that means fully on, or the top of the range; 1 for binary outputs. void NativeOutputChanged(ControllerEventKind kind, int number, double value, ControllerValueSemantics semantics, double maximum); - /// The controller finished one poll. + /// + /// The controller finished one poll. VPE publishes it as so consumers can + /// apply the outputs they collected in one batch. Call it after the poll's last . + /// void NativeOutputPollEnd(); } @@ -42,22 +170,91 @@ public interface INativeOutputListener /// table's mapping. Backglass-only outputs (match, tilt, ball in play) are usually not mapped to anything on the /// playfield, so the mapped events alone miss them. /// + /// + /// When an engine implements this interface, publishes an extra + /// resource (<controller>/outputs) and installs a listener for the + /// game's duration. The B2S plugin prefers that resource over the mapped controller events when it exists. + /// + /// PinMAME forwards each poll's changes after dispatching its mapped events: + /// + /// public void SetNativeOutputListener(INativeOutputListener listener) => _nativeOutputs = listener; + /// + /// private void DispatchNativeOutputs() + /// { + /// var listener = _nativeOutputs; + /// if (listener == null) return; + /// foreach (var lamp in _changedLamps) { + /// listener.NativeOutputChanged(ControllerEventKind.Lamp, lamp.Id, lamp.Value, ControllerValueSemantics.Binary, 1); + /// } + /// listener.NativeOutputPollEnd(); + /// } + /// + /// + /// public interface IGamelogicNativeOutputs { - /// Sets the single listener, or null. Changes are delivered from the engine's main-thread poll. + /// + /// Sets the single listener, or null. Changes are delivered from the engine's main-thread poll. VPE sets a listener + /// when the game's resources are created and passes null when they are disposed; the engine should then drop any + /// queued outputs. + /// + /// The listener to call for every output change, or null to stop reporting. void SetNativeOutputListener(INativeOutputListener listener); } - /// Layout and technology of a segment display, as published in . + /// + /// Implemented by a gamelogic engine to declare the layout and technology of its segment displays, as published in + /// . + /// + /// + /// When an engine announces a segment display (a requested display with a height of 1), + /// asks this interface for its layout and publishes a resource. Without an answer + /// the layout is seg16 with no hardware hint. The B2S backglass uses the layout and hint to draw its segment + /// digits. PinMAME maps its display types onto the layouts and reports gottlieb for upright Gottlieb + /// System 80 glass displays. + /// public interface ISegmentDisplayMetadata { - /// Returns the layout (seg7, seg9, seg14, seg16) and hardware hint of a display. + /// + /// Returns the layout (seg7, seg9, seg14, seg16) and hardware hint of a display. + /// Called on the main thread when the display is announced, so the engine must know the layout by then. + /// + /// The display ID the engine used when it announced the display. + /// The segment layout; see . + /// The technology hint, or an empty string; see . + /// True when the engine knows the display; false to use the defaults. bool TryGetSegmentLayout(string displayId, out string layout, out string hardware); } + /// + /// Keys of the capabilities the Unity Player publishes to plugins through . + /// + /// + /// Plugins read them from or + /// (the same object) with , which throws when the host does not provide the + /// capability, or with , which returns null. + /// + /// + /// var scene = game.Capabilities.Require<IUnityGameScene>(UnityPluginCapability.GameScene); + /// var outputs = game.Capabilities.Require<PluginOutputLeases>(UnityPluginCapability.Outputs); + /// + /// + /// public static class UnityPluginCapability { + /// + /// The current game's . Published per game before plugins prepare it and removed when + /// the game stops. + /// public const string GameScene = "vpe.unity.game-scene.v1"; + /// + /// The application-wide bus for typed notifications between plugins, delivered on the owner + /// thread. + /// public const string Events = "vpe.events.v1"; + /// + /// The application-wide , which gives one plugin exclusive use of a physical output + /// device. The DMD plugin leases its ZeDMD, PinDMD3 and Pixelcade ports through it. + /// public const string Outputs = "vpe.outputs.v1"; } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs index 265e33f3d..ecb555453 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnitySettingsMedia.cs @@ -13,37 +13,197 @@ namespace VisualPinball.Unity { - /// Native output transport. The Player owns placement controls and test-pattern rendering. + /// + /// A movable desktop window that a plugin opens for the Player's screen layout editor. Native output transport only: + /// the Player owns placement controls and test-pattern rendering. + /// + /// + /// When the user selects a screen in a field, the Player asks the plugin's + /// for a window at the stored rectangle, draws a test pattern into a texture + /// and hands it over with . While the window is open the Player polls + /// (about every 80 ms) to pick up the user's drags and resizes. PinUP and the B2S backglass implement it with a + /// in placement mode, so the test window is the same kind of window that shows the + /// game later. All members are called on the main thread while the Player is running. + /// + /// + /// internal sealed class MySettingsWindow : IUnitySettingsWindow + /// { + /// private readonly NativeWindowSurface _window; + /// private readonly IDisposable _presentation; + /// internal MySettingsWindow(RectInt bounds) + /// { + /// _window = new NativeWindowSurface(bounds.x, bounds.y, bounds.width, bounds.height); + /// _presentation = NativeWindowHost.Register(_window); + /// _window.SetPlacementMode(true); + /// SetBounds(bounds); + /// } + /// public RectInt Bounds { + /// get { var state = _window.Diagnostics; return new RectInt(state.Left, state.Top, state.WindowWidth, state.WindowHeight); } + /// } + /// public void SetBounds(RectInt bounds) => _window.Configure(bounds.x, bounds.y, bounds.width, bounds.height, true, true); + /// public void SetTexture(Texture texture) => _window.SetTexture(texture != null ? texture.GetNativeTexturePtr() : IntPtr.Zero); + /// public void Activate() => _window.BringToFront(); + /// public void Dispose() { _presentation.Dispose(); _window.Dispose(); } + /// } + /// + /// + /// public interface IUnitySettingsWindow : IDisposable { + /// + /// The window's actual rectangle in signed physical desktop pixels, read back from the window system, so it includes + /// moves and resizes the user made. Origins left of or above the primary monitor are negative. + /// RectInt Bounds { get; } + /// + /// Moves and resizes the window to a rectangle in signed physical desktop pixels and shows it. The Player clamps the + /// size to at least 64 x 32 pixels before calling it. + /// + /// The new window rectangle. void SetBounds(RectInt bounds); + /// + /// Sets the texture the window shows, or null to show nothing. The texture belongs to the caller and must stay alive + /// until it is replaced or the window is disposed. The Player passes null and then a new texture whenever the window + /// size changes. + /// + /// A created texture with premultiplied RGBA content, or null to detach. void SetTexture(Texture texture); + /// Brings the window in front of other windows, for example when the user selects its screen again. void Activate(); } + /// + /// Implemented by a plugin factory whose plugin shows content on its own desktop windows, so the Player's screen layout + /// editor can open test windows for it. + /// + /// + /// The Player checks the of the plugin whose settings are shown for this interface. + /// PinUP and the B2S backglass implement it and report as + /// . + /// + /// + /// internal sealed class MyPluginFactory : IPlayerPluginFactory, IUnitySettingsWindowProvider + /// { + /// public bool Supported => NativeWindowSurface.IsSupported; + /// public IUnitySettingsWindow OpenWindow(RectInt bounds) => new MySettingsWindow(bounds); + /// // IPlayerPluginFactory members omitted. + /// } + /// + /// + /// public interface IUnitySettingsWindowProvider { + /// + /// True when windows can be opened on this platform and graphics backend. The Player does not call + /// when it is false and tells the user that placement is unavailable. + /// bool Supported { get; } + /// + /// Opens a test window at a rectangle in signed physical desktop pixels; the Player calls + /// right after. Called on the main thread while the Player is running. The + /// caller owns and disposes the returned window. Exceptions are shown to the user. + /// + /// The stored placement of the screen being edited. + /// The new window. IUnitySettingsWindow OpenWindow(RectInt bounds); } - /// Texture and normalized placement data; no UI objects or styling cross this boundary. + /// + /// One image of a settings preview: a texture and its normalized placement on the preview canvas. No UI objects or + /// styling cross this boundary. + /// + /// + /// The Player draws every layer of on its own canvas after each + /// . PinUP uses one layer per PUP screen, positioned relative to the + /// backglass screen; the B2S backglass uses a single full-canvas layer. The plugin owns the layer objects and updates + /// their properties on the main thread. + /// + /// + /// var layer = new UnitySettingsPreviewLayer("backglass"); + /// layer.Texture = compositor.SceneTexture; + /// layer.Rectangle = new Rect(0, 0, 1, 1); + /// layer.Visible = layer.Texture != null; + /// + /// + /// public sealed class UnitySettingsPreviewLayer { + /// + /// Stable key of the layer. The Player keeps one image per ID and removes it when no layer with that ID is listed + /// any more. + /// public string Id { get; } + /// The texture to show, or null. The plugin owns it; the Player only displays it. public Texture Texture { get; set; } + /// + /// Placement on the canvas, normalized to its size: x and y are the left and top edges from 0 to 1 (top-left origin), + /// width and height the fractions of the canvas width and height. Defaults to an empty rectangle. + /// public Rect Rectangle { get; set; } + /// Whether the Player shows the layer. False (the default) hides it without removing it. public bool Visible { get; set; } + /// Creates a hidden layer with no texture. + /// The layer's stable key; see . public UnitySettingsPreviewLayer(string id) { Id = id; } } + /// + /// A live preview that a plugin renders inside its settings page, such as a PUP pack or a backglass playing without a + /// game. + /// + /// + /// Returned by and owned by the Player, which calls + /// about every 16 ms on the main thread, then scales to fit its viewport + /// and draws on it. An exception from stops the preview and is shown to the + /// user. The Player disposes the preview when the user stops it, starts another one or leaves the page. + /// public interface IUnitySettingsPreview : IDisposable { + /// + /// Size of the canvas the layers are placed on, in any unit (typically pixels); only its aspect ratio matters to + /// the Player. Nothing is drawn while either component is zero or less. + /// Vector2 CanvasSize { get; } + /// + /// The current layers. The Player reads the list after every ; layers may be added and removed + /// between calls. Layers are stacked in the order their IDs first appear. + /// IReadOnlyList Layers { get; } + /// + /// Advances playback by one Player frame and refreshes and . Called on + /// the main thread. + /// void Update(); } + /// + /// Implemented by a plugin factory that can show a live preview in a settings field. + /// + /// + /// When the user presses Start on a preview field, the Player calls with the field's + /// and the page's effective settings. PinUP answers pack-preview by + /// playing the selected pack offscreen, and the B2S backglass answers backglass-preview by rendering the selected + /// backglass with a slow lamp test. + /// + /// + /// public Task<IUnitySettingsPreview> OpenPreviewAsync(string provider, PluginChoiceRequest request, CancellationToken cancellation) + /// => provider == "backglass-preview" + /// ? MyPreview.OpenAsync(request, cancellation) + /// : throw new ArgumentException("Unknown preview provider.", nameof(provider)); + /// + /// + /// public interface IUnitySettingsPreviewProvider { - /// Called on Unity's owning thread. Return an owned preview; cancellation must release partial startup. + /// + /// Called on Unity's owning thread. Return an owned preview; cancellation must release partial startup. Loading may + /// continue asynchronously; the Player disposes a preview that completes after it was cancelled or replaced. + /// + /// The of the preview field, selecting which preview to open. + /// + /// The game, the page's effective settings, the platform, the published resources and the selected package. + /// + /// Cancelled when the user stops the preview or leaves the page before it is ready. + /// The new preview, owned by the caller. + /// Typically thrown for an unknown . + /// was cancelled during startup. + /// Any other exception's message is shown to the user as the preview status. Task OpenPreviewAsync(string provider, PluginChoiceRequest request, CancellationToken cancellation); } } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs index 4cbe9fecb..3f10ab2a0 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs @@ -10,10 +10,48 @@ namespace VisualPinball.Unity { - /// A borrowed Unity texture. Its producer owns it; consumers may sample it until Changed or resource removal. + /// + /// A resource backed by a borrowed Unity texture. Its producer owns it; consumers may + /// sample it until or resource removal. + /// + /// + /// Plugins that render an image other plugins can show publish it as a surface with this interface, so consumers in the + /// same Unity process can use the texture directly on the GPU. PinUP publishes each PUP screen's render texture (role + /// backglass for screen 2, dmd for screen 1, pup-screen-N otherwise), and the B2S plugin publishes + /// its composed backglass with the role backglass. Both report unity-render-texture as + /// . Consumers must not modify, resize or release the texture, and must not keep + /// using it after the resource is removed. + /// + /// Publishing a render texture: + /// + /// private sealed class MySurface : IUnityTextureResource + /// { + /// private readonly RenderTexture _texture; + /// public MySurface(RenderTexture texture) { _texture = texture; } + /// public ResourceKind Kind => ResourceKind.Surface; + /// public int Width => _texture.width; + /// public int Height => _texture.height; + /// public string Backend => "unity-render-texture"; + /// public Texture Texture => _texture; + /// public event Action<Texture> Changed; + /// } + /// + /// game.Lifetime.Own(game.Resources.Publish(new PluginResourceDescriptor(game.ControllerId + "/surface/mine", _application.Plugin.Id, + /// game.ControllerId, "My surface", "backglass", ResourceKind.Surface, game.Session, 1), new MySurface(texture))); + /// + /// + /// public interface IUnityTextureResource : IPluginSurface { + /// + /// The current texture, or null while the producer has not created it yet. Read on the main thread. Keep using a + /// texture only until announces another one. + /// Texture Texture { get; } + /// + /// Raised on the main thread when the producer creates, replaces (for example after a resize) or releases its texture. + /// The argument is the new texture, or null when there is none any more. + /// event Action Changed; } } From e7d91f3412a337921256b01d5adea5fc59c3b4c6 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 10:54:31 +0200 Subject: [PATCH 14/26] plugins: document resource contracts, the host and settings sessions XML documentation for the resource contracts (kinds, descriptors, selections, controller, display, segment, audio and surface payloads), the ready-made signal sources, the plugin host and its quarantine rules, settings sessions and packaged game identification. One-line enums and interfaces are split so each member can carry its own comment; no code changed. --- .../Configuration/PluginSettingsSession.cs | 154 +++- .../Resources/ResourceContracts.cs | 749 +++++++++++++++++- .../Resources/ResourceSignal.cs | 149 +++- VisualPinball.Plugins/Runtime/PluginHost.cs | 285 ++++++- .../Plugins/PackagedGameInformation.cs | 34 +- 5 files changed, 1336 insertions(+), 35 deletions(-) diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs index 1bd472db1..d043f843b 100644 --- a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs @@ -13,7 +13,32 @@ namespace VisualPinball.Plugins { - /// A detached draft. Game drafts contain only overrides, and Apply rechecks dynamic catalogs. + /// + /// An editable, unsaved copy of one plugin's settings in one scope, either the global settings or one game's profile. Edit the + /// draft, preview the effective values, query catalogs and run actions, then apply. + /// + /// + /// The session is detached from the store: edits stay in until saves them, and + /// nothing reaches a running plugin before that. A game session's draft holds only that game's overrides; fields absent from it + /// inherit the global value. Applying saves only the fields edited in this session, so values changed elsewhere in the + /// meantime, such as by the volume mixer, are kept. Before saving, apply checks again that every selected catalog item, such as + /// a PinUP pack or a B2S backglass file, still exists and is available. + /// + /// The Player's plugin settings page opens one session for the selected plugin and scope, disposes it when the user switches + /// plugin or scope or leaves the page, and applies through its so live settings reach the running + /// game. A session is not thread-safe: use it from one thread, the owner thread in the Player. Only + /// guards against overlapping calls. + /// + /// + /// + /// using var session = new PluginSettingsSession(factory, host.Configuration, host.Platform, game, host.Resources.Snapshot); + /// session.Package = new PluginPackageReference(packagePath, title); + /// session.Set("Volume", 80); + /// var result = await session.ApplyAsync(cancellation, host); + /// ShowFeedback(result.Message); + /// + /// + /// public sealed class PluginSettingsSession : IDisposable { private readonly IPlayerPluginFactory _factory; @@ -24,17 +49,64 @@ public sealed class PluginSettingsSession : IDisposable private readonly CancellationTokenSource _lifetime = new CancellationTokenSource(); private PluginSettingsSnapshot _baseline; private readonly HashSet _resetFields = new HashSet(StringComparer.Ordinal); + /// + /// Whether a field has a pending change in this session: its draft value differs from the value loaded from, or last saved + /// to, the store, or cleared it. Only pending fields are saved by . + /// + /// The top-level field ID. public bool HasEdits(string field) => _resetFields.Contains(field) || !JToken.DeepEquals(_baseline.Value(field), Draft.Value(field)); private string[] ChangedFields() => _factory.Schema.Fields.Where(field => HasEdits(field.Id)).Select(field => field.Id).ToArray(); + /// The game whose profile this session edits, or null when it edits the global settings. Fixed for the session. public GameIdentity? Game { get; } - /// The table package being configured, passed to catalogs, actions and validation. + /// + /// The table package being configured, passed to catalogs, actions and validation as . + /// Null by default and for global sessions. The Player sets it for game sessions, so that the B2S plugin, for example, can + /// look for a backglass next to the package. + /// public PluginPackageReference? Package { get; set; } + /// + /// The edited layer as it would be stored: only the game's overrides in a game session, the global values in a global + /// session. A field absent from it inherits the global value or the default. , and + /// a successful replace the instance, so read the property again rather than keeping the snapshot. + /// Change values through , which also enforces each field's scope. + /// public PluginSettingsSnapshot Draft { get; private set; } + /// + /// The values the plugin would run with if the draft were saved: schema defaults, then the saved global values and the draft + /// in a game session, or the draft in a global session. Computed as a new snapshot on every read; in a game session that + /// includes reading the global settings file, so avoid reading it in a loop. + /// public PluginSettingsSnapshot Effective => Game.HasValue ? PluginConfigurationStore.Resolve(_factory.Schema, _store.Read(_factory.Descriptor, _factory.Schema), Draft) : PluginConfigurationStore.Resolve(_factory.Schema, Draft); + /// + /// Opens a session and loads the stored layer of the given scope as the draft. The file is read synchronously on the calling + /// thread. + /// + /// The plugin whose settings to edit. + /// + /// The settings store to read from, and to save to when runs without a host. The Player passes + /// . + /// + /// The platform ID passed to settings providers; the Player passes . + /// The game whose profile to edit, or null for the global settings. + /// + /// Returns the currently published resources, for catalog lookups and the availability check on apply. The Player passes + /// . Null means no resources. + /// + /// The stored game profile belongs to another game. public PluginSettingsSession(IPlayerPluginFactory factory, PluginConfigurationStore store, string platform, GameIdentity? game = null, Func>? resources = null) { _factory = factory; _store = store; _platform = platform; Game = game; _resources = resources ?? (() => Array.Empty()); Draft = store.Read(factory.Descriptor, factory.Schema, game); _baseline = Draft.Copy(); } + /// Changes a field in the draft and marks it as edited. Nothing is saved until . + /// The ID of a top-level field of the plugin's schema. + /// + /// The new value, copied into the draft. Null removes the field from the draft: in a game session it inherits the global + /// value again, in a global session it falls back to the default. + /// + /// + /// The schema has no field with that ID, or the field's does not allow editing it in this + /// session's scope. + /// public void Set(string fieldId, JToken? value) { var field = _factory.Schema.Fields.First(field => field.Id == fieldId); @@ -42,8 +114,18 @@ public void Set(string fieldId, JToken? value) Draft.Set(fieldId, value); _resetFields.Remove(fieldId); } + /// + /// Discards every pending edit and reset by reloading the stored layer from the store; the file is read synchronously. The + /// Player's Revert button calls it. + /// public void Revert() { Draft = _store.Read(_factory.Descriptor, _factory.Schema, Game); _baseline = Draft.Copy(); _resetFields.Clear(); } - /// Refreshes aliases without discarding edits already made in this draft. + /// + /// Takes over values that were saved elsewhere while the session is open, for fields without pending edits, so the page shows + /// them without discarding the user's own edits. The Player calls it from , for + /// example when the volume mixer changed a global setting that the open settings page also shows. + /// + /// The freshly read stored layer of this session's scope. + /// IDs of the top-level fields that were saved. Fields with pending edits are skipped. public void RefreshUnedited(PluginSettingsSnapshot saved, IEnumerable fields) { foreach (string id in fields) { @@ -51,12 +133,22 @@ public void RefreshUnedited(PluginSettingsSnapshot saved, IEnumerable fi Draft.Set(id, saved.Value(id)); _baseline.Set(id, saved.Value(id)); } } + /// + /// Empties the draft, so every field goes back to its inherited value (the global value in a game session, the default in a + /// global session), and marks every field editable in this scope as pending, so removes their stored + /// values. Nothing is saved until then. The Player's Reset button calls it. + /// public void Reset() { Draft = new PluginSettingsSnapshot(_factory.Schema.Version); foreach (var field in _factory.Schema.Fields) if (Game.HasValue ? field.Scope != ConfigurationScope.GlobalOnly : field.Scope != ConfigurationScope.GameOnly) _resetFields.Add(field.Id); } + /// + /// Checks against the schema and the plugin's . Catalog + /// selections are not checked here; checks them in addition. + /// + /// The issues found; empty when the settings are valid. public IReadOnlyList Validate() { var effective = Effective; @@ -64,8 +156,27 @@ public IReadOnlyList Validate() } private PluginChoiceRequest Request(PluginSettingsSnapshot settings, IReadOnlyList? resources = null) => new PluginChoiceRequest(Game, settings, _platform, resources) { Package = Package }; + /// + /// Lists the items a choice or screen layout field can select, through the plugin's + /// , based on the draft's effective values, and the + /// currently published resources. The task may complete on a worker thread. + /// + /// The field's . + /// Passed to the provider; cancel it when the field is no longer shown. + /// The provider's catalog, or an empty, complete catalog when the plugin has no settings provider. public Task DiscoverAsync(string provider, CancellationToken cancellation) => _factory.SettingsProvider?.DiscoverAsync(provider, Request(Effective, _resources()), cancellation) ?? Task.FromResult(new PluginChoiceCatalog("empty", Array.Empty())); + /// + /// Runs the action of an action field through , such as PinUP locating + /// LibVLC. When it succeeds and returns , the values of the fields editable in this + /// scope are put into the draft as pending edits; the user still has to apply them. + /// + /// The field's . + /// Passed to the provider. + /// The provider's result, or a failure when the plugin has no settings provider. + /// + /// was cancelled; the draft is then left unchanged. + /// public async Task RunActionAsync(string action, CancellationToken cancellation) { if (_factory.SettingsProvider == null) return new PluginActionResult(false, "This action is unavailable."); @@ -77,6 +188,38 @@ public async Task RunActionAsync(string action, Cancellation } return result; } + /// + /// Validates the pending edits and saves them. Only fields with pending edits (see ) are written; other + /// stored values, including ones changed elsewhere since the session opened, are kept. + /// + /// + /// The draft is frozen when the call starts. Apply merges it into the latest stored layer, validates the effective result + /// against the schema and , and asks the provider for a fresh catalog for every + /// visible choice or screen layout field, including those inside composite and collection records, to check that each + /// selected item still exists and is available. A selection missing from an incomplete catalog is accepted. Any issue fails + /// the apply and nothing is saved. + /// + /// With a , the edits are committed through , which also + /// applies live fields to a running plugin and raises . Without one they are saved + /// straight to the store and take effect with the next game. After a successful save the draft is reloaded from the store; + /// edits the user made while the apply ran stay pending. Calls are serialized: a second apply waits for the first. + /// + /// + /// Cancels the apply. cancels it too. + /// + /// The host to commit through, so that a running game picks up live settings. The Player always passes it. When given, the + /// task completes on the host's owner thread. + /// + /// + /// Success, or a failure whose message says why nothing was saved. Validation issues are listed one per line as + /// field: message; a selection inside a record is named by its path, such as Items[0].Pack. + /// + /// + /// The apply was cancelled, or the session disposed, before the settings were committed. + /// + /// + /// The stored settings were written by a newer plugin version and cannot be overwritten. + /// public async Task ApplyAsync(CancellationToken cancellation = default, PluginHost? host = null) { using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellation, _lifetime.Token); @@ -154,6 +297,11 @@ private void Rebase(PluginSettingsSnapshot frozen, HashSet frozenResets) } } } + /// + /// Cancels a running and makes later calls to it fail with . + /// An apply that has already started saving still completes, and the other members keep working. The Player disposes the + /// session when the user switches plugin or scope or leaves the settings page. + /// public void Dispose() { _lifetime.Cancel(); } } } diff --git a/VisualPinball.Plugins/Resources/ResourceContracts.cs b/VisualPinball.Plugins/Resources/ResourceContracts.cs index ba3eef40d..47dd28c90 100644 --- a/VisualPinball.Plugins/Resources/ResourceContracts.cs +++ b/VisualPinball.Plugins/Resources/ResourceContracts.cs @@ -9,28 +9,224 @@ namespace VisualPinball.Plugins { + /// + /// The type of data a resource carries. It decides which source interface the published object must implement and + /// is the first thing a filters on. + /// /// + /// rejects a source that does not implement the interface of its kind. + /// VPE publishes the game's controller, outputs, displays, segment displays and ROM audio when a game is prepared; + /// plugins publish surfaces (PinUP screens, the B2S backglass) and derived displays (the DMD plugin's colorized view). + /// /// carries every controller output by its native number through . /// It is separate from , the outputs as the table maps them, so a controller binding without a /// role stays unambiguous. + /// /// - public enum ResourceKind { Controller, Display, Segments, Audio, Input, Surface, Triggers, Outputs } - public enum ResourceView { Original, Identification, Presentation } - public enum ResourceBindingState { Waiting, Bound, Blocked, Disposed } - public enum ResourceChangeKind { Added, Changed, Removed } - [Flags] public enum DisplayEncoding { None = 0, Gray2 = 1, Gray4 = 2, Luminance8 = 4, Rgb24 = 8, Rgba32 = 16 } - public enum FrameColorSpace { Linear, Srgb } - public enum FrameAlpha { None, Straight, Premultiplied } - public enum PcmEncoding { Int16LittleEndian, Float32 } + public enum ResourceKind + { + /// + /// The game controller's switch, coil, lamp and GI events as the table maps them, addressed by the engine's IDs. + /// The source implements . VPE publishes one per game with the controller's ID + /// and the role controller; PinUP turns its events into PUP triggers. + /// + Controller, + /// + /// A dot-matrix or video display delivering s through . + /// VPE publishes each DMD as <controller>/display/<id> with the role dmd. + /// + Display, + /// + /// A segment (alphanumeric or numeric) display delivering s through + /// . VPE publishes each one as <controller>/segments/<id> with + /// the role segments; the B2S backglass draws them as its score digits. + /// + Segments, + /// + /// A PCM audio stream delivering s through . VPE publishes + /// an engine's own sound (PinMAME's ROM audio) as <controller>/audio/rom with the role rom-mix. + /// + Audio, + /// + /// Player input actions delivering s through . Nothing + /// in VPE publishes this kind yet. + /// + Input, + /// + /// An image other plugins can show, described by . PinUP publishes each PUP screen and + /// the B2S plugin its composed backglass as a surface. + /// + Surface, + /// + /// Named trigger events delivered as s through . + /// Nothing in VPE publishes this kind yet. + /// + Triggers, + /// + /// Every controller output by its native number, including outputs the table does not map, delivered through + /// . VPE publishes it as <controller>/outputs with the role + /// outputs for engines that report native outputs, such as PinMAME; the B2S backglass prefers it over + /// to drive ROM lamps and solenoids. + /// + Outputs + } + + /// + /// Which version of a resource's content is meant: what the controller produced, frames suited to recognizing that + /// content, or a version processed for showing. + /// + /// + /// A declares its view in , and a + /// states the view it wants in . The + /// difference matters for displays: PinUP and the DMD plugin's colorization need the controller's raw shades + /// (), while the B2S live DMD wants the colorized frames the DMD plugin publishes + /// (), or the original when there are none. + /// + public enum ResourceView + { + /// + /// As a descriptor view: the unmodified output of its producer, in whatever encoding the producer currently + /// delivers. As an intent: match only original resources that are not derived from another resource. This is the + /// default for both. + /// + Original, + /// + /// As an intent: match original, non-derived resources and, for displays, request unmodified + /// or frames through + /// for as long as the binding holds the source. Used to identify + /// content, such as PinUP triggers on DMD frames or the DMD plugin's colorization matching. As a descriptor view it + /// marks a resource whose frames are meant for identification; no VPE producer publishes that view yet. + /// + Identification, + /// + /// As a descriptor view: frames processed for showing, typically derived from an original resource listed in + /// , such as the DMD plugin's colorized RGB view of a ROM's DMD. As an + /// intent: match presentation resources, and original ones too when + /// is set. + /// + Presentation + } + + /// The state of a , readable through its State property. + public enum ResourceBindingState + { + /// + /// No published resource matches the selection yet. The binding attaches automatically when one is published. + /// + Waiting, + /// Exactly one resource matches and the binding's receivers are connected to it. + Bound, + /// + /// The binding cannot attach: either several resources match and the selection must name one (for example with + /// ), or the source refused the requested view. The binding's + /// Diagnostic says which. + /// + Blocked, + /// The binding was disposed and no longer follows the registry. + Disposed + } + + /// What happened to a resource, as reported to callbacks. + public enum ResourceChangeKind + { + /// The resource was published. + Added, + /// + /// The resource is still published but changed in a way that may let a refused consumer bind now, such as a display + /// giving up the raw encoding another consumer could not accept. Raised by + /// . A resource that is replaced is reported as + /// followed by instead. + /// + Changed, + /// The resource's publication was disposed. + Removed + } + + /// + /// Pixel encodings of display frames. As flags it lists the encodings a source can deliver or a consumer accepts; a + /// single always carries exactly one of them. + /// + /// + /// Producers advertise their encodings in , and consumers state theirs in + /// . PinMAME, for example, advertises + /// Gray2 | Gray4 | Luminance8, delivers by default and switches to raw + /// or while an identification consumer such as PinUP is bound. + /// + [Flags] + public enum DisplayEncoding + { + /// No encoding. Used for "nothing advertised" and "nothing negotiated"; never valid for a frame. + None = 0, + /// One byte per pixel holding a raw shade from 0 to 3, as a 2-bit DMD controller produces it. + Gray2 = 1, + /// One byte per pixel holding a raw shade from 0 to 15, as a 4-bit DMD controller produces it. + Gray4 = 2, + /// One byte per pixel holding a brightness from 0 to 255. + Luminance8 = 4, + /// Three bytes per pixel, in red, green, blue order, each from 0 to 255. + Rgb24 = 8, + /// + /// Four bytes per pixel, in red, green, blue, alpha order, each from 0 to 255. How alpha applies is given by + /// . + /// + Rgba32 = 16 + } + + /// The color space of a display frame's color values. + public enum FrameColorSpace + { + /// Values are linear light intensities. + Linear, + /// Values are sRGB-encoded. This is the default of . + Srgb + } + + /// How the alpha channel of a frame is to be interpreted. + public enum FrameAlpha + { + /// The frame has no meaningful alpha; treat it as opaque. This is the default. + None, + /// Color values are not multiplied by alpha. + Straight, + /// Color values are already multiplied by alpha. + Premultiplied + } + + /// The sample format of a . + public enum PcmEncoding + { + /// Signed 16-bit integer samples, little endian, two bytes each. + Int16LittleEndian, + /// 32-bit IEEE float samples, nominally from -1 to 1, four bytes each. PinMAME's ROM audio uses this. + Float32 + } + + /// What a reports. public enum ControllerEventKind { - Switch, Coil, Lamp, GeneralIllumination, Trigger, State, + /// A switch changed; the value is 1 when closed and 0 when open. + Switch, + /// A coil (solenoid) changed. + Coil, + /// A lamp changed. + Lamp, + /// A general illumination string changed. + GeneralIllumination, + /// Reserved for controller triggers; VPE does not emit it yet. + Trigger, + /// Reserved for controller state values; VPE does not emit it yet. + State, /// Raw position of an emulated mech, as the controller reports it. Mech, /// Ends one controller poll; consumers that batch per poll apply what they collected. PollEnd } /// What a controller event's raw value means. + /// + /// One controller can mix encodings within one output kind, so each event of an + /// resource carries its own semantics. PinMAME, for instance, reports physically faded lamps on some hardware + /// generations, binary lamps on others, and GI as levels from 0 to 8 on WPC. + /// public enum ControllerValueSemantics { /// 0 or 1. @@ -43,20 +239,92 @@ public enum ControllerValueSemantics Position } + /// + /// Describes one published resource: its stable ID, who published it, which game controller it belongs to, what it is + /// for and which version of the content it carries. + /// + /// + /// A producer creates a descriptor and passes it with its source to ; + /// consumers see descriptors in , in change notifications and on a bound + /// . VPE publishes the game's controller, outputs, displays, segment displays and + /// audio with IDs below the controller ID, such as pinmame:t2_l8/controller/0/display/dmd0. Descriptors are + /// immutable; to change one, dispose the publication and publish again with a new . + /// + /// The B2S plugin publishes its composed backglass for other plugins: + /// + /// game.Lifetime.Own(game.Resources.Publish(new PluginResourceDescriptor(game.ControllerId + "/surface/b2s-backglass", + /// _application.Plugin.Id, game.ControllerId, "B2S backglass", "backglass", ResourceKind.Surface, game.Session, 1), _surface)); + /// + /// + /// public sealed class PluginResourceDescriptor { + /// + /// Stable logical ID, unique among the resources published at the same time. By convention it starts with the + /// controller ID (see ), so saved choices can store the part after it. + /// A selection names a resource by this ID in . + /// public string Id { get; } + /// + /// Who published the resource: the game identity's provider (such as pinmame) for resources VPE publishes + /// from the gamelogic engine, the plugin ID for resources a plugin publishes. Informational; selections do not + /// filter on it. + /// public string Provider { get; } + /// + /// ID of the game controller the resource belongs to, normally . Selections + /// filter on it with . + /// public string Controller { get; } + /// Human-readable name shown in settings, such as the display choices of the DMD and PinUP plugins. public string Label { get; } + /// + /// What the resource is for, such as controller, outputs, dmd, segments, + /// rom-mix or backglass. Selections filter on it with . + /// public string Role { get; } + /// The type of data the resource carries; the published source must implement the matching interface. public ResourceKind Kind { get; } + /// + /// The game session the resource belongs to (see ). Selections filter on it, + /// and a derived resource is only visible while its parents belong to the same session. + /// public long Session { get; } + /// + /// Generation of this publication. Use a new value every time a resource is published, including when the same + /// is published again; VPE uses a process-wide counter. Display frames carry it in + /// so bindings drop frames of an earlier publication, and a derived + /// resource is hidden when a parent is republished with another instance. + /// public long Instance { get; } + /// Version of the resource contract the descriptor follows. Currently always 1. public int ContractVersion { get; } = 1; + /// Which version of the content the resource carries; by default. public ResourceView View { get; } + /// + /// For resources, every encoding the source can deliver. A display selection only + /// matches when it accepts at least one of them. for other kinds. + /// public DisplayEncoding Formats { get; } + /// + /// IDs of the resources this one is derived from, such as the original DMD a colorized view is computed from; empty + /// for original resources. Parents must be published first. The registry hides a derived resource while any parent + /// is missing, has been republished with another or belongs to another . + /// public IReadOnlyList Parents { get; } + /// Creates a descriptor. The parent list is copied. + /// The stable logical ID; see . + /// The publisher; see . + /// The controller ID the resource belongs to. + /// The human-readable name. + /// What the resource is for; see . + /// The type of data; must match the source's . + /// The game session. + /// The generation of this publication. + /// Which version of the content the resource carries. + /// For displays, the encodings the source can deliver. + /// IDs of the resources this one is derived from. + /// is null, empty or white space. public PluginResourceDescriptor(string id, string provider, string controller, string label, string role, ResourceKind kind, long session, long instance, ResourceView view = ResourceView.Original, DisplayEncoding formats = DisplayEncoding.None, params string[] parents) { @@ -66,17 +334,72 @@ public PluginResourceDescriptor(string id, string provider, string controller, s } } + /// + /// Describes which resource a consumer wants. Empty strings and a zero session match anything, so a selection names + /// only what it cares about. + /// + /// + /// Pass it to to follow the matching resource as it is published, replaced + /// and removed, or to to list the current + /// matches. Bind copies the selection, so changing it afterwards has no effect on the binding. A binding attaches only + /// when exactly one resource matches; when several do, set . + /// + /// PinUP binds the game's DMD for raw shades, honouring a display the user picked: + /// + /// var displays = new PluginResourceSelection(ResourceKind.Display) { + /// Controller = game.ControllerId, Role = "dmd", Session = game.Session, + /// Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray2 | DisplayEncoding.Gray4 + /// }; + /// string selected = game.Settings.String("DmdSource", "auto"); + /// if (selected != "auto") displays.ResourceId = PluginResourceChoices.Resolve(game.ControllerId, selected); + /// var dmd = game.Lifetime.Own(game.Resources.Bind<IPluginDisplaySource>(displays)); + /// + /// + /// public sealed class PluginResourceSelection { + /// The kind of resource wanted. Always matched. public ResourceKind Kind { get; } + /// + /// The exact wanted, or empty (the default) for any. Use it to pick one of + /// several matching resources, for example from a saved choice through . + /// public string ResourceId { get; set; } = ""; + /// The controller ID wanted, normally , or empty for any. public string Controller { get; set; } = ""; + /// The wanted, such as dmd, or empty for any. public string Role { get; set; } = ""; + /// + /// The game session wanted, normally , or 0 (the default) for any. Set it so a + /// binding never attaches to a resource of a previous game. + /// public long Session { get; set; } + /// + /// The view wanted; by default. matches + /// original resources without parents. matches resources that are neither + /// presentation views nor derived, and makes a display binding request raw frames from the source. + /// matches presentation views, plus originals when + /// is set. + /// public ResourceView Intent { get; set; } = ResourceView.Original; + /// + /// For selections, the encodings the consumer can read; all five concrete + /// encodings by default. A display only matches when its overlap. + /// A binding also passes it to , and + /// drops frames in other encodings. Ignored for other kinds. + /// public DisplayEncoding Accepts { get; set; } = DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8 | DisplayEncoding.Rgb24 | DisplayEncoding.Rgba32; + /// + /// With a intent, also match original resources. When no + /// is set and presentation views exist, the registry still prefers them, so the consumer + /// switches between a processed and the original version as producers come and go. The B2S live DMD uses it to show + /// the DMD plugin's colorized frames when available and the ROM's frames otherwise. False by default. + /// public bool AllowOriginalFallback { get; set; } + /// Creates a selection that matches any resource of a kind; narrow it with the other properties. + /// The kind of resource wanted. public PluginResourceSelection(ResourceKind kind) { Kind = kind; } + /// Returns an independent copy with the same criteria. public PluginResourceSelection Copy() => new PluginResourceSelection(Kind) { ResourceId = ResourceId, Controller = Controller, Role = Role, Session = Session, Intent = Intent, Accepts = Accepts, AllowOriginalFallback = AllowOriginalFallback @@ -92,28 +415,101 @@ internal bool Matches(PluginResourceDescriptor descriptor) } } + /// A resource was published, changed or removed, as reported to callbacks. + /// + /// The B2S plugin observes the registry to attach every segment display of the game as soon as it is published, since + /// segment displays can appear after the game has started. + /// public readonly struct PluginResourceChange { + /// Whether the resource was added, changed or removed. public ResourceChangeKind Kind { get; } + /// The descriptor of the resource that changed. public PluginResourceDescriptor Descriptor { get; } + /// Creates a change notification. Called by the registry. + /// What happened. + /// The resource it happened to. public PluginResourceChange(ResourceChangeKind kind, PluginResourceDescriptor descriptor) { Kind = kind; Descriptor = descriptor; } } - public interface IPluginResourceSource { ResourceKind Kind { get; } } + /// + /// Base of every resource source. A published object implements the interface of its kind, such as + /// , which derives from this one. + /// + /// + /// Plugins rarely implement the kind interfaces themselves: , + /// , , , + /// and handle subscriptions and dispatch. + /// Surfaces have no ready-made implementation; PinUP and the B2S plugin implement . + /// + public interface IPluginResourceSource + { + /// + /// The kind of data this source carries. Must equal the it is published with. + /// + ResourceKind Kind { get; } + } - /// Borrowed row-major frame memory is valid only during the synchronous subscriber call. + /// One display frame. Borrowed row-major frame memory is valid only during the synchronous subscriber call. + /// + /// Producers usually reuse the pixel buffer for the next frame, so a consumer that needs the pixels later must copy them + /// before returning, as the DMD plugin does when it hands frames to its LibDmd worker. The constructor validates that + /// the buffer is large enough for the declared shape. + /// + /// Publishing a 128x32 frame of 4-bit shades: + /// + /// source.Publish(new PluginDisplayFrame(shades, 128, 32, 128, DisplayEncoding.Gray4, ++_sequence, timestampUs, descriptor.Instance)); + /// + /// + /// public readonly struct PluginDisplayFrame { + /// + /// The pixels, row by row, each row starting bytes after the previous one. Borrowed for the + /// duration of the subscriber call only. + /// public ReadOnlyMemory Pixels { get; } + /// Width in pixels; greater than zero. public int Width { get; } + /// Height in pixels; greater than zero. public int Height { get; } + /// Bytes from the start of one row to the start of the next; at least the width times the bytes per pixel. public int Stride { get; } + /// The single concrete encoding of . public DisplayEncoding Encoding { get; } + /// The color space of the values; by default. public FrameColorSpace ColorSpace { get; } + /// How to interpret alpha for ; by default. public FrameAlpha Alpha { get; } + /// + /// Increasing number assigned by the producer. VPE's gamelogic resources share one counter across all their events, + /// so gaps do not mean dropped frames. + /// public long Sequence { get; } + /// + /// When the frame was produced, in microseconds of a monotonic clock (VPE uses Stopwatch). Not wall-clock + /// time; compare only values of the same producer. + /// public long TimestampUs { get; } + /// + /// The of the publication that produced the frame. + /// drops frames whose instance differs from the bound one. + /// public long Instance { get; } + /// Creates a frame over a borrowed buffer. + /// The pixel buffer; at least times bytes. + /// Width in pixels. + /// Height in pixels. + /// Bytes per row, including any padding. + /// Exactly one concrete encoding. + /// The producer's increasing frame number. + /// Monotonic time in microseconds. + /// The publication's . + /// The color space of the values. + /// How to interpret alpha. + /// + /// The encoding is not a single concrete encoding, or the size, stride or buffer length do not fit together. + /// public PluginDisplayFrame(ReadOnlyMemory pixels, int width, int height, int stride, DisplayEncoding encoding, long sequence, long timestampUs, long instance, FrameColorSpace colorSpace = FrameColorSpace.Srgb, FrameAlpha alpha = FrameAlpha.None) { @@ -126,47 +522,178 @@ public PluginDisplayFrame(ReadOnlyMemory pixels, int width, int height, in } } + /// A source of display frames, published as . + /// + /// VPE publishes each DMD of the game's engine; the DMD plugin publishes a colorized + /// view derived from it. Producers normally use . Consumers should bind through + /// and receive frames with + /// , which also checks encoding and instance. + /// + /// + /// var dmd = game.Lifetime.Own(game.Resources.Bind<IPluginDisplaySource>(selection)); + /// game.Lifetime.Own(dmd.SubscribeFrames(frame => Render(frame.Pixels.Span, frame.Width, frame.Height, frame.Stride))); + /// + /// + /// public interface IPluginDisplaySource : IPluginResourceSource { + /// + /// Registers a receiver for every frame. It is called synchronously on the producer's thread (the main thread for + /// VPE's displays) and must not keep the borrowed pixels. + /// + /// Called with each frame. + /// Disposing it stops the receiver. IDisposable Subscribe(Action receive); - /// Acquiring an identification view owns format negotiation until the returned lease is disposed. + /// + /// Declares the view a consumer needs while it is attached; bindings call it for you. Acquiring an identification + /// view owns format negotiation until the returned lease is disposed: the source switches to raw shades the consumer + /// accepts (see ). Other views do not change the source. + /// + /// The view the consumer wants. + /// The encodings the consumer can read. + /// A lease; disposing it releases the request. + /// + /// The source cannot deliver an identification view in an accepted encoding, for example because another consumer + /// already holds it in an encoding this one does not accept. A binding then becomes + /// . + /// IDisposable AcquireView(ResourceView intent, DisplayEncoding acceptedFormats); } + /// One event of a game controller: a switch, coil, lamp, GI, mech or end-of-poll notification. /// - /// Events of a controller-native resource address outputs by the controller's own numbering + /// Events of a resource are the table's mapped events: is the + /// engine's ID of the switch, coil or lamp, is -1 and is the engine's value + /// (0 or 1 for switches, the 0..1 strength for coils, the engine's own range for lamps such as 0..255 for PinMAME). + /// + /// Events of an resource address outputs by the controller's own numbering /// () and describe their raw value per address, because one controller can mix binary and - /// physically faded outputs of the same kind. + /// physically faded outputs of the same kind. Each poll ends with a event. + /// + /// + /// The B2S backglass normalizes a native lamp to 0..1: + /// + /// if (value.Kind == ControllerEventKind.Lamp && value.Number >= 0) SetLamp(value.Number, (float)(value.Value / value.Maximum)); + /// + /// /// public readonly struct PluginControllerEvent { + /// What the event reports. public ControllerEventKind Kind { get; } + /// + /// The output's ID: the engine's switch, coil or lamp ID for mapped events, the native number as text for native + /// ones, and empty for . + /// public string Id { get; } + /// The raw value; see the type remarks for mapped events and for native ones. public double Value { get; } + /// + /// Increasing number assigned by the producer. VPE shares one counter across all events of a game's resources. + /// public long Sequence { get; } + /// When the event was emitted, in microseconds of a monotonic clock; not wall-clock time. public long TimestampUs { get; } /// Controller-native output number, or -1 when the event is addressed by only. public int Number { get; } + /// + /// What means. Only meaningful for native events; mapped events are created with + /// whatever their actual range. + /// public ControllerValueSemantics Semantics { get; } - /// Raw value of a fully on output (or the largest level); 1 for binary outputs. + /// + /// Raw value of a fully on output (or the largest level); 1 for binary outputs. Always greater than zero: the + /// constructor replaces zero or negative values with 1. + /// public double Maximum { get; } + /// Creates a mapped event, addressed by ID, with no native number, binary semantics and a maximum of 1. + /// What the event reports. + /// The engine's ID of the output. + /// The engine's value. + /// The producer's increasing event number. + /// Monotonic time in microseconds. public PluginControllerEvent(ControllerEventKind kind, string id, double value, long sequence, long timestampUs) : this(kind, id, value, sequence, timestampUs, -1, ControllerValueSemantics.Binary, 1) { } + /// Creates an event addressed by native number, with its value semantics. + /// What the event reports. + /// The output's ID, usually the number as text. + /// The raw value. + /// The producer's increasing event number. + /// Monotonic time in microseconds. + /// The controller-native output number, or -1. + /// What means. + /// The value of a fully on output; values of zero or below become 1. public PluginControllerEvent(ControllerEventKind kind, string id, double value, long sequence, long timestampUs, int number, ControllerValueSemantics semantics, double maximum) { Kind = kind; Id = id; Value = value; Sequence = sequence; TimestampUs = timestampUs; Number = number; Semantics = semantics; Maximum = maximum > 0 ? maximum : 1; } } - public interface IPluginControllerSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } + /// + /// A source of controller events, published as (mapped events) or + /// (native outputs). + /// + /// + /// VPE publishes both for the game's engine using . PinUP subscribes to the mapped + /// events to fire PUP triggers; the B2S backglass prefers the native outputs and falls back to mapped events. + /// + /// + /// var controller = game.Lifetime.Own(game.Resources.Bind<IPluginControllerSource>( + /// new PluginResourceSelection(ResourceKind.Controller) { Controller = game.ControllerId, Session = game.Session })); + /// game.Lifetime.Own(controller.Connect(source => source.Subscribe(OnControllerEvent))); + /// + /// + /// + public interface IPluginControllerSource : IPluginResourceSource + { + /// + /// Registers a receiver for every event. It is called synchronously on the producer's thread; VPE publishes on the + /// main thread. + /// + /// Called with each event. + /// Disposing it stops the receiver. + IDisposable Subscribe(Action receive); + } + /// One block of PCM audio. The sample memory is borrowed for the synchronous subscriber call only. + /// + /// Samples are interleaved: each sample frame holds one sample per channel. PinMAME publishes every block its sound + /// board renders as , from the emulator's audio callback rather than the main thread. + /// + /// + /// int length = frames * channels * sizeof(float); + /// source.Publish(new PluginPcmFrame(buffer.AsMemory(0, length), PcmEncoding.Float32, 48000, channels, frames, ++_sequence)); + /// + /// + /// public readonly struct PluginPcmFrame { + /// The interleaved samples, exactly times samples long. public ReadOnlyMemory Samples { get; } + /// The sample format. public PcmEncoding Encoding { get; } + /// Sample frames per second, in Hz; greater than zero. public int SampleRate { get; } + /// Number of interleaved channels; greater than zero. public int Channels { get; } + /// Number of sample frames in the block (samples per channel); zero or more. public int Frames { get; } + /// Increasing block number assigned by the producer. public long Sequence { get; } + /// When the block was produced, in microseconds of a monotonic clock, or 0 when unknown (PinMAME leaves it 0). public long TimestampUs { get; } + /// Free-form description of the channel order, or empty when unspecified. public string ChannelLayout { get; } + /// Creates a block over borrowed samples. + /// The interleaved samples. + /// The sample format. + /// Sample frames per second. + /// Number of interleaved channels. + /// Number of sample frames. + /// The producer's increasing block number. + /// Monotonic time in microseconds, or 0 when unknown. + /// Description of the channel order, or empty. + /// + /// The rate or channel count is not positive, the frame count is negative, or the sample length does not equal + /// frames times channels times bytes per sample. + /// public PluginPcmFrame(ReadOnlyMemory samples, PcmEncoding encoding, int sampleRate, int channels, int frames, long sequence, long timestampUs = 0, string channelLayout = "") { if (sampleRate <= 0 || channels <= 0 || frames < 0 || samples.Length != checked(frames * channels * (encoding == PcmEncoding.Float32 ? 4 : 2))) @@ -174,60 +701,226 @@ public PluginPcmFrame(ReadOnlyMemory samples, PcmEncoding encoding, int sa Samples = samples; Encoding = encoding; SampleRate = sampleRate; Channels = channels; Frames = frames; Sequence = sequence; TimestampUs = timestampUs; ChannelLayout = channelLayout; } } - public interface IPluginAudioSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } + /// A source of PCM audio, published as . + /// + /// VPE publishes the engine's own sound, such as PinMAME's ROM audio, as <controller>/audio/rom with the + /// role rom-mix. Producers normally use , which also implements + /// . + /// + public interface IPluginAudioSource : IPluginResourceSource + { + /// + /// Registers a receiver for every block. It is called synchronously on the producer's thread, which for PinMAME is + /// the emulator's audio thread, so it must be fast, thread-safe and copy any samples it keeps. + /// + /// Called with each block. + /// Disposing it stops the receiver. + IDisposable Subscribe(Action receive); + } /// An exclusive lease replaces built-in playback while leaving the original PCM stream available. - public interface IPluginAudioPlaybackControl { IDisposable AcquirePlayback(string pluginId); } + /// + /// Implemented by audio sources whose producer can stop playing its sound itself, such as + /// . A plugin that plays or processes the sound on its own (for example to send it to a + /// different device) casts the bound source to this interface and holds a lease for as long as it plays; PinMAME then + /// silences its own output but keeps publishing PCM. + /// + /// + /// var audio = game.Lifetime.Own(game.Resources.Bind<IPluginAudioSource>(new PluginResourceSelection(ResourceKind.Audio) { + /// Controller = game.ControllerId, Role = "rom-mix", Session = game.Session })); + /// if (audio.Source is IPluginAudioPlaybackControl control) game.Lifetime.Own(control.AcquirePlayback(_application.Plugin.Id)); + /// + /// + /// + public interface IPluginAudioPlaybackControl + { + /// Takes over playback until the returned lease is disposed, which restores the producer's own playback. + /// ID of the plugin taking over, reported to a plugin that tries to take over as well. + /// The lease. + /// Another plugin already holds the lease. + IDisposable AcquirePlayback(string pluginId); + } /// A segment display. Its place and size are known when it is published, before any frame arrives. + /// + /// VPE publishes one per segment display the engine announces, as <controller>/segments/<id> with the + /// role segments, using . The B2S backglass concatenates the digits of all + /// displays in onto its score digits, so it needs every display's position and size before the + /// first frame of any of them arrives. + /// public interface IPluginSegmentSource : IPluginResourceSource { - /// Position among the controller's segment displays, in the order the controller announced them. + /// Position among the controller's segment displays, in the order the controller announced them, from 0. int Order { get; } - /// Number of elements (digits) the controller announced; frames carry the same count. + /// + /// Number of elements (digits) the controller announced; frames carry the same count. 0 when the producer did not + /// announce a size, in which case consumers learn it from the first frame. + /// int Elements { get; } /// Segment arrangement of the elements, as in . string Layout { get; } - /// Display technology hint, as in . + /// + /// Display technology hint, as in : a free-form, case-sensitive string made + /// of tokens. It may hold a technology token such as plasma, vfd-green, vfd-blue or led, + /// and/or gottlieb for upright Gottlieb System 80 digits. Empty means unknown. Match tokens with + /// Contains or StartsWith, not equality. + /// string Hardware { get; } + /// + /// Registers a receiver for every frame. It is called synchronously on the producer's thread; VPE publishes on the + /// main thread. + /// + /// Called with each frame. + /// Disposing it stops the receiver. IDisposable Subscribe(Action receive); } + /// The state of every segment of a segment display at one moment. /// /// names the segment arrangement of the elements: seg7 (a-g and a comma at bit 7), /// seg9 (seven segments plus split verticals at bits 8 and 9), seg14 (a-f, g1, comma, h, i, j, g2, - /// k, l, m, dot) or seg16. hints at the display technology: plasma, - /// vfd-green, vfd-blue, led, optionally followed by +gottlieb. Brightness is borrowed - /// for the synchronous callback only. + /// k, l, m, dot) or seg16. is a free-form hint about the display technology. Brightness + /// is borrowed for the synchronous callback only. + /// + /// VPE publishes PinMAME's segment displays with 16 segments per element, one per bit of PinMAME's 16-bit segment mask, + /// each fully on (1) or off (0). + /// /// public readonly struct PluginSegmentFrame { + /// The segment arrangement: seg7, seg9, seg14 or seg16; see the type remarks. public string Layout { get; } + /// Number of elements (digits); greater than zero. public int Elements { get; } + /// + /// Number of brightness values per element; greater than zero. VPE always sends 16, and the B2S backglass ignores + /// frames with another count. + /// public int SegmentsPerElement { get; } + /// + /// Brightness of each segment from 0 (off) to 1 (fully lit), element by element: segment s of element + /// e is at index e * SegmentsPerElement + s. Borrowed for the duration of the subscriber call only. + /// public ReadOnlyMemory Brightness { get; } + /// + /// Display technology hint: a free-form, case-sensitive string made of tokens. It may hold a technology token such + /// as plasma, vfd-green, vfd-blue or led, and/or gottlieb for upright Gottlieb + /// System 80 digits. Empty (never null) means unknown. Match tokens with Contains or StartsWith, not + /// equality. PinMAME sends gottlieb for Gottlieb System 80 and an empty string otherwise; the B2S backglass + /// picks its style from a vfd-green, vfd-blue or led prefix (plasma otherwise) and draws + /// upright digits when the hint contains gottlieb. + /// public string Hardware { get; } + /// Creates a frame without a hardware hint. + /// The segment arrangement. + /// Number of elements. + /// Number of brightness values per element. + /// The borrowed brightness values, exactly elements times segments per element long. + /// A count is not positive or the brightness length does not match. public PluginSegmentFrame(string layout, int elements, int segmentsPerElement, ReadOnlyMemory brightness) : this(layout, elements, segmentsPerElement, brightness, "") { } + /// Creates a frame with a hardware hint. + /// The segment arrangement. + /// Number of elements. + /// Number of brightness values per element. + /// The borrowed brightness values, exactly elements times segments per element long. + /// The technology hint; null becomes empty. + /// A count is not positive or the brightness length does not match. public PluginSegmentFrame(string layout, int elements, int segmentsPerElement, ReadOnlyMemory brightness, string hardware) { if (elements <= 0 || segmentsPerElement <= 0 || brightness.Length != checked(elements * segmentsPerElement)) throw new ArgumentException("Segment layout does not match its state."); Layout = layout; Elements = elements; SegmentsPerElement = segmentsPerElement; Brightness = brightness; Hardware = hardware ?? ""; } } + /// One player input action, such as a nudge axis or a button. + /// + /// Carried by and transformed by s in a + /// . Nothing in VPE produces input events for plugins yet. + /// public readonly struct PluginInputEvent { + /// Name of the action, such as nudge-x. public string Action { get; } + /// The action's value, such as an axis position or 1 and 0 for a pressed and released button. public float Value { get; } + /// When the input happened, in microseconds of a monotonic clock. public long TimestampUs { get; } + /// Creates an input event. + /// Name of the action. + /// The action's value. + /// Monotonic time in microseconds. public PluginInputEvent(string action, float value, long timestampUs) { Action = action; Value = value; TimestampUs = timestampUs; } } - public interface IPluginInputSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } - public interface IPluginInputFilter { PluginInputEvent Filter(in PluginInputEvent input); } - public interface IPluginSurface : IPluginResourceSource { int Width { get; } int Height { get; } string Backend { get; } } + /// + /// A source of player input events, published as . Producers can use + /// . + /// + public interface IPluginInputSource : IPluginResourceSource + { + /// Registers a receiver for every event, called synchronously on the producer's thread. + /// Called with each event. + /// Disposing it stops the receiver. + IDisposable Subscribe(Action receive); + } + /// Transforms an input event before it reaches the game, registered in a . + /// + /// Filters run by the order given at registration, then by plugin ID; each receives the previous filter's result. + /// + /// A filter that halves nudge strength: + /// + /// public PluginInputEvent Filter(in PluginInputEvent input) + /// => input.Action == "nudge-x" ? new PluginInputEvent(input.Action, input.Value * 0.5f, input.TimestampUs) : input; + /// + /// + /// + public interface IPluginInputFilter + { + /// Returns the event to pass on; return unchanged to let it through. + /// The event as the previous filter left it. + PluginInputEvent Filter(in PluginInputEvent input); + } + /// An image other plugins can show, published as . + /// + /// The interface only describes the image; tells consumers which richer interface gives access to + /// it. PinUP publishes each PUP screen and the B2S plugin its composed backglass with the backend + /// unity-render-texture, whose sources implement the Unity integration's IUnityTextureResource. + /// + public interface IPluginSurface : IPluginResourceSource + { + /// Current width in pixels; 0 while the producer has no image yet. + int Width { get; } + /// Current height in pixels; 0 while the producer has no image yet. + int Height { get; } + /// + /// How the image is accessed, such as unity-render-texture. Consumers check it before casting the source to + /// the matching interface. + /// + string Backend { get; } + } + /// One named trigger with an integer value, carried by . + /// Nothing in VPE publishes trigger resources yet. public readonly struct PluginTriggerEvent { + /// Name of the trigger. public string Id { get; } + /// The trigger's value. public int Value { get; } + /// Increasing number assigned by the producer. public long Sequence { get; } + /// When the trigger fired, in microseconds of a monotonic clock. public long TimestampUs { get; } + /// Creates a trigger event. + /// Name of the trigger. + /// The trigger's value. + /// The producer's increasing event number. + /// Monotonic time in microseconds. public PluginTriggerEvent(string id, int value, long sequence, long timestampUs) { Id = id; Value = value; Sequence = sequence; TimestampUs = timestampUs; } } - public interface IPluginTriggerSource : IPluginResourceSource { IDisposable Subscribe(Action receive); } + /// + /// A source of trigger events, published as . Producers can use + /// . + /// + public interface IPluginTriggerSource : IPluginResourceSource + { + /// Registers a receiver for every event, called synchronously on the producer's thread. + /// Called with each event. + /// Disposing it stops the receiver. + IDisposable Subscribe(Action receive); + } } diff --git a/VisualPinball.Plugins/Resources/ResourceSignal.cs b/VisualPinball.Plugins/Resources/ResourceSignal.cs index 1c519e8b2..472a16176 100644 --- a/VisualPinball.Plugins/Resources/ResourceSignal.cs +++ b/VisualPinball.Plugins/Resources/ResourceSignal.cs @@ -10,15 +10,22 @@ namespace VisualPinball.Plugins { - /// Subscription changes take a lock; synchronous producer dispatch reads a cached array. + /// + /// Thread-safe fan-out of values to subscribers. Subscription changes take a lock; synchronous producer dispatch reads a + /// cached array, so publishing allocates nothing and may happen on any thread, such as an emulator's audio thread. + /// internal sealed class ResourceSignal : IDisposable where T : struct { private readonly List> _subscribers = new List>(); private readonly Action _fault; private Action[] _dispatch = Array.Empty>(); private bool _disposed; + /// Receives exceptions thrown by subscribers; null ignores them. internal ResourceSignal(Action? fault = null) { _fault = fault ?? (_ => { }); } + /// Whether at least one subscriber is registered. Safe to read from any thread. internal bool HasSubscribers => Volatile.Read(ref _dispatch).Length > 0; + /// Adds a subscriber; disposing the result removes it. + /// The signal was disposed. internal IDisposable Subscribe(Action receive) { lock (_subscribers) { @@ -27,56 +34,196 @@ internal IDisposable Subscribe(Action receive) } return new Subscription(() => { lock (_subscribers) { _subscribers.Remove(receive); Volatile.Write(ref _dispatch, _subscribers.ToArray()); } }); } + /// Calls every subscriber on the calling thread; an exception goes to the fault handler and dispatch continues. internal void Publish(in T frame) { foreach (var receive in Volatile.Read(ref _dispatch)) try { receive(frame); } catch (Exception error) { _fault(error); } } + /// Drops all subscribers and refuses new ones. public void Dispose() { lock (_subscribers) { _disposed = true; _subscribers.Clear(); Volatile.Write(ref _dispatch, Array.Empty>()); } } } + /// + /// Ready-made source for producers: it dispatches s to + /// subscribers and lets one plugin take over playback. + /// + /// + /// Publishing and subscribing are thread-safe, so a producer can publish from its audio thread. PinMAME owns one, + /// exposes it through the Unity integration's IGamelogicAudioResourceProvider so VPE publishes it as + /// <controller>/audio/rom, copies samples only when is true, and mutes its own + /// output while is true. The owner disposes it; disposing does not unpublish it. + /// + /// + /// private readonly AudioResourceSource _audio = new AudioResourceSource(error => Debug.LogException(error)); + /// + /// private void OnAudio(byte[] buffer, int length, int sampleRate, int channels, int frames) + /// { + /// if (_audio.HasSubscribers) + /// _audio.Publish(new PluginPcmFrame(buffer.AsMemory(0, length), PcmEncoding.Float32, sampleRate, channels, frames, ++_sequence)); + /// if (!_audio.PlaybackReplaced) PlayLocally(buffer, length); + /// } + /// + /// + /// public sealed class AudioResourceSource : IPluginAudioSource, IPluginAudioPlaybackControl, IDisposable { private readonly ResourceSignal _signal; private readonly PluginOutputLeases _output = new PluginOutputLeases(); + /// Always . public ResourceKind Kind => ResourceKind.Audio; + /// + /// Whether at least one receiver is subscribed. Producers check it to skip copying samples nobody reads. Safe to read + /// from any thread. + /// public bool HasSubscribers => _signal.HasSubscribers; + /// + /// Whether a plugin currently holds the playback lease from . While true, the producer + /// should stop playing the sound itself but keep publishing it. Safe to read from any thread. + /// public bool PlaybackReplaced => _output.Owner("playback") != null; + /// Creates the source. + /// + /// Receives exceptions thrown by a receiver, on the publishing thread; the remaining receivers still get the block. + /// Null ignores them. + /// public AudioResourceSource(Action? fault = null) { _signal = new ResourceSignal(fault); } + /// Registers a receiver for every block; it is called on the publishing thread. Thread-safe. + /// Called with each block. + /// Disposing it stops the receiver. + /// The source was disposed. public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); + /// + /// Takes over playback until the returned lease is disposed; is true meanwhile. Only + /// one plugin can hold it at a time. + /// + /// ID of the plugin taking over, named in the error another plugin gets. + /// The lease. + /// Another plugin already holds the lease. public IDisposable AcquirePlayback(string pluginId) => _output.Acquire("playback", pluginId); + /// + /// Delivers a block to every receiver, synchronously on the calling thread. Safe to call from any thread; does + /// nothing after . + /// + /// The block; its samples only need to stay valid until the call returns. public void Publish(in PluginPcmFrame frame) => _signal.Publish(frame); + /// + /// Drops all receivers and refuses new ones. It does not unpublish the resource or release a playback lease. + /// public void Dispose() => _signal.Dispose(); } + /// + /// Ready-made source for producers: it carries the display's announced place and + /// size and dispatches s to subscribers. + /// + /// + /// VPE creates one per segment display the gamelogic engine announces, numbered in announcement order, with the layout + /// and hardware hint the engine declares (PinMAME maps its display types onto seg7, seg9, seg14 and + /// seg16). Publishing and subscribing are thread-safe. The owner disposes it; disposing does not unpublish it. + /// + /// + /// var source = new SegmentResourceSource(order: 0, elements: 16, layout: "seg14", hardware: "", fault: Debug.LogException); + /// var publication = registry.Publish(new PluginResourceDescriptor(controllerId + "/segments/display0", "pinmame", controllerId, + /// "display0", "segments", ResourceKind.Segments, session, instance), source); + /// source.Publish(new PluginSegmentFrame("seg14", 16, 16, brightness, "")); + /// + /// + /// public sealed class SegmentResourceSource : IPluginSegmentSource, IDisposable { private readonly ResourceSignal _signal; + /// Always . public ResourceKind Kind => ResourceKind.Segments; + /// public int Order { get; } + /// public int Elements { get; } + /// public string Layout { get; } + /// public string Hardware { get; } + /// + /// Creates a source that announces nothing: order 0, no elements, and empty layout and hardware. Consumers then learn + /// the size from the first frame. Prefer the constructor that announces the display. + /// + /// Receives exceptions thrown by a receiver; null ignores them. public SegmentResourceSource(Action? fault = null) : this(0, 0, "", "", fault) { } + /// Creates a source that announces the display's place, size and appearance. + /// Position among the controller's segment displays, from 0. + /// Number of elements (digits). + /// Segment arrangement, such as seg7; null becomes empty. + /// Technology hint as in ; null becomes empty. + /// + /// Receives exceptions thrown by a receiver, on the publishing thread; the remaining receivers still get the frame. + /// Null ignores them. + /// public SegmentResourceSource(int order, int elements, string layout, string hardware, Action? fault = null) { Order = order; Elements = elements; Layout = layout ?? ""; Hardware = hardware ?? ""; _signal = new ResourceSignal(fault); } + /// Registers a receiver for every frame; it is called on the publishing thread. Thread-safe. + /// Called with each frame. + /// Disposing it stops the receiver. + /// The source was disposed. public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); + /// + /// Delivers a frame to every receiver, synchronously on the calling thread; does nothing after . + /// + /// The frame; its brightness only needs to stay valid until the call returns. public void Publish(in PluginSegmentFrame frame) => _signal.Publish(frame); + /// Drops all receivers and refuses new ones. It does not unpublish the resource. public void Dispose() => _signal.Dispose(); } + /// + /// Ready-made source for producers of s. Publishing and + /// subscribing are thread-safe. Nothing in VPE publishes input resources yet. + /// public sealed class InputResourceSource : IPluginInputSource, IDisposable { private readonly ResourceSignal _signal; + /// Always . public ResourceKind Kind => ResourceKind.Input; + /// Creates the source. + /// + /// Receives exceptions thrown by a receiver, on the publishing thread; the remaining receivers still get the event. + /// Null ignores them. + /// public InputResourceSource(Action? fault = null) { _signal = new ResourceSignal(fault); } + /// Registers a receiver for every event; it is called on the publishing thread. Thread-safe. + /// Called with each event. + /// Disposing it stops the receiver. + /// The source was disposed. public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); + /// + /// Delivers an event to every receiver, synchronously on the calling thread; does nothing after . + /// + /// The event. public void Publish(in PluginInputEvent value) => _signal.Publish(value); + /// Drops all receivers and refuses new ones. It does not unpublish the resource. public void Dispose() => _signal.Dispose(); } + /// + /// Ready-made source for producers of s. Publishing + /// and subscribing are thread-safe. Nothing in VPE publishes trigger resources yet. + /// public sealed class TriggerResourceSource : IPluginTriggerSource, IDisposable { private readonly ResourceSignal _signal; + /// Always . public ResourceKind Kind => ResourceKind.Triggers; + /// Creates the source. + /// + /// Receives exceptions thrown by a receiver, on the publishing thread; the remaining receivers still get the event. + /// Null ignores them. + /// public TriggerResourceSource(Action? fault = null) { _signal = new ResourceSignal(fault); } + /// Registers a receiver for every event; it is called on the publishing thread. Thread-safe. + /// Called with each event. + /// Disposing it stops the receiver. + /// The source was disposed. public IDisposable Subscribe(Action receive) => _signal.Subscribe(receive); + /// + /// Delivers an event to every receiver, synchronously on the calling thread; does nothing after . + /// + /// The event. public void Publish(in PluginTriggerEvent value) => _signal.Publish(value); + /// Drops all receivers and refuses new ones. It does not unpublish the resource. public void Dispose() => _signal.Dispose(); } } diff --git a/VisualPinball.Plugins/Runtime/PluginHost.cs b/VisualPinball.Plugins/Runtime/PluginHost.cs index 539818975..3d94b067d 100644 --- a/VisualPinball.Plugins/Runtime/PluginHost.cs +++ b/VisualPinball.Plugins/Runtime/PluginHost.cs @@ -13,7 +13,55 @@ namespace VisualPinball.Plugins { - /// Serializes game hooks, resolves policy before constructing plugins, and owns reverse cleanup. + /// + /// Runs the installed Player plugins for one game at a time: decides which plugins run, creates them, drives their lifecycle + /// hooks in dependency order and cleans them up in reverse, so that a failing plugin does not take the others down. + /// + /// + /// The Player creates one host at startup from and keeps it for the application's + /// life. For every table it calls , forwards frames and state changes (, + /// , , , ) while the game + /// runs, calls when the user leaves the table, and when the application + /// quits. Its settings page edits settings through a that commits through the host, so live + /// fields reach the running plugin. Plugins never see the host; they get its services through + /// and . + /// + /// The host belongs to the thread that created its (Unity's main thread in the Player). Plugin hooks + /// run there, and the host's own asynchronous operations resume there only when the scheduler is pumped, which + /// does. Call every frame, also while no game runs, or pending starts, stops and settings + /// commits stall. + /// + /// + /// Starting a game, stopping it, committing settings and cleaning up after a fault are serialized behind one lifecycle gate + /// (see ). A plugin whose hook throws becomes , is cleaned up, and + /// blocks the plugins that depend on it; the others keep running. A plugin whose hook misses its deadline + /// ( or ), or whose workers are still busy after + /// , is quarantined: it gets , becomes + /// , and keeps its resource scopes (native handles, device leases) until every hook the + /// host started on it and its have completed. Then the host shuts it down (after + /// a missed deadline) and disposes it. Meanwhile no new instance of that plugin is created; the host itself never waits for a + /// quarantined instance. + /// + /// + /// How the Player drives the host (simplified from its plugin controller and main menu): + /// + /// // on the main thread, once: + /// var host = new PluginHost(PlayerPluginRegistry.Installed, new PluginConfigurationStore(root), new OwnerScheduler(), + /// capabilities, new PluginResourceRegistry(Debug.LogException), "windows", message => Debug.Log("[Plugins] " + message)); + /// + /// // per table: + /// await host.PrepareAndStartAsync(identity, packagePath, title, identity.Id + "/controller/0", ++session, cancellation); + /// + /// // every frame: + /// private void Update() => host.Tick(Time.unscaledDeltaTime); + /// private void LateUpdate() => host.BeforeRender(); + /// + /// // leaving the table, and quitting: + /// await host.StopGameAsync(); + /// host.EmergencyStop(); + /// + /// + /// public sealed partial class PluginHost { private sealed class HookDeadlineException : TimeoutException @@ -60,17 +108,89 @@ internal void Track(Task hook) private string[] _hostCapabilities = Array.Empty(); private readonly Dictionary _dispatch = new Dictionary(); private Task _faultCleanup = Task.CompletedTask; + /// + /// The owner-thread scheduler passed to the constructor, handed to every plugin as + /// . pumps it. Code that awaited a host operation can + /// return to the owner thread with await host.Scheduler.Switch(), as the Player's settings page does. + /// public OwnerScheduler Scheduler { get; } + /// + /// The store the host reads plugin policy and settings from when it starts a game, and saves committed settings to. Handed + /// to every plugin as ; the Player's settings page and volume mixer read + /// through it as well. + /// public PluginConfigurationStore Configuration { get; } + /// + /// The capability registry shared with every plugin. The Player publishes its own services here (events, output leases and + /// the current game's table scene). takes a snapshot of the published capability IDs to + /// check , so publish host capabilities before starting a game. + /// public PluginCapabilities Capabilities { get; } + /// + /// The resource registry shared with every plugin. The Player publishes the game's controller, displays and audio here + /// before starting plugins, and passes the registry's snapshot to settings catalogs. + /// public PluginResourceRegistry Resources { get; } + /// + /// The platform ID the host runs on: windows, macos or linux in the Player, or Unity's lowercase + /// platform name elsewhere. Plugins whose do not list it are + /// . It is also passed to settings providers as . + /// public string Platform { get; } + /// + /// The status of every installed plugin, keyed by , with an entry for each factory from the + /// start, also for plugins that never ran. The status objects are updated in place on the owner thread; read them there. + /// The Player's settings page shows them to explain why a plugin is not running. + /// public IReadOnlyDictionary Statuses => _statuses; + /// + /// The installed plugin factories passed to the constructor, in no particular order. Factories registered later are not + /// included. The Player lists them on its settings page and in its volume mixer. + /// public IReadOnlyCollection Factories => _factories.Values; + /// + /// True from the end of until the game is stopped by , + /// or the next start. It is also true when some or all plugins failed to start; check + /// for individual plugins. + /// public bool IsRunning => _running; + /// + /// True while a lifecycle operation holds the host's gate: starting or stopping a game, committing settings, or cleaning up + /// a plugin that faulted during a dispatched call. Meanwhile , and + /// are held and delivered (latest state only) when the operation ends. and + /// still reach running plugins while settings are committed or a faulted plugin is cleaned up. + /// public bool IsTransitioning => _operation.CurrentCount == 0; + /// + /// How long each cleanup hook (, ) may run + /// before its plugin is quarantined as , and how long waits + /// for a running operation before it falls back to . Also bounds the deferred shutdown of a + /// quarantined instance. Defaults to 10 seconds. + /// public TimeSpan CleanupTimeout { get; set; } = TimeSpan.FromSeconds(10); + /// + /// How long each startup hook (, , + /// ) and each may run before its + /// plugin is quarantined as , and how long waits for + /// a running operation to finish. Defaults to 30 seconds. + /// public TimeSpan StartupTimeout { get; set; } = TimeSpan.FromSeconds(30); + /// + /// Creates a host for a fixed set of plugins. No plugin instance is created before , and + /// every plugin starts as . + /// + /// The installed plugins, usually . The host copies them. + /// The store for plugin policy and settings. + /// + /// The owner-thread scheduler. Create it on the thread that drives the host; plugin hooks run on that thread. + /// + /// The capability registry shared with plugins. + /// The resource registry shared with plugins. + /// The platform ID, compared with ; see . + /// + /// Receives diagnostics: plugin log lines (prefixed with the plugin ID), faults and cleanup errors. Null discards them. + /// + /// Two factories have the same . public PluginHost(IEnumerable factories, PluginConfigurationStore configuration, OwnerScheduler scheduler, PluginCapabilities capabilities, PluginResourceRegistry resources, string platform, Action? log = null) { @@ -79,6 +199,53 @@ public PluginHost(IEnumerable factories, PluginConfigurati Configuration = configuration; Scheduler = scheduler; Capabilities = capabilities; Resources = resources; Platform = platform; _log = log ?? (_ => { }); } + /// + /// Stops the current game's plugins, if any, then selects, creates and starts the plugins for a new game. Plugin failures do + /// not throw: they are reported in , and the game runs with the remaining plugins. + /// + /// + /// The host first decides which plugins run. Plugins that are disabled for the game, unsupported on , + /// still quarantined from an earlier instance, missing a dependency or required capability, or caught in an ordering cycle + /// are not created and get the matching . For the others, in initialize order, it validates the + /// effective settings against the schema and , creates the instance and calls + /// . Then it calls on all of them in + /// prepare order, and in start order. A plugin that fails a step is cleaned up and + /// the plugins depending on it are blocked before the next step. When the task completes, the started plugins are + /// and is true; a , pause or focus change + /// that arrived meanwhile is delivered then. + /// + /// The Player calls it on the main thread after publishing the game's table scene capability and its controller and display + /// resources, so plugins find them while they prepare. + /// + /// + /// + /// The game to start. Selects per-game enablement and settings, and is passed to plugins as + /// . + /// + /// + /// Full path of the table package, passed to plugins as and to settings validation. + /// + /// The table's title, passed to plugins as . + /// + /// ID of the game's controller resource, passed to plugins as . The Player uses + /// identity.Id + "/controller/0". + /// + /// + /// The game session number, increasing with every game, passed to plugins as . + /// + /// + /// Cancels the startup: the plugins started so far are stopped and the task is cancelled. The token is also linked to + /// , so cancelling it after startup signals the running plugins' game token, but + /// does not stop them. + /// + /// + /// A task that completes when startup has finished. It also completes, without starting anything, when another operation + /// still holds the host after ; every plugin is then . + /// + /// + /// was cancelled, or the game was stopped by or + /// while it was starting. + /// public async Task PrepareAndStartAsync(GameIdentity identity, string packagePath, string title, string controllerId, long session, CancellationToken cancellation) { using var gateDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation); @@ -280,6 +447,24 @@ private async Task FinishQuarantineAsync(Instance entry, Task pending, bool shut private void Fault(string id, PluginPhase phase, Exception error) { SetStatus(id, PluginState.Faulted, error.Message, phase); _log(id + " / " + phase + ": " + error.Message); } private void SafeDispose(IDisposable resource) { try { resource.Dispose(); } catch (Exception error) { _log("Plugin cleanup: " + error.Message); } } + /// + /// Stops the current game's plugins and releases their game and instance resources. Safe to call when no game runs. + /// + /// + /// Cancels at once, then waits up to for a running + /// operation, such as a startup still in progress, to finish. Plugins are then cleaned up one at a time: partially started + /// ones first, then started ones in reverse start order. Each gets (if it was + /// prepared), disposal of , , disposal of + /// , and finally . Exceptions from these + /// steps are logged and cleanup continues; a hook that misses its deadline quarantines its plugin (see + /// ) and the host moves on to the next one. If the running operation does not finish in time, the + /// host falls back to . + /// + /// + /// Cuts cleanup short. It cancels the token each cleanup hook receives, in addition to ; if it + /// fires while the host waits for a running operation, the host falls back to . + /// + /// A task that completes when every plugin of the game is stopped or quarantined. public async Task StopGameAsync(CancellationToken cleanup = default) { _gameCancellation?.Cancel(); @@ -302,7 +487,49 @@ private async Task StopCoreAsync(CancellationToken cleanup) } _startOrder.Clear(); _entered.Clear(); _gameCancellation?.Dispose(); _gameCancellation = null; _activeGame = null; } - /// Applies live values and persistence within the same lifecycle gate. Plugins must roll back failed reconfiguration. + /// + /// Validates and saves edited settings of one plugin and, when they change fields of + /// the running game, applies them to the running plugin first. Both happen inside the host's lifecycle gate, so a commit + /// never overlaps a game start or stop. + /// + /// + /// calls this after its own checks; plugins do not call it. The steps are: + /// + /// + /// Merge the edited fields into the latest stored layer (global, or the game's profile) and validate the effective result + /// against the schema. Provider validation and catalog checks are left to the settings session. + /// + /// + /// If the plugin is running and the edit affects the active game (a global edit, or an edit of that game's profile), apply + /// the changed live fields through or + /// . A plugin implementing neither, a failed result, or an asynchronous + /// apply that misses (which quarantines the plugin) ends the commit without saving. + /// + /// + /// Save only the edited fields with , then raise + /// . + /// + /// + /// If saving fails after a live apply, the host applies the previous settings again, so plugins must keep reconfiguration + /// reversible. A rollback that fails faults the plugin. + /// + /// The plugin whose settings change; must be one of . + /// + /// The edited layer: the game's overrides when is set, the global values otherwise. Only the fields + /// in are taken; a listed field that the draft lacks is removed from the stored layer and inherits + /// again. The host copies the draft. + /// + /// The game whose profile to change, or null for the global settings. + /// + /// Cancels waiting for the gate and the live apply. Once saving has started, it runs to completion. + /// + /// IDs of the top-level fields to commit, or null for every field of the schema. + /// + /// Success with the message Saved., or a failure whose message lists the validation issues or says why nothing was + /// saved. Errors and cancellation after the gate was entered are reported as failures, not thrown. + /// + /// is not installed in this host. + /// fired while waiting for the gate. public Task CommitSettingsAsync(IPlayerPluginFactory factory, PluginSettingsSnapshot draft, GameIdentity? game, CancellationToken cancellation, IReadOnlyCollection? edited = null) => CommitSettingsCoreAsync(factory, draft, game, cancellation, edited); private async Task CleanupAsync(Instance entry, CancellationToken cleanup) @@ -379,11 +606,65 @@ private void ReplayState() if (_paused.HasValue) { bool paused = _paused.Value; _paused = null; Dispatch(paused ? PluginPhase.Pause : PluginPhase.Resume, state: paused); } if (_focused.HasValue) { bool focused = _focused.Value; _focused = null; Dispatch(PluginPhase.Focus, state: focused); } } + /// + /// Tells the running plugins that the table's player has started and the game is playable; they receive + /// in ready order. Call it on the owner thread. Ignored when no game is running or + /// starting. While the host is busy () the call is held and delivered when the operation ends. + /// The Player calls it from the table player's OnPlayerStarted event. + /// public void GameReady() { if (!_running && !IsTransitioning) return; _ready = true; if (!IsTransitioning) ReplayState(); } + /// + /// Tells the running plugins that the game was paused or resumed; they receive in + /// pause or resume order. Call it on the owner thread. Ignored when no game is running or starting. While the host is busy, + /// only the latest state is delivered when the operation ends. The Player calls it when its in-game menu opens and closes. + /// + /// True when the game was paused, false when it resumed. public void PauseChanged(bool paused) { if (!_running && !IsTransitioning) return; _paused = paused; if (!IsTransitioning) ReplayState(); } + /// + /// Tells the running plugins that the application gained or lost focus; they receive + /// in focus order. Call it on the owner thread. Ignored when no game is running or + /// starting. While the host is busy, only the latest state is delivered when the operation ends. The Player calls it from + /// Unity's OnApplicationFocus. + /// + /// True when the application has focus. public void FocusChanged(bool focused) { if (!_running && !IsTransitioning) return; _focused = focused; if (!IsTransitioning) ReplayState(); } + /// + /// Runs one frame: first runs the work queued on , such as continuations of the host's own operations + /// and of plugin hooks, then calls on every running plugin in tick order. + /// + /// + /// Call it on the owner thread every frame, also while the game is paused and while no game runs, since starting, stopping + /// and committing settings depend on the pump. Plugins keep being ticked while the host commits settings or cleans up a + /// faulted plugin, but not while a game starts or stops, and a plugin is skipped while its own asynchronous + /// reconfiguration runs. A plugin whose tick throws becomes and is cleaned up in the + /// background. The Player calls it from its Update with Time.unscaledDeltaTime. + /// + /// Real time since the previous frame in seconds, passed on to . + /// Called from a thread other than the owner thread. public void Tick(double delta) { Scheduler.Pump(); Dispatch(PluginPhase.Tick, delta); } + /// + /// Calls on every running plugin in render order. Call it on the owner thread once + /// per frame, after all frame updates; the Player calls it from LateUpdate. Like , it reaches + /// plugins while the host commits settings or cleans up a faulted plugin, and a plugin whose call throws is faulted. It + /// does not pump the scheduler. + /// + /// Called from a thread other than the owner thread. public void BeforeRender() => Dispatch(PluginPhase.Render); + /// + /// Stops all plugins of the current game immediately and synchronously, without awaiting any hook. Use it when there is no + /// time for : the Player calls it when the application quits, and + /// falls back to it when a running operation does not finish within . + /// + /// + /// Cancels and stops all dispatched calls. Then, in reverse order of creation, + /// it calls on every instance (exceptions are logged) and, unless the + /// instance was already cleaned up, disposes its , + /// and the plugin right away. and + /// are not called. An instance that still has a hook running, or whose + /// has not completed, is quarantined instead: it keeps its scopes until both + /// are done, and the plugin stays , blocking new instances, until then. + /// + /// Called from a thread other than the owner thread. public void EmergencyStop() { Scheduler.VerifyOwner(); diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs index e7b72d12c..a121043e3 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs @@ -14,7 +14,27 @@ namespace VisualPinball.Unity { - /// Reads provider identities without restoring a scene; content hashes keep fallback identities stable across moves. + /// + /// Identifies the game in a table package without loading the table, for per-game plugin settings and for plugins that look for + /// matching media. + /// + /// + /// The Player uses it when the loaded table's gamelogic engine does not implement , and its + /// plugin settings page uses it to open the profile of the game selected in the library before anything is loaded. The + /// package's components are handed to the registered s (see + /// ); PinMAME's reader yields the ROM name. When no reader finds an identity, the game + /// is identified by the SHA-256 hash of the package file (provider package). That identity survives moving or renaming + /// the file, but changes when its content changes, for example when the table is exported again, which detaches it from the + /// settings saved for the old hash. + /// + /// Results are cached per full path and read again when the file's size or last write time changes. + /// + /// + /// + /// var identity = (engine as IGameIdentityProvider)?.GameIdentity ?? await PackagedGameInformation.ReadAsync(path, cancellation); + /// + /// + /// public static class PackagedGameInformation { private sealed class Cached @@ -25,6 +45,18 @@ private sealed class Cached } // Ordinal on every platform: Linux paths are case-sensitive, and elsewhere a differently cased path only costs a second read. private static readonly Dictionary Cache = new Dictionary(StringComparer.Ordinal); + /// + /// Reads the game identity of a table package on a worker thread. Safe to call from any thread, also concurrently. The + /// identity readers run on the worker thread. + /// + /// Path of the table package; a relative path is resolved against the current directory. + /// Cancels the read, including hashing a large package. + /// + /// The single identity found by the registered readers, or a package identity whose key is the lowercase hexadecimal + /// SHA-256 hash of the file. Errors opening or reading the file fault the task. + /// + /// The readers found more than one distinct identity in the package. + /// was cancelled. public static Task ReadAsync(string path, CancellationToken cancellation = default) => Task.Run(() => Read(path, cancellation), cancellation); private static GameIdentity Read(string path, CancellationToken cancellation) { From 413ca0a384cc24cf007582bcfcd3803bf83b7dcb Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 11:10:53 +0200 Subject: [PATCH 15/26] plugins: let refused display bindings retry when a format lease ends A binding refused because another consumer holds a raw identification encoding it cannot read now retries as soon as that lease ends, instead of staying blocked until the game restarts. - DisplayFormatNegotiator signals when its last identification lease is released. - The gamelogic bridge forwards that through the new PluginResourceRegistry.NotifyChanged, which raises ResourceChangeKind.Changed. - Bindings guard against re-entrant refreshes. A notice caused by releasing a binding's own view does not retry the same attempt, and a binding disposed from a notification stays disposed. --- .../SettingsAndNegotiationTests.cs | 53 +++++++++++++++++++ .../Resources/DisplayFormatNegotiator.cs | 13 +++-- .../Resources/PluginResourceRegistry.cs | 42 +++++++++++++-- .../Plugins/GamelogicPluginResources.cs | 9 ++-- 4 files changed, 108 insertions(+), 9 deletions(-) diff --git a/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs b/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs index e657c64f8..292d0c432 100644 --- a/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs +++ b/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs @@ -78,6 +78,59 @@ public void IdentificationEncodingIsOneTheConsumerAccepts() Assert.That(requested[requested.Count - 1], Is.EqualTo(DisplayEncoding.Gray4)); } + [Test] + public void RefusedDisplayBindingRetriesWhenTheConflictingLeaseEnds() + { + var registry = new PluginResourceRegistry(); + const string id = "pinmame/controller/0/display/dmd0"; + using var negotiator = new DisplayFormatNegotiator(DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8, DisplayEncoding.Luminance8, + _ => { }, () => registry.NotifyChanged(id)); + using var source = new DisplayResourceSource(negotiator.Acquire); + using var publication = registry.Publish(new PluginResourceDescriptor(id, "pinmame", "pinmame/controller/0", "dmd0", "dmd", ResourceKind.Display, 1, 1, ResourceView.Original, DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8), source); + var gray2 = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = id, Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray2 }); + using var gray4 = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = id, Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray4 }); + Assert.That(gray2.State, Is.EqualTo(ResourceBindingState.Bound)); + Assert.That(gray4.State, Is.EqualTo(ResourceBindingState.Blocked)); + // Stopping the first integration frees the encoding without restarting the game. + gray2.Dispose(); + Assert.That(gray4.State, Is.EqualTo(ResourceBindingState.Bound)); + } + + [Test] + public void AFailingRetryStaysBlockedInsteadOfLooping() + { + var registry = new PluginResourceRegistry(); + const string id = "pinmame/controller/0/display/dmd0"; + using var negotiator = new DisplayFormatNegotiator(DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8, DisplayEncoding.Luminance8, + _ => { }, () => registry.NotifyChanged(id)); + using var source = new DisplayResourceSource(negotiator.Acquire); + using var publication = registry.Publish(new PluginResourceDescriptor(id, "pinmame", "pinmame/controller/0", "dmd0", "dmd", ResourceKind.Display, 1, 1, ResourceView.Original, DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8), source); + var gray2 = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = id, Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray2 }); + using var gray4 = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = id, Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray4 }); + int attempts = 0; + using var connection = gray4.Connect(_ => { attempts++; throw new InvalidOperationException("receiver failed"); }); + // The retry acquires the lease, the receiver throws, and the lease rollback must not trigger the same retry again. + Assert.DoesNotThrow(() => gray2.Dispose()); + Assert.That(attempts, Is.LessThanOrEqualTo(2)); + Assert.That(gray4.State, Is.EqualTo(ResourceBindingState.Blocked)); + } + + [Test] + public void DisposingABindingFromAChangeHandlerIsFinal() + { + var registry = new PluginResourceRegistry(); + const string id = "pinmame/controller/0/display/dmd0"; + using var negotiator = new DisplayFormatNegotiator(DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8, DisplayEncoding.Luminance8, + _ => { }, () => registry.NotifyChanged(id)); + using var source = new DisplayResourceSource(negotiator.Acquire); + using var publication = registry.Publish(new PluginResourceDescriptor(id, "pinmame", "pinmame/controller/0", "dmd0", "dmd", ResourceKind.Display, 1, 1, ResourceView.Original, DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8), source); + var gray2 = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = id, Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray2 }); + var gray4 = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = id, Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray4 }); + gray4.Changed += () => gray4.Dispose(); + gray2.Dispose(); + Assert.That(gray4.State, Is.EqualTo(ResourceBindingState.Disposed)); + } + private sealed class Factory : IPlayerPluginFactory { public PluginDescriptor Descriptor { get; } diff --git a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs index e603fc8dc..8f77ebdf5 100644 --- a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs +++ b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs @@ -14,11 +14,12 @@ public sealed class DisplayFormatNegotiator : IDisposable private readonly DisplayEncoding _default; private readonly DisplayEncoding _supported; private readonly Action _request; + private readonly Action? _released; private int _identification; private DisplayEncoding _active; private bool _disposed; - public DisplayFormatNegotiator(DisplayEncoding supported, DisplayEncoding defaultFormat, Action request) - { _supported = supported; _default = defaultFormat; _request = request; } + public DisplayFormatNegotiator(DisplayEncoding supported, DisplayEncoding defaultFormat, Action request, Action? released = null) + { _supported = supported; _default = defaultFormat; _request = request; _released = released; } public IDisposable Acquire(ResourceView intent, DisplayEncoding accepted) { if (_disposed) throw new ObjectDisposedException(nameof(DisplayFormatNegotiator)); @@ -35,7 +36,13 @@ public IDisposable Acquire(ResourceView intent, DisplayEncoding accepted) _active = encoding; _identification = 1; } - return new Subscription(() => { if (!_disposed && --_identification == 0) { _active = DisplayEncoding.None; _request(_default); } }); + return new Subscription(() => { + if (_disposed || --_identification != 0) return; + _active = DisplayEncoding.None; + _request(_default); + // Consumers that were refused because of the encoding we just gave up can try again. + _released?.Invoke(); + }); } public void Dispose() { if (_disposed) return; _disposed = true; if (_identification > 0) _request(_default); _identification = 0; } } diff --git a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs index e677d805a..65fed537c 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs @@ -72,6 +72,16 @@ private bool Reachable(Entry entry, HashSet path) } return true; } + /// + /// Tells observers and bindings that a published resource changed in a way that may let a refused consumer bind + /// now, for example when a display gave up the identification encoding another consumer could not accept. + /// Bindings that are blocked or waiting on the resource retry; bound bindings are unaffected. + /// + /// The resource's ; unknown IDs are ignored. + public void NotifyChanged(string resourceId) + { + if (_entries.TryGetValue(resourceId, out var entry)) Notify(new PluginResourceChange(ResourceChangeKind.Changed, entry.Descriptor)); + } public IDisposable Observe(Action changed) { _observers.Add(changed); @@ -121,12 +131,27 @@ public IDisposable Connect(Func subscribe) RefreshAttachments(); return new Subscription(() => { _receivers.Remove(subscribe); RefreshAttachments(); }); } + private bool _refreshing, _refreshAgain, _releasing; private void Refresh() + { + // A notice caused by releasing this binding's own view says nothing new to it; retrying would repeat a failed + // attempt forever. + if (_releasing) return; + // Other notices during a pass (another binding released a view) are handled by one more pass afterwards. + if (_refreshing) { _refreshAgain = true; return; } + _refreshing = true; + try { + do { _refreshAgain = false; RefreshOnce(); } while (_refreshAgain && State != ResourceBindingState.Disposed); + } finally { _refreshing = false; } + } + private void RefreshOnce() { if (State == ResourceBindingState.Disposed) return; var candidates = _registry.Candidates(_selection).Where(entry => entry.Source is T).ToArray(); if (candidates.Length == 1 && ReferenceEquals(Source, candidates[0].Source) && Descriptor?.Instance == candidates[0].Descriptor.Instance) return; Detach(); + // Releasing the old view runs observers, which may have disposed this binding. + if (State == ResourceBindingState.Disposed) return; if (candidates.Length == 0) { State = ResourceBindingState.Waiting; Diagnostic = _selection.ResourceId.Length == 0 ? "Waiting for a compatible resource." : "Missing resource: " + _selection.ResourceId; } else if (candidates.Length > 1) { State = ResourceBindingState.Blocked; Diagnostic = "Select a resource: " + string.Join(", ", candidates.Select(entry => entry.Descriptor.Id)); } else { @@ -135,7 +160,11 @@ private void Refresh() try { _view = (Source as IPluginDisplaySource)?.AcquireView(_selection.Intent, _selection.Accepts); State = ResourceBindingState.Bound; Diagnostic = ""; RefreshAttachments(); - } catch (Exception error) { Detach(); State = ResourceBindingState.Blocked; Diagnostic = error.Message; } + } catch (Exception error) { + Detach(); + if (State == ResourceBindingState.Disposed) return; + State = ResourceBindingState.Blocked; Diagnostic = error.Message; + } } try { Changed?.Invoke(); } catch (Exception error) { _registry.Report(error); } } @@ -148,7 +177,8 @@ private void RefreshAttachments() private void Detach() { ClearAttachments(); - try { _view?.Dispose(); } catch (Exception error) { _registry.Report(error); } + _releasing = true; + try { _view?.Dispose(); } catch (Exception error) { _registry.Report(error); } finally { _releasing = false; } _view = null; Source = null; Descriptor = null; ActualEncoding = DisplayEncoding.None; } private void ClearAttachments() @@ -156,7 +186,13 @@ private void ClearAttachments() foreach (var attached in _attached) try { attached.Dispose(); } catch (Exception error) { _registry.Report(error); } _attached.Clear(); } - public void Dispose() { if (State == ResourceBindingState.Disposed) return; _watch.Dispose(); Detach(); _receivers.Clear(); State = ResourceBindingState.Disposed; Changed = null; } + public void Dispose() + { + if (State == ResourceBindingState.Disposed) return; + // Disposed first, so a refresh that is running further up the stack does not revive the binding. + State = ResourceBindingState.Disposed; + _watch.Dispose(); Detach(); _receivers.Clear(); Changed = null; + } } public static class DisplayBindingExtensions diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs index 33c8e9824..ffacd922b 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs @@ -80,7 +80,8 @@ public GamelogicPluginResources(GameObject table, IGamelogicEngine engine, GameI : DisplayEncoding.Luminance8 | DisplayEncoding.Rgb24); _defaultFormat = metadata?.DefaultDisplayFormat ?? DisplayEncoding.Luminance8; if (metadata?.FormatAppliesToAllDisplays == true) _group = _scope.Own(new DisplayFormatNegotiator(_formats, _defaultFormat, - format => (engine as IDisplayFrameFormatPreference)?.RequestDisplayFrameFormat("", ToLegacy(format)))); + format => (engine as IDisplayFrameFormatPreference)?.RequestDisplayFrameFormat("", ToLegacy(format)), + () => { foreach (string display in _displays.Keys.ToArray()) _registry.NotifyChanged(DisplayResourceId(display)); })); _scope.Own(_controller); _scope.Own(registry.Publish(new PluginResourceDescriptor(_controllerId, _provider, _controllerId, engine.Name, "controller", ResourceKind.Controller, session, Interlocked.Increment(ref _instance)), _controller)); if (engine is IGamelogicNativeOutputs native) { @@ -139,12 +140,14 @@ private void AddDisplay(string id, int width, int height) existing.Dispose(); _displays.Remove(id); } var format = _group ?? new DisplayFormatNegotiator(_formats, _defaultFormat, - encoding => (_engine as IDisplayFrameFormatPreference)?.RequestDisplayFrameFormat(id, ToLegacy(encoding))); + encoding => (_engine as IDisplayFrameFormatPreference)?.RequestDisplayFrameFormat(id, ToLegacy(encoding)), + () => _registry.NotifyChanged(DisplayResourceId(id))); var source = new DisplayResourceSource(format.Acquire, _fault); long instance = Interlocked.Increment(ref _instance); - var descriptor = new PluginResourceDescriptor(_controllerId + "/display/" + id, _provider, _controllerId, id, "dmd", ResourceKind.Display, _session, instance, ResourceView.Original, _formats); + var descriptor = new PluginResourceDescriptor(DisplayResourceId(id), _provider, _controllerId, id, "dmd", ResourceKind.Display, _session, instance, ResourceView.Original, _formats); _displays.Add(id, new Display(source, _registry.Publish(descriptor, source), width, height, instance, _group == null ? format : null)); } + private string DisplayResourceId(string display) => _controllerId + "/display/" + display; private void DisplayFrame(object sender, DisplayFrameData args) { _scheduler.VerifyOwner(); From ff122a655e89590b958ddda18cd9e7fd1e5769ce Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 11:10:58 +0200 Subject: [PATCH 16/26] plugins: document the registry, format negotiation and the gamelogic bridge XML documentation for the resource registry and bindings, display format negotiation and the gamelogic resources bridge, including the table of resources a gamelogic engine publishes. No code changed. --- .../Resources/DisplayFormatNegotiator.cs | 60 +++++- .../Resources/PluginResourceRegistry.cs | 191 +++++++++++++++++- .../Plugins/GamelogicPluginResources.cs | 117 ++++++++++- 3 files changed, 364 insertions(+), 4 deletions(-) diff --git a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs index 8f77ebdf5..3fa2780a1 100644 --- a/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs +++ b/VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs @@ -8,7 +8,32 @@ namespace VisualPinball.Plugins { - /// Provider-group raw mode is held until the last identification consumer leaves. + /// + /// Decides which encoding a display producer delivers when consumers need raw frames for identification. Provider-group + /// raw mode is held until the last identification consumer leaves. + /// + /// + /// A producer normally delivers a processed default encoding (PinMAME: ). + /// Consumers that recognize content, such as PinUP's DMD triggers or the DMD plugin's colorization, need the + /// controller's raw or shades instead. When the + /// first of them acquires an identification view, the negotiator asks the producer for raw frames (Gray4 when allowed, + /// otherwise Gray2); later consumers join if they accept that encoding; when the last one leaves, it asks for the + /// default again. Consumers of other views get no say, so while raw mode is held they receive raw frames too. + /// + /// VPE creates one per display, or a single one for all displays when the engine switches all of them at once (PinMAME + /// does), and passes its method to as the view negotiation. + /// It is not thread-safe: acquire and release leases on the owner thread, as bindings do. It assumes the producer starts + /// in the default encoding and does not request it on creation. + /// + /// + /// + /// var negotiator = new DisplayFormatNegotiator(DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8, + /// DisplayEncoding.Luminance8, encoding => engine.RequestFrameFormat(encoding), + /// () => registry.NotifyChanged(displayResourceId)); + /// var source = new DisplayResourceSource(negotiator.Acquire, Debug.LogException); + /// + /// + /// public sealed class DisplayFormatNegotiator : IDisposable { private readonly DisplayEncoding _default; @@ -18,8 +43,37 @@ public sealed class DisplayFormatNegotiator : IDisposable private int _identification; private DisplayEncoding _active; private bool _disposed; + /// Creates a negotiator for one producer or group of displays. + /// Every encoding the producer can deliver, as flags. + /// The single encoding the producer delivers when no identification consumer is attached. + /// + /// Asks the producer to switch to one encoding. Called synchronously from , from disposing a + /// lease and from , on the caller's thread. + /// + /// + /// Optional. Called after the last identification lease was disposed and the default was requested, so consumers + /// that were refused because of the raw encoding just given up can try again. VPE passes a callback that calls + /// for the affected displays. Not called by . + /// public DisplayFormatNegotiator(DisplayEncoding supported, DisplayEncoding defaultFormat, Action request, Action? released = null) { _supported = supported; _default = defaultFormat; _request = request; _released = released; } + /// + /// Registers a consumer's view. Only changes anything: the first such + /// consumer switches the producer to raw (or + /// when Gray4 is not both supported and accepted), and further ones join that encoding. Other views get a lease that + /// does nothing. Matches . + /// + /// The view the consumer wants. + /// The encodings the consumer can read. + /// + /// A lease. Disposing the last identification lease switches the producer back to the default encoding and then + /// calls the released callback; disposing after the negotiator was disposed does nothing. + /// + /// The negotiator was disposed. + /// + /// The producer cannot deliver a raw encoding the consumer accepts, or raw frames are already delivered in an encoding + /// this consumer does not accept. + /// public IDisposable Acquire(ResourceView intent, DisplayEncoding accepted) { if (_disposed) throw new ObjectDisposedException(nameof(DisplayFormatNegotiator)); @@ -44,6 +98,10 @@ public IDisposable Acquire(ResourceView intent, DisplayEncoding accepted) _released?.Invoke(); }); } + /// + /// Stops negotiating. If identification consumers were still attached, the producer is switched back to the default + /// encoding; leases disposed afterwards do nothing. Calling it again has no effect. + /// public void Dispose() { if (_disposed) return; _disposed = true; if (_identification > 0) _request(_default); _identification = 0; } } } diff --git a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs index 65fed537c..994df2bb9 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs @@ -10,6 +10,34 @@ namespace VisualPinball.Plugins { + /// + /// The directory of live resources (displays, controller events, audio, segment displays, surfaces…) that producers + /// publish and consumers bind to by description rather than by reference. + /// + /// + /// + /// The Player owns one registry and passes it to plugins as and + /// . For each game the Unity integration publishes the gamelogic engine's + /// controller, native outputs, audio, DMDs and segment displays under <game id>/controller/0. Plugins + /// publish their own: the DMD plugin a colorized presentation of a DMD, PinUP a surface per screen, the B2S plugin its + /// backglass surface. Consumers describe what they want with a and + /// to it; the binding follows resources as they come and go, so producers and consumers can start + /// in any order. + /// + /// + /// A resource can be derived from others (). It stays visible only while + /// every parent is still published with the same and the same + /// , so a presentation built on a DMD disappears when that DMD is replaced. + /// + /// Not thread-safe: publish, bind and observe on the owner thread. + /// + /// A plugin publishes a surface for the running game and withdraws it with the game: + /// + /// game.Lifetime.Own(game.Resources.Publish(new PluginResourceDescriptor(game.ControllerId + "/surface/my-screen", _application.Plugin.Id, + /// game.ControllerId, "My screen", "backglass", ResourceKind.Surface, game.Session, Interlocked.Increment(ref _instances)), surface)); + /// + /// + /// public sealed class PluginResourceRegistry { internal sealed class Entry @@ -22,9 +50,32 @@ internal sealed class Entry private readonly Dictionary _entries = new Dictionary(StringComparer.Ordinal); private readonly List> _observers = new List>(); private readonly Action _fault; + /// Creates an empty registry. The Player creates one for the application. + /// + /// Receives exceptions thrown by observers, binding Changed handlers and attachment cleanup, which are caught so + /// one faulty plugin cannot break the others. When null, they are silently dropped. + /// public PluginResourceRegistry(Action? fault = null) { _fault = fault ?? (_ => { }); } internal void Report(Exception error) => _fault(error); + /// + /// Returns the descriptors of all visible resources, sorted by ID. Derived resources whose parents are gone or were + /// replaced are left out. + /// + /// + /// The Player passes this list to settings providers as , so choices such as + /// can list the running game's displays. + /// public IReadOnlyList Snapshot() => _entries.Values.Where(entry => Reachable(entry, new HashSet())).Select(entry => entry.Descriptor).OrderBy(descriptor => descriptor.Id, StringComparer.Ordinal).ToArray(); + /// + /// Returns the descriptors a binding with would consider, sorted by ID: none means a + /// binding would wait, more than one means it would be blocked until the user picks one. + /// + /// + /// Useful to probe before binding. The B2S plugin checks whether the controller reports native outputs and binds to + /// if so, otherwise to the mapped events. + /// Unlike a binding, this does not filter by source interface. + /// + /// What to look for. public IReadOnlyList Snapshot(PluginResourceSelection selection) => Candidates(selection).Select(entry => entry.Descriptor).ToArray(); internal IReadOnlyList Candidates(PluginResourceSelection selection) { @@ -33,6 +84,30 @@ internal IReadOnlyList Candidates(PluginResourceSelection selection) found.RemoveAll(entry => entry.Descriptor.View != ResourceView.Presentation); return found; } + /// + /// Publishes a resource until the returned handle is disposed, and notifies observers and bindings with + /// . Disposing the handle removes it again with + /// . + /// + /// + /// The source must implement the payload contract of the descriptor's kind: for + /// , for + /// and , , , + /// , or for the others. + /// Parents must be published first. To replace a resource, dispose the old publication and publish again with a new + /// , so bindings rebind and derived resources built on the old one + /// disappear. Publishing does not take ownership of the source; dispose it separately. + /// + /// The source type. + /// Identity and metadata of the resource. Its ID must be unique in the registry. + /// The object consumers receive; its must equal the descriptor's. + /// The publication handle; disposing it more than once does nothing. + /// + /// The source's kind differs from the descriptor's, or the source does not implement the kind's contract. + /// + /// + /// The ID is already published, a parent is not published, or the parents would form a cycle. + /// public IDisposable Publish(PluginResourceDescriptor descriptor, T source) where T : IPluginResourceSource { if (descriptor.Kind != source.Kind) throw new ArgumentException("Resource kind and VPE source interface disagree."); @@ -82,16 +157,72 @@ public void NotifyChanged(string resourceId) { if (_entries.TryGetValue(resourceId, out var entry)) Notify(new PluginResourceChange(ResourceChangeKind.Changed, entry.Descriptor)); } + /// + /// Calls on every publication, withdrawal and change notice until the returned handle is + /// disposed: , , and + /// from . Existing resources + /// are not replayed; read first. + /// + /// + /// Bindings use this internally. Use it directly to follow a varying number of resources, as the B2S plugin does to + /// attach to every segment display of the game. Exceptions from the callback go to the registry's fault handler. + /// + /// Called synchronously on the publishing thread. + /// A handle that stops the observation. public IDisposable Observe(Action changed) { _observers.Add(changed); return new Subscription(() => _observers.Remove(changed)); } private void Notify(PluginResourceChange change) { foreach (var observer in _observers.ToArray()) try { observer(change); } catch (Exception error) { Report(error); } } + /// + /// Creates a binding that tracks the single resource matching whose source implements + /// . The binding resolves immediately and again on every registry change; dispose it to stop. + /// + /// The payload contract the consumer needs, such as . + /// What to bind to. It is copied, so later changes to it have no effect. + /// The binding; own it by the game's . + /// + /// + /// PinUP binds the game's DMD for identification frames: + /// + /// var displays = new PluginResourceSelection(ResourceKind.Display) { Controller = game.ControllerId, Role = "dmd", Session = game.Session, + /// Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray2 | DisplayEncoding.Gray4 }; + /// var dmd = game.Lifetime.Own(game.Resources.Bind<IPluginDisplaySource>(displays)); + /// game.Lifetime.Own(dmd.SubscribeFrames(frame => RenderDmdShades(frame))); + /// + /// + /// public PluginResourceBinding Bind(PluginResourceSelection selection) where T : class, IPluginResourceSource => new PluginResourceBinding(this, selection); } - /// A binding follows its selected logical resource and owns view negotiation for the current instance. + /// + /// A consumer's live connection to the one resource that matches a selection. It rebinds automatically when that resource + /// is published, replaced or withdrawn, and for displays it holds the negotiated view while bound. + /// + /// + /// + /// Created by . The binding is + /// while nothing matches, when exactly one source matches, + /// when several match (the user must choose one) or the source refused the + /// requested view, and after . + /// explains the non-bound states; plugins log it or show it in their status. + /// + /// + /// When a display binds, it calls with the selection's + /// and and keeps the lease + /// until it unbinds. For an identification binding on a PinMAME DMD this switches the ROM's frames to raw shades. + /// + /// Not thread-safe: use it on the owner thread, where registry changes and frames arrive. + /// + /// The B2S plugin connects to controller events and the source is re-attached whenever the binding rebinds: + /// + /// var binding = game.Lifetime.Own(game.Resources.Bind<IPluginControllerSource>(selection)); + /// game.Lifetime.Own(binding.Connect(source => source.Subscribe(ControllerEvent))); + /// + /// + /// + /// The payload contract the consumer uses. public sealed class PluginResourceBinding : IDisposable where T : class, IPluginResourceSource { private readonly PluginResourceRegistry _registry; @@ -100,12 +231,36 @@ public sealed class PluginResourceBinding : IDisposable where T : class, IPlu private IDisposable? _view; private readonly List> _receivers = new List>(); private readonly List _attached = new List(); + /// The bound source, or null unless is . public T? Source { get; private set; } + /// + /// Descriptor of the bound resource, or null unless bound. The DMD plugin reads it to derive its presentation resource + /// from the bound DMD. + /// public PluginResourceDescriptor? Descriptor { get; private set; } + /// Whether the binding is waiting, bound, blocked or disposed; see the class remarks. public ResourceBindingState State { get; private set; } + /// + /// Human-readable reason when the binding is not bound or the bound display sends frames in an encoding the selection + /// does not accept, such as Select a resource: … or Missing resource: …. Empty when everything is fine. + /// public string Diagnostic { get; private set; } = ""; + /// + /// Encoding of the most recent frame received through , whether it + /// was accepted or not. before the first frame and after unbinding. + /// public DisplayEncoding ActualEncoding { get; private set; } + /// The encodings this binding accepts, from . public DisplayEncoding AcceptedFormats => _selection.Accepts; + /// + /// Raised on the owner thread when the binding re-resolves (bound, unbound, blocked, rebound to a new instance) and when + /// or change because of a frame. Handler exceptions go to the + /// registry's fault handler. All handlers are dropped on . + /// + /// + /// The B2S plugin clears the live DMD on the glass here, so a lost source does not leave its last frame behind; the DMD + /// plugin logs when the binding is not bound. + /// public event Action? Changed; internal PluginResourceBinding(PluginResourceRegistry registry, PluginResourceSelection selection) { @@ -124,6 +279,17 @@ internal bool AcceptFrame(in PluginDisplayFrame frame) } return compatible; } + /// + /// Attaches a receiver to whatever source is bound, now and after every rebind. is called + /// with the current source and returns the subscription to dispose when that source goes away. + /// + /// + /// Connecting or disconnecting a receiver re-attaches all receivers of this binding. For display frames use + /// , which also checks the frame's instance and encoding. + /// + /// Subscribes to a source and returns the handle that undoes it. + /// A handle that detaches the receiver. + /// The binding has been disposed. public IDisposable Connect(Func subscribe) { if (State == ResourceBindingState.Disposed) throw new ObjectDisposedException(nameof(PluginResourceBinding)); @@ -186,6 +352,10 @@ private void ClearAttachments() foreach (var attached in _attached) try { attached.Dispose(); } catch (Exception error) { _registry.Report(error); } _attached.Clear(); } + /// + /// Stops following the registry, detaches all receivers, releases the view lease and sets to + /// . Does not raise . Later calls do nothing. + /// public void Dispose() { if (State == ResourceBindingState.Disposed) return; @@ -195,9 +365,26 @@ public void Dispose() } } + /// Display-specific helpers for . public static class DisplayBindingExtensions { - /// Checks the concrete encoding and generation before delivering borrowed pixels. + /// + /// Receives the frames of whatever display the binding is bound to, across rebinds. Frames from a previous instance and + /// frames in an encoding the selection does not accept are dropped; the latter set . + /// + /// + /// The frame and its pixels are borrowed: copy what you need before returning. Called synchronously on the thread that + /// publishes the frame, the owner thread for VPE's sources. + /// + /// + /// game.Lifetime.Own(binding.SubscribeFrames(frame => _bridge.FeedFrame(frame, displayId))); + /// + /// + /// + /// A display binding. + /// Called for every accepted frame. + /// A handle that stops delivery; own it by the same scope as the binding. + /// The binding has been disposed. public static IDisposable SubscribeFrames(this PluginResourceBinding binding, Action receive) => binding.Connect(source => source.Subscribe(frame => { if (binding.AcceptFrame(frame)) receive(frame); })); } diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs index ffacd922b..5080404b7 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs @@ -15,7 +15,69 @@ namespace VisualPinball.Unity { - /// Adapts engine output to VPE-owned resources before the inactive table starts. + /// + /// Publishes a table's gamelogic engine to plugins: its controller events, displays, segment displays, native outputs and audio + /// become resources in the , where plugins such as PinUP, the B2S backglass and DMD bind them. + /// + /// + /// The Player creates one per game after loading the table, before the table starts and before + /// , so the resources exist when plugins prepare. It owns the instance in its game + /// scope and disposes it after the plugins have stopped. Every resource carries the game session, and its ID starts with + /// : + /// + /// Resource IDKind, role and when it is published + /// + /// <controller> + /// + /// , role controller: coil, lamp, GI and switch changes by mapped ID. Always. + /// + /// + /// + /// <controller>/outputs + /// + /// , role outputs: every output by native number, when the engine implements + /// . + /// + /// + /// + /// <controller>/audio/rom + /// + /// , role rom-mix, when the engine implements . + /// + /// + /// + /// <controller>/display/<id> + /// + /// , role dmd: one per in the table, and one + /// per dot-matrix display the engine announces through . + /// + /// + /// + /// <controller>/segments/<id> + /// + /// , role segments: one per segment display the engine announces (a display with a + /// height of 1 or less), with its layout from . + /// + /// + /// + /// Display frames are converted with ; frames of unknown displays or formats are dropped. The display + /// encodings come from . Without it, displays offer + /// and , plus + /// and when the engine implements , through which + /// a consumer's request for raw identification frames is forwarded. + /// + /// Create and dispose it on the owner thread. The engine must raise its display events there (they throw otherwise); coil, + /// lamp, switch and native output changes raised on other threads are posted to the owner thread. + /// + /// + /// From the Player's plugin controller: + /// + /// if (engine != null) + /// _gameScope.Own(new GamelogicPluginResources(table, engine, identity, session, Host.Scheduler, Host.Resources, Debug.LogException)); + /// await Host.PrepareAndStartAsync(identity, path, title, identity.Id + "/controller/0", session, cancellation); + /// + /// + /// public sealed class GamelogicPluginResources : IDisposable { private sealed class Display : IDisposable @@ -69,6 +131,26 @@ private sealed class NativeOutputs : INativeOutputListener private readonly DisplayEncoding _defaultFormat; private long _sequence; private bool _disposed; + /// + /// Publishes the engine's controller, native outputs and audio and the table's dot-matrix displays, and subscribes to the + /// engine's events. Displays and segment displays that the engine announces later are published when it announces them. + /// + /// + /// The table's root object. Every under it, including inactive ones, is published up + /// front, so plugins can bind the DMD before the engine announces its displays. + /// + /// The table's gamelogic engine. + /// + /// The game. Its becomes the resources' provider, and its + /// the start of . + /// + /// The game session number, the same one passed to . + /// The host's owner-thread scheduler, used to check for and post to the owner thread. + /// The registry to publish into, normally . + /// + /// Receives exceptions thrown by subscribers of the published sources, and errors for display frames whose buffer is too + /// small. The Player passes Debug.LogException. + /// public GamelogicPluginResources(GameObject table, IGamelogicEngine engine, GameIdentity identity, long session, OwnerScheduler scheduler, PluginResourceRegistry registry, Action fault) { @@ -100,6 +182,11 @@ public GamelogicPluginResources(GameObject table, IGamelogicEngine engine, GameI engine.OnSwitchChanged += Switch; foreach (var display in table.GetComponentsInChildren(true)) AddDisplay(display.Id, display.Width, display.Height); } + /// + /// ID of the published controller resource and prefix of every other resource ID: the game's + /// followed by /controller/0, such as pinmame:t2_l8/controller/0. It matches the + /// the Player passes to plugins. + /// public string ControllerId => _controllerId; private void DisplaysRequested(object sender, RequestedDisplays args) { @@ -183,15 +270,43 @@ private string NumberName(int number) private void Lamps(object sender, LampsEventArgs args) { foreach (var lamp in args.LampsChanged) Lamp(sender, lamp); } private void Switch(object sender, SwitchEventArgs2 args) => Emit(ControllerEventKind.Switch, args.Id, args.IsEnabled ? 1 : 0); private static long Timestamp() => (long)(Stopwatch.GetTimestamp() * (1000000d / Stopwatch.Frequency)); + /// + /// Converts a gamelogic engine's to the plugin : + /// to , to + /// , to , + /// and and to . + /// + /// The engine's frame format. + /// + /// The matching encoding, or for the segment, alphanumeric and numeric formats, which are + /// not published as display frames. + /// public static DisplayEncoding FromLegacy(DisplayFrameFormat format) => format switch { DisplayFrameFormat.Dmd2 => DisplayEncoding.Gray2, DisplayFrameFormat.Dmd4 => DisplayEncoding.Gray4, DisplayFrameFormat.Dmd8 => DisplayEncoding.Luminance8, DisplayFrameFormat.Dmd24 => DisplayEncoding.Rgb24, DisplayFrameFormat.Video24 => DisplayEncoding.Rgb24, _ => DisplayEncoding.None }; + /// + /// Converts a plugin to the to request from a gamelogic engine + /// through : to + /// , to , + /// to , and anything else to + /// . The DMD plugin also uses it to hand frames to its native pipeline. + /// + /// + /// A single encoding. , , + /// and combined flags all map to . + /// + /// The matching engine frame format. public static DisplayFrameFormat ToLegacy(DisplayEncoding format) => format switch { DisplayEncoding.Gray2 => DisplayFrameFormat.Dmd2, DisplayEncoding.Gray4 => DisplayFrameFormat.Dmd4, DisplayEncoding.Rgb24 => DisplayFrameFormat.Dmd24, _ => DisplayFrameFormat.Dmd8 }; + /// + /// Unsubscribes from the engine, unpublishes every resource, removes the native output listener and, if raw identification + /// frames were requested, asks the engine for its default display format again. Safe to call more than once. Call it on the + /// owner thread after the plugins have stopped, as the Player does when it disposes its game scope. + /// public void Dispose() { if (_disposed) return; _disposed = true; From 283de89901af9e614519cc762ec468ecb04ddb81 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 11:35:30 +0200 Subject: [PATCH 17/26] plugins: keep resource notices that arrive while a binding releases its view A binding ignored every notice while releasing its view, so a display published or withdrawn during that release left it with stale candidates. Now it only ignores change notices that cannot help it: the one for the resource it is releasing, and those for resources outside its selection, as an engine-wide format lease raises for every display. Publications and withdrawals always trigger another pass. --- .../SettingsAndNegotiationTests.cs | 52 +++++++++++++++++++ .../Resources/PluginResourceRegistry.cs | 23 +++++--- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs b/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs index 292d0c432..7f2264e21 100644 --- a/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs +++ b/VisualPinball.Plugins.Test/SettingsAndNegotiationTests.cs @@ -115,6 +115,27 @@ public void AFailingRetryStaysBlockedInsteadOfLooping() Assert.That(gray4.State, Is.EqualTo(ResourceBindingState.Blocked)); } + [Test] + public void AFailingRetryOnASharedLeaseStaysBlockedInsteadOfLooping() + { + var registry = new PluginResourceRegistry(); + const string first = "pinmame/controller/0/display/dmd0", second = "pinmame/controller/0/display/dmd1"; + const DisplayEncoding formats = DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8; + // One negotiator for the whole engine notifies every display when its lease ends, as GamelogicPluginResources does. + using var negotiator = new DisplayFormatNegotiator(formats, DisplayEncoding.Luminance8, _ => { }, () => { registry.NotifyChanged(first); registry.NotifyChanged(second); }); + using var firstSource = new DisplayResourceSource(negotiator.Acquire); + using var secondSource = new DisplayResourceSource(negotiator.Acquire); + using var firstPublication = registry.Publish(new PluginResourceDescriptor(first, "pinmame", "pinmame/controller/0", "dmd0", "dmd", ResourceKind.Display, 1, 1, ResourceView.Original, formats), firstSource); + using var secondPublication = registry.Publish(new PluginResourceDescriptor(second, "pinmame", "pinmame/controller/0", "dmd1", "dmd", ResourceKind.Display, 1, 2, ResourceView.Original, formats), secondSource); + var gray2 = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = first, Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray2 }); + using var gray4 = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = first, Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray4 }); + int attempts = 0; + using var connection = gray4.Connect(_ => { attempts++; throw new InvalidOperationException("receiver failed"); }); + Assert.DoesNotThrow(() => gray2.Dispose()); + Assert.That(attempts, Is.LessThanOrEqualTo(2)); + Assert.That(gray4.State, Is.EqualTo(ResourceBindingState.Blocked)); + } + [Test] public void DisposingABindingFromAChangeHandlerIsFinal() { @@ -131,6 +152,37 @@ public void DisposingABindingFromAChangeHandlerIsFinal() Assert.That(gray4.State, Is.EqualTo(ResourceBindingState.Disposed)); } + [Test] + public void ChangesDuringAReleaseAreNotLost() + { + var registry = new PluginResourceRegistry(); + const string first = "pinmame/controller/0/display/dmd0", second = "pinmame/controller/0/display/dmd1", third = "pinmame/controller/0/display/dmd2"; + const DisplayEncoding formats = DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8; + using var negotiator = new DisplayFormatNegotiator(formats, DisplayEncoding.Luminance8, _ => { }, () => registry.NotifyChanged(first)); + using var source = new DisplayResourceSource(negotiator.Acquire); + using var publication = registry.Publish(new PluginResourceDescriptor(first, "pinmame", "pinmame/controller/0", "dmd0", "dmd", ResourceKind.Display, 1, 1, ResourceView.Original, formats), source); + // Bound to the only display, holding Gray2. + using var any = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray2 }); + // Refused on the same display until Gray2 is released; once it gets in, it publishes a third display. + using var thirdSource = new DisplayResourceSource(); + IDisposable? thirdPublication = null; + using var gray4 = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = first, Session = 1, Intent = ResourceView.Identification, Accepts = DisplayEncoding.Gray4 }); + gray4.Changed += () => { + if (gray4.State == ResourceBindingState.Bound && thirdPublication == null) + thirdPublication = registry.Publish(new PluginResourceDescriptor(third, "pinmame", "pinmame/controller/0", "dmd2", "dmd", ResourceKind.Display, 1, 3, ResourceView.Original, formats), thirdSource); + }; + Assert.That(any.State, Is.EqualTo(ResourceBindingState.Bound)); + Assert.That(gray4.State, Is.EqualTo(ResourceBindingState.Blocked)); + // A second display makes the first binding ambiguous. Releasing its view lets the Gray4 binding in, which publishes a + // third display while the first binding is still releasing; that publication must not be lost. + using var secondSource = new DisplayResourceSource(); + using var secondPublication = registry.Publish(new PluginResourceDescriptor(second, "pinmame", "pinmame/controller/0", "dmd1", "dmd", ResourceKind.Display, 1, 2, ResourceView.Original, formats), secondSource); + Assert.That(gray4.State, Is.EqualTo(ResourceBindingState.Bound)); + Assert.That(thirdPublication, Is.Not.Null); + Assert.That(any.Diagnostic, Does.Contain(third)); + thirdPublication!.Dispose(); + } + private sealed class Factory : IPlayerPluginFactory { public PluginDescriptor Descriptor { get; } diff --git a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs index 994df2bb9..d6681e014 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs @@ -265,7 +265,7 @@ public sealed class PluginResourceBinding : IDisposable where T : class, IPlu internal PluginResourceBinding(PluginResourceRegistry registry, PluginResourceSelection selection) { _registry = registry; _selection = selection.Copy(); - _watch = registry.Observe(_ => Refresh()); + _watch = registry.Observe(Refresh); Refresh(); } internal bool AcceptFrame(in PluginDisplayFrame frame) @@ -297,13 +297,20 @@ public IDisposable Connect(Func subscribe) RefreshAttachments(); return new Subscription(() => { _receivers.Remove(subscribe); RefreshAttachments(); }); } - private bool _refreshing, _refreshAgain, _releasing; + private bool _refreshing, _refreshAgain; + // ID of the resource whose view this binding is releasing, while it does. + private string? _releasingId; + private void Refresh(PluginResourceChange change) + { + // A change notice for a resource this binding does not select cannot affect it, and the one caused by releasing its + // own view says nothing new to it. Retrying on either would repeat a failed attempt forever, for example when an + // engine-wide format lease notifies every display of the engine. Publications and withdrawals always count. + if (change.Kind == ResourceChangeKind.Changed && (change.Descriptor.Id == _releasingId || !_selection.Matches(change.Descriptor))) return; + Refresh(); + } private void Refresh() { - // A notice caused by releasing this binding's own view says nothing new to it; retrying would repeat a failed - // attempt forever. - if (_releasing) return; - // Other notices during a pass (another binding released a view) are handled by one more pass afterwards. + // Notices during a pass are handled by one more pass afterwards, which reads the candidates again. if (_refreshing) { _refreshAgain = true; return; } _refreshing = true; try { @@ -343,8 +350,8 @@ private void RefreshAttachments() private void Detach() { ClearAttachments(); - _releasing = true; - try { _view?.Dispose(); } catch (Exception error) { _registry.Report(error); } finally { _releasing = false; } + _releasingId = Descriptor?.Id; + try { _view?.Dispose(); } catch (Exception error) { _registry.Report(error); } finally { _releasingId = null; } _view = null; Source = null; Descriptor = null; ActualEncoding = DisplayEncoding.None; } private void ClearAttachments() From 4946002595c8e4868cf18ea5ebab220f62d23d6e Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 12:40:05 +0200 Subject: [PATCH 18/26] plugins: describe mapped output values and present chosen displays - Mapped lamp events on the controller resource now carry what their raw values mean. The rules come from the table's lamp mapping, the same ones LampPlayer applies: on/off lamps are binary, fading lamps run to their fading steps, and RGB channels run to 255. Consumers without native outputs, such as a backglass, no longer light faded lamps or GI levels fully. Numeric IDs are registered with and without leading zeros, as LampPlayer does. - A display selection with the presentation intent and a chosen resource ID also matches the presentation views derived from that display, and prefers them. A display picked by hand keeps its colorization. --- .../OutputResourceTests.cs | 37 ++++++++++++++++ .../Resources/PluginResourceRegistry.cs | 2 +- .../Resources/ResourceContracts.cs | 11 ++++- .../Plugins/GamelogicPluginResources.cs | 42 +++++++++++++++++-- 4 files changed, 86 insertions(+), 6 deletions(-) diff --git a/VisualPinball.Plugins.Test/OutputResourceTests.cs b/VisualPinball.Plugins.Test/OutputResourceTests.cs index 5bc8ba077..5391ac529 100644 --- a/VisualPinball.Plugins.Test/OutputResourceTests.cs +++ b/VisualPinball.Plugins.Test/OutputResourceTests.cs @@ -45,6 +45,43 @@ public void SegmentDisplaysAnnounceTheirPlaceBeforeTheFirstFrame() Assert.That(binding.Source.Hardware, Is.EqualTo("vfd-green")); } + [Test] + public void AChosenDisplayIsPresentedThroughItsDerivedView() + { + var registry = new PluginResourceRegistry(); + const string original = "pinmame/controller/0/display/dmd0", presentation = original + "/presentation/org.visualpinball.dmd"; + const DisplayEncoding formats = DisplayEncoding.Gray4 | DisplayEncoding.Rgb24; + using var originalSource = new DisplayResourceSource(); + using var originalPublication = registry.Publish(new PluginResourceDescriptor(original, "pinmame", "pinmame/controller/0", "dmd0", "dmd", ResourceKind.Display, 1, 1, ResourceView.Original, formats), originalSource); + var selection = new PluginResourceSelection(ResourceKind.Display) { ResourceId = original, Session = 1, Intent = ResourceView.Presentation, AllowOriginalFallback = true }; + using var binding = registry.Bind(selection); + // Without a colorizer the chosen display itself is used. + Assert.That(binding.Descriptor!.Id, Is.EqualTo(original)); + // Once a plugin presents it, the chosen display is shown through that view. + using var presentationSource = new DisplayResourceSource(); + using (registry.Publish(new PluginResourceDescriptor(presentation, "org.visualpinball.dmd", "pinmame/controller/0", "dmd0 colorized", "dmd", ResourceKind.Display, 1, 2, ResourceView.Presentation, formats, original), presentationSource)) { + Assert.That(binding.Descriptor!.Id, Is.EqualTo(presentation)); + } + Assert.That(binding.Descriptor!.Id, Is.EqualTo(original)); + } + + [Test] + public void AChosenDisplayIgnoresOtherDisplaysPresentations() + { + var registry = new PluginResourceRegistry(); + const string chosen = "pinmame/controller/0/display/dmd0", other = "pinmame/controller/0/display/dmd1"; + const DisplayEncoding formats = DisplayEncoding.Gray4 | DisplayEncoding.Rgb24; + using var chosenSource = new DisplayResourceSource(); + using var otherSource = new DisplayResourceSource(); + using var presentationSource = new DisplayResourceSource(); + using var chosenPublication = registry.Publish(new PluginResourceDescriptor(chosen, "pinmame", "pinmame/controller/0", "dmd0", "dmd", ResourceKind.Display, 1, 1, ResourceView.Original, formats), chosenSource); + using var otherPublication = registry.Publish(new PluginResourceDescriptor(other, "pinmame", "pinmame/controller/0", "dmd1", "dmd", ResourceKind.Display, 1, 2, ResourceView.Original, formats), otherSource); + using var presentationPublication = registry.Publish(new PluginResourceDescriptor(other + "/presentation/x", "x", "pinmame/controller/0", "dmd1 colorized", "dmd", ResourceKind.Display, 1, 3, ResourceView.Presentation, formats, other), presentationSource); + using var binding = registry.Bind(new PluginResourceSelection(ResourceKind.Display) { ResourceId = chosen, Session = 1, Intent = ResourceView.Presentation, AllowOriginalFallback = true }); + Assert.That(binding.State, Is.EqualTo(ResourceBindingState.Bound)); + Assert.That(binding.Descriptor!.Id, Is.EqualTo(chosen)); + } + [Test] public void ControllerSourcesCarryOnlyControllerKinds() { diff --git a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs index d6681e014..8b5f86045 100644 --- a/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs +++ b/VisualPinball.Plugins/Resources/PluginResourceRegistry.cs @@ -80,7 +80,7 @@ internal sealed class Entry internal IReadOnlyList Candidates(PluginResourceSelection selection) { var found = _entries.Values.Where(entry => selection.Matches(entry.Descriptor) && Reachable(entry, new HashSet())).OrderBy(entry => entry.Descriptor.Id, StringComparer.Ordinal).ToList(); - if (selection.ResourceId.Length == 0 && selection.Intent == ResourceView.Presentation && found.Any(entry => entry.Descriptor.View == ResourceView.Presentation)) + if (selection.Intent == ResourceView.Presentation && found.Any(entry => entry.Descriptor.View == ResourceView.Presentation)) found.RemoveAll(entry => entry.Descriptor.View != ResourceView.Presentation); return found; } diff --git a/VisualPinball.Plugins/Resources/ResourceContracts.cs b/VisualPinball.Plugins/Resources/ResourceContracts.cs index 47dd28c90..e568579e0 100644 --- a/VisualPinball.Plugins/Resources/ResourceContracts.cs +++ b/VisualPinball.Plugins/Resources/ResourceContracts.cs @@ -363,6 +363,8 @@ public sealed class PluginResourceSelection /// /// The exact wanted, or empty (the default) for any. Use it to pick one of /// several matching resources, for example from a saved choice through . + /// With , it names the original display and also matches + /// the presentation views derived from it, which are then preferred, so a chosen display still shows colorized frames. /// public string ResourceId { get; set; } = ""; /// The controller ID wanted, normally , or empty for any. @@ -404,9 +406,16 @@ public sealed class PluginResourceSelection ResourceId = ResourceId, Controller = Controller, Role = Role, Session = Session, Intent = Intent, Accepts = Accepts, AllowOriginalFallback = AllowOriginalFallback }; + /// A presentation view derived from the chosen resource, wanted when the intent is presentation. + private bool PresentsChosen(PluginResourceDescriptor descriptor) + { + if (Intent != ResourceView.Presentation || descriptor.View != ResourceView.Presentation) return false; + foreach (string parent in descriptor.Parents) if (parent == ResourceId) return true; + return false; + } internal bool Matches(PluginResourceDescriptor descriptor) { - if (descriptor.Kind != Kind || ResourceId.Length > 0 && descriptor.Id != ResourceId || Controller.Length > 0 && descriptor.Controller != Controller + if (descriptor.Kind != Kind || ResourceId.Length > 0 && descriptor.Id != ResourceId && !PresentsChosen(descriptor) || Controller.Length > 0 && descriptor.Controller != Controller || Role.Length > 0 && descriptor.Role != Role || Session != 0 && descriptor.Session != Session) return false; if (Kind == ResourceKind.Display && (descriptor.Formats & Accepts) == 0) return false; if (Intent == ResourceView.Identification) return descriptor.View != ResourceView.Presentation && descriptor.Parents.Count == 0; diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs index 5080404b7..c4bca810a 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/GamelogicPluginResources.cs @@ -120,6 +120,9 @@ private sealed class NativeOutputs : INativeOutputListener private readonly OwnerScheduler _scheduler; private readonly PluginResourceRegistry _registry; private readonly Dictionary _displays = new Dictionary(StringComparer.Ordinal); + // What the engine's mapped lamp values mean, from the table's lamp mapping (the same rules LampPlayer applies). + private readonly Dictionary<(LampSource source, bool isCoil, string id), (ControllerValueSemantics semantics, double maximum)> _lampSemantics + = new Dictionary<(LampSource, bool, string), (ControllerValueSemantics, double)>(); private readonly ResourceScope _scope = new ResourceScope(); private readonly ControllerResourceSource _controller; private readonly DisplayFormatNegotiator _group; @@ -156,6 +159,8 @@ public GamelogicPluginResources(GameObject table, IGamelogicEngine engine, GameI { _engine = engine; _scheduler = scheduler; _registry = registry; _provider = identity.Provider; _controllerId = identity.Id + "/controller/0"; _session = session; _fault = fault; _controller = new ControllerResourceSource(fault); + var mapping = table != null ? table.GetComponentInChildren(true)?.MappingConfig : null; + if (mapping != null) foreach (var lamp in mapping.Lamps) AddLampSemantics(lamp); var metadata = engine as IDisplayResourceMetadata; _formats = metadata?.DisplayFormats ?? (engine is IDisplayFrameFormatPreference ? DisplayEncoding.Gray2 | DisplayEncoding.Gray4 | DisplayEncoding.Luminance8 | DisplayEncoding.Rgb24 @@ -247,11 +252,11 @@ private void DisplayFrame(object sender, DisplayFrameData args) if (args.Data == null || args.Data.Length < stride * display.Height) { _fault(new ArgumentException("Invalid frame buffer for " + args.Id)); return; } display.Source.Publish(new PluginDisplayFrame(args.Data, display.Width, display.Height, stride, format, ++_sequence, Timestamp(), display.Instance)); } - private void Emit(ControllerEventKind kind, string id, double value) + private void Emit(ControllerEventKind kind, string id, double value, ControllerValueSemantics semantics = ControllerValueSemantics.Binary, double maximum = 1d) { if (_disposed) return; - if (!_scheduler.IsOwner) { _scheduler.Post(() => Emit(kind, id, value)); return; } - _controller.Publish(new PluginControllerEvent(kind, id, value, ++_sequence, Timestamp())); + if (!_scheduler.IsOwner) { _scheduler.Post(() => Emit(kind, id, value, semantics, maximum)); return; } + _controller.Publish(new PluginControllerEvent(kind, id, value, ++_sequence, Timestamp(), -1, semantics, maximum)); } private void EmitNative(ControllerEventKind kind, int number, double value, ControllerValueSemantics semantics, double maximum) { @@ -265,8 +270,37 @@ private string NumberName(int number) if (number >= _numberNames.Length) return number.ToString(System.Globalization.CultureInfo.InvariantCulture); return _numberNames[number] ??= number.ToString(System.Globalization.CultureInfo.InvariantCulture); } + // Mapped coils stay binary: a normalized 0..1 value does not say whether the provider integrates it. Modulated coils + // are described by engines that report native outputs. private void Coil(object sender, CoilEventArgs args) => Emit(ControllerEventKind.Coil, args.Id, args.Value); - private void Lamp(object sender, LampEventArgs args) => Emit(args.Source == LampSource.GI ? ControllerEventKind.GeneralIllumination : ControllerEventKind.Lamp, args.Id, args.Value); + private void Lamp(object sender, LampEventArgs args) + { + var (semantics, maximum) = _lampSemantics.TryGetValue((args.Source, args.IsCoil, args.Id), out var known) ? known : (ControllerValueSemantics.Binary, 1d); + Emit(args.Source == LampSource.GI ? ControllerEventKind.GeneralIllumination : ControllerEventKind.Lamp, args.Id, args.Value, semantics, maximum); + } + /// + /// Records what a mapped lamp's raw values mean, as normalizes them: on/off lamps are binary, + /// fading lamps run to their fading steps (a few steps are levels, such as WPC GI 0..8; one step is a normalized 0..1 + /// brightness and many are physical brightness), and RGB channels run to 255. Numeric IDs are also registered without + /// leading zeros, as LampPlayer does. When several mappings share an ID, the widest range wins. + /// + private void AddLampSemantics(LampMapping lamp) + { + if (string.IsNullOrEmpty(lamp.Id)) return; + (ControllerValueSemantics, double) meaning = lamp.Type switch { + LampType.SingleFading when lamp.FadingSteps > 0 => (lamp.FadingSteps >= 2 && lamp.FadingSteps <= 9 ? ControllerValueSemantics.Levels : ControllerValueSemantics.Physical, (double)lamp.FadingSteps), + LampType.Rgb => (ControllerValueSemantics.Physical, 255d), + LampType.RgbMulti => (ControllerValueSemantics.Physical, 255d), + _ => (ControllerValueSemantics.Binary, 1d) + }; + AddLampSemantics(lamp.Source, lamp.IsCoil, lamp.Id, meaning); + if (int.TryParse(lamp.Id, out int number) && number.ToString() != lamp.Id) AddLampSemantics(lamp.Source, lamp.IsCoil, number.ToString(), meaning); + } + private void AddLampSemantics(LampSource source, bool isCoil, string id, (ControllerValueSemantics semantics, double maximum) meaning) + { + var key = (source, isCoil, id); + if (!_lampSemantics.TryGetValue(key, out var existing) || meaning.maximum > existing.maximum) _lampSemantics[key] = meaning; + } private void Lamps(object sender, LampsEventArgs args) { foreach (var lamp in args.LampsChanged) Lamp(sender, lamp); } private void Switch(object sender, SwitchEventArgs2 args) => Emit(ControllerEventKind.Switch, args.Id, args.IsEnabled ? 1 : 0); private static long Timestamp() => (long)(Stopwatch.GetTimestamp() * (1000000d / Stopwatch.Frequency)); From 889edb87462f31e8d251c004d37536f9735c5a94 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 14:11:23 +0200 Subject: [PATCH 19/26] camera: bound orbit zoom radius and drop drag state on disable Wheel zoom-out multiplied the radius without an upper bound, sending the camera to float-precision distances where SendMouseEvents fails with "Screen position out of view frustum". --- .../Game/CameraTranslateAndOrbit.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs index 803a79f92..6ee821a51 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Game/CameraTranslateAndOrbit.cs @@ -33,12 +33,14 @@ public class CameraTranslateAndOrbit : MonoBehaviour public Player player; private const float RadiusMin = 0.5f; + private const float RadiusMax = 20f; // unbounded zoom-out pushes the camera to float-precision distances (broken raycasts) private Transform _transformCache; private GameObject _dummyForRotation; private Transform _dummyTransform; private float _radius; + private float _radiusMax; private Vector3 _focusPoint; private float _radiusCurrent = 15f; @@ -81,6 +83,7 @@ private void Start() _transformCache = transform; _radius = Vector3.Distance(Vector3.zero, _transformCache.position); + _radiusMax = Mathf.Max(RadiusMax, _radius); _focusPoint = -_transformCache.forward * _radius; _positionOffsetCurrent = positionOffset; _radiusCurrent = _radius; @@ -137,6 +140,13 @@ private void Update() } } + private void OnDisable() + { + // a button released while disabled is never seen, so drop any drag in progress + _isTrackingMouse0 = false; + _isTrackingMouse1 = false; + } + private void LateUpdate() { if (lockTarget == null) return; @@ -281,11 +291,11 @@ private void UpdateMouse() float deltaScroll = _mouse.scroll.y.ReadValue() / 300f * zoomSpeed; /* --------------- UpdateMouse() – ZOOM in lock --------------- */ if (lockTarget != null && _isTrackingBall) { // ★ only follow-lock zooms - float r = Mathf.Max(_ballFollowOffsetTarget.magnitude * (1f - deltaScroll), RadiusMin); + float r = Mathf.Clamp(_ballFollowOffsetTarget.magnitude * (1f - deltaScroll), RadiusMin, _radiusMax); _ballFollowOffsetTarget = _ballFollowOffsetTarget.normalized * r; } else if (lockTarget == null) { /* original zoom path (unchanged) */ - _radius = Mathf.Max(_radius - deltaScroll * _radius, RadiusMin); + _radius = Mathf.Clamp(_radius - deltaScroll * _radius, RadiusMin, _radiusMax); } } From 1d49856987f5db53aab7fcb37216ea45d0e6fd32 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 15:14:04 +0200 Subject: [PATCH 20/26] display: add cabinet screens that show plugin surfaces in the scene CabinetScreenComponent marks a renderer as the backglass, topper, DMD, full DMD or a custom screen of the cabinet, with an optional root to hide when no plugin provides it. CabinetScreenBinder shows the surface a plugin publishes for the role in the screen's material slot, picking among providers by a preference order. A DMD screen only shows a plugin's DMD when the gamelogic engine announces none. SurfaceRoles lists the well-known role names. --- .../Resources/ResourceContracts.cs | 30 ++ .../Display/CabinetScreenInspector.cs | 92 +++++ .../Display/CabinetScreenInspector.cs.meta | 2 + .../Display/CabinetScreenComponent.cs | 116 +++++++ .../Display/CabinetScreenComponent.cs.meta | 2 + .../Display/CabinetScreenPackable.cs | 87 +++++ .../Display/CabinetScreenPackable.cs.meta | 2 + .../Plugins/CabinetScreenBinder.cs | 322 ++++++++++++++++++ .../Plugins/CabinetScreenBinder.cs.meta | 2 + .../Plugins/UnityTextureResource.cs | 12 +- 10 files changed, 662 insertions(+), 5 deletions(-) create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Display/CabinetScreenInspector.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Display/CabinetScreenInspector.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenComponent.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenComponent.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenPackable.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenPackable.cs.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/CabinetScreenBinder.cs create mode 100644 VisualPinball.Unity/VisualPinball.Unity/Plugins/CabinetScreenBinder.cs.meta diff --git a/VisualPinball.Plugins/Resources/ResourceContracts.cs b/VisualPinball.Plugins/Resources/ResourceContracts.cs index e568579e0..1a4c8a043 100644 --- a/VisualPinball.Plugins/Resources/ResourceContracts.cs +++ b/VisualPinball.Plugins/Resources/ResourceContracts.cs @@ -902,6 +902,36 @@ public interface IPluginSurface : IPluginResourceSource /// string Backend { get; } } + /// + /// Well-known values of resources: the + /// cabinet screens a table can show in the scene. + /// + /// + /// A table marks a renderer with VPE's CabinetScreenComponent and one of these roles. While a game runs, VPE shows + /// the surface published with that role on it; when several plugins publish the same role, the user's provider order + /// decides. Plugins may publish other roles, which tables can name as custom roles. + /// + /// PinUP publishes PUP screen 0 as the topper: + /// + /// game.Resources.Publish(new PluginResourceDescriptor(game.ControllerId + "/pinup/screen/0", plugin.Id, game.ControllerId, + /// "PUP screen 0", SurfaceRoles.Topper, ResourceKind.Surface, game.Session, instance), surface); + /// + /// + /// + public static class SurfaceRoles + { + /// The backglass. + public const string Backglass = "backglass"; + /// The screen of a topper above the backbox. + public const string Topper = "topper"; + /// + /// The DMD in the backbox. In the scene, a DMD the game's own gamelogic engine announces takes precedence, so a + /// plugin's DMD surface only shows when the engine has none. + /// + public const string Dmd = "dmd"; + /// A large DMD-sized screen in the backbox, PinUP's "FullDMD". + public const string FullDmd = "full-dmd"; + } /// One named trigger with an integer value, carried by . /// Nothing in VPE publishes trigger resources yet. public readonly struct PluginTriggerEvent diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/CabinetScreenInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/CabinetScreenInspector.cs new file mode 100644 index 000000000..bb4b8a3bf --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/CabinetScreenInspector.cs @@ -0,0 +1,92 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using UnityEditor; +using UnityEngine; + +namespace VisualPinball.Unity.Editor +{ + [CustomEditor(typeof(CabinetScreenComponent)), CanEditMultipleObjects] + public class CabinetScreenInspector : ItemInspector + { + private SerializedProperty _roleProperty; + private SerializedProperty _customRoleProperty; + private SerializedProperty _targetProperty; + private SerializedProperty _materialIndexProperty; + private SerializedProperty _textureProperty; + private SerializedProperty _hideRootProperty; + + protected override MonoBehaviour UndoTarget => target as MonoBehaviour; + + protected override void OnEnable() + { + base.OnEnable(); + + _roleProperty = serializedObject.FindProperty(nameof(CabinetScreenComponent.Role)); + _customRoleProperty = serializedObject.FindProperty(nameof(CabinetScreenComponent.CustomRole)); + _targetProperty = serializedObject.FindProperty(nameof(CabinetScreenComponent.Target)); + _materialIndexProperty = serializedObject.FindProperty(nameof(CabinetScreenComponent.MaterialIndex)); + _textureProperty = serializedObject.FindProperty(nameof(CabinetScreenComponent.TextureProperty)); + _hideRootProperty = serializedObject.FindProperty(nameof(CabinetScreenComponent.HideRoot)); + } + + public override void OnInspectorGUI() + { + BeginEditing(); + + PropertyField(_roleProperty); + if (_roleProperty.hasMultipleDifferentValues || _roleProperty.enumValueIndex == (int)CabinetScreenRole.Custom) { + PropertyField(_customRoleProperty, "Custom Role"); + } + PropertyField(_targetProperty); + PropertyField(_materialIndexProperty, "Material Slot"); + PropertyField(_textureProperty, "Texture Property"); + PropertyField(_hideRootProperty, "Hide When Unused"); + + if (!serializedObject.isEditingMultipleObjects && target is CabinetScreenComponent screen) { + Validate(screen); + } + + base.OnInspectorGUI(); + + EndEditing(); + } + + private static void Validate(CabinetScreenComponent screen) + { + var renderer = screen.TargetRenderer; + if (renderer == null) { + EditorGUILayout.HelpBox("Set a target renderer, or add this component to a GameObject with a renderer.", MessageType.Warning); + return; + } + var materials = renderer.sharedMaterials; + if (screen.MaterialIndex < 0 || screen.MaterialIndex >= materials.Length) { + EditorGUILayout.HelpBox($"\"{renderer.name}\" has no material slot {screen.MaterialIndex}.", MessageType.Warning); + return; + } + var property = CabinetScreenBinder.TextureProperty(materials[screen.MaterialIndex], screen.TextureProperty); + if (property == null) { + EditorGUILayout.HelpBox("The material has no texture property to show the screen in.", MessageType.Warning); + return; + } + if (screen.Role == CabinetScreenRole.Custom && string.IsNullOrWhiteSpace(screen.CustomRole)) { + EditorGUILayout.HelpBox("Set the role name the plugin publishes the screen with.", MessageType.Warning); + return; + } + EditorGUILayout.HelpBox($"The screen is shown in {property}.", MessageType.None); + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/CabinetScreenInspector.cs.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/CabinetScreenInspector.cs.meta new file mode 100644 index 000000000..b71af5afd --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Display/CabinetScreenInspector.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 90b40b7891cd8f44d972c8e22fb04c85 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenComponent.cs b/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenComponent.cs new file mode 100644 index 000000000..90e7ef351 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenComponent.cs @@ -0,0 +1,116 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using UnityEngine; +using VisualPinball.Plugins; + +namespace VisualPinball.Unity +{ + /// + /// The cabinet screens a can show, matching the . + /// + public enum CabinetScreenRole + { + Backglass, + Topper, + Dmd, + FullDmd, + Custom, + } + + /// + /// Marks a surface of the cabinet, such as the backglass or the topper screen, on which a plugin's screen is shown + /// while the game runs. + /// + /// + /// VPE shows the surface a plugin publishes for on the renderer, by writing it + /// into a texture property of the target's material. The authored material therefore defines the look of the screen, + /// and is what remains when no plugin provides the screen. In that case, is deactivated if set, + /// so for example a topper disappears entirely when there is no PUP pack. The material slot's property block belongs to + /// the screen, so don't drive the same slot from other components such as lights. + /// + /// The role only shows a plugin's DMD when the gamelogic engine announces no DMD + /// of its own. The engine's DMD counts as the screen's source, so the hide root of a DMD screen may contain the table's + /// . + /// + [PackAs("CabinetScreen")] + [AddComponentMenu("Pinball/Display/Cabinet Screen")] + public class CabinetScreenComponent : MonoBehaviour, IPackable + { + [Tooltip("Which cabinet screen is shown on this surface.")] + public CabinetScreenRole Role = CabinetScreenRole.Backglass; + + [Tooltip("The role name a plugin publishes the screen with, such as \"pup-screen-8\". Only used when Role is Custom.")] + public string CustomRole = string.Empty; + + [Tooltip("The renderer showing the screen. Defaults to the renderer of this GameObject.")] + public Renderer Target; + + [Tooltip("The material slot of the target renderer that shows the screen.")] + public int MaterialIndex; + + [Tooltip("The texture property of the material the screen is written to. Leave empty to use the unlit color map, the emissive color map if the material uses one, or the base color map.")] + public string TextureProperty = string.Empty; + + [Tooltip("Deactivated when no plugin provides this screen, such as the whole topper. Leave empty to keep the authored material instead.")] + public GameObject HideRoot; + + /// + /// The of the surfaces shown here. + /// + public string RoleName => Role switch { + CabinetScreenRole.Backglass => SurfaceRoles.Backglass, + CabinetScreenRole.Topper => SurfaceRoles.Topper, + CabinetScreenRole.Dmd => SurfaceRoles.Dmd, + CabinetScreenRole.FullDmd => SurfaceRoles.FullDmd, + _ => CustomRole?.Trim() ?? string.Empty, + }; + + /// + /// The renderer showing the screen: , or the renderer of this GameObject. + /// + public Renderer TargetRenderer => Target != null ? Target : GetComponent(); + + /// + /// Sets and from a role name. + /// + public void SetRoleName(string role) + { + switch (role) { + case SurfaceRoles.Backglass: Role = CabinetScreenRole.Backglass; break; + case SurfaceRoles.Topper: Role = CabinetScreenRole.Topper; break; + case SurfaceRoles.Dmd: Role = CabinetScreenRole.Dmd; break; + case SurfaceRoles.FullDmd: Role = CabinetScreenRole.FullDmd; break; + default: + Role = CabinetScreenRole.Custom; + CustomRole = role ?? string.Empty; + break; + } + } + + #region Packaging + + public byte[] Pack() => CabinetScreenPackable.Pack(this); + + public byte[] PackReferences(Transform root, PackagedRefs refs, PackagedFiles files) => CabinetScreenReferencesPackable.Pack(this, refs); + + public void Unpack(byte[] bytes) => CabinetScreenPackable.Unpack(bytes, this); + + public void UnpackReferences(byte[] data, Transform root, PackagedRefs refs, PackagedFiles files) => CabinetScreenReferencesPackable.Unpack(data, this, refs); + + #endregion + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenComponent.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenComponent.cs.meta new file mode 100644 index 000000000..856848e41 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenComponent.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: eed12cfe991430e4d87a21b73bd6207e \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenPackable.cs b/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenPackable.cs new file mode 100644 index 000000000..6d585c57d --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenPackable.cs @@ -0,0 +1,87 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ReSharper disable MemberCanBePrivate.Global + +using UnityEngine; + +namespace VisualPinball.Unity +{ + public struct CabinetScreenPackable + { + public string Role; + public int MaterialIndex; + public string TextureProperty; + + public static byte[] Pack(CabinetScreenComponent comp) + { + return PackageApi.Packer.Pack(new CabinetScreenPackable { + Role = comp.RoleName, + MaterialIndex = comp.MaterialIndex, + TextureProperty = comp.TextureProperty, + }); + } + + public static void Unpack(byte[] bytes, CabinetScreenComponent comp) + { + var data = PackageApi.Packer.Unpack(bytes); + comp.SetRoleName(data.Role); + comp.MaterialIndex = data.MaterialIndex; + comp.TextureProperty = data.TextureProperty ?? string.Empty; + } + } + + /// + /// The target renderer and the hide root, as node IDs. Renderers and plain GameObjects carry no packed type name, so + /// they are referenced by their node only. + /// + public struct CabinetScreenReferencesPackable + { + public string TargetNode; + public string HideRootNode; + + public static byte[] Pack(CabinetScreenComponent comp, PackagedRefs refs) + { + return PackageApi.Packer.Pack(new CabinetScreenReferencesPackable { + TargetNode = NodeId(comp.Target != null ? comp.Target.transform : null, refs), + HideRootNode = NodeId(comp.HideRoot != null ? comp.HideRoot.transform : null, refs), + }); + } + + public static void Unpack(byte[] bytes, CabinetScreenComponent comp, PackagedRefs refs) + { + var data = PackageApi.Packer.Unpack(bytes); + var target = Node(data.TargetNode, refs); + comp.Target = target != null ? target.GetComponent() : null; + var hideRoot = Node(data.HideRootNode, refs); + comp.HideRoot = hideRoot != null ? hideRoot.gameObject : null; + } + + private static string NodeId(Transform transform, PackagedRefs refs) + { + if (transform == null) { + return null; + } + var id = refs.GetNodeId(transform); + if (id == null) { + Debug.LogWarning($"Cabinet screen reference to '{transform.name}' points to a non-exported object; writing null reference."); + } + return id; + } + + private static Transform Node(string id, PackagedRefs refs) => string.IsNullOrEmpty(id) ? null : refs.GetNode(id); + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenPackable.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenPackable.cs.meta new file mode 100644 index 000000000..d46f4917c --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Display/CabinetScreenPackable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fa2b91a61465fe64fb7667cf32445bb3 \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/CabinetScreenBinder.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/CabinetScreenBinder.cs new file mode 100644 index 000000000..0f345385a --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/CabinetScreenBinder.cs @@ -0,0 +1,322 @@ +// Visual Pinball Engine +// Copyright (C) 2026 freezy and VPE Team +// +// SPDX-License-Identifier: MIT +// Licensed under MIT. See https://opensource.org/license/mit. + +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using VisualPinball.Plugins; + +namespace VisualPinball.Unity +{ + /// + /// Shows the surfaces plugins publish on the table's s while a game runs. + /// + /// + /// + /// Every cabinet screen follows the published for its role. When several plugins + /// publish the same role, the one listed first in the provider order wins; providers not listed come after, by ID. + /// The surface's texture is written into the screen's material with a property block, and the screen's hide root is + /// activated. Without a surface, or before its texture exists, the material's own texture is put back and the hide + /// root is deactivated, so screens without a provider are hidden from the start when the binder is created before the + /// table is activated. The property block of the screen's material slot belongs to the screen: it takes the place of + /// renderer-wide overrides while a surface is shown, and is removed when the surface goes away. + /// + /// + /// The game's own DMD takes precedence: a screen only shows a surface once the gamelogic + /// engine has started without announcing a DMD, and drops it again if the engine announces one later. Until then, and + /// while the engine's DMD is shown, its hide root stays active, since the engine's display component may live under it. + /// + /// Not thread-safe: create, use and dispose it on the main thread, where plugins publish their surfaces. + /// + public sealed class CabinetScreenBinder : IDisposable + { + /// + /// How long after the gamelogic engine started a DMD it announces still counts as the engine's own, in seconds. + /// Engines such as PinMAME announce their displays right after starting. + /// + private const float DmdAnnouncementGrace = 0.5f; + + private static readonly string[] UnlitProperties = { "_UnlitColorMap" }; + private static readonly string[] BaseProperties = { "_BaseColorMap", "_BaseMap", "_MainTex" }; + + private sealed class Screen + { + internal readonly CabinetScreenComponent Component; + internal readonly Renderer Renderer; + internal readonly int MaterialIndex; + internal readonly string Role; + internal readonly int Property; + internal readonly GameObject HideRoot; + internal bool Applied; + internal PluginResourceBinding Binding; + internal IDisposable Connection; + internal string ResourceId; + + internal Screen(CabinetScreenComponent component, Renderer renderer, int property) + { + Component = component; + Renderer = renderer; + MaterialIndex = component.MaterialIndex; + Role = component.RoleName; + Property = property; + HideRoot = component.HideRoot; + } + } + + private readonly List _screens = new List(); + private readonly IGamelogicEngine _engine; + private readonly PluginResourceRegistry _registry; + private readonly string _controller; + private readonly long _session; + private readonly IReadOnlyList _providers; + private readonly MaterialPropertyBlock _properties = new MaterialPropertyBlock(); + private readonly IDisposable _observation; + private bool _engineDmd; + private bool _dmdDecided; + private bool _disposed; + + /// + /// Starts showing surfaces on the cabinet screens of , including inactive ones. + /// + /// The table's root object. + /// The table's gamelogic engine, or null if it has none. + /// The registry plugins publish their surfaces to. + /// The game's controller ID; only surfaces of this controller are shown. + /// The game session; only surfaces of this session are shown. + /// Plugin IDs in order of preference, for roles several plugins provide. + public CabinetScreenBinder(GameObject table, IGamelogicEngine engine, PluginResourceRegistry registry, string controller, long session, IReadOnlyList providers) + { + _engine = engine; + _registry = registry; + _controller = controller ?? string.Empty; + _session = session; + _providers = providers?.ToArray() ?? Array.Empty(); + + foreach (var component in table.GetComponentsInChildren(true)) { + var renderer = component.TargetRenderer; + if (renderer == null) { + Debug.LogWarning($"Cabinet screen \"{component.name}\" has no renderer to show the screen on."); + continue; + } + if (string.IsNullOrEmpty(component.RoleName)) { + Debug.LogWarning($"Cabinet screen \"{component.name}\" has no role."); + continue; + } + var materials = renderer.sharedMaterials; + var material = component.MaterialIndex >= 0 && component.MaterialIndex < materials.Length ? materials[component.MaterialIndex] : null; + var property = TextureProperty(material, component.TextureProperty); + if (property == null) { + Debug.LogWarning($"Cabinet screen \"{component.name}\": material slot {component.MaterialIndex} of \"{renderer.name}\" has no texture property to show the screen in."); + continue; + } + _screens.Add(new Screen(component, renderer, Shader.PropertyToID(property))); + } + if (_screens.Count == 0) { + return; + } + + _dmdDecided = engine == null; + if (engine != null) { + engine.OnDisplaysRequested += DisplaysRequested; + engine.OnStarted += EngineStarted; + } + _observation = registry.Observe(RegistryChanged); + foreach (var screen in _screens) { + Present(screen, null); + } + Refresh(); + } + + /// + /// Picks the texture property of a material: the given one if the material has it, otherwise the unlit color map, the + /// emissive color map if the material samples it, or the base color map. + /// + public static string TextureProperty(Material material, string requested) + { + if (material == null) { + return null; + } + if (!string.IsNullOrWhiteSpace(requested)) { + return material.HasProperty(requested.Trim()) ? requested.Trim() : null; + } + foreach (var property in UnlitProperties) { + if (material.HasProperty(property)) { + return property; + } + } + if (material.HasProperty("_EmissiveColorMap") && material.IsKeywordEnabled("_EMISSIVE_COLOR_MAP")) { + return "_EmissiveColorMap"; + } + foreach (var property in BaseProperties) { + if (material.HasProperty(property)) { + return property; + } + } + return null; + } + + private void RegistryChanged(PluginResourceChange change) + { + if (change.Descriptor.Kind == ResourceKind.Surface && change.Kind != ResourceChangeKind.Changed) { + Refresh(); + } + } + + private void DisplaysRequested(object sender, RequestedDisplays displays) + { + if (_engineDmd || displays?.Displays == null || !displays.Displays.Any(display => display != null && display.Height > 1)) { + return; + } + _engineDmd = true; + Refresh(); + } + + private async void EngineStarted(object sender, EventArgs args) + { + if (_dmdDecided) { + return; + } + try { + await Awaitable.WaitForSecondsAsync(DmdAnnouncementGrace); + } catch (OperationCanceledException) { + return; + } + if (_disposed || _dmdDecided) { + return; + } + _dmdDecided = true; + Refresh(); + } + + /// + /// Binds every screen to the surface it should show now, and rebinds those whose choice changed. + /// + private void Refresh() + { + if (_disposed) { + return; + } + foreach (var screen in _screens) { + var resourceId = Choose(screen.Role); + if (resourceId == screen.ResourceId) { + Present(screen, screen.Binding?.Source); // the engine's DMD may have changed the screen's visibility + continue; + } + Release(screen); + screen.ResourceId = resourceId; + if (resourceId == null) { + Present(screen, null); + continue; + } + var binding = _registry.Bind(new PluginResourceSelection(ResourceKind.Surface) { + ResourceId = resourceId, Controller = _controller, Session = _session, + }); + screen.Binding = binding; + binding.Changed += () => Present(screen, binding.Source); + screen.Connection = binding.Connect(source => { + Action changed = _ => Present(screen, source); + source.Changed += changed; + return new Subscription(() => source.Changed -= changed); + }); + Present(screen, binding.Source); + } + } + + /// + /// The ID of the surface a role shows: the one of the most preferred provider, or null for none. + /// + private string Choose(string role) + { + if (role == SurfaceRoles.Dmd && (_engineDmd || !_dmdDecided)) { + return null; + } + var candidates = _registry.Snapshot(new PluginResourceSelection(ResourceKind.Surface) { + Role = role, Controller = _controller, Session = _session, + }); + return candidates + .OrderBy(descriptor => Rank(descriptor.Provider)) + .ThenBy(descriptor => descriptor.Provider, StringComparer.Ordinal) + .ThenBy(descriptor => descriptor.Id, StringComparer.Ordinal) + .FirstOrDefault()?.Id; + } + + private int Rank(string provider) + { + for (var i = 0; i < _providers.Count; i++) { + if (string.Equals(_providers[i], provider, StringComparison.Ordinal)) { + return i; + } + } + return int.MaxValue; + } + + /// + /// Shows the source's texture on the screen, or restores the authored texture and hides the screen when there is + /// none. A DMD screen stays visible while the engine's DMD may still claim it, so the engine's display component + /// under its hide root is active when the table activates and keeps receiving frames. + /// + private void Present(Screen screen, IUnityTextureResource source) + { + if (_disposed || screen.Renderer == null) { + return; + } + var texture = source?.Texture; + if (texture != null) { + screen.Renderer.GetPropertyBlock(_properties, screen.MaterialIndex); + _properties.SetTexture(screen.Property, texture); + screen.Renderer.SetPropertyBlock(_properties, screen.MaterialIndex); + screen.Applied = true; + SetVisible(screen, true); + return; + } + if (screen.Applied) { + // The slot's block belongs to the screen; removing it brings back the material's current texture, such as + // the one a DisplayComponent assigns when the engine takes the DMD over, and renderer-wide overrides. + screen.Renderer.SetPropertyBlock(null, screen.MaterialIndex); + screen.Applied = false; + } + SetVisible(screen, screen.Role == SurfaceRoles.Dmd && (_engineDmd || !_dmdDecided)); + } + + private static void SetVisible(Screen screen, bool visible) + { + if (screen.HideRoot != null && screen.HideRoot.activeSelf != visible) { + screen.HideRoot.SetActive(visible); + } + } + + private static void Release(Screen screen) + { + screen.Connection?.Dispose(); + screen.Connection = null; + screen.Binding?.Dispose(); + screen.Binding = null; + screen.ResourceId = null; + } + + /// + /// Stops following the surfaces. Screens go back to their authored texture, and hide roots of screens without the + /// engine's DMD are hidden. + /// + public void Dispose() + { + if (_disposed) { + return; + } + foreach (var screen in _screens) { + Release(screen); + Present(screen, null); + } + _disposed = true; + _observation?.Dispose(); + if (_engine != null && _screens.Count > 0) { + _engine.OnDisplaysRequested -= DisplaysRequested; + _engine.OnStarted -= EngineStarted; + } + } + } +} diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/CabinetScreenBinder.cs.meta b/VisualPinball.Unity/VisualPinball.Unity/Plugins/CabinetScreenBinder.cs.meta new file mode 100644 index 000000000..66107ce50 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/CabinetScreenBinder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c6f55a0e16f82b2409390e12d472ec5f \ No newline at end of file diff --git a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs index 3f10ab2a0..c8665687f 100644 --- a/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs +++ b/VisualPinball.Unity/VisualPinball.Unity/Plugins/UnityTextureResource.cs @@ -16,10 +16,12 @@ namespace VisualPinball.Unity /// /// /// Plugins that render an image other plugins can show publish it as a surface with this interface, so consumers in the - /// same Unity process can use the texture directly on the GPU. PinUP publishes each PUP screen's render texture (role - /// backglass for screen 2, dmd for screen 1, pup-screen-N otherwise), and the B2S plugin publishes - /// its composed backglass with the role backglass. Both report unity-render-texture as - /// . Consumers must not modify, resize or release the texture, and must not keep + /// same Unity process can use the texture directly on the GPU. PinUP publishes the render texture of each PUP screen + /// that shows video (role topper for screen 0, dmd for screen 1, backglass for screen 2, + /// full-dmd for screen 5, pup-screen-N otherwise), and the B2S plugin publishes its composed backglass + /// with the role backglass; see . Both report unity-render-texture as + /// . shows these surfaces on the table's + /// s. Consumers must not modify, resize or release the texture, and must not keep /// using it after the resource is removed. /// /// Publishing a render texture: @@ -37,7 +39,7 @@ namespace VisualPinball.Unity /// } /// /// game.Lifetime.Own(game.Resources.Publish(new PluginResourceDescriptor(game.ControllerId + "/surface/mine", _application.Plugin.Id, - /// game.ControllerId, "My surface", "backglass", ResourceKind.Surface, game.Session, 1), new MySurface(texture))); + /// game.ControllerId, "My surface", SurfaceRoles.Backglass, ResourceKind.Surface, game.Session, 1), new MySurface(texture))); /// /// /// From 7b0109a8248393596b606ebd4d71c463d43eb25c Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 16:30:13 +0200 Subject: [PATCH 21/26] plugins: multiple choice settings PluginField.Multiple lets a choice field pick any number of catalog items, stored as a list of IDs, validated and checked against the catalog like single choices. PluginSettingsSnapshot.Strings reads such lists. --- .../ConfigurationTests.cs | 17 +++++++++++++ .../Configuration/PluginSchema.cs | 24 ++++++++++++++++++- .../Configuration/PluginSettingsSession.cs | 4 +++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/VisualPinball.Plugins.Test/ConfigurationTests.cs b/VisualPinball.Plugins.Test/ConfigurationTests.cs index c3fd107cd..a582d049e 100644 --- a/VisualPinball.Plugins.Test/ConfigurationTests.cs +++ b/VisualPinball.Plugins.Test/ConfigurationTests.cs @@ -101,6 +101,23 @@ public void ScreenLayoutsAllowSignedDesktopCoordinatesAndRejectAmbiguousOrInvali Assert.That(schema.Validate(new PluginSettingsSnapshot(1, valid)), Has.Count.EqualTo(1)); } [Test] + public void MultipleChoicesAreListsOfIds() + { + var schema = new PluginSettingsSchema(1, new PluginSettingsGroup("Test", "", + new PluginField("output", "Output", SettingKind.Choice) { Provider = "outputs", Multiple = true, Default = new JArray("native") }, + new PluginField("single", "Single", SettingKind.Choice) { Provider = "outputs", Default = "native" })); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, JObject.Parse("{'output':['native','scene'],'single':'scene'}"))), Is.Empty); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, JObject.Parse("{'output':[]}"))), Is.Empty, "picking none is valid"); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, JObject.Parse("{'output':'native'}"))), Has.Count.EqualTo(1)); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, JObject.Parse("{'output':['native',3]}"))), Has.Count.EqualTo(1)); + Assert.That(schema.Validate(new PluginSettingsSnapshot(1, JObject.Parse("{'single':['native']}"))), Has.Count.EqualTo(1)); + var values = new PluginSettingsSnapshot(1, JObject.Parse("{'output':['native',1,'scene'],'single':'scene'}")); + Assert.That(values.Strings("output"), Is.EqualTo(new[] { "native", "scene" })); + Assert.That(values.Strings("single"), Is.Empty); + Assert.That(values.Strings("missing"), Is.Empty); + Assert.That(schema.Defaults().Strings("output"), Is.EqualTo(new[] { "native" })); + } + [Test] public void NestedRecordValidationChecksDeclaredTypesAndRanges() { var item = new PluginField("devices", "Devices", SettingKind.Collection) { Children = new[] { new PluginField("gain", "Gain", SettingKind.Number) { Minimum = 0, Maximum = 1 } } }; diff --git a/VisualPinball.Plugins/Configuration/PluginSchema.cs b/VisualPinball.Plugins/Configuration/PluginSchema.cs index 8ff15c5a8..752d73604 100644 --- a/VisualPinball.Plugins/Configuration/PluginSchema.cs +++ b/VisualPinball.Plugins/Configuration/PluginSchema.cs @@ -186,6 +186,19 @@ public sealed class PluginField /// public string Provider { get; set; } = ""; /// + /// For fields, lets the user pick any number of the catalog's items instead of exactly one. The + /// Player shows every item as a button that toggles on and off, and the value is a JSON array of s, + /// empty when none is picked. Read it with . Not supported inside composite or + /// collection records. False by default. + /// + /// + /// The B2S plugin lets the user show the backglass in a window, on the table's backglass, or both: + /// + /// new PluginField("Output", "Output", SettingKind.Choice) { Provider = "outputs", Multiple = true, Default = new JArray("native") } + /// + /// + public bool Multiple { get; set; } + /// /// The of another field that controls whether this one is shown and validated; empty (the default) always shows /// it. Inside a composite or collection record it names a sibling in the same record. A hidden field keeps its saved value. /// See . @@ -429,7 +442,8 @@ public IReadOnlyList Validate(PluginSettingsSnapshot snap } } if (field.Kind == SettingKind.ScreenLayout) error = ScreenLayoutError(value); - if ((field.Kind == SettingKind.Text || field.Kind == SettingKind.File || field.Kind == SettingKind.Folder || field.Kind == SettingKind.Choice || field.Kind == SettingKind.Color) && value.Type != JTokenType.String) error = "Expected text."; + if ((field.Kind == SettingKind.Text || field.Kind == SettingKind.File || field.Kind == SettingKind.Folder || field.Kind == SettingKind.Choice && !field.Multiple || field.Kind == SettingKind.Color) && value.Type != JTokenType.String) error = "Expected text."; + if (field.Kind == SettingKind.Choice && field.Multiple && (!(value is JArray picked) || picked.Any(item => item.Type != JTokenType.String))) error = "Expected a list of choices."; if (field.Kind == SettingKind.Rectangle) { if (!(value is JObject rectangle)) error = "Expected a rectangle."; else foreach (string dimension in new[] { "x", "y", "width", "height" }) { @@ -510,6 +524,14 @@ public sealed class PluginSettingsSnapshot /// The value to return when the field is absent or not a boolean. public bool Boolean(string id, bool fallback = false) => Value(id)?.Type == JTokenType.Boolean ? Value(id)!.Value() : fallback; /// + /// Returns the strings of a field whose value is a JSON array, such as the choice IDs of a + /// choice; other items are skipped. Empty when the field is absent or not an array. + /// + /// The field ID. + public IReadOnlyList Strings(string id) => Value(id) is JArray items + ? items.Where(item => item.Type == JTokenType.String).Select(item => item.Value() ?? "").ToArray() + : Array.Empty(); + /// /// Returns a field's value if it is a JSON integer that fits in an , otherwise . /// Floating-point values are not converted. /// diff --git a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs index d043f843b..03b656bab 100644 --- a/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs +++ b/VisualPinball.Plugins/Configuration/PluginSettingsSession.cs @@ -240,7 +240,9 @@ public async Task ApplyAsync(CancellationToken cancellation var catalogs = new Dictionary(StringComparer.Ordinal); foreach (var binding in ChoiceBindings(_factory.Schema.Fields, effective)) { var field = binding.Field; - var selections = field.Kind == SettingKind.ScreenLayout ? (binding.Value as JArray)?.OfType().Select(screen => screen["screen"]?.ToString() ?? "").ToArray() ?? Array.Empty() : new[] { binding.Value?.ToString() ?? "" }; + var selections = field.Kind == SettingKind.ScreenLayout ? (binding.Value as JArray)?.OfType().Select(screen => screen["screen"]?.ToString() ?? "").ToArray() ?? Array.Empty() + : field.Multiple ? (binding.Value as JArray)?.Select(item => item.ToString()).ToArray() ?? Array.Empty() + : new[] { binding.Value?.ToString() ?? "" }; if (selections.Length == 0 || _factory.SettingsProvider == null) continue; if (!catalogs.TryGetValue(field.Provider, out var catalog)) { catalog = await _factory.SettingsProvider.DiscoverAsync(field.Provider, request, linked.Token).ConfigureAwait(false); From 90671f0aeb887873b2ff39e54f39c8a8969d0433 Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 17:17:58 +0200 Subject: [PATCH 22/26] packaging: render screenshots on every export with a fixed HDRI Exports only packed whatever an earlier "Generate Screenshot" had left behind, so a removed screenshot folder silently produced packages without table images, and stale ones shipped otherwise. The export runner now renders them first, dropping old and partial ones, and asks before exporting without them. Screenshots are always lit with the bundled studio HDRI at exposure 4.5; the HDRI the default pointed to had never been committed. The camera's aspect is set to the portrait render target before framing, since its gate-fitted field of view otherwise followed the Game view and shrank the table. --- .../VisualPinball.Unity.Editor/Assets.meta | 8 ++ .../Assets/studio_small_08_1k.exr | Bin 0 -> 1033507 bytes .../Assets/studio_small_08_1k.exr.meta | 117 ++++++++++++++++++ .../Packaging/PackageScreenshotGenerator.cs | 58 +++++++-- .../Packaging/PackageWriter.cs | 6 + .../Packaging/VpeExportRunner.cs | 40 ++++++ .../VPT/Table/TableInspector.cs | 92 +++++--------- 7 files changed, 252 insertions(+), 69 deletions(-) create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Assets.meta create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Assets/studio_small_08_1k.exr create mode 100644 VisualPinball.Unity/VisualPinball.Unity.Editor/Assets/studio_small_08_1k.exr.meta diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Assets.meta b/VisualPinball.Unity/VisualPinball.Unity.Editor/Assets.meta new file mode 100644 index 000000000..81ea14387 --- /dev/null +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Assets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5460014938a34cd4bb7b7305458b22d2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Assets/studio_small_08_1k.exr b/VisualPinball.Unity/VisualPinball.Unity.Editor/Assets/studio_small_08_1k.exr new file mode 100644 index 0000000000000000000000000000000000000000..be55db450d4a7fc300268850ff3c2cc7b7fa7e28 GIT binary patch literal 1033507 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
L^gb8;yHzU!F48nQAv2qGvw)C=QZkI2YO6^* z(Ya4u8yu;bX9jfL7WOPIb}i;EE{pLvT?8Fc)aHJjYrDaux%u&8PMTPE z{G0@45@L1nU<0pK7d~K=iDjdg{s>FH^yOPg2R0ALQ#0akX0Le>^PGLqe@pd-(7s`( zU#zh@Y*ud@H|lAV{x#jY92Wm>Jd!N5EJrvd_OL?KQ z#0SfRSvQvuPM^9~A&bW)$`W^Ceb7j@1xNagx=c2JGDs_@bq2d;c{+HTL|DGU6|l~!Llugc#q4(E2&`);k;RN26;tO|%9 z*N%-f%s|UU?$_k9_;qc+Uc+`5RY{tX%(MC#?U7#X%NhUfgWtu*q7i=nQJ{I?rYHJt zl~EGItn;7Cf@3poKHe2}d@_p$6dY3=+W5nBWPL0vrcRTG@z8ciM_`u^#?D;5?=9?k z`t$1TUK_l6d;)@fgS?VQ6u71i`39Q zqKuRkGS|>i7A>l*ip(ap!t@jpT8-LsUvoJn)+U?G1{-;aHARncFwU-5u0#z;*Hm!} zE+*R2R({_cm20PEk#trBF7lR3f80nP*Dj?llUfrEm`FYJSnlK}KrCE!z#a-`R>e~J zl!=uEJY2kl2(#UEQD?ma6Ib~N-Y^9{c5$pCqXH%^xpoW39E`T-i?mJ-jU^(QK35hn zt9V*m+&~>vQKvOi3@Ui-6pZV`gKA|gNn<@dHSO}75g$$jo=mdHimDJF?`P=dkGYp@ zq&6n|o}}3B_Hk!-AaM535t650X^|Ghlq5kcCS3;XFRU5$MEEem**7eE#M+ih^`oys zQVy_L%<}6KuB-SK^k{KtZwI-W+ONrrQ@7gf7)0SV@F@zvKs57x_Scs59sU6|N3~6} zay(d{R|LVbw60NSTs`E?j*X#NW-HOj=zl#)4Nb=#am$L1`12v`Y5Y}5M!OYH&9*lB zQ~O~nnm=gtN`9W`UCtQ_nv+_sppLfQ!y@MceFI~oZ zNdQ6@rf=&r9Igti;@4?Mua@fQ!}KC$uGh~ro0HLP!&!TGo>!XJtdHE!divJOPm?cR z_E|Z_8+w$x%I3Ak%eDHCx97He)(}|>CSjW&@cox|k6+7Z946)v$zwSVj!huR?Fd91 ziAF>JxQf5vvAZ{eF{;tHFgLYKop#!1G<|YcR2Sfxoa3uO);iE>REdE#nEJU|W=XL~ zGH=d_3)tLYlG9I^Ww#)cWAYKZt$12nIvhcDh>Qy=Y5wH>G9YOAka^=eyR}o89j80T z?d0~@Hr-~hr_8i;X*l)RXE6P+kr9VvpL!%SFC;Ww)FILmMfJjE*goY;PZkJ-4mz_1 zw9~$r)2YM&a2&=rsTlp$$e_|OtII+2+%PC6h&_+1vrzz}^%M{s6QX)Qq-@$r%N9uH z0kx;Qc&Z|2%BfLOv=DPwJ;aCk8+O!X*Qcj?f}>W%wDpQ*PW-{c+G zu==lu(I8pi+IRi7A>J8pi`&H3k^XtB3p^Xj)=Fm=Zj7}}nG3~16!!g-6V99=D50T^ zn0)UC);{LtynOGBkqK=+>PO}tc>mGd2N#yteq1ss{xxc1EixE~nq_`qlU0ayawx2jk;C%TzH2?t)R}Ll`&~{+ z^54=B70#i=%eoJe1)ul`gc)_!0ZmXFjcJb(jY#UOn#R`mqc@$qPS46XC&ALsA_S;o zFPVXfIu+Z;q2T7rt}M;{EPHriD-hrJ8zi2gd1DN~S(6OmGGygsp@ zFV!#e=Ivo47(ZdkQY7CSPBj701G1fZkUnM8Fg$=j0KTEHQbk%>k<&+rjRGpfz@So! zeQ@D_ZG4vDq$6RW2VeHC{o~-88&(kP*oGf$RcB2(M2e19B+h7*Q=I(fYI6`GOjc*s zID?kv3{3WHm3OxVP1|(6E3~`Nd^M~saqZNHZ%$lG2|KWP_1C+*$SIN?=Cz`0-Hg=k zB(Yoq45JL$=hXlwW_TU8-5BeprCp3DP^Kx!sF1%#07t=d0d7ZWlp z6qRM{CXhb74|Q2u=JZdiUatBheE(lA(DqlT-~3RP^tAcL1>OY*pf|5Jw3UA)mNCu^ zTAAYpq{vYC$9*f)sO*zV+U;lcXW^xAfRKeqo}B4=hzBcsvM3R{o-#k7KpNqnvMjXX z97~-xtGDe{VwC{#jchs?rwtd$Lp&zSlLZ|#dR9CZA+K+IDBvGHp~H_#TPHIO?yu5vJ_d9V*I5KgOt1K>sEoFTnEPii7PIpf}>rVWRc z>n)=dZNI|YPg18Hrqlo`jWw%Ft5WpjlwVcBD$W23mU6L^iP*TYV+ju5#l<)m<^fjM z{8VNu$L8Xkt%t6pe|ZU}QsK+=1!oYzZTSrAf}$ZhfIVI97IidtxPo8()d=shT?IMT&YOP`&Dc11s?m)jqiRubh-#n z`J(ySGwqr`_El=5qxnwBcfCNknhLH<3Cn9SYkYy zKA)igE@Y85iwsm|`D1yDA-!2YDJs|7%|0R^tuf=Yqpn_O5Nr1^Wpo&)=62z|8dIEl zOkq@U2ju%m1t6S`+BNAXPc{1jBM}ork9SLndCbrnu;u}$iK~t!9XLbT6QLQDC7d)* zj#l&3c+kGckIsD+5(->7*_3s!iAp9taoO5Ql~-ITU%RQGW3}uVfq_tEg$83=%rgdw z@|1omeRurkkKeuP6}z@PA*DQN?IU%NeEn~4-{ofePi3cOQAzb-Bo+8&RFmqIrApv! z1Jh3dDsEe=d66Qu)&PSIsy6iKaGSBZ$JSEWrte9IIb~nljJ64DX`77CR|7bnWIKKD zRO9=M0QAUnVRvtBg`P^%Ofjuboc@CEJq7V&ZTDWt3+2JS>jdxq7}|3{v#aL4;p5wD z3YX??I3n{bf*(Y>VkiNtf>K|d-p-Ti`2@~z#>k~%y*^s0*yX1n;PNkO9v)GqRPT? zq&3i69gbKSposO;_4d#iZGPL=;*jA7bQS`|m(;?8Km!KE5R=(o?Ttv$gMEti2@YeP z)J&qKR+AfhpCoHhl2jZZVTuM*>Tv)Shn{E(mG#MF~X@tI6_2Hf3vciQ2vic{$Tqh|+fN-g4~LiiB0@nkyUi z$0N@O5n4J}&?d3`tG7a8al%=^Bl4m`F`^7%lmNcwAN#`73qJUu7XT!^PD#cPItLSX z$Sh-udUgdIjBnqWCa`m@TCN#ZdvKfQzlFG{S<=FTW7xKAItjE=vi#1&!+?{r4j zE^5wYwj|nrqg4XD$E$7n6DPi#96FDd;z338q6W)YwY5cKihQ($Q|-skupXH#H(RNL z%8>c=1#NVq$K=P;=LR}y4$@1ZZ0}CCjqvgX>UNaZ#S0%^d$woO-S`h*{1$zo;Nss` z7p;Hin_Zk^%c4s^J!^kDDAI<>H+;T=Q#N?@;h0Ji zF*)igyhcdf8^~6o`AVw`CDBVb?VUb!XG0Km&>;$C0~EG`)%(-nMssj$#6uZi|r|897(M5tupDhi8`Po_396~rKN5IGD31Pz_e%0xTKM%y5A@FfZ7b2EAN!iF}?vv)E;fM+$$^#%}lIK%sO z@6Mm?H5Y-E-}?UU?|pyn;jLQr9c@6e-lxVxO$XC#%|CiuKX~^0!3okucc-htP9ee0 zZ%;g9^<}5JoVX)7-WMBI^wv-cmg*lv9-L1<(60MZr>C=*`C`s}NJ@-T%FPa*m;AQ% zM{a`S4YIOH%hQiYXEfUa(9lyLn}9SM+Xz>mssdqPQEzw4jpqa9!<8eB{C;aN#}IM` zILL>VIhH_WPS?763zQ^&%yiLQNtoA--(Pp!OVx{G7uwZPw5FM=*pwQcbq%eH{du}7 zr0Gn*1AqQz4e$F~u;FUgl&0_|@64a>4xzDstBb39RVM!8 zW3)*;9y_lMqRe}K@$X|HLD>%`mQF}7RsR-Xa4LPP`~=S!aq#u7Ct`#7y@Qzt9$ysi zFRRdKUU4jGGM(mq9UMpY8M&vi zh<#`dH}S=Vo|22d9C*9(4K0?ezQf%6oOO)-ouDM9$k@j_tw-bZ79$^q(mDoy`myQM z(~2$RzJg|o$jGwuzIXT$=uLtOZKg=tR4>5j;6_MOK82_Ogb5WwjPi@-XX`&$iB*wZ z4oO?+rX_tDgbQ%=8OD@W?a~ywHG`S$ESnIy=l{>6dUk^Axr6Gf0QP&p%PwL63aiI9 zpH3Ld+5*Y)W#EtdvC9XO>JHimjpTCm$B)*P$w4sKPG(kEG1{=oz)j9g2z%l<$w(`< z!C%spy?MRY?_f~b2P_pB=&q?c$L2I zZFJA9(}!nBZG*`a-N4|bCAAeBu4qTT%3V;~uD46A_PBlrH~L-DSEebGs9b9fZy1D+ z5Idh|zUE#TnwmgXHYLM~)JFM%5t0#DQAxeu-N>r*q-J;-Cz*9+n$Z%L1J5o?HH258 z*@E7D=NF~IGagYFJIGGoYQ7Lq=#rwt z%aFAMaq(I^fd!bvSt)27~%CCig1d>NzHcDg_@@}4Vx$+Y4WMy z0NvJFKnESS+=Jzbjk-x>fk6+hI%NtPs~Z(leo@-Iy{;(NmdBK}?Ug3D97G2qXPOi6 zg4zPv?!#A|e=Jyw*m2e+^-}f67e;sfu&>%)EOfk?TI%AOT4jFX?BB=TZP}gZ>&Ea$ zAfRYXALz{VH<}pu!JEWBO(T4qx>^S#YwLO$bZ_CTf zhp-!MQ8pttguw38F4CcSWTYecc9oz340|Cy!!lP7Zc+I=Alf=?+`j6+yxtnVp&N3dXGnCK=9_4II;=-R?z~u%k0_Y(4DKKb1wuSWn{P{<($S(b3Dr?oKfzC# zAbZ9j#TjcgM{j)D^QV%Y8%1w@CMq8Ox?6Io6+P`Rnj^c1-_D0;kD*BEM`h)yh3 z1!i(g7=NfrBx@6gOu=g(u7CnWs*f9z%{+$jGnzrulMP@732Sn&BH-jn&fK7%Jovm? z>M_uJ%Bwj0dMwNR5-czRN7|K1874+nvn4&`>x4B^9b;H3O$aD4g;_~LBq_=Y(O3Qe zi=V(qHSw41A)gOPZ9ew>>Nb&TNqMzqMxOeqOrXZ%P$^5GGKx_scD~-owFIDc*WP-r zp3K&cuB5$|oi)!^)Q8xKo&*S!q?AfN`Jl?>)$xNx^V$Do_;5f0 z|15M*HG;cDagP1qt;Yn6wNodzSsW>j4^=|NVBJ1&=&A$u79OaA+7ktpAyx3u0r?@|X5 zf+*h{y7AW?7YeSs8Tb7>@zV1^(cIO7NT8e##mH22~9bOn=ovF-T-M9~@ z_L;6)ZO~qfJtuwAwS`1guWw1S|0z(7`*3boiKYyprnOIP)baix!ku3#L++*ajZrjj zAF$2fyW!?#9Y*Y8%aDCxVq;suA##_g1Dm8avx5ubVV78PWFEKs5c^$^>(dIUTSNdt zpXe`;P4UqV=f;Wu1?sWsq|GNl;kjw*ysy2h5VhM)tCsPUyajC*sd-sq6E;RR=8uci zukp8U}5&L;)04o;xXYLB_|7gO9kTB~Co&NzQ0Jb0ScGq%Um zj&s52b@}2HzQI$ZS*nvgX|Lkw33jH!+=5JN7Nl=9n;;C04V7awmZ;>4d=urG+t6Bh zpp>n(kV`eow4ugvVFQ&)w9r^@?C(a(DBoI@ z>8_+mobRbvZMql+V7OS)rWo!eLz=StUEG)w=HBUIY+n=uLg_?drLpZm)n2<#*tDIg z8dG0$R|R?b!sXRFTxYby*)6~8;G^UU864(dY#Qu8;dFJ+x0Hw5Zaw>M&Yif?x9gXK zZ*k8~-Z(V#kck|^-}vCfhc1)@uYI5O?R|ltz>#)d+Vic4$6uU6s{Qf)9yb=9e`4*? zT_hK|_SO8M-YdKC_#TLf?>Y2oW#G-)=}K_N96KPLSiFj3I{o(I!yk7ma}IiR>|IIN zc?MVDPH?&Njf)Fwx6=2a!L?`1#lJ6%NCC%NTX?Bf?-}y?CrLBP8DurhC_C)>6mHC{ ztlAdj&Hh$rv{KA9-m0aQrAmQDQ#SC9RqL2;bc^bXjqWWzb8x~K_WI_&{+H~(sUj;6 zlowe7l7nHe&SiR1-ma9hAV(VVSskqC&yI^tA_%HejUp9VE0e}ocbqwOeEutBi(~Gv z8(XAyj$WYoOP$`tgq*Dkh_f6M@&s0IXEV@Rj7rhc`PZrGK8|jHR*a8oZ+9g&zGHI6 zxAQl?Hc&Q_W@gn{oCTx?lgu`NK5Kcjr3vq14nE8M)o=LvnkDPOBsBi8glHnVBgAF}%H^Ons#Hmk)b?0o50Ed)DN7 z<8L?$*J^jkaB&sO zXi298m6X4qCrQT|f_&o^8c@IeMK4MTz46W=&i^`69et6t=d-Jx)o}%IiXARkb4vQ~ z0=8R~8dF+HMVr2V>5hDr=?m|~*2k!yn2__cb&_?|hl6+cQ8T#~QpCB^(o#)Y##kla z7&p#MCJMXNwbJG<5xW;-d>Hx6PJz#nk!p?}l9Sf1HH`D>#o?v84I5>dsertf13(1w z5oAg-mC*F;pT~UNv&&s;A15^rCd^bps4eHae#Zx3S-<y z6+8dmeWyd7eN%AT={KgQb9vOfZSRl!%j-FoR94u!EOw#6RP&Y8QN6~3v+jy2z~Ga; zI$Ex5t)k=ICMhwlJ(JEqM;ti)6Sc@iqd9is?N^Tog})uFKH?wz{X|61V9^U7|8qtc zP0X%K2XEXhe(g`(`KsgUp_^~NeDpka?$h|Vo}VhDuTO(l+bwqP!Ru3`=>ksZTrjD& zU|Ib-M4P^QDZuFBb6Ht)Q5QS?GbWcFj>J+*3mnH>tp1l{id<55x6<66 z&mFN^Z%w2>zKD?f=H$a&v67}&AFSeEnj+SQw<3osecotcg)VhCS~Sc19h35`qf9N) zXLY2mGFwbxO?_HaQR%(s_1eHCupUvqi+^osxQSq19DopMv_|9 zPvrOX)O6RS$^#W8!(}CxT!MOjEc_3z@1JolWq;c9KYb3|%tV@SVoQF*CXp*>+=RNF zZSZz1w96M9z`dAa;T@Q0)hN;$C3WGt&Bb;VoPu3q-*p+hG$5O#hlB>*gJn+o5<{$dPzx5GphWlt1phbw04LbI=UKsJE=+YEqKa zl=t9GccaCO8K0`JOKiSSps5=`&kw`C=Sdas>^k!Cql{za2wA9Hvj6~K_Z6qKz`~OU zp9u>@F3+AiefYtRvpr)wFYiY7X7rS6ZjTc%4oM_m7W&u8+43iPBEL_%IfZWM^j z(OJauO5%ZviiT07IbO^=te#dQ+m%a|pd-}a0vEa%si3w9vA|om8x|f9B<+9wruXE&FL>UBExX9c+btEZ#}-aR`6eVmpNL`^Wuq+iQn;iK6L-$ za?c+w9r1X5Vv4!zzjiNrdi=>LDQVwNY5QJC+`Dlz!_3GHJ!lS#m_+2SScf}v-`RZbp^8mgO|3T5WbBUVsF3@tXU~I2 z6Za?Ry}up5VLWi-pHe0Pmd`IXniFjEKYE5FVfjl6vKWF>A(@ZSvzA~Mqd#?k7beZi z;4_BRNK1RAYd~~AD2r#@Fa3NxJ>{wTv!tk;E0vzG&BW-oI6f<^89GuFVxHY3teRgn zjAHE364PO|pVVu*l3=5J)MMXqzhG#`*uD$LZv5A-zc??__e2rBeB$B-4#<5v@`6J_ z>R{61Z46@@XGu=(>P!QT(UCX&Xeuvgq>*udT6cXsylYEN)IWQe?gWxvD0u+Z(? zX8|_^L8(FV6+i7U+Y)Av8>O>4_MKqL$! zdw=_2?u38T1#!Xs9cK%6?)uKjDWJG|{js_E(z{Eek5sZ2**PC9kkp(+JL*F$#VE_D zP}zj@WDHp5Nk}S{+La-P(ZoM?Z9kmf>p^~g*csXLFZVgzGa`OMzF%n% zsj>6GP;W3$(I?sf22o563M87E^;CVz>s2Dolg8RhDfPXPUGU$oU$6`A?JVC*J+a^U zr+7d2f|WB3ktX8?9=xf zDa%d-$JF(n{&mL+ZvLEmiSd|gPdBC?cXGn?d%q{{-pu6OohWD^G%OUVCn?`}-%5?T zSb5@UVeuzkB|YDi%#}=r%sd=*yF8hDX|&4ehrZ_Clari51`dABk!#}ESAZXYr&rPu zB$=s_5c8=9F>^9}<#yIl=WoA_Kjq%n6n{D0cd{+t(w}Py*NnC!P^KT(~z=Zmu!W}T9DeYWGjNqhe@ zQyfSV$!t{G6$Q|tOw;X-yba7~54_t|TN%>&9#Ac;4`16{f{SWxJ8a3vx8E((u8X=G z7G&$lY=hYz2XU6WT2!!=w*(ml#RAwK@U0jb@*iqDU6Rv2g#roaDU5Ad3YI`Bl1&Y3 z{WtFXs?nl+RP5UdvF}6I6od@_hsa_72M;^Oqi-My3V(aYM$6Coe6z#FKJ#U~ALrn_Ga$J-_RNF8lPc``9XOK5V<2bib`9KuI zo3UZ1xbZDFD&&S8+E8@=KVYp8^7`D8%wrS3#)@Q-6@(@+PN@0oJwvz*->gp&mR5y= zNk1Sy3?KHlRP4Qd&A!e?S`dsSFRQ&K)g0_r57ypi(UWM6DR zkKHNk$OZPrQ#*Hh?qaz;!w2mQcFsBcRRdcd;P0f9P_>L zY#E*vwhG^>`5yyGQ+?S2-&5Y0sX03P_e&ce+^~AiNhg2iB_&;rBD;X&=IYUbOO!K( zqyzrkOD_BV6}gKW=3MMGq1yY0$vdRt*FQz>@euFS?eoAt4{`q@V`rgj_D>i8&peF7 z`93XIWraQ9q?$;v!h0Fems6@ulh%#X_(_gDg5zvz!r39+rxhgz=Lc-k*w(5hsdV zPCt51IpXp0g<}(u7h*2Boc=fex}cE`gMnbxa<|L2fEme(Aq72;Bux+?(^bBw7^$!T zsq?L)!!;5-|La|1RM52=U=+#+m_`m!s2izm4Ba%IH?qxHEp_b}ZA3kbi6yHG@=?Sn3n{H-jyBq z#0?q`G4>hu4P59d_`&&?!~Sg{$AGQ!)3=`#4tU?3|EiH)4%&Hw5SgUJKPmPU%#?r? zPi%~19d{kD~LnD3pb8FE_ZNn=7tuziK9Q8TL{%a7!vLHKq3!_{PP3_NQ zoX9YpWZxc=p3PuDLWZ*l(1b2=V1KyMS2x)A#6fH?n=Khb&S2U1+l%etrR~;WPxfEO zO4n=c1V?jx3en~%)Kn<;zH}Qv!wyZ}5%t9lYfXucl&}Y29h-58nNn-UKwq2FPapNX zEL0!wncR(cI~{bxxH}9t&^zG%aY2Ujok-`8h&{sv_ndw@c7vxr_*(kDSTiHBY5?`4 zneZpi_lbq22rw@&daY(Nbs&BQLcs=id0_G69j~|7o$s9NH4`2_f83K;TC&awB^EObS%$Vqnwl=&&nE+U8xMyyW8^n}_hMS3%KBU+m3qE(d zaJJ{tMY{Wi)`|4O=o?kLzd1hq_(D%{wX0V_V8>dC4t^kKVY2S58PrikND4BGSdv%)`D-<7~}MDfm4GnK$1St*iS zz>bI%Km!y=-+)(YpuawQr7RG!rd6zLVC=HAS{YsfwK3{8+CoEG-6LhAE76O>WJ@yA zIfIa!cys@esxl<7D*RDq;0Z2dc>>;Tf-7v}DTFM+A1!9bKbsi>f=q__3(4Nw1%CO*0Fq$qZG>uch(%EChj zo_`ns@-{Mq&|>E&YWEj0{L!!@pB7IsDDR?qe|-MyBi*p9#zC6Vq37}TB}iTbb8bU znVoAq)pWH&cIvgQ-{zWn5&VXu(mg*B;}1WM>h;>$g@a3I(Xql0T&v}PAN`l;re$!f z%50sk&_IcXCHHz{`^vnhJrF#|2t@Uk!IBvgmGp7h6ziM`ItYV-9e`7WD*&zVRO+bt z-Z3MGeJ00Z0OcZ`L!|Rf=X~SP<~$mt;f>gGo)KVbWW>%FR5S_{I3A7`Jx;z3{2i+; zG@WCVrzbuh(wV=GO6mrlPTWv=d3$?B(OYR4dGDEMR`wcia{dor73|(2et7qB&rXk@ zc721p@Nv(S+vRk^T0A2VT;Upx*tkl^HAZ#;f2g+7z%jLe)ku;Pe~K{>9c7x9FDNLt z`+wxshs+{vd{hAc((U0{%erAlwleW^G%(@?X!)ccXfuIaLYn> zyIs)!c*HKwU#6N}!Lht0Shrh7y4z&;5pb{Xw?(Yx2aSqRfeN1Ln`%d0#!gKr!$Ajr?$a=YQ{R-TToo zJaHJ}B_<%^iP{^ocE|-R`}V6=YyV4`or zIFVZry5m-%aBq&snad}B_V_KuZ5qi4=)ZL%xo2ue&cmpJuXbK`Zy0*?>X665P#xN}^8yT(oDW;n)a=2jaRwPTZ!$A5Pf<>s0!d1Cdk42$-OL2{z5J z&czl~088;hni+JaJP>(P^-fRuuAkT+*bpcWfIyf%9}e|}@^%?~qm#alk)FRj<7iTq zAyJr5XD}fXKwBG40_5v=$*7|yJ{qDha1?GdC+p44F^xPNbwNIGMKuB;+Uvf}6W&Rb*4R5FnzWwFNyj%`&g+6Mk{M;i^o zc_|$LIAbrUTjd6blk8kOy6f+*UvLWXPfPkte z<;&bJC)kM=NF^wxG16EIF{Fm~MPZ9#v?Rex6$B!^oz)zT9;Za(I71puDHm6+cAN`L z)BGjuDOl(SF3rQD4#yq*6jnXo{!J#RD3diC%yJs!)>9@XkmEKy@IL*gH9>)rAPZQO-=+L+cP&}@=tFoE4@r-{~OoRM`| z%Vb6>%iB?HSJyb~ZU)X^yx6ZH5LpFlGY2%NRmlI}oo-y{k}n~j2cTY#>=C5a5%D0K zp0v|CvHy!pJ;V#jBDVjt(@ZY{0jbIU>07TeJ(Yz~J8F0SbJs~HLQ{P|X195ve&nzEY@#IEUH*#h%~)kX=oV*4z{ zQxXU{JPN{5P9ts3=BGttl25n^cpJ<)`OJFy?E02{*%urSok5Z)EYW5$0SG~`RkzS? zgI9xT9q!d;Ah-fzRZrQNc8D5}si*V|a5ZrBG&F3AIi`MqaYiuCs2wEUbnZq5e&sto z_z$IGabTvH#=?%tv+Pu&IcC1m3XBy69m(BH4@YJll67pPOVYVa1C9=Oq^V=u-Cg$W z&8#|s9f$I^RxRQ3abhTOv0}YGP7E&OsVMX$NgTd9p9EJRY}~ZI8gMzFzw0`nCIB`$ zf4DMaTNRK%@)+!08C$ebE#WrHIIOO*`~{33FMgk;-SJQ7C|AmnxsyK z_5ZkM?&glE9W^_I4`+ALtM~K47=OyaU$%@W9ad@y7#L}# zNN&;*`3ds!B_e-c&MGC}guGpB&a1 zV;7Xsx0ST)n>g=b$5VyCZUJQQ@*r?_mbb&`7Nx z(@W}*#$2n$IPF@vrqk6m2kWAewK;7jw~wNvI~%#ncDt7a9{~ zd{eh%D{jDiuJE_Hj6RpYxc43H{k88%DOGlGg~8^se=Hbs{%i+z&s*PTm!bw@if4*S zzws;j_76obn9eT8CRn{soFuLDcV8H?Fu_vdt$Np&0|=yPd`S%9P(>laFOm3P+R?CC2y*L+KRaZ z?|r#n8c-{()g=hK-mG0|G~21UdS94R0ozI!$RBiCmSFtOYFiF(KqbhQs2!GmHdAc- zV@v7?gdDGU2`YiG>>2WP(z<-Hiqd8Mp2XP9s>Kq^mDhYvF{N@+GfVRMEkcEq{Z6Pp z)8zSMWTP15U4T}#YWt_lHE`j0F9~Pm=fi2apc?`)3`1?uo(;Y6TvNxkB5#`pKU-oI z-5wXCgqStrKrnH*oMc1AOYh%fK*j(MA~F2YiJY^Ov2eG?@-iT(X9w_4^O6?-4;Jpx ziMXY@DJCNz`#+65&p&>jk#}NFp4xZh=*>GJBN#A^Mv-m#qIO9$uWGoBna@}W1&?=B z?D~K9{tM?p%6ai?(J98>XO0gX`zPvm*mi!r>&#B+F`qDuo)=8$fY1xPK9bH%m#UTp zRWt(=v9rG$KuFO<1p! zqpgj;u-FQ-+PoMR%ZS=~BE{pUra1EJ|tQDvpL}a8wD1#)Y*U`3@ z^OlLn8d4X**T5O;2Jb?1vW3VnjOX){&46D8i!PQQI_7v&W!b2$r$;h|8w@@U9!7s! zmFwQq>z`H}YZz)U{U2}^4T5D)3fJ2UcdzXB}97N$UZvp#|x`F4ErBG(in#od;r^kgO#+l%$LjpWCm?E zCy&$ZA;5Ej<=!H^@-$twGWK%d^1fxtc<*>#_yi!TrP-qVI8(NGnL~`#eX1l=Q7<_u zQGR0+g=F?g2k`vqjTbr}t}VvKDtewa(gk0dyzM|^-fi6oJbv4t2q?xH?#f4W*Ta=I zdYn*(G2v;W=Vgik^JX*JfoC+^u(<&|0|l8oq7cY~!Ee7kYlj14nY;ioJsaQ%BbB)* zwp7|a1L={O*{-|=050ex>#HOQM!(XvUfMWtow^-~32cJ3@kBWuuxxW49j-avbNlCl z{R;|_!}a?v-}v(KiMK1B^AowH56@o6FW9@&v9n%&!>3W*nRPakCy*Ox!)gdV@E5Er zqZPFd2lDwwi+sSeZtoLpMdYFm_NY2%nLCQu;iHbUI~Ari?I`2vYqC&q&xG6rPB(r&x~}61p`-IAVE%x zuk#!*@~^F$>%`k8fdc|Gf$0F>tn`(dyLqn|fhCu%>yXxHk#V+hvF(k1Rh%rp;f8cv z?2=5(9t-ThHPK!aJNFam(f9tp$A+u%lOJBCu{X^bX-I}CR#M&VHuw7=E8^-Tchc9FVlVo90GQT_Vh?(+dZ+2Y%@8uXU7l&kM>+4n@+)cx7?nM(kNXlr0wZ$s zx=tjul4=#qdV-C*C?iT4Xe&rfhK;rB;oH{fM-HxmKaYvsrH6)!lRjj54)%ZlD$(W} z@QCD|huV34YS zmo{)U;m?6`3}P~U);_YX8tQtAJm?Owg;)B9WduZ<$>F+WI&*EADPL^NOOrLjn~a-; z(MBPoZ_!hJp01Uyn(B;o)O&+Qwq~(gZL?6R`jCk)dqR4y4fe+Ear(Mo^yw?-QMnRQp%0+%f)EplyTOU!pMM~Sr|4d<3QCNFqt`M1b|mhwv=kR*F>0jNtG!c0r~)EGXtIO zs`=1yHb}UxvJA~w?**3CYKh@M2)U)KS0a#N(l=1mBSGoP^yGF{EV zk;QGyM{8#K`eT7cH13v6@}8R8o-$%PR1}w^Hy~ns;gCP^`{yBNPI^RL#!ozFAOGI} zK=qLuZM!}>)GG|-UF{+{_)GBUwL4a~mw=K6V=8x8OC@nHXF+3Q74MwnoUA(6M){^? zQl9fS)h)7SEDmfSlKW-QC7ZRHZjP*d^pbV(RoLYIujk{VS+(VepR~FE<}byQHQ*F9 zofIL_X#k??<~dlTCw4}LIAt~17c=pV=k;TaK8^@@Q9i9onX%`G17;LB4BeYcE4rwJ zNZgppl$SwS76G9TIx9?a8k9uKX?aV^CI|P+I%(&L?g1fUr&%@}afSu;*0gQ}ZjPWc zE^4!wjbyGKa^yNagb^`76`F09jg{r_uFi4V=7M8&d~;)~jk+O!cOAtx0KhZ?EfkL0 z3QVY4Y}dX6(v5v%OM(Hr^$IqsV2R%eRcxP1^I0pL0nj`iyH?tzv#uV@2 ztbu6jx)RViiREDSX>y3?y|KM^T=s)j&EH_|5`m`~*yZEmW9@dKHa(O8)h=D&4zAT^ zxEjebkAmH&M!Hv*Y~ha8<qEJ zT!!8Ry(0KP9m?IX(M1iwX?+SXl0)%LUX|JM8%uppq{3P$f8bp_FhE-50i%$Z7ic%} z&o+{dCLAudPF*_kZNhQiCYz{MX#+ChDP9uI>L4WoeLz`*;TV~c&#vwoa3bv3+W9|@ z?X^Cy?l?YRA+o=5``59Fz#Fu_xx1deVQ0<`j8?HryW!5SA_A#Ey_yE;Q&qapn})j@ zqrttjT(+`_N=GZR|LuibIiQK`t1Y&^p^+Xr{L^Y%Z#cgBNl)s>St?v&C z`HWEMqAdFH{r`%qSD0R|w_b%2oi$;lC6_a@4JFz70Ti;ZP+?g~6UQyeSct-tk%mzh zR75%qU?wJ8Wbb{*uaF^*+S zZO>bhZ_LPpV_&_grvY6F!o(d9B4%VZ{Ptc2VoS9 zwxkV)RX06iD}SWRAvQsKEETQYmckcgLB6>xUTW8(EtTPb@2U6VorO(xwxQtj-P3L^ zMqRDpGmPWoG_(4o6otW^;uQ$dq^w9 zS%g&_X!|wIDnaLp|K+2b`ZmgPXp|ZfSj)?=87boF;P$yiIyeXFV zW5ALZb6}|=zS0dZ(*AaUw3cI=IeWXpY{qQQFDr_xL;Q(QxLEnHQu7hI>k=pMp^I%# z_K}IhH=N(@_{{H7Znn=Wo6}teq{MyYCOa&#GbNL>aWQwKs{`Bnqd2i$Zgv2swbPP0 z(k1S{zqPGJG2Sao%pGc9*WTRB^cDkxDAbT$+pZ9YZ`IUuKb6+J%w8K&w)qCsDSTTS zmK?l1(kVy2vDQJZH|O>Tll`D@sB2~hSVXg{sDfDqDhd(J8&jcA8^#xv+q!WcX1U$y zNnJ#2{LLWtyW|B$NKSb|(~R!=h&0ZU30*)9>G=eVj#2L|K*NL*13DVixLYj7S;y-A z^)5&a0wto2oz+A8$gg|P;Da{sSsJ-BnirGLtA_PIwbWYc>90V+v=8c;siT1M&^Fu$O(4v zCCWvgy&6CN|F!#@qtK4HlRtwrn*{1}qwR`OZLYy9vaVt9iap-~+vZNo zt$J~6K6P_&TyJVw(Ha0Q)Uepy^#Ss=iGN*ur5!*Qz?{!qW~B&_rq zr!YXZ$caXKoLLHqe?L`5s3o1yF;f|?1nd?ZWIQ_~m4|cwIXgEjDvIpdYq|Bkwtr$N z(?FLv*l)WJYEuXGA+1u8%0h1~2f;BlrqU=j)qGjy>)zYCTkVm3?m||1N#ERxf z{N>~%WloOI+%G!@PJVV7S~o5x3)g^^V2;M80+WBkZ?fqOs}>Wr>&7sNP0YbV-;ii> z{yFCaao=!-{KWj3UWxTqn$*6Tn)Ep(?TvzJ;N_*empwMI-z7aMRp0rU=ixYH{6L*_ zO_GTX$th(Esx&JJ|3tO8#_V@krzhY3#AO5gB@m#ij#FzxTaMcGt&Umbj^^86RS(#u z5~2_93`FMaoDt2-{+GU_HlbPysSrC<9e|U74C{P2*%g~<4Z(yA8mkQzQ&D;P7|4d3 zOR&OR7Itl6@2=_4W}zHLo$zTH&%PWElcQs{QLKN*;>K8~Wo`Mu3#5(n$Kxy{Xj9{l zXwyTU>R>2-{|^AK+kB5w(?{g@j5cEncvV12ZmH+;p3j}0@bovjU&xj2{xj~v|F@?) zcki8)L#W@5|GDG-L)(rY2{tfi4;WAKK2$77xmVidxoAt3CEa+|#8kjMJ*%FA9@LPT zS$S?k9a|G=YL!#BHw__D)2Z0`=`!%RIueVsW6B>heGU!7I>VPeio74Y6g&b!@d%@w}ktg62n1u=Kz+v;2Cn&&8Tpcqg+bWF=nx_{ z#$shSat5hBG9f4|_BaiBl~Xs_dtVTHKiNOwJ5zjGv}1j@@JRo&+U!r@0vdQ9sNR1X zcOB3Er`}IJ(_T+CFo?`rh37_$m@ZC)Xdtw?lMQZ6mY=xaA?;UHD3dVWw5(cTj$V=* z?fITm%BV^&Y#@r`pwWUe+JL@~4CsAqA(Q5{@i92Zh>{rk`XnuyvG@7>%9UcziLNlH zcKhR3(0C%!=?%Qer_)uuDML7t3pyOqVYRty^6sVb<6G3*+Jy>fOQkPdI#A4%y{(0V zUsj7`f=qABK#i_Dvc{+d?pWc zGa$`lmf&z%SWdaHu1iphd@X2~r-#=AOJ%4Sz^62UR7^98G zNo-!gFzm#CE!76FprFI+U{(K1LPs-mkOi-?bS^_;AJ+>NCfIGn4I!XO%-Z8jDT$3K zkgtSS&`6~Snu=Jh4FPC`74w~KfEwJuEIBQiUMFUOR=pW0C&DP%Xe9^k+V=WXlhG2Z^?pLYugg+<-5C> zPg^vjymv6WBI}^fjkRFi+4%TZbgp$ zc1dO%=$rS*h5cO_7P}zhFY7r$EL+G77J?J%lCFC}unUpj%M@sb6#V4KD%$+{c?^>;QOv!{SQHg4V$>xXov4uo!J{}paBdYYyG)|5Ha+~SxF1d7CP zaUK0sUY{AV#3EgER6ObYUFm98tt#i;{KwjRL4iR-wjQyLwToQ8i|*X&)as<%z4$s3 z-SvIl!H4|H-BG&^dt|%4c4EKvstYek5H^sSw7J(Q-`KpxiIt-6E@HcMRw_;bK7c+^ zOSUgF+c7#G&f>ea|8^~xkKiL#Jw>I3x6DZdkz|I>NpJ(8Nt2@WXS*y7$42b{K zK*zee15N}OnJhF>w;BWXTAYE8u9=!ejb_`Joj%zjZQkB5-)5YMyP`F6`Iqr`}t_%w0UxtL59e?Yui}M-_++>MqoWM4k^@)DRd1aR11`0(fK2@SiTtW znA!)nP;?b-W?9>H!YMRtRyVjz9)aSxFi3HI0Du;{9}2>d-=Y@Hwt=}5AA4GPF#E`} zx?R8TGw=8>A>tuuxZ%=LH1~1O|K9WVFRkh~B?+qrv7c@rVb(EuWu{$|E~-^ew`(mq z%b^42@u{Kq%%m8ZFz2zLr_twjs#O_Mco=aeoq^zsh1rQ9rsL*lM%QWsmGT~;+0>JsIP7= zR5aXrNm%&{;_eZ6RL0wJu=M|iDI(+p3hcZUJl?H|rp^e6h%?rv4 zW|-}pkxg zBohyo+C6=FfR7K(r;gMrXXVCoU_$rQ6EZ7#FKMiZC?e(EMZ0Xxlt)QBr&<;p^2rdx zKF0_W%EB-{4+pJOPmn2#1=tFkr1t~lY*$?hw#)#lB_uf=ZDfE=fnz}`z(UDuLk+{o zy}Rm8(u@^$m}t{Wv=g>|dM+U41i-P&HR3Ajid)JnG@HS(8+0Bl#w7OGMd%&ruLpA# z`{?Pq_LZesWzvDUyUs6ymgE6O&T#p3%?&;PO!H? zx6Ep(=V8qH#9Bq@sC3&lR@R6%CkX9i+CrV9Zm0s|0Cc~M!gXgb%&${@51GlaOATYP zx=`v}O*_0Dj3T5~dt1s|4R+I$Q?9F9%P_*7B^wlKiFw+4!*Y7;sCr`0yPmz(R~}C7 zJi6;l#GNwygeqjus{hG}s0**!^N7u%Mj&(=3 zP>Xhw)`Zir?Gl29reB6yrC?zrZK%rfPN)i8fsR^$g+tj+2!QZNDV(uNc1-ra-2|n` zhsMYQCB47APG*R0j%Ou`<9Klj@V)si+)kKE~$y&b3dDI zvIhA^w9z{kU`E(($f{MhXw2(c%e!mVDF&G{4ls7(x;%H&042U%BAdy^veEJ~EM}W; z9<-;5MfKhSCNjkso zXsQ?1GnN@9rgZ;Aoy$Z;;V()1=}5CU-;L_k-3+_M9vN7$#)Oa>&!!*{*z| zq)rS?U?hn>yj}Ww9o&fkXSK)m#aV78+jgb@m zl=sjM7(zE}hN13y*z3QM@p+h-e*SKGMHS&q*`yI-z#*Nm>TOZ3>5bOe$zP>hDDfx5o~Y&c)c^~y8^4F;)=R>fB*`9w_4nNtXEWoFd{m@&3(pM1!sKYN$$xa?=}VugR5?zF|4pIV;H7!L zdw{9!lP>am5TOoulApV96mg)T0TfJdY-~IHKhsnB>;~H- z$3VFr3%uW?Zf{sw(e}K7$BhQZ5-K#%6T|8!ngfgkih3Sag4y~IB}Bn_D_0Fu;G7%* z+QlHZaJK@Oohl4% z2$iYT{uR_02R6r4AmfWp`cfUXrNk>Jc38fX=2Z01rHvF?3WqBvg!1c}@cK|3rqI*}ejXfN`)Dj!JED$gm-E=zRhgZF*$`eR9uqkxeY z8|xO=3ZHy>muH1+W#AwYUeBody#Yu~ZD3PXY!Rjc_^hZAVS_)}`)*u9D#HZ;%S(>o z*>w#VTwQG?3H|9p(P#GWY&LA~9QkCrsM7q;?Z?H(Jf_jlPCOG@yy;&}P|d~60ie~$ zb=`@Ya%dVY@19xR(sqjPH}gIyr|c$?J;WH%%e%h`$tC(f!7>D^ZEaGYI&X^#4Te#m z-?j9ywnj@_zylTvApnF0A6VDM4s1b6k8&!*CTL zkTx`EjHzY(Ju%^n2F*68Nw@m|K!0p9HOAR=(uaY|z+PBbjOMS9? zZ?ShD(8?P}jNAln1k79vW7~a^;iV z!rr~&(C(%ww34yVH2Co{Xa55yNI7lPbV@sWF~Qjs@jyansWCpoR?=+wZJ^g(!Qp{8 zv(whJpxNgtx}Z+{K(8Fqf@Clc(W6zHCXd|(u_j-QInC76a0Kva(tD(If$2E=jc;gg z!l*Gmy+Nl`Vq<5tL0|3lIU93S6oiKRaFA0)c_W$_+y()`d~$YemCFQRN;XcN|E19Lz_=SCvej8H{VuD{R7{#koll%#h>Ie=DKySoRCU3dXYkwkG8m!sD}s_{W5c(6gE zo~waJ;U*+*;_p9%G2-&;xyxl@Os<+qalpH)PP=hVlFBU{kZtvrRT$g zOPQ`#%g;=1pQFG6c-cW3<Lfu-tAm^gBodzV$6-PCA(Y( zfH)!SGvC{x7*`|%k`ar{i!(lCB=HUWE#WZLKb z!$l5&FI}{qCkyg>qmAl>%X}OI*@d}G>!$a6)Dallo<1Is`PQcdnO-|Vww;~%DAG!X zes}JC%EM&urP~3=siy#IUDi8iB9HUi`8^N10yZCr{qkBr73&D91ZDjF-kLKTG4x*) zBi(Qg<|=(Mqm!|q;;SU&L!wFN&juntgyNu$RG$czieb%hV0*9^79bkY!S9r3kjDrb z7Qh2DQYfN171wPZNZnPW<2Nn+JxiZ`?{VlAnu`r0B*RgmDqg2*7h{{Ue1o+2Gsa^K&6m=`Bt8*?zu|%Li*)uZwhQiSq&3&O9#0JZWonU-?4p)iY@x%8~#3_ zHK9OfVQp4q{vW}Yb=YPflg~N=cc~3F;fZ*%VOYsqH}aHh*?NN#Z-SA=y6EmON7lQ+ z83TxA{_=H7g@GpbHw9)9!E4~kaIG3#txSMjrP{aYCUv2#uMyA=kayN_`pV__%W6^_ zDS?C?x->T+ZAm^wh>G%|WIr|lT1C6}@si5cK9r#IVm= zSpo{urJdi}=>sPoaV|NV>rn?Mf;M>I8p;0J%?uQ8c`gYWa&YJLjUVZa$CnQQ(C=%% zcx{C2SGgZRydXo4&?bF|9KEW&yi5@9Py0dhzR&Kz^aP=LRzy3hOdA~ zJ*5(ii8zCXq9+esE^MI&4PbmixBBoAw zDe6NSEJpUptPokIlvNRS9`Ln2ouc+ibVwS)5_CMGhTmKj5o*o}IwZtXv$rnc+ymxk zXFAG+(%H1dM`}hRzM>hMG^0(r-;`d(R06hYM-NNoCwy*5CfJ6CBDH()b=QZ^H}h~j zHJ4|~gU+o~I9^XQ1gcv>mM||kodB5?CS`9bP}>!u_~1t=llukK#Fo#kX!5{_+JNI_ zQYoOLS5qK;sn|*4$31~#v64u|v;n}7Un^sSo+n+@0jXW(zSwDBjM`r5iz45OjA=_I z#M!mP6-k|I!u;)&R!_rieLd`2KD~JCG_|&`A=7Ph4frYdOiC_%?I>6RvRhvO`{4W2O+)Nh{55G9~^mHBz3GtRUdywIsdH6R?-NYY)bDFIoueln5h&baj!RAr~9@!v|%c6yy>7?B0AW(sPx1f*=$bc+i(o3prS19&kv?d(dl#Gq{uU+hzuoUeF02V6c8zJE__mG(H~_PudkC()h^d;t7*mW zuJA3cIMVSKKC`dx(j<(+!gorOSfy!8>U_k~=i4j6Yg5|Li*&hyg8cR)rN`;uuJ{a{ zZ$i{aSvUXO$%`$2e1m<=hoahZs~3aY z5XzN-M7SiTOLA8QdgeGxT4BB=lWFy#B80y2H))O0L7w)~1*Y1)9fAlh2g0YBWFNUV zDYVk^4?@MGINI`a)suSK@|i!@E2Pzilf$D56_@M-PSR6hNv$Z(!>FMzARt+vLgM(8 zE<7&X8eADlz7GoS1USCyu(3s-F_(oO zXm>UH!D8shjjy?cIJlu{x41s*qAm~mUdimn&zBaU*p`n)7{&zc-KCAPSAqp;@^BK| zrpc@-@-51+>Vz`jO$IN!vzfFm$!5zYDx^;kCbcc;O?L3txYLF;X*Np|duQptA6_oS z$F$t5{)aa{#qrjF2Z`#AB9C{Aa=io29(1WFe!Bj+bXw8!`+Fp;%j1bOGdDZ@ZMOUV ziqaPXE+gQ-0SV`X6ges0(=IkrNkDu3$J#@sL)pVPcj9p+%HufMk0W3X4=L@`l!uJx zfFd4()rKGHEhHd6sx6adHL47u^T5bA>h7sF-p@Zw*7?8`wIL%>ZlZz_4wOf5xP#ee z{OzqVkAyvw+s1o%fHQs!xuK!JV(MZIr@U%-c#JdO?9sN~r%#yR=jr#x^%G6dm<9Sp z#lsj}ogC5u+=$KWO|5|%CWdsTh7Dxn5k)F_Ifm7sVK6?Lm*i;ceR6$my&Z@at{DLQ zk_BFfJ~;i{`dV=Rxb3r7mXWRJnlgYIL>SB4Qt?Kb02u2*e}j2$0)bH%VL2~1HvwU-zYmHIBq5L zxmfowF8GP$%bTPn(Uq%MAplpyjL~Ek|8y{s_7Zd!)QFunWk?1S#&k({*sXeW$Zf?K zbdBHx+b;X9G6Z+dk2Jw&>)BghG`fYwUIzJ;$wX-aF(dfSlk|y_h34UP!|(^?sCX}R zQ4yzC)x;Ltexo0Jmqp)t`zml;VDLbV@>{RFviLn&{Py0&24EBubjrY;uhoF;oPC`V z?jz%+<#5>*68#}P9q}Bk2jozoDiH<;)Y+5C-W~i_m*xW#_yZbS(t*D_JlnZ*;D>`V zON;}dj|;vkpgbHvTf=eJ7to;!s~#)13q(~0h&E-%s{fJAb!8^q8yX%|YiihY!z;^Q z8`;g|Y@{tIwk__&sN{jrbrjC7!%2uIYj$I&olo0id{xem&!nw3r1e!uI{EZ1z%Y>0 zW@@F#ZVEN;@(PvfDBHP;BK^G!fU&omX}x;eoD1~7VhP7>=8NyJ4`XKVDV`vn;f4w* z8{L~(WVJq&?<=US;KQ{g`m#6(0_%;TW$>`f53Q{v#uJip36ZO!sO%@Qaw?Hq*%k{X&@eSwHeSJ+CAfVcm2+;aSa54d^Vsp*Q)lr zpz+$x)k?CedgBKkF2+1xWrL$Q%Rv+cWZ}Ce?0f}`;wP&TRJfzSb7+h4^r)*?tYR&|)={4*-?v(T99z5- z>+44Qt{ye|FwPmstdA(JPL*0Ad%Ky{G>N>qz1W~hF?t9S5hGKVyVffNhBLb4aB#!I(vfV{D<| zXyH#bPh~)cLohbKqgu?9oR)0BwkxIWlEG;ftjC>57+HX zjQG`ETyV~7ajt2M)2uojjCpvG7KO5W@eQ`f;EfMI_=RE&5SB}!GhirZgBm>1bkeXgKy0vL)wtK^_LcuT!sDAkho+{iwuk|^YKsZ3RVk@{T zxYINTDd8m+hJ>Lnd@eAoS2XTiiN){}kyf^BRgskG3-7yey=Nuby|ytr4^EC1lGyAZ z1vQLMq4NC4Jez?*7_Q`{%hieXCK#nPvl{b|b{PAZh_!Q!c?B;GTL~4bX1O2C3;cp; zYSk>ZOs?X>sGKlHoEQ6)*Q1~_aoni^li^-zvHxqg+&esQ_K-Iz!XnAJi1WiUSZTEJ6r2};6c z@jbOCUOp;tXU0(Xd+9NtnodIi#k$j#xTQ=)YN5n56_i-d*rM`Q5tu1+Rl=ozjIkt{ zn47JJLsf?aC~cQ3T)$~AhvR53E^(lXlP9$57Tr0DCZ6yaGaDhD%<)y4-KAfjR?rS< zjTRoW3y;)XyYza>9C?VndsG7W5GEw^u`|CmMzbWFpU&!~(#KKNSaN5|C_88o!0=v1y6yl=d0X401->P7ha22Kj%vr?gtbyf7GANFn}#iMPo z3lG5snzjp$KNHPbO5vut-|C*wE(On?e_F!CH+i870uS}=33F4 z!H~DF9kT5v-5B~j$36OAL*WOA0T8R+WwjgRK7seRTwqk{by-uyOfi2ixqIVHn%(^h=-$;m@g&qK;odh1*qV80gPxsP*AvhA z`x5fDHzjJ_spE@>``4M64mZiK0Uqor^3 zl$2TT+~LuZ*6VHOadat6?O+=c%vq)Hu*>qXt+^f%NsDa zju?GCc)sKeEjYipp$e^$Fa~ktH}E1;3q!jVpwW$NgG@XN;ZtEv_e&5WWE`pBGrIZA zG0sDZ2X0$DQf2ing9gzWW1@cO5_DIPb0Q_M`7irK`vKsMA=YC#sTHzI zBr?78<_YjKwrpL*gN8K*j(!>=cO_34g2M#QT^B^4JdBCpk{r?o0k&7@DZ1>U^ihYa z+KoQh96`;sUoN>>)6jGu_$2ff=30Gx)cM##!KB5N9EY?0T?H^3v8Ekb87VIT%JuRm zvE`yv+J_-PkTW(mziHXJbU|WeW{HNPGy9B3EMPL3o|! zON8m060&6=JQ|TrG^RXh$livqKW!f+;;~&%*6N|l2M%vhHdw&j9Dc;hzAB+di;}Zx zuN0#)Q$Wt`v{wh0xq&xLN`~{s(=&Wd$wzlr1~%AM4$kzj3n=rS%(Ye`s~1rytJ9Ix zaIf`SQRHFOLM5R8!@zo_>3|V&rW5~3Jue-ptMVYfH4t$yNE4!Ly;}tC7LlGs>BN)8 zFU}PGd&@bh?LP18flZmC^T%`j`9&V~OJ5c&;4wD?EYeT(CzaknHCha3dN|%(ihDH3 zpnn1UUTewjouJB!v5ZYRev#(n+~f7A-{@2(uzM$UyVQ8_ZFU62esE}{ zhAqD}E(q#PT{QTV^!`l9$X=>_)E!s8Ma-!R0M z-5wQS4EXY$MY$>>WQ`Ac4weBB`r8KZsYO!}K2zn+g+)1Zxd+&&7Qbh$mcPqfTSO>2 zwbGjYW@XHzeFIDKhOv+-6_=Nrr3IXA$dpgk2OPp9?gq~{X@sfp_9gp{G=o!C$lB#= zkSBs&xb{r37Ppdh5yHL&X{|m$KMslCmHex+iC(U54Uiu_o2nXkA~IFSCn3kwA?cJx zt;E%YXO5wm6Ozu-nVnbzDdljI(X&^GUqiZl(WDqsg7xndhoYW3MVy1dGV=I9ifi#x z_R$4NOM+|%J8L{h3394(lJvopJ)Z@(NC<80d!Mor>zP9gZa_|Yodj}YQ~1B5C0Ss= z6YX}nH;44IrVC1+l@{MQ;q~=W@OiL*IiLBm@Kduk%iw8rmJ8f*5up}{D%W8p zZV$5s7=}v|7MhglU8vzND)HYW1fuw3Hj;Df4vAX(?ilSH_*|{Rg2?%=iZ1FMq1Hk18wtF@yi>F0AYQ7=%*V9 z#{So$U#nxwicSOR^)p-Br>^u=^9R$uGbFoX>6Q|L`45iMaTh;6cCF{gP1uyNa~O!*x-?x2S?+fq;W-;*bSVE|8%5hzpNm;7bxJm>2d^_}hI*;-;DfoQ4pc z3X<{IPN6XZ;^F~FI}|AiQ_z(KT3@J9Ngz*6v9_hg@m>Str)^2>69EQOAMYa+R*`6!%wcUIU45L|PMr8;t?5yRHKoTVY zTj?YTq;K99b(TM_@(3o(20yR?&IdEyNAE@`y^N39h>`zUx`Kgbbdp2xNPucWgl z6>ojHtc|}lsoR!6=dCE+AlvD#Po0O%dUxe`v-Zn-+SyFIw|*|xXTCc@hZWD8Ec7Hl ztP6 z{om8pzyA%oZBUT*QKt$P=0%Z0x9BgBo?1z*LPPsi?5~CB-xWju6v|9x{o<(erx((! zfYt{b|LOmy`2Fm&TLW5Pq+92txRySn4C2YQ)13i1c?aDz)xK%3um@!k?uE^g95gp% zP@U)Crpvr7VQl=K>$E!bQ6;*oU z(~n@%(r)z9RkpHkJc?}{-@is_PYhiy_{YJ)rSj^InrR_jK=Vn$ioV7yiv65+sRs0F z)fm|a5SW8PRqNuzDI!y++W6}OcTQQ6_KKgY{l)jpNOj^vNUpa0cQ1@|BP6?=Mk>M* zPlAU?Kc$+RB3w5{1CxF8#{G(d&%y3%^)7I_(ax)Z^|-(R=k8EU`lO)KVhH7ISM6AH z&EKbK`DIEgQd=w06S*loon~HPBI?)X<(c_wqR6DSd@gs2yvKvNj=F6X=~~@2j@Pf4 z)W{;zrRw5F81tZTq6c=#o+>|YQM}d-*eXr*geZ4vzt6bZ7?7;X;K2g7Rr-CI}bX7#V>26L5#C2D#w*dZhhGhI~hzin?cTE%D68Jd<& zcvW_8E$atY8^++~!d&@kK}KcF!DapPl#X5o7h8kkGPw*O#RX>xm`eMk*&px4aa~-n zNwwXQj)Ku3`X~DM8>H)^z21cryrWloLqjGh)uuNq%I3r+B zpXo=&;N{wu{|2~ZWrSEoS9aIuea10{HWRBiewCgLYw5&c!bEq6RMgMi`AjC`T$vb) z@L-vPZS9t{U56bJ{FX02g2z(cWk>J^rt%XdwPOE}VOD+Kn+y-e>n_rJXZ~>)#*cS( zUO!s=yX8kNUWEn!UT}1<+WRKWsk6Xi0O#=*1?f!A)7NMaTL<5f zrGwP-mDGsM`KIS=aFQS6lb2+l%89bdK!2t>tWyy0)oQ`Ed*OHfAC|#^7I1|3^Ael= zZ2L6E=6CG*uYkWziZF9QnVqzHdZaqO$o^=NBnWN$)5*8E#GH7~?3^`$A>j*ox@Z@W8$zn6&geE5hnb(R^l*_L%~?tw8raP8#ZSn= zKc_m`%NwJ;e-*7h4Kpz`At_gPZ%x3F!{|_paQL_mBldy0huJ^P&zgUS8Xz7-?C9lA z19?FXMTa3h09h)`4TohHhnS4Rlqt2r1T+hy28oUd zSG~De$9MO@w6YMga*VVD)ZV$Ml_>D4R0Z%yitR?L@F^$M8!v~KS*6c4TW`U8+@@Gx4LM)RfZNf@++5udH4)?uL#gyB*$?XZ#Y&obh}t8hK$619mUOwp zm7JN1hfCa*SuX$|Acr-O8{$o2A26?XS8<6#Fsp*`lz#4lqdC*QT44UKgB$+mMMdd0 zw!sfCXw8GnZd-kfE5aOfJ~JKP|K37$=r7a%R>F8bMoyc(J^T8|Or}5W>C^QRV*(jZ zWxL}8hTOTPJa=FoN-~aui{t4R~htBjIo_S~Uj{#!K$JvKo znQs?^0`YQ5OpFo#H)(s7H5W}<)w2-HcGHkHwzC(qAdtDx;W#Bm>!cnee6?w8{NXaf z^#o`Dy^mGWQEWHhNINGxcRg7{pFh@-lXI6MLm(`-y9pb*a9_G zNAGv&pPmVAbI4ZOuYKve?G@6EUV|%sGH_V6@%?QHoGZpc=Vfb^K#NwA(%a}gGw+hE zpFD~ou|KWnsv~^h&?DWPf*n`6A&9GCCi`QzCXrMi`CacWMb7!~!*9e4zO(qlBF|-p zbacHa$&G5e6g=>2EYbGkI5UrGUoUIAby;2xtjgUh>ap-j|FFlZhhkrRsH|743Rf@c z3CXtw9sFpm(dPqfDpTH!i|+go#|xcH&YhIBa^zY-t|KYjAjnLLmF^146c>tG#{38| zZqqbH)q}KiGb6OuNDU9k?x>KfknjvhX~(rU2SQYNE{+XX2 zoF0E&GW|$;=-Sb1r*0SH-yJ!8p~J=NM^eDlXfxXwpUs$W!kbWd8A$i?gLD~z*<@Wd z%|s>T(>K)DMUi(^3(e6ZGIdfbIzB6iIVzb0U|SV7$R{T**mp3oyb^*Kr%yjAy)C{8 zd0cCaVGS(shah>TKe_>plUq=>!^V2Q!Ge$?kv>8KlQoUI{miv znb_4{2YGuwZlRF?m{h}xwk_#P-x0y_=s`MSOFBfie&OhR@%;Jo_e;I5oxfAm;q>mx zqYJRWclRnObhP~a&^1Cor;vMK%HoivgE`xc?D&}PaB^VivvWglpX<_Ozk<($R>uA^ z#7De`b~`FgGho{VEP*~SJMT*55wBO~ZsvT8Z|vYC5K1xo+A`9i3}4d156=0fw&B*L zMpyb}(0Y_b&>sYUiJhIhgq$XEZf3W_S$J5N-E%K04(aHn{nnJ>VGLBYH!1h2OQQP~ zqApey!x$85bT2cI@*+b^f{V+_P66oXKP>V-3p+MiY)f@~_PzOvqu1#hgO)u)=Rj_LLjjb%s?2ugxrRbYn+{_1^x~COo)MX=FH`@0Q1_{ko_(^@`AH+Yp5Odcw}L$+oPI9k4lA-#WA@kn(V3D4e2iI1zqz1xDT)RMzXCtgea){C=y%*%{QmF69eHyZ$ z70@pC)%i#>mDu*C2xG`3!huVIM#v3W>#b{cXGn5)${z%JG^uDhANB?HezNEg~W{IO)sVm6N@!huW_*eS-Qb5FB~$ zP+8F*QJ}vpsgvG$@sxt6Fp@B$o;xp>^=uj25Go*NIaPXuC1*H+&If#o)Zo@1KinE= z^7V*@Q#!B-MTUcFj>He2UT45UCFt{bKP;b$Nr;TwQca%#8acp|xX_NAh;J3w;v%VA4v|gUC87rG0U;KWv!^fRqVjBL(hK#6;2Es|9)6;t%-^ zxuY|o@ zuqezkJG;wWSFG$T!UkzK-*mu`%4aVw2vS;&Ce1?AgVqh3Z{=FOPNU~M8dvD1fYf5= zk1Y1p@1<<&K4=&92V(W(-2&qj*Gc-|bRx3*X36QR#uUJ?=K2wabr~!u!|6#jue-Q= zM4(G?)vQLXG4RI0sjIt)e&^7q`k(|&UA?BoDtZU#w_Oy)TAz5lww^pjIvjHq1cut( zZM_QV+1oI$`mnm#4RW3BzU^H)_BEV1w_)BfMYy^x*ZCc|W`tGh>Xs>Dzah!PnQ4sh z%2Kh!s^EH$y?TPGPGW`&AMdMFu>|vvn_xP5wNEhZ-rKP`Vj$!@?4w&Dd*6RFv`;jB zEw$fsCVodOn0&h{@aEAyK}YS=A8qf1s|-PJ-p8M|69R=*6pdPP8k9C2g07tc>^SN#-Rr&nX8d^_oR}Xz#Xs6$vADicfy{ zpfXIQv@7CjA(5s?RvRWN!Ijd5#$I>z2r5@7U9oLCsC&qL78G&8h>%O!JUQm1=RgKf zeY*P3VGxm+oMicj1URIPF?(xNyawWBSmJrq7%5Vt4i;Og{P+;{?xt!MGu>D0HqdDH zf6a>ycooIklv&1OZUT=r;h=%^{-+;F=?7&Wd10mx--ti|~(fDVsb3A)@+qe)!F0=gynU+<2AHZ5H=Aipc^gMjqr?qLK*J0uVIC(ejdW zv$%nN0e%6VA`ft$c+KG7qfz+pSX(1Ph(r<%dm#|VUllFzVb=sH|1BMk(&z&)){vOSDyA>5UuyDXtNozH5>&^)OM*jS++5AdbB2WEl}J&L~Fy!1yI*ELZf+J}e@k9oA} zgEZ%Ewm+84U2g!1B^u9DdWfn5A=088~(;@6FCdhk!yHbNXGA!->y zc7W?sM_&c%bO>~h$SW-II=?GH3x+?aLc*g`K>BTQ0R(rrHeef|H4CD4X`8kuCq1V( zAAZ_%%UhGp%sgb5RY_X3}LX)a_~Y~(?1lfr7%g)??homYK3`RE zqcX&oe6wbzeHLKi+|(vq9Y3tD%s@`Bs3`tTu2avzKjKI4sWw9a!9^fNx#kI7vHteM zsZ4-Lp$L(2^$$LiVh()l@)$%8-ih4>czm!(@) zzk;45TEkL|2x<{7HMr%USE1Uc2FmZeaU7x$aKK7JnBZ%dLox!oy||Po^+A;Slyj5Y zyKO2KGN;7O+#4J0Bk=$gDIWX{@YzC7!4o}x2T7C~O?hwDB>EsCXEPI?jF}F6n4#}{ zw&@3Bqpp76B(s0ESmV8?qs~7srX6!WmVGMNTU0ZHe`mc% zHkFsFVMeJa2*t?R6V`6PhowxK8Zx>7i?E=0oNN8t*YR(Ss-R|9MnkDSw50Z-<`9w2 z$eVEhH0umu{gM$mSND}Q=@yku^SR)J9DV_daiyc?$2NDqQDOg8)Y<8}V4 z3R8+vwZAh?UTw;qV)yn^&g8bc3hD3W{B=$QR+1c5uYu2ABC-Kd|7t9>l?rSfo}d+eql&>F8-^=#_2 zTRR8`(F4VwCC=UZphEIZfE;M=u40Mup=Y0mY=&e|=Bs!U4a9m?+sI={WGup6n?*j4 z$L(!5r!dJNxYz+Ck7Qf&2Q@4mQzv|1m;%T=YE zS}?9x!R0yqvjstLHWcX%^gpOh$?5R1t_(=_o+v){;&qT*r!#R`@(dVi%)U|yxhe$N zH_I@uZ^OrJK?1A-74&#NcOj33WV&rpe8wfMuYAFEDVubI$5JMQ+V$j2#!Q#nOr7Y? z&vBmkvV4z-6l0(|tojYCu1G74*A&rkT5(nT0Bwe11@dS4`k5b3PXs%Y84{<6;0b^8T2b)y2TQ~ znQc+LZe*$x_3E~%)veSZo-TJKIbG1QElq;rtgPZ8yG*fK84#IM#otA0@{;{sT^KL9 za2MvxzI2*<`#dh183&{`r5j_`KL8MP^JM~7w+IA9A+S^ehimo5`|IcgCA%5U(tyqa zTd#@soq?ADpjcH;E@&#kvr>_KuzfJ&>Uw+n7rd9S{I8*y{P~QiCQIa z+&Bj=2L=NGAA8X|^Md1Vvx8whQ~{WFm>sI)adTsdr6;ML_^NIY)(#EE-`K&x5 zJ9prZ(2^O4K#lPRb1NsemDLB!cqWUdPNgHl&tli ztVIsJ-c%unzD3BsHfXq16#q@G^Chi(!9Ob6h+)gKxwb*_Y@>sbExli#$VZ@2wLJ^W z|K^a_o8#|6SN0YoS)SRb^ds`62Za~fn@_RxD$=;_3=C3iWaJv6ADUVU+=b_7`6bmZ$t>ompZU4CWYBD+G~YOj$A z%^Bl|2V8EReo`?}p9hHiy`9}f{fAZQcK7zKeveom(cMUh(r)pFreL3=O4-&=ie@ID zWtG3e+8nQiWn9#QQ*eFLS#fLUhZ?zC1N)r#gk1A_^;Wq~J463FGi zzEhRYhKeNMTl;vpC%d92WfhMreSoP2^&Kq4`>B-5kPuje%l^(*zygTV2U88QiO5ah z4aH%eG%T|eV>I>Uhi`vsq^(vcOhKVA*GCco(}bh%#^0WOi4N$1e?$A(39Hp&GGrv} z(r)~}bPf&tS2A{PHX738V8<<*U6yo^Qb2AtnT$v@qBoV}#Y!2R!Ls zSlEJQs|aM>-gif(_p3&cxslTJlCO%(FMhnV0k1E&urF^?os#_M|K}3n3$ck(+ZZ9* zZ?O$)y8=hylOff+89*ea>ROd9!a@}|>dd#k4)da@5q@H?J`hMJ3-SzCEMHwGY)nXs zy7`1pUYDeLD#iiB%3#JAXe)Ug;#FuosBNj)O@0&d z)%xgUjRB3#dGTQtjDfYaVxm=~`M>OQ670_eNd!)V$0F=6^mvt*mz7StJv%v#_Oe;Y z2M13yl|L4!)U>{K9;|*4mTTgrKe}{lu=U!58UuLRdAgJ1kJb=!1~wH>u~S8Jb$%<< z4x>u%AUG9l2#Qy#;#Z}S^Ofpw?^Q$K7_>U`DRQ6*K`RRfOht9r0GieP3KD#Yog=G%2fHfWDcs%Z+#?J(VANEs#r;cj_1@mbW2l^c|9{(eG z5mA?mn)Y;}2_W*_vX>aVTDh}b#UrfBdPS+6Dh?yb=G8?4XkdC}N@Qoeq1;ycU@iGcUza~5J{Jgu@Cl_KY+5UEe zCMUig%?-IPhHGil>Mk#vmzh6lq^&nb505#_A}*4ivz;@xg3-tWatw`TNICW`Mtf+-o-_MKD0TSwKZNL`7%wO{f00h z_l4O@h#Hzp;tcgd15UptS1l?oS8)kRsXl8DleL%Oiv%ZAsHLpyLEJ4e1{ipLgehXU z3fX_82f!~S%tLy1nef2l@5*poTs8wzL35S5e<97lExT(KFfIa-yEeog%gaI%@f&+(DuC?B>u1;G?}LNGZ%TUqSVY zr=Qr&+km)%S@GU5^UwTHYH1;)_c&(p32hCb|`dy376+!2RSc> ztn5{n^+Ha2)k(cF}LGXm{o}-{98b)O0WLk#HpvZSHuTHX|&H0Xe5)Rjj7+6ii z6??I_dgFqJgVFwC4dV>g?eN<~?y?O+1>8_9FM^2@V6`>(sfMqFE!#L$6{3BQ1WSJ`jiD!=D~eXoiJ_5)810NEg)Thv;< z9>}hSkk!Vkn^RnDt`eU@-{^+vC@{gkm1lBQ@bP@|Sl}|7-Z0YaW}H$7HDsB_B@g(^ zM&?8Was7^nrB3$N&wbuu#aMW-tK<7K9b3;*NCqa-r9U>^|PWee~ zXOjvn578zKymjA)={wtRYt^l9m{f4T>lJJ|hUV~e8yLiNN3;dyT*rO0VoGjMyC z&ulCk4u0`yMG4sn98;&wlD!iJI`C@_!(o``M;@<=R+6*H+3}h;X(vZ!ap%%9P~;U( zZ50;IBznr6dge%-;^JDlv=inqKohA_x5H^Dw5<5upDSzSm97i}IzfxnHo=a@H$=r? zKhm@dxV{NwU!afcRne4?^=?U>fr5}$Mti$BaLVl6-jm;p;ij;Je9^8Uv0I$Isp=H; z?ZD3uh^K+NRmDV~;jMz&u9$%R;6CA1zAB$kDCy;hVgLQ7r!UG)q$rkNGwp#;d46Ut zqova{`2Layj+>pJeXKsP8E{8-puGCFg~X}ae8W6A(w{vYoIGavS9Q%RcJV5_!J%)x zhmYxkaU{>As(#REJB!iokp z1lzrOFyUVdUAYm4P{lBlcUy4Es0@|fGeQ4TAF)?Y zWlQhr9vHxyrD8)ADzSO3k&Pf+fXyUm0s9uS&|4?%=o4)Xk2Gjo5y~!`-^HA$Pe~L{ zxv@kDTd=1m!*z@WKQBOJnpoZ2-Oc&=t9+!rnYc9-0{fFL0toCIO#pcXob{$UwKxAN zvRJj+%gfaoxcTF-R@s4L7<&;QubkbQmjU-Teb-@5jI=L^GlD|KOewXEFlvidGrp?# z887@#W=y5WcTw9T}cjBJIEviZsDej(cCjQg|E#=sl6>YaUB;R*h&i&U)D2T>n?CsTat@$xjRwnzw!~tkZQsf+y?190>XK)i6d_&gdV}ZHphEOqt44S?baM3KG z&^Q*TrU6Fpv3LwUyo`{RKX;JfD&Vd9lQ%8q(DUK9n}bV5ZR!LF5nTzQZ_C#CMr@{5 zI0k9R8B;*(;UN{-1iV!ZWUOlTXWfoV8`PtZF8v6@oy5XIGl6-u+0hedY!a+-{9OWk zY3Xs(k6|ilSY>KmCE) zl^^iralKoDst2I@8+lgZw-LPq0!ttacTIrRHVp8I9y2=%1y7qm%HJ;;N*x z0tv^b9Je1WuMn?L@IPOkZ5CWbAe#d3YMz=TEBmb>S9^u%9(~C_QG8DsGUX&8GG-SQ z$^PW*2pEMaDHV-l34pRC`@&SRL(2cEp10a3fe2&XCa$ixqdBFE-3RCd?gyU*xrQqA zB?I!Fv~pRxU9a8WHyOFys|_cY^I~g^iJSWeT+?I|4peNtND647hA159tE?;5VOqpn z8Ifo2a;fAovUXl<$_L?ts6!eV>p*jExn=F_iL=Q$TksF-)-&!m1N`Y5W?IW{%pJ_7 zwV?f#R5Ff~Noz%$fFji4^t=qTE@R9`BMg}|pdsj;tbvwxGjA$-EMa(nql{OH_T)Ql zL)R-LIX_1#M-hb1cbJ`h<#u>@yk2|vsxeWXfpP<8SvzEJpp>$S%V#FlZK>Wo2>Xl% z$X7m%IuCynT{4qx@Put;Ch4&*w913Y^8uxgUn#)bu-S{g;QatGvmLvONkS6=Jbb|+)&kFg{pLv8%+0WVq zWKM@Ocx+8+eLW%q-@Xh0I(qcga*Y|9eJ`Z|@WSt`Mz4;8Pni*yToN;lCe@2Z?jZC(buhT8ieY9O0AV z_6B4Z(NUU{kCJiFQ<;>+61L05D)MmBWmKSVsC*0vlHs>v!e)(RC9z{+7i4CPhK4N7 zEPjtUIk)`!&3%9fxdGK=3QW7|R8clJh@dhuso<`q>Iqsy$UT-AXK>%pZantgfgAYN zWOpM`tgWq27|Sz)aAUoSu%ZKwrnX+E62t3cQ?t28G-}97iu}H)E5Nw(hM;N}bik)t zomwBH5{`=QDB?OBvN`exa27?A)^>#?@6VQ@PbilUveAO)UK+ z(@H(ZA1+oBHi3Bkq06eH*Q+k%e5no4Gosp6d0Z#GC&r0~H>7N^{qfW#mao#kR^T>V zCK;2!{{>Mh9TsNuJ<=LL3rovQLbtIaHsdPk-_6bP<5NZJiU@^)?oh{^6QVRhf28Fd zNZ~~xrHBJ%EF|^KOv9K6F5#Zg~*W8eR!!PD&Vwd4G$Q6&|^Rn;}J;Z z`l!%V5HQPkbCuWDXvmNWRayrUT?R9NY4PN72O$R;^iwY`2C3;?)WUM^kRK(8=UUIX zvUVY_Gn4eK2n$>BPs-FpICe-1SU+YEf| zP?p7H%UPa*9WC`js-n>vX?}z&Uxv1-2a5by$LZ zA;A)*5HR%;<0{G=SeAeuQ^R0TbP=u%DjrWu3$K;LRZJPWle@lM?Y&{0c&>41==ob8(8&zP>s{`y7H`f>-E%vnnU`uoBtXIgYy zxeh$NJXlT83AySt(+I0~QN%?!nS6yYak5xNhXi50C#zGwG?woU{HpMTQLW`9;S(Oz zR-mH=8k0f;CPv)PW#UKdv!Z0OPyvf8H{gL#tsaN7b0qIY3B)NrYlJMCfdRELjYB)T zHZD~erbS70+YrFBwk$ydyV9|hH_gKk{+<-WwYfKSZ5b6;Q5@|O%InJJvLo@1#9#AFenRYn~C7o2iRzOLvp@fr^ujRInbd@T^k4f zl6(zUuGA}1HX3fkU#OJ&HSVflrL-DeY!u}tIi_-UFY=CWkH({qO z*CE%HZSYa*BINstD1AhdD|GL1-*f_`u@hwWOmb(<`nWW*leb#FvN8xFd2VE#>eGfK zxh5(q>bx=rjF%g_6E5i%n0Ls-F?+Es3#4x>&zitKn`42Jp3f;WL>Jbvre74f=}Oi@xfKa z{vRf6>rCTfJq9okZ%ISizC_hJ(Lw+%IS&7DHB#Ol33=pt2v+gk92Uagc0m;qpPwqs zTx?LJX*UBPic7t?{j&2VuX}TmHBK>j=<%s65+!0K->+_Y4cm@UDS*KnKn5Fjk#XPs+=6vdTNN~m+$l$KeTGXJbwD>c-q8W19gY@ zr0FjElR^K1Ad_UCC)yQlqHg68sAzY_!h3vRxds|8D#Hsdi~4X#1Xb3Nh-BMN@w1sC zep0LmsXs6W4q7HUfS;x*dXT8DttG ze7`SR0ul;f@B799u(xCW$qZjTS|i$6OSI)(j`f6dkdyUyQgi#(b1o$z@}uqPl^7W~ zj!~y>KtZ8*LTT|zEcjUW7Dt`fPaT0YWkE}5Nk7?v8b;qu0p|MU^3Do2rCNg9HR3R5 z^8F69n`85LL0_>$D|akdeB{vNsTmX1R4ST&3`+yQlwfVJL<-^>QaF2MJRh&ng9v_Q zn=H(0q3}T=NgZ=TR~y=RnS)z8ry4;E5z;hN?7>Qs6d$e5<5GVC#6{FgKW<}MxgcxB zvx@0p_Vvn6>8^X9e@1?<$&)zeUG>_Ct8h#ZK>S9dTl_#jRCrqHzuVkYf+$i zAAMuBr;SY=En8c`%eUCW0@(4lN2n6SiZJS%Z>zNkCx#j#Az?E~Sdj&3cjhm$jfO{9(?d=>2e-UsECme@6QqLfP6BdF!C;8s#Oxi>s)B<>V) z4FIo`UCJcxq);Hh37~HTW|s}o78jzVq@cP??TDIX`MTHX#22XGALaKaZD>oce)S>dRhTCoK6yrrKJmf90TlaYmL4_7%ib(%vVDq?cF)-n$- z193YiIDn9r1x1>GJypK$FE5i zrIs0H3D9*!Onm2Lat}DM4%b{B2c2afB>XkG>c_B2uItU{cna^h&NE540*#fN?anoI zwq#*Et%K5)+znk-r4Cxh@dtXy20pyQ(;rx|IbBdzKnS`awX;Fq?yHOdq(Ba-to`}$gtynYFa`MJ5;jB<;B_nC*b9wd)TA&t!iz0F#||c zIzNDhH^h(oCXUO}gjkObX_}!{RLk1@a<eCH!h;vvKmy=x;hExri28gL4ZF+~i-EV!# zpbMDh)y2RdH+u#)D_%+pb{Yb5nSPM#I)13LLNAN$Ipt0$6>oYV1L#^O_M+(Jm_Un9 z7iGjCwY};@Pt6J#iirzMDPEdZ80_%aU|H%J^4dhIw9%=qN_0mEKJorJUUqMz4q7a;BqKt$+k{>MunM#pJ;OsiU`- z!v7F>`vp;3z!LYLX8DfQG`xQaFaa{W69&GLc9xe#nV!<8R#dPYY5j9u&69i=+2WcE zPic1}SMYH$TAI|3* z6GK<`g>F>P#l^%2P>xKw5d&I)i#XJPCG}xZ5wBN|mEOCy^l=m(t6$=la?&`OGpozf zoX8HcFEG!f)>12%zNva>A<%{d21dWk;<-Hpu_TOkM57Ge*`ij#OlfIHD0?4!Z^ys~ z*Ok#OqcH?bTQ7eUOx$ZW=cx(YFv^-KEw4o9H{in|=TsMgoy>>}e!x$pW0}C6Se zrIdA$U-8=0;Ou`$09$nKUcxgDRs40hf4gVP7z9U6^0GOErz?tiD5PaZHMCK&5x3dl4zXkWGneXp=T|$_NyM8&dUd=aby}y6tMn7XSby0t!@|9pso%zU#I$_q6)+EI zSBBTdanHA2Y`v)TBE)pRKR222*No2c^1Ed5oGu9$TcJ^9Ub|NZRs%%t`1^eTU|mJO z*DAn*(Q>x(KqV+e(D$A)Mh&P`EivBc7d-q}6gEvLB_JIn*Xk@FdD_Afps@vPiw%QA z;i}VwxxMXNyh+=uMdrLfoU3f2M^1HWn7!$us4JO$+YL8mB=tCRQ2PH~5#dNw02)>sKpW2iLUp^mOPfCqsiH6I z{Pd?`o{7sd!}=0U3n@grP%*+b2?{I zAsc}1B=nT~&sGjjGj9jym*t$)q6hVnbpbI)AQC#Z zM?WOl#t(Hi3~UZ-32(y4v=CK`lIZT6l-}dNMel&7IY2*_C2mZHD)X=kgNwXpfGpy6 zlG6WVCuakQTr>(pZtJ{s*<*NZ;H*&UCsF$J%hxiq#+S8BN{vLx^i}%EiCChCqo$v1 zHnJLoBb#P~*j2EgT2pAGPN)O;fj-pKcj5H!71%tj!kUyQim#1Q3{Qf+UR<# z-nBnEfGzLNcn=bDGz&ip#~IUi80rZ_t0d;}~e z2D`rxEDE>f-cjeKi^I~ykX=PmG}oWz1I!+@65NHH^Qz4ON+C*RO7wmJ?36`ga^ z1k({=bWPN8sv>H(Bdk>@LxTY&rj?Z{%G00jh?=QP<(-Q
O`qTisUenxuaP<#F@_tFfv-$pBU?A*q2SE+Bo}Y$w(Q1wIUKZYO8w=a$ z{OY~bN=Rh@Q?3ubUM>k+XZe4t;x{rtPHpbwr`u z%524xo2GxO=?&*XPe4KHRtQ2p6U23XWejF93k!aI%rPl)FXXG)yX04K@9Ori9@&Xz z36~DP&`w+S18WFsDeHz%Cq6M+QS`3Zecw+NxT%#;z0jW46#wvuYa>l%n`VGFU3%W9 z`{RY>Hw=uvXHMN?_V3rGt_}4yWJY8k$W(4}b7DXEMmL`XG6y)uu_~u9yu$pGD&0a1ud}lHrm<~a6SIf#_XI^SPoTt&I6nFf z44QypgAy;0dO;@YUf2YP9tN)IElcR~{)moHizSZ~?_+zq(0l#)+_(LsaxlvT|M8Q~^48s@Tp5qS0IHABBB>gfyj=OO;vw7>Fp{^<6&~-8 zoq&j(*baMKWJBA9h7S9*I$&hihJh7; zHC$xM8WtqF(XiyC`BrhV2f{OjCn|`AZJZNc4?x7prVt3qPA(?4wQ#T7?2L6Be>&Fp zsPJ=k!#}O?1{Q%IB14d13)yBK+S?g_npU0vz9?^&m=EwjG3&Bd|KAGkAN+Z`^%f1B zx~o9dP%Jz_QA1Z9KKa?*RMP63^_VPLP|K3RvMRbtS(Seh#Z>pRU-dy2DBej|oD(0$ zlHRn808b&2A2Z0KDkGX+aVIkv&7-?AJUr<0BVK>`!Q$Rkzu)+Vy^p)-BX5wCZrBrk zyYkVkH&^|YtKn?@F!BMu*aMX2ZHJ2@s&k9wu1V{!hYqbOEf71}%xUL-gqKkrR-lb1H%+lX)ofhQD zP^1iJV59wW`WIa=`OKN&Q(|Z09@hV(_&tG zKRfP4DT-gu!`rtPXY8L~sv0h4ITvv;+NKGNWp63y4f@#@DGv94PGT3nF@R}$%PLq^ zQSjq3&#d1?p}M`iYm%Ab{Y5*X1J~CkM)M1^M+vHeDOW`yaPj@#k%5v*X zd~@E-#R_prWsf|G+lP2Y@2aD3*JF{`X%b@1Ic;TL^b-l;&Ac=I&lOZ-Pgz}wStF12 z=KWqxsV?H=OUbOF+?ZUa&O8?MA&}YkGUZw4nERCVa(66}=y*y-B&nK0AB{+|^Y#3V zzmm|br|{vub3)$iC6Lw?=11SDzfZ-s%T-ZwnWtAFm#wsy^3no_Y@a)?Bn1hDHKNDN z>S-b(rPZO)P|^m()U2nsWg33kr_@3zKA3*UaKXrKXtYmp6(7wj)_jXU05UDn(Gs~6 z(IFXWw#D!n-t-HSAB99b@*rM#_tUVVeijQjW|)2PapgZga?dQEKjJ_^W3}feXaj}f z6wvP-U%cd*W>lOd2vi9xZjdfJsd7Zq1=in?DfQ$=pJ)P}s1kzUU>j)3Ls$0tZ>apo z?l#VTUibXH3Q<<*+$yYar%qak-hoyVv14lecp8E^$g)9t4gM2B|M0*b+!;s&z!LEH ziDK{lFyiqE&Dg5vZ2m3qg=m|@HIN-l0bd6i^m9kQ+uBpE9|8Md*c9jtz~vU7p?U>G zsJzBY=V0ac8G@pEz*ZWXzA|jAQF#&3(Ch%jVdEQ9j%#o0uc$h!#81rvVhY}nSV8On>$vyC{*Y?B z;1@7Y$`r0ybJwa)UJ09?^wPxL8(*9COsSmMtpKl^w=WVe%VN>Rgh~#!+3P zWMbS#roHaM&)+9lYE(Gx$NQUgOH*0$y}qq{O5)0b4#h8L$f^tAw_XhXaNCx7ik6x} zhhjgkIzYmN!_A|CQxBG*77m}VfhuVxAqcI1z!Y4Snn_WTAJ$o(yqI~1v1Q0hd@0gF zZ_VvumaEIRf$SlX%c1V}&VCM|*oZanhFX1mv7yrBZY6$Y+Bt6I+8RfR7G0G$mfhzD zZ{b74g$n0kuUJ32fsh!T1g;7$BZcQXV*E8KyY~^v${1YL8epE^d@Py2Q}Lw(+p5nF^X;hA00l(vLkgN1bTi`?NuCQf>ipPV_7N>%N$mR8J!kzvgagojRh(qB%3=|3#*=nlX4~KMLE1T2br{&-YM?(ljGkhq9q(HgfLtcD(E}5D1F6> zN)ccVvA%EZT6;~e=p^r9{801k085pGK*K^%<2EWcKWF6ASYd~8|CdY z?LlLy(h}0;?9&nmIX+%+Iv-c_&NDEg0)TY1>t(g1#~ao43}4RplkZEzn?7uCU2CZYFw_Z8mQ~Lw=!{KS3+0v?S{Yp0!ax2tUhMJs<8I-Gy!sDG8)`e}H4<&G%?!l%8H z+b<1f3pR1nx5bD?t37unCy>=K`Xz1(YtwJi$Rf(vpRSX)h;`i)eJrbwl<1GU&NB8U z-RUh|Qv-FPb$@Vtd^kHpAFEMBr?Dfmk-|@v4(b6kA252GN{Zy>{SbjZHL2MM4|G&T z;M?^}ZEo&r{hGJL@tI|t7UG)C;dV)~RRzWTy;2WDgk?uYCb*Z)W~B$}zTP$cSe5`$6<(3$IbDAsUi3!dx;$P?c^StSOe#mdM)DDz>zpH=_t54cS|2#aR{zBrMLPK2tR!}VC3EmAG?`|=bEU9)m z{%%lGD39iZY4+tXpuoJ4_TTFAMtNwx0>_Yc(hb%MrS9HSZBh^bHG+g8NH9P<8l<}* z`E`f~ZNwI+mVzl&i`@m+Q+tw+nnE8R02xUmSam@2U>8znknh@ub>dVQ|Ytx&rn0zakP_V2blchHy09^wS)O?m*Rz87X{wn*^=)?hC$R!5{NXlj9FF3Dj(2#vW4$M`n4qP5 zx6OUWRdo6!r5*1+opK+R1Ff^zHSC<9g-hBu$qk4k&o*(w* zUCLz1`76A?V^FN$k7#qlqsUd6jKtDE#SLFbKQHkicEJ;w8+Y}z9`&y)3kW(&ic5}( z9K))a(%l9Wv8VknZ1||WJ!8g+VOH<4TbQ}AaFw#`a!D3tHd}gIOd7RpN+R374YM%? zxwC*vq6)9G2Dz9Ml4U{o);XUiJ?c{5Q8bnc^P>rgcFKGvcP|7T10r@Gf~m%J&toY0 zIfenbeu63k)*J-|vPJLE>rgd{MYqVT0?!=h@Sx*72?>dTBq@o}88b>!*#Cci{m=9N z`A$MY3jhB5MXF413<`w*C%>Zpw|;og5zSGC-96)RGjr75%JG>`p@Mw8dr~CoE)>kM z` zEvEf@1Ri07v;S^z>g0k?|2|Du*iKd69+ll5RapEayM1!%WM?s5c5zf;i4IOFES*!> z9#vj4RDdJfV=4k;@XiX`7=@iwg&h+Wa(dYvIu&w56>=jD@-(!DKf*WMv>$vXEjI|HJFW5$TPK% z##ER|G{`fMg2r%}I(Y^uvw{jUBdH+#PSZ66BQ$s!HFz12^o-i%nY#7qq=Jm3yo_4) z#+!oaNI?c9bC~WV<0fMKFDU}E$;+@Nm`+O1NJ^fmOm2)kd71%NukoI|9XxJc>CPk- zjMklu*40-H7X8LJD>;>26GRkm&ZjhXa}Lirp)nnSW`oWbR(+GFcOBMe;E2 zUcexk;bnrEa37?C9jCO(;bPcDVE)nyn*1e&xBkllFFR(q(!~g`9mf1+xYCFD-x`ho z|BL<){03@uH<&xfv^E6a|2q!MI_Ti99SuWTw?0#M>Al(p`FkV}gEl#XT2HiYeY7^Y zqB?mxj1_J2R^25y(yCi;qD>yHyVR~BXo6%mAw9f|s>!qmx)Nnubk8>w%cLoyqyFu{yTN0OgYhABl(uS~A2 zC@_99O?McSVe&1dk!*ibVP-fv1rw3kh?ESYy>kv`r1H)=IkJi=t0z-D}sIynx;CgtC= za0-kKoISBsvJ-}2uKvev{nu#yzaDA-wPdss6M7O8C)QEwe|G-IQqoBL*L=d`i8W0E z6TlmLhRaEvmOh%nNZILXil+3R2O7SdOwNd$n%fEmTjuDVk2*O_0teE8Dg!iB1%p`_nwV|w<+g5V(WFiPn8Q*Z#-fe6a&aIrNqp$tK zgUZ;fn(faiSNA@7hKw&QI=n86ehYC0-nQ3;`h|KwXVw^?L@bv_ElD=52w~`;mZ>k~fdXXhfHsEKm7)m}yw4okOD2-}k&q zf1ua*DRq;?!q092q5i&wl&Rv@vPpat%ZLDy^ADFv#iGgcLAG3}IE>5))z|9YjHmBZ znch5|9KT!j%8YuCZ8*-{FyZMQyYbzsJy{d%50r@1t@9si3QSXa<>v3$Hj6%bvTz;o zsBH+vEbzuahdpDUCAe52P>1`fZ=4_hVupVpNn$d;zaJBucQ+|j*@dxh4>4pINDan4B3S~D_X~#@ic=Y-po^=|3gpQEdjDeW)b|=CRd+-E&Y+A&nI;WA0(tu z(8Tb%I6Ars!N0fUNXw+%llaS<&*?2yjp^fYRneZXln>qpG6;zr?LE=HE2)DRGe3&M zKGzh@+t*YhId@(;YfpRAi!URK8LEr4?0n;)(I4 z4W{@@6@2qMRSxZdN{IulJ70QJ%o{pwS2baBIuf;RZzeWj$tKp~PO(Mgf>`*vbeRI>9`TjR&+%;|}m z5^s|E5OFi&&sdBHZaCgmzQlXZ$6h%tnqqVF>5+$A5F*l}T%Q3&e1JvN5QVJ%kUX?n zMXWTf*q|O@ub^=|_`k0_l_@-R$~?`I$89myL5f{M&G5+!0&VH#WZQKh)j$8dJC#9} zo-J_U%Vc#$j`YO{1m6E3Kku#IO93mBGJ)luKWwjqx*cf$geQuIB&?QiEo<)G7=5s{ zo)zzJ{A3=TAa%WLy=X}wyu}b1-tHjsV2fZkl;>bmv^rH>Yk#nc8-)-gO1C_eI2VDb zZ^sAuTs%5Huf1;!L)p~1-1LhbXG(jK~%D1*35KuU5&g$r>6w#)p zdT)PwcV%Vu!EQOu%i2ynV5J3~H{Dl3Qfr&x=KN8kegxa8xu6Jde30&ANzQX`I~7!4 zT}~>yy@c?n!1s@ez4ayX_@s7L#33k`c3W(l@rqtjc8mf`-kvkNwTj{al@a1Pm_~m> zPUWH3oI-@hO?#asyrNrKi$y4=%^M%#+L~7=j$E&)E?vS)O<*#Jd|<~GS9!#4iFx`r z+fE7iak-_KXxfT8)l$T5*s1bS3{xcv`nYy6#$_@z>COv3dk1V~iM$dtU${0nd;*ls zy5t^alD#5(r;3Orr98_;ukp;In~GG1(mS*++s4-a+Xs}2<%2gy;7ImfU1qNlRKh;CbMR+$e-q}7p7M*BLPW=L zkS;!8KgjTau-;BMBCMkkD4mJceY-1!eH^AOZ2wkqadAp_pm2{Z?^Q|LK!HJRog)5n zQoqoj;g8^=LNW_iI5$B@jh{Px?VRbIZfPf0pMH7irg!R^r zRzjn0Ee?m9YHcSRpfL>{Xrkf-dgA2JTtM@yMl4*6n+qcP@8b{$grkm8q)8bbY4W2* zH@~dlRi>$lS+iN5ff63ku#DTW-jB6AP1*n?5wcLtsX5qDeVV>Q!@Xk86 z_Q%^KvB6@Pm% zz5mf)m6b!P&zgg2X!bLU9H zSze!4F(RkqNS?>_oibrfXOOt9N)YG@W6#J~X!`S6P)OTE$z0B)Tqhsw-~-(j3xh_4 zt*DD|vWG%}lYqi4h75mKCd#B_XMzl)(=_67oq3Vy2>T*+-_k;Opm^q!YVM>`_HwPh z#f-nV8I~3$Rpl(}M0-MHhfR_{=DkTah{N@6BQc1kN`)X4A~LL|#Ux9^)WSAf+@(Y$ z$;UwjcgjyhZHGUd>9yX%DUznxTcl2OW1r(M(Tl1(3h^$+B~iQcj|D6DKjPvss!MR! z7Vxp{+=#EIWz+k9MDx{HOp`rbjZdEDPYHL-`WZbc?YpXdeEjFR=x?j)Qzl!{S_V9{ zXQxl;kz)4QhAJ+;^GQYrEZzIe>3StjXRG`)on{%IKKS6xW~?d*ZvPMcxW96j)9JDT zmT^@jrHq{x<|3ouO8t^*57_jiD$jA{x>F7%VNA=76oB=}tsVMEqOV9GVY)Gwy*lfqI z^Eh>B`xx@jiJBw3`@TEy(7mpV!(M)b?j1KUs4vRcY_SMiM~hDtqp+)fKTybhOyf;! zi&dRn*wC)6EkIj<+XWldN?6rxZCxk8_(vV$lV{bTL0sgTP@&z<{~E0hqxUy4tH}u z3SNOg1xe$;hlSp%Rh&X2vbJ#{d;3_sQRxx;&?&SC+z%nA9Opsz)JE;ERJHW>$W0NX z+^x9?;@nO-qK@8F^&CYHSB7oXp7Bmj|A@4}O3ueTuN)t>XX=`Z4J{)!MVcYT;Wq3~ zE+XaI4_2q(^I|gzD1Ws09HI`7Kw<6XuwGfs?GE-K8-wa zeqP8c_df?2DE71*9TTq{%OkanMH%)QSNDIpTa1IEc#!`=IX07V z(X^}5Qk-}6Q5l&IEv0?8+i5*U*`U}MG6G#N&zRJ+na93L-1L&X(Q5TfO&262N@mG_ ziSkLsrE2T+Fm$x&HYLZLVm#|^ z()E}tvAQD9)3kh|a*h}^;!oJ0BR;Uh?X<$OWiH4%$lAdle|U)5hmnHDdaS3aGLed( z?{^VcDU!@eT1hgdH83y`M~NunFoGY`^W+5>7rscAO01t(`9^QVCPCR`&`m}!75l9< zF)Uf&jjgwgf`)B&)1L2FUnW0--JPLiFPq|a`6z<|yAd%DlWQ6`G$p_KE&ls&Pj;#4 zSJP5!P4?#3rB$6#zks4P+#t>yKXZ8TSn;B6M^QE&leKNKAJAfoA3Zj#)rpYX_wT4Z z29edG^^R5C;mmOaVl^lNyVBZXw~g~YXu-i;zZpuoFqT|=wZ zcHRwW+?-A8(Q1%CINW075q_t5{YVa3ks}p8-dy_QM_Qg_4v+K*pO|ExQbF=9slYe) z73Vltt!@9_ey~mS<@8%+I8qhM(N4&yL2>1reox!9EWFf_GdiH(EBg?ool9noh-DxV zoM(8$Kqcrci1QFLi>pfGVJ{b03YO=uijMtAFM9s<={>fks#nrH+Aquk&u^gPu)#~4 zTW0M9g1q#_2jNY7?6-7tM3BwZ>>0_z>3KP2;SPM_IZ~ct$%6a?2Wd|PB;&%9Q)agN z1(t-61DefCFCQRhlB?NYA`C7LYPJkDweu}xyA`MOJL5%P$S{}sy|Qz*E$}mt<4$%d z@nX}UToRTXXfYrOPSp^tByFUV)#nq4HdEOoZ#iR2+AY|*9~(k;O8(SGYVFg14dcyN zBuKU>e_efcu|zE4=bu6nbkg@2ngQ~&m-qsxtD-WJmYn4I7;j&_D(T|t!S+JQTnIwb z@Cl5k78s3)shZ{m4rZfEi;}jRUwszSpEyltgkS$<4q~q9lmr=l|nnA){a}BJv^9 z6C*sdn7&X`^&(VTTU~04kvaK8y;c${A0I+IA;kE`HERnwy0OZvCkx4&$=)LBcLU{F z2m9{yG4&&qlyq}gG+)VcCq5Q!k%;-gV5I#lN9R$QAjVjjw3O;B%^wN3*iU4_=hIo~ zle^5R6i8!en>b$Q$GM&j7D-Q2&PW^(mS~9yp;YG|H#<`=JKqqm04>_5?D)LeXink6I7i6j1!e2@A?zI--SEQ^GR zY9xJ=k!q>|RWhefZFEO;MQq>2E;}ARF?k-wRqiZwNPBj3bx=U_stAnA(9sC*m6n;$ zC@VtSZZlpNFD-;IzbK|@R)DFN9;)#R{?%^lZOfBs{AGsxEF1kP1c_zuTWy^@PYVy@ zzprkI{aE&Ewv8AVO0iH{a!ux2ApgrZw)kiS6KWWQlrvdLYo@A}{@9vd7M@+z7mCG4 zgc4zzWQ`!|jJE=^R?wj%pvpF)D2}_|jqh-OpKwkSJ30M(Lh-tl-LX8DEg!<*40*Gag1VxCbX#)}}X8e=Jh+Fw0vuhs2`L60vP)vqB`Hsfs zmu_j+5r#~L>+(zlQLH=yvEU!BsnvUNA^tYIpWTe#(z0&y5X!)|`Xa$B=MFZq4Vzsv zfpHM=Tb()du*s_Qy}&Qp;ZI=zpLYAW-_P;4KGt16b$$e7n% z{R{Jkz4}u&AALS^(q{7Ww3hTwslPDzIIC3BZN@PT7%BT`U?lW;z16=kKuVUFLc9=!aSyH+y@)~xb_^5ui&E!T#LYqw1S7tI(1^qBHn+?Lw4%LHLD)v{MD6Pb2dzg3 z?S-Ls@FqCooV7nnM<**U3ps);D>w0wYcBgygYmm5qVV_FDoP=+?v|rLd=pXYT474 zH7klVC6c=<($#sXC{JO9M)`V;zc$mv$7yz3`L>H^?IUr!d65pfHSu?J%lk(SvL-Un zawbdQXS8pnj_~lYY2N7{)X9?;5>H1BHQ&%05#d94p`^F`Gs;{W%1{asL;*IP#Ba^b zCgzzbT3+0FZT(YONtHF(R%|nYms38beyGXz(>crb>zeZ)E~77Tc}$jMWqhwM&aXv3 zkJr1|6+)u=;XL)1Vbb5^1&HTWH=OJ!&ht3BsV1>WKE1R!-b2YczC zebfB>a@R_Q$))wCH4g=ocm)Lu5!>-(Reb~gMYBh7hRM!fTKELZO5dF0&f!W>Xbz%_ zjO1OELGYC=N+o(w$x#;T^!I$33^!*T;m*jR!`KFzrSy(}$)NtZkqN!WbI5oS9l$|lwu!KC0}=nIwQ!RuJ70%U#6(?kg}fNmz4$@sOlb` zV$6Ri#+v>iRRvll#h!;N&oES3sf^M+&7%Gp7ngaqS{AvWM`?K4ADKu0v*iNsbQ}lc z(<>#@GN&x)s#JKz`7+2(@j8t&I-lnu*#0e?)2-Ak-}zCrvKr7@)Ch|td_b)YAknp$ zP?MbevgT^3=2nz{O*0|H&p)foPgHG&sfqT>8B3(#Vn!ZBnI5G};_Nv92#Keay-Ztj zOP+B92crbqp^p@3pSJM76l?Pk5|hZ2N9d;Nb1tH|m#PtAk? z)hW0E93p2OJ%tv>iR-qO6L&hwiMj-WH^CEg(7LLF!{QI;a3eucfWaNCcN}-LwgOtV z-cgP@ZeKkn;t<6c2z?IVa63MNxYK$7@fEE40DZt5!77e$RP5i{+HsVPKh}vlHas{k zC*rQ){c9sa_P24N+nC6Z%9Zj^Ory2oR8h_{rZK|9{;i?n%ub@{t)4&XC2r?F=Pl1g z?5;n4y~d{Y=z{Yc;c}AC%SYLqMwb?*HH^2U)6&w*CezBIWO$IAF?bT+RIU3ppK}zZ z{3P!q@$g=w$O8xq+C$1m5IJQ;d)v6}IUBSPCgi2xkCd5$mu1Bweh&<>AwN)~RzGTj z*7HJ4O^!SrDPSE-QRzDE!ZD`* zjM0bw%~gs^x-o~RtXX}e1Rc#gN!)X(-I=9gZc|%2s2~}Z1Z=}Gue@@s>@G_0tb)g# zf&g0oGzsHNWq^5K*I+?ZwoAH7mgkT#OI)j*h zKA@cOXvU9%TsXZUrj$}pLD^l!i*l0Jhj~ojDedf_EUN_4u{-)8PLo24A(2dhGu^y1 z=TVQCBbS7k3Sackm=JR6Q`V=+&l|l!F*>0bd9OQFGCLBYIZAjQW>PSVl$g?faa8$6 zp-wj-IOg7UI{HZr^bg-bswZmkzKO=CC;SxlIUStzNd0E_KkOR|^F`fBT(9kWzTE1v zqY4-Q9^#J{*^ijRVekg1DEk8#FbA+wIm8eM0RPT>h6vcE#iG%swcG=den876IL4a( zJq}U;yQ#fYvNI^QK1l;nt=nu%Vg#Ml5$#GImVpnyT!l1@?)PSxIgz}Gmg^*uRh;*m zXzE)caJ^|N=SpIMWE12E`a*8>+`%s6yb-M}bH17fl{)%5CfAS^k0(=+z3je&au5P7 zBt!hu4y*t0tq0hUfK+T~Kcev_?*4v|&_tzD!cnUmqjLeMfEHp^D_=F&>YkfLA$IZ!JolMc#*AKh{5IiM)my!$!%a`$r`KEdydX+@gU137e-o@$X2iI&|QZ<-R4`Ped3 z2K!cOxzq9hbD?A0^DndEW6^Zxmu3`n(%@r!t}eS!mBFG;u9HyBPLryeitI_e)jOW7 zU&ISBy8m^-GM7r2henSYgl6{y3$_AM2RrAoGK#3o7%1fzsL!XncI}=iqf35yN>bC->k-j9;{$H9ZVvf1TFz=f1kSPGYk!Kg+nVj*!|6qNIz0`!=PS8ufXJvaH0WKYmy) z(6K6_t!iPWf0NjxMxgs5{Y>2DnYqdNv7Y!PKYpiD&Xf;DZ>}#8==b%J{yE4SgQI*_ zFbBd??Ys?EAJ}<`SgmrkLa2^MR!a*|!LL($5BkQ2YPP#nisVI@$`+|)VQnMjsIAQO z`qWi2@8B1(CRpXs9_jCs%r%h_7|`b#nDJclU@;LZ^IUnH?xjjW|K??DIVPkuOY08g zVQzxK{(DH&!JW9RuyP_S$l5#rEE9p1=9~kBFS~%X;llR8S{$sOAaV!Cg!T0f0^p0s z0Kc_BSQBv!0J$9og~RO}w6=R4v;t)_6WKPqEYNPxR6&;u`|03R$!+AU!qDd+sC?^ok^{(^Ahg@UT~H_2OjOV12d zuEo!A(|dLXby3D3DQ8Yko}!~HqhaGnM?ZNM(xp$b{18j$PG?C;Y1I9QVwi7ZFlLj? zgky{+o$+-@*Jm;Wvrp@zqP;;)S=W(^Sl~f6n}ra-2wd zUiCor-L12UK3cv0fMF@&bsVILa}f{%Y(=fh3uA5L}W}{Ka6q+Fw)QVDQMJa|5!dj*P zPo4{kazID5w@s8EM-UA=Rwr5a+chgr1_Kw}$q!C#$V0ydR6rGI0e;s}VAcEN##mF<)Z;xPu^?!P9> za^%V?@FMJJ!LnDGc3ueYy49KXoMi9!#iLhh6}^LTw{7L{oY7B`@G=}NehOYi*_Apo zT15iRVx&j>j=eG8&b_%I(6?wC9&W{>5QofYHnxomALC`%APXlE%uL)`*19btz!@Jz zW!HxnL70cN1o8OS%2-w8>6lfUAKA%*^&OuLDO0mv|W4X5^ zKDg7G+Rv}W6F&ZJaTTj6@2h!DWHVR3CeBHx*Y_)x998(%)gXjevX{{LK?{S+nPmmq zMRnh2ZJTAUy2WV*-*`^<9ZTMGi{d8r%lKl3A30LyVp*CMICfYn=%x^YG_Kf(?xaHk*3{{y96 zYG8I-%ue9Vwr1J$YT3!~TM9P5P~kFTgAnX@%MoEJrTk-~LA8vLsHrO9XnLSFqtNeU z%wP!|!u0{c4Fwj3jv^-2jy+ ziovKO_8esa)mPd3U2o_aXsJbq>6?v2oG6|=%l_a^?xL)EL_aXhVsuZ{j*{7isz7+& zq`QJs;^(7FV)NRncPQwDyWa_P#nU_$7|T^-y!4Qcn?71Y7IBd}*g@s`BSAL$uhLgtKTdl@4VPIWn1cQGtf>;eek66zIw68{Z z*%R=Pn7@cE6qgn*^3+FUnHap^GTrhWyrIQ+^PEJU>K*cKO1;yzo4IA;d{PMl0~obr ztk^<}=1Uge>gTOVO`mT|B@E~}Hpgf%IrU-)dlxB}abCzE#ZicCrfMOhgC*pEjK);0 zBIFa=Lbqp#)`mEomktha^n=#*b_g?q<_;$+A$kdlIQ(Bcop)H%Y5Mg6MXE?uKoIE= zG$3UF>7YQuNC~|oU;!N@6afV((u?$8oCrZNF@h999h0D;NEKN$IMJC+7U^c=25B1s zA@9xp@y=X27IeJuTkiXu&p8jSt{CP)R8p{IR{v}3$X-VscZ zJ+`tvmV36h>AN^4xIW2|Q@uRS1{w5&#rA6)!8DuA7XXy7w?L(N;?{e3+xRUI%x=;c zw{E>oc*S?HGfHNJCoFeH+(0pJgbL2Pm(t}*TPSO9o3F(;75q+(K=YTKw!($ehM3Aj((q~6K})W|9c(}bSDuDP+YH4x4~5};ZvHTG&6)e1&e zZ72&8jv&Qw~r}&=gP;Xe^MbL9rF2ZGNYQ?zYj*Gz<#5g-*aYirt_F$S7qE!MUAv+$oA$WI%OTrT@yYk z_ERG0{NN~X5&PuDQp~j|>Qzcmee#=#>aqK-&T&%82ZOBdI~~vJo65mDsz#yS99Eig zrpa9n@8cE3+B?bR5UOs+OL89N4;*|f^V#%4r;NeAaz!hX2fQODmBUd-pR)3TEu~5a zCw}Y?vN8L-vLU))YvH>$pi&W~1i6 zj>@TsHs~aJkTB{CLu;&+zpS##wbO#;uX`#?jV}?iG+i{5+$U5@uzqTpGVVv;lk>Gy zU!6~-bfh;O88#ayWF#dIbM+l;X|R#NCb8T@NAETo>Z4jh+|Q{~SoE;#Vb?t^jd5x? zUEAxC!PfIjxDe(`Fk?2FddW;6|5sNK1y{3LKoBgykM8Z!0_jyg9hbveWFCuP;&eK!Ol>gG|DkV;g=N?f~}mMh|a! zb9;Alm-CZehp)xhpfT}o3`_F@YZAuNJd<_tz-Z7x&-UMP&c-mZ(B!6tr%h?CHYXTvTE;mw5^OKzMxvqw1+SchD;{X zIqo^TdYJe#G`9&v_1c-@A0IQzAHQpnz{r!_-QN;hK>IheGeq@JQ*psIbT#isw{N9~ zj`rK-a`(TL3_5xIb&jC;uBQw7cyq&Fr^K_s{b?j(RUk8IYt5!d;@}KL4SB zJ*0cEIAUz5swLl4+54aQ%}R+zn%3fHR1+)TAj52&{|@`L`0{T73PCsGnQ*H@C+ zS3C&aTazAy`R(m?kc-I{I*awFN0r1FulghmSW;9&;+**W@hXEaQg?-|Wr~Z6AAfti z8K{j?nKOMD7N2pn6y7Zzh}rQ{N@r<@g-Wj6ex6Q$XBJ~~0Tn)$^n|UCzjYI+wC9~! zY*5frNjG3E;jNZd3y z+_asY=`_KPF@1ZR7P-;070Cz7W_KCzo9U@#PDmt!o3;b$`qZ|dm)FY|Fq(FD4EZ~I zk_gCYaQDGJW0cB8GhXc^EVdh^k|ynR-!_3&lXWGOP`j~2n?`StZgSbzdO9Os-{edL z{b7`=F1#%HIN%IbycX-PZCn1K<)P1*iqD!WY4CGsUq+wepR|eK7JYqXwlytad6(-! z^X&mWy)%-^1s#Tya*t!dX6uf^Cz!}Vc4uvnKk;y$ySRwJ(0f#BhpVSUJxaodkC@{* z*HEcc|N0T8>%6s8X$NA?wg_m`a#kh0Mn)&JlWZ%s-M8yed#{ozYFH~~kuwF{o*-f~ z+hZGNUT_+(NBsy*%X-j+S= zqGJCdaBkq<@{Eq>T57m!;@`9{~eIIhIi^B_-*6 zLCz<|O0z=xv!?8t=&z20It?E9`0$CWYcP)NFm`!+SPBoWG15fDHkfmtnwoItjbn`P z$;6lWH4^WH@t((<{}TVh;LM#9HL&$tGj+z8U8YNv>tuvFDanMZiM=X$!_oY>cyY>` z5LHX$cDZ)a!xsxEk0t_4UQ4ub`oeV5GL^@%`?+I?tixy5+pgnmn~)hcgl=BO(o)-c z`=zD!i(vn3^!AX`U~G!uWP1pfY3J67(>tfZ3rgFWB29q>1AGdIc%1Ak-5IBi4?Nk zX^(mk#A{NBE!ZbiY^8<+i%OfcV}>)jdAB@Y#|moc-AkKU$FDK#mU@eDmg!C+bv)WA zm9|qusl1czNTc4;-ck<3!hFiLB^Wq&pHyt9q^ zocJLFS*vgRr6VO2_nRwNT^bvfD?jU|#hIT|G4#JPPQ|9iS~VtP^hT#jx{nNowWuiP zmS8EVB@BfjANx3~$|J5ZspZ-d;t^-9mx@P|2Y>xg?pgG?yg=>4n0RR|m;`0S`DiUY zR)N-U+Nq@&x$oQR`8K!%ihACp!q_8Sv3gJc2D;}>!TzJ(W4AAw?We0eP3=4V=8o1E zo8vaeb&tEdU=BZX3D7zo=)_h0;?b|0cSlrF`+ZNr8OMQxGK?9Gs=SlZ?$*(Mr)I|G zzr4C#BOfI5BfzQt(Dy3?9Y;S%iK;pEjRIjO`)B_DN#8Zm3>0HMjy$vfM2E8M{rm;( z+QBEa#oq~SW*w5FCJ(U^;<2WhyN3)y!&lbE30&Lt_A8<7@bFCjL}Md)xKR3y%yp7J z0D~X#k*uU=9(TM@R>K}`8t?rbb)qCkQ_G{Z(0c9@!x!9{u3ZGzxv+RY|Dnvlq14jF z(xo!J5}$)2trjwuF9vpI)@5FgM1)PP@!3XTDL{)OnsIY)g#a=bVBmKB9(dsIL0`Qj z(52|Otf^j3wm`s!J%S}>*{**(_hS=ELl=^OeOO3N;aJ#^H@2oojNWzf+6Zjro+hO;jgpVg z-mk{QsTlf>xsOx6W%51RGS_iqtVUgiXZ61<&9Bcjc$YSpH$>X{)+>A5c}rM07wqF~ zR53Ps=Ev4xPxB)_tG-H~ykTX8xf`l`YGiJWKohYz+G z-%U9BI-$A+CHApw%E6W z;wGAQhrY85uF9rgaFWe$-OoA{B%!SGUHiC!=B(mt#7=^%g%qVxHt}siTkE+j zBh)63y9a7?hNQqXHw_{vxFlN}OVmudSM5A6jNS81+S12Ef2+&xum|k`IyE(^#S8Cx8s8aO8=F)# zLU#Q<8B|PT@k0sA?R>TEdt@byzqXnC$lz59i}C^+g&~BG zF@MZ&^w7EdDXu4nQN3f`!m#nGM<*$g5p~O(eq@a)GQ)$6&iuF)DOe{9c7{P>;sTw5 z+@`U1Hh6Ps?1as<&~`TCWiop;EzKdcJOQ|{x!25aChr9`Els^W;RbV_3$cJPHv8rR zKb5ycU+S4lON*qjH+rkKcWYA?Nmhl#k5vm8BfKBOyx#oc_lKk<`t+M#Dk>-E39(IimhxxP{ zvJI!GAc&h`F7M)GU-ZDH;i}89udzg*#i3mDhfn3NoVGcNlL@w>p$#T= zZH4KU%HyS`*fFdW(#AFtiB3O|LQNiC!UfS6Sl3@r6#am+1~RP{<9d0^aT@P@xx?Pf z+zoGE-9pqW9(%Z0N&d9SyD`x5(yP7SM>5^wq4Mxk&-?1Kx6T?2e2H`P>#Hg(7r#89 zeo8|8>vzStOsQM~m37Qm?tFP)C zDn^%IS9*FEODQb;=6dGW27~keR`zZ#3L238T3m1@My@OWmxqb)9zv0EN#RV9Xum)u z7gxE*zfFq&=gkdOS&#Gbh2>h+2w{?9v#Nx-YL!pa9jD`XRoM&QYX;5LOt5bVuHySW zb9E0@_2oY@lm2|O@Z&LWVnM#G%e;7y@q_=)Xv1-$F101yTsf2f2;D4rWgaI}?DoI` z?${6c$S+fcIwqQL4qi6smP=81to%L3(aEth*I0JNKc;s05r|8(spnT`97M|vn?)27 zYh0T}P82@3elT>f#VQvk6VlHQ=IWawTg%4Cj zhDUR-V^$Zul*e4!>P*gKg+d*O2s}_^2GM_QqQD-K98*aY_UX!cED zm6v;)1cI%!&|aD^es^o<(-yowwwHOFPs_lGrR_ms#_&o>%d#Q!y*T{!<=thnAapAL zP-haG$y?rvOx<%|*TL8aiI_dT!(K}jEP}EBzl;o6B?4NuV28223xWghH5^9+=w`T4 z2m?|Fu)cSO^0pY9h?iLqXroNBZ$0NgG>4nUsG};k3ja&^A#_SN!;1%04foYY_-7bb zuKVWfS1-rioFhv3XABT}2+muKEdd%+yFt1V;(^ZqB!N&njh^;g?jiWm#?}~-ND$a< z>u*x$Hv=O%7FR;4r!h^Q34T;nH>OJ^-apn*g{nd{(l+l1)NqFvkJXmjs#<|gf@{~4 z@|6xV^DJ$SQRTdT6|rEHVV0y;pv`l@*L3*nQCrZyTaoL_WK;`(;|>Px=UWss?_Iz0 z+pW088oXD{Tzw6VVS}_^_7KZdOamm{Sy*tF>J6gl^(6l=isGJ$;>XGODT1sqgZj?O|DU6D5;|yc!XE z@p;AXqJG*{4{|1Q=wvc`6kgnoTHF4T?f_` zpN0%X%6ZR~e4p#t#Q1z35R}aX+7EwvLuV-dWUOwI=ASp2Lhu>F>IU$b|n z`E0O9vTQg^91uBVR0@^6`NX4=U>4Cub0yOk*vK1vJ7fm%{!3uo8`<{aY%}*aPVN1^ zlZ@Ia{30Pv#zs9StLFBu@XbSA_K1qXx5Mcj^8f>HVpk#&esYh0?7u3LVbJYqmKgbF zFu^lCmc;smGhQ)Nqcq!+dS?hr<(;gVZ*DmauICsUy>$;ly4syhE?o24b8tdK8vSAq+DvzEy83Qk|Z|AWJ%3c;jsv}Ij z3G<8A62DRnDit5JC?0>Ae^~nKxxU&QY~SOY-<<7)g++hspA-kG#!XyWD^c%wqqEBa zY1tNWll$X^=K5cn`o(TXA2Fd9NFVrS8ug|4a@d<_`Ok5VrNveQir%O%6X}Zw6U(S@@Bt---Kv=tA?0U9l@iCGV1t z7UPGi9+p(K7b%+E-z>U&X2!I#;Q0~nP04ih%-I8(#>UPYXMJ(^wS$1(cp6k@8v!i~*ed~KOTum7G0uivA!VDKHl4L&&s!$1 z_reO2#?PJtJ3Z?YIkjhb8*BtV{6eENORw!-T6Q*{pC;gYEt96#dwanlhc`vLP3PY1 z$>c`xZr$vGh?o}#OcMZtZ=|IV+6~hHDM#D9 zAXdL3tUwndHZKxlqmI*8skt+f@PY)yS0G_&lZX36W4xM9j*Ph3X*ZqhH9dt34&3tu z0zA9r=jsW8=H+W1_u4uZ+x6QMb{7eLQy%3L8lzzqKo8g!Tu35~Vt;nncdNCIuKX+* zJ*OEo0d7X6Hhfi~lMdfjaL1U9VjQX{Tj!F)I_mCirEF!{U*RnixFLa+3tq~uaT-iS z6TOBAa}dgHY~;d(F?Cb?8^a&;s7ut-sB>y_;;p9$XpU}VO<8p7+xGQ4$xIvUUn+)% zm=62rO@w-?Q4Q6mhL~2{*3+pg_am*)&+&|{uCnV&n&9Huuc8@Ri{9y^s}*A|Kv5Y z*>E96@V2+9Pb?C)VHSUV15kJUq|G$_F${XfSNf}Ly3@RNk~!r^dc}|-#9xy%I>KW+ zUP^l0tHC2GZy#{g9ZYwcGfC|qkuLUf)(BGYZyyq$KUW^pkc2kIA-nx+_x2IyGp0={ zWad5L8LZE?Z)nsbqr`uFTN$%~D37P7wK4b=Yawph>!oL=$ zecSbR6SjOwr`MP7_0sElwzoh_6{Kw}_tN0Hbx6zFpl^3Z_HMB^U@3QnCPqxdVGp=4GQ+HIAT?2azmeQNgOxBer! z`eO64R&+(|=YjNUM1p$E+yZu$oM#mDqgtsisxNHF=da*b7{Brn=H|oKNPlfpIhq?IEVCdc>D1L=jiiS%u4Wd|kE=h`;A|iNae_)p zQzzK_+tk#Q$5bPnUFS)|b5(qZk}TdCHuW*JJQB7#*X@HaE;kCs{*}npKZ={wM; zOUSS(8Yes_+&Y#}hmZ3s3c=Lj{XT$2;RezGf(XSnt!QE^rjBH@5D>OD2|{p~QPiKs zRArWiti9fyA%kyvU-Z9xEZSdpUeD|bp^xu_T4so3W`C*ni#N&dR!i>pLe5Ny!C;UZ)r3iKJvabuUV4gJyUAJEE+u*Z7O*}TUPr)rD&$mf|PRbz@5uh_umzS z1&V)j_9$veG9i{e#5&LZqMXRcmzQ`aAN25mxuUqg^9^~5_}$n(^#^tbzZ!U}CH|xR z0vq{)utZ;&M1t|1v`q`xG+v~(93_IM%2jxx8?=d~T9wq4>vYWgcA!oxG5yUir*dbG zmYy2@(dy|}o|(CNL>r4ArnJItfIE!U@>Fk;*M?KXj!9h>RT}3&90}A(Pe1AsDTDXC zGvZ~$O#&B`r|{`e8WU%+(X-S8@p;c>I;cjW%hb*#pjM{f-ed3tBpdS7HW_Y2o9%l& zWYDx=c-rn=7ToIv9X1O-pog;#9}5bSfDW>)faE}in+Ca+y#p{kq(n$Nr!kGE)6&w= zsU(3%Pp^Pv4?z@=r>4O(2P++Ck?@+&yaGnam9&)hy}OM)4MGMyYu0IF=&fwQ)N*HX z0M3JC^PXy z@Ep7GN?ZsGri5*9AaSWY9E#^faQ5_!OY-?G_EB{e!H*%2dBUEsU>ig5H-$LGi0iDk zcuOqMwp5w(t0LxS7K~!du&WQbh#GPwWph4GGfA$ES(FYKFSI~`tKO+CVa@-bX-lepZr-n3X-fpWEDPll`}k(i6^ zlS%VLp#Dm+E13y4$XC7e}K$ z+_W;+Tw@ZW2U-=Ls($B6rE6B{98Y&VC@v8sV|CcXASy6jr#qnfkip@OMQP9ep7q;IG=Gmnq!Z1AJ82cO2KQN z_c-jNkk61xmkqk;?0@ic$I1jwCfkglpU?44^ zhJ&oxv`5c9zr*khOWJ=zx5{f?Tlsfe_j;KRnZe3~8ClVPZ4H)AVomGu$A~%)O`0ma z4)>2A?)y&;=B$;hK?#i+2lOX&bPUHXwKK!xf|ECD+gUWeLs}^PB|l|}SIejk`&dbM zsUMtz3(nkt-QiX^6Pg=njF;?behNJvMj?+~m{DL(0f3#eW4z6Q!O2rV&JqaHykLw6 zyPM7d`IX6;W<%y2kU(-OY1)I%gPj@{3)*^5JAf1syyf0y`tmM^&la$ECitGaFjeu> z!0AJWpAJ7A&=g5rUu+9r8~*Wejq!}JH61d6O5K`Fp=KxZ?XIK+^F)4G1h5{!d82hg zyc?^Pb%Ho^h$t~C`cC?5ei7wcixtjJuOjRsz%bXUD)Y_?rR0p1#om8b+t$!{Va%i6 zB5rMpnnKdq^v7AE8C+vLWS*S2>$6TzjEv|q_K*$@qp}NOkxF6tA6i|Xm}e)mwNG8s zuw@Xr8s;q=O}pq#l|!Xu0$Llaf@3!34esS}LEYhF!_9TA!z5;xUk^^{6Yg*z;RK<+ z_$OZ;X`fy>i8#4P44f)mY}aZ{slJDIt|R7u#QW>v@;ttR?FZ<=Nja8DuddcAKYEbz&AFB@e^@MlLQR1%=5DI7DXlAn@Ub48oIJ3iGyd3VUNjM z<>vjLw3Y2*Zl~=p_oaGp9_>)_(axSsL+=eIp}g#c^@S-S4n1tjK^o)6&RQcq<&?Wc z!-QIjtx5~SlGI}#Vn?fw76mtd{?P&H?sp$reDLlot%D^7^;rFq>d(I`-oNNHC*h~@ z(t~TB)`@iXe9=A;@roY`kAAozAf_hRgH5Kv`=u{)BDS{yLrI~*z%Q8MH9|5S2V=i6 z$UBe|WN@;89P)sS6L%*r%~Fuf*zV;_0UWhV?-iu+r$GlWCK*9Q3i4efWBM{Y{f!st z%bgwqawLt>1kn$+fDd}^^t9J9kB1XLZUmgb2?6^iV63*S@YJ`1%^K2)TY??yP8!C8 zyaVDW>@LQgpSd{)E9dfUvYWLK&UjYUntMO>kh+t+JJH+y>PZCiNHToq7W%5KAE!>3l~ zVF_O%PLoW$nOy9%0aoXq}+|QVv%2#Z_5aE z=C^7`RrUqHi5JZmr-ZnO$b7YoI{bYxzhhstu$jzdab@E#72~d{vv(J))exjhPJkb8U%%cSfK|L)uDoENJbnro2d|V(eZT~{R`R!$zi;N zdMw~UUrzHqIzLgLKvHOFo`Z}-pfENlduxW_fbqUtr;6{stSWz7n`1f>BVlj_HF7Dh z`36(8~l&t&`2CsX-J%|F^8qjZW`K@D&cHnZ0jHb zUfaf34reE9O=tZ~;4l_Jj8g#y&NAp*gD(M{u>fUXQ)83*fZMPyu$eb&sF3?hsN;t0 z7=99PvJ4T8%(_L+G#br83oHz$(Y~8om?p2SEd6c4G7pyo2nGp;mkSVxWiwxoftk(; zUFvCT3MD`YeW^Xe1{kIu-kzn*P2xsyGvRvTErEzcgI~1K10W!qx%|Ho<`W&>B8`T~ zI6zB2-n&=1h&Lg&_~;=@JDjcMbz*#(a}q7+@C2{gy>2k>BzWH4TnD?Tshsj0b&kiK zwA7xq>y3tt8c>K&RF*S@ZqQHU7a!OWPYK7@CIM$!?4pUvA!$~VZa8n}35fUwkd{Tb7%eT=m>fFm+!7_$^&mfp<2 zYaUhNtVE!iBv+@_s(1exoQgS5ap1nTXbE_J}r@i{eITb z{^y5e++CXy+7}?gbBdY_4=FUJTETJxVWv_NmY-jtqoes+{=t9MXRgV7-&QR)NG%$D zQ?BKQXqKGKcX<<{@6vp#wy4-(CQ$P0J1wk)Tvy7Ov&vUSG<#$WGT^-wGl>n?_Ido9 z^Ld%l0VnwcZLQ1=qVS@pEXDTzdfw6f1Hw#c@p%WA_(NYa^NPgkl;1zg|E41QF8`wH zl+*9x^Ga1UD5qLU1CpqYV&doW^SxsEE`yKZ%rq5~7X@kwhAy{+U8E^KBT-AY6O`UR z%3aVDRddU0KAfmIYb$j(&^f_cVNS)MTVxek9@F(pSlx=2LRE9F+>rqV*}#LJRWK%{ za0c~kew`Wd!J=e(bpIv!^M<##^oP0XR30?THI8=%b9GZGBXD6-rP?R6G{?Ql6Fi?T z+a`G$jJcX729Ke<q(XKYga9yyD;k`kBo$xwgw~=iG#4LR)GAjY1<1i4B61MR$ z{%rRwkrH@|d+7>V3J9zun_hZjLeq3M5H^%@X32Q{3!t$VqQQpf%V|g3p>Jc&#r)1b-;a%lLFR5!a_2W=SPop z07xW-2Hjq)KVgcSW?S>NvO)L-+8WJx&1|4li^L0eNgJHdoIR$uFnP4X^~XJPYoeAU zTH6R|Tv^`F-#4u5%C)!<&c5O0vE4)8f|i-R)CSkHn+!(#HR=-&uIDpI=dbc=l5CUg zbtCQc=)E32i|rGzLHH3fS5{W|8Y8fIp(Fu0W}KkFyO?46aIt=5RV`ZsC1XEt4Oml= z<|xDUe=ilq|9J}jU?|kPZ=tDv_uwCr!!Bo99~H>FRsA=^xqFdtjkq?AY;xaR?BTBW zEKJz`$y>t1kcp17x5SoTw6XO0R6*bBsh68 zrM6z`2VL^6t;s z*)hX!;z9xRc)2z`IYr>r_qLx7XlUV%L4!^b)S5t>P8wuP6FBfOH$nX5Gjhd1)_6d+YxMrb0i@iZ_{KVVt^k|<>gE}nm4?Mv+ zXoOri`%^F68rSrumt z2g)g+H<%YcnbIamO9OQG{#Dkez0^?;wWqg@^=D;U7Asq5SzGMgz8g|M3W?tnhPot< z=AM<;e0B(=K>q4FgFrs%oAw|<)l-)R|w4+6@t2pOr;aUBA zXVsQFCLz$XaXRY?@&&JPl0^w}Ka12Kb=<1g9bU}&eP%4UnUcuRcN;-k!`~a`W+my$ zPC|k;_IGhM)BY4krDWxIA5+6O>yb)s(f+ZdA*AAmthYtBz9rqwKgjaU6kpVt-wkfh zr73oxU5_%7QeYFnDgTVOk*HvXx#N=3Ec#mxJ+(c*C2V}qO1D~0^rMa})-mJFt5e6< z@(7|0x=-l^e(Fze_TN+PuNL(ad!^^?J3u?2XdPAUjFP%L4DhPcfr41&E-ht~f8<>| z`1sVUp5$ZmFrrmwKY1t_<#wJIZX}zO z_zHKlsxZGn>1l$~k;3rvZ&WDxlz^}U(1cD1TlIzNwGcoU0Ch-aBX8l5oTM~}=XMwq z{G%JzP-j(;XiMArL5eX^^PXtF)r^l^wU*|Cf16YcM>kNRZzqWaHd5EIS-Zhfm-!^^ z!O>Pj$L2hggr9Df4NhB`89q*f034D#=|O{09EnR?hoYyw%N5OLPh^2Zyxks`1$80< zkG)tNM71rh^)7SPcboVOaD0u)Qv&dLBVmu_Kuk`My#tSL63p<^o=t$58bje7E^RH9 zmI5N|G6!UvrnFD@z&_~R24vF{iWJhkmIYuvaQL2$4uah}(0{h~#wFt|e)7grJ@Q2l zX_|dH0I*mNn?GJE*amM64dfrhC6K+Imv85$9`ZJ7&X*3F-+$GyZ=o#8pt(-8%_>Sb zUo%lk+}Bb5l$4NE^be6=6uuww7DAXEQn#UYwvS>KdwB`Zo9>Zr^#X}VS|{ML*oGll ztjsk`q~>G<$L>?x1}A7U&@)oQsJg=4OSP(!SaAX4uVbj0WKl{1BN*@6VSxbl3A+xt z`TEf$wQNn)G5hMDAa`cL_lh)8QdPtv?Y*DQM$|xP8=VYhqoF5Gz(->mGprwx(OG)c zWwHABIC}gz)t|a`&JqEibjB+xnRKz1*cgMo+gqN_x>k?uUW7jiHun1EdFsPCRkw@q zS46s^fm|Y8B7lb+5PpeRm;a&QV&~yA(g@ANMW1bshxPXg8%orP=t;1o#ZO12I(-uQ zXVvWv;b%ftPT|tZN4`6K@pDv?z5C|6&;>`m^F2aB_uU3+WP%R`}k}_~Sj&-cgy@F7#&ODd5t9gMHqcOs${yvLW>{61Fco2zw z9Qo!nTH`!baTMv|kFj@tHcktWb-VFi zs20>iD1=e3ZCmx7L|WTjsv$fZ^MX{*WJtgZU`Z`g+ZlBvLAGZdOyzXQsBq9(HZ))2 zN1890zhlVQt%C%cF)16G(6-?s1=+?6fH_E4^CMob?_IW#$mfQ3f|v>P1Umr;ht~=4 zfM13?Yl^om0Au-O1g|MOdz!u8OK)1Il0Z=mr~^GP_~8P^LWzMsRpE}NvC_6l*1Q`` z?&Nr9Dl`T2Zq^W_FN^&chy#@gQbU-!1z;cSUHy(eukX9+oZxEcD4NcSIZ9bJ7xJwi z>Hlr`QO@xNjJ+syS@<>R&8Z;9YX}*xqk?Yn#d16 zJp!LVg|5$ZkD5DUF7K>oO+8)FmiqEfj@ANDR?oZ%wNRHrrbCM&7t#hh>PUuzcj8FC z#498{^2D?TJ1&_$sn39ZoEz)`n)K*@qQ5%ax1HDSN=ZnUpqR~c1t*4(|QWU2dwOj|e>LZEtf>%mUdrM9D(qB{h z&q>&xoaq%27X4{*{+f{OkaE3LP)h$ZQQJC6ac}oYkyQodm;tZD14;H_c7tEbYD<%_ z`3Z4J{zc}iJcBgz_wkP1`FY2hO_6PyuQqPuGg1i|kc8nTv;G8XI-G9@S1@z_Nhs-4 z35z-lGfabvN5$Bfm#*!RBwp1qlS8syh3mq)GR5V^RwNm#-<-{aiiJT z^mzf%K=a=gnh}fmKt%QGVw>@dRvuHNLre2zGV4QmX2aTK3jKKxfjbEl=td9eV*50t z0OAb~f5~7YHESCqWN+XJ?9*N-~Tju^nLYCTAH#iJ<^w8%uEhwtSa+ zVfKfV8+;re#Qf!5+zxbkfPZWZ34keRXz0Irh6ib zu?+nU9zBA4n@^xvw0&n)#N8* zURoF-Zawn~VbPxx0z)Fv=O$1o`1myRg!ZH+JhtcK!2UHm>pM)Di0*JcvG;xVU1!)) zsebiIEgC4PN@A9dUe!Djm?8VQ&9JNyR)<6V1d0mJigj9w0RK{uvipKN$@<8_Zjf&5 zWGsx6(W@=`8s@E-=lI(7w#=PNgqp>Q(IJCBjOSENPnx$fUIxuK#y%?fkaDdC#~-hx zjaSz$5ve3rW9;Tq6NIrhId_ELhC^L4O z0NDZsJZl0Re2+UAYiv(#TN83&QhL-3a<7L)kn*4R6My+49A&c0F*96pP7IT%}o%JWDBOVcTTfyU>6iHpw}!r3sTCkMN&InvteNWU%;8S zySG=uCFiuF?It>&5b|H+xQA9dlP9+%Q6>Zl`A1dE03VzsCkZ}1)X|~eP z%8Ikio{E*xVrRsHH6VhMHnNs{4m~;%?6c8>2WE+P1>dc3F8Q2>uK9HY^(a2$VT}Xx z@+S=qON9MiSzHE&G^sqHp^i6;@H|PnRCZ~p#58$qu*C)^|KNkh(Xv>cC!#I0?&9YB zwYK<|*ud1%PRbLSYv)|Jp|%I{VjVHt(}8S6db<}*WxPZ#ZF129$+3s8B9Z$THGxNQ z!n)=bMkz%{6k3krEztqb)+>K(c05ZWcaC~l80oVvTB}r?^*L=+`d~K9cASv@Ajr#0 z-_~&?M!dt%^=x&%eoW%Ml9eOoF<7tK(g!3p9JF$hVxis3Dn5CP9lLY~-R4QqNYF?x zt}IxGzD$VkU+rIz{2^s^#o0pE+w8nqZiP7ONzt!PeawP&Mdj8bCJacKzW?o;&i_@3yxH07N{dW55A-;0tD6nWW=1M@3-UBBr%$0*+a0SZ>2I`JI!w8vbUG2sQ* z0Wmvj)-0o7Xmu|Y_ZEk|0h2a#hud22kFU)9t?s;Y^O6h>YC$1k`KCs+}>yjr5AnOH-GYr z#(6I>9sT3u#oq&ls!nMN6}~g%y+j#&Ot6f2&E(kWQ@5c9;5Ae$F`n;rC+*>uDLqhz zau#M@9+q)b_9UYs{;^GnPsYGN zA2+v%j|aR4-?b;4yv6&2Z#oQ-x|TiE>D*zAvtOn5$Os_+TYK^2JyR9qTysR@EoC0v_7<5 zXA5+8d|hpYM{P~LA_+CA3;JQ#%9tTyEG%5{5qQD&i&U$3b!+TP^bB7JfJfLIKBp88 zds5JRydks%L6LKQyzy;Gb(3nRak)hE^Q$t&8961z>E`!en=Pro+SMqKEIH$v^-kVbY4sf;5H2?7TnY$n3R_|t zEd*%J{2d0sE-YEop4(geNE(a178&s}1so0BSXjLd)uwj7MRz3OW-YPqak9(0ww$oY zA*s8^R*ngs=|51|7H^p~s1t8+=IaB*iyF|tjhaf5-ACR$zj(5|?|{`uboFgj1SO?U z*g#ZGD(LLYmUEZ=k(~s+@lqd3F%H?4zq*toZA(>V56c}o67amKjSnra{^psFTWd^Z zg$b8gb*`whtKN%u&aV2ng2HP_t|6GTHLN&i*xX^!`otuGmdV&+Lz7VhfKxw&{$kYn z!XX-%hKr4QB&)3~<1hQp?Kr?302eGjO-OZbT!wEX1p-fVlg3XJehEiuAbHSgl-zzL zl?u6G=9hgh;3$pNa8^(I(5Fyb2KnAw z9>H!y%Oheu@ce!Z1xK-rhl}MCn&#+wP4{;U1?THEkBV2I{0L1jnzQ@9e;O>OF+P(U z#2$WtLa590vX|*Xis;*z11u3!XXmOziumePqM=F~6|3^UQ#GXBMe>AWd4RAKd2&?dEyH@C$4`)EKEjR;;Yo<6H%!Cm!fL^l8?L9n<@V8 zXS-#WHI>{Q&rTpNf6UDP5X*qdZ8%h;r>$-^=?mZ1a&WOQ4?Q3yd}seYAE%O#IV!CQ;;luszR#_Hv-Cxw;U4c^VF`u}HYlP|d{@#DEpy6O~lJj}#ySh~n`G0Wg z_CHK=Zyq%8cQKV4b=M4({;d6;D;;$4tg^q;?}tq2Geat%>C3^QfkFy7L8IyWq5Spm zf$P;rB}Q4UlZrD;=mhjhIidCSlFK)vFG{CAE55flE)gVUVEpA@Ia+gbrcY4T$zH21 zi0VVxX`yAX_)#;3>^xCgWE3; z`I~-&VsR%;7x8cMR+*Kh#0!{O=Gv8X_S(`sVsAZm{?g^4SEkh8Jl}sD&7^LFUhO zAfrKr0g-=?3WZn4^fcIfkbo{4U_CZO|~wq-)E|5K3c0V<4BY&0%K?xU4^UFQADd;x!8l+?S!?9(G2& z`U%_i6}u}Qd+*Cv{D@mO+mTi5!V`uM!n2vRYfx_Fw-$a0e`7NA-Y%l#{id$iuSQzG z+PI(72&RVX)!VkdT_m;+IEd89e%68h7XO%fZYYtuR6~n|jyN28XF4Ph42MMs{L{u< ziHmobk@~@VO-XQ|NIfK$p`6z6XY0>3jC1&DuL(rABCPQKqpXfV)T2dWhePEY6kP-k zTtE6W)Gewf-&BgC;O+K>ktZq+jYCs8jfKBzJ&o+ka+DU|A0}KR7ABgn@L2x4%@>=+ zvirB3rNq?KpISZ;%4y1N7ynhpO0(*70Sj^ekAf2y{aQ!?S1O4ewU|1NC1`AGUZ}K) z*FR&_iPVR6WectfaHo@PlZ+|v=r>#!?*xSLZLhoPGxaBxC-u$gXsE%pI9fimUa^uL zY?v8??D}i&d3_S+ZRaOwsG4en3g^u4?S(&$ADk*o2|o~P*Y{VRy2!r{KaGCVrjfWT z^a;+>_*=Mg-)V6*$;T?+1K#F0t0lVK|C}q9Cn|hU{MUm9(dXo37xVrl;b(gPwo3>^ zke?zSM2cqSPrbRLT;=@bOut%QuGp`#aLLCwGu4R~i`0ywZud8dE2=*=(cFLgv=p4L zDVaI|ABU{WG-nSdQCw#;Pt6{%kt~j$m?EdN^Pw^?+>pQCy9Kxua>s@~tER5rTNDbFz-tY?M1OtxBc(L5RlTF_eL~yb;fM63uf++y2?>20^ zpeD>2oC$pd2bmCOP4lRtl9)Gf5ep2S=ayFr zk)NiRaEz;c=sk<@qH=x<``4qKB7(o6al7sWqmGcCUQ~nB-64u}Uj}sLmK)vWCIj z>Bw3m6p=coL6LpSrO_Fkb7zj7=BG2ta>o+>?>Ucur^o4Zl!vDIem?Kz^?JVKAABD+ zuQ`@WeZ1pxsea*8nVD+g+*Tn{v0radlU6IUIMi?cp?+t;*!K8!WiK2lxt|`Bf=XoD zFx#F|KZgHaiBov_`EtLxQFVUzN4Zdrbhfy%uF(O1mVgl7ovepf(9cAzHKk>~MWfv% z6SSlg?ECMZQ1@)gy6W;IIuK$t@((LV_M}PtWk8l3LPS?(F1Q!GPk3LSUaV^LDmx+T z{wI~_;vQ862QS^T&63%Uno{EC2@s(go$L4|DGzZ++~0aOM(Bh2W7@5ZXBwexlA=|z zp#oW7jOMm7(jE)qzxnn3ac=GD3MM(K!ZHrjo6UiMkBw!(NF00#JKQO@U0jnscBJjO z8H;O^xX6G+jkp4|g1D&SHwo8H#FO)bj1qT~ zYZYk48nruaCEwrdy^HfUpY=c~{F-@LGh9FPm4d^?xi597w;P28n@y1ceqc}q8Im=9 zi2^v)cp#fz;7L3881M+3g1rPZMJu>fXv?7(P6nwt3woD!)~VgD&V2wj|3ii4>4#43 z!_;*rh{c>DZa@RI2TBk&FwA*EyaU+WK{Iq6o+`JFo#1JYCg$(^O9#zd)3I3mlqnR8G!S@yY9UZKsa4#i3$L^i#e0vtnkSiLmHN@;NH%iJ9{h(P~ zXDro5P^>fLTQa(11T0NWGPT$y^agc;!XvWDvmp=uJ%QuK4;Tvz*W>dUk&jEugK)rH z@95kHv)cua3&iP*+uKVNgWID8UT*@HmX|pANk)*)I`dY^`XrU>vE%E*AQv`%s?}|) zBNJ+|^XERlFf}YIfw9wfpkN#wxPFxCV}hI=6V^UG^Y#I{NB4bQCBF(^cPc4Ws3`IM z*VNp5!l}oqFQau18Hjz*Xi+!Hw11kXA?m7O#J4Xl$)}UroPJ;QHA+KBP`}1cTIMHH zl|pL7)%_Bp=@_N~7gd!qb`jF7+UYn*b$B&k+EGFcmYEkH)ZCtx;!In5Lwx}lpl_QbvfrYfV!AJxPf56qE~2ZL#W0}l=#+} z8l%+kvrUqs*Sd2A3)HJC?78XxKJ=F`RqQHR?y<1YSCJ3a-G3h%5LR=!XCb!b9*VsG z)+2T0zPuTthF?phPqn4?zMzj3MieojKa*ge4Bni4{>M;RRlm&f$&$D0W(nsX(X*PN zrH=@QD}8Ir{F)RfrhU4a*z;7sdzt^R{H&i?+mmxx^^l79);;cme+ei``g@(tQ|i;v z#|EJ)4yO04mSFyV6O@-A<$+}vY|ElRfdFR5ww=umDt9ncDwP8F&37PjV*1~x=Decg zOI<-jvKKZNo^;JeYtGtF;3!(hx%%&HJ&7#||CIFqQ~Q)Q$k9X@7w68ChUZQ%EDpk` zUh^Mf{k#OQln0Kn=usdIdP>$Z@hY>>!RWQafL)DZO@o(r@X88QCZO=(1YN(tjfFu$MM%A7n7%mSWbzrv%bfQr{K6jKaKPr!bRE_iaJ99q5Sc$ZcjC%`E}#t(Dg3QSc)^w) zjH#Oev6Dd6$NcjX(=f>TTf78P)tC|A*B$e!l_e~udt-ZF-hyVsRwk*DJg}7REBvW+ z(p`CjnluHA$XuC2YK?)1y!5+8Q)OF3>(euetz@qU7DDbJ#lRFh$QU$l(C zoXCl5#-uF&7iYFc=qqJgh7LuRgL-zclUm3<0^ZwY-cnlXKRT7F_U)$goQ+FyLDVm~9p&N=f3D)b5{NsD~(?^#mttN^@hJ#MHk z2T~FPv<%Ru>TorI#1?9M!!pu;n^>cb4n4U-jrrGJ{t>-Un1^7Xjqfj*g!U>s>1~m~ z0OxS2_545`rMKW39qBPJJ8qqHt(B@ZcQj#ZTTGdl6JtAn?)%U)U3a;#GhDTt=&FJN z8EnEr6teHj*HHP(L*uHHW3;VpTW=p&JYhApA>&ctnr^p7`oPKiaoWG;NUInX+rLfQ zF%zN+|9mn;#}piuQY1Y6{<|StpD% z#y}}#P&0hwoSeA1L+G&N)pO`pCGO$R0eyn%ACJgH*X4XaEl)Zm{-x*C1N7r#kB`bv zjOW#ai9ZZQ-FVlGE|G2Qr+yA(%SXAL)FzyLQPyNu|aoByejBbj71=~ykc&` zaBdXvXg1@-J!WU=wl~1Js0}J!OXM*|!eaRH;$Zm94=}VupV4SbMTUsGwvyJ}O@1$n z#3yUPlSU3W?m}A7B?lZGq3t+J|1Dyo6}@nO8b}FmkQl-OvXBgV(zm}upvK4H<;Hct z%3qhu5}z|7*{FpD?dPW`m?SP(ooeqifo+KAgJS`BX_{rl*{z%AX}eigtDOhC`&|cd zU0tvuheQlIl!TxY2h|5Vz74KAxSBv6JMGcIhKG^Mx*ZIALv}3Gv%HVD2k)VvyW>K^ z*LASD!n(Tz8^!BoptE8{Krdtig07`1_>$V-f&xlmd=tbKRjxqx;GpIZ0qr4Pe*qd6 zpXnPbSC`tbL*<)1Adm2**`~ z*AiARs|BMaouvQ5ED}^tT>9N>U~O$D1nB^t6m8|o3UAZfHh^C60{vAmkW0#fEXPcN z@Y)Ji54cFNL%y#7iZ)uf=v4~h#zvUc#XNctb6N9QUxWE6gi*S%L3+d?1M$z6)&B3r zUHR!sYtsJqlKzm?`QB7R0B@-w+ahp_FDX^nGuWvmyvIz4>5(>f%t#N485$l?D{Hc~ zUVf0Qfh(W(Xln=4Dg61j=N=43F`pxCPjpRh5X3Bah4@HRn ze6%R}y?O}a-eKWJYZtJc=8E8hy4i=K5Fx{v8IYZ!75bOR55;fx)z&kM>AwlI^3NT1 z4Rlzr?b9}_zJE4dXaJG(-p2m27PZ|m|Bx)+2KhJH+N@Hd`yU$7JF;!bsK3WP2 zUvrPevQsuXVZQ;T$r7xYA+Q2aVyo>?UUUIL5a?bj@ypcB%Bx;|C#1g!Tl*o9)*0b>Coj|d{+MS&-5)@__eKw2%IWH<01N zboT&y+J|)@JR`|g9LnAXjGy5Koops1-{Alyd6n~q3y%}8^?3(kS)jGU6oGM_*97rk zWz#(D?5`b7n;k1eH@L8-0XGX?Y|fjwm4FwoA2p4Ee5*odY^1Wp9cgCL9*Y_LLDn}Z zXKBxSLplL=Z)FR#?=ExDxU6iRjmF=$@Uu$9Oqb2s3w&2z1mEX(i zFOJr~Bqj2*Nl0xPy!C<<-Rh3k^^?{JGB+BY%oB{BQl+JUAWxWrr&_j(NaY3 zwE8JxSZkbjkx77;$&6^ySHF<7QRWQkeU-M}VF<$q17XJXJa?5*f$ak^di-S7>{L7q}KNf1{0Q&%v zQ=M}eLh`0HIvI4nYZ;nC;i8gK=I47qC`XGYgm(L=q&D3%mJsta3=I`EL26v>NfY<~ zpeQSixa?F67^emmYPi-?QJIexn-?E3$z$@axRtbf$j`bRQOMz{EB9G{qUAoo-jBmQ z=sy?dFgzIkSQmTstNl1?PPl}LcEZzgib-Ud)3Wn(4^ZCj4_pTx|72^FyXXLkS=3Ch z^|@yLOfa)vo2SSxI&)5e*}XKj?Fs4o!w&@THw}-grQgCGN|VhF={}R0qgtf^4LycaWUMREDgqKdR<06x2_O_Icy@GT!$UmUZPHpY_B$5X}UaG=2o0H=Y9O!F;66dg0fKI8S^5a zu+>d6b$rpSaxQD|-IPSVQbZOPbqFowms9EsifB$MuV-ns65@E#8gF!Xvcpk|H60oO zUVvX>?K+rn5VNsd3yf&c3_=tjpl`?Gc3~U1cN2i)?0C3U;y5eGP)_etKx7Z& zf`bi+%6Q#AoeWEO(9?2OJ1Kw#M_z?!w4MztD2kEw8U3WKVJxxtG-Xc%R zC%kTZZqV9*w61_xx9vPDq7o*I)F160qtrcCV$9ki)Y!$bzLA#0F2nc=Lz61hD>~=) zxWS#KE49P))w0$GEY}S(GK-JtU}-zpc{bugY-}u#3n+ztS!rq=;Qu@inM3dl>M@2a z=!Bm_ezDil_}8}+VJ&t*>{}8p&zH!CLOc^M?#~q8@SEY>2^^*@X{t2Y-x4h34 zWoZZ3;taq6ILKN0JmJ&}vSZ73lSKJF%96MlI(ntYOD`PfZ zR4rfZ>P=_qI0vkf(N74m9HXP2fjvmk6a4feR}B!g{+89`vv>HnML!*rb`FU;+bnbg z0(LTQ2=^%%xm2512LcF%axYn^QGdQ@Rm?h&Z#ILWsZnIVka1t|HQ=<+&(x&f!=dIa z{6kjnOtqbcw#MRpc|}6Z4Q=TQ_MAM#+z~z17)#}Ek1|i_d|RX;J?paM6~9|5MEmvx zO6cJk*SPeZJxB=iVgmst3hEXvgGZ}`-UE2Zz?5m{#zRBS07vE3`1=d8TY`;dHF_Bv z$-_?I3$Vnvgo(D}tdQb&s%pm!tV|-%#}+PGM+hceGsv6aHxben{2}*H92>0KX4zY5 zCO`iY+Zta0PefRNWuMJxn_=ZVx%mRC@8$+5vap4@;C?`Yq}RRp!8l;M-11=aWL#-w z9U8z>&_K+_Fcd}b7Oz6>SAn97rH}kE?ZzG(8)c7S&$Hw9QvjlNu$=-9r0HQOzb0Yo zfr7^YdLo7gATmL}MMM&z2cp2rY2n3+&D3sVJNySs<5|2Qw~BGXN?;RXxoM5_wxgZ? zD;W2YfYqW9iAli+lSR~L00jVUB!Hv}!4kcZPxt<43mM}8k}CT(M?SZMR>KPC1nrI4 zTppJpT5oohtpp@}Jg)yO^8N4k#u26?yVNyVqcd!Eb%n`FKHtGhM~zaaQ}sdw`z&f= zl=|4wj7GZAfeJ4WgZ^YD1A^MWY0moaIoLben`?Rgt!SS`MkqL0sHQ!ZCwMJr@2=qH z#hxzJGACVt6!O-{s9NW~YhGiPe_VTv z&yUz`cSj-=FfQC<1p}s8w?`YD2IhW34r7MLl!yD*X8(68XDvoS=U5eN{W8|PW;~te zwqwxHrykmeT@!R0!AL%(+CieJW9y{+B*|^+ zWhVm6g=ruBRC0=q@fmg_4_x{Zz2stDy__Q(j)Lr33Ohv!SP}o?{l1FkJ?to7u6b^g zl=ho;@4w}gZ}Rq&y}o8LGBl^l1E*u!;^#^0ZiPjpGC48*TE|3cnDG&n99iiavh1bL z(aw*5cFp+Vl9ed-!ooV}SZ8r(J|UJ1CZj~=NgKxk3aT-BiC2^n*??5$GEzh~ zvl;ideXfH-j^>9~AAiyk*4@mfts!^=^b!qTaxM%sX|u4v6L)s zs04~IQ{&}fg`g<%;+WdP;y5O*X&eZ)m9NHZu8%IRGhkixijfGr8XFyYaA4yk*a-mu z_$C>YH$c+qfDw2n_^I}Hg2$4%EL-*gl!2g_2R3>#V4-+)k%L`eywP`e4|q)wXD1Ho zMi!4tNF=5J^Bqne=(eWaKFAj~3H1Gqn@H(Tk(tFKwPrAg3yin_+ zujJBil^g4RY*|mp@GJS zf}W=`dWJJZw`_B*BAJPEA%CO|$QQl8A9q^gr|Q|XKtW%jd=aYTL&KW?naU&X@Uk|f z9)3L;vYKl8-nP3*`1Q3jQhG3v7JnGy{MgA3#NzZ)_9Vzo0HzLF6j0MZG#NBSU~OgY zL2y{G%_voW?z65D+11%Ne9d_1*&Ayg>nVl7NR(kaGay;>^Uh#@tNa7U>`&#^-5q=) zGa{dJn++D38Mln`!=EEj`BXKpWJ zOe=}0|9Yhe@?3pkk$5?gVjS@jlLp1w?dj-pA9oXZ%{R^W?leLO7YMFq+j z?7h*&4Q6Jz(mJ=5v2i;BB=L#|9CdU@U=EunXJvo#$PYU`Tc_;AmA|B`?|fEKNr(SOTk%26L+$}jY_Lh9_(T0jbDVkjS2-! zFX8ws2dfaHq649~G9^yuaEOTf>oO(G@+ey~EfGPDiIi@xOwm{dLF9*~k@3uBw3~qM z5m60knUANnLhlJ$%IL{?dCsL@lKUtmuJ=iLyLvf8R`0Cg{SDC}T+hCY=ri%`pJq`R z!YA&3va`49X+1hIpH`+2tyNPeD({5u;l*wEYZd)*-_b|lsMq&ugDcK?%A|HQ#Db^uIQcx}&hAcFJG z-1vXQaP3~?x^Xki5*ul%kG#0>9AnO`(0a))ivZ=Zt`U0X%rc3Z9L823B0#@mrsY2d zHfdh?1GuEa!)7hS92Grd9dDRNQQQ{h=id-7zCo32QrF#<%W#oNoo|8saKXc518$Gt zi+A6y!<_<#K^7gloLx`}fJ16}Z*M)tln$2%d!w_0n*z@g4B8F=qOc1IokYOPfQP&S zsyG`5WxI&_RqFX05NZgy|PgE;_7LJZZzu72m_h68BVuLK1Qz?uJTiCr@ z(>X!o%)QY<3Sq_0dVO=XDe4UzQmCb70&UxT9|ZHYD4!8(GWZC2qT@xcDBIQsL(m;V z^_D-T=pnPBFp~QmteVV|p-sYS`g8aItpVDUN||%9wVywbIs?S0k?NSX&g-!4N(N=% zK0En3gkzB_6C5?ZpxBaVKLd#ythA9s1tQu?o?#5JA17zjq{Ey<)jfslkrTS1 zg-1NSAadiVNRo+`ky;C%>@g!$^+48;SxvL}m&4|A-!LJ!A#&rihIWWs?{US}njq1) zs1S?U7^!dS-{gJLuSxuI-oaTg{D}Eah=#*o)FwP;db6$uq0JD7o~pKJl+MQM2+1lM z-S7RkLbS&W>;BL3(VL2WRj`l(G0M9;ov^M4kUj{&0DR1wpRtgTJPy{w#W55~Q;ig- zBwb{mj-^_S=E{u)SePdi3|^12l?W^3B8RHTi|3u~8=OBs9$4VG^+dL= zxP3HVdzNu3H}?Bb4?Ym7)*N6FgR)Si0MBZ6KT$s>pSDnbN@p;9_6-sleSO@fS7ghR z*q2DX?C|yD8Vqs_v|R>jooijO;D+%|%EK#%f-QvvLe~FUeLh?^#i;%F@wSr$^R|tB@II6kPhW^8;0K zJ{rh<$J>u&uNkdd|D1I-tFRJ_Mu809d_`WX<-1JFG8k{HdpO4~C6#KOQL=WKCg zN=8OWj54O=`|>kxu=lG~_t~^e$^YKN2>vPhvjV+_a;Q^@lp!4Maj!<$+d^X|HR9|Q zzVh@nRg$xqi76sR{4+M>b$XP{R?2?$wPt~Zvu|6eR-~0x8 zA9pg-8e!CXe?j|O<2^rV>AddeqCd1-Xa7`u)d%{6#ZZ%s4fW{X(sSiR6VeGnaYi#I ztN*-rm~Y*>{%+PW&2lu_RNVaXS(5OAg;w-msR{SXwWfj>kR_Et;VW*i!C_ErhPfA^ z4k2=fC-<*myu6vvFGtks;43bb0;v+y|NS#M#QFW{5XI=3?4j&D_*;Mh&jR3&RVtkq zt~`i&p35&+gsH7j2b}bZ9;IfcpppBO8t}wtKO&MJ-3%Utg8VEq>fGtj4Q(`iU zb(?DnY0l7?9Dr5P790Q+x7{XyQrWK7aiLk+JAiT#AXK~U$&o3hdnnGke_Sk(MGKVM zpi{sD;eNVR;)A?=;3}qBIsP6VDEJGBS zISr?piFOwu0%W9;Ly3j`%qviw-DwBG!5f~j5pe9Fu)g41vd--68fkRRt#+~hk!fu> z=bhwAdv^7A?FbU}nw1LKz&j%0L1y2PZ2l`C%p277(S{q0v&7y6yd&4l&J<3tu9QIV z6^_}luE6|1as{=p!%d0X@ukGAZtb|Wy+{Q1g7tGU211s{!nL)w@eK{GJA-Qt>i+xa zmt_O+f(9%48Z`RF`~vtGpsu4F9(XAD8o~_E*MUTajK8v*b+`)4^ZQ^yhK6Ug?t-ty z(GsXJT|(v{fupZH!XlHG`=MKi(^-KGaT#;r6MWO6rH5&Tp~DghN9q+@KqQ0yAOA=G zAh9npo;A5fIxtB~;k)EOIIgjIZ&Vb2h$-fun)w=0bn(4e^>CV&;7=eJ@aOxN*bUW* zQ;6qM-`uOa?@b=o%S}7Xj}T7?NB11R^t1ZJi!?EjPsY`rFkye;jYiY2Ug>l1y(@kd zJCo7-L7`|a+ke3gj)`lah=gMnq%4Nh7k`Df95)cKWx*HxI^QM6Q3%^BHi$qmZ@JGR z!*hx{P9y`(*_B-C3w)psLu))fN6Ah$a!htH>`;lxSD*#iVZ6I;2bR2!eKI6) z?7_r1`BWG9Szsqq1y%tzH;z-vYtQ#rSApCH1{f%)z?=^6_+}twL7H>a>Iw+&cq4Fl zfS?NjMgzQ6p=i0rq66cqX|pbwv9brVYF>820XT)@s2Lm7-)rgkPtc{( zHNwlJe%f>$Bu1!RO7dtO0qo!JMmtjT+Rr!b-7VSEUc_f4Gg_`WDId97BkU;rT3qzW z5ic*}&m_!~x#8oae}$uL4YQu7>GKI`NYs}c%l`fsPK|_90#74cf-A9Jzd~?Y7>U94o~^3-cEiqfq%%)B_tM%rG$xY-=BzaYe}AC_%mI8|KV1ZsT1WKjnBem|!23 zodn6uv-0{+4RAtHKZ4DY-uARCvH{|-8rPuBLqahhrW}J7`6T;`o~Uw?qET`Kocr6@Tm zZv{D7j?1)s0klfKdsh8}jLiYITsU<3bLF&FjmL@!` z4xQ2*au015?Br|DR8#dFN{_HTa`lRR=RKFhTiK~!?=ABi3UBB}|CT%3EEf{c{kMGe zVj4zbOCA(v90g+`2!3YBeA&{R=u0m<9$F~n`il@#Y{<3x4*ECA&$zR%#6REclb4VF z0$UV4yV*K1Zx!i_=x1UZ6P+19PF@0fx!aL-n5X$6WotbAuizD>LtHeENEi~FLl_GHqv+yFY!KL5;Ot!LD%-DM-Pz^*T1zkOTI~Wd1I&x|tl(e=NGMFG zs~?m!!F8|!*uxYczD+}ZIrQ$4drWXZ>^4mu z9IR}xCu^sZQB?8zP6wAkDV%MY{v|bc9gvvzu?zQf8DlX%Cc&E3W`m-d{JW( zFUd~{I*5ftF{&;Z0}&iBp8}y7c?4cyuQRS=m!9>XcRKG(8w)$*+;-fBY{E>|0Wj|M zM-a<|fhe*)eKWVZ>EQj+?L}ub@_>xDNexEyt*8MfzPVnY!+h z@VE90{lPx=<5F7Z94uL^REkT=C?+0xS_FczrR_`I`P0U~#H=ZqaFMZN%0o`S=h2?H z*7*03@*5WYavvWT&wh|8s4^iciq>#;xg;xWBEmGDv91xn7ph?218Gglw^I)?lMWFM z&B2F02+Qkhyv!^$4SAdTJ>BVn|F+O4=>+VIJ0v!d?mZH)6(~?g zoWs1>I$;@1kTrUj_gG4T7Kp92f)0?wdBeK7{~7=xa8EbK!@gt%=$4zyivV=khFgt- z6a#Ni?)%HmwBIZQ=s3+eO?ZYeJ%Pw$jbQ;R(x#oJCK(eti%!MT-hJ7(NSQw*68Me) zsfaVD2^I$xWsDd94$inZudbvL>5B!9UO6qZodGh&JqNFG6o7U27%+Mz?rH7B#!)F7Y>fE%veMN$Q+)y;&4yvQ zv0rt-13atkbGBhn)gA;ov6L0o08FVCVRa9N_;^UjxYIPidAr#Z_9l=L-_gXZxE}N0 z%H^gg?5}N{Z39Rt=T22M4Mx3fpDG9fN)}XKXNc1qwH6IH&C4!hM`2}hJQ4_-5HV3i z28svCwFE>RTdkZqb?_<&B(0BjEVVPbhJ&u_SaXGZLjFuGPHiuX5~@mOeRGU7)z&Q} z%W5rL$%pWZv!i^UQsuvuychd2pga8jipQx+@BDf`vO1!lK-IBlW+e!|7P`TIQf8Lp zjO;B|l5WZPT6!x{wT@-!MKRSMa>qP5$i}h;>1bPRh~QAdI(AS${_@1j=x>vZj|R>s z3Ug(=168sT&%5;8C#}`Y8RThSl4~Ctec)X4Cps!opDi&G)c^5wol>87(gqMHjfc|n zK{k2gmQit*_AR~9ob_P_-=b;(&&AQ`=y9wu6p6Gxqzfx74^Uk!H&!r;;voER%%E7F z7sTPJ#+z-qgUi6=nKQgpGOyf6sH{jP*Cy02u#Ol#oT+xynASN{{(dzbuu&)hMZSOC z(}K{LC(3ErSDiFZBL@++Iyx9X>y3T8LaNs3PG@*I;>*scANhD%Zemr26mH`bT~~P` z`7g{||qH3mwqW=mVJe;j-l;wby9xCAr>D$UqJ11PK(Ly{sZ*bHz0eOcuXcKx1H+4a%A5qMK%*0Indja(FGE|bwpL?v`I4g0=P|G~X@I~f&OqYUr9 zh(hXlYWsj3NXdSyG(##5qX!hECHoN!%%74z&92zjy(!v}af(VYtqw{zl z|8WIhU6)HP!h*+J22$;VPIV@1EfXPugF&u@1;u6u@Yi`J0T7QxCU0^|7{SS#Fpr|| z@{R_|^knBd7Q{VyK_)qy>j4vC@Lg{3L^JORq_DlCc#kqHotPccs53qiMNgLEO z(?sq@?WwjGfR^=QC!-G7#Hl)nL}N~GZWB#)A02c+Ne9;z@U`~AS2G#%BYMY4Q`2TNipYwMTMmozAUUH=Oq{ z)dqa%rE=uV-S-No=a&n{39<^Lc0W*=*6Ig5XMkQVD2nsOjSSjpGR&w~On$^th6w}? zdAag+XQEXZ1R-EOY#6PvaXW+t15imYY!|(JhGJeA(mvGh+ZMx0|Nr~zArw#{pLzX3 z%nO%%#u*6a8Pi4iP{*#ak>HQEMdc9Yk|LrEn@5_o zIk;j!HQM5pGK*n%oxrxGlCs-w)QVwdDedpU(W1(w6&;fZ9LYOO_6dQoZGbONhG%Ll z#iSiGhQt^a(2eI~`$Z|oa^9F!^ooo1DYs}t5X;;2eZFyiige4+z5nw2ASACIf|LgR z86{~_&t3jH{zSRzrZk2ut%RQ>EGTEL6|ESZWbcMvlN3FPKxFnwJ+!R0sOH{tI`k*s zcS${&39i!nhiPHr{*qsU&}pLD{J$v|>0IttPD=k%@aIE6DZjLorb&Fc(cLTQdKn!c z7di-;#qm&NLI?-jEpPTOt0P`>j_MPe0OwURK$Khn9bp#_w%HDEv)h33+@Zhje79M< z4wrvh9Ay}QDZNo*;rvS)HK!n5h;;g;irnm(1Mw ziwsE92Eq_Hn89icZ_vrkDv0)L4~EU39c=vk+EYA0OihF+zs@S?dU0cU8U{cWc~RpN_Aa>S%1ox6_69CV|+?1>$a5$()YkQhs>~MsGKN7;;+68y0`iew$wm3Ts{3 zZ4=WsX9k!iJPX!Q7+CQy1n*0D6dGrWDj6Eso@>}9oL=-$X&P5xF-|ryv?2SO;nUw` z5`ZK5(GBZd$8^h_{S0hNyu7DO^|4+zcfbvn_+r1MA-`YrYZUI#vy%mD1WyssR#npX zwipo|8QCW<^KZF4=%*+kz(P^tDni^*!7>pl?eQ)}@+98235J#kG^`3xfK$b?6=UC1AqA5Lis z9Ewu@7SneEg=mr-iac8*ZJeYxQ*t&yz)RNuh5Q6FLs98mxWybz%SbDPdB0D=xc*8n z?y#$#(ZjQ2VmGYqF=szYh$@^m@*Q}tIPBf-@51*YDHsdcaD6^c5t>; zj1EReDHM%n%BxDh&#P;K#*;YJQTc1xug%01o5*pl9sPE!TUnG9@WJemO2Z??S$~*g zH9XH6r^*&tT}sNR*q{a|4MgN6H0MQ-<{{_$$^(1=QkJ8`(8XhOqfcu*?)8-pTy6aL zADG{I`80!kYc1B}0w56IF~DH&##>-?v^}3D1_MJBhO%5!*nRIGbU<;Bd$Ylw>gYOX zkLx-B{`vIuIw*lB;fa7jbjQIikSJg|&P!wBtakC{EpcqF=~SIZ7s$3=!*2t!7U1JJ zRU36F2eDpVm@dxsX0TgVSh3;J8gtu)SU(u~0Mvl-Yb_`y>93paumc;xKZAOM1%4nP zUn(8C0Dj67m_?pyY@>Y$Rab&^p-)*?Uue+{Kt;~uUInPcKo{T#$r1lEQ!B~8uITFU zCXln*gj|W;izJ#}j~z<_=VbE*FoYIDZ1uR#(Z;0M0T1>fU=%>fuqN$xQqqDT%963A*24ClI2k zaV*qb;;N?f#rG&gujCiuuL3xkhZ0-TEqHXXxTH#vq^N0ioV{5{$onLFK4qDru%16P zL&Nx&`Q`W)@2!Z)10x{LuA40Ntvu8FxODfuU6FhV*RKYlc_t|k%S_Nu(ho&ssfp`} zIG#i(rv;cy{JT5lu(?(XFM^}qL0i)X$^>?NC%kQVzDS52hOY57C?DYZTU?7PZnNyU zd$h$PRFqUOG=}lyB9(lN8fgo9&ym)h9EG^Ds|y)E2%#VOnA{rqqI=20Kjd4UWEiv% zeBVHvkEL+|^A-+8LA^nlp-z=h0xTKxfU|mog#EM!8y6L%TdNQKT_YjrG6Y-EzHWe~ zZAHh1$pzjdoZN8sr+9Sjmv#aNY7bSqTWhty&=C|C{PhYeY4uJx(sb+ z9NDl89U1Fx!hD8tH;A08Ck+q6R)!K-+|^Op%C?E(EN*sjf-f-Fwk>8RQ?m;I@jt~& z-@qv{<&pP<)x{ zL|b7_?BM0WQ*CS<4tzI2=H>-(+zv8_0FDPPtaVJ>5;Z?56yrTV_iSKps6^3yaDZd$ z9A%LG<44=#m;!+D{j5vBugP=prD4Z%V%_5@YQ?~E4@L6wz!M+9X^XQ!%4&%Yya94C z1!aXImo!>2HyhWMvtGO*+Jw&SgDdrZxR3e1X(RQyq5$Sa)NSxv}`R#nC0s z+iy+j&YJVaL$(@sMvC*OH5P|*5Bn1^1I9v9{x1-7KVkI;jPu(>pDjE&>P5jk&7R8h$_Vi)Hl=ESdF zaKuMR)3Wo!l&h3oT@d}!F_!q!b;5G4n$OX^8pZn`z70_P3Ec+88Mg%$5Bpndo}8P?nsx0snr z|4HuS4U@;G@ixP*Jk#olJg$_e|OxB#_|LkfwT1lv!|4;yjGD=1w36NqG z2yT+~AfD5mw*%N+g=AweFdD-3!3FjUEG}5A^&svT@xTK}Mlf52jm7!)Aow>d8xDYl zw8uhGfQt(k0ffVXS@Hl!fy0>{$Q}b6`%dyk(?3R&eG0f#4)$1<#8_}Xwsq~71cA9^ zZ{q;QweZVeKm<}MAYJcll8LO_jCXgMfbI)fe4V_4jd9RHF|13rfD4Dh8s$XR4)xVr zh8Ty`-FLL?k1-$}Px$olik8WP%a|c-nKLSDNXtlQVY4>zxoP53BaENoIw>p^WWag> zArI!>?2BE{-CV%9K%Xpo9@NvmE_NYOmopcN7}+9XKx+fry=T(~@G;W`3@)?R2HMt1>aM~3_2QJtwSWa$Fvy>o{< z9ZL_IEFq7qXesrxbfyD##&8Lr%A=}ADM@Y5iHzk%2F}^aBp9LWJYT%hFIpn~Mzt|O zKFXzF*j~SprvIp#AHeqN+$1b-mQHB|~+iT}+gY?yfwGC!n_@*Z<)?Y9~Roee-h3JRVAtu#hX*P#;gqg=CEUG&)sni_?zDGgVc9Buw+t@1boFPCE6PuDDokRpzV92OxH!To*9VZ z=y=3uJ*PzPF(>v6ijDTPV(?<-&qQwUZOey{9FYNFeU58~%q#0na9k}>kf6VP$KWj2 z#dFB}oG2(b*s)%=zqZ9!mN0n%;O(sI7eP?LCE|9wfM>BHJkQWaby2r@sjTk z=oJSk@DDH~_IJBDOS}khqFYBhJPI96yj}o0fKJ$R<3JI}0vPNL3%+q7OAQ!9M2{v| zuf+ym1Tp~R@qyn2TqjKC6kwe7-PmqrrL^C62F*Whf~=j~(OKFQ#kotdW4x}7dJ|YV zquCb31)wynY3R6OYNA+t=7%U5vFKQPREUP=arGw89P@|yp&NcuIfPh;G8|%iMfpd0{Vv7y_GDqfm>G)v0K-+u#k=OnD9GyO#Kc+l ze}9+{qpTa>GtOr+2?+}xd2|1|l54bQptwHnf1k#z@5GnZ#|`R?(*C|2GXm-_Ez`zZ z+GEm1)s5N{x$kY9T1L|h1r+6tuJli&bxWtWN+p;he1!WDp2_G=A*xe%n8?PVGD&lp z)6uaGw?!pQYp#fX&HIGC0i2FO%oD_4O# ziv6taXBzbi;~q#+M-tQ(bkYtOGZ8@_hDN+Qzk*Y!9tHBO)k6BG}R0+ zMizs-yu1zCEaDh3WsggY^?>BFrP|v=17{ex<>Kb}r7|`M@yXOr9n>k)2*9HMNY=KH za&DCy@;FNSR6nKYp;Aa5S&ZST!*`S9aFp}Bbv_*uHWJ1LUO1CB$ON^dk3TX;mfG-B zW-!L8wF7mqTNA3}Jg_h*23L21x=4+>`-*{Y$4u2(H>{-Sm`37O%uIlV@j5>uKYX4b zlV1+>&C*+FJgw6pU%QUBiZrkQFUj2<{EI}QosA9ytbZ6f*l)_LE)4bRiY^#mN>Vra z?ZNwq_j(bTjq*iSGX^2b6N&F_>@74-g!sPCO}{2qE}px|s9c4#Hzrmp~ zwkSZ+9-llSWH%M=~g7=kZ$lvrTxiw`<}b}<86*6N8j0KTdlxD zX_OM93h^01NFD1aIK-OFvyYCW{xKN&5fGZ+- za(B0Rva@u*!XggByNhKT4Ezk(t^-%MAL9cRW4?bbQGwv1mk_@d#B6ti-TB{Bm5|{G zc^!!}6Zbm!(@mJSR^mHoi?*dOXfG_&9(epmwMhH8;#sTI-Bg9>0*7meOH!wDPtTv8 zks{3DX@U3?+8V;Ycy1p}KYm<8+Ea|J_tk)-1&)=7*VM^i)iR3NB&6_B^^esJjj5ZC zA=I_jT~dmuN#HP2mwhtlm~_ijb^$SWMhhZjw9cW<$SkeI=jrD6zcH{~i1MyaBqRI> zUVa|XcXeTnA1lr*Ehi*&;&0)Dbd)iD+vdztr)QM)BJUgFt4Vn@6{#a0Qm6g^V5X^|jcZFO%3ZIX`nvEOQPR=jYYNGrw=bfJzqvjW29H4%9i!3&jnEA=F z_frJo65hIBWnQ^Z{Neq1(W=8+HPYwWqW_i{@<%_jmGRHY`IzDCWPW2X^o`WjTO}nF zWv{!Y17Sv`Jr;-mm^H|{(09Ud991O$sz*YkRL);>;^%5ycf3+hl$ucrkFci^9o#b@ zFCFgilg#I949Ov0*rpH6vF|S#5_(`ENs+je21uOVwzxFPIuP zq=<>CXH%y35q;$l=aAOSB$abFBtH8({J5nLK`<@KGI3Ej&9Ik$_GO>z`M2a&yR`Ye z**3mC#U7;KZ@PVKy{nkMAw0T3>3e--kC4*fbsg4le4AC}>&DIEm6*&Z(1WTI16HXP zk)~P{)a;)?zdKiHT%0z7wX$~4EEm=mkrXcX$kOS(g?EC12N|NOpE>#Dp-xg=8eQi+ zpes;PjOMCkzBbW#lKL)h4wwztGoTpv-t2tIn;o(4tbh~j8C( z^jAT*gYE)cWwq_aueCRuH#imaQrM911BuIRag6DD&xY?(uYiQ;tt?1au#P&TI`=Qc zI6~U%oQidM@KE=6{&6Mrt=ElH$s1LWv>DIgbZsuL*21FuI_P&dIZ?0N%51$m7N{r- zgA;6`Da?|0@XyxIQ((@CZA_vAUJKab;3fAmC^G;l)U!yHbL)VddBU+JaS=lVCG|l7 zLk_MfkYj!TkM|h_cYi8e#3w!Tii!fsajhko?}_7+9nHgT^Bml@F*@a9E79ar{?8Q3D&!~y z^@?k~7QgVg{$d|qVdB%hmty#nGt^X&Cdp(AhYNjhu&1V9lg?IdkrWL+E10^OTA4u? zI4&gUBOG_6-aPc+>+S8Kz`GHcO9(CoktcxcE_Q;43d!Fgqf6CKA z{~yi_c`PGWl6K^aqsF5idMnHkR{?Q`gYFXIG@4N4<+km(V(=vvC*6Aa7j7=xT%*Q5 z#?>*=_SYhw17zSXK5%$U>RsfKSYL151w$>dtq(~q>6$RD;_`n-Eq>20cg>AyTdoYc z0>pJ&92S(2OVBJs&=U}?Qko%O+tN46_V(He?0B|eAq*%;E*$L)p2Dc31H`N>yV{MN z^N?7--3`eGdXI8tdyjxR2=#l0JzHn_9HLcv}E_}b0T zRE#!82sfL7J){GmCJ_E%{^5JW3)>cbbNd<9iNHQPqyXwfw(Ek}7U?i8DYYXF!rof> z-Hv7Z-${LvEz18NA!eyXn99njj%J(>YN___(rWF zd`Uhdv}ciiHeuG!sayj;m9`*#R~WddG!2eY)JRKZX96jTT2r^VSPkXjA&`kTCt3d1zGM~EO+`zoT*21)jCp|k~=eEDQa)G%QY04=7 z)Im35R>1=i9A21P7JQSNxiiY9#&;w%sC_X#Q5Ru ze8k$R2QHc}YCtEiaZv5l)ZD6aZ9kPGgEt7D`y=MC|6>2|C8R91@+#}n_L#p&55H>t z-bnieNr)Y@7R^5Y|5!Tnc&PXG|I3y=`z{7!GCG4IWXZmatXVTcRHP$gX{_1zeVH>I zp_B<(D%D{|+1KP&qlR;iIkrJ=-4n%a3%|?#{XHJ%kMl>5PB}B5&-;B{uh;WwxdQv3{{2a&i)^Ao(GzFANvn)r9^m3N{*1N;haiF zxgU4F=4?uDr$9`Zfc{N0y-cQzi-Cphs^W^KTI6jv?Ju(EIwPEW;CM2}A+Rm#Z3$vXmIzVK;l zy8!AG!0}hzUYr?;#OJU77%*5tiL0(4#hI)9_b|+WUsB&GXf3mh$Yam}C$0Qf_gsG2{lMq+nb4 zl0aJ-N0-;>-tBTc0FAQ7CTO=N_DV{Nm@A5eL^a@$v|(`#z6LukKL!KOh`RjFvsb(b z03MpyV3;l-eW7Z~jM;Ii--gaM%LMv`&@ns!@eydq9N^Kds-bAR3%>0lVEVcvanp?z zG0?tt`r|OHw$aoH31$0>CkR@f7{;Q zIA00X>pYZpp3mAAyvP3~tlPN9N2)s`9ju-w5gp57J>V-twQMeS~UtEf`nzX05U z{|)$8W*osOKk*G7>+s&8!X5SsjP>Ev?S}!69mK+LJKo!kTD9ve`QcI8v9zkw;Fdf8FGoEIo3Zhs+vG!^ z>Tf_Rpshwz5*cBRv%M*0Qt$4=-V3brF^o7Ko_ zuwl|k&(E*C6HHQ}X+TUAycse%XCr_aCH>TG!sZ>x_WgC0cSOa=L6Lmh;@lSBp=*7g zzQY9xX>?&^@C*ToM4foSB~minVjwjx8Y)M!uYQ4Qj<0^9(XqoqCT;Z>R<=}n(@GoG ztIMz}kpu>&%Q$Y5h^}mW*85bTL4E$q1^37r z_EaFMpkohK?I|bS>IXyki<=sF(hDr>i$oFMp=}U$5~1fch#Cm5E`Xe4mh7e`uheopZfj6oxQm+bqR&>oj7axSJ0qg7~Y6Zkqy z;@Y&+&Xcsu+ycsLTf#4}b5-L;k`asaSRh4&8c(v~N!|pw4MulWw|}oYUg4%3*)S?K9XJ)jW4YV zvYko)LvXCjz%kkf0&*A_nHw%ay$sR~Gf^gi!N}*ZqYDn~8_O$j3ewxxfW7Wb&jaGYE(kUlKQ0|qgzUr!;(dS){ zm(v~9M6hE*2L{5{H#eF-w!HWWpeLF|3A8erU^(t?#{&fdxAZJ@DJV0>AVqqis2v{i zOGQmBp0JuygrP9B+1!!7>W)?FhY)Ha9+api&el9Y*C}t*^4}d;xc{V1Sa0*wGz>A4?6M5slcTFAmX}i+0l+)gcMg(F)vV zZDl%5f*kYpx~%iFjA_fhA*tSLe8W~sS9@I8TU7j4(`f7_TtVLKcQ{=6V$2`PYw>=; zK13T`qAe1e!#wD+_z{B;PJyqGC`-{lBvQY5J=W4UE;g(8XW*RBcT=HjXV)^b_ zDz~d+4D8gMJ1b%vunu}oEBiCk-_I;MpdGOgx(R!;6%9{t7y{0Kt9+6Q1Px!!2W1u5 zQn$hgPx;!2(#X+v>?|!O+-5f1;tV@WOrQOXtT11Uw!ipCH$|5qHpkq@T7M_2&YzJK z;bmh-NT*$u7e>{L^Ka*;KEY&BP0I_+{yug|vU@$_^*DZgxKlH)(P%5e760AU9bg!% z7VO4R zB4+Q~E{qrxx3(CENi#`+QB)tJEm5dZo#oU>ygNOnx8!$-CPVYT^ANbUVA9^dPKho_ zNwV`igzYutB%qIvEvYs!wC#-9%NoI32fJ<7Li+GP5r9pB-mZ_HT1!-`^z|F|{(<|R zK%HE!1=9v>zNiFmPhB;leMv>@<7KKVYb9#;-3rFUB!r2y4ti2J7-mxD;daNSE~)^( z{sWhg7$QqV$I7_6;vL=U;Z%Ov&MFCWx_UWBG@+fSS&i&_@?wgFMC-{m)qgD_AflSC zui~FZ7kIXhN9E59-Ypg1;qx6jMDIHMoV2fFz}0Hi5%+_TfNntI)7MNwkob{VRkFpE zHd{Q-xi%uWx14;V_TvT<>CsB{+>u#yjg2D76b=u#)EDGvudSnmfS&2nowu-dRAL+FjM|m%_E=ce8{~E9hrni%4`3;;I z>F%=oIeOQ=egJVlLNuktH@?s#WP#Vf6%Pl(Q)HtbgUci|&jW6O z5BdEuR?0^5qTvq8IE4}p&R%6i9Z$btu-Pmd&a@jHa3)!0wl$fWKFwvRp`?hoa9qdK zG&@IlAV({@XI(JZeY2yiq;NuU%%!sabHX#*0CtZzFl z@I0M?b24oaK7jVSeopXd02d7ShTvad)KRf?)c=AdE2hn<1AYfMdMB#GG@AiWaX5k} zZPX>qfJFx2xUh+`-^Cq0!tJ}B{p@(QpLF;P3lI6k3lNyLhX78J(|z*wI^Dw!2--y- zK&W!p59aOa@H{YttL_9fG3ow_V-*dK__2xf5PVPo?>19?L94F)lCAj@?R2IAA`i)b z? zk}l?UMc25V9u}-I`0tO;xK!ATDMrW_s2)XWWZEZH(C7tENe9`56%81;<61yfZRZAp zI>mQK`ix0EJv4+f?CbQv-A6v3L<_Bp+*fC)HOI5Um?-Kf#Z}@N0;T9^9Zy+_YWlor zM}O7P)Nr2ux{Pe#GF4_oQFn<-s0xDdhz1&qbt2&te02jCL5mbCV*-$N4P9tWd#^xK zXZ=@bRcqiA?6d8pp9<@{_bN!6l?dtE`Cqi$mVLg*{+q(Ap!RX@CMhWT-Y~swQLQ*D zqs0fGQc-8EngIT(Q_k;_|4!=^%2o*1tSRCP=F8SV)W~KGj1mD9jF@2G!f)G=^`lmG8fNahgt*h28 z3R=8V?Ce~X%Edb}O^zAge6s)4C(C8$L`3I@Vq)03Mux5R0IKG9c?`2^Jd@<8QDFi_ zz94@NU6lwIIi&PR-*1LeHdoVuma7LRYiUS+KQ^igY%})fh=w8cGW8IS029P0vSID9 zv)HIQ+k;Amy6mpQ1aX2qku0bP&Mg=S!szdSO&50h(U8nHgp}lk#IQHVbfM@n4w?Lg z#ObYsDoEbb*v`}2F8Gu-Pj^tkl<|K8jp-lV$48s5F=GBR?oGIYp~9~P_An!it#Yqf zeUb_aE_HO7tFBJM$EUV7-N&)minB-Ycef0cOEzP?aM82;-39G-kt9L0UhNk1On@jg zVVwS=qrkk>?Y!#pgS~Ib%Gb;qIBp&TX?S<@QiVIxK>`N`4Y4_$Y+H63+rwn9{)_DI z45UVrPe4_Raw*~Zd;>3^seR95wr6RV*-i~OZ^MJ?Z;f5x;&ac zD15(Tbw+fl<6G^`5I);+8u!;v6j>{L(D^Y5Y+mf+ISUnC<%{Xw2tEUzPxY`Z2p=+wy%cg%k~A$@T+Z7reKhvU1d zS(`|4zWMHnFBg>b-4ARt`^#3HN76Hn=4;$U49e|A+Y>i*y5{%1T~oH`DQU%%c_kZCRa? z?DgTVmVI$zwq%hK^HY|;FN9B(f$U?1xAxk=DXODmnf0ujE1D=VJVX;)A2E=2=zC>q z?LR`aeUG|&(XJ#z{H8TAXB}r~)`wBIQ9h{+;-u8Vfgk<1+NUd3&;GP4&cQ3AY_3*@ z$!RAb*X;e-Kb@UTvJB#osv zNrcC%+rpwk)_FM9j|~yAlqSFEZ@W=ICr#!n zuLyqXmjChA#|;*XQ>Yatsu7oZILh)>AC~pr;X8dQKq1xcg!Ve_y1P0$WYHsx_&=Pt z1Hzl)sc7`^`;%(*?sq9{M_}~>xwReMJ*Zd};)tPO+x1I4vacl`d4XW4+dQ*b=SO+gLB^2w4}p*bjj|0^Xh+3sN@J2< zWFj59MwG_=q}?rD$bVk;AU*@l=ns$8$p=jnpg3l|>U<5W+$(o|4VzJOZvlJV+u9cC zQAWG_s*D`Bx%gvj%P&tEb26}4eJ((BZrzzG((hC$BiQ| zk883F9az`PY^=H$~L->c? z&2IAzKMj6QYtSfa-y?$V^TI*sN;El^V7wK*AiapB=?!Y%>$OJN4H)P%{RV@UU;{C`a;`44a_kIhAu`$ihjvMB#N+6^6{GTw{7|DXN2el+;odW&&&w67dUlP9 z(bRYC9(@r~&h*S%4k;ohoX^eXn!d_-BaJ_EV(NFczmuy{{=@dKTBeH2{zm-uCT5rd#vXBiEDz{*wH@ zli#4!1_)bz&)~8G`ivQf#=-TZ1Lnf?)g)uJZ>%`aj-z=_2-ZoL7U$|l zji@8q)vnNRNjg<(6^(t7Bp6Y;jp6__is>y

aRI`%fw}gU~MNW#^I&@cLi7UAbGjB>e_hoTvGZ7`+ zrv>Um&35=>^d5dl7JvRJnUwL0Q(V^hZJyGbysP!C=DG+KI~iUfw7q#s0|Dh ziGXnwU;kmP7lBVtlIWl6)aA{$v=*TbeE83al5@&+Fm58-j!91kpu|;PG@R-TW_rp4 zlrH~{`@p)Ip2UFkvdDbCkI{8Z{?I$%KqCH)^Fznyhd??{7C4W!Kfb$c+|SYe9^umN zZS|qdN?%(kr)L7%E5g$9#TRSzjumiLv;CTN@47jaG-fMDz=?>i>{ht4aHp)N@>4F4D3d`-o5O`J0@N%6zIuW?AC% zC#669I?{7P1pl4v0O7+K>;_m-!BC@dd4Cc-X7J&Inde(f7Xt?Jy6>8vj&1H$zk3)QnlxjqlhyAVGzU{=g5S9dH#t%49dg)QSMR0(g#ZC2BabNJm>ZhB^TX&4+-D z?Sjt0W}VL}4c=HsD`|#C;(3p6OVo`nNUk1E0AmJ1NK2H)>6u8}qz>TI+X%X`JG+}K zV#V7Km^tWP8}}hziLYj{LU#6Q_czI#wPw9EC4^ROBJJoEiBk011%Gqg;g5v+AoCLf z^&%e|9J>5XYrLS2k4dn-x(HyPYj0!?zvWM>r{@H-K?5m#0py8fwF;G;G zm8fTtcD6Rx3!|4y_nPmv=|$`^t~o`1Ct%8c(tJ&-Fs0DF1;2erL!@6MwqoIoOHh8s z^+jAFY#)fwJ)rB_{((F7h+81o_F+$cFI`2+eT*$H`4<4EN;um;)Sta=n%eDeDia`@ zLSh#InNe|uX=Cm~@#p4By!s~o$HckH<<>0cLfKwr;Lvjy#2&lc`_%sa<7S21q@Lr= zOXLgRI$w3JTp$oocuItO0e#2d(RPv4xP0Dn_PUIGekf{OCr~FM7HUMqk&*GCSakz5 zy(Fr^fMtl%33H2xU9JvZG$5c~Q>SS=82hrO&Mq3@%fT!9sa;ezl`1fGqgz@l)Ph?! z{A$k}?=Z?)VGTKJ;(ychehwZhzlKptvFI1w7MeQ0FzW~(n&RL3ZmOGZq!!9)jU^YP zEeQ@g^&TMm;@m_)BYys2s#z81Bkm2T?VEXvF81A3d{|;eJoYzdE`PAmL&b+Z8U30; z%I&srZU2&?}#PCx`enBWD!S&#`-;%ZNHvfvxk@!TH1xqe?pxW;LN6 zhV4WxjnTZ?$0fteAETutq*3{nw*lC)V3NoP-SP&Gi?PXr@vTI4 zW5|zA0(8mmplW{t&`bNbzmY+#5#QC>#xPkW9F7wvSY2C*@drDsHsJNc1_GwB%SU@= zswWv-8X(zNh4$JaBaCExgE|S*5i+LDIP}WuC=ll>p$8OSdbolgsZ}E*v0&BERaevr zR74ypAp(KRh=yzPXpRKy&Fu~{9S*S%sQrJFyh;+%-7g5Y>Xx=oe+cQGDlICh4a7R2 z%V;JyEw`g=G$gt|=W3bxgWyU{X->J&vSJb*ACGYLpFS6t%qClfSEl|UJSA0Z@Tk4l zT4A30E_W$EKr7$>WBxUoF47U!i9|30L3yXqo`}LmG$0V-Q1KX{ofQu}qcGGT^C>`4 zVrV+ST}Ap`aWR?xBENnIfMe05%VU~FM2R6#tcwF9eZ;;(E4!U~HoDdiDK(@#RVU(- zf4$Xp-mQJQyt4gk`>6%#I)zMJxvyNZ)1dZw&E$VFnYswcWDaLXrSHj+rt+|6=og!k z(w>rX?oFoWJb#jD$xh{I0+WW9-JGFB{}*=a)0y%ZC5@U;{$Y>a!n30D?Jl+8PIRze_$h6WIty7Ti0JLyugU9rw53y zA{BD+=ISsd~zXxSm?qT6<%BC}^|)KYG9 zeG22-=C1w4U-)^cSpcskhl!l9aQGol(2-ojiv65=k8Z{r{ur}4h1`cG0i#@Q>Ij(l zo<-~$@ZF2DcyDQ*E8HGwAuINwDU+-3j-*yqI=k{!wD`KYnt7^HfSt6u*JB7a{foqafBwcgHi>a;&+6#HK=)Cg(N z(`kWD;`;YRz@AGKH)RB_%205LgIepyPpgJ6WFDmxpJ7aJ-zAEQYCo@fQ)+`A1J(J; z8+bw7ggrX6Q;SGov_T&2R|xhXfSRNpZA+j+v;xFDxbqy&Bt?@U-;}s@v_0?K^?0za zxd($hARDa^4x=C@wHo!UVyW|x>e;ymz2k4YM=vnHE_i<>?!!0^B+j4V0I3PZB3P;1 zK74lN=q2^}kacq_n)+jt9D*tP4m;P&P6RN4#CZS-5K3;8MUQ2;3o#$HNA!C0-SduU zjHJDOW_+XK&1Ut@(OlumLUWfBYgJj^Jo+^tM8o@a8o*iJoiVt~{wF z)9PNj;=S|?jVTz=SdoL}DxF2z=l~58<{Rt4vAl2$rBj)Irxgl>`0uQvHVDLa0b%QV zop{+d)DAy#$+Ls}j^ak)r!25y>q;PpM=RM6K}pz`k;irJv?-#HC~rvHhP5s% zuK)S?by;()L0!e0N=ARo1GY`}tFmjZ-G5}hvM|?Ve_E`oM9T;=GWXh zs^#%dlF!W^8a=d!?T8b5slc{x-+PmO9W+?RGt-uie-ex_@^|ccA!+{Tl-X_0Qh^xb zYi1qC)8*HOGSYq7&#~RGoKkj$o!;BS3BZ4?0%IRs-oD^QgY>oi9;K1K>eEH_gJThm z^@{bX8Vq4IR-kSCApOFvAv%AAhRd&PuThDp%WYvnXZPs^aB`{xHZ;syDBB%sg!X_r z7x*WRw!o|ZDG;8OtI|%<1*6nKR%oHe8xU5|S_zKW14j}XTjrop!CYwdM4~XU%cSUP zKcmf#Ch%N>5(8j4z_2;`j?}?7g6LU;AZnay^R5!n>~;SFyI@wL7v=Y7_8Wl z$Ht7+DsT+s1QDf3a||n8TEU><$R9T|4u96^JFh5Ft3K4x_Py*}%Zo-98%qMZlC6dcl5XaxM?$_8sMNK zyv-KK)}Ix7sthUm7@i~~A+PuplsD!V($9|0IHyr+bkS1v_Ray3tcrxzke*zDbs$gU z0(^Gw^_s`*7&OR~71cH*bd)V(ON*(#AwHyG2g%XulJtq88H}}o?E~xv5$^CUS_?9l z7(q8Ec(HR;f+~2^U+|p=f~nDWb*sYs3?DVe<|Bef6!4m=t*goCB-mXrfyym&74=p zn!8!rI-yTR6GdLQdCO)~csfs4DAcy-=@f|vmMBZOGmGWy#Il?X3^|rI4bIRFkcWwON=jf!PwR6IQdj)w5G%H%!2ixDOeRVvvMFCAr4&A@|x^CRD>>bs~ z(T&m>x7i%_a+HYl`WmK@HqX&39CkRO(76ExX#(4n5u{dJ0Z5aK;RxNLq6|(% z@j%L3ss5mi4yxU|f_J4PlWHbwhqMw9Rk5fMRaW`4ae z@Y0^>MO4VZ>40h|R80R#u)sk z5<@Nnww+`L`oX)n1WGh>6{T~hbtj>c=83}6d{GoYx_hr&@M&ss1Vyij36TOd@!IJO z171%>=+q99$Lw0dJP|{{_yt-=S@vd5bdH$i@HCH?)$DQ_T5wA31Fy8)iv)gs>hTTU zQ}NhHZdNce9a6DB(nE4;f4+Yl(3g$#>$okvlZQHb6vcarr2sj@f(Gs zOyE5PV6cwjgbScNEw6uHPbkS#^|u~;RNu>%n=*3Dn3H}SiCK^;)xa zIsZmklLg&3s3mR)^`Lo1pJFuX^?xbDJX*|%qjm|e#A#C1~+h#p(U?a5_WM zU($CI!`cS6lAMk-M%wE?ta!=TlG=m@w<1_fC44l8(ubS7Xb3?xrm-Ve;YJyiVOSAg zjkCsWHvgRTX6`(is@g;a^7 z8eNvQl;0a+f-cVffh|fsfsh#L&db0Zz#@dkt zo5%EtNLQ%II>?aK#a8Ve@9;jN)FN$1E36TL3#+2F%2S_*t?=t0{`)`Q#>r615}>Y_ zfyS5}*9dL52sC+6Bt5|Ecjy4R-9cFi`ChZL&qwxiMxd;<1(DKplh(*Vu@UkdFGn6(rEGv5_?OGZ=a|% zKL0%8ce#ATfL9gXCS8h*{Nd{RKjX%TM|>guP1Sg$2YguD7h&s6d!RGnss%W+r#A13 zh`JiytH;Mi>~p>^$ecRQeAu04&1T!pvwa*Hc&7F`5xA?1HbF>R{Lks5${VouV0%wT zno)Qzn!oEJe_w(t0>&4R8)H!P(ijKp?3>MJ@DgLS>BemyL5=*3Z#R7gdNm*o?IG$9 z-SG}ck%3!n0-`jGt(l{Oo)ffc_YQ7%98>`>Y9@(s;adj)p+dX9K~w=4Aka44N;HA+ z8Hf@(SECMq8%}^vWvC1P$X+MMS31S^J5*>5p;o}8(+KefE2wr-HA)sx^Mvl z%|_qJ2c55)Q60@&=(}$=fS%}6U4lJX^T0ME%hGyWB+}|*zHLF@n>%NP;3hCi9UB_W z!MpL=Dmz!^h@dffDoj;X4OymIfh#b~GY}sIf|ZK-M_4!t%(`DPW{j?b7aK;F6>rBN zZ~}ghjb_wx?e~PlF(k$@1pXmh0B3#duCMCu_!Wp;L^ac68<38b)6*G&D+s$rjr$e2 zBp4#XB_~K75uqnB9h$ufZ!p(K@EBe5G&|(pkP}|cj7IEAiU=Mz((@n8ya?INT7Z(k zJWx?~;jGONKyr1F5uMV?X^E<0XcS8H4+H|LNRaXPetZ@*I5W&QkffIXB zROCF)PX}IuR2nXfOW@@5H_3O>t{ksD{)unfLP_W0i4RJ`C$!GiT$417?zwcVgA)#G zLQ^u&5BcsLZ?~As;P@~5Iay%~ST1j-q;Q-O*Lsy%E4e0Le6?qRk zIfcE|BKqjr^D$99D#`h&Io!alt;>`@JKKy`z+5^HyTm z+t4d;p{9orFd-NT7jxcpq{#YktD^}P{h(SG<38YuTlbyFJt)C77U!v|tB5|>d7yk1dgHbA?&RemNKeJ(Dmb_1F{ zlu|~UHb|Iw_;SR}M2<&yfN;?1sQCf_1qczpq9&?esoKY^*hAU211>teBSfR(v|B^w8$(5OWKq?oYz5^&1r;81Nl?K%iI6 zL<51$r_+h3Q~A_pw4%JM0<;%ns4<=LY1%w>4C7SWK_0hnzD-F0r#!s!UV<)WxzIzm z?@7rN&%IBR5mGupzJ@VCLyKEgfBjd7Y}@0*>1pSW=bKvY@G$L~^7Czpb}nk>OSuaR zOUZ{t$yVL$`n|}ZtHaC;746|L%Wd&XBd$2gGZ?FTPTp@eAk#{I%#5z0aTBhF3{|qj zkjf6vbivTjH4nGSg2B4oafJ7!VTJ~+@(%WwinEap(mxZnj1nSqU3GV4&U;vE0_RpP zFR)c?v%r2ici}Zj6xGxY4ga}aLyW-*IAj%Hx9b)2k`Z2NPfXF3O?~&gp-E#!6b!ry zwHOmM>{1>)&1?q)nJNoQMN2DDU3D{?los6njppfc^uk$7D}=qkm&B|XkZ%M9%~eTT zZRTo=hzGhvncG}*5{?|2KCf2Vo?TRJqFG#4R90c4ikGGk8CR2R^;F>|phpHV_@WNjs^GNaU?G8qiP5 zkMmXk5lM@iY=W*6{Vjdfo5hNoXr8DpU)80(1dQ?ujAIGWWu3SPr^0^Do3cPsY&!mv z<8lm$=wWpMK1mf8kTAe+8!KxaGo~75U0juklMGjVgpngLW>9ED@8xGkKc=^Qs7^Ya zv2_)GSKu171M#gR_h9U*G4cn!?FxVwfE>4_8+O>a1aW*#DpXAf00s1mq9$JGd}{(< zh65JK9vFi#eK>-lzgmrgGbvQ562o3oqo9}t=}#aLLU`zW)DiTW05s3nXb#1WUG{87!UzL9W;_DH-nPkS zmnXqePfdgtOmpDDiyw4i?Z;a#S?#H5h1~Ln4x-lv-grw|foT2g;FA)HTXDO>n$!!5 zx&b&Gr7c_B8At%)PVe8YSQ#HnRF42*>IBuvE~;rO8nSdnq!9+q_eRBC2oUU6=Nj5nq>h2tQ8`VizoMMw<}f=&Mnx;Z=F4 z0-L}W|LDo)x&&EILz%Q%vqHOluw?2h#btPV8vfVNJY^SL@xKm~M!29c`6|(6g&xcX zxxyh7$<8qQUQG5>O3oQ=90OJ13pQM)a{hW$dF6V3zOVF%-57#j+8*&=H;!o~XK~?k zbMahnM2C5Xg=`hVy-g!coqKL`TPZ#0{TIh|f%?3ZeU7`lO!*HZSzoU9A~|F^TEt|W z6|^{T7TxV>LMLbKEW#w73&x0r+dYgmH8<7wS5?CH*z@N&*QH;Pd~Sq@6#8J{A7ORs z+`|DPT4wF0X|x$xkmq*yVrn9{IKZl`7qn!B|4Q}LNf5_lg{6Ia1%Fijp^JnThhLv5>Dd4Qo>c;sK&l1 zxJC~xB}u_vz&MZ({C(P8by(}O*os!raZI<96`IQbPsag6SrBrpVqT6LGvLELvt>M! zc>WMbR=a>=SebEJncR&c$0e#y?h@z^4*m9a4?%hcyb{LD^cE0t;r`zYub2N_-nIxn zM@<-LxHH0t@Co0#@)`=0uaD+@W%bz_hc8~ox8R0+E<~1`~0qpR7)h&^K`nWT(hW71k!$(kCU)hOXI0=RUfa@24mM zhumDI@|W#Tle+p}8=g?4TS7ZJvDPZX!!|TSgSBcLXUz;(Ch>6X=y0ah+ zkly~BQ@wAmjb;Ti@5HWRVv?&3F>uign#;o&AO&L$&iA~<_z7Trd!O!FX79*6Vs}eV zaJRc;nZpA&7Dbt&TACOf$|h<*HJh}3Z^3FOsugegBZWKjqLlCz zGw$qrPUgPC+K$Rs&z*#ZJF@SQqc&xMIM;)HQ<7<@G1!lfcPc=guB3{FoW z+wCHi6RqL@IiY=A(WnwcfjxFY{CTx=|3&qsYUUw~xlSAQB z_ck>aKkQ-CH*I8wNq3ZObyk0P&osd-KooH*Wsd(|Cx`VZQ?njOz3R|eYDGYKZ71%f7Igq?fnhP&Zia$^9 zFpT7|t*+-@8u$8HGTyDy@h-XINhJ}DKQBAIGnwCWy#`))!N zgd#KF{)d3Ag|a2DPbZNag49E8!Da(|Pn*w-Xs}oS#nWbOr=QWJUv*nFISvk81Q0)U z9gWdCVP!)*suj#k009xwKTw|Vqk^#$is!&Wfzt$BXP_i@1UlVV-Ew4w9_$@$R47!i zszZwgPH>R+W!!z>1C>mAS0V;xS+}XQ(2Au!h%luN4f+Kc?@wg2S703t?IIrI&zPTHiS#?VDA-f|yeT`Lgc;)y`@Lthsxh6e z9RuD~Z`YLznp96%R6&o@A-c8*73tyVPAe4~q5=0=F+w7LOv1z5+!GDiIJd?kaGxx| z-0kT=g6Gl8r46i|?*>5Am!~1I4!kjI6YG+nS&DWoAuv9=Ti(1+`*xokH>SCggdhI& zz;;liUi7)Of4+Tj!SwAi5V`N1cS3lpGga{G1aX%7dP1dhe*Zse(~h-H73xEQrTBMF z7(I#pTOZRa0~Mul0k*vhRjM~AC&SJfPj@^+>VyRknyAw>s|`l%DT^~0&R513LNeMI z?)S&yg-gtNHw3^QpcW7>Fr}mPHH{@=d#vnO2v54;FnVs3KU+!o^z7L=5`VkU6yKCn z&ja2sot*DQhczEYSv)$a^n16fxfF-2kbZK;8$_{BcW6%;r!`Ndz?7mE8(madU+LeO z{YE{yY3#=@>Rvs^@txg7;`t@>zZB;FI4yvm&4z9O8N4MUP3oyXye9NE#GZ7XlVJneNqZC*Hno~u-7|#Zd zi-=&X)hvVTYdz&EVp{!-e2l|X3H`aak7||a=ZCH5gA~r1-sl!yTgNnpn{TMP~HX4tzR+IczX{9T6u9t8(9SzY~xW;@>yg{;Cz2FVsy1Q)SF+Jt4HAe@9MktEsqGiu(Z&%&sar%n! zyJbovc7ptF^}>}Urbfc&&e9c~kALzFlHb)fsPh*U4r7bTO2|P>j7h9qanr`)$nXF` zwzK5jyW(p5l0sK?>mUuU&mky@p)io5kE0}PRL(FpqVorLn4#Fg#t2v?jpm3hSF72t z3Vp7g_YLW5`do{xC|N%lB5UQgB2y7^$>ts4qQh(Nrg+syJUdRvfT~`55)? zowPwLyQEh79qu=xQ`p5pk>R|;e%$wkh)cNg&1%i2BC!TtLbDX(zO%dBB$zhr*3G`-WknbHGx zj^}^mAzp~hUN&15_rGX{%-G=5x0{RP3r8t({%T*fni+vTPUOo8 z>B~L8^KPFD@D*&woBvn-xz^kWyO_YXz5j6PUlPs^%NZM}8oWqxZ-&u@F@@3`PmDs1 zZ}@N1Qo>iQ)_K{*xIUShsbq}PLKerzjrPOf$nQfPkD?se4B}{Ui6M4%b#{w5z)Tg= zL-ZFzFnY+*bRmIu{+MHFC$!sBNhfd(k6nV26uo6?G3Lzc`Rn)dX=m{bkI&%I<|h$d z>`xJhK3)ad-8U3h7??&njWgi<3hNm<3!b}hUke%~Fm?FA0)}>YP!3OgfJTrf zU^5E^o-QiPk}-RS9ncq>I9Q1Su;C$~PavN*0ncs53`Ey}asu~~Z-*uTFoaGSrRfq#wQ;aAk*%CUq=|viOeV9cdR4X-AJ6*D0^M zP4uBelS88Gt}H+M5xt`0j+7i4cC{dS3!T-;7#>4-^I2cd-QMA)z|;g<@W9Z5^;Zc< z7vZLWbJsayrlTutiWU|fj)qohi@51eb#}g*hljBIZHww4|GZ52LxA;D#y7|8or#x;u6_vu_fnMim|+BtS0Xo!3~hnNlO z;Y=+({s~d6p?`jsk#*XzM-(g_caHxQ;86WUK;a-C4HwYA?s#*ZTbgDI#{beN!|;u zt7q%m&plN2$2_^85;8+b5Z-mrxo&b+=VDPwwe@_0ANx&?Gk{dU9J?lMd8?8;h>}p* zAid4@yCMP)w!M0u?AT;`Q%_UQQ$$`0&jTJ#@wKbwOOA$K(eU0Lz63}wTL+zwhj=dI z=>*yt5w((y;?Ck#TJWcj^-oVeDR2;?X$K`Z@1Z5*;U93_K+?ROz#(uK#f@LEWGSiSSWcnpw46=P=$1@ zveIe}*;s>x2N0eN}aIu|7DUau;pKWGZgjzALJ=7*n+CpqQ{6 zTbDB~{`fJy3Bp#b8-pU|&mA7huf-3nXK}c)R}1Oe&stauuVe>X%%hJ)9Vi8&?&yPW zpp!R89$69IryIhZHf#G%hPbiiE$a+b!7fK+&DO_fITgYnLFy>$hIFB^=)tEN*hQ8e*o75HYu92p zhstd{5}i_>)Iur9hq41Vuq(0d9TltlGglm~?F%Com=rYq=8y5DXcFxu1H|Btt=rQD zYPAy)|5HXaY7iqD5w@QP+;g`lWr-R^i}=mrMeKc|*G{bloSMDUee&v;AXC|gMP^QH?!8vL z+i-AcFG&4^Klq~ZMGkBJZDXaPZm*uLW19Ti+5tpf&R>+|iZzu8X8Y`SkM|#&mx6Y| zfmVth=fA{@WrbaO?|{y{Uwam7wT?YGyP0x=Z7j7^D_xEKKu#EM!N(tC|0wGGRgBzp zSN@2dwH|GLn~*p6-4UI}Qkb>ufz=H<&JRNQ%WxryK9a9M9`UkgHE}gjV_XN;Y$MZE zsw`|UQ7Bl=nMXf0z0=&SKp6@r4}@OaYJV)$x-)OIg;$|kj*y2fe%^G{1$Bo;N!oR` z6Q(LPg3qS!ym0fAFDsaiFf zuQAJ1*tQdlJ~u-jt<9cAg2UJ9%szC4Cl4NgM>%SRb{qI_5Nw&5f!!iw;|R@LHwnp^ zFu2L=Ln0<@bt@4(9c=`-pg?er2p{t;Kr&Imi2!%4iO!oKCV`Li27IRu6AuYV)06v{ zz0MV*#IZ;ZVnvzL{{Lg|z2cg{*1l1i3P|ro=|x&VgaM>?0#YRgBoPrDDYKE52`Vv& zfJhMtLL7!-fuS0#V-hrUkYNBt%Iw+64pjy+JM=eFa~}44e&^hui|^`4j3LB8$Xd^O z%D-I2H22}!AB1?_+~fHjatI7-R-iLW0|^SS19M@2g_=2yvkBtPt=Ux>=zdwRqnKz> z2Z8Q;e-so%tB}JrOj@%WUxijRzOIb5kDit-QCiDBS1{`kFIH!x9c<`OMa!6S+`rYn zG_>Qlx8ux>hh> zr|<6=qnk3PLWvKtb{xu|!%=??1mRx%iTm;&k^NV;Thv;|<{+ihzBsD(JV9xy71<6* z&D{d~I+SmSHOK&0>&00Ck+AYQxNR-LJdyh@qPFs#A6fjb--8}_%}?V!Uty|Jgevsh ze+*502}T}gymdwYzIJHj;W<%OuAds>OZH(I6c;oZhx*@_d-A7^)OvE`_VEJUAl?Z$ zx+JmzVEye^t!FQN16v(N#c$iG<^43zIZ-sAJA3rhz}a3Pb9)S{V|NfIa;I2r%?5U>rmJzR8Jl(@d49(a6kq~CNN~U*I}2#Kn1{=jA|1+ z;U<=4zrw9XyaAc84S;g;d4QRLAxB#(_V%I5BKIM1^V&I62V@A>VBWzC0HoT|MHa{w z_$|Ty0_k1kS2~)k%Goz>2kimF%A1oICR!mzS5(wiH1@{*V2#qy&yC;OO3QT};?pPe zj5L!pUMQc=fZ{e80e&f5?$_FyMX-t`I|6fatMY@>4)E0@pokhjVh`L)XyV?JZgXco4(0z+IO*o*Tt58U5>O!r zm^m@F2TpTVg-q~dE7AqwuZ0vF=utU+t&9f}CZhIBy*czJ#`tX)`P$*AqJEkfwfUW| zzqWY|xAdRSUWF_nIEGM~AdLstF+9Zl(us0qe-&me60dFbqn_t^0G`avH${+IqviDK z$EtGTB5FXNkal*ES!&F|>y&lI!Jr_E)v@%@OrhBt5NQOz;DwlL* z{@`-i{o)ZlNuP63D1nHs9Km6cNg*jw3CTzDFik6zRWW}o4q$sr;Y&iZvOdQ@BDCJ- zcqpixxvUZt?_xczUV?ME?GotvPDn>_P4;-KW{`~*&SmXxzMQRysk3XTy!L_8DpI>h zcwJgW>2sQ~cCm5%1#55x|9Q0``}PS7iZFzc7tjWp=>A6*Cg0L6& z9jfsypNagf($~qP_^yz(+?7TzJ*`OB4vE+hH_0UZmWce*AAeTyPP8{v9yQDWlb`@h zRaN=5)*m6^8DvkcklCAcRe)hSqd1`VtZWqFdn0A;Etoysm|uK8dJ*u!;O#}^5djWq z-tdL<1q=zGqvFS33gkdO1c?rQjL!wBJ8K}yw0At;0*cHO%tJtsV;exnfCYeA2Nc`; zasM?WXXAb0lo)%vGX)0?n8-}E)5o|sK|%w-1E3U1;O)oxhCEytb3=V!QtmKbM>Hm- z`kNH!1R9>aFZ)wV1gXuB&ZGxMfCC}1odiFi<4}#Hk7ypVi2^|9ZIgOkOj9VJ3a%g$ zF_Nmb=N{kh8WsgeUQo`7hv#eb{F(Lx1H(SwwrzDn=4LVHGCMOn?xp{(vHDwK&wli) z;D2$yHW>QieM9VFu^K$sj~f^mOn$V>h%;%h*3+G>8hZwW%d6n+QLK2r7!U}B6l*1L zjKA@z&Ly7BxaW9Tn$7JscbsI2w%XFE=QrP(cvrqQQr zl!A1g>R6x1x+Odo3i7sBgoJKN=A|ONWRFPM{;K`d*j^zTMqb|=w?2&aO&zvT_8xn= znO}EBy|mM}3_w>jM>`nh!vFANnLEBzh*4IXU*1_~j+S+#p1AQu4Wd$>{$HO9e z&pp(9DX;9UQ$m_jg0?u~wVur5uGHC2f&QM_Uz6W#4s@_DLA@vUue7RRlVyZYgqM`Pp)_ zL!A$mGEpm=IDOH1$6hPM4DyJT;`);gg{W$2i^;q{T(nj1s7pLDhV#5Br}xy=PSV}) zG_EvW_t>7{+G~pcMKS8Vdc{~(W;FY#bgYBULzPb%?wdwYCV8trxo?_3ZS;#m4ILe$^8?)vuL^KS#L#uiCB-zuQNkfiR6@dy}BLbu;WzC!us68 zC_bX?(gy%-0#+Z;CSvurH}{EH+kDjjc+Gy4D~*x*OXAcMk=t5e z8mZFv1zey|*PiPS#Ow?PI)WO`*7gn#%*62l|LgQ&jR4n$^8Jw2jeZ{7PMPA1UBI&`x2l2g}#520G(j=K0ggcBy8W|WX!Tt$r-SWPdO9m+%|mv+~Ira_P5ecHCFEx``Ef$ikO8M*OI+e`M=s zx$!Fs;D|wRHN?sHrf(}M46yh*E!4lif4Sk6{yPl@@vol25z(Tldy=dRdRA&>l}29Q zSI(VbR-p~UlQHx_xtKZ%xM}9=r0M{{1m&P>al5nYzC~G=Le_yo zNnGD4S1sqq*)G+0Zz{fs>pppG-sEw3Z;aw9(IwgS{?V93DW4=)q1-oS+R8Z+N4l{HlptQ;i$^^y?f)@wZPHfhl@PPd%?fwW{VXi;SA5r9wJf42Q`F-e zhXp?Myof~*gSx~SgI>qVJfH0AukVXVym`T*tTF&Ws(YyO@{vN4(4X3~YN{^_k>JEt zp!d{Y=TFb`o*~aL-gV&)XrH!%YX!EHml_ykzJ*MXyY{IpVp?1Hv-ewMD>`eElR}4MgkNu{B>mE6#dsx4Thd2h;5%2=0*lm&?lX+|T8U{gYBEmWu1}#jm2Q=^5@Hgx zjjfZfUKvys9~kxpS3l7Fr8v#oX?Vq{`7n$6p)$GZy-q>tAwtKRK$NTui+ETG5^lZ( zhypjU22g&D+88f{l=-XCxUVN~j$vU}K(Azc=;P)Wv1`>yPZjP8Hi z30!fB@;{z;#f6*MkZFGTQP+gD$ zvxst0d_n3}(2!9~8ov{D^mp^9RFRvKje@DrWKB{JQjwXuQ+tGI8Fe75VtU!($yfEH z+nru+DB+ZnzWYh%dq|-sOT(!t$v=sH`}X(g$9i)r#b8+eM&VPk`)%1jo*nx=Ts$_d zJ4-g!Z>B>&k<0>)JTxKM(3ZioK@d0)cW5LY=M*si3#qkyJ?No}+MG&3fu6qiFyg4` z!<6)YfT~#Z1&-KrR@W5t;?|!w1!-AY{6}6(;0L7=vBtAkfG_e=weM87#$58?oLCA+ z_Vs|?)1~BpCc_Y$&Wwhv<@xoFIWkCrjm7&6YpXrDV2?0|W?<^TwL(L(!xpK=el&IR z7CH5hVt9!Ql9A3J={eP4vq?`S7;?+fKy{fb}c8dF@+~?q7(U?={+1~Wx-iS-UkS~&@2`uw`q5wQe#q$`Sd;0mLccd_&HjFNa6UR{TeWa4 zyDCyarz)F}h)KbiTphe%K{j+aUj}+aex$POJvR?bigwJL*sR0aKa+7oHS)k};ohpH z4=vVcIfF9`dJG+)9}@2$P^wYu8&ZC-+j6GQ#` zVbl=ybz%P#>)bTE7SX@$nP+u$Zr}xe%%d6x-Eg_vr4DAtz`RUz?x~P|l#x{+BqpS! z6jXaxL-j>*&-uH?vW>>tnq_z0tug7Ex-EE ztrBxmMrQ_wA5wY0TSQxgxOGc>{PoDZNtDfr`)h>{(E|2;V4vcF^g7eq7y{benDckU z?wL#Gsmsaab{XZJDtyrM)HlFRrsS`JnLpJm&rAkB9{|bgR{GdtD>ialM(G`kGE86GR(tq^c z)`MZgyMhGiweOT=D&2*Ig)H)h8iqgwQ2W>UJLrQC!+f3%$X!DD-f?h!_kd69C}xm> zMglUJNHEI0(82TLuy}9>7}y@lLh<}S@RCdR9lKHYweBIYuf*`OL~NvMl+;U0R7@T4 za7PRWdMe$Z?Wh>`_jxLm()Lev6v{!Jm6ripYi~ePip9E7&q`pKqy_?=1I)+CxO%YP z@Ct2T7+Cvf@IS`hfczPDjARq`Z3pQ+FcwD%9XrwN%_PLwPV|G>AfO1Wks97`py*@9 z%aeJ!NGUfG%p#B&0!+GXjOV`oTmpi_A5)sF#&`S!8@|*JgEcC*gK&*j*D#N`2|Y9p zULQ&K@oVt)qcu&=e}z}`wsj2D)%C6D{c_J#?M#3*_<3+U#JYZ9(7XR@jgv3d>Bq-U zMS%SWFS9E54Rn-B8?7$ic6peoQbOn+6T}Hr=57i;Jt7i+{12T~RHs$6N;RA{ujC$y z``BJ}*EsfGt>j3Cg6)kvUj(m8+KQ@#b&sf9bQVoolp6=A$^KxU*0!cDwc3*NMk~qt zw&Ev_@I}*tsHl?#bESnD=d&Ited3=0h130g6F)`*Ote9K^w5SQ4iR@2x%M0Kf_>Qh z2EYb;jz}HL&WJFaOy4$|=Y$QL7%HuwcYimg@k@=9W}SXt(k}os`{Oi>vrk_p_|fc( z27B|KooHQOY_G8cvkH3z_UaT39J(3r@DUA>oGHj{7VDbCz^dui^J_Hj?e{$AE#RAj zlqC-Ue4H<|wg&!}l?DO;N1%FR2S9qm=LG<8gYf|rgP}qI3=@xtgGyit*FisAt9U%f z6TRUA@C3%#jNJy{XOthj11va;We--!JM2IPq(H#L;N$s3!mau*m9Ify+F-lAa^Zqm zF>U_aHph<)YwZx=1Oo)@*z0{BvpRZOdppt(oKX(SxuhjcO~e_5S3-H10%=Ap(^BhY z-u=4A;|1V)eaA+2h9+I2Y;m#MNG(&V?x}{x^rA{!^Pve?#$ zZ5kSRz%BDnp`a25*Z~VEjIT9jbu4ChI9Pz7YF<*}$8J?R&7n%>zLqP@erz^r(A7VY zpWJ*Q6s2#R-u$*kmz}H;;FMnMY$!W3s{Wu4(wO8>I{1N4T5lCaCjZQ}PNPl*oJmct zd005cb#Ku0O=ttt3=R=RrSm+WMfp|UXF*fubFj9vu zUY%j38}}w=f*xX@B6s>fBTD{H$Y^ z!>sZy%ey&L!?*^807^_j2sR_$~^w7&Qe>7pNX=FZVP6|*P} zKwQk~xeJMX}(%30srLj!&$zUMVJ>o*57!$%$U*10?r{tmEQ; zS?V}g%sdz2o{-Ve0UfP)mrS=w$?c?=?x#|XX49MZ{yKX_T&3i%Jkj)=LTW$a-cr(* zv2TFpv)7X+<$Kfu?2P4_k}xh6S+b%JKmFwK^Ya0O_~7i)*BY-|42Y|T+-K$ls2;qc z;WOxawVGfDvX!4YY^YE_X0ME7^}PrW6w6RbJ|_OC;C`~Q^jdZz&|qk(tWB~p$vU4( znWa5hKjrc978oT8-In_HeUD9jA|@koaq)TLjSw6Ol!#3x$$s_FgMV(${V zz%{+0apvQsdnxRPZ`;AB=rca+4iu1vN9m`~XlQMuR062SA9j8+K5v#IX;2YZfBCr* z)W-<`ss}S^JIa!wRWrQm!oC#CWS%jpojx!c&Ox3}Yj4m5v8XSsVMLs<*St;|N@dbc zf6QLr@wb}vZ}aiKaYl9_&%{)X8lLYPfrwUX3pu=g9Y=Pk`b?$7{yg?hI_M`4y@30W z1zzeT-D#KjcHGBc)#CDb7u(02Ox4x@2#p9n7kH4fX>l3bC35Gg0Lm>|GEe9?!P8<@ zQen2*{z4(bAUPuRr&g4m;0J;7dm_S2g|)L1q9;pCFQ0zAbI0?DkPukepM4f|zW0xA ziATAG2pdhSCc1lmY;bR=Swqf3Z@OvJsRwR1)mEuR9v%wG9~s(FZIB>e02 zpUV5FmVEZ4rA(y1SYd{eRbJk=V4q}uMqK|mp(nil}3VY|BxJT}b90HV~+!EyOC@VUY9!Sj0;$PtH<^1zRH zH~B6C6t6e{bV%fOgdc*|KwgW>@x?J#o+p4M%)z1m6EIjC#+Hno%QdZkD<>Fme7^1U zC0{o@k^gLIiqvt)00lpgZ4QhGv7?jo4R?i(dPEr~0$uO#Xc2WK^ls){w!?@62IxMdIztqS{eJR%K5&s(Qxj=pP!ppQOGci zdEdAb64&xuG2Qzkq#_&sWATB=(89jb=0ShCXzMzZYFW81!p%!>7#Vq@urGN1mwbRe zuEn60ek(2}wMHpu|J(RJ9#CFDiCz0A5ErAC1MCo_l!cy6x8A@0(?9$0A+pA3K-UKG zQU~-fsiiEQ;}!3;r3{}X-}AF=UsoVY-{+;`p5Nf$+N_<*Yq*M^q6ZR`yS4<`Ru9h? znt;#usp}Uo-_7(H%0JDn*7??e3~qVguWPmSb+O^&dcBwIx^;0)Xtrx#u%%w#i+yp? zSLXNfZd1DrJxiVzUMqO2liqDnuJAP{+vHiPj?BkG@`-%e!j{~J2FV%g=b7txg*Zj8 zCb0~=YlT@>eMh!rO)SQ(3n`{&ru|MNL_+0q_lTgi>`NV`7k}POJZ|jr zJO_ggQ+-k4ZXoxFqG@C(o$RK2wNKTSZm)TwXjF13mFbl+e)r~4EA1ejRNj+h!{<>w z2rTO=3vqLD@i}t~+(CRtQy>&RALOMyX9g{dDh?H1?FD-fI9>-&A@sNFsebjp22~`Ch_e-8D;{2NyAVU?;Ekf00bs^2l3~*Z@JfZ{PApGJh7LY84pPXL{Q`lS=xj7!9_nX(ZHIVycgR%q=ozenxaygV=Sy=d4>ar(3&Aes!DAH&R1hwHSxOK74oSbt;?n+X2~%k%%>H zufJA9;@0K_g^K*Z;lR&ap|0kQhumd47`=me+Tj)R1QTCgx&Zvm}HqAxhD)s*p4M-dq2 z1a+6JY5Z_u!#`u9+;OPvx&D|%flk`zY-;yy%;&A*Hlz`bY!w0)U+cIPTJjdnu5Nof zr2*>VO3cRaNjQXVzbPx1mFdW$oW4i*Jauj+P_$Jd&uIEWF9xtck9D=sUY7%1mU7Q& zm8gFb`@>f&*@cnQD<*nU#!O2*%wJdZNUzXMoy#(-i5}^8k8iq;3O*6>JyIyDcQU9e zH`E-&2qcW3lv}0Dspp>5`wD``>{K*pZjv=mmX=%nc)IPhZ)4i9OI*}q8I-pDc+JR^ny)vQnA)VNNWq?k^b(iqKBjDUas?8hHE zO`d+I`0^M~E$Y-h&E0XA31Z}ZG2=P5_wC{?wta2^prpWE{+(_IY^b z6dV);Fsgiw=U-X@Q$7GY0M2^{&kIF+o}oaLF1UcZ#<>smdm3MtoBm^*F2QTD9LSo>+G4?fY_BMsWSC`? zt4O!pA0{(237^2u7}sQB1GCH@bKXnYDz8Z@cRtM#Q5(M(aj#_tQ84`ttfnY-y~_Yv z(^Us*Eu7#9`H#o%d7RNLRW=N_hqwSXUxysYgiG%(-JAq&7jPn>dpwX}{4%3jzYo$g zz(CXHQb#pKobQS|$;Ot7w))(?X(KXr^73tu6LNoK<+kcte5WmxNzb!Aa?T(~Buyzu zOvS(RYmURoWI>@T@-L!#-Y6vha5Cv;Pk`dd&9cIQ=<@opcb6g^6QJJ!Fdi;>vU+nD z+9f-%*S3J_wLaO;G4@82G7rekrVbU!$ zo@ArAhKdeecK~snN^lKvn%f2q-sHV0lj5&5bRb}_&o>0p&u=#c9%4Vtw;2!H{OXM& z3P&fg(eqa)Npf>b$v>@>)yFxS7n9l>K;n@x-{e)-AH%C#?(iGnjGkM7<7RqXg$8A`G*T-gp!DPKHrhL~qQpBQxhJGWOMo^j;Ib?rka$d1R8S@qwMAs{`0OqG z6dDK3jsNq11O&WX0(=Dy|A-0*i;8ugk~q96E08Jh%tati=;6|TzXn%=0>UB!0`n%v z{(FbWX|EG0qFN;1Q&00vUkzFGNc_K-FaB2l_oc{c4lcvCUmn=b{qLn1ZaB=1I2>NX zCD>tO)OMrzzxRBCYgggJ`)xO69Og<~g+IB9$hwNiI10#kiUfIz$U2C~!fjXKPFDpN zPmw?u5x8I0V^hXeB-m3###KSqVO82w20oQ`RFH9jXArsNswulAAnV=h;;HfZKN|3a zE-oU{@PNNJWd#5GY=WA>@Fh=$plt;g_jv~oksucl2e?NNE}F9c`R|Qw0ogUpPTBux zmhMSvc9-s{D#*I`$_Pq#m%`n_dv==L!FvSh&fvq(1Qm#~-s408@73{J@UO7<#<+|3 zYADgxLxd>ey*hu(do_({4xb2lzk%DK@W|d<-o3&9QE>5oLvVj{_=CdtUelEIeiQ6H z?=0XU670RIaLZG(^#A#peP^+sJpK4Ln<{}W zjpIqzk8V^XMO0|pB2C>Kl&|Iu$yB}03&5L=Odr)e;I<b;oPZzNdb}iB?_8{>xLa9ncY26Ij-h>K7;9^A?=`vV)dZ89ag$s%#}p{Dm3blX zUWlTuo>|6S97&R~$>30#@wrHOJ%NJY;3pSJPcF?jb7e-1eSV1jNo%vTE+<}HQ9yf2 zL)-MmjnN{#DxD(5-mRYc=6n)Gz`02B{yk){MPzW$ALmIvx+B!)`=sL>l1!glsf8fQ z(<|%SYL=?7h@}EaG&v|r+~ptK$K5Bz-7k~nCGvCh^Y3T+jK@y&M#ZKX9pf4#C#yKB zEh=m1n(7uy7mGceJv}XyXX5&!=8?v1GnuTTNj2KXQhKimZw=Yz3?-$G%rUtFcAO6G zK{M5=`GBj-bNVK*zL+wC7vqNa>MT%|cWBIS$I>NabhJ;H$!4}>Vmpr|CsiulISE!y zd3R=o_d=yDTwE}&y(j%dK3{$A7QM1m$V@Xb3KOXy*ee_%4k&jA`X(A#Dm+d;UvR6$Wi+uDP8eGLP zrdxy`9!nSd%eiaZ=~%k;T_37~?2M`T(U-Gwo#N*Us<1;zYM0NrXoK*0$>1@gcL$J;rDLx@g-NsLy7WEe@Su975yq=NIv8BAU&b0JJ&!P{*0YpPJ&caR52z zZEd0V_|SPk@Phdv^retes0}OY;@nIQ&y9g@| z)Ns4)?TeADK|G(mkKV$grnYzlV&wh=3Jd=2d^8#*fGsw!CbBRI+@1F2WzI@V^Wr`$ zbf@LO=TSy%Go8P~rO=xh?bVU?jN~Ub?~ORiP#K74>+8(h&YH#e)Rn5m^YDSjZsBpZ z6S%YOy9{K^GR2xn9@J==y*f8&H_*672z>`?tLP9ED+QZ8U!>d4D$1#t^8nfojh%{(~VYNHi7~g2D}= z2vfuqR0`%C%5y)!dYt68_N>1)7!&$3=y`xuI@7=BnHQ<9X^`br6yja=GI&wuex6(U zCB^Kwrf!#%vM21KUJWVAYsmVg=3>fC$(WhXQrf0oPpxH(q??%AYqJjL-oNKYFz~F2 zA(1f6X$`GEg*^m+aX3mX)XjA7`dNcq^Jc%QqmU(r5xpU>K$p1kbQxhSNgDb|2DtQ|8b3E+t9B2j=7vx-?l<#bpyfb1TO6o+9 zh&u>dPS+@iOYHXHOx3bwu||lF5YfPS4MG zxw)QVJ-)1MnyB176Hl@-!+@+nV|H1the?Iw2p%6Yh_?N*eU^5U&hTkmN21h8KG*E@ zic<#rtKvp%Y%io>%7>CZrp;cHY<%?S+UQKVM#HVQB?%1HRWc7$Tq30&`A3Vw3T+FxY<7bwQ>G{jablO=O zViB#kj58&TFEcud1ap1tD`uyaOP?+)H;P>nwT-cyZqQT1VA2DFX(P$O=ZoYO`vzrO z7Dxf%2**J+2m6`TNWJfLMgA%@lF5EicQnA^ob*c-e~Aih@uZ+)k)^KpB_CYJ(+fkz zzWJLmy&T@j&beQ`Gnra!rhV6eiKAp0 zYLOdHIfVJvjumh`3DbBq?Y1u3ZnQRIn>Sj`8f_<6tyGWFuAQ&oCgb&9d@DSMaYBTx zzQ2vow6D-zd*ZM>qdzhetJ8HwyySyNSCv8Lzr^OV--#jQR%>RQ#i)LIs$G96$^_mq z5i`X1%zU0n^lOKWw$6NKHd-B64WJ-A6wM>c%e%Xb?I}DS3*nE=LT!Qg3dA&LhTk%U z-UcqV&2(q?Ou1^YOO?EOr99j!7ueu)#CQhEDAi ztG9%SlwKw|c)o(+w{1d7w$VrR1^D1K0t{xaB;22+QjDKApA7og_ya_~q`y8pSk6qy z2V2Ycj{C#KV?0o;jtVibsDFOGxe3vL+M2-a^KdwH2%4`4>0i~d4Ngs0rYYKENN>(< zy;s+ZWMCCv6w+fCzuVGszgAJdcjPH0Jf?c+xJPePx$V@Nm`+q|W8hkd=h`yuY*pxu z0a{{2d#xQCpS0Vwwg_*7UurvpvW%0+dDa=9{X*)6%5*~JuHY@fe&KoH+d?76LT5Dv zWrZv@`hg zpb!o%M=p21dSrowK*q!A!Ms_q(8$v)Xe^eQel4^ociE!rf?uAiPV$oyWpZlPxy2*} zQujs?PDAAVOR>%i-o>_lVzcshoBD>1d*{sc7t2;?+HO>F=SH~rj066{LE8d{;agGH z-1um0AkS||wqmYVmziG34Wn&T5QfZCtFdf;>H&q$Y36iLmYW&$-R4CiHnn+$LS$q3 zjAkNk5A4%w@rE1i9Sr>b2n0&JeLD%j3!L_jMYJ6gPEi`0xk#88S+pDBZbN2j1{451 z9u3W3_6C;=$1f4jM-w6^w(t|7@you;zT3vj21xC&kW2F2B}?YCYZ!XWi=v3}< z%J}wGiin<2V3B%R_1>aB9M5dEXQ6Srr@k+%MWdj6p$IK6YE-tJP1pBI%0 zLr#>I3ZE%`R3u8RuBktFYP`I+cLF(rn2ipN{b{Y)CkBN%_npa%h*59%OKYi|D3Vw1 z_)CjAw3>B*&kv!YrPf3Z>E7P{V!W+q-v}2Gro+UfQ`d(?K5AY5Qz_50XI92HXL=c7 z_2S#ATh)!;5kyi!drQU45!Mv?So{2L?$lr^m9@p1U=5~>R^7N-J;KOX96x$WXwTf# zOQ>0(`hG#qleHkRFqu_Xq`2!55ebaCTUb%I&)J!^r;s>HP+PdFkT+R!8#HvygDsY# zKDCCPGZHx!Vx{mnD%Ydy{dC+tjMC-Xck=>mKU2hmLP%nt>|FcYRujaoJe@{NBWB`8 zo(-8R2mN(TxK-(Do}Zd1sq2d5!o3m4@%OPg@Q!7fu4J_;Jt?&~I(Wsh6WiwzeVaqEx(O!!b+O zH?dYTruy97rW@&d+Z&5$`}Q4}ksj=FpvlRF9v6R?yL`~WZKK0c+_rMiLg!MryIf8! zi^$_)!6a`#63s(LvY{`J<=aD0+n(Bj?v*_k?@VWKS6Y@k_{;oiHh+S+g+lRhgDm32 zJ^|YIc>WfezYm*YpNM7KceL@BDf}I%&Z-9|SdnTCQxL>>TUh@1&Q3K7$(|UmjYLhX zHScoTAUf~Ep`PM5P<0#eY;C?cg!z7v4f!f7V4l81UtY{uD9SkZ{$M^gl4ypO$JsOJ z3_4ApG&g~ofM8CH9DnhzT#r7ad|!Z#+H9g~4?;Bw>Z_O+ZQ>)+NGa0EpuwI*b=zz5 z!484)eWCtdO|}zi^K;3~0nM;`cMc8+cs9JkEw-|9@cHctWKWdzXpvc8Knp31S=L;H z8b23km|RaS8u8<%QX2`Vo~Lpn%CpVMN%;nvN0r6hqzpLAVV_%QhIp7+^F~%GD9DYf z?f%BK*~Rlsx9wazC{Q+sL*ucm$Uz*A zkJ6ulguQxVYV;tIN;CFZL#gSWrS9Ec;rh*&HLW8Di3Fs)Y5X?Wv)9pVd20>oO~wxA zVyE;%1J!)+ib)>}W1EY`dt+w&^5>D~3St)*^h9TW#L&>VLTr38?hRO(BquC)Sfpj{9FfqQp(ZX%1 z#$r21^)UXuorpQ_DmPVupGe1U<+@L2-H?qrwz<*Z>Kemcmk&$aHH(~OySZfC?0KDFvC zt}oz7F|Qnz|BI#Zzh5lHLM*mY(w@?7OVry&CKc$IEt# zaOX)$*MuHOJof4B`pZvjrX(UihoTvrar@FGywmLTU6N2Dr==E;!$Tuv@$6wnR<)?N zP;1LWo2~cE&R&^WTPTKok*oqGG&h+`(M}0at~EW0(X7JuM!Us+sz}<+W8QZ397=9B z&l~sO>qtqh_OrWY<~PzXTD8Jh<^#D5_HZ+Y!)c?LFCRc1$>%Sk@q_G06bh=#i3vOw z4LoPw1b+g{;;|<9{L$Tm4nB#y%V>k*aT%1y_eVg}tQtkYK{j{5XEZ=nlY$!Iaw$6$ zJnT8**4D(<1b?IjnASjzSmw6ycPNxLehY=((&WThS!ro!q&Dp>U-B#K;MUP8ZEf3n zJERqU$0DLS1Wa z!18dZbedrX*SWvKy1z+pna}XaUa7FcgqEJk#Z90*F{u7r&!nMTq$Cxc6EB_AAW2f~ zLn`(l41Ac0Hu_$3pfhu}=`2*i^jZ{Op9d%56b=<^Ep@LycLLQDmEO2E^ZChI-Tfjp zpDB5HH5s@Sd3=BigIy+djSe)AOp4o2N&jaRX}T(ne?>T+Rv62>7& z;9@F`xd^G1Q$%}vEoAxdH3A?b>}w+rXVm*s_$_wiK2qN^G=66eQ;t)6ymwZyVpgX0 zoaJub2;Ql=j&!l5gA1X~&b}fgNo!g#_6X)&peBQhS7#8`9Fwipo~?W34$A8t%-%p~ z5|De}JqtCG$Xyegz!@G#C0k>?@*A6%aTv531wsxpwy`GS9#XQUqFGhVz#wpX(^9cW zSsEUDv_q)Mkg~k# zOO-;eFk1A)$Wb57X>J(O6q9ZT{fO1~FKToJbHlRJFCsKel?U#P$oa`uh;CF55#{%` znHo)r1m<_ii^{BxirF*8W&sX!TRjMFYIXOgGcNb?JYAiHDg_gys?23Fyg)pbQdp7ul?7V7RczxA%Kdk~KPl3v?b&5; zglduNnlgPY3u7tyxZzmRA89isf9SeOiM{Z>gF2Q}=OsB4D?ip*ap$y1;-dbRbYo1Y zbdtU6_hDc1{2&B=pG0af^+JUA(>7YDrwH!TlMdQ<&chf9pQL%P`%UWPAQNg-)3>JD z6&X@8#+NTOn~)dVJBc|0Wlu< zygU{R|DZ>PLPw$}CPF#<=3PohN7EJ>9l4Lw2Sz-KeU<^o=WYv^OXI^4iQ?_=&W?Bzam_Jg>Oq$oIWYF>^^NUIq0t+oOF}EeW7)K zc69qxH&ZU|m1Arbr`uno7DlP#F4K3R`o=4Yn$L2T!DnRZ{Nl(8ow`w*%Jr*gD{EdR z4C>!GD8pQR%5y3_SsvjyqGKO9Z}!E&LBL)u0hEc+CuU*b0(@S!-TcPuX-959x* z2hGj=g14i+8@DuTo%p-dN6eQ`Z_#%vtQKl}uc2j{`Y_Advejq}U5)T)*kbj0Lm|D< zBVp?!i-eH_ZUC3Y9sw7qHU^`GzFXEpCRZ%bZme(6m|Wah2Any3&PwwEgAaYNT~0H- zi`Zs7J;|5&&IL%&WG`ehmRzrwm@Q44fm(EK#D~>Y-Ql>CV2ZBAd|b> z(!RS3_EaE)SPO(h9)Ai;fRqb`!l5F!_}x`+kFQ6v*r|-U4blp|rIx6_#bT$p6weYn zD2wN}5gl|+%W{K0(&La9xR&Hmkv!e4t7xiGKuY?fBbUXr;hMA*;cP1x)$n!*UZguehxg=}%EAhvJB9ARl-4&Cf#Jkk2baQma%_S-nGvAV$ z!zPIR_p79M8jHcpM}E+t=M5AqV_vRMx7PR-vOQY&Mix_7Xe`v!#CW+`=*wlR(%|NA zn$tgKt_Xy5I=i-t{nT|S|CnsRO9h{93RKS)Z&R~2nUfk_5^C3IK@Y!*eC)jlFbJyHJ%6u z+x`Rykn!+nEMbC2oM2)3k&%NuG!MQAr{zAR=R{m8a49=hwt1)w?tx!z#xnG!+NxoJ zV2N80oc0O(Xf%;O5y{_=gj|gnIm+*7O6=I?O@O2<+i96dKu6+DYvKGvZDEO!-%s$) zH+Qr^0>^FMo`}Q|d3)*C&;%k5ha)m~8S`z;oHqLQpvJ7VEXl7zUF?a(j4280r))bV zZ-%)Mg`5~|Yc8Wxn(H|CD+qiP3$0gr=02Q#J;nx4UgzlL#49w0B-Qsk^Gvpux2-Jd zWhRgg>$m{lGSd7`6Ew33`}nOXW+}rUS*@97e{Xy?dd+%G<>w+4b}$65=Cjs}Q=5%9 zSZ+|8*_hjV6`+s@EiLQ*V)c98q~ze>)4|pfRiPzAq?TsVJcY5mygeRUqig#5uKN9| zxbfu_KQ=C8prE5^>M%d7o1HJR(?#NKuS*rFSvg5fw}^bR&i*9-e{u9Ka7pL+{=c{K zZkm~ilb1xD47D07yrgMql4af)tRSrHWX-H25C@wi%v)-)7OvRNz^G+Mg;}h*oZ-^q zU1QllRs%me`-8;`E_3kw@wOaP0)B6I_c620rj6y7&*%Mqy`HZZ%X`XFM;`rzj9fP*ja0x;b zJRKI0;cZF-RB$bH>2%2oMyg0*glFGG)0p7mR8V2p{+FhzbI`R~mCiy1x5r!rYE?-G z79r({ z*e>P`^6l`V>SbzvHY22B)*v$ohs8!y`n5T&0~s976XO91Z(v`!2))J%VqMsG^_d{3 ziFELuf5~D0iTs!&r$EdqB=x*2QXUN{?BTpR)HfFS1BSw>ZkW5KAW!6_c8VLz?l!!a zqlGnlPB(*)7l{gthDC}*n)zcuqrnHJriPHy59b&S$2FCg8WS$Hizf1d$@n{KFdrSa zpcWx`VR~_&`i z2$M(Q%s+NFov!V#{jmkqApZJOjoHZOSSi`)TWK9-XOIQixH!T0OGn#Y9 zLIZJ7jU_8P!b+dMGk`m@!0~%~=tSbI**Vbp@9t3b9jvOt*SAzgc~61X>CXu{@n`4X zyP-Vbts7qND2V9^PrkPxB^eZHzo1vw+E5D*yU?QX*f7%J@#_twJ(r_Mb6``hc&x(1 zhniPhnBrL3HHUMB!Uk9eYfPl)8bjC%;Y!cFNDAj`uDlR*Ofq3wB9_?2M!m%7;K5eb z?ikwS3mC9t!R)QW;czgZ5Y5r>zi>ybmT(ys8@x8O1crmifI^{;@_gOP6UJ5|J%Olrd-$>v@Zti#-13CRm;JCD!0Z0$2fqOFYv&jjFaNUw5jg zAUCiu7LXuPSjM@^+;@9EuVUq%Lss(g*fHUVGXpYm9)(2_b2@_q5y$0RqgX#r z&d0>*2a*$xV9O{+t}zD;+i%$kj;{76Iyi5i6b zy8e%2>oD@)6o$=@R!2nKZg~59iYRpnoLM)#>lyE=z^cfIw(b7;`Xwm?99@u#a2AcR zW~02VkeYrxj|^%xY~$2)(hC+N)I`%N$O_Da964v??dA}7aL$d#lE<&D$`esPXzs}6 zG{u%I zKsq2j!Doxh+(36!^5RN`!rNz4l0IwF-~#C< zk<73ctWq`dRo~P=Le-n=r%mfCyamxD=NCWb%E44xVEn1bCkAx60exKa@-C-3Gg$8N zd}6AhaL*shEX#{bG_4o}r*n$=ykZu-&WxA1(`;j3`k=Mt{x`wwBPVBqgTk9JFcNvg zjrBR(_e$w$vLdR8yll$-hMjbjS6ZHPIW#!dZzJ+mVb80cR|ikF)RpAKgzOOdJpQf! zVt^*m>vVnY%@7;oxp;jh-2=9#?vYU1C31o@S!(3*0r1k!(@$ zo{#D42#OALPK8 z&{DbDJSr(It1oo{h}>wA^7ZSl*%W5j^R)qu(MVTm*I?=eOdLjahgHCEIKbJ{IEPgu-y!8&hfETQ)nQ+PVMpRH!T{|M*sc6c z6Hma0XIZOdXw?=1EI>NPw8X)83=Q&(jzz~1Z;{92i3c~ucCkY`w2`li zx}_L|&SbbAsZ{zTJJ*sv52_{XTw7-x9SgHK9WZ{H1*cvST;k%WY88e+Bz`mvNhDw` z3IxKXZt8YGjaa{io8i`GuDDPj`{OtY3kS$h{Pyh!E6%6&6ERV~Ztxfps1`o?N-m2`VsS%Ge99^Nm#DA0t7OXhjYdY(V{zd?hXLZpwg-|^XI#0%PE@I>1kOBf%zE$+~sV}dc(Zj zV;yv)YSC!&)rH0v4>?ingwo3tVJe8-K!gnmVUT5DiE0GDsiXrT(xGPWTwSPdfscJY zp@>4^=Yr!$f8|bzG`Jd23nk!**pK1695QtvR;}z(N9PO;E({8IVxD~nvSvd|rrW)Q-F^Bs|mE*&|yZ=65nU(fqy--M(qX?sC z*rkmV^6e`mO2g{-zM1D?;iyFPT7AnDJE~}^?$H7#XMr7{NSM7dA>Xz-#Yra_Jp1CO z4Cv38;JP|Zx#HbQ?o1394DVdt`)szl!Q(g23ime$PBgUC`FXuPI`UJ<=yFVKz{9p6 zM4D4{P*6c+e`sd&L*!M>c$Pf z0*2g<_ALSMeQ=us8Y{rL$F@LL5jS3W_uxfjk>ujYl3`nLOLo~W$N7(NZiPqx$F z>Rh_pNQN*&6`N$(!NJcACg1RuhgDr*1taJ{m!Duj<_zi6J)!_6l(UF>WduU^$E`X~k*F4Zmt1qlopzTLFAWbLw8 zhzw095mpc?DB5bO44jycUkz>!N^3f(Du5$YD$wNNX?0NPj_IYLr9}`Z9HvdF!NGvr znGQY~L*=m06wqS5^&vRWix@Crs8m=6aSJ7&)3HsoJ-TEa9Awhr^ac4tr6LpPWSWYG=tJmPc`TE$ z#$hs2i`hnlS$k?iW}9%(DFZe#fml30K8}3YhGHp9c}>}{56lU_iz7Y9=CDJ8eS`xO zCAh#M5-(=1ya}KBpd@AX3<$LoReu3kLQ^TW_r0*#FMs%g0|o>tWIJ zL+C_QNlqxLGHLGl#HoEfuRd$#`1dU5HjU&2-~aa+uGi?NUmlKo(aL8%gIUbDH@f6- z(hoU}-J{(f&%XSv;pkrfrYx_<${^1Eq8!1XUO#YPb0fN6G^&n2EwbRP(oIhFyTaM` zIZ``#vm_y}VP#J_h`b5;{@IwS_WFKg=4H?2R%%LpZg?N^7e=~KFw_`bJL1~wSGXsh zy4CxmWzfK@r5L8!xzZ@I^6O5vp$Lo&Smp*+5P+#zk`vPIvJLVu`h;$5!IZGRvLJ$K z3^b@}OFF#AD>QO~6SFkLw=cr2cc#;jm>92^2c1D=QS@nvut=K;YKDzQw80>RRZ+FD z#j9vKES2bhIyW$fEikde13{$HGKS#WXM_kZI5M;~i9x0?noJb2L~4Mql)|+!96CBg z*c@B`wQ08|d|+W58G`&FL7ytmSEM}61NBp|BsSD=ZB%OVomg6WjLYd3sn`yjpcsIP zO)z7u0zOaA>%wAX$c=>jjr7G$qaMr`9^CTnto%GqrDTz{$m(VHJTYOEv+eaO&z@rt zp{Vo7n{lBO?hFt83BSF`35h1 zN%ik!VS9s9>Q@Y?0|5ggivw5Qugv~TA{FX6opQv5*y8?Gf-atr-xo{6;&98o+dnkq z>}q&>&-o@IZEo(MXZ7bhC)=t@Tk1iAkGmlE6`_AuQn9Cu>H60c*FR;UGo5Vc>}}0^ zW<)Ofy_vsVj>)}!_T`7$;msqvyQL&v;ON_sa;E#tz|@1$)vTMNdos6u8nMqmw_bQV z-9LMD&rWLbe0F}%>$&p#{|BSImoRrMK%~3});?4lEX;hXl_y)M1f_{*U07fSg~2uL zClQV2dVl!RQ+9RdiSh@-x*ZKgqrbLI&95!+XKL#FwpV!PdgaECTy?)F#ppng;FxIC z_-Qst&n`6sTd{>@&2Q-s*B9F2<*3&tTJki<)E6(4(E^4Zc6KGE@)sqnn)Q!&JK2Ih z91cV((2P`&oN`c!IxRq=4oj@eX2F`N1TEHq-O`ajJp$Xyq1LHwYAn_QD;w2JH`8KQ z>C+CWaR_h(7~(b;t@=$^-IkWPIzS4r7UYHoMtq#YN%AQJev_a*z{Ut_I01wyode)4 zHJnN|e7kkvmK8j`A=p3lpyn>}+vlWWCfG$d0+~Ri0^6kpbkI!35@>-M79#~%R58q9 zphOrANP~i>HrOrkm_Mp!Q4C`4hDP7n1r!gNB<|OeOSrg6&e;w! zsjx~BTJT?y9f6ZfECLj%IMSisx$CWSx8mk{S@Q(gJmDB5Y^m}*2XFn|?B%^atX|G7 zaOuBGs(24urd)Hjq%&GpbuZH8pCy6M;P%|MF=qAS`0t+}uCif8t*K(>N-~$XhCvL`ucs zaMPX5vS7u+!a(IEHjNHkCXQw^lll-b7oz*&VM)8oSrCh^H?J(TaPTkfpg-KX`q>uK zlFGr~xdG$<7Px7l!h0fM31;viFwrHis8j4054m~4sqBOr0In`5K{GcR;9}8X8Ct4Z zjWyG$R5)&MYK8+x1cO5b>VRbn3S|HYu>^0TDA{`8O@fU(TD-|lhkb#rRak+eqS%*2 z4jNEZIsj1|R2yBT)2`AOaNtD4(*`;vt`OF^UcT|!)hll64Tq0S8-Q$KP(d0c<0z8F z+Pk$U@mo3-qzgnDuo0XYNL~d(4od{bG!~aKfaIz$MSvZb+)QEd&hYxR>QxMazS7&u z;vK<1;bHdm&cp8K(^T@L$7g@Ro8yS2!#hqXlFoMcHp#+5eN0J5lX8H#!vBzcvge6k zToT+6?2R$r1!cuIjb*nRU?p`+?c|QW+jAUVdz}HCh_+xFj8fsvqpQj%^P{W@c}=zO z9m%s?xH`$XqkLsRzM?6jk`ri*ly<41>QwE2JJqm*8Xo(iU9y7M;lTm5dBf==gC7gWC|_<7dyPm%ZkBdC7)^ZWlBg33w=@w_(T=l83H;*l>m7p>#+FHk)yP*8plAYaY&TP zwID7r{Ttq&Le}BEa(!P$zi8~)<(uZaT-<<<_q@6QHeAt!0y9=dug}eJV%Hv+U1moX zn$qmFuq|p#PFi5i8Fqk57L&01VFRh^r<~H25O9?}*1*1tN>&_)h4R8$+t##%fAT{3 zsJa}VLljH&9IjbSz-rG+$%OM#6BWc+xa8nnI~fidovyY_i`xhFf~5_IdF#rR+^0u)37LydRn?L3gqNuRZ}`LwX~{qF>PDLF45gZ`tX zcTL$McLS?rjo;?XNL}&&def%M=KgN z`(}ei8b>|K&z_@_vG`m3O@Ur?rSvJ)rqdM>=rzV?#|c=41bj;dt{CKr@f+LL8VU>W zGotazg!xr`S2|g%#&<@mRJyQ>`~|>H>`P*am1SLG1K3G2UF5J1DKRqO`}t76rO&QXM(H4x+vI$S;Vp7u&5bY8-a$i!AS|s3edo`nngISfUX#X zzd^hLD0r?erON_tF_D@g7B4!$dbHY&ivk+lPhi^MO@kntg3MrCY!{fsLVAvYBC(26 ztQ&D)6iWDA%CjzB&#??hUf5JNhK&I0El$&^R!6IJ_zp9eF&KOY0f#LDUBQe+Fp6O0 zCXhr?IntOTIm|%cCdFRV%?XkyHrB^Tf1-{WHH$*_qSkb?ROe(k*FwsplKt$L{^R z%Z|#OR($T?3mpD6(X;!rvF^Fx+O{^=+u^mD!_R8bC-5UR@g8OWjI7)Bq-R%Ru=962 zGXGHG9Y*z>d9|%H!|l!I+e?o<7(wjJ9<9jsjF@y`9aCWt{JC51DQ(EtU)TLqoq&m# zD@;c;R{qj}yr7T`s)CNHQkijNDqXJVdGhS)C$a2pC1%5KxJNza;A@T@)_t zU&*kdoy)8G&~RsJND8JcOy(Awx>&RZKQb)>1nz*+Y6;%~TbY>%z$Gj`W`NJJDngAz zu(-%`7#EG=p+PR7m{9m8f<6;23(FCNj2S z5e^v8;T=_Azth6_j-{H{HV2nLQssdJy6G5l*!vvQhF}YPmbrKk1k)v>J&)(u9Mbdb z#v#F`esKt57=1WKGy!ik<{9-&0#Rw?ZyJy2Yy<*MhP7=O%^-lsGVoSz-ZUIMi$kKg zB^E_Wi-z}|D`COpfMnrsF_j%u)Oo}az>s1QJbk~GfCupJ2mzxl(M#rTq?$lJI%5aY z)S)6%H5H=B(l4`;j*92=d|O&sA;`nInZZGbnCBQ@rt2=hs<5)ZY_Hf~vOm~+gzQy7 z^R4gy+%FQjyQjJ#tl{lfE=OHPomXA|8c_C^kOJ9=wN-d>#2(amtzY8!eUl(i{HY+jH1iKnnoHb%Fp`q zlFlAuJ^QR#u{X$%xxE0LIqhD|)w~M3*f?8#a(v&x@8%YSD>cBu1-sArzplQV`zR-_ zlsR6RmX(wq%t=hvEIed~C*KG{Z{g-??d$~!Cj=)?uJn2vUKZg0#Z>Lh;ednra{*OO zA6@EPw)=J)OXDFG;}cx=C-32q%7onpk1H>?opOnB`?4Zf&z?Dv_4cRWGI}Gr#t6$$ht4%-G1|ZSyaEAK0NF4A636|_wS9a z7sj60Q-(f~H6GF9S@w^>6GzHS&i9@3Tr*be&wAJQMo{X;e*gosiWOXkJe-!E7Q-yyVLbKSo5tlqAPY80MBi^fcsSR_*)*|; zR8fLG=MIt}J{r>&0El`H@MXUQYvvV~By3X16envQ%tZqNGrHp4Moo;x!LE%9BqF)Q zbgMy^;aD;N1uUaMS`At?KtzD6oIwPV!3=n?8cQTlRXP|mV3Q+2piouAWe~^=232O$ z!uSMv9bgC;4#t`U6saY_A`dP@EieZP_lIo$^L&DO7U9ceSqhqwn*M>+RXz@ zv|w)llZ>6TlS{g=u#k7*u$s=T0})<3wq8W?0qlw!i%l9`Wt=sx$R_goob|6N*oOz!*aQzL$nUb0x9hN{*blaP$;01q;!U-Lw$1_vjPNF3?% z#?VWQj&?3xT|^_~LvYC|=mn$wmRQe;WbJCodDu`T7keb6>AybK+l0RfIl(B>5tyc9sev-G z=4SQNFJFFm`b|sgz$BBL!y+pW$CUPe>GkxfnJSCNcZNkFF6`5HB}Yy7uRUZ%QCb91 zMV8o!OulA+*tSPUz5Ox5=*)i$-S%P6 z42k8+arJU531@EKIg$1Fb(Zsc@7Zl94DNF~GhatMY;&u)4e8Ws$l6^Ds_#GUj0sf! zQpji0hS$O_M9(i{TI+V5jSmdVGidQ##rdw9UAJoV5?=d2+~z>aQoHe3O()hx?^PY2 z858H*{oFO}tXHK+;lZzvKb#h>o?hq=epb0-_=~zfxU#$7AAtzt1?9Y)7DiVbIkj)6 zEG#_y;e+w&71GAFDZro>IDd9fsp(*k(b6wW3ng4xSdvK!uOkr>M8ZMvV_>qk0Zonr z_{m`dKnlL6I5mY0^B&~$`1Z}AO@~9MBhc`i4l3*lYO2|6!K&yE22A1LLpf4;n?pc8 z4-HC-bW;uQ=~99L6Bew zMlw4OLS&0z^)14eD&}CR#iB&kF>nq8ct!!gy-P*L1jiztlSD`+E?SGfiR}L)WVHCA zM5%z!TI4SdBp6LVLh(1n5SW9*55@?VktZ3D$q1z0<}qZwX1@oIcTsE@i%biDF#5Rn zXc8;Gm%Z&nXv8n>-Sqo^*{ZEgFg^s;|;S9wBt{o7Eu}V=wufq_nQp32^<`Mbw>ocrPs5}xF4bc>1AAyPo z)`BHMLTp01{xp&4iVBJ?cvJob>sdK`;qZIo_`HqTcaY}W5KO~qSW0keFIz!vN@UL& z(|#0HUhIm8IoDyfX{k7!jv9tR>Y*$GK{6zp0gsA8WhHX%T+`rR&zVdY6mtf}1q)0x z;J1P9?69e*DngDi1VK9DZ9&~p&MOSTDA@C^8Wa>hdkg9|uQF)ZSb}-dI5dzU<}m>U zXP7~yRKeB{5HF0o7Mq1Z*FY@ZU{TYw=B-3>mqnMl#av2Fba}E?mkj+-khTU@9_U&4 z46_BqDw&Na0}WI~hxwa`=Tcb0Z(8d$=^=q&{=o@mT^dM)b4_XzMWhWo2P2X?VA#@q z?9$Sy0P$k+U9#ws4rK=!Thj5d0}H?uHfON^J!tXBj%Y#-k1t^zD>|j@K=6{Vq@E{- zbDB=AX0G4xZQS?#)F_K|s4(qt952+beeUYja+baK!O4ip5cq$e3Ezn(Spnoc6t?QO zUCXrO=(dT|&02g;TFGp+C^i_~zhZnoHWyGhyCzMD3JgnFYMvM$o4t7zG6kHRICj3C|TIV-l6@9bvOeIIh7 zaC|tZXymZ}uxRvV*|*MY&#K>os?ZbJqoo+P{>#Dj9*MP-``)OKtm@>(o|h~qPVI}< zfr$x%3=GaQ_pfbhUOnBM=TpDZ{r6pU--qV!O}-JBDwfO%jD6c*7PSC4Rde=G+HoJ_ z@^8+64q&IvU2m&;y&x7!8lUF}H%5&uG>~Rj=EtwB%sJ+p(T0%26ea5xxhYOGpIBmx)iP~mJUwS|aFmJwj6s_B5JGZ|zn z;Gs|lbQTEQlj%ev7=$W^mafL>u*Cui&n^~Q;SlR|K%j=J#o-7bewncjf?{FN!NqqJ z;S`2dOtdwtNWi_J=OhDC$!rSHiaO=IFW50&wAXjr(4)@ zrMJ%>4e{Y%v-1x$ZDFKm=JB0@DeYjQ@2*3J*CAOY^KfI}xdcoRA^+k)0zhrz4I79y z#$Hj1zCX2oPgsN^+$}e}`x<-qgFFNk{vd0-m-j}%E|4@Vadvjz=JFq|dS@w(n%b3q z`SL}s;<@r(+LM|eA)`Jm7O94Vul;@yoLjNffa(nAp5`pF$@w*Ru8o(oFDMtdplm}< zbPZy|7bPGO!H@;Y5OgW<1u?xsx+O84mYBi@#jD23AMB!LY+Ac;W;0rEsW#f=#!3$-x}^o;fJ;`sRBRyF6jWU-vclup6ZGcFpce40QQ;IAG+bpzi0vt|j<=^_a4+IKiWz z?wR>{)Z<9)3UK5gd=?yt_{)y$v51a>x^3&3zD>g~>fd>TT;y3^x1Q0-qX+Fy|cWY~5P~k!) zOiOGH;>0XEeCECP!Ne0~oNe$$_b0QCpYC5hEh_7B{j1k+{m87+<6$X5NBB?5qUc}^nqk}~g1$+DkxU}yt^yVa5OsbZWPe#S8vI1c;HH!Z zP$|G!n>@Z%U_1}cm2_}X3}OxsI4uE>CzbYLi{ZyGV2M(AE$}Mtz#9d62^;vNL9tK^ zq{6tCbS#LJ_C?ElHYPdlSSV|VCuYI?WSl?E!ldv9Qh*Q$M8C(@bYLMffy3H}Dhr{c zy(#DZe>$1BFJn!DxZt{tD`y^5eLwT@<-M34a`*Yn8f0_Uhzid`-GB3mM7=zDzcME2Y;`uOWcIDyrzf}kXt!+yx{&gGgkPSmOfp>r zaSE-*;kj4>IrCQcAc0XVIK#w}^FhEx-V((LVlE^fXcwilFA8{P_ygk+#@rQ;m^`2M zW*J2cLY-ci>0W>u$@ibSpXY}5{@|$-{<<)rQi6TfhP?YwXn|IkV)!P5VkFqKN{sMl zC>}_u-7WmxMCQIEM0Vz*h^CR>4mA`s1&n^0>2Y=DM|Xe9_Q&gcW}H6V_43o1zg4;X z&r3pme=_ICy1%~MDEmj0_rwTiZlPIY2!LI0*#%}Ftz&t5kq5|?4|JtC|GxCm%b(`9 zu&cx1KcfL~M`bN=;whZFowcP~1C$O+8iKyLgE-AJtsyDPdocN{(!KJGFtSQoDy-qA^;Yqny_@N6<1POn&ihy)ef%hjC26r7re ziml^ba<9?q&v!LeQffc>iYEq1kHUdZSlPIBL5_9~tMab`0j%JS#;psOSeNZfL!xA4 z6DBCoX&=UGtZa%`=NL5k+z3RLurOX2M9-Pl)><46bq5^JNtW;=B9^K4=_)LuIfY|< zn7*vsx2?HPpHeZ%fa`*oXSe)S7>|8d-a&2L*|4TeL=?=|hq6!h*6-=h_B!#!X5IRi z`+e%po-H|h09~HqJD|C>AN z^!P;oIxnah$W+b+-v3_%`tpMC81%5j4{smR>Jn`qgX4xI4`CB`E~Y6=vv=FjkA6^W z4>u%NZxwz;_Ml=P)(+I-hvTk|zFl`~a60L7)cu2hz`^60&MyDv*KMlGYwR}V3Ty6+ zdVHA@6BqM*_-y|1DIO~_xN1@IAQPVRoVrJQGSmG}FzR-BA9~gkePYPzIVW`Fmizj7 zkM(_->wCKY=$aNdL$0sh`NRX=P!+dt=6ClmIG;xm{oDM7&ii`ae%MtN3uRwke;ipq z(%8-4{=;xD2S2~YNe2g7JM6hk^ECQK{4H>-jPLr_9dB>C{&ic!?(S>#Q2nLw@VM;y zhp4*0?&>zWug4Bo*4EYce^Tc5p(rZs^7{Gck{tKu9sa%*yDJ06Bkt8#AA_o~QjB+I zB{VrWJxBfcP^8)Z!PkFhLC`_p=10bD_yr;GZLz{Nc=(;-@{f1T z>~#6JZEv>y^QBKk0=%v8PkV+B&p5vcDscWoP`6XA(|h*{_Z_q#PhevcazEacaP#v( z@3q1tAuz!kOxxim=juQfv6$81M>#BXICZhBG&S9(js?ty0d_o94c;Wwc0q)|f@7#5 zp${)YI9zpfK(rWIbE*hPSnw*LNlHhBPrx|{1cJrkC<4Ps(PxPT@WPA*{0q)<7*313 zU|VviCP93Lg%x-&$xs(WPUska)-g34n>H)}1@(^jiv)Z z$;!7v3>~eaCdO-Nprt9lU{d(T%Y9XERu0-}DeOdE8v}Osl_SaHGUby#=n#ypUpdHV ztOV97r*-Z|&{(YBZ2m4`antgPG4z*Du&^s13oI0V%PbV3HI;eoP&5nVK7UiF z1^NVsi&b)1lP&XWV1BYrA=x4R!nd-C{189T+K&W=?P_UZiK3aSr^b~})`g(Xv^I}c z)Eo_59ltR)dnCWK{MGD}!=w9eRLT3_t7JuESJQ~W$dm9Thkra0ZcAL(+@?Gx8@7lilB zncUXe#@`-=6kh6+f!TuO_5OA|0pK`;M!Grx*^qINjGGGz_HXW zHi9aKAg^lloCSkiE}xb{8^VAM9+@rX2Usr%gu(MdBlG$0+Z%KeRSu zbDCAk9A2(i*e^=Ba6@6TvIH046@mYAK2WhL46+ilPb5R$ZNqmWF35@>P{TryEX~;z zQlAg3(qggj26WCa38SwHWN2y?E7W7(p&p<4iO~=l`glP9^4Axp8g^=C-GX4JT`}xv zC?w7K*O3bMd}%=6HqQQx^XVcH$93?RBaj%VoTrkBGWaSL28DpH7jw?~79M07!knRo zX>9()8O`p<$Fh<9B*au(*zUPI*A%d*oGm##H=pf5oiA>0BRxO;05iJWloa`}?rcsd zbbkGYNWPZ&c|u@zUcdu<;J%qlY`^jc%dApixmU%-43dXPlf?@OcKw|^81*>2ZCi5G zgq#*kLrWwNg=$~UC(EBeE8?x%`z&vNeC_t`Mwc_YYfG|9(Y3$SwlNI#~#(2FwF{TG3 zMss3jlV%e=1%XvxxX%=hWVuZ4)L0*ky?C&Co;%g@kn=ONjX*XiRk{eW z477qm>deYAEgB&9sI{z8LDXe9AipNF(bY5}&ISW3wPte(D&fT9CA(v3P`oHGZ9*iV z4^BZEbWy;e=&%uVDxHn+FZ30-bRF@d!<2tKpN=u7wl8q=4aHlXc47$a@|O7E+ihfs zfCizFEzwk}f)CId5VaNt4)$?Aiw~l=eUT5lke;2_wMwknfF*E#1r`&<^A#}OOja&P zn>c!&OlMJA1#@WZs)HK)Jd-|JiZ`{onO+FzXs&O2ODu9rOzhWDU?^nh>*F#CcKv$^jvSQY-J z%?3Owejh8SD+8!6y%A0Vg0fxWFhMtn$!NR;vus--c}i4m4u`l36p^eT>VDe3Cwf@D zt@OLyb@=&{W#_aC&rZ}!WMx9>ubh{DA=ea6V$Khvr57Wss3`nPc9iF{&$e>@=rt~+ zm;lz;6sgHt$J*lN!ay7^CTXELk31nKk6$+c(st)AS7V@pUL%9&1m^%VzyZPuMEr?F zb;hOuNZ2hI7*hJMPgCF_q+4jI5<$%`;Jw1fCuVH9tbOT}F1Y`63V;c~q*qYkp0z>r zNn!0u?V@WnOJKdjoC}C9P$hanQVHh?s3FlM;q|M_ulM`Ggu7bPDTk+xk!rfUp%A!z z;$ez8g4J4Rv8g7jzF&pNWuCn9YOd#fUaHtsHG~bs7JTy@JNY2zfo-abtc#V&kzc@a z!(;LK5FuD)=lHl9VF)Afo}ZfLvEmNQzOCB@mA^Lvy~dSCceRQle@2z;4}yY=>bLiH zoP2iW=Kx7_X3gOcgvsCr-9q?R9*=h3*wHq{P790}aQ9!ooDdmMSCi;@O*32lC_s=< z`sFLn5&I<;S#yA5H3cnMggC0hiKcRB=-q9B9lJE2_GobNV zz(Hp>kLI|X#)tv8UDd}LR;Zp4i;d5&-rDQaI(uGNUj3+=uOGO2_~YX@%O8qjLj3BI z_Q>2m?1t#~R{{6G35CUd#hvndcPp_o?I5#gJj|&Yd zI67<%3uec8KAEYlPrh>Jnp~l81glDznJSFW>^}txpAqs=QixIIZ(0E#f!eUmu9aiaGI*J5I1Eun*P6sW zGz6RhJhH$|wwMO_f=f{F#2WyN3KUJVZ50?6c%i_=h{bJXc@!5C1*XA4u6$~%%gUg@ z+=wS=HSJ)Ja;F8b$a7^BCN(fVpJxoErWmj?YKjO6Eg7tug~df9q|eTaH1ns*%o8x3 zbp)bsc;)XMvF|PTzpdcH-S8?oQ1x~0myiE%V|6C-K-GtLM?%}%nOGT6ZK3Tfp`K+TD#c&<*`VlJj)3$+q=eEd%D^oes=cKvd0Lb_#W9+w>5ODK4ZHpE z759U`_jvr1@4>8wg1q}|mwR3Xh_b(Vcpl(_fKsEcEBE$j*fS~QvCe~E1t*(z;7a8t zSK(L^SyWh^MV1c9jz!U$p-}&DZayER<9o;RIM4FN==yY% zkAYI3$4U#uYC=zQX6CGI=*B5d#q#TP(!883PZe1xVgqfuU1|gq9ELP16(Wf+hc67Q z4XiD|&q(b8xIV=WknOAx!UQ(WE`ZRJ6^I~#NdVn(@P1n`uq{C+FodDt>FNT~2yR)O zR?`XT4bX`wra=i8{Bx+eXdeP`F+(CSq#H5Nc>+RX_Xs9EK!(pT&|P zaf*bblEWcEgp=3B4LcQS!Bea#j~DYJ>*2i*8{kf<-}4}#KBO|rEqP^Tvh>}a+ph$1 zF#CJ6Db`@RH$ zf!-~4>C^Jcxt=F4SiN0n3nBwix(2TXn{0?(ML) zAh$Q=-YW%PA)>0jd(nFTco=kVZ&-p*zA98ADy%X3z9|>VMuMX!rX)fK*`f&@AB#<8 z@8<8Mld%_TW=rUWvq9sv1jL0T-FyLxD0;EvKnfi_+nc59Oaq65ZV_?H-$ z0IFCV;w^zR4eH2-#iDhj@|UE8CJ7`o?fDS72f2F)=6#s0Bmk#D^&sS%OnK09;@C8x zwXjJr%y6{mlq}dXabVTK!=;-RWf&O_$|lPI8aQmc3#FQc+Ki z%gA8rYF_acSb`0UrhVWp7u?t{SkE!(221q#4JC|cg~d(En`8URTN-1%N=m|8XFG#{ zM_a5{c`g5G^!0u;&5sGHWe=4ETHFS)mY@p zq4|;j-nTQSg&8yf2-FiEe~=jCa#%6reZ6|S2D78W?KH*V3W=`|Y~ zQ$}z>vc_~=JCd-(rQFXXkrsF}e@naDxbTn@A`hD}rN#TV1^csuIgqu4GjT#=9qNQK z%EgVZjWF`Tq>B6jidaD`f%gKanKMwJpJ71+yqbXkled}xjF5_oW5lW;ISclZjgbh~ zmoCxbfQu{)Y*^^*2cr$R5NKJcgb@q6R22*G4^jgKb*bFoMv zrSR`VpG7V9)TF_oodoR=;vg#IldzZ&kE@WO^yarLKHTJYLH2TQkZ=E^pFdVc%EnbS zPY$0l7xVhRWh%Ccz@8kYaE>N@Cb|>JIyJ7whe5m7vw(sR{gEfHxE)P9>OTRc5!qhn zK~O##)WWGKuQ-VeZ;bf(r0C=wx#zPhuMQO+J~eW5evPi`7|rHzdsYPt^dni5uK=j?kh*=Ks@ zPYVmW{}<9iv!yERu0%3(Xj$_^yQQP>NdU%;MiSP;VOwK2to8nD;%3couyycKD) zyIui)2kl?mUwK?C#e@a7dA?{?L?&1I_9aiTFZ<*1v(O%99Z;I6kX~n#8PW2OMu9=` ze0)Nd2>sR!8tjfx^WuQ8B``j8D8QuYXg13~bib|NRsn8p9lX6R_ntszPFvz$fq*Ie>4$kBm^{xe0gqCWHcjU$LL>LUmwq_{9NL zO@~Z})N?SJw=Ke7nbBj>%{j1>CWMYv)4ZnOxWk9d*s@-S&tBfu!knBZ<2#{u zAF}W=a!Dr<2X}!Eua`pHipPr!s;}<<9Tb5%AQh=+!3q*h^3Hk(l0*je-J-5TM?7l;PJulacJb@|FXcB1r9hgUpD}AWt0fScDqq; z7l1vg1&%C*{bEkvl^E7HEwLLQ1FLCYfE9{FVa*+b(%vTvT7OZ$GgXqvg z&Wx&sz7A>;nM@{N)obg;+);C=c&m{m1K#;Tt5U6r1vFfNhvEou1;qdii2)uOoVyNN z|7)Uh(+G3Eu$;4C1SC>0$mvS&9h3;3=Wt7I%08Z>hCxCEA=1oM_qUDp`ORJRUtcyh zqOa#LNwYt;a&E@yg+)nND)LFy_YvG|{5i@XXMPgA*uykbiC-sbf3np8rB`<0P5%gqTlYyto?qM8YHV7=7j04VShyC>5*-?f$1>$HTT@ z1CV>_Z6R>I5tCU_?SrY zdf5N*_3W?p!cR|{{JKMaQF5Tev}Qx4G|^!Y;)3DyuVeuG+3ZSKLU=H$`qABoY#M5- zchdlEl^`8MHS*8#*^_O_m2~AVsZw&jK_2MON&|@%f^_ge(X^o1Lna9X1_;x_H7K*0 zWzeUpHUrQ4(P)$o4%(%F#0^3T3TRS%F}xuHaOksk``Yn`J3Prt? zAv^4NOOS;E=tB)VSQo|0v9b&lD_2&$+3v6}8YO_YiH+UfuM9YdZ0lleTM94mkQR{9 zwUf~2!rL4)N}z1R+BMBz0?Gi^MX4PgRpk!^Nd5sCM%1+P*}a>BJd<6*y40*p4-Hg= zdS``i+~(u3lnudlt}_&{8nhe-}e?Q zHYS83c5x0Toqg5YI?Fkynq=j{7zKSgIC%Uic99AXG2J6L5>;uW?cg(0no2m;-!gHE z%e{Qo3+Bfsq}~<|D<1<5HJUm7?7O)e=*Ilql_S3Fz98h;l5;?Uv<@H##rpO^$58)M zRX`hRW^}F(`TCc1s4**=ddS89KaSo6uIW49`d9W*QA?15FQt(!(h_3=mNYsU1F}5nDsPmB7Zry;ay;_zvo|w9 zrKeAKww>de^}87I^!e>jZ$A$>Z8a1P$zl+uRIcG{fZ-L-jS`wFLCoizz1mQCpC^xB z83FW5{Tjy)j+muFv)Byy$;^x>MA8Wlv>73Lletu79HSab@NS;POK&|*3X!D*UI}wG z#x2NBIz$Os39T_2&DyMJsZb&sh1XVO)W`*xyd^7O1HcSRQ^A@|E0yMweS=Q$JL?T% z3?|FPegK%Co|_+H=kKFl}Ak1bqr2h1gou3hX~Pf6zM@DTcKh zob>N?wBZgtzeCJE!(WNxrRk$SrIH!+7i|I^Qal;_2Kuv~kF!Caf7QR8MR0uj??`^_rOJaHI9#22Bf~IxfDNzJXYo2`10!X-*TdiWwfgxxJ-0Hg zoaT-<#J_|mnXo6)G#U9LWlQtqL0?_pmKVbB!{hgYFxDFV-DP!S=*mb#s~;ot96lO4 znQgq&cLUG1+7oU5**p=S_LT6@qu_VM{LsFeQC_>{+kipzCIp8*Y}ren7Cy;7DDw(U zy*FLw6+7{;!b3_t-B$H-yYJI~@As}O`rhBO&dc#>|EBSJyRF1Zp9==> z+PaP8jlUzvVx)m<`AXZbH@`IGoez)STyW%gjXI4kC{Dq5bn-~fq{ojw84NiXkYh`V z#t7f%BkVI=3P(L%Zg!}#|97y^sb=%3*xtNeT29wBhYu`lf1{8b;d+c;EtA^#*-`8V z(P8A8A7`NHm@#PID72PfQ`3EhyQhcZd(c)t&_#5k-FoJDc6}}RB9>Gw#YbAbMbc3U{Xgs zg=Ja8F(iiN9NurtBAw<{RA->=8%GgO6>MQymY}-BWRh!5Qlg)TV$oakawXO*B{{AK zP6crfr^Rd$WHebU_-qG{TG|tIV-^q(m6}p)D5<%*S^83=Qo~XKFK5AL#Avh{!7)#r z;bcj)79G+8@Q#!k14j)kGPpcsWe!$mROZ-{ru57V<{y@lJSJr-)w4Yk?qk+ztQmu= z_7$**3C%ZGb-n?CWCo?4J9&9vS#{<kO;!80e;r0}=z5psNY6exC*Lv5rZO31 z1U0D?AGz0r{U}El2O&A3L|zO|y9AlRSvH|yWzVe}KBt#Sbk?cJq$|Rmt&XRXle((r zet-TfBGne5?MHSBFHFZN$yM8vQ_W(&epPJfmY4)4F^`%t!ik@ZN52OGkftV_SiWGq z&vIAfj10fx$%AB1?(zezkZn!+zI0kkJBy_F3#(J334v{lB4fDpk6BdY+%qEUp0JTO z+5|bvu5#YQN0eejcd#o=Wxg3rm!){{;4pkwJW` z6^-I)BT_EvTk(*{afq&Z$q(4@Uz{VUQ%S*>lev#_?QdO>xK}mCT-r}!%(W6f&G6kj zHFTUebX?%00J{md#w!nx)mB7N@~SXt7vKJKJ?#c~)-Ef{HFOiidh(^Zmz*4PQw9HC`AD$1h-5UfU!R*lS z=QjQJTWz;_Bqb(EH~wRMN7q%SJvbl(qPyd#d9u)x+mrn^{B7g+ZsVVqh3&pl4t()! zQ!Foob!NI<{VEgSuZU>j8YRr{+1#>lVes-i@PgOE)UakxOK|L3A59(Ya(%I$#fk}r z9s|w=$S_6#53OES1d~fxxMSfi1!$$Uw+BxpG80QWa|i72e9KI|!N4yymO$@OSp$Z& z-1QQplF3>J7(r7yrdqJN8`ZNNP{s8~2tnRNJQCt#32tAACNWAaQj6N$Vi768lEBRg z!AUWABmC|bQxiN9O2mkyR_$X2%oH4cfc$cfWEARltgd$QwVjrgm@!K>{eYpAtU^_W zl))Mcwj^Zg0agw~;zWKPvC0lZaT86W_(>z5)ZW?subsIBrW|A_?oUQPmUm!gBK!~! zv~{Nq)00=lS{}XCi%HDM|2b!h4tULBw~5Du0=W}hWGLem9- zcs=~YYL7vXfgplG(!=(=$BB9rDQ6P%g-<@qV9ys%7X3V|)n^_Ash)DEnZLu^i-5&8 z46#A{%&4>-zB}AFlg%x{tMVX%)c%)5hxb3G6x@#5pMGUR{pg4?SE|fal~`gF^)-~& zb(s8NM^ktI%lZ9Btzo1> zQN+w@gMHh<^8eV9{2z`^8*Cig2;|aItLe{DLW|{PzL-+bcGV0HslR36G}Ez|U?gJR zCHMZHLTsGq6QM5+;f3S1G3qpid{fb4)!%Alj;HC)z{iIY;d4qT9G^6xd&6W-q-u~K zTguL*QyvFv7Ua94MSbd*g+O`JS$=tJ`GgT#5gpU8WCl4AQ6lKIVy&fzt%7~O|gN0re|w05l#0PRxBmCC1hhZ$&KUV>&3}DsCaaQM7`M zI*o!uFV&36VtJ#rZJXz|M7q3t^2f5{7K=CPN|u5tw>jz9dY!6HSGx13mQHt1yaMp6 zLju}Y^%5*EOTV#chPYY}Si%d9x=xLcR+xcl7l{C1mvY^X2$Z@+CdNyH0;E)S2- z%iVol=E&vA_D{l7$3LpC8It?>#@qP%c%8EU^-H(NUv2;T@b=`h`~!Cf`{(lN?9c1L zm~(;VzSZ^AiK?!9_HFMg2m?>|CH1%@t3OJDe}7%c{@HW?>s{tBvSYE1@P~411pF{ z!o~YdeOZD`p_0Wqefl(m#K>?Rya`Okk)+Zxesw>pUX->i&cEz_I&3pZNPbKC==j|c zBrAi)@7Vs2ZOew88}63>yzxJ6PB{MeZniA+8(u%~stF9a3t03kLvmLR5Uf8B$?V!M zF7*8sG$GfQ+lwiI*j2lusmO!-##x8fps*3ZSLDkXSY?~)B4|q7oWb>;nm6c*SFU*z zK|-O8p*K%OURQw?fTgtAwA#%F9J6OdH5Lr-6S-7@*b|VbvrS4Iqf08xZ8%yr)Ka6m zvDCM=be%9kst7nvE=QM%a zLdDh(1O5OGY&$QszP`X3|-o9Ha!dGI+>4~GO9i1ynAwp z4L+e?)C#{3MiBF7x*#NgFwDi34&@tJ1p4!%avu=Eyy%pZkK(fk10f;JxAxp9AydZ~ zr7D#&)5@NiDn={@S^&t|A+0$z#+gooxVI2L_bscah8oD$IWxQo0r|M zsZP%x{_;mIW{ifYw4H&e&q-iNyRbbgUG7}Y-067_M6#5RtNicXi9W;jqLnCz^`=#Q zF$I3g5#(1f1k&{(;afRTdV{tD34nvC(OKSZY)9b%?yh?R!d#(U{@x^!1NqGO8(W2b z3W=nJ6z2=q%KIIDE#6iNze*=q5+b=k#{y~1zKGN zjSYqE8l~J+7t8&2x(07SIu1vM`b`{~$ni4Y>EaAbd4*O|y^cJVU~0!t znw>?ctrL(qTqbAq7F_gAeDliE8Yt4M1~A}Qi%`XZ3;4~PYeqo}02PvYiNU&hhMr~3 z=81b|#O7{oQ>mKAg5*@+A?WPEmj&})Fe^Z!&C2Ontd@lx*NfDR8Y-eNv{ELMB~35~ zZFD^?7R;Cgo#1O&I4xSv!L>#el|(Kfl?Gekr74&58N7x;j*>deDLPQgFwtC{pxLTLWaDN1CUc1XkIQT5z>zF-Gy;Bw}~!;k<( zLc*Wru_>`<wiEgXQpFJBAK)K}+llB9M|cKW~Hd1FiR zvIn7Z?reDjjQxE-?)Bh2OaFP`;`_^?9(oTs$v>W0CME9Q+;?+oVB&VwV&z=5y>R!X z$c(Cox3^W!Z%^KMhGK}yRUhV_e0WH@tMUHCVh!cnvk0KEFwy9pScSY%GZQx=dGX8U z%LBnKsW%w~zqgNyb@+AOrEJu-ciOdo?_Kpzep3s$N!h;jICY@+Z13s5n=W0kxeJ*F z$k~M=&4Suc*zlqu(J%hx^MNVc2w|G=jtHf^34H@jm<0Q7IU{qnE^e9!U=GHyMUfg@ z*AkzsvtkSWy)!7*x)RVD7URI9@dQ&Tgj?vY6Ln(uvXjw{x1O-;G z*3tpFN))U%6k9q);iPYPa#YP)8N&L*KuYI4&W*lGUMIr^rm_Oa)J^#9>&>03qJ+cq z9qpwZO+?sql$JB?aoqZ*u(TPSSF|OUMa!1P3hxp;^;!^G~wp-5G|Jg!H)|x?Ye7xilgrkxlRp{mD0PX}CIPDl^k{;v-i^a(Fz5U>C+O)**$m zb@zSyxt1?Dlfo-mr?sMRf3JekI--`VCxtQVIPT5lEu+lySfEWOUnL_ z-Lj+bVEF=kbXfXZL=o28R=bfgzq|;q&-wG`8Zg%2F@kHk8IPLM=tVHgMIe4@;jUVv zyvp#XgLEKAPV>?<&>>er%?rclbtWqE7pid;VpWFQNz z1CXdn1C`_@8X4dt3)PDMGO8%%8C++r6lt6*k{+yuR0$>tCVz+qrC9x$cpABxm0)O% zlQb>z)ClX2E$VfuMA1#rdghF^CWiEoDPz!+01WY69|g0W*YR*)2jB*JAHlf z&!aefun1&AwO6U%B#wVsmP~KH(bI$GJcz0-wMeYiNdu+60)DFnSXHcrfe;sS#Oy#O zs5^mxdHZH`^P9hDCyLp*#FLMGeNHz&dkM2-_fGn$md9Q$X~HMyKX(I#_dwhGU*3No zw3i(?B->Q^p&4yuzsvb_iJn`?_C1*TG$(nG`;BlP>~*fk`d?-x zQ^$gLVSSkn%kFd6H%tjVPSNc06J1h1{shBSegsHh6j`q$?c9?YqYZ?u;AGULFhtvLk9DZ|Atg5jq^v zzbMOdTJ)6;u}TbQZ$e)|#B!QjxpaA8a@+u3m5S*MQ|P1E_NK>vs7I z;Q|0XdQ(q_xVsaE7Qp^=@>VhezH2zmAd*{2h3MH>Dk%l$k;#NuLxseo_=!fwdac8R zKD-`5&nmVjtE)YhSy0!lUMZMQEj>Jhg-~utTgTv}qA^fpWmrW4a>ME@-bw~c=q9sv z1^#(OW6T0GaI{twexgaiqZ-L$>trQ>6jp&sYK+CF1lM3yEGiOaS65eH-Cws9^VB!s zuv!IHf3;hm)ovMG84Cn#mAX_yjs@$y!wf!|S%ag&Swc0x3B)`>*LE)rv_qT@LsIR7<-cNzra6`fo?b+81s-(kz4_>BE zUVLdSLwHc%wov?X|B`QF^v~*Bd+)O&7{%Yz9F>hWpp3Uwo?+;0l!y)5_K4X#7ZOs6 zV(--qjBjJ^0%6|e;|Cx0{d(8F=9oj*?M*E}fn9BT@X&ues`u00ugc<+(`>88*BHqxvyk(f0?R{-TJA2liRc zSMW5u&3!Gx^n?8W!U#HA_Gs%|hvsoTOUrxkQ)t>bF5XEdF1BkTz*0$aEh0nkf8(qH zwO%azZhi%hITg!@LxzlK1IvO77FEe&BH0b*9{er2xF-SZL@r?fQc;SYj939;lPWTn z(y%Q-Q711MnYgHx;1Ov(wD8!pkTnaaF2KJm!2vX;kwXi$%$O_^iKQL3s5AIKW-O=m zOuLZ^$|lOdu#X}yDINz(3oWbNtGe81BRS5ZH@|?ey1FVb>2irbLA)vmHd&rKunBS63}7L{R`Um$kqkEl_in8mtc&GFt;PGh2An#SWbkaZy=roE}1S z6NF#U~!4g$xAzJOP=;)z$@CN5z^nbEve`dPql2BWPZEdgP z>FqK1+Oiy!zz?pEHX%!h&6l86MtqA~Uc-4kfWg?Htti(T$C#|xc0fN^G1!4mgPc00 z4#udY@{Z01&3+pfN*atMuSU3rBXa0Vk`*)tSW19&TW92w74^b76B1^b@tBr&g?`gM z!lhvNVWxyOk6h~tIwOl{?)WD^S zk0P(y{C<=0a9u)kq&YtS^IeX@;xvN#)`DyRWlDGUOe_rOK(H!?!xrd5?ED`QSQ01-|nL{=l6fy zG?DMJowVUh(wSb_QrH-Q-212UMBD#)1yu>fK^0eWy54sEews^;YtWSz*5m|MhBM^h z=7Xfu{1dzq>YU87GAIoVESK9>Z94U?xGOSg-iGh;FqPxkH#q)}ys9*3R3kco2qubt z@QF?AMw!itEh%e3C`z>Qz_3H4jQ^|E+mY;-cf@a(-=@oe)E>Tnr+FgkC8Ixoe*3($ zjJ|`1cdvov9dpf-`?@CctY)OngZo+{?y*=*?Xleu%t4dt?13X4$wLeBwk1ZKC?LD6 zVO3;dg7O8{I8HIr`nr{*zyt#VKuk*@jb;G?W1U6?DUqy%;|Uf{ixGB~+;!>#YgF7h zvr^iP+olCTmF^QWut9-)uUpH4I|3ShYc~C0;HU{tR2>bWb%nV^o&}RcXVY*k8ao#t=9LDHF><&( zW(mIflvMFph1ripEh$}>O3&0b1cmsxDA#f*xdQq{WU*_DtvKq@97%I1h)Ufpa!FcrdK$4iB0y%NaPQiA!Tsg_G*t~qK>aNgoD(@K*zOaL8%#TifwFctYb+1&b6z5VkIEc2O=p< zqbxER>sMCjY|~=(X1u?^s%B*~X)(&;MmXCr!c#1(^=Of;UrFdOtlll?35Hohv(`i&3sx>LsFam} zw6fn%egUz`Y^A^P#qGiPcCmt))RJ5hP^-CEaI5epMe`jfV2umZoa;n=2b+#&#M68* zx|@YxK19&qPi*2Bwe7@O?pKpHTK%Sux4pkT@e)M(pjO|E_7L5`o|_>)Hut86yPqGu zBIc&Wa}<(@Q$1-G_Q#+Z0yb3Smg; zg_632Dd+$<{XHAfg;liBlz^du$$isx5z%N7vO3dOZo#2G+E8eKKDh{oQ4a`l?P^$+ z;FgqwK!;`@B%txx@3dZ#R$}Dmvt~O%R4#=BlZKw@VxA=-i*K!jLa#YCdZ6o?AzvWR zGHcE>;EvWQnZ>%tGy^vbu6E)c990~A@FnfaB~~H$g{2AzOqN=pGofdhB^9l&>r{O17fO}5L1wnJE1J%%B(4fdtdUsj z8d1rTO3Cnek~P3XuLH~i|AQs^Pfaog?eyWmB?XN}rUlB3EZ?ZKpr8!S2{ETzu%bHM z!W*u=OVO7q-cTB;aO7xIF)=DkeXu~B;7P~el17UWuAjl7heEbC>~!jAUptv&2-#No z!RhP0;H(7urVF=?AL(n^@A&IS(G#D@h*QFmrqt>Zk>%y$) zuQ99f0~KB+OeGB^ICOr1`lU`yF(GS(O1~I66jGQWJAql#&a7%Xo51rcZdzI@f$&s1 zMq9@~hL1mD1<+^Ft%sY$)`1%4VUVHQ=dovV)nDDHKlAWO^F1HqD2zsl)8flGdfc$ISyHDkGqT*Zw}fWM@^Vh%r@FMR(_o&VX+ z!G0$}$X0aBZ(xEkE$>{p9H}j60L#|JnrKZ8FoeIy@4ZfX!Ce9u{F%`)Q^X@^hS*D*w-C@($IV(it7@+ z>#7jN%`M&o)+geMz35^Fck*VTfkjxS1XP56(SR*o{9~ZiG-R)J3qkQHGW0@ZlF9Mu zKZ5LX66_S4=TfSZf823&&-1~+@_X3#x&HEC z8}#(zFhX?kp}gWElK191`b1Qkcb!MW*-JXi_TTW}p1URo!AWEln;2+Ulru|F0712~ z4vJT*9=z59ueb2?lVHK-ws8k)I7-JaI65Afq!Z$TBgBg?q_3eB3nWPvc zx}S&Z+_1UZJ5qgAN7Ktor@{J!S{&lcH% zB>KJyyUEL`f}oIs|K2lswOZ{tE^&xF*lXNeu^h$reY&Le#q4}{POPEUt{ff)pRqeL{(%m^|#r0C-gk#Q;M54I<*+eS^q$Lgm@6drT9 z{Er(aZPgUF_xpP9ltuKmm)$D6FA?)H;CCXvU725A@*YyW*@f^s!oi?Xf_jZsZ!yeD zn0wSZGW>US*(2TYFLh8MVFgA|M^l0$C=r=TYe}@eL#U*rnVJ&x=V6CQFfdsOFkto6 zuY!ka)fQL6SNU29EL)OeG|~va7w97jj;rtX z+4%qOax7qhLBAm-?Y?mC@&@nZAA%~KvfQf*e%O)YoBRNXXggEolg;}Ia;r{#ALUx` z>!%J~Ld~s_K$20j?ch47h{12vli2UAX+KvuOx$DvITJbHb;C^mMxEKKJcp(fM88*#lGRmrmju8CrYZK+Y^N z5<%8j0cW7FDZy|y5N=1*tc;4MMT*x=3COLB4cO3G%*Eg!Sa-{2%ZwRCgQYA`->3{+ zv`WXAG%}S;TYJp3U?{KV&=O(h^d|g z$sXV@Y>rk}0tOe-EHgq4gTlR0EBu*6@SVI`JHgA9uW}y;GS`bAp&sK3njEycU;?_O zwe?soXLgDOW`S5M3ik4~S>0fC^f+S>J~{6p_sH19@tVkcuzk34N0sN(;XGlsQ$eM8 zyaEk#kwN1w>9%Vd4{VsKbn?~F>4NdPQ`epR{H97m~H*gYiwEXs%XfzS>Ib!itCcgt$QFPe0#{`o@ z6YE_2w$Y!Hyw%^-i)XY3k7NbmX{TeWP4(s^UKv-QUqNvNT$3{5x91udY7@>N48fKj z5Fjj_W-<6O8?RNXbG9^Jko!19R`@uM54%om4X8SG&jZ=woqcb2q{2KLo77T%%we(2 zJFwz^KI#KW)n#E|y7I|`;F`Yw$P-;U3@ZxiG&cO%f`sopc}bZ{6ofQiy5R81r7iup z2Cq488In0q1N$Y9f9WHOKCc5JqRba-JBbK(vQpY3H#O{HF&1Q=hUaqDh9hH025L0&*`Dr>)$XT`H`u;APw!9h z2|y8}lRE$XWykS7g4e%|)oo}y#9z8-^p%U|0t(m(UD%bWW)#D2N$ z>xUN!P8Z+%&P;3tn4#jqUi|VdqvETK&31!fr0=hOmD~3e{C&{BD3J&R(fz80z8{Ou zBf}OyzNL#&nP265Z18elW<2o@Q1<7;9lxnVN28?j2Cv@;@>k+$cYsE&E64+kWOA2$ zwro>4LQ8iHX9urx$1jRS>NgwzX}i@aFT;6HByoJ(+(t#pmVxmtkpvHiyj{C@O4uy~|*pNnfIr$ILCjLD4Yhdcrf1h+MI9C3@JbIIo z|FE@%<2e`6`|&mB-T&LA>-#N=t^RgyT_?-_Uwfym9(0?}!Z%HXx-azd%Ck27Qs*hG zO50Un6ZP7S@Kdpkar2iVs{b6uTfF}LLY4D~m4o6EL5{pY63edk@~Seq(b z9Bce`i)>r$zutD?YGuu?r*g;Tm@;OiFtm5T2@Pm}aBTC~H~v|#a{=jY<_UufgJt`` zrOh$Vp(Uc=_ul1t_ox3m-0puU#5?1!J?_dje4MeXMqcdbk)95#5B z*MDn7$d-@W$JIqw>8$v#&7!#@Y9ylNn{Ki^gvJefGd2VNRq)4f*VEgJi#|o;fcMR) z`q>)g+%MZM273sLAKr6(;lXej zAK_K8P{}l^aOVUwRtnwMYMsFX3U`(g2*%hOz_7n$k*-;g&BfwiR>B8&kKdiqX@E7$ z($OK&6*rbH1xm9?>(VpuS(sZ8K0|a3yzfo~fJACe={gnakNQ*E84TIi#x(W#?FB2;IbXT?OoSlJZUFx{?En6R1wc_PT5*IEf$D{>~X z%fjy&wCwe{U+X;4cfeSZF~J5iClU&m|JF{d0}Cu+Ct=eBssH8pzaLt?7G8t`=|It! zMPELm97@0DGIEDlEe|P>)wZc`9y~e^aRysW73R%A)I4Vw)~||U^?V42@MMrKAIm(C zm`@tpgB_ZaqKD(FI@th1`xGTM>g0Y=_+cr55O`cTC%xtgZ&-fSAwn7$)RP{6YM{4< z=0BOk5eq-3mdl;^u%m`TJ$j{Pa4lIQ;3VR`N& z2r?T3ZGW%>`q-l&aQCs^nBFfjKfWnr0|}OGvuVTYvg(Sh_7Cjr9G(y!W|e*2_TWbQ z4d$S~UDvz!e*x~T_;nM$!v(Ru56aJMB)Il}A%U$1_Xm_J>Q~ms5B_is+R zedp=F@;*@$s|tRR;NpF$Idb{IEg-ay!04)5GwZO@G%h6?_!%AS=(X1kt&9bEFqoW zqei8XTQ3%9)UkvaY3fIfiW$8BN%& zQO8(|eAL7lx?gZ~8y>cyAY!ot>DSe^`4+ZkeYN~W+hRS+RMtTRqcd`k*War_{EmlU zc;JF`?J9c@pR6#3t026H*bMap3=M}Cm;VbnzDVdrhof9-6^sTAjhks z3;#xxgijjEAWq(%qA^O0l*cF*=oBSrCHNQv4{YjKet&Sdsxf-b^~nd9y9%3|1?1|~ z7Zu$619g6RpEvW*US+!ppR|4D$%*KIbigYV{(QVrRq)nZ;oso=sNh{yz?}>G3TrF0 zX@t}lH7WHKS17J83z)>?RX@wz%9EX^R30@lx?j|xysc|f^7K8&D|$j-FB=z(teB#Y zDPBc3|Mn;d4d}hs;5J@a<}k6<{-cNuXzYqBKd|9u<$K5c{bipfCH7@I<~hC#NSbm5 zmvRj-1Mj%<|BA9r3G%MJBaDJI_~2%4Y6C+9bBfn^MRjlF57dV?;Z*n~=n z1RV>~FxCa4z`H|IKY~?>kVOlN4S9ESoeddPJq15*8Sk)jOL?CBL)b>V#8LUA(4gOd zU~^5C>8Uo=&U0Q5f3IV0zI*)cQe1c9t|qGy4L4)pCIGq_gCz};_Bt{QJ#cOqu#b>{ zb$563}rh zGg|CiU$6^Tu5jNhmBQ4JxQjOeRb3j}a8|Bc)Uo1_W655iLN8*Ws4|0)kk{1BjFs>k zn9)}f1Pmpm2weoFj7{* z+?;=%`{CrF^e-nqMB*@%M_msmSs{plC2*scA}Ne}VkuRlnt#E46PTORICO0A%Kf%0 z)SQ!HegOmm!8eG1I5ZcxSQX{^<+NJicZyGs*AYgzVQ3j87O51l{O4Z@mfRDvM`p}< zZHRyXx1i5M0?RcEMvA5c6HjA|ZAkB^dVeV8{pM4?;|F?=^v3LtODZXAE8pePdpvbK zE$Mn$N7>!-?`^)dz2f*pc%1kcm&LWTX5nY)N(*wAC`phsMJLi*$Czq8@NMwn@Y^+G z)ODp448X-{K%Jk|dB@yH+*NnFm^#9VsFB*|yG5QX8?{ew_m3ysj5=iVFGInnqMIB4 zGulXqk=y-PWZdxghWH6$n1?Wyo0doNcHTp{k6wZ3MXq6*w}`Tr9#N^pz_cv#`E5_W zg9S-6(!dzJ3g=q%!eYY;o<-o`n0Q!2fKh?FGQVrlrQ(*%jVc0Qg6gXI8mNcxebqy_c7A?#s zl%T9~GSkuH(t-=6n(8TdxhFv~Gyz&@8KX(}z$F=S+$O?E8bU=9vZs)o9# zZTrsu(P`9Aa!(#Yw9^<}LOOjUCzx|E!*{2zx2cXPE6L535qY_!#L15K%c)<8>)Sqb zu#v4uYx{6O|2)m|NQm`wl_9@<-5xn)q&16o{`BU zm%dCVC*k3$4@{?V-Q7Vc`nz)u@$m&c3;y|>{y)U*+2L93nzQer(Bnv-o7x;NO}18-nI(v#3I}7 zlM9`Wgc|u-Ig6MA-}LgwG&BT!w$p3Ft%msVFzuZQq74T*g07FB&3(?O% z=@|U}EDMzeyV@AQoh(@1mXvTMB^qF(74Qu`9Xu3N;!z8mKrW4Jq%?uJf(zc#5(^-K z!Wjb=pb4GmGz6R3Y~jI|WRT2s3(zSIVM=0dg-SGPE|FRiv@19tp%cO6Z-AQ(IX$y( z5H6SOc`>(RD0df)SRW<=6LE1ScScYMXX(OFKk++f*@X!{_(g7VFRu z@=b$bR*wTcTMZA2C^)X8;UN9Rv|bM}u_;i>IBt%5jNZsB;SvBB!J9=23%0e9otWE5 zpt}}yx{zGv{@(v~??7|2@EV}8J_FY_jsJUs1eoG)iOQ(QfteZnRqa#`5Bmm)SlBT_ zcL-jW`h}kmcPy*iDog_Ui^;D9%^vc@^AH8xYiFi1rxa+0Dy$wP-p$HS-8b?jzE?&$IYNi*EY%Y+xi)LAo>**cb>ZNC-@F>hLHU>N6q%<&liX=B6F!+nP~ z_W_aOwtbUFgLOQwEG0VjAGGZU+c#|jn^UTsH* zxu?FfW2VWNUBUwH6^mFJ+}c!}aV16#9=94mu8U_e>)|UR4`2rA3AqQ{y3w*@K*>() z5-XNRWuZ847SB$TNJTS_;RhD|tnA$3wx3Dav|J!J?%YV+JlQ!b7Jw{lnC0`Bi&`B+ zMYM^l)pm449fwrB#Pp4Nm2o@{TsPvm(=Y=8K~+wnz1y!YTWKUW=n`#xTV!x?T-a_IGo=?lm2HDs_UledK` z`C&>udhZdYz;Du8_R6EuY^fvngkNqv=o=84=Cd)(*YQt?3MCKh9<>&kga>V9GY0(O ze7whDQZ+2*>a6 zy(bAZl;aJpC}%=ou!H;d)lpQdE-3KU7{%V6W0r^_Oaf58jBvCW*J1xCG{Znvf=&qw zEm7Q|Mc+|3ERJ9hb8(_rjRX=)4D7UW(mFvH1PESTCl^vpB-ZtUnR?YCkr`VK=&x447TV?dSb z7?%I)dfh07!t5-QwqD=T2*_f>3fjV`Sq6c{RL`%~iuN{P#8D}wfuvkkXf7#~Tgq+p z19TP5Q8_6HiSKIxUIT7!aYF{DS+KL+NILX1**SjH{0W3iR zz0eT<(vKAf{&eiD1iKU^S&8V0aY?!a^GIfQCYp8O&7L*r)-7aKDMMq7o5AY14iub{ zQAy(ph{V&P1^LQilai@bV;y`0gQqGFHS(EoWY8b_Li6Zp)7ZB(E1Ib!;4ot zbFZ!KiMaY`|Mn+eLtvQvN+{8{GTqukU~$Vyp>;<+^5X}Q;y4G7s?KnPf8w+f^WW>) z1O?s=`vr-o-hJ5DA=rE@`Pa4w`+W#@AAjjzEpxB(j6Bwsx~V~*er(g7%k;=^ciKfi zNyXXl{vXwgb{P!%1MUQ@g+4-%IfL#p6`K_^ryC(B5h1@%ygH>IB!96Y~?KAsKPv z<$l`-(0G=;vtD?f*7`_0@p#L4Nq>erR+h!1;5r=lS$1{YR`M^2xI zdk*~u&e)G)*S;4rdSPRgcWBoNY;lYX7|ef))TbZD8MSrWMaj_q|ul(4Q&m6+;!y=P#5UV7zHr_ z%tROvaNpp7*NS5rOC2-p;xO^TEd}GhfW=~}8-o>1j8a8ouw|{u2-njRJ{u}PP^p$e z+<5^C>xSFSkj3Y|Mof#}xf0j``;$V8nT)>@lhDaGx2!BkWx2U5-wwTawS9q_i6DJ( zB8)1fMrPCC(e6KygEj5n{26tu1Cz#)}t&+j0_?V zun>GZ$dsmBf=lvWPaeQsdQbf0f`8Nk;IN-`!zZr&G9L3g>V-_OWeLN25}s)v->aRaJ&Olfr__U=%QSF_Tk}&x0Xp> zgzF;y(WbK(qf(r^F52wh_z&AFu|6>xsfuQidfu7%=(Pp=E%OP9Nv!Q$HJL;ayo_wc ztw2!tr(uvw(4#P|WYG*B6xzUMYGlO8c>P&v=o@44H23Y0aKUvYh>fXN)t! z5E%zVCJx^w-~uu;fLdusVw0C8v5LGH$Z4Y{^NCedhFiuIJeIP=N|hs9@h3}T)_HJ! z9WVKBPL@(AX~sb4AZi25!U#?Q&zXtCPgjgRvU;{1m~zd!+`P&~?K)4Bg@-81iUhG( zR-u#VTM5#zoG;KD-C3!O@D#<(HnAA=)eg|O4Vboz&FRjuYz8TpL6r*-l36;#F7*n% z9ze}dl~~DVEV36>uGeRp5c!0zB@hVE0|lI0CO>GkLEEPCm52t1Qk%DAxN~E{$uFMx z@GPS`=O%sP6*ly>)3p;Ps+oa|{;qHJAzBV;xLrM6i_+TUu%nlro;fOW7hS!3%;<8w zE;=#7&qMyaf4k_K)1_tMks%=7-j+}JU(o}keZ#bwrbx;ma z^R?PC;*GO=4wN-=T(?mBE`_)}d{=xjBJ|Cvzbg)%y;48HTN=vnJ$p0#XKDYdq10!O z3PJ}iZvDg!r2OsAUdF>9m#z00I@Xtf1x;Iqdy~h#iHCs#KBKThTg%-9&gGff@=u)v z&C_*Z8PV&7oeitpoddhA=Mxn{>NWjAnKZf%XwVqc0P8l)Z0IhFNaosq31W~}@t!==nF1WCL5wD<37Nb#vD#uD-$g&47E4r2?_iK&@$UN&t z>d>G;c~giH&4lT!9lW=;C2!1cVVVpXelmCE^3kFN85wj9{cQItZ;U~Mkz@=$Tn)qm zq_vBiMpqJ7Vz9b}qmqUbr-VgmWRA{4u}J3P&eHJk`D?l z;c2o)!q)-mgdbL6)y|kpazoaNg+Xa5ghLL9)l%FsE!KL)wcA)~&SL3&KC}1+I(>V1 z(l|Ze(!QjUgFD9~sUKV2DXOIsxQ4Y@EJ(3UP#Q}LJ3Bf-A!UIHQpiCz$8dtvf>txp z5}DYwl9bFW-ES#f7KM}31ME|V=<=R9&ax)gM=xgp^ z-aVS+pAZC1wphv0;PnK%3$HwEnRv7hr40`u?gk8V4`B_Q3EXdeC&nKj(8h`z)sF#q zMe=y!Rvtjf&?R{AV|zy+$zPB-Am*T+Dxv{~#sr!Api5u!~-85+v0!K_niu;^Kbc$mAyQZ$j#*`*35s}R>k!7~PT%~&F+ zb}UPwsT(o@6t4~mf)UX4DL8k`hC)3O4(KOmsaV+7_QZ*CFpa@NAO}Or!ct>mgM|QS zyX?}Ek~B{9B|iUPNog)|uT${gg{5IGIIbKpHFSYg<2qG6qe~c1?6qn14Xy_!_;4wU zNo5?BhCbT2Z{M?*t%C|Gc?q4pBmyxD=G9;r2{a^m$BgcDcT~<}8Y73JAT#h1k(g(= z`JbVk_v=sGgb~$>{LeNk_HQ1PN8OCd|N3NF)LzFI9FLT@hc;6Z(p3cayXZ9p`jJ&w zIj@WURE1nN_2dVs`(|C6Q$DdH({>zp}@vlbMzO%LL-U+*p-h`&HZ2#>wHKa&m6bc_q zjg5ILDS=g8C%IEd!2vf)C4<}+U-VsILjXD#PVIe~Uv|Dfqughs-oDtr*x6h_t<0;M zj9gxz+`70}L3G&e_I}UPW4-56=0bO8^r>RT@Z)By9}CQ}x)_;v0(}|pwn3Rkog?tI z!Ye7>HNcH+85*gPhv$i8$Y?gO;-+Aplf}Uh9$S|!M`_|4Rz#RAd)`p|?Kv)V6!Z^b z#DRq$>MCN~vsOQqMv5Sn1~EGe{G#h575*HE<~bx6! z14J^8zNOXxS35c>kQPKitWtHFZ!l--M(W#d!(7&5G4bS?tqJ(hkjPY0q69$%7L^8P zu+h2ac#xfcHS(*t{A_|Bsq7sTIVsJqr zi4(NTkt9w)Rw)%+$f3VEVURVQFcY>iA#6EU=6BxD=l%2j@BH&BrGN+sPoC#~?)$pF zm-7UCeG#+%F`fsur}sE>2uN}m&A#J^j9EN^JoXS>{{M0yI@Ep=!LV>n?j)M^4Ah}H zO@~~brX=s_$pR(DD|d;L=IYgbYmCZ5P|o?;X0}y^UsM!OZ-6O~qAn^dZV`y?i(iCa z4RweJHobZz$DXG*^U|rP-HCLtDDxa9S(_YQ$P=5(71Oq8|~N&+{^@`bNE}w1LqB z^Tpw#2YPK!e4**5Ls}wk!riY4H$XUZ*NDRbCqJ({d=IHp)JhR&I19N#gUlpJ(md=U z%S}SHdJ2hEYCs3*7{!4sjw5CWfGH+T&7kZV#-4}neU_$czXlO7hL|GW+Bh zKrdcdM;po;%#4-gNwxnR)Y3GisBf0C$V8az7&A{5BkIGIB7pApC_SoyR^(O7`s?Dh zSeVg)&>Vz~WzdECP2-0WmgiJCliVj4ipE)E%szi?Rb*u!F%?QlXm*Hu6*0o*;Y8sW zkw$TjwMa;>rv_}jat^$KP0fR5^!Nm#ZG-jkZ!9b=>}pfwu2$CaNX}UbHu>K4rpZHmFEprBr@f3x&FBA zvRalfpc;4qmm6%L^S4g8l--EO_WTZaH^T92NXmpcukMhNkuWqwRecq}9Ku~J=^ia+ zLNJeHt(Pw^=E~@Y#nVgBg(HEUujxsmpUp>XgT`vPBx-`CoG%24X^qFL97jOVIMlH6 zydEgxoUub7-C-I96!sRZ2Q*G#3x!(&oD}?RIojhbYVh7LLli?uiOZn4rDeIUX9b97 zwSp0d3I`{-W^9K(EDcgYCTP=xWNOrp!n(skw8cfYaL1#-f57Uxm4%X3=ICZYmB7?ac7DCO{U?>Vka!!819C>8T~P&*N(D_Zh$X$*u71;@;*@ zZH#W&Bw#bDt*xzGgNT~dd?SFdr+;}|-B{j}g(hQ59fg|gFq`r4H1U+e6KzwE=(e4= zgPncNT6vd$SZC{M`YJ%D_5~TGutYMuZJYh5;S9^@Be^x8byoYEel=eq8jD4=UX$=v z10ab0;s1T5g9khp)8ZyEovw^KOjS7fTzgh?;wwu=dbj|yGwF`GKC?atpTlKWJKNFG zy5*O1#xh_tLAD1uRe7Co9IR4?sa3Pos_lFgnhH7pQ4Q5XEcf4BGU#EN2Ll*DFez6l zJ*#<1gLBNht|qv*)v60EQ_$}z^7<9a&9GCU5(2c~u;_+V#>jvYP8uzIHX~)XR?T4HT zW}aGh7n;2M#FLVBOC~1^mwVrB?kqxI@()2-+v&;8Re7->brU&%QfPC zq;u5G-`^bwUL!jEK(PbsV|)C8Fd~M_38VwO@7*>8nf2&76N^ zJ^0~hW0-TY!|~f+M^UM}^*`R&@Tzc^j$X*aQ*BoIBR7A$efw>x)h3o<`s+`(uiyVb z*VmM|sq|{7^jS0xq$AaCI?|=0p~%hL@p6jx6Wu?yleTv_j=lP`-(GMNtvZz+M>Dbt zZZ|78_BnZMe3!mBqd~rZQQTcjdj9c_&s^Jlap$Z0Y$hOFw~W$0FQ&>2`6qgZUV*@z zRjbX`$<_YFvaGl@rqy)Ddeo%G36!$im-X}-O-=WGn%`mgyGL+}opzqjH4HQ#nyha; zM-KX~sI^=F{-*c`dhHG~#|vlJmiaHKTX`(4o()HSz`W=4{3xTiQoTF&D=t?~rGkg> zQL*_{lc9s(9_!7{1v~YBH=5bg=Hpw?{7G@p@s3k&_ld3MXePZr=c|CSGW{}H*ZgDF zt-UNKz1z*-W(%M*x++yk7cG|>Px|d>6#+-9lp5D1(7X7{jg4O)^~#Ldn%^Yd&}jC| z_9L!>wd2bqo~Ox9hy)y-oBoIOi7OAif=%xCLU2 zB|s1w7(MD$TsP><7%>=pYfa!b@`Te6asXfsUbI_Q<%f6O8ABbfvJWo zVrY1^Uje8F;-&&%vt014l*caBVV=p%%X3pYt2qOpW^?O+(^U@U)+t?9sVxZO5(^S7YUkQ^Cb2i%(DU#=o06K`_s|%nX?#Geun&wvS!)ZTb>ScZWZV?n}&1`RL zTrPb0ZY869*D+)hgKL(&(Sh{N=ApJLV?o-O4 z4B5W1{mjFLO*eESBG;{avOeP4CNI66Nb!CBbyQ_3x7^wfQaS601TK;q3X>f_UUjxD z_xbv$tBpx%p=tWrSGngub&ucz`_&~Eq0`;#vA0ty+e=R(C-wP=N>Z!b1q9D?$QKrzv(MmAtQZqy zO*WIkecv{pYmS9Czt9TnDjVIOzGde3&pcmlJ$>^;A*5vU&6LvSKM}3hpLc2f$A;F! zq#dtvb$2{Ft#{lCk+nwKUoGaIe_Ky__FIAhL)X1LIFEdop81DkV87u@cr2K4LG#MClrx;zIXb5)eMW^@#4<{GnP7k7if(4MQY? z`f_%Qn`FK+3fVHS&eBH!nn~}W;Q%Hz7TyWOu5T*tBqpL)MvVodA#~R7{CjHDDBt}- zZS>L6G^>tTZ=*)`v2y2FvreLg8@&hRt7;C{ri{l0@7sQM2F=}>x+nfo=vO_~elJH) zOAvK8b6P*05c=wXpZNoiO_n-)wb^#-el@)>HZSG)@V9Rm>g*B_?2Ja<9cY`Qvicr{ zEY6fdD`Z8ooPC7bVLad4xjK;l_(l))uVe{Dxyr~$$l#jGloAO|o!PTsI69&mgrLFz4KvYstaoFELSsDbwarkVFNO)h;aadT6-dRy07YD{ zmLIE}P-))(_&m7kF(;7lSJ#&d_OyzC+oM{@U4x`rG_?jW9zfF~;Zh5{TEG{yOxLb0 zBf%H0z*~@ufz3$8!G<0g9o<|k7C|2$Ju`eJM&Ft&GY045avh2}aSRvp$~nj_DC#+o zpQ;(?Z&^Pgr3lN1$T9Lqp=~>rmK%xRkg_ggHD)U`5ertftBDXmj!%aQw z1(Ofy{2k|V=a(sq?`d;py6Bg5R^>KlKbRDK{D~spfPN zr4B>EK1aJ1 z0XcM9A8}Y(vACiD(i77iK&`QH0kjU$7M}>pkxzbfC=t1a5>YEy{;`m&5ak1@l}WRs?~K1&fvO^ zFV~r6QrGI2?!|J~n&d)xJ%>OK#J=SMWvH}L>^aTl>_(fI--IhDV!ju8=F8P^t=879 z+#|ZVWeEZ?kraxSWY|>()?$vSg#snHXCY~I685B%c zf5g1#mV%$zm)GZ-*Kwh?8h5}B2{|`8WvnyQ`38oGquF1NoV2oV!-e_i7&)eS(4~t* zaP>?rhrJ=Oa;Xw&vx90()&M3UJu7_(8RUuGtcaQ4&XGf0^39_#Jj@u2wDfIm-u97x z$usyyp_JhvV3F`D^GE8}vXwzlqV{RqpCUB>P1k3#3Vi_(9e3$_2l z)ORt`;51Bo#w6PjeSMV^8Q8{5W)`0-IQ>Hy4e2UmU%BWW=?IRHCfVR%Z$|x%jO_)R zjU4Yo_2fz|SRsT;Vuvfg}~WKgE<9_ecQ&3rjw&V8F192Fef& zr;MT17`_Z_#o@qZp?HlODW9%G)&eNoJ%kF4-S>JJE9Z5TW<73nHJRe(My5j+<3_PQR@c)*&P9q+#4yI7164FKsVaej z7s-hBzLzwR##@@IWJL@*lSse^{r0Jq&lerisPk;%S?WPfCpf{LQKLD%X5|0KqFur5 zOd|JK#^cCCt+9bH!%-G18nsL|=<)7kzTgL4hH4Rx_1xbleiK~Hd)rEJW6 z^_Or#4@PE`J{3XEu-5CnMZ5qMD}?D@63GzS$Pu>-M?AAPF5ceA-^yAq)}sX%j?8n* z#(ECl8Zk1tFEB=O=_P_sq+0;;mHJd)?L9Q?F@5Vq3dmIv*8&1fuZ)@k%{Wm8i?afR zj(Y$y1TYTY8{X@CkFk&!01YXkK0Clo2R;G!E+yQ^Nf-ttEztXs8>Av&?9As*X}}CX zYVt*}8X-M)3C1NJwj?xMX#oGDr_Aiv9-qdxsxfQLYs7PdRg@GQEU+Kwv{-r~u}SrbC^=9zOO z`22830PU&;ws>ika~+(jAevzSSvQZ$PCg}92r(5nwuHp4B_U79<#CYbLC4vv#8+tR zDoY^Aq^bd|X1<_{l>sv9xymJuFRwgH)Hs-#8RNIh&1ZH$MgWxFM`ExGM{?E0IF_V!geFZbvc>znImZ;CsTzsPZ@(j}gbxE<_a z$1*SK7;1lTldW*yJj!)X|6fHEUqpZj`nboq@(eF$m#JkQkK-#FP8gQN$i>q*7(|Vd z0JowTBd3^#__4P4vE>O#&e8pNz&|*C`0A^t2Oi9@xv-E)<#I+Z(uDiK7qh#Ok$aJl zB}_yQI@adimHc+CuQ)C@=4Tt^xx$Gwam#xOv73^TH1A#X4@t%Zx+ z13+E4P|~z;1-ShO<5 zJoqb$EMbWNjV=am5z#}v{i;6ZEQNwuCp~F^b##nq9l`dVpDIc(azCDHTW)qpjTnKc zsWsmCT43L@!1>a$7W2}logwa-$n0GFVh#jb=PD^H{g-lQiks0Gc{Nm$2SoqEDv1>% z_97ylJ=TSmo5B(}k5pN>S@T^ZhDgYcsmcuPDtI4q=HcmQPe~>t&n|A?RC?uXaBkFY z>x_qIkLKh z?)Y(Z|F+nUa|O2#7fAP-7QW0G$Vst7C1I<(%~3>ra<2ZnF1%Rj;`^{<-+oHJetMMaQW>6}|fPylbmk=bHT` znZc<$2B!qt7j)`vFX0qD$ugKp*3I60GojFgbUAt*KRs1H{j}i&XEXbwt+|)-u6(T6 zc;tAlo{`(7o%bVJyLYL4~!B)=-g zZ-U!g!>Z@(ZfY8NK_*k5^9g!U8+KZM%qVJCtUqsKPYTx4B|3QQ=q@;M#xy+|>i#b; zvo>N@@ysw{!|QdErfuM%UAQ7UT<)VZt-Vmd&-f@me(l+J1n|%heL}VFIPG_2ORuF5 z5t6W6S;lq|fHShgH!?MXIa7E&#(GTzU-iSd!kgJ_^109g6URX52o}c_$5&C$ zRbbYR{zlfChV=}c{{ne%<*$I*n87u_N?h;-aUMf~Dqnmn0--QFhS>mKMvWE#Fo@=k zbO={20uoV{>eMbhd4ZL@CPg!w;kJr=B{#Lk>;x?bvRD}aH8X+8B~|yV+#3Y4c{S$5 zAbe3{VHi>L@c{@YWZ$*FMPUl7m~9hUw#3~yx8iSmriQ%wce?G}2NgGs(?$5_CK z1B<|q5%C3sJp&`=QM)%BdA^Nk#<1oRaoVy4tMonnGF#w&(6|@i3}~TSGxl2c!i{A6 z5Lye>{4}z&o79lVt;RIrq{A-LJJf%#Z{pSoH_HG&%)Es6h7Z@#eZgo%UA>IUq;Ogl zSjpw9992@-~;q5p(|7z`T=ZkbFXLkaZkAY*|62|h!KWz&PE zD(+(l993hU};h7qp_Yt15wn}@(hE|-2Dy^_zm$(oG)%TAI&PK zQdvs3jxpTuC~&?#jl<^ z78LvItbF3&?0m)DGC%aD^~bLZ)2%;NYsYIR>lPcOzoc$)>Wo=$@a-<0@7McmJ(XHG zb2#{W`+}Lc%fGBQ2z0jWF2$T|V{2xCL9oY$#X!T6h(lM-_Ua!0R=+sb!LY~^f1x|Isc*(ekskVzOuDQ|8 zZKo7=>)+N}1^GO99{=doKiZwzZa&>@Yqhb>u#*(^v&Z?{m7@k;Z}53G;yXp@+2DrtQ8p!VRB5Lc)5 z#T$K2ZoK(AkS>|$q@4U*%#@DbdNe@m?Zg;#{8lFV*^S}n zP`?1kNcG?_e1lK7S`G{YDm4`FTUWD;m{-_8Dt^~V1 zq|vcR)4I-%GpA=>X5XI8Du-Ym0gUAy7beQ?MeZGvR%OWoth5%iZWc6t9i{uLmfGNK z+w8W3myGO>2LEI-GV>>Wx_-KU@EHUA{gYd$8`JlgC3 zvVL5|l@V|xXSUl1b>}Zeub){A-WaqO`xn`RJcM{LV(XahjvpdTYe}}{`su$PED&_` zo#>FthNQ78A4Ti_AoogEQslzXynDHnJW0sK$CrN2|JL|f(MHpKPG?LuZ?nDsi_Q9( z43nac({rsoKFu?hrrUIC_0Jp6gcb+q?$!BL@2TNV0Va?q(kyR%F0+hV|R)(6`GjV&%9m7GBeQwrD+ZRj5#1bhF-o}y{1C1 zLk*ba!YWj-c`ihZ;2}Hj8B+JPfUv+5xaGljwvdO=$OWaF6yzfHGIo>%CW@DZK#n^x zP!KpSCmsTIJ0Z6YYI{urqOV|8&FBHH&39Zy5ObTjvJH|vH1O+IiJ4S6ylEhMtmLU# za={STrAno`hptx6W8|prW01+@!8|p36a5^(7l@SMLj>1)oYk2b`@Ik0po-yoyAZ7( z^CP)@&+Kz!L=W5A=Ltl!1meKxcP#GIl*$cF6A4{{pJi>eTSxzxw9XoU54FlyE?;cC z(|+1-isN&;In21I?Pi~=Rr|TJa@*}5rlwR>nUBerS?jEILpKy}w|5GVW6au@#bPRZGrNkVNqf2{YGHjq<`ncW zc@nVeR^k2^$qudd5tRvjpWX6%`9rVIyr$sFRpAdX98|0^)aqe1jiMn*#WS*QH86UI z_w-HIRw{6Hh1?y`ihC3$1AWKAImosU6axJo)TDXPs={Z6ZHtx!ufPIxm6kN5#teFu zhS?^LA?1*gsb(tsYpap*J}e;*gM={%Lg2tOOD>;c!B*5HI4l=}G9_0W8zc5e^61o; z2eu=6`mRu|kqlu(7fD+H=H2CJ>7{F9nMvT$0$oDSOQ7YVo?1{!h;HQ7$OH3rS~)+ezN}cXIp}QHH^sB*0Ux&diA~5SL>hE7@a=< z^^POY{^V@+>ifO6cDjEzYfUQ-);E-GzSL{*&;+Na<$cSW2U~Z1{G0CgCh1y#Y?>)G zNRP=M2rx+h?DE5mIL{!N-g$>t-$xxV^xpg*^4f8Wx2Fv!3W~!FMo*jDUHUJhTgD+{ zM~>gljnX;eMappzIGzJ8KAm;^Jx;+b!7p$A#Oe~(rYd4#fFlE%e10J_YmCt~RVfyZ z(Woi>82-kaZVyd9E%31i{`7kKC+#Dff`6?rbhOwLK5;okFL%@I{R+be*|w)WH!NLM9sji-L>CxrH-}0w+YFW?!RqICTqp_n?EyoaYJpRLVnO6Qk#FOV-E83luKb zB_Ok8;$BY{)L>jl#$Z>QmW$Vr+MvKKD33Nsu^=;$%*vdX3kcKd z8=7h@7FhzCj5p94xlqmqwD5VDuP*_3NGHQLz@#PNGDw6B1Jg6)5}->SS%);l zh6Oln-12A|e&F-?eQx$9Cg14l#WcTt>3GB7%m~`L=})QKq*f&R(!%|Z`b;{Bx#w-~ zmuPpb<7y|bv(_pth;VF85#BoP4_=yQ_S>I>7x!NcJ9^8c&k7m{Khp<#h&;sHu;_@v zxr0SrC7b}d3+_c^I_xcO!+JgTA{XqxWH+*#H5hSDN8v*S%aEd^0A4>xts*;*_*ti; z=>}3FF=ayFPWP1`W0?eZn0+iLc=s{p$j*(8hJ1dcERmCwGcSImv9V)HBdp5;R#C2) zT0;{Y@DY}LnRnk_TTAO(wU!o5e^%c_S6fHRWWC?hC&L)*h2ut5rT)MDOM7rl>;L@H z)(X|t(whCC|DV@&_aEJsX<(mEbY_+AeE5sU551bFA2;r6H~N1)MS0@Y4|vO_eeKbn zA2$8x2`}I&2G4yyl>6Z2vFko#<$hz={pMrv0^aC1*5@{*zCH`e80a=Ki;Bw z`oqLuSM%gI-ybyJ8mt@SZ}!$-=WUp|u{PeW^DfvtE?Czz%-jUe{mo5-b>8~x1_kTJ zhv~)zo5zRh8vk>~B-q3>NLMQiZ#OZ~HaF4wBVOAa|FpO;+jrpu%#eYgP~F_{8QnbX z@Q(O@8Q;|2{8ro8G!!4h_T9g1-~P+abiu?_W!DEhYN@R`s&!|;JTea&0+!v})GXGjsa+2br~#zWbM5YsHGd z#B{~V?loR%i(m1#eYc`#VmfR03hxjfjL&NO4u8e;Uz^{B{}iwJFdg{x#&O|41%(gf zhU(`1-(DvF|L6bz{2dq$|2GM13+n)D>R%?NnpG64i}fq#LVw9Q_fedHDQc8Z>`1Oy_N|PQT9|CO-4m@oxWlu6dyO{9){)c`zcS z1P@Hj;|{NCo<88IT=QuC&x6+!FXO?hd0ZQLwr~1>9_e_n|DR33qfN6X?ECMXLG%2V zoexWQvan+q*kA727WeLwwM*C8c>jgXXXE|AYU>)w%t`EOsP>EW6P5Q9KCb_-C(VnC z(%BN(So|E{MWDE-xWJxX)D{9|*)+A-??vPs44-p%yB1lkbvUw$aF7r{^=AiAzsyN& z7ghV^&P!8r6ttmMV@qb9my(vgntPoaOZKK}O|3`7 zBDPnVH6+br4oO>NYg!i-6GPG_fnpJGQ-FbKu%cWhTvk^bdOV_dZIY_Cy&~#m% zY&mdOp2E4WCou5}3JEeuI5%Ukp-K2LTRl|+NP$+o{K=Xxx1JvNwU5qrAIV|aV%^SNuSr$OGsK(ZxX(&jMt;sysk0hhQ3Rw^mL4<^gJ1w5UO>- zb+@DS>!OUeq|){|hx+2567)(QajiR@xLZb_pLnQrjnbkyRl7!&+~=4aaW4MGF|@=i z>`cYQsN&Sb*sV3b_jQUtY3JL9ck!)Ux@6Y|(!8Z90nXLio4;U_nkBzfYuSZ&xr8d+ z1GY^Z{i;M(MX-1@w&fu)Ly-Q8;O+M%Rjd_SZP)e1(Z?Y@nF5Oct5ijj-;96j7ng1o zny2i7QX^4odvAGnYjlvSeZVi5ipxWquWz2|+?f9F_1Rh0#gokhyzjEULE_~vw~l<` zT;!ws)0dm~aW4J|TG2(7>zpWK; zuKRTRsnA$m=jLD2EFX7Gv=s`N$7-UqI&G{=9vt0o`b*)CW@$5djMU!5{;96&*!n(whN^7nwfr0TERTnhVcDj6d= zaQ*w$8b9c;I!}_6JD9|HGu&I@C*V&(>oz5B0VPl&R86sWuMGE-@im~E7q{ME3<(ct zKdYPM;)~!Z2%8Y+h*@!qkllck&nHup+$o_vuTFtWe+rKcIDcB;az$bmsiZ#EHT!YT zIE|$A_KJqyln@qy%uqg$KRL-2G;nL=PuSt)VaBjzoUECZoZIrCbn8fr(O=trzS?M#UGUFMCh1pRXKkKn z&z2r4cIh11vem)bW#lQ*ki2PhW~DKdZQVY5Jb(B0vQDRlq8G;PK2P6$cAdS8Iv-6f ztMI&Wmo4B6W!JBlz9DC2QKcUw(R6xZcy~0}F^9*YyXQO_of}>G_HG4%dbh&yar7Iy zRfSpC!|bp!VwH_&X^p62aMOb?O`ZGoZwDUPvgOoY^@~rg@u%zR#1|VX1_#7R{zI6$ zBREG1PW%U@QrYha(T~8iv3~B+{&y)ing_mZRxPTg**Pj%9a|==qG2sBb0|#U+m#fI z9EQomxO+rXC5dyYiT=Ip9C^-!RDAJ5omhpmF}56OnOqfrN$~`;EKRbDKs42|tU-^e zL55PpQwOa!CSG71K*oy@YQ;quU+}tMQp~>qfh13)n#S;*4}&S%jR2WWBiu@e(H5jg zNUdJe{KrN2n!6-O?6-vT0iKyZ|_0sa8{;!Y7IBX!i)U6Cbc3l^DF!$-eex~yMx8?y74b`hE30e z^t#+?&b1;jJUp_}jk6_Z@#(OmfvP^tiSGBU(yy#h)lfrvFPN;WcKoi|(L3`IcOv2j zCo9SC_VSGiy97b5u%+fZIfanS99W>M=@~|w)4Mmm-Pig;=UZ(jPd*iWy|eKij-k(+ zFBr!4nswc8*fth-$It9k=lbJG`Mw)wm<5Gto8n%rOUv2!qje$iVMPAcQ-9r95fSo= zwIR;+eq_k&=)&K#Hs7a|g(Rn+EqZ^yS}(G|XuHGHZ_{j#xNV6jzRz(Fuxc-U8f#HJ z`gYqEf~+ipGx|e7sNI*aGBa5nC_n5W!So-&{8)}v-2W=R-|2Wv6F({=o{9iW@ z9jmZt9CjULO;vf8O!Stub)2y;OpePh&~*%UFwDq#Flaq7_poBt&-`OmdF#C1Z*Jj7 zUV7#iZa>8_$jOukkc3rs{zI|WWns=eB`%>E?;pn!s2qknRT;QfeDUIU;_skvWIyir zm(D9Q6*;mA>Flhoa4eBR>DM^P)y(;!Sl1jG`y%X7T!_clxV)w*02+mSe)5yMr5c$( z6LFhLIz|u+3+hCrnm&XOJ@O!eG;zEmgPh5XrZLq@2~Co;@PCP{k^sgfuZyXip=xtqIW#nEEX>@z$^@{U^SdhxjU0V*jw0#wi$> zH$%qQB}cfC=q`vFu-V^5JF$f?L~CArVUjCcR`H*x zrj|KVHEA+ApHCVBUoF<#Q~G|HGcl)FuF2sp)$qf58mF+}4tA6793VVV350jK;z_{{ zM7)r_y_f^K61v^fLe!6?pM2OPlSkZ^} zV?`^sBl7?0a6h>)!t#`A{i4yfm)ji1)7-AU@FSXpG$viyx%EMj#i{4Ix%xguoA#Zm zF`2pHxMeh(5b}CgaH-+X#_icJ->%GNKlN^WVLb3@eRnuLksRyQPe`qiC*y(6tFd%2 zHf}$}PvfSgHPy&?oN=mj&?~w-ypz9|Vq<*f<{Y)L!?V02KKoZnv3!r?qemIKhdJq| zu4L~IN#2qYPRo)UD|+61Hs4*Z`9QGU`r^%IAw60BjQ*^F6=iNohE$RjJ2dP!h+%dh ztQ$$pp*Qf2NC)#IQjTfKSVmJ_`O&egf!R43o21N?v2zlY{@qd~#eG8Q$73(oH3``l zH0jVhqzcJOhG(!fI}emdxmqUvW)0C|T>1KDgS606 zq;`ssMYV+e6jr@S4v!-ok64mF;Z1TZ9ECO96uiJqQEhQWr_YNd$t|gmsPKAYUf~t( z?Yi6B^Ujk>qcQHTtreMP-n&+8ykBCYRRdTMM1nEm^fXp9^-;7gn1EAKPzHsry^h z<@X0oLmH_i4`+8Ej<3MH`5$JzlVw@aj>hQ%zi%4{a_?jaGIU)dt~CGLpOMO57L_U! zsRv%g?DkW7GqwtO9PVyf5BoxCrgJQ_&Erl((edL2QAYa`tJe)|8@<_J^ep%4NT$iV zng2;j3)tiCcqQW3SRLneG20wsbUO52dW4u;e7sTCb$`mv5Z^dKmNYv3{a(Krg>C!H zzn!Y={5V;?RsON=ITO9riQWDE1Vv8P8*F}KiVIbl3X+mQntS|9wd(|l*_Gz)`6ZPz z(H|HHHo`C>^VEJNOggqYGM&amE+;}~2NRi*sB@s|onp&jGLU1rz_y?eF3ANvByXsf z+E579=rDiPO|AvL*g>lR_wh0A;GF2?#YP0!5JTs+alS@WrXv$Y7p^ z`cAWVao4!_jkf3^Oe+tfTZTJqE^Eu!i+OhyO)AyYQnfK0s{QzCR@HE)CcRM)4ru1e z5GWuAghpmAFn||X$dxePP$hYT5=qvumt+BY)V*kYPI7CSY7{bgLj^6dp8bVvsa zblrm}p(a&?fNi4&#A`uAn?Ihvp1GTrGsuYVI*m3&7|NF*-t8!4pB>Z{7_A;fYMEpha2JJ_PGaJMt45$eDKe*636Gd z?;pO4NH)mX;;_Xi{X+iJ4R7~{jUA-WR&F0HIb)#Xr>CoPOwW9V*t%?(zBlM}cbFQ+ zByp-d$sc1as6EIupxcM?h8NUBa{~U7n3S+TbD~RDnIV|yWz+f@iMV+#`1cR{4>Pj* zy#Qc@6>bnR5wwJplDx@@1N44Ej;y-hPfY9Y_gk6o?;naC#u>JdM_%Yp%uAAxNehG2 zGRz?~et6*|j4o@F7>t1317yOK7=@b}QxG9Uy2}{m^7)Deq)?+ogK-aztZ7B+vV3Y< zzI25j4I(q%{{3YbTs}k*7d@T>(!a#VW&%t=e}V_DKozrr%~CYE#vdOm01}Q zT4oA&af&_qA@_6k!-ehG+`soOeu!Y?b2g4X-fjQfnEu|vab`4`xY+%E{5#b$ zZbcjwN;S^%#Jp4>Gg~x^cNi9!pQcZNCuYp*i1}v0i zWIh71y@bkVO(@{wj;7$FrUQR_vSyNxq;qadi(KBKQb8TZ;{z}xQco$CClyU~O=>O= z!;uyM&V+3IA?^fg7kSe#DPZ`p2Cys=3%DBbZj*Xx8s<^891R>HPc<%GSw-9Ev zaD|+YCL@>E8IA$B;@(xop1ty9&CeNFm^-O^Tn4_f|Tlr8QJYhEOzkdkuO@2Ip~wE0NbH zd47092%V30r#($dmW<=hsbV!y$U9kVA!tH`u;a{dt`lLPJN*}F|%SDIHDOA|sr z%Q@gM?%coGo}=@%OHgEFb;dikYw0oej`o=*-E-#l2U;8Q|JW6DreI}nw0^eJsmJTm zOh1-fIZt1A2AKh8?2FI*;5d8xtBx(Btka97z5({5gS{1R5+f@z-IKYbp;!vV^IdbQ zjrrZ0da8s%aUFY7k>EIYb{re7phZ^c#}0k(o-)Z6;i@qD;n>xWt9H80J|WI2yDYBk zl)47clRm+vVWbID&47RISjwKh8ye)c(73jGB)iOfg(+Dp#`GGaFpca z1a5Kzhhft77<~>>vvlrp*B8H3yXRze@ey6D`5THH2E&QFER&-W+BN7!p;L!QiQ+VI z%TxtpICmI?Ak5K+k$HuF1@8AuMwSvq<0?9OY6f9?SvD;bRciz!3_1*7!~VZ0jGJ6$ZWElo6^F*05mNwDRuH# zFH*7feFuB9Ip+b%JQ+Qdyp`cM_{tU%QqsaAIOsoQk=V_OlF*95p$Y{oY) zM7h1Z<3o2ZXDnm3teRl;OL^oqsUY*w7~Slb@>qk)UmAaH91xf`*917Rsg0!*vz)O^ z1vM>u*sqzU$R6tMnAz8wd1?LGxXa~1<(C{E)HbRNw>suUtwK==*fpvNCCmZ5EEA%j#dAz-3RQe_L_Q8;B8>bOhm#ZiPkGWlPyMM9l zb;WRE$l;og6B|rKgwSSbV+}QT=5N`l8!No}@iouML`5$MqbF@by))%G(?xVhep1yV zhq;vru0@7hQVabyr}r9~Sl$0$76o6Mm=qN@ep*;|)#OLJ&0qB9j~m5jJDw`N9KUg< zPit8F-|I%TQw<`{GQEe?y@Ts$?-x7bp%57jzP*ZXe2j*nuu>?eZ4!vO@d=`oU$?D)BEr|-5u6h{z zQ4%nwxEOfmrhV7K0nv6^oH~uPT$KR#-tW}%@0L-Yws81q$xq-EaZ03@t*j7!4F55>s)pRSRLV&{(;6a?Lp_ z7o*D4_gV;ypXG|~BK{A%@U#fYgm(oEs4H9HQ48b^6v!Pw3KB*Qw^V=!sQFayh)K9p(LaaLsAYO=Q(G~_^^{4!i3~W zp&-NKL$riE=#}T~uc@BAz2111@t@qS@GGr(vNWb1r(RF3c+IUKdkva|a4fctvc9aO zcpHsgbf^i~YiF1-YgC*ZSMKS}HsyY-xbmAp<0nC+SEHNb-?k@w5o=@i#jw)Kr-cv{<+UOF72Yi;ir;=Hr%oZW+`mV1Q-Upt&SdwHsEi612} z9d^v|A6~^Fg!bHudYG1%H$2Z&dc9%vCp6Sm`z;VqI|b&}NmPrXilljl=ioY>iUmNH89>Vqh@e4}2h#>5gF$H7VShCtLwH`QZzTNsgCmvfmw3>GkO7JSSdt zf5x%mhO3kOs1iSk>l37N6`T`ZXOlZ0O`wFU2}o^Lnaz#IW(dU8+so|3ocM_4=6^8! zz7;k8gDTj6Xa9VXUzs>cN!Uf0=n+=Cs>b-F`PK!Kj+;B}ciC@G*ZrwvqJQh9=#Kau zWyPb}u3Zl!I?NuwvCco&`1|iqH$S5n{L_xG!@e(ut zrQOHR3vb6<{vR8k`2;=6-?A|-ikcM)Uwmo&UB6uGJTwt8T}kQT2o5H>DZ+9~ z$DRs1Qz}Y;S6U z1p+D1{3e7~ffmbEJdrhFGeg7eE>|wV4N|~O6$#Af64E@~i=OG8!r`V?udYVZvsUSJ zgm~cJP4<)cl?gc(UP(%?et(8PjX|)hr1x-ku#;N^!W!V7P;R86q@rFAi(rR)yCygP zBP)wE=r{I z*LY#wPqVW-oH|VXPL8geq<*<)TTyyW{(7tS?9>hE4Tk$fgsr28iy|T`4fmV7w7q<* z7~}f}^H88*~BX?z?p9_uA}EffgrnqTgE^XB#VJdNV$?U^eum6+EA zM7<}Zg}t7foZ{feHg$KL6uW+>Py21FbM1%Bq8+_9rLE&z{Q}$A*}_J@SDUnx2b|h( zU$mcOkHwqsZruYB)OV>B8Ijj3{D$5Tc4azC7?=hq7nF3e=F$)8H682(GELejeLsfg zTfhZD*3d6(8H&0@{J8sbQ4k8`G6DaB2#p7oWnP|>h@Xr@U=P7NlTTflYp5cS=KE3A z_13X3h%`;OEM=COmgmUj^D2Pya1~FFC#f?7#fnrmB78(G7|*sKq#bht$jkxs0DKR8 zBQa_>6=tX~30@Gf)#4m%S2?n~Y;19O7mN#p@U)0DHv>L$qd>b_0)PQ+47i?oqVZ_2 z-j-77?V_;>X${P)C<5ilPh=bmvW};|s-6h$ski~A{Tg)VY9^a%p45b)qF_dQmei9L z9P8gWa+A5y(O&bog-zBx($xCr6`@1X2`Nz@USF?pO}0oR*7$}dq?UG=5>1c4@ccF* z^v?cdFU(*c)syZ#d`>)=$sMgY|9kSL(H~NLclq6ExL#r7$q%Exleik({`I}AD&s`_ zoy4OL4G$&Gn5cg(CZ6fHxsIgA<=KG3f5??oef2h0c;i)owv1nh;|$ zj;wmCBFC(|Uz@Je(QNWAJO1)}t-D25(*-+lAzjQ_|94V~rITqf+hmrrBYZd8!ogQp z*WTf&!M2URu~JLh|2IkR9*}gN{r_ibdB2;Qnu?}nMOiqMgLgxdGfQ6k2b>URz3Q7xC(PdYTo@?3V!oPEb?blZ9-i#~-W!oP_xiE7pOeMo zOI_UcYLAkKp@V)e3liPSXX3XPn)w9xMbRBqiAQFTrXMk-7Mie?3k+ew%`Y*=3tYu81jQ1XWfaE|qW`vNASqHqJT zu#jqq)PZrIv?(V}%F~SMoJ2yD$lwl!b8eG@Czmhq1=(W3f*>2F2~fwB&n*XX+867% z?P75UCyUF+5eeDwA`pw5C75LmOSA|PmkSyd2WES)vJ*_%{AwbdV62J|n!J}wUqLq$ z#?Jy#OeFHE$`*@GXZSvoVjMq_Zw>LY8}>c&uT)%n{{b5!{G(^_A1*qp?_@l`wl_B8 zxMF8QGgG#(6d9uUF=AeQmP3+)hX<>ZDf*Tcf@91F&O9Inh;bde1j1wT1)gT2!D?BfHn0!q}D)e%=fr0y!Qww3d zH7SEz(jb%TV06KPci@lMWXi!6w!w1KB#CHHNf;)T5Jkg4RkpW1H=XqFs9X5F&g*U$ zfP+Vqt;vE3Lnh$ZneDt573V6Oh=R%-o*J1hl;kgR#xDl~#gYYD0rVm&HhDZCk&Vb~ z_#2`Y9oQ6KK?`mkrd?7JmOjLD^UgV5n!PWNOJzR1Y}W|DC4_e<2HhNgJ)=;)aulhfm!ftsLdi6W83N-pt$tRjn&enyyLSrQIhGw-mej;iJm&hPw}BkD1F;M29D zs=FVp%F;?KlsGk?d)t4Lz{`z)?vHgm`*AC%tl>n?K17Y;T=$ovrs(AxFzcO@j&k!#d`qXzZG+f) zScR6X_#mzUfsA&UoeD!t5b4+>JlCLV6sN|a|9OJR zKiy_$!LN5D`Sym8aIfFVGdjp2+BwkTD~I3OtwlVb7BB#?$92 zahhgaQv%zef5Odo!SvUHY|DL^nCG#<0qori;x^8Hr=c(FI2pen$g~Zb`R&-wpTnZwZ2v#s3r*WoL+w=DIqVVA-H`zC*7Sg@ zFFq=~ci1F$8?ES0wYN_p6UjejFZFiOiXdN749N;WsR>%ZHp2%io(gz7P*&RL7}0E+ zQCJavuE`A_4Dvfh0S_6bMcYqKAt8CXGj3=2|FdoM5)*NsopCRkKNebCeH%kD*+qDmbD%&G= zo$wF)%d@?PIxU(?p+(m5%ffi#nOBM@O*Bm$qWe%9fg|w5BQiAXlufAfaLI4lG@SZ% zDE_iNi3AR(VPhQe7s5u_a~vWp1Iu&9@xwB1CPgAV$>)5F3qZz;MeGmst-xg+Mx9CW zPhbzWRK`WSp)pTpUbti(wnaSLoG7!Ap@dEE6YA;J!wFTw0AHP*s!F&v9Za~K#~~*> zM_WT7crib%AEeQOYyrV}b~bJ3TkcHeX;Cf)6fq`EeQ)&s+tSlZywj*Qh4A318qCF6 zb(xkjq$T%shX$jOrZ4RrDot41f8CkkJZ3g!vHqQN{r767jP*Kd-MJMymXIBR+*+PU}7MOXV=G4IFNfe}IXZ~+5j zjM5`LNGPJGcm4l3DyB9V)I8=cfbY6$08Z@=MG|y?s3tz(?g8vH1m1&m6A~3VVOWU3 zgdrtdsrV(5qt*8MhrsS`hq|N${a*Zg5(DQ zm@wmjyf#fY6dEAe0TQ(i5rhkCFew2gZ%gnQf^(pq(P*Fr>h9J6I=5aYxB&rSd?*LE zpj+?gQW+Luoy%7HmN8Hh`uIfmJMS~x16TVM;E=psH2FBuVe)m4FT1Y<57S;?T& zkeP5c%WW@Qw`RN8<=Bl;?&gJ_OqT^Fxj%btmri|f>(SH)+h;u}4}XU&uK8L{2-)t3 z7?{wj+>zHs4NLqsiNvKGiS}_Vl9~KZ-ih89)(%HwGxqkS&L_;pVIS6F4#zHEW`~on zA^Wm}O3rcSM}WD*GoWb(1us(&HXHIj)5$+{q;23JE;wmrU?q_=M_!8mE`on3u)d;E zli3`*JY(7LQ+zF;6^$u>w-`GWo8fxopB5hhX=t+U?Ax)=%g@}RGw0TIbK$-i0^Z*D z+H^^X#mc8TQ$-}|i3p8!^8G0=5{9TwzW>jJV|wjrRhPJ6vFIA=_EfGs}& zs>r~|vjG>@CP>7Q!OhDW@`sg`73G9397;?oL&5~HB26EvCm9M^R61p%Ld;J6W@vsW zJ6}~Bd^4X0oSqbU-6$GrwU8^(Zh#fF5FnVnuI9jUv$}4vYD*x3{>1=K1qg-OuC1P} zMQt_&uRy%Dy5}iKj1YAW;lN$Sz03xK8WykhY^}ir81lXh#@Hsq7Tnb9-3AS;ZG%`I z7#9mvf~y^(g7^8}6h&pN1lk)JLq-$6wA)}v;6WqGpfVuJ(SQk{paMY`C}k80Ld>QR zNukN95t&^)XG(lK&y@`SFYt|+C~L+Aak4}$?QCyEe#lpi4dY+)aRr#3F}~5pnT7yeYw+HkEklZr;n-;A10IrD?J8?~d2@W853^ zh{a!6J5Wn!>tCVoB0S*KKOc6kI=c=i{iT4?7;`?ilg#v$wFI7B}sxH}NQMjvE-e z?@^MoC;iLb@``ArtDni~w)Hz@5q&YXHCCkDv#XCBr!E#r!8i5FFWuYM^Px>Sed)ts zc0g6dz$4>&CiWrniX<{TQOzsR0pkaI@X}T!;ZoG*gr0G@c645*B&CsH@&_~?gk9^2 zX*#%J(+U}K9W)~&JV6}t5b_uuYR;7vg$`OJJsg&h-wPgF$Pn6uF$mPBh5}fs8;XYt z6w==CN)lC%-Xv`@sU)Vvu&LW5GL8OQqLE6~5ur9UKrw3Alt(hqR2g1R>2(jR_zVi= zI^+T@0y+q(!CDw*7+``SUbxR9DkbnYm$E{2vQ2$1{2f>zHiDy$2yhk{ER__poI;|* ziA{kdI9VadSP9diU}eD4laXp^s3eWfkm&-o*Fi&?OXRECc|I5&Mkb{b<(T&?$(ZiB z#C9%Al-k&MI{vSCno5xQd2yX5Sb#E?qk@qJUIy}^0x&rVG(%1&oZ%R(WYbOn;gE{N zj-Lp1NUTUU6Glf@-VV5EbL6XA=e_%GV%>snUG~3~y?@7vXOi8wU9MS;`!>6mJWOXh zzNvBN1l|X1YcCbG9CUwh8Vb+)?Q^>>j#b*sK1)d`^Dk6vm_I*n@>h&`Q{47HIgDK` zzi`p~>bd3Ze~Bb<{az$ zhu6TTQ`pq01=Dv9oilqtb-tSW$sHCxZ<@Kpxt2Wq%P89?2R(67&@IUD^mWK_&AE8X z)w;9G?D@uW_PUsZb~sgu`wXgz=5rlKv#<^(2pD4lHo)g|^47>p%l|#==n~Az^889N z@@760q3%|Tpe3M75n5H3Y725X!||vMQG}*sw;5< zQNd&+Q7V^_Q02{^(u*WM-Y$FtG&pM=Fq`AIbDCBP<9VTo_PQ#b50t6nfqV|&FQF4M z7mz7Y?`41lW@tEKFn*9wRR>UKl2K0@p-)e}TvhGxhTFhS2s!)eX%(oE2SN!S@ZR3^ z+<0wR=0XK-Hur1=wsLi#+y}A$^&wn9%)z#shb)etZhPc1AM?kd`O%u-+1v^vXmm4a zNd&ms%y?s}CV%xNIqL?PYoakCb*(3Ncihl7%Z;?@=R%nTW7PN7H2Ea^6)F8~?GEA2 z5lfF8q-6W=Oz7qrtN7wygZhfCmc|HRV*r0h>upTg^Ro2{T*JPXuIXp*Z$lpJqL zP2SCRi2eCxv?ar4di7A>IiHfV`}=(C-9srxxmxb)=yU&|fin|%4s z>8;jd!`t#oUL3p}cyHi`URUs7q8SA;^rwH@KkUL}kq@6LzL#K+*mqI*QE@HcwbzNe zp5@vw!ua$~yXjxE0`KGMCxV9>GE+2%EOTDi%)r)M)V09AKbc;$et~&>rVO#~x%Cp~ z&RLH}N6eDfIiEWp-M$V95R%HF=niP(?s0EYf%*DV+aMdB@ z^zDKKtB0eQJ^q@*Miafa|B?2TkY35QpKlL1{4M(scv>n3j$uzfrj{I!<5EV1YxHDf z{;{{+BG2rQHnSICocm_1B} z=9a;R-EgTiZACAYlHiw=Nhx?WU{^1(P`?3FW(xTOa9<1t=t>~x1FA=yWS~KZVt}nx zItfOBNF5xha;1*A0k4fs84C_apvmBjX6Yb!MMr^YHDt%YSVjiDJV;y&Fi^-Lh{FJo zjh-b%{fIBwIh(i{cKnUN zAnqaavxuhtq_{x$hR$Zo_d-IoOO5RXpxO=(_jX}+3wJd|$*e!IZJZjL$=}}7yH}6! z=aPds8EY!GYuW6~3mZhN7mDpxRWVWat6exnnEru%eI;(sE>uKccD(T2 zj*;BmrIF=za`&ulI$42@3*Jp*;Us!bvMI5rLukfw9&!jLNqaGIV?kAKt zbe0r-)%T~J*@O?wjrZdXFHXVvy1)~NMcoUWZjlH;%q#qMHc`HkIFCm*;|oNs?Vvkp zRo(dl!IiFdo~rwLPn|aVM)hQp!Bwb# zIhhP^(^_vjk*b&LCeei?c)6q=&|Klyajx=QCmwKcbUZQj0FTedD&qy2EgBpp9!5bc z%+$a&ujNbee1QraUP)tqR5oUI&;7nM?FS*&rkYO1WPFh6O9pRbcCToWm){al6zu$8) z?`8PiFUwm?*kci~HrDUuyKY^Nh_Gav;mn1_dxjNRA(-Ty7ELR;`Nt-r3u7Ojm|$A` z9b)jZtR1v=OJk~M^Oh-3EINZP&>Z(ho8-K<^)uf&!gPxH!xy(-`bYKH0kLB;ZkfZX zFKX3erI+~KRT2$>O*UE9;oj9 z5NI~w>R09V4A*zYIsFIg@eAL_*&O-e^<%4_Q*IB&JnyP%pIUUkMf5B_Ifdy@+C7tD zm!r*iKDG=K2yH|_sdF+V8BV?l9Wqm^dMfJRUg4^qax>RdTs7xr1h$LIhrGRfF^ofD zLzk!>=)wOUJPSbm39hi|gh|?LV+cA;wlV?UGIf`_9olXYEC;v&9Z^q*xCej=n32E~ zMhw0J7%4)^k%rF~z~S~876QSFOf4ULOR#dFu6wPE2XCRVGC*pKF*Ou~wKZtM4H`{1 z%nUlVwEV^OuFnuU3U;S8NNR!b8PGo$VAQg{z=dY5aUM^Hd5wHi&VXi#1p2M`~=J{b%>>Kb!v9)Q)U^&u=O&dcRX_nL;>`Eux9qwV7S;#J9Gy zF;&?hzp}wRg_EFUh$gAa%&XL2yKe8)wS7%&M&YV_|xU5b-|6P z{tS1v^OA6Ny|i;9)vdxAf$f@tUFsJDOH?RZ`1>IA8Tlx9~8Zq-dsbYd@{X>7v(ASQSn~uhw4Vd0-_N#OH z>66dyl&0*zu*Y?}DbXf{?G#P(NPn{HR@Ls_u>N;u-}_AkO#NiiV0+}7qS5U)ZBzXH z%xyZ0Zi(&2e5SuJedRHge@6&b1dFn`+b@x?4+`ligMn@>cf5A-VNyAPcPnoUS@sW) z9Vs!{YyI$Msk$`fY_(|tBlhgMyKR}M%S3|km$`VLbPr4yo8Kr(i%Ht05(qLBkLzY3 zwUG`XdGH1rAn50TiH7kZ)@gQr0A}WbdGcj)dxaF4r<4{f8?D53K#Q&DnLf-BaHYUi zT2LmZfy_#dCSeMtxd%n`A$6^>Zq1%oT+XKtOT*ua3cyR%eN#)^WQ7(eWRc_wUL#-R z3SVsb3gA79bx&be3Rf+Lcy4In0dD-<4MAMs|1+*7fc=a;-RgN@A|sg!(Y$h!z5;nO zbRZ^af*Tz81TZJS8H?F8?#!5k-fMDGuV+d&Cnq;2;h-f^!P{Agmmgm5&JIgjA#XeY zr=>15p65+)^SxT+2oOFJu8vg5_-S2W%Xb0M6Pn25<0Yu`nXW_%!B_B{4U{d&c4>{kshvZ~$K4h-_+ zzv9-MF2yAXU*av;W}Yo|cX_mnW9}`LIKMNCMc=pmzSipfj#MTAr%MWRoxFz3Zw)l9Q2Wr00s_V%~)CcSUR4w@(-6*|?i44)>*cVILx+ zelT0BM~aSi6E3%~Z8d(SGx1lzhhWDmLaR!0YRr#+be*CU@7W#u zIK2N(*36Hd<%m44T9~`*KDGvN2;q3)yiF(D*{HnrPwv#X#qg{$1{V1O7Nf=kAMRPt8x{q?i5>^UB5V$5Ul9CvuMWzdV|3J=Wz4 zPH0>;8L&c6kfS5KEhfnM+lh!B`6@5o#v9=qc!C(O{*ibb+9hw{BE~rMq!T=tb&s~5$ zf+JyZ3r_m2I0ki-D&M508Ta8*iF&3?&tg$w6%DMFiImIbM0oi18+u}>0n~^=|I@li z7BaYqNILm(E27qoD_@cCkhBCCV>JV22g&_7{&r4}e635BpiP{;!b zU}-^pNCujyMbxS>h}?*iBq@qVe_PsF>*K+VV=z%Gq{4}nxX}{#Z)v`jxA-Ouv!AW= zj`zRwomJn9{lqMJ7|`%=?=$80m>v7h3g2ZBrH5-t;DMcxBl7tgRTp2)=d&>%;%i*k zf%&dzL|*eCazElgZdR4>`TK+>LS-d63kx9$0ny7Lc3#$B#O#`C-tn(lI14wz*&Yuba0&0l+h$<}p-uB2-gkPzv%_G0tcRw~ce)`My zicquX$o|28-D@vlE(XI5sNF5x-{)-h(d**bj}xM_Ax*?odT9=>_xxKS*ZIOktW9n} zL0i&oMObkvS(wcA9K3`>#6M5;6gf-7`~KJTj{B6yi?bzJ+@ykX5@K%!?m&l%zYY$^ zEaM0=8~T*&tKc~2S8+4eocQ_2^xnal^h7mhZH`KdP>H!XeJJQ-@KYDWQO|rBLS|D& zwfO>Ymv?X@K%$_J)-uEO;ZU_k7Ajz(D$~KTTOv~?0o4}@QYs|Xkzg+=L$8=*$P98N zRjNnp!9WO+`UWO*1@@K7^ph-b2SGrTl(eY_H$4n*BVqo}B&ESeNGt=O0`NF)f>u#T zVJHoou;sZ>u9LyDItiNo;cosh5<;NO>K4!8SE#9hLQj5JCXl*(U>dfr@_Il?Xyy5& z9+)E%Ce!c}6?~2uru>&RI0m8{#|RX&(+Cy(YEo(-w<(?@Y!bmV5|0wK2*Q%M`vb#f zn+P8iG6Z!*_*dm?;c7?d#&WPvHM~Mwv@~f(Lsezo>&t8qNN@N4I*4oH=i&+TERpf z4u4z44GSz)&%a()aedX?LlANhVLFGZb$?J}5}EUpjjFWs!dz^Cup%#A_-MbgB<*Aq z7^0_FpMO!&dv;KSrsG{I!C?6K<(b2_4SBYmF|YrDwws-?E~U+e2sx@+$HBcrjZXf# zW6WDymd6jhE)R7pbri16jouXoOt+F85(Iu6LV3>JyLPsx-`Cx`Nyep|uQ)j+GT~e0 zG&=NtNU5}acJx$vb96(5+c!~fkWSykW~}Z%IkmL=PF}w0uif+4ODT0tpM0qPo&QAjx{S_ESX5CR+xPRR(xQyGj2CD2rO z29Zu7D8TswqX<$cLrEtniHvZDoJyw?pVO?;hqmh+`rK+w6A3nI`i_X(cq@3)QLMwxzL*O_yBAlW&Yelk5B&cYk8;!C^B zw{Cld(-h%k`axfbN%HnoSHFvSpSf9!PdV)!Q1G;JG+A@WY4LoXn|H3vd$=FPsRq2X z4nzseGPo{69>*pYUz2&Vo$=1`<(FQ!Jr8+%FWGapfafT3}zIX2A`TL!7^LrFHi}pcAG-}bOyF(Bms5pe1U*N9#i^ph!h*|9p zp(7CyhNz4!`_3yFnel4nbq2M({(8ow>pjWtT9I1ZoUXQ=v2@t5A9KiYudn14 zfO1}lb!w>-bF;JK&?G1#VO+@Pf+_^EIS*tAHZ7=het1+gO9!@*E(wO-8O<2wVwYR_ zHIa0w0z_Oo*lS5h3R#f_Na!qiLmxi$ZEZMNG<$R&K0GW%-Y^=CKpVptsZkc72JHrZ zofu%ao7vg8LfV0K0pzp81{2sUAzJIus=IF(tz}!#`ay&RfCx}Js9^=HdO@2Hn#HCR z4p`9ESZPXX_@VI-zBq(@+yyAoETQUgL+g2r?nvhm=NO z;882_ZND^;P>R$hybK=*oF0EasvO)zaq%2=1@3YMp`4GsHh#G>E)IwILE?aQRE*D) z$0IVG0^U@_-@{#el)mC^UAz7N#`13e!E{%|uKII3K1TI_9}?wunpfbcBRg&q9F>F+ zr6ORn_q=k#<4fTVg78()Nbx;azddHZ21O&2JZCI2gF*EC<7=;E%<|s1pa$!AbwQ)j zR-6ySFZq~oZ9ehA^s8mijw1*GpurbhCip({O-B#+s%>)orVKWY2Mc4cfqf^FeAP8R z4&<)8?$6I1vcJ@i0-oIu2^FuUI|7mMszUg(id-=MwZQK-YRB>8FhqE_$3H|=e{S}4 zIgx4o;&%}m?Bn0x+UZdw^Qbk^ViPb~gpkpqrs9*1EnQEm+D@?%*vIK5U*()@djPNC zyq6+}@k17!i+3}d4DM|wu%Cv5J%$?2a?QTKyyx{u5cWN$!2W|Yr-LrFQmnk@%`1+Q z!8D6GtdC?2YH) z0(cJz%EmH*MVTxFftN^SN(~e_30^-129{h3v=sOzrIRv9K_D?@kgj5^f0Yok!jOQ6 z37p>y7AU{q0GMPlNDR6BZO?*Q&@P(!TXP{$kQ99=@y&g>Nc2>Bcp*u$!jKuOd0@*R z85b{+lSm4XGC=VoQPb$jltO?E;PMHDHV{$6kki3JEEaXFbNH=uOh#}kcP8(-@KsxJ z(wQ(zcl%GT9iql4b%DsFv`T!iqm~?Yr!2D9 z;9kC0;y7@$X<#D(g~#AgCrYOW5`Bi|Pu1wmP-RWmbVOuWCgV-{#r-LnC5dITA*lD2 z_sCze9fxMop0$yf^3(CV(_?W{fAL8hsR3Q2;gNrH96qp75H?e5v4rs-srdB2^?$d$ zT-H+UNRED63D>zLCLkHrhO4o3e*N=R3wK=c?*i{W{^s7$Jh_`Z&IjKzSv0>a_}tEE zPULBCbX%pw_wU)}SH!*h^S<6=&ulyEO_QuoE24vw&L=rf4;<&E&ciHxaj}ax+Z=m* zVo!R;E!9rpA?zmtjQvu>5Dy?subhTw{_UO_qlzpFj=-qecSeYd2UEmrvVQDuZ6qQssa!Mz^3IoYM`L0FkaBa zNR4O#5@F&gI6(kO06!9t2eSXd3U~@$2cJbsz0Acvam8GF!pADgj~;-w%5k~TYiu)t zlgR`xIksx%2MH$Hf*s)PpL00oo%tU^qR;G_+Liijt-* z(XO_^{=?h9o4VX)^>0EzZ|NY|c}kT{X6|v(0br$APPf5qTIy;QNG)l9cF@O^SQgHqn1KBD7 zY>NvqY9QKbx6}}?0gtpX0D%iN0tNvhpEcX9d1VQt7CO4oQd;5G|o3%bB6d<(Aub+|pyBXt%HV z`cEUw$vcEG(aXo|!?-GLrb^o)5@gPi>naG4@xjf`Y&r#Am(=`F@7&*EQ$xJXzAt;p z6Y!HR2hCzG=B>5X_@KTm*;nE=lgF9K5Ai|3H8vsqE7leDL|C;uw!LKk^n7eZ>Gycg z@|NlZikkayFpv!!$R~&NI>6?)U^JLVWv(HnLvJW7A+iqsM`j9*y+=;(fR)3KY z9QXQQoYMdt)ZBGxZsi|f7sJA;k{Hi(Me-X@^XR?%AXf8oe(Hbo0!3*95P(FK zVzTqKH$+oA5A<|EE=;9PzMLiMDf0Sqef_hc7GjPJ8`~>^5ZhWSR4Nrf)_yW=hS4w* z6y*EKdgWW=7h-nJE2RfOgu6#sQ7Q!58(l>{H$<%5O)1j&TM+bR{0c5Vt;m)^veQHu zM9@4g7WrnceHJlRq>Q$b&&E$N>Zr`Y=pr(;F%X5ahqItsxXU&zo?P(em#GJ!K6f-n7n~x*8a+<=;`^&9G_WrX+XWfDcM@R3IFVD`$ zK$}KR42~cl2I6}rAysSgB}Nd63`OoaRSVGwRz6Z?Z|<#snwVR?2)MSx#w2u`_uG4K zi#SC|z0q%rP($_azPt@wgd)Mz?C4O#xmxown-sf;*Y4&W>)+wrn4{Vjd_Ts?p|i&- zH$Eu3*m?S>_CC16cI^bzaW$uBl3_9K$-b>PR=mUe^DeZUFx8+%n5kLzGaVb{M)RfkXq|m zyYrWMNGNeIwRx;aw#t3{9oZK!$bsCYCn?NYD2~bPr*<_u6_f6aiaBOttH4lm{%;*+j&!whkeE~LG8ek37{N!ZeB>Vu3 zXA-oIO%f5%Fi;hM8l%E=2Lv`S?nEjf^9D!@sGAE7pg9=i6edH@P;MpzOJOh=3cx0z zC$T7nOe)|(6a(ZZ(OEFHnSk|Lo5^ThXfoQ6CRqjzulkKa1PZqLGl$$|9M=d~uq%#|rp;WQK_qMxVlw9LyYj3UzvXAL8yC^e(67$7E*^#);9^i9q>AbMR5_818#t@jg+R; zSbutLHI}a3jhnt~-+3h3#PqqfV?aXAKabhGa7l0&aQM6Zz$+**;@ke{S~@%Zs^E)? zxY(umRaWVIf23PU?x_kXB$Tw*@j3Y|?Bzh1!-_<+jSW9a;D^zDR>^5PRgoZ?OAh2` zY6v>y;b5QH##T-~Er<=@#!!g|dpG0=zbbcJzJ!;+4p{WvJgo5f9>0?=>9srZ!uo4v z$wv4~sYAj*Y9$d+S@6J6~2fdoS@D(F*a7L%FEUrUR_`_Njr0J&TY$})IfKXYLWO^1(%1A_x<82od?PT4}h9f%aji6asv zdH|>)_JL_In*Iw7N;wr6he8VQVIZ4Lf<=o_sD}h7DeUQiw<~%eBsR%3 z_$q-WTeWpntXj}^FY@bPl`cTxfO){c0#YK{*92FMbpwLU9 z0vPKZOt!y(@bGtR3mQ89Eig1%Pc1(Ewzqih%Jw z;~%TS%$`?r^IlcuJ+Blw#1ll#}|9@&5j~Weq`8z)Mq32q(_$4@^n~hY&I%zd7!0Eu5;?YeCYFWEO&IWqj5! zdJNFA8(kvW)p?RkFPjV}%OjO6SmUMKG%_;aykbmJiAp7vN(Byp4CWU-2_9o6)RAD9 z-vry0%mBJ409$ZAK;I5dQz{r0m_Rdd+gLo6=1g#IqW7kyM=lNG~aDPGL|1RtsePhIkwzs zduO%e<>LFs@&j&q*CEt-Lh1qACS3M@T6WTzE6_UTJ5PvQLnTBxdgI!G{-#)qzahc> zkbRY-t^>!hKO3Wvdj}uNy*@McTiBjcHSQ&B;kR!0a8S>l$YweWDXH*X28Eq!FpBGi z5@2gIP!+bVY37rI!RWrUqJWqju3E%YfM%yvi;cAZ({2C+7Cv{>K@=OeaTd%s^HQWH zpAHc=U9|pJHf!I=1-T9onnwYh4R#-YYs#_W{smQo&{l>uvzanuK7?O=H$D zTs#nqS-3d~K}&k3!T`MjoT+f+8lA5UrXJW?X*h7Q6Z#eC%b9?6vqIr>i3YL^%592V z4q*mN327sH4jb|WjL}jOJh)1O1cnUY`7_1rT(1u5fOs%ahqBgM_;rGAum)!G+r^Md z!(RZ8DjOv$kP#)}_%y=g2w;Vf9*F2*qdp`Ent+X|V|#~=R1ON8aNdK6@e_`j$Kor7 zlMzYa%9jzny^%>vZB40%Nw$8kB7IT=hT{^dQOlQb+1VQ0>rXkQf2Ykd-5!*;R*)0j zE2JLxCzeFC>KIwnt?=BT_QPg3=p{PESEvrtPgw7KA|LR+cr$bF=|vh~wW8vIv(W2%@y*`DB_gM2QQ1!@M#XsU?Guqy#0wnA@5r zgR=N~EovE34xVb&A_$HKhzi&)D6(K{xH0t-7W0b5K)yf}5^rJgDFoa=z@ZvSIdDT| zEx=0p1x*u>RNzWQ128COuq1%ff^QW{ISNG&kPFeEXTkXd2P+Zw+Yo`+-rQs1Rx=5_z{&ysn zq$C&xR*)Tt0En|D#sk;eoX0HOBcnc_&Zm7_3jq_4Zw5hrSPIW7r2ClVr`RaWz>$L| z8}@PY7`yKBn)>$d7_r$G(|c^jJ@LVV68D=<^}pJNU~HVDvmB=&7d6Ota@YOAtV5n< zO@rCk_e+^+iT7p0Ljv*>ZI()e`2a6J1$^ZMe@@FB6mAu~c1}L<>Bb0_tsA_x=GdiA z<(OEP+FhlOg8eH7oKIEY9b@(+js4raEIj&8)?-Iwm%ursL-`MkZI}g^qb%HgRJX6$ zkJXu)S^g4owHxwJ=O{E)4T%nO;4KLEfDO`HSz6fprdESlU!XrX1o;iJNpH z#413R92lBpdNhejr&5^|r4ATYgA#1~45b{3HGrvDaJ3rbX+TSD!ft8!I7zRE0(x`P zKw@q}aJ50o)XBq%c(6t5B^xsGBzQ|0U^AJNph+;Ohvs+V!}4LHbqkDI?JAIvG9BzU zs@fra3m}%OoEEr3Ala!&CzF6vK?VU5V9z7SNvZxCxH2`}SsK2eMa@P-{Z52O16E6* z-hUtn|2inbRKYX>ysXJ+7ru&{j1nq`<3$et$P5L=J&lg zM}9AITAP!eUZElzQ?s*RRH1FDMXe;$W}6j&(l0IVmCz20!F_%J{A8+m1>{$n4qV(b zh&-?jH}Zg&Q&RkY4xbLazi3+7)Dvp32DvI$QL+I}GhVJa8aJ>EEEug% zN~obk!={vq){!Ow2q7|!$CcvgdL87E)NQHZ=(!4&n}~m%+sfk`PomZ?@Byl;X#&A@ z2=wgI@>{m93xLGeh(AM)Fvys!Ip|g8krS{gPhLosZWc~L-c$1KvaSwscaPwvCJPdm zV4F1`#B#SZvH@o4L_GyY=t>|)$>RWOU8$=DmX%2&S5^mOWvM{|e(O_qxRF^UlAw;I zz}>IJ1C4-@BeFQy6^L1^<17fedxRXv`60j6cjxn)NNDpmbad5@pa1AlfjyeF-^X5Y zWm(so_%Upi%amAI^C9d zi{^7DpFt?zOA_`-1~x!^k{eUmxGhVi}l)nB>V3@gns0sx9zng2@*q91yT# zOdFad`5*(igq39QEG+{jhxTzkRSU66%P?Nn5|*`Uqb?+XA$C{y7ETHAz{F8L@AJ%i zkv0NiGPMKdqZHT9O{{T_}=i4`oV*H!vl|IXJ_`)RtJ{As9Uhq!*wuVSiym}T1@}E z{R{NYkyXb|GwhSo%TrFBmSy(u$Nn8un)936^xP3_XCZh$+SV8@MU}VyH`MB3+=P|; zq3D0)JqmbGfpv~9CY0T@>})q3FuUekpK5k(e+=MhdAI&uKjGtlq@>op_?mM%&*})a z|IgWh6$$s=R-3P0F};)Rcrk;zxvby-bj8p5&C$dbnnHjnk?7i6!IH zfi4&RGYX{@thxC@qz!rf^Ue80UYayVy3Xf?P8P;oB|mB5xL=AR_`kNXV`KcIuG}to z956+Qxfc-g(qC$KAuGaTZ~ymhz85Q_9nHvAWz?cU6F|1eGM~3?@?rhr^WiZc# zr9U=w?5%FiKPMfLOVwjWN!69{8vZ&=N>*>?wWSk;Q zw+75}Vko&5G$K(8Z>y)HyBos3Is{oAFc@s_QS;&G?ST~~-O$L0x3&QHfN-&`ML9+Jo`}qr9kXg)ud1Z%)k9`2uC(lU*^Hl&rG+o+O zK~^272V#Xuib>suj-jKkF!iYArnjGKq`EXhIV~I78va^4;4t&p4`d}?2?j8tlC0pu ze`joPjaizoZ%83`p7;RM9Eg0=v}oGP{06xDX_q8K*+7cERP~@jYI(H&`*WU&B|6(| z&*f+TvCi3%;czfePH zBRzca(XK8tFUGUN<_PZ<+1RB_r@Tk*mw~&g>#74#rw)uX&9N>}Zz7rqN<4#UjJ-RC zz*C?AWk@Jsc&&soRTLT=vAdrT*mufp+Sk%JJ`@SY(pb!zLUORt+uvm ztKMAq|2qG3e&;(R@AYFc~_XTv-kj!u_=!k+(BsJ2baF&rKRAQI3^I#BmTm*tGZAK@V&X^yGRibw z_*~SH0yYU-4LHgYgM0WwvA4WeGxUiIkkjPQF>WMTOZTC#{S_c6R@dRXWkhlm<@PY$ zW1M^WE>{d7&}bLyNKvWsLFUYMRmkDdp2D<|BU~DL34R0b*`8)*1uvpgI3!Siw)R1#ml3ojaO9Me<>wt^n@s>Hn}cs#$d$|-=t);i7h@Pv_+c;ET7X%a(y5`Ci}$CPYby6M_ZhrF$qHXo|2TK#7rzUo$;+`#I3 z;aZt~_47#Mq+x3phbhm4ySJXI@d)t?E9fBdQ>RH6DFufBs-j0iy8tm09Q`=J7y>fq z0+@|GJX7_`Lb@lPG3ab z0McqBvdwUxOD5E{_}0x(yjP&eHo+~nq52R{OXkeOXRyQ?hB#UZvGyud7Oh^P<8pD{CB6Or%?&c-o-!Dk; zepg?q<5KH?#WY$L%VMcmNRd*aBk_!OtEtNfAq%i4*V}5?QarK}$p}qBj7=i~8p0ER z#R)%kgG4=8bl!Bw&V$y*+1#ObZ-Ers!W%bf$1#woby8|3< z%k1hO*S|hVwmi7e)N9Sj^WP6p+ovLLc7N+*e(yiNR`JH6H$STynSS4MS4H~P-p(v#ss z9DXFEYDuCRDHiUGI5K#93TT|F>{emOt|aTNFYpH$0l4nDj*vo(t?SZaZWsW;SI)>AWiE8`kKGVuv-Jw)3y zm;)j0@v&w?LJeLSb&|MQ(X7f93^bc--#8$}-gOays@jS4vI9+>axY%=Z5 zz5`kOecABas2gz@N%++?`5Z|Ju0TYz`vm|~1qs!(qD#VH=m76=2F>I6VnuWVw9kzs zBnzth9@KP~5F$bwFuFZH;ap6*7~n*@ctY$IW)t?u_!iYsEy~>#VcB;4 zSwRVr`M{>Tm{`+Sz)K(#j6ZM_V!D(UMX!ZE8_q=Ue|?5W6K_fOPb{OL+-xNS!Sxua z2bppLI>2!B!=0&E;<%^hGB0qMXK5$@z;oZ`cmP#pe0+UMkKqN&)^>|~My|VLEabHB zUpM!=woq4i@O7kP{=nR(;;&;RjSN*oP-G=&PH`c|* z0qrR_at@om>2g|4+cJ=!YwX{~pSK)a?dqNLsiS`eBkJ8l#5eD~qs4pMQ16+F?4mN9 zR7;HVg1~0Gx`J1QM|e=d0z9_n{P**lA5V*ZhSzk1j3uYu>#{FJdY~hlyW)$A`#m5A z^gXDoAxEwYbvqE)NjUG{mKS>SzU`aqd0U_R6&|Eo-xHf3?J;|^87Yc?=R6jz_!#70 zvorWMf9qjKkM(H-$F3Syn$A}S9mu_rc+RjJ?xXCvFqR?yk~INMmU*~{4h%i!?S<*>Kl3OlQUzY#arEFDJ* zjDiksTcCmCviq}v3}$8Fh9XBGQa2;W)-6`3FX;GVD|LLG<`TEAT!(XSzD2CQq*t2& z7U9GoaYR*+wAi@vVar>Bjk-UF%Ojvz*>-YJwuQZCsG^P$N-%{t7LA%L_{2%%g|< zlU9?G!xNcH3L-Umk4N~-*I)&_rLYedyI(FNQVB0TBFUk}KG?mFN~Q8pu|UML8m!)~ z@Yz-@jZ!!i)qlT}$PBdQC1VvlhE<^0j^IyyuqEF+Xtpej8J8&VofVx zolX;s_n$LMFR5^z#RV=t-QZyldjf zS#hIlk0?VPZ#n3?xkM4zPME`&oW#7co8pQAoL~IK)33I`PAAE#>Ph9#J#wF zwdCr)gBAvV<@pd--Oc^Xa(%+1p(%Uo`12oJ9u3eAY&ReHa76SuK4!P( zjHZ#uXjgi75kKv#2={@%uRU>#75Kl7E^SV$zI86>%^&!tu+`P3@(4m}=9b3i*{V*p zLC5-mRVQwK&a?gc!2dC^TH|sm_~q)gdA9d9*MOe+H)&_pd}`ih(pf4Jc~>%oHGNGL z75cM+N7YI}HsH@=mJ?pVLSLa>UPe69ZLz(yWM?b9a_!<#<=C z5OaYcgq{Uv0sMEk=D^;8Y&KT1aoI9#_?N-U=D;|u(`&yv6$g7ct(3#*pUsxbVhi(_`{ak=jH`;R1|zM{im3JuIKv zZXq}TRTa>4Ex;JD#VPyL<5sC}zPVh zDUN|DgB>fE>-^YR4B)QV*|;=v2*abD+q+O9(W`F)DWxR)*vLGFJ};v*R1`!8xNFcG z0`nc412akga_8dWe5Rb;M*ur>X|`SAk=}=XUncsm{q*Y7lGa9vb1)6W%a3)cEC4j! zfqxEdEK$&vF}Nx$HWhPNsu@`FCJ@}71n+MJ1=h>BnxgQgq6rUrQ(1q2N;RX=FgP$5 zWNJC?lNCJm`1ts+CbqW;S`be&s>^0&2V`Q%)PF6Tzo?lXZc#6afhla#7m}z2HL3_Q zJI+?PsBWGBUpzPS1amexpjjl7DLN9-Gp*!aJG;8t;Fgoq3m3Jx<<4BUfD7&r@+mHQ zFJ9_NA@vhG zs!a*rRBx(v{B-E^yoA)_&uz{;d>8KsqWYSP(dVDP4cmLyi5y&85E5wP6w`qM-X9Y8 zm=8a+sJ$v9t;uI&zdxzB8B%yRGFjMGmx4g9(~+;Z=qbH4EX z=-!(@^k#$A7DL{q7ddy@r$uJ>zcpm6`hL}2^sZbxp{a~~>1L{3Lyff9R#iauYgcUM z?OOLSNQNgafzGtUCRJwDW`0(4zQuz{7IoS2GqZyJ1!100c#t{$_I`nDUe(8MRz0_I z@d^^@{~w-|8$an-W4^C5{>krK+Wr|X+T`N0)q2furpMc@_dW8OI&!UOKxNQ!bN^Q& zmV3rg@7CRpr(ENA@YZ~1^xCS7bU&oT-7SA4EWZ*a_FVss$(L^=j7Dw}M-Pr5lncNK z7RkvOG41br5f#F|T=ZHT)7ArjYj0_QPnM3nj9WD5Z%|y=@MOW=g0mDKSyGbdv8YR% zB}IG@t_QhnnXQdQctzm@t^xz*yexpNfxgaKX2j0*XZFvL(VM6X&|3=nwVZyRS#7@( zVoWxC33NCX*f8tOY3aH+yuIP5lEFw52ZfzGJ}<#oso$4e;IGu__(BYu=t1K_2Sodg z1OIR>bWb3W2;^nG1exu8ddOf(7`xDz;n8#O3C0L~a! ze4Q|P1@9bnbL|QZ$7LiDiP;E5Y|t+YDhxBGgpkoxlJ=RVPIDQu%)%HiUaR3(s%r0foZVQt`$US9*1o4&v@E?Oevk5CDcLSNo9ZC6zy$d7vp5rM};4zxr z*MY~v!;I?5)FA1gMjG$zC z-~6!e#n&aKDSKZ2XKnpsVk;*tF=6lTHZ`pyCw;4rG!)(Z=brYdF?GgR0gMpw8j}e%|+hFyZ1Cc5VOM5ySj6j$(!Kk9o=^IYeOa*xc z%egwPzJOS*9vheNG@!&Zk2R~&Cey^#R7dm`;il86$xt)Ji)EhU%^5Jg@NtF4MA)QR zLD?L(@Qr+ov$%buOGk*^m=GzIMIrnLD@dp`ASSrDva&edsL@sE;hNyQp#BM?p3Xo>8yrFSq4)VZqY%If1i^!i}pTZCDqJG!{!B5mFh2a5i+)3^z`gEFw zD=h*(d*jvj9q!33D`GW7is}e*WN|D%uk)$zYI(a@ z%IM2ww!&cmIh%L}rdMYT4!6Jgv@7tK!H?Jecj#|x9o8THOa10YV}%C>HzcNSR*xJQ zCCvt2a=Z4^x`yknQ%`Mgjds@6f5?ckO?09%<2Ot-nLPrEs(1*ECBnfR-;CiX)vLloRnOu#ZV#?$UA4>P>29l^9*JTL0-cR^ zthX#&`yuq2dSm6Ooc~6y{optlZ<*3&WW1+!t+zv|vvn&qaqHE|)qO_Fi^SGV86~_x}KCp4$AxHf?-FX2)|avCZE$_?U8QD3O;0kk%FE z`x*5W^%Xs!=n~>3jld4^=W&_T`$Nfen(h@hl0G-5KraeIe_;lLMJt2G_vgZJ_$cm#g`{S|!=*$F!5UCQm z3O420EER*Ji^UIsvL;ZoVEV=VB`#)JiVqrXfKG`MZ5h>ddWlgL2_w}kJTJ64h~o;M z-ZF+t(KolWtUT6;xD}Qp*?hP#7EmjN3T9w8%W<6o(jA5FI1(j`4KSe! zgK4vIgd}b?vgU+I=vBp@0zpN?sOr!dKK^QGis+G88DRP>Sqz%$;xoLC@JGQb;IOh- znaWvuY$UlG9RzON1};;M;#jy{K+p8<#?>{0a&h{7NCZKhAWROXMh}^!{Ez9IxO%Vl zsgpOl_q3VYS0DJ%-RMK=YRY|wv)6Kb*77YAijC60C0ut6i2nLeIuE&n4T%~=Rq{S^ z*B>o4I=?q1Ja@@@AHiX7YR{2}J`6h4h&^poU=?7M`uPRD?2!dL{ z0#{Og`(U^qnwN|66PYJ$$4WPJK6K}0)D@MPkG35y=tw3O-TJ#93St9Hr`PNVKK1SG zm;dW9ptY#Fw5^_FGQE49!$*@HsjDBGr~KuHX#dOaH+bFu-Xdy^=<^pTIs2U8u*o~b zW3GR)ZGaYNwjqAnd^>YyTju)G&6xd)SJT!!HVSvF3~uf7c=>LABv$!;_RvFsxnVb-LT z3z`!edFP*HN=jt`Y2$^xr4(2i7#^4Pd1^?Lx~6d~R&dwAIn*d97t9Ni@wx+0VzD_H zQzVkpP3n~<$V13aYjiCz;OKMKO_Ck3t>w&@GwWBQ~$I>qL#xA!@?T)bNl z?4I0&v$6p|EKNdfu@rr{G=KMuLZ{rS`>aCntjM9-`VeIIN(Xn_xq`$V=YocDpN*dD6MCgWzbL ztnnsB%xn|IbE$cU?iO^MHZo0%KYpfxvq(<#a64waM_L7fBAqpkzQffjJ2|7v9H z{W3A!!IS5>y>t9p5jFa}DkLQ6_=hiY{_A$&sqs{gZBfy1OwjZeG9bc_QqO;5_S2xD zkwZlO8L#6x#vi_M-xwdNJa^4$^VR1!KRkPzUTqx{m}n$Ab^SL3CzHKBo9=YA?S5kx zx;1{e548!}^)91kHZsa*r6$F$pXZ$(nK_f@Qe~d9lX*k?KYr_Df9r|H z$D!1MaIe_x-UBC+_s#TbVqhW$;;7vxHxAwV#d9?&agXtoN9Q)1&iLZ=h-+88Z6eLA z?(>*ooA0#Wd$;|%tIesmtAE>4_1HW2_+{g#(|H$6o{H8gZ7V0NTc7P0eg2B;e06%w zZtJJ_kJUT6oV)td^*+nD%4o8c)GqHUP&7~Hh6dj)rM&M_q0y>|DT-Dl%ZsPsWyNRq{SL?vY5^R+O-o z`eoPxWz8xxD^M2Ez}H;j)fJQa(e2Zx!@&;!m>lj7HgW~{_*Uv=tr&b}qjCs6h0 za=s7eI##X2V5Jn(?3lrNCkx=24drXTYu%RvlpNojgWE>vOl4`gQe53gckc zo`r)5KQ;=+_<$961K|~tmaf@J;TDbbJsA4oSo*JzYRe1KKC2Fqxj(Wxe%Zs*)Uw4m zG{a-NsG~E{t6gr@7e+ebv3*c^@>2b`zK?{#?Kappzxh2pe(SB2a?_(j+i!k&zAO0D zB=V>StGAy1=pVG7aGGG2GG@eEZ@)J2TI+_R|FC!}dUht?Ek7fbRFsU7ms^AKnL{-l zgdGJ=ZC`pF|4`Ev6nc3*u_$j_6y@yZ)@R!TTYZi5D^=*@%dd*|HsAX@mC(xcdeLQW zx%te|4M%TYB_AZMf8vR6!{s&s;=$GCt95_yG=DT;1SYThoF|wln{k5z ziq_3}XyK@H!jT9=JRm|X2u4KXC4YpUCG{*LHA!Fqg(5X37HX2_8^w)c@qFXhVsY$| zvW;jxXyPGG9n;Gj$HOb0AD4{wb24>42>i$+I1o$Y;-vBjtcQIR{W@|SLxsvA@Tvd~ z!uAPn#B9_}!N3n2BW_AqWM^snIk5D{L5RxIW$8O{mzNO_kmC=${@IEcU@c~4ki7yp zXmgilA(N67(NBU!mZ8n&(7T(OhRGg#NVBooedQHn&5PxHR@l{^s3Q&d=_M}U0jt%! zz=XA$T9{N(@!Y_-Ve7k%7~Pn!r37TB5k6%^e=cYki3lK!7n2Ic#*2v&Va0AGPOpke zoO%SP6CKHXm$;K7rSLo{?+^3PoJaJ%+;F$V_w>*kuJt(oKc= zX(xv=&(gHvkr3%-j8vD(z_UpkFK;Mn;)O4XZB$){M?31oDNs)~^m5aGR*D{rYdV_K zvw1_t^Ip@Ub;k?RUuV7WKsRnZ$Nb6OcB@bpfnib8Q)XTo6I5Ob=+E&2Yq#rL{0c%s zi%RW=ZF=g@nb=UHtIlo}?d`p}UwQw-r=3;qUhBRbwjSv-Ie+>(^J22|H32hZ&Bq+a zm$_SWL;IbrQ(d;std-8AQ zW}!pXmpx_9o?imU4IXYfPD5hBMwqVMjmfh<^jKPZ%TtM$wJ)AKN30 z{*{`(P-^b|@}S+;zc>ZEZ^4@0E7j>mFUjQouD1K9t~TXV6JMUR|EewYTZhVTZdtAm z4QRdD`n>t`kCa2FZWO-!-}H6&!>;8xy|{SmCxU##*C8PsXREdcHB0Z*8L-LzF_>c0 zfy@rbKN&SDso!D5(U(;m2AQ6P!B7sKDjXCD_^`EfWaHw%Z&SYKq@5WilePU=m?J5y zr@H#d;FZuT^Ko)523tMTvJcb9GGZy@EFJD{K&~$%kguR;;lafH2xZb)aXJPYOZ7QO zrI=DP71$b_MAwAE*A zT@DATiMLXl3J|}n>6m${=vf%SwRF=k-GNJvB2%BAr3qbgQ)k31laC zI@1&EdqX!9OS(V^z`{10zT`SR(R98+BSsPtOSuN2pk+ZIbOwMX(u3B7UKV~@9XLU< zO;~{BrwPfbF6Jzpk{S(NCS^hUgm6i1gy`zwslq=d;yb{5mXU@kAQS%PKg!WSI(VzUG!Av?%Ig zqxM7Fr(z=Fp$sqy3JgjkT^_y0%_Qzvl9A!+WrKxGBA zqPBz`^`*5tBmo+?{C3~_FuquwSR@!+2@avX&qQx-{m=BU#%5S1OQOu*eHC0=OsW5H zB|MnuOFeeJ`XZ_RL-pIY8J0RM`*xB3#7!&+VFn_UbpF$xqWV9T+TA`dNIK?gm;BM_ zZwLEC^gFGur@UJ&ET7bvzIke%(ra*nng4KS)u%QCb4JDm4qJT(YYqS^@b$}M>5r!l z$vl|ENXm4=C{y8_$_F24RAvl88@=#_OmQmAs&0|a*wF(8IaAZ=R(Ryb7aDK$TAHT# z9bgZhpqc!0?HbQO-!0TB=5<#$nk|v*d!>f@*O1T~Uq0MX;ClO@>!y z9KFkV{Yd-XyQ#P=*_N6=5u19=R3FTQ+?y|qZT)XF*;-0ELA@Ae^sl{s)V&*`!T?p> zFf*`YtKph&WIZKr*EbLg%Fs^46yavV=@^7UP3{LJm;x$C5T5oSljZ%1q{ zq|Uy0Yi8L&*>`XWNcj7?58sMYMN03&ww{z3pACfcJ=8b9yX9{~zS{z&sIrv*iCV)B z{B+CV(m0d%t@M}wSp9gz)PaNjtv`5|TAV6hKb-P8Sco|&DzdCI{Khn;mvn>@Wy+P(EizKdohhQr)@}b9as-?dG3VR*T)$=)3cDB{Fj4xX z)!hbK7TbGpxVP0KV-QS*u%#n5;zwZkcB0uzM(?_N8GXw1~;Ts<2cAOWa)IXSb@z!Jy0=lv}>{9 zkA)YVgKveWna$CcfarYUmizut6R_W!&Bhnlg0X1wfL&Nxjq_WupTgj*24k7GbJJZ= zts3EsPMT;Dh}0-RLgMHGj&;bf5_LvPLwTd#j0nh*M#pVdE-Es6Jdm2%q^( zkW_?&RB9*sF@(kj*@t$57U1MqRM=Yv9rlc++Gj&&^7eutZvL56?w#rKLYaHQX#3QE zl8QFYjvh2{P<21F0fJ4);1 zvCAFRVMBl4h~{|%;+o%9Z8SZ))**NGpZ6RHZtGsNJKk`?C~r&EF{6KeMoszhYFps} zZ{pQIw?5m9QR(z*L&1hA%cH-q{_eiT-Y>5G#d_P&y$+HmZ4YgC&-K2O7S>g(ZCXDW zyfj#l&i42u(mHYz&(xCoa^Ko+;Oj@Hml6kJb3;EpP+XWwaSN)y;_&de%;acR-oON_ zHS?lRL052m`r~;2VPW?5AJ&C>y`Tr5UP)jb8VOOpr9^xBpfM09DFj+vRul}ZS+g=9 z%(IT@yH|V4UKRVjDrTb!kC&vur};)+!b;MLrh1OkBU(w;Eb@Y1NyIT)_bZnn%fbbP z!&amBJ2g&DG)n{sM+ne@B`I$lgC2{5E#!aTYMNBT1KO;cT*Mz5beJz9uW)6N2folG z0vAvppfM!rtAociNh^o}RcO?UYK^1GgdiB_Ce&#XhFVfx>+7}^ zM~`$Q3&In|LSslK)n0M|bH*e_a7Uf$~e(LdWJqbHWW?4N=-0*1M z?S&B=i|(`?54(AvK){2urE}_dd%S!o4-7R}FY?X3780J5X4`%1ZKWW)~tIVNXL@ix+ z-zomM<&#f4AEvv%jNg8@)Ve!&W?(wH= z0iT!OuBl%A1Eq9xQK^Uj@I&Ja%U*7*jD%f527XKC0eTg}EdX2uvWRt~5dqkOvm#LI zBPT27C>Rr=p=y1|$XTnhmTn6w%KC2W7S)%~(m6A)URh{?zp@Dro~E&7K{FwdfT)0d zZ4(GW6R@=-ytTj=uL$5&*FU_%75JT-S9FWIm2BN)^9ro$%}J=8g?Ut`ZqUGPB|vCD zp{|%5y4bAKWG_~%Q2SZS(tsF!GEo2=-h z$&Ai{gj=9+Z{hC~W1E_0U74M8al^Y)2Llfo@(pj9?QQc20QybBN5wXNuG|rH(7x|` z_nFw9I4q{rUvMk=LyZ8v#ogbh6QTYFoAy}nPq)0?J@tFISI6lqKGwa3k&eS@XBf_@ z$5jzdqzAS2A3i>QH)Q4Y&Ud2MpOrdo zu75pUk1=It#q}p{x31cH)0pc}_1h5-FBEpqIj1(0DQ6$q z1pTY(FV?4%?H>6!gS5Ol+-MZ7gI?*WHbJ*})BwsTDX(CGEupR@fe zCCn}}i+bg1r9t0J2n#xlOmI18>1h-))F44}l4bQXFc!dvl?}WgtXlfycL4sPTS8)~ z8BSDuv2uA)0$CR`H2T>m8+frCM2-TKT5aqy!r)mE0RY9$f=R%U&M9D?L3&cBAnW9z zy2(2_7)29{_|UmHEZY0POY0o1-pa`|1=`rgzz_lLvCEp4KTuub3(mAD|;@ewF`*H-81XGA!lybmf#f zHV6~_S7qxKZg;p1GuG`_hIt^Do@rX&7wLE-@AX-K6SK0E4wiB^Q)?}AA1s>WWoYc1 zc#`^`BU>}nqfINw zxWw&Bo8-lIjPow>=yH%D2xGizdY`0}LCApRV1DG}!`^e^b+B1!n1aU1JMZH`%D3b_Jc3DN&#p?X#^fE~7tJ{Z$R}kB7Xj&0cQE@yU5Cc7!kXVl5^#*P&ibu*D7jOlx zLk9_lUNH__HR`yV(AS-ukVS*(I*dOO5$!K*;w~a7D&W?MF-Bz6RorQh0tY*X8tuFO ziSy}8R+W2PPF?+sCsNw7=7nPs|(bboVA~X+QeXVwetSd>QkY(Z8(Tz5}PuH(@0_azp1}!6(66 zN84G{pN0~1!}e;tLFJ=0l#xUq>??yh1|Hd9qUcM0lo|c-sp#(;dJ6iYhyHFpJXp^~r=WTs<*21gaelT&B zz`pYDW>1W6)cZ{xOrg>uIz{d8Qv;^S3>(&TzI~@KnH;Sq4sINXVVXZTV$-(0IC-GS zECns_j$2>5oQblHaEhKR?kDMp8EM19%eQHuX7-dhW!s@{+Sz_|+P0Y~avJ=UnB!~M zxTgBkLYi=UZfer4Z$~4oR8v2N*g#6R{d^eu{)UGc7Ojf7R?PlK3p`rxh2$k}nO6fHC z@%6son$E!z{?Qg^(wknBnVL4CuXADB{pX@n`46Y>A3PNQ*Vfn5`KdlONAQp=j>^bb z4tDjYHHpvbq{kF zv-!&4V?dAJe134Z)1dV#qSaPq{l1{Q$0E-cwbxG2?6$Xmd+JALch{WnGLQLNZyT=5 z+m;%Z_BBmhH_nA2#=j0!E#-qn;4;H_3An5y&f-oJ!v(LVQ^CMJ;^77W;WV=UtbUJ# zVX+M(mcxnBE-f|3aX27uqHVKzalD~Lf)_llUFysHNo+$Fk#s6wTv*h=7NrAL4dJ4e zg+{b!qMSo8xdNQ)ipJoj3tdi!xZ58P8>`cD5H3c1rXSM)oua_pO6P!w8OPG;h!<9% z@8UE3o|vo0R>l_L{U1@nl^aKgDjb`oSYD1JAGy<0x@)XKh(mH7OpWq#puqrHBP0N| z4Gs?&8Q7(bP3U1)0$~#Vk}R3vV%AhQz$nTUjKFIJ_&9|tD2^to1^&TluFU1Wnfb-V z`d`tfeLB}<>guh5ok&sqkYD-5!5g3DpW(RIq|LM46Ev}}MDtkprZ)SZ?{_8%nF{n! z3Nt>=SALiC+Bkjv8#K~*TfUU64&QZfBkTrs|BsL?7UGYp0Z2%j|M@K2kH~q?trdHZhqQWKK8x zcCbC~G$K||P5RdbLFRA%=45=lddH!`OItc04JGWW?nE7Cl5ug^zB5CA?KoQ? zr=bKy#M^qyYK^DNeBZ|WKUCy7qwUnFV(z8a|w@S4JQs; z)JWxWsMv`;uE8(%?#)ExA#}#D6`9bOEVKc;Esm~&8?qCBR7jV z1|+3c&&7N&E@lMSFtq`h%PO4#S=Wy-GgYESDrtsT*jXbpsDc)Z&U%b5NZgO-yOp8? z>8_a7+axmVoYe1bZ_`<^X-Q(qz*oE1Kgn zXJcPYiWe)ICkNIoO7N?!frf$fYI3q+7@5}J6+x(84tAwW6&p(}gX7J>#uVsFohC$$ zF`;98@8zrm=!QLeRcBC9Vq3q)_I+B?z@-VUxLO5m%)pKIK5UHZ$qFr6$dxk-gu6)j zj~eO?Jwb}C{~Lo?9kCt;2#sKuJapo!wR>Vo^w~W31ayf8?yKeQBE1jFu>LA&X6wff z2LIk=*>fYo%l!ThppytYt&V^2lsb_}CB!NZ-9X2 zj>^1~;d$T0q$U=>*go*>t>=DsF?H^Hn2_pb(@FSK-cUNr!+$Urr!~W!X~1%JD0LX^ z9Q>Y{3{EC3e*6flfV2C+8e`>!J9=|&^eD4|H&B+7leub+y!np(Q%afsPd*VXY}hM%oEXc)`( zK6o!4O2VeQm*)T^5ISG;Fr#ed#reo*IX<#z_2r5RF~IWkEpX!iS_GvRpdA>xkTc|A z_K5?Nt-pUZjvfmee701@g2pF@`CyKK?i{f|GNw+x05URs3%Uw;Vo`b5IM%cXjRT?m zl?KReNGhO7Bo-(!{79T-whAfh7<7?k>z4lDmSASq`)`pOt&5K+OJ}et678ccw63yol?gh;Sofv6|G0i2QmTLJeEt1JGVbbIR97IAjOc|19 zVUqx(zF;MZ*MJt%M)ZLalEcM~Eftfz<}pE2MLEAt<5nU5L);*im!yI0+fd5xIz()l zuL*t8l~>?C1vC#RflT` zg9&y&8QaWmKIP(FZia-L@kmn>tnus$<5Br(+q;XS)wRu^NFDM|D+x!6!H%NBSWISa zYyECpZ^6rdSX%yQGPV2VF4F*gm2AHPui)-74kryJgyu=aqF~=@X%;r>F-jUxNh4*^ zpD{ckCm@PX6VuV;g;dgbgLsjT5ujNO*$M+`<76}X4b_rHZY<7XnEPa&2piNjVI>AW zf@Gq(852Qs%NTwen~_}+J3%Fm%~o;x$slC*gPxSt-w(SZ2!|Y`XR@$L26~gD%S7aU zj+42p^HVVuusI-{&sNBg;>Xs5EsL82hzX`QG76U9lbB_L;Uu4BCTB#gL2(#Vvq^vr z(Pz`F7AB;)ctk`j<79z@yL@2*W?zkdI#{`+Q`bs#+zY%55&>{GK=7#R)Pf7Rx-{~r z?_=rA0ZsUIyPO`gORbd0_buXoG2XqWE& zve~hxFsM7Yo=Xifa*Ue##cAqyU#DP~&~EYg?e6h{?vEcn4@J3VT3s%u?L6vdG#U0v zg%4pRxX<=0xUE!ai5fIVbMF@0ap_?^&wwr|Sy}(g9jg!P?SY#0miem$HV@?%<9A1C z$vY`wMxxJCEbbk@@$244R)L=y(d@RSF(suQv4_i8sw0KL@jq>Qm=Lw)LF&v{sqI#a7v!IQv*QF4y1W6AXEEr2b7j+Ras1VOeE>8PRHtM3yZFuCj+QC=cAAH`)K2qLFZ8@q$(ID0P@sg z){fO8MGIAy4j)3DCWDOhy&_;%(F@OaG_izOTdGtjwMv*OfFxiR6jKy##XbD$P#zV* z!|DjAYf=_dE@cpacN*gYa}Ucbj>|E9x4J-pwljW}yS>Z%)fe+xSWGqwWrC}oWs+_U zIeh#k<(KqtdTyNG(N=Hodf=t$y@RMKus3+0>%iM~%lp->r=g6z%F^r16Q8`ivgJU+ z?E_uLZ{PfEViS1CYi&KxbZTE~R{PN-w`nJbiqn%w(L>KKz9Uqm)e6c8YJB)yf*(Sn z33m_F?A(AXm%=Bun-`m@r;G3f>DBemMLE>z^bFfuQ8N$IlN@qWmLfNbE_a9@)wEmw zo-oj3e)M$i%-NCN;sDm1`=&#K-y6drS!BG!`vvPZwHSA8%_7$A8A9I%v9(6cfNMGn z{u6c_Jx;~Z(c=*0U;sJF6oc^7Tk5CJ+!uDio~cJ=!g!^HNgto29Bo!?KYW5(9eb8d zr^`s0XYtw;>p_KOl$pADjx2!VfjfH>cV%Tv*9=Tw?Fc)LiFaeR&Ii*n9M~!VM__w_ z11}Z=sSKxD_8d#cVQ};X@{mqt_z!`OiPASEUWKzN6;`o+{fq!Suj~MPGbzBHQW@-u z+PPRf8!U`=%Tg`fN51qvPI)%N@)s{ZkV)czdt&PiIRR`vD|03MqCAGSOnWcSmeR>! zI_C=#gt!6Kf=sx`4WY|vC zhaBIB#R+zVoy3HP-=0Ig&fDW^an;A?;(fzW$E#1&<(;Op%D)r6YOj+)LI)+?-l^S{CCnm-xuP?nZH(o`wKQkQj;;)9g4DROqmw52MzIQsS zHkhX7ZGTnfm|L9hWxhY>JA0P~!x@Vy^ZK~!O0&0b>;1ceT@TzvSlPX@&7k<&*>(GO z28m3&>DNxUU7cPVYOu;A*r1rJjV?y_3<+bEtf3C)P9$ z{hUP)76^TrTH6s?)5i;LW#&h(Upu=tboZgk`0LaYwp*`QyiNbp&9I~I$To*dDTLNl zR@(-x*X>~B{9stLX3LI)zu8q5-tMd{Ej9URI9iyYA&Lric?|aXyv|z zqg<+JYQ}o3)F)<1r$UTpHakw2-S@sj4NysQ-5;kY)>DzXm@FC2=vb6)q6Cp6kEElL zcA2d};946;&qSnaSq09pUmR;`E@qicXetg!Z4r-X$v}Db0h%ccA&jlmj&_f*XbSx2 z-|K6Q6|z{`J5(?K(RNPAutF}@2M1$$vraGuNedHGbx#Q~vTurb%E~h1+GkM-(HR{3 zQioeZ=RZ7tFgz4mw?_2+nu~@r=0`WA)}OQKz2$ZMPqzb;SG`)(XOy}7faj^2wZ@Ml zY?wBubJunktqrZVud+5fI=0%>!g8tpZ^4g{zd6DU7ge7<|2k7Vj0%WiSsZD`ojRKa zrA$3So%xF40|y5&v3w+Q=Cg(|bqGgwSW;?vXz=;eFS|TEEvi#yc3Da{=WYEV*v#wv zW72iXCj)J(+SgB~F9^6ov1PF})smL`#=;@h-6AC*&|__=ZFIc6KO1ODu7Fpgl1l?P zSlsC5FmPr2W$XF_bO_naXqXC*IFF2RaFfPMF$dxh%3OxS^p8mdemoT(A$nXt+X*|O z06*}~(Z9WBjmZ8L>A`hJu&bKrQTDXfsbqiD^5Efb%RvPD5CMQ6@YQ$|q(*oze ze!&j<3vo0 z!To&-1~S_2L*HKDYU(EV6OxAV`6l(`SVP6wfC~n(1u%BF!XyZ&Xy;Dk+r1;O9$A=vgNrzEzn?S&ql$SS-^u3o7!s{QubM5BsO8p}IojdmP$-W6T($vG; z1aB0A9n$!QF9mf0Yg&|isf4IOCFanRRG}{e_)K-&rg9O_mrTg;A!RCFasBJcA=;<` z0w!rasI#w1HkG(WpG9S_(Q(_o#io|U>$i^B4O_Y1uYSNbHh#A1CnRrArClaP-+f|w#U;TU|;;Y`PU4vI0+_m<&fxr2aom-#3(^#Jw+Ei?kdR$?BB;Vah zy6PJ)n3(JNz zlurf?Q~X~1xQe*$`26sEj7*ttCHncu&}TzmWJHS=d7#4Lih*L^2Beb#OKQG^uUzIq zMFbxc!N~E_Ssh!akORf5n4{x!wiHE?*$}MdOc07$ND1|4Eu(iKdl{9Hi05a;GNi27 ztn4{DU6BPb(T}m59jAaJFBVWQ?7|h`C+c*vxlENxJ{ODT$o^Omw^R%k!g;{;gPkjr z$3mAxG(YX!*tq(_d=s*vNnoJEUdKzW!`Z}d0d{v0%q7_Cg}RmW>Y2tS&~qBVju1AD zEl31S<(dm(1f#I}fnur2#)USid_mo?0s~{C4v_K%(d%G&b_{MRp6bL!Vci6tR8XoZ z7w5>bXrj2GWm2PFS)oV7MD%jAHQ~*V5IE%4U{12T%T44X@GnS$T8nIdBvT%@3=0C7 zV14gu)wQLzCCG?UF21MhDT_LKl!r#dJ4rg4iZF zG4XbUk3H8nAtvJJFFP$m!iw!m)<1ga`2Vr@=6_A!>GwDLCi^NV0zpAhaSM&LLH0!@ zWsR{;5O)DP6i71x1zA*N5il}{hE4=!OXvjc*ck$rO;D85h$cg)pYUM^8)hO)Ghxqt z();@-+~42sKG0$WTIHR0uGhKFd7k~QkG?m5UD#XZw!_UHvs7qFS)}#-Yfdc{9`WA> z*fi(a_uc@#A!zFnwrz9G16hb2Z*!3T#&k03ZDfbtIF82(&PAxK0^h#&*yp)}oRM*H zQe=L#_~>n^5OAypfRE^(aN``xH&uAVoIUJX?m;qfuSg*_bT|B(qI8g_Q5(6WlxD8> zRVMe1C}=!tysF2fDz#wrn~0f+gnY!(|66k2k+6TTSmdHM z{*@^~8w}Qnm$oI~K1osGhr*i@joHyVoFT^lsf0SH$jp?4$Egs4!YE6XgdF2gLYMjK zCAmyi$4Ls0rh}6g3(gQ-!R!|gX1x*CmwCFgeMH{jRH_uT%^G509*4|6m%j&-!+sQksP zu{-Z<2?za?*{0Ps=E*m6mJL4C_MCr3afA1Je1d*5{@dAkJ2&)N2xOPbQqv(tHI|!B z@%Wdq+F}x}O|z}JyNMUWIM`_c5U#eV@lm?Aui+kun^q%@8{Q(O66I>{T>bp(0vqd` zM8l2RKYDg@H0L)us|@dC+)UfHEo9Z=$z$zZ+JB7|#eyIdD!5z*A{|VxG%SBrLsapQ z0%Ym6K^jmw*h@~ga$q28tPr7uT%r_#j+m+PVX692phHt+DyU*5f!-pNLkXScUPGIn zk`;?{1w2AoQ~(|Dx~z_lcqwTl8qUSB%wbmSP+V44*4i)&z&w>2?6d|9SQZTN$Sna1 zhSqc3F{vyc7Q%24UYIx$GuOgtG^GR*w30?doAc$?=|&8Dvae)F9WOUx&y8ZfnVD9B z5G>|LSGW^LR@Sef&96-s#29qtCN>*vq2K_VsUYFXjw25vI6X+BqMsCDI>@`QRp7hs zMWrabGTy^H1kt1tCjx#8+m%h~1ynPX*D_Z2ihI!cJl-id`+`bk+#Tl$zO$eScr z4j!*;oZ_U{`2AgWD4k_p%8j`{^m1!PIDzIwT;M(EwZ4DZ{1hrLW) z&8ZcyI-?GUeH~?2)nuI9qvfFE&>|DJ;>!{%uOpSXvW191$8+`0JT7NQ$C^87$ajYzL zifvoi%Nrj{t05PK^n29CD7|s@0I7~3g%bY;h7cD7&Kn@mFHluzz+r^1iIhWN#>L{U z5H|<{2nIQtM8OmdWr`g8QR20r^nvU$JfuW-I4WUCLoqPn;OL0ZCmJ3knNZtQ)HU^S zG7~o^WRNmhk~IcgXtBfI!*dW5p)W-YH}RApy|J=I(1cZXrD};p z9HKDb*hJVqI%|!ZxsVmBB*kUI*ACB}SATzO%zMT#p^?|rA``X=;IHuUNUDeCs#K4{ z{c9P*+9M6pg4&>3kt8#Ghygo3t+(~ogravMMnW7Uc_by(x1kM8?Wl~A4l%K}V5VMz zi<3lh2b;5w%@i*%GW!?&|DCV5^^~=9bY{wts8O}H!6ZHyD+`!^#5Qie7PzmZ%xX)GBHG~Bfvt%9?;(&m-A+)VL2h;@!nt33>WhbC+YueKk8t3!ntabli`!6rY;xda+0}! zjV4X685$>dI_@H7M%k?OpwPAoQ=PXYXze3AzXicu>fZ z5_Nr>&O1*px}WhBd^{&9N6J#qr)~{$6T+88WK#5c?Ia_Znpzx|9JZ~_Pp-5Yz+6-o4wG{YrYp^ zo0lw1c{{(k+s3l<8X@1crC7O*r_18s)rva(f@FYyK~d+@HfRGyXL$N5&zvBP*gOel+GQQ zZfRg_c=`pt&(b^+@VepjDacw@6FQrk zRPx3ZsQ*oHoHrt_i-Mm8F_bhKD;9DNEn{$ajjoJU*MP@iJ%RX+TE)qv4XJy=0krdC zWYI8X4J%M7fDSt234$LDF&%j>`&C{8$zJ5LkkOl6sw}?DhA*?qW=^55wCK% za6w+?OsiJZ?|k*~W6MX`_~a3G>NXg6&;S*uSi>Q9PLic01H2cV3D3^&B|B?$2^}V& zI8=$_j*T-NLL{(FMtS1K%0^?k6#mE86AENVcvj#b@q{Y^HDvfQD4!D)cTU)X7{e%i zfj;Q1Bn|a@q_4m~$0i}x&TVOdZ&MHw%fKFwJxFZ+{x#dNXRIvMy#z1sYN*JSna11x zM5B${`mTR#HFA>Ia=Yv6{ej=PPJt`a{YmMK4`M4qT4??m2e%vX^xGY|oEW7g1!hMJ z!Iqv!28-Q(8rO2d^^cBzV(DsU)mH%zQdq-Gp~$)-WxlyP>hXaP5qW;=i?r|(1cI2U zo`Di45{X1;6u95WzhvkIyFFQA-P|3WKmKTL&iLzjz`Z9V@Zh8C_kF(?%@HL&v%lC@ zm+0v1@c-sPvzvoz{H?&}yH@@w#{Y&ry8OI#z<54}+W!F&JdhN=?7H(_HL)DxB^7tMZDqL)+F$*$ zY^AcPEerc*Ktvv5;osQwaT?}Byo1A4r2sYGa@EJu=E8)>wS5(=*x1#U>Bf&3*Z6|R zRa??#%KDj-xUA@K289+)!#6>YPzR+OV)?*cVOjg~JiWR5>$aE&jF5hkM*>})#A~bov-5RZT5Z4QA>ic_Qn$V4 zUMbsCVOLI`d*APINRYmNszU3KZoI}PI_SP}bw2F7`_n9qQyrnGA~w~i8%z6E>zC1z zD);ljXBLfK@7o-u8UJ5uXZrRZgvMgulpAT5bN4^ogZ_7Xr{Nv8HRtfSMcUm1T7`%Be@cT*RUh%t zNdC=5VvzH(Zi_EccIh1ylX7nx9wY~!NzyvIxu4}Q;xhV7^S{->a`UK>)EoDoZSTm> zq3GRra!#voau|Cz5tFos9By{8IJDdg$3rF~9D833eZ*F7f?T#FTj7D(iBJ9&=rU);`3p6F>Jgh$vcqcBPpgTbp(vLkQ{!h?j2epwspCoTx=C>?aF*1O_oSG^p z^6cDsG78SHeUL}5$ptYFyrKTVt1I!Q;2s9`4MTYoXJR3yU}D}gD3n{rf$&x%Er5I( zZe0!}4rHOI#I%|n!Or_%+!uef?q%>KyI`>7*>wH(T^GN<}eK8lnr-EA`U@N2XvHhmX9sxZf)!KREx)7JNq*zrCDa8~BbO4Kjn&=E!7VDcj3- zXznf$+7S+iQ9L*KJ=*d*_cw>>ye}SG3<#@yf}L0NH##_2fcI$m1$3+3gP>@)t3_&EsWj6SzG}@9u@) zX@)OB|35!i!G zz-zu%HzAhzs3*idM2RC&V(>i@HHdl;qxg@>1s2xt60NrOoU8uk{SwpTANyxi73M>a}N$(Mds;ZR{x8|*rEMHunWgXDR@TT1aHA?2JDLq?Dv zAMXtT(F%NF-A~ow=+bhtOQp>}HZLVtD6KGC=9K6eaCE-3!JecudwbP*KhgBg!A`DY z=bzGkJnPs!N1XdyutWToR<3tnJLZ_i<(#|LBl&D=Kw~!r@b>9!>mKm(|5kg8e#7n~ z`GoB!ZY=J5k>(wcA9u?!#=`v54o*(E&iUhud&8|>%$i5u*FI0ouDcz`Zm_V}ti{*e zth=Y5kW~G>`0WvYy^U_UpE&wY1Z-e?i&>kRYn;!dLc)tWw>(|p9bBJx;zTiho*a_7Rph%>q8^-eJKKP>L`&)E7x$LvnerotCLGtAC> zmeZxtXK-hmb4J0)?Q;8aoo2{~CE2n5wi_4K#LiFri4M-Dv+uToT=Vx_!c?I@Soz&& z!|roU@5oWyVU{|5BOPKPH)eJo7rAh{`LDmX^o(7tNWrtAo;%KX(|nc8OL;IU0;FRQ zVxEGef?@;oCN?O8>dul(DiB0u09Ts(hm^D^*_C#L6WUf(;3%!E3)hjVK`{iDlK>;N zv_q*8w6wG!4K7nHw*qmDod|+rpt7r4TU#pSD=Te6K|2C9TsaQDEwJRa@#|o`Ce>$cfJ`uAf$2=l|M{yfATQsyo+@-6$Bf4FmPS_|AXzwu_BZ-X;|J zRpund7+ZA;(l!e8UYnQeT`lWti?2!KyHAxAq(}T>duG4Y**oppbZ6%SUFU613q@WO zH^vD^x#j6$ z?Jte@9=~zFWV0`$;r#8c-Lq^>Ib6VnDMzMr&u<)W<+eB8xbHf$xytKRXikgPVK6r3 zVeeneS1;{%!^UN*4fj;sk@;A}FQcewx$Ro)F&^xLS1UxRQ{2gZZ}>`;UP|JE^u01# zNn40jti>@0F{mYhBLSiW>A1L%mq>UKk1|uPHHx4q+LlP)5grFasJ^+@McfKh85y z6tk2}m+R1U9VS&edm;Y_t>0XA+(jv1;|9L%8;LB?Fe@2+7YhSN~7mc<%W#;MdL zkT|U`NN}Mb4d86hd~ z0m*Gg)YE}$1pQmAA#Ty`R;YQ?%(7Rb4=sU`6J1Noh2I8*G=Y~J2klmC$rys#&?@O`ES_lXY_ezV$6bXkCpE$t>^g$ z%^$ufGka4id|ds}{AIP+YgDZJ>`+nH4eOEkq7Yy_bwA{q$5J9}Q%-33+l|2$-LNC(-?JF)@7lHh#m_Z~J0n)zUUPP* z#1BR)9hsh&D>itsHt__99fMs6Zr8hyS%h@j*yMgm&TcOb@-&%y8{`+P_aP+L!(!y> z%@cMbFDyH4v~Rmqtr_2m%3XCDJJH>|c+b*xpRs;}hL#TTYpXMc`gnb5EPDb;$HdZgyJsv+Rf5t0ml>-RyxAcVmdR*v7|*$XjFh0+kKTQ|egw zH1Gv0fKkPH*5h!3E&*(Qr$8lYf!ldT-E0VxYm=b$BUgusYy(WdjqT`5YY|*)S1rY1N)1;4={ZbN-|vRi;qaK%#!9qzaHc*A@SK~GCkos- zSEuANJm#JXW~Kry&huHatcU2@eK-XH6HZ1|)6%j4raZ{&NJh4QL}neK<4R7epdKN_ z7TFY==0-25c~gc$>H@*#W!%ScFRW{7Dt%ZUohm>^Sq2D!9FNgg6&`6Hr1`aCA%jL# zX1U7}YQx>8d3DGx+YxH0bm_YZJh_MpT*;Omf%Bf1%RF|x3RjbnuqNi!mvA>7&9Be> zv&-p|Y^~qU8qY=o&EorghtMIXZF9$@o8G_7KY7(L7(t}yZNFzV^4gAP`FL}<$4>i# zJwiL;?7NL;L$`Nr-PU<`}udWd%LyP-E;KyJ~$&vRz-LeDm|OP z@`!!OpW+GlmwEj4u=|CEZ+pxGBYryMNbwBYDNb~{9Y{vgEMash(s&?rra%VLfRkO` z#!E)#KN+M2=NvHit;h>?IfDwkyY&5?`?uxD_GzTwy5mqasGsK6*`@p8P;m8x1^b$5 zgkx0^43#m7mKWaSywMudH8$E<;*@vi0i(CLXN1Kek(=b{=AU#N^W9(dUSs&?hk#Dk z4B>%c;_5(iZp_KgWLNkza$r`m8p28%l)(LB|Qa$#!yGV4q~RDd?sGT@g?Xe|(B zn1c&6TuRU-Gf^P`9g{`Nf?O$HgYLr^9>%>_j9W0oPF1E<8H@X=w-QWIY{hY3^&xq$ zttp4pVtiIV19CnqGcFdS@3rA*#xONnp&X==6j?s#QI&_uv4==@!HJ*xeFVmQ1~JEK@{rE2?i}XhQn)9)9DvNvF(nf zL?Q(4bp&p28#mChLu@&R0(tw=KKu}Lu7+Y$=16d2Gp)g*vn^vf5)$?SsFc|7q4r4g zwH*q=^^-@($8E6O#@`^@ z!@ZoDif*-3+4R^uHC}!awJO5NSJEI%u1HxJBesOl(H}iX-T=n@0_4m1zS#r5>Dg=c zxof`c>=T`jVqEI}p_yI)@bmBS-{$uR5$-&6Ba~dBDRcO)cT+2hc4V$XF zH&t0YsWeV$=uX;9*LrDi@Q2t8X=9sZwV`p;=)W^OLOPFcz4hdOZ2@xVIo-J7NvA${ z>)721R1UGAiQYY6lXqgmI{tc02+`%y#y{HRT-j%a z9>}?7@mzuVs+(`0Rp8yxlkNrex7f`;9y2)`Z#(iAy=E(zL_;sb7@)Gh5g%Uc8{_wh zX(VxHK*7kgXZ&-`JOhJ6!7fo%7jquz7;ia!y)5N~ID~kdz_EXMCeQwiEy<~uOY){8 zxrjn7p_RHdYl1GE5=O&BtR7d(kxW>XV569YF%@P*QdS(iiE9j{0<>+fK`%wzphHO$ zYO+ALLz-m$iVRh4ZSs$(H-k@2AP2{|9e3OHwib0)TcvuT3?ExoD~cew?I^!$TAuj` z12r6M6LHuOt>V(o#MUVe3sFT}97;mqv=n8n9m6eD%`t+cwDJ&l)Joxc-BLR$JF*@w z)gtPU$;>FZhO)wPAMX&lqF)jot6)hL-a|+EzsJVELZAh2utFm42=m(&jJ|k5Sw}z} z;ay?dilB+l1%nvUy-KR#`pg5Xq4o&82zNVQYJg$4W&NnJ8+i zB4stiy4=HuZ4vlgee=-K@n!Iq;81Ge&j^I|Y|PATa!H752?m&PfE6ahMeL}wZwEkG zCRvaDt>5<;+l79&7dJ<(ixn?8^0gY#Ks{#I~5 zBlGBEe;ejkt8 z`KdPgyN{f!6$BvZr=d%!RaMUV<;F@i`)F#NLi5DT-2$|6I9ZuGxgbI`3&qhNr}5&B ztT$x*mP)={)}p?wyEuxQ}at5}ml12<$-r z3{%d{yK_jAz*M1mPVtMhgB^eH+o0d<+4Zg0_3ryW3h9~BjCYID_iri)^!w5wdI3X3 z!eE+tzjOr$K~`p}E~|Aw5;Kc6kF0_q=rf6&=#Et?S69|Z8~b!*UySZ=8%OT~px}4Xs!F&as=DTw@!o?kjCJXbBMT8t8627EzNf*<`SAwn49Ll>@ysvy7s`m3H`mQH0*I zJR@sD1p_uPQ-YROUJIId(U!X|m{zp|yNuI6pfjK=VxUzsIk31wO)pjtGlDBBu)fxT z<-o!J3X*rgAF0+Ua0H?5ys}b_m6LrX%mVE;PP{g_I?%9O;VM%>D4SWInx4qoscya$ z%FZ5m zF~LIYU3xo(Jm4LuunfKR{Me@3a}QE@V*~epwIRM6e@rE8TWq%*kN97kvwtW^a)<1- zj$+PjH+a>D(`>5m0eSdE&M$gfw%r;_h%DL4amssli8EebGBKYPRi=OP*1pY$M*rmC z=VEBS_4EU+Q&<2vc09;?o0~T)vm15T;&)}^=;58(#sM0?ZkW~I9+j=>{DUS}w_!hQ zBY7s~u4i;N%wE49=u~ykKf~nCZr=gJy#>0>hhN0|cE1(A*gE^#ZR5ooL_5CK*)%wr zH)6?uxbD_d9gUC;v(@Hr@<)Df$ho_Nu*qM#Cxo)Gf$xkfrNX7n%00`i(@V9;3{Un5mNFK)vBN(+i4EN3t9dzztn`pi#-gQg7-kc7b%HCu z1yCh+G!UROmGTfEm3>&TFuN-mSmZN?6wqe@K44~w7z%G>cw(h%3UF?R)E(-qJ#Y@J z$kf&+!L_T)YTGBvd#InqP%6FOObc-%|H%D_+qk@`wW$)9I8ZSNuYq?6UvLY?4Db_L zW&E^Z6;XO}hiA~R3e?yul`wF=_e<6LS<_*sZJAS)#|{F2CpGF2Z@EDpgmrukcBC=f z#6k%CL)3*BZcu;%_Rh8htB@F}56TIlCKPhwbNMw235CcE9jyWXD8X%q=jh_KzyMUD zp9Grwr+5s!Mg4YDm28`azxI0tofny$8+CB-b9AHlcV~QkO5dUImA}JsHf{D+g%O*) zQ>1-bHdRk#>)%Ak3lp1m-Y`` zdND=@dYd74?(KdXliTe|(M_-lI>h$a#@p}m{OvpETajsj`AHgD+5OgyY~V_FZo2Vc zpRrrlZ}$WIoV5QK>6pIXH6TYJeb4r*^cPVzjQ z=Iow*-f!K&eo}hBCaSuL5N5de#dRre#=1ZIz0ld;Mg00}n@6*M(Jj0lxKF&xZ@&}g zz{Y~44SSy&M+3NA7&Q8g5Ah^@4$nXFkxlvIk-IJ$4&>iEVD+Zl!<@wONS9ltB^cV7 zoGBrZr0yca=*~CVQX>PWYBQ!$@;_q^=6+3hd772&)ydI$rhhOpM<2cFZf@MI1!of? zg~>uxP}w?5G)oLZx5sQ6l^R`3Pky*bIQs8nri*!`O_kd&!|t8<~b#`|j?Wp` z%%QmsA#g-xq^8?=!PW<}=}~q@B`55GRX?$SAGEMKNM&RUvfv#op05qOQ{YfAT|`%S z_xDr1*Lndd*jwOHbv@@N%{aqZRM8nH*Y7FraWN!lVI>x9aNqLI!LH)KxrQHkjDE-z zZoH{w6tnZ;UagZS)J+M6mJ?VA*TF}bnpSVa-5HhmS;+)Fx-igU*YSNGa;MwDXT%rx zY#cjSvrp_@+^w*bhNo0Q%5*4tUFPVKE=zAJnZ6zH>7)3&TSEMfnd{Z`A-5ZHaS)H80mm|_ z@G7Y7qr))htMGoNO0pKmqAo!2#cpnGkcH)II5grRCYnc)DaZ_Gp^jmG1-nB8RKBWg z0W$_i$Cef(GlWwrH8rdiMjH?s-5X09A97$WDD#^Cj91sZX;D@d9S$&3TODLx3UR0x z9)y<4DS#jNIKegY+R#v_F26S#}Rc)W5zzFNNY)gN#kzw*{E%6bHfRY_i;}b9Ig!{qf=h+cgdG*t5Z~6nK z2iVP@PZx!gn|~dDbmH1lN;;G5v(Pgxi#e-JCIpYAiH~XYm2cR0^j5?J>jrv-$+oU< z^V1zoC+Khg8)ADac{N|IG3qf9PIcSuXc>I**7&1?fp7xc_kZ#8w{LqK3j6X-ETuFv zQU+#w+|s0E_NF4gd9&mu!-ziX3cFjDf~n@1>p!|mj^#c5qB*E+V@W~PjKSdzv9CH$ z(F0zj+vwgW`W4!4?fiU)O~FXD2RaHoZEO9{H-uCevztFJ?)>iXp%{ySg2xe|FCQhD z(=5X`NE6e}aTl57HoGK?u{*5}d7s_x7a7b~ebnS0(0H5k&ja?k-`e74JGU#!(skrN znqFpS5^Y^Rw>qOwNDHjM&bpNCWLwx-Nh}Zzre0})b#Oq%DdWEbD_D8fl+*L zHIhDn71Qz+7d?K9D`>>q2*2@zFQBpkt9jaRrqB-*&AGa)FJtv4JR`_JO z$|k}yir{4du?}fEpDb?*u2iJKvxeY0SQx}|04`-8Z%J&H1U)8Mh>EXKhZG70+zM0? zl|@CbQ>;=cSz8@sWkthwPgBMY_cIklIJGi|NkAB93^QT}QJD#FV$ph1V`GyLnIsMh z!{Afpr{fd|1tCj49jk#GDpa1K1zcU}1ChfkN`6_;!fD|HW=kVc;p@Wv7qLtWs_A5HeF=e*cG0HRWed?tW zb?`-GKfW)tdq=>}AB%g=9>TcXkZyZ(rr<)i!V3Cc(s(pw6R$?h;Urkh9p$I1i z*0Up~kM~)Jy!fAd`8MuWw(&nq7t=isc!E@(vNiw5q@DK7{oSkK#FJru#IN7EmtniD z&@%||oiuZfWowSb=%-hG+H;y1G}5U3=dB(6ws~*AH+WKOFq_6PmFPbh^-uHey)Mmg z9v?OH*!bAI&-!LVt_>K{+X|ZdUrr)Ln6~bf(ppThq->ZiHa7p>9{q_@rS{jl&-0(( zFWh7Nbl1U_!hkOr{u$1X;!pFiz+u~ScUbxF?)v(y;X4=G-D)vVvrhjDyn_17H$PVw zTdT=l>Buoaz(+wzBP1ULAl0+NbZPUm`TEH|{49u>m+$aA93$~a7yOQ2#&qC0+4pf$ znd(#e5%V?(>aeLNr89tKz`T(z2mb?ks;gM_wl=mm$r4aoS1DKm!DV?0W?oghy5ebC zCZEF8E0kl2yDkT0xfRSKpzC0>0QYXCeT92Ph)%t0aM!l0ADY3Aw^b&WFEv(p2(AJ2 z0%K1rzx^Y!c6HqKtNf3RZFPX-jTl?>kg_l}40^3GqxP&{+ec**!X7lsxXZjcIoyH1 zO`3ND?>{eqO+{&yF+eClGUi<7ZH`K?q0*TY z8U|=W3+GWeA(7{0Q|8Wfx;6f|`B!h`stS)Ze3((ALox3sIQA0^R4Ax-R`qCqfz?X4|O?gJZ)#elNl5=<{l6jB=T?m(s<(yKgWhe+$fvQK6mui z=qICxjnMls;qG@R%wjG+w6r*A^!x4@@2qgsSr5?t$i%jm61XTrYd!#~Z}xa&!XjG}vy%j<@nYoP0Ziz_s(WpZ}Z(z>D>HiUTna8)l=iL&V{0YIe}Ml zdO7?T$BN(NeiD;(c+<=ANxy%lFJ5RJ{Zl&2UdS^3hquUkRwyEua$I8P^*t0adIT|T zlOmEUsyAEM()VJ(-+;~l+J)&2Rcn|@E}(fl&Pb{+SMP$++#{FAf)=5UM0qbL7_s0% z00u@=psOCOlbP^T!xDiShb###b1jy_!n2J!tU?wM5qSlID~PXVO5l(kQZXfKsHqWIp)y%OH;{)| zDRXJk%z2N5I?Ui0m)h`*gBxv`8#qjgrc!`z#uOYIQ&h>vhPj0|L5+DoNF%A&CckeO zk(Z(u@>X!K6z)spepSYzN)=gN60CVd3}AZt8FRacT$jMqjTg-*L@1ho(nNa@jbzb z2O|Ev({wCN^0K}>%c{dCl{0=swtS+h0zgE0f~=Bo4AlN!J3hH{N8;*XZu-I}@`@*njjcm|GAx zFRdeeg~$sPo%X`1afWnGiRjhT60vfa4tfNW<_ez}Jc|J4(1r&y6@!_`y$&N77&rfED>DAKQ?8U22792Wp^;Z3 z(!Mc}1hE&bwyY?O#Vzb;aiJ_I7L#rkNjx0o!)amTITYH+0Uy5w^KcT35y0rvqRt;f z(BKGnr*)ZI*n0{gh(U=0jZ%C(;Q)Ox& zKE`FSFPw&ff)*-tpiT>iTo7;E#vWoo@?Oo1}nutWi*PiM=W96brexT%KplM9xZSXjLR zoAJ_gnGgAErRSj~hU8U0A;EHKrg90`Rmn^2{t6a3DBPOg*7tC-{}sKUad~=L_z{N5 zX(0v(#0VvWj3LCkP(sS`SyO?M!x$RuS4siBfkAf=fo|*>R72<{$r|<=8d_Z)21A6Z z$Q(wcl!~epsgWiiCDCw$RVmOU%|gB(xEvUV!qHPTsB(plrxHumYc8HFu~Lq{D7F4D zrJ6uo+${^6y(Sf(gKCJjkG$pS+eb=Ep~`U+8=+TIMA4u?V2jsQkHI#PFND${z<{IX zzEKC^MvA3C0!JxZG{OWBjgx|q5k$6%fEWQkp|1E9lUO2OsfZquNb`T4JeHUIR5$d~ zpplJlcXsS@={7_>WLL)7@r=ar=$DJ&z)ixyxCkb5M$+MU@jGGEg_MP3TWamR{#4K1^B!Xx3pbvNct!q< z>lkh+Jv(k=m9i`UD)Cb`@FJ1LFLVx;vMpbimK4amJS<#n_qZK`@bE0ZiTF-Fo;tSi zs`V|V>16Qav6AATq|43S#GA#xWX53oCqa@Y(%Zacj9K zmG530()X}c-c&*JQD*yUYG@=WH8gVdXZ3vzT3Y{S*hd8!;J*MU{a^k^U-@5+|MQQg z#!uQB8a4OzfB&`4zTnNdN47sI+ST>r-WSHdznScPK_R{mJ2+lKd|yHw|NP*%h`hii zFT5p>i@dEw-a2O9+9Gf3@ZTR4`RSPY{$_y(eRVX*znKUAX0EAY9;9RLr{n8qZ4vU! ze9qcDq}w;-S)`_h1t4;oTLxBj%`Ft>7Mj+XA+uY3LuQFrgUl^fHLbJsLuSwWhRkiv zatnFpV;`vF8)Q0w&-xEOe9OR`Q&;2?KH1SaBgk4KNckib6jhII-dpVWcrz# z|3@eCKThVFX6lPb)7;`eX6BmK>Z>JC=W56^vmk3fM=1uxZ-p)aOHaI zm^$ zSN#F_6g)pfz7uBn81llMgTI;K$$`&OKe$kOQ2k&jdHg55b8w;L;CS)D@t?@!rTAI> z{`=2=f1?n`e}8+Arz3^<|2{i^f12+4+lF1=_3e68xc%;tt#=KaQ(kR6bJLpWn{9KE zP5vZ)q4U>$az|qNNLygc{VyLJ=u*0+;Z5^edOe#FZpsatlV81A-&!V0p|QN^v)WtV zU1T%p>1d0)TFQVou6coy>Rv|H=wmF4V9bA}{*1q06$+=>jD?{&7TCoM7DKAk$P*%-g~Rx@m338Xo2ChhI}l?=Zgnj~ zV@QqF5hwC17K;QY7d7i@8wRNc!+mku*J|^E?-6api8s)=`5HfHq}@2O|n%>FOZy>d^d?@s-#Aa6NSA_1S06*{>{&UR15RTtBG&?@to*XRep;4)8doH{3_<@=j@K z(3qB=6xgq286xSpdrhU`(CtSztxDI9w>q7r-LT2`GMu0<98CJPlZa`SZaF`6YQGN8EPRy<JZ2n16 zZo^48>+7>eZ|se{Za};q`sIC1|GEYz--Mh(^UlMMCb~9$8TK|DoU19WHLiBXE3Sr$ zSg?=rngd2rJcJG1zx8sQhy^3R6BUAw2L2b*rfDcY6{9a9M(<&HtryF_LRIR8GHdqF7Y^AXczFNLY zu>J+Wv@@&mhilQnfNU38)!KrBn?ns3Q8`6i8F14$&_cGaW)usE{fV zyP68uz54ogwNt&pW};udHy-m3huZN}>GLH1$%=}Kd$rs~uHcLD6u~Ld?w8}VINI*P zd67fym?xo@FHFC1?+Slu{Fun1>p+)RG&QAuVu9f&w0d=`bBj*1!F}ryTk~A4v&L0E zz9PLlSA4c0X9X&%^l#EZFNz5J=I8L9hZL%zX@ zZ5`ph|0HZ6oSt}hTDrfMcZqjFkbWsI-1wfhvC*8J1vO=Jl6`W8fuv(yFI3;aD<_qp zAJt9m-*t`rpuWt)E$qSm^!?{&FKagwUfme^r{3Eny(g&_r4APPT+<^tKluLd$f`-z zn>3#n^*1lvymV{!*U{COz)uK{`slqK?uym1km0LGG5%b_YeW0#Y zctub*9>O-ed6rOBud}$(T|QE-DbT#25tsd!95Yk4hC#=Fd zbT^tunC-QE>yu4;l|8PzePb27Yqq>A;?D*KW_SJ^7*=%S>{&E)q^~b6$&;>K7CsI) zOr{b|FXc>j%%?etbha3P0bleUI|G*u9K_ew1k*q7KtM zR4&E8e+?PDp8JO~@;~fSrLZZXkQ->i*WBqcCK<;b=^(OS$d`cyszCc9ZI3FG%Jil} z!J%ah$7QORnQLH5u|(>`LM(`V!)wYtz+3^mqNKriIHX(~%p67#2c}z9=EA}tvbYR* z8Dina9fU7;ZCItiZ=)aqAqaJhdN6Bv50lnMqX7Y_phkjKw6|zqzyfY8W{7vgP{8EP#{(P0x)^6 z?Q!Tm`#llG8T)I;Tr&cHn4f#(n%cFuyw9Nf2-mhuyP+Yf>bgnJrd@g)9ZnzY@qMgK z)}cGysLOtIZ_#33RcXZc0gDeu6=F(kka+c4vq0wARJwYljly`n9zK6n*#2-aAbBfY zKS}eSBik=!@9+HC;De+8ojgrf1LqdaU$isy(zbP;w>a%^I`+n+ZEf}h`G&1sTP%5t z!EW)h#j$?$%WSP~y`LeRGdoSg>_uA$C%!hRF5i$D(4MDEeioRR8?)bls(Do_%s4W6 zH~smB3h9Peu|ZnCW9Lz0Pp8hM>Mh2#9O51eyIp%;a?I%oBdfb3t7^4B_-OZLre5S4 zkDSSQ_FbRe$6|}sCy{LFYRr~fo#(?gJXv(??)a;2M2%C5BIIc{VEhPuXqPH?p*wzYM!C zoN61F{w-jNH+D(*2su!Qo%8N_;k#yE^W#b61;2mE;~o0@UD>@2t5-^U#Bp|!84v1# z@rKNNr454nz4S}6Kn3w1jMPM*Eff9fz;~bb5SH+`@)$!yO7I^MnpqnjVnz?GW&FUT zqToo0x^jTw<1(|>XbY+}g*O7Vib2$bO4g`Y&OTR(hWG4IMj>-6Rj#2XayK4F-I)UApM{1os5y9d|8r(d zlxB&;*g=WI_^~KeR`knuwpE-3qc^A9(m9j(BqXQXooJN&X*SKbOhb@kqqW5_IkE_d zK5imjcEHIHxlM=%Np3YUgYIShl{4^zbr!UR# zbt3*7Cz{`FEA8oXDtkS3Z|BX)o0o1*=f)VBySmJJinKmz9ZIv6>o(@3j>I+W%{?FX zlwL5_{AJ+IU7v(q^Re4?=tT6*vD5D?a>-_=+jI-~Ve^=hPXEW>D>=yxtJ=Qiw@bA} zH>%3C7cG)gb>qLZKJ#`lBWAyOpGjdsZo~P(XbJLUL#POFM6J zvG96@10iidMtBa(#HUs;*7Q5ODAoRzJJ-;oQdvd z`r!msYY}WP(?UBqu*id~1t~7ptOYQBQpMj5(Wq1i_sG>y5eJKd^n|EF=Fpl7zrdx4 z5!VNNkIgSf(nhB5*6V`bu9Fk05upjRQsAQ{HRs|3SksBdU6GY%}tA&D{;{4{3P zpeid4ioza9QNv0V*o@-ujb-(`L=*A%doXp9s1^~9GTn{Z-)e+t-qR*;&K~bruR1ky zbfm57dZ*h6GvX)9{KCeDV=MPgS@051@$DlUd5M<1%MI_ksMK9gH@ogil$|2*1+r=n zK8Il0K%z$Vc?ZTMjME~b8bWfL-+oKi7ue+adc|0e(6ax~alTCNj=l4{P1^Ye4f}6c zM5MY+W`7}mZm@JN#^9%ux#h&c2)Ceel~R^& zsIe0cMIdy8+Hp;Y=smqQV^{?lsR(ACUt-NUt6Mn@sBjz8EW7#)oR;_Pk+ZAcn-{uq*R9k*&_5O|V6f7*SzQt)1Fa9^$YsTn_nk;_{o zB6mUbl03NY-AD*Rq2TVNttB7j?nvbxIVCm_Cqb)^XZva;!kpT3Sqim^Ix-9f#!^<| zIuL#mzh-Dky`foB;PMED_J#?<3Okf)D5M7{RR&PDHOxXLfKf=p3`T6DWLc=XSY5%r zC0`Q*t9ea6i%Mr<0Y#^?M)T<#j1A7HvXGu`oxRlAt?TZP3Z?DStD=ht2+PHGwzH!f zfe1S3=BluL6G>vLkY<|dyU{v#w{OnsbfQi{OP%NtO|J9_BlH%zdtZ- zRv?+UoUy$xZaL4p{8j?Y%igojv(2_mwu$yQezu2v{CCISSYpE16| zTk#qC_I!L?vgLBowB6-{jl1kWK@8{jN4Ki?6-n)t;9e0!qD@NMlKMArPF zUt8WP@}FGN$b*(*oy%@nSTyZ*C>MY2SxYIG-hOnu(WAAzA+gwDyUSgBmtQaWMlAZr zI+c9)>+e0C1l-{LCG);v*K9r%#hl_%-d?Zqpr|q?h$7#l_L|JV$jAjc$enw>J>@mU z9PIto_SZ`eH+Thwf=kiKpIOiUbkKU2?I*j(&KLihyHqa zxHN`CpuGKDo;4)UgTstXKS9lSBcjSP125KuWN<(O$^4>ByfNYnvJ2g>D`I0LUjyxn zF>4*ZYWZO5W}(WW+(LPyGRPswMZZebutN)RcOPYM;Gn|5t#AXu9{44Qao2xganQ`- zdn!|7K!0mIhkyhQTVo88qeoC8Y@BYc(s2AmH8-j@STHPP{9@XGVPTMgCJQhG<)!Id zwXL6?lUXMy9=|wjA+kNR_2J%ICv6|wwf;ct`F~sLi{4lswSBzv@zGoV>e&0xCh35} zKz8~(#%5(&GU2c5;F+fR)@gQ&eY>w1w#zaapK8K& z%N&|B??UNIes-aZ5Wt8|ZS80kE(@2t#T~8EuE5UfQNlWNJ(fdQudJ`;WPUcwANQzA z_fzi~3w>F{;afh+CNiw|?7Q_ZR{wafq1vf9(az4{yTIOS_VY*j<~{Z~9I&20Z+E%M z@+apjXUj_7eqwO9Jy|mohgkO@tssKai$|r zi!cozK7b=vXx>z;OcXr zp=8UWTR&NSL+H8Kd)DT3(47g~nyTaVLDiJX8WL?#PIxt;3w$6Wa)Z735Gf4tBHn(w zW4KKi3riV6u zj25RXKHu`6W3;0Cx3+p%CC(ELeRSeb8fCxb|1C5e3U3g36TqXxjG>0Hydg_YGUOS_ zDQ0{prxphOltnIqyvW^CS^J7ddHj#p_4W*(+YMcN9V=?m-}`R&7_0R7tL>4!D=UYu z^POIm32Yzl9)JB4epSIZU=x?=65gbbSjfDl$gLrBuVjZ>#~nB$Xc4$p)P@p?zEc^Q zL$zK>l8NQE$q~Prb1`Hid7Xbt;pmdG<$t{*-pa`@vdi7r!N>n%|4};4YM1k&*y7vo zPM3Ul!)^Ouq}(e;PFgx~=@uG%CDlVRLy{xxgMRYuprj2#v>H7RvW>) zW)OH7&S@`j;_#_gf{1KM(~Vln?pX3d))}PvMAre?F1`iHCCkWGFCmzOvXl?MO<E zN?0sF41r>;!qG2eGqIvInai;uKZ<>>FleD2{o1*O=DQg}`{}y7i78+3c9YyAfXyD5 z@R{0kOTFLrK#|S#-IFC552ChwG7x^CG@`t@)LqR3!-y}Ltmpr#1I0%ybtF3FpC23CK$1Ki} zUoV%n3y7ZX#Qi*?#b>r()c8`e#XEe~{p|b4AO9oIGL^VHzWcU2%{9Kc=wCZ{T@DAz zs$5C%7nG6qlOm5xK!^*R@zJ48twA%1jD#x zq$EAfu{Y7ZSNyu_HXfwK@PR#(Ot&; zMVmt_*EEXVa5j*q`}kJYZn;)u+i-tNZ&C3r-nQDlA61qE2ezf(_4ghBOBDQ_Ol)z|Y|v^?&VLXAV6rCwM1K7xV%j2>MnMv&Px z?R2NG9g(8l?Yy)Qq0*#9GZ6akK11(LP)_D*RhR{V1vb89Ae(;?;#t_UWimYHBq)Akilr<>DX=;#!y z7~O#9G;{-%UI?f*V^2idZX_S;f+{A&d5>ENMCaoZHoLrfN=uSlU(+qvmeE#f=LTYf z*JaxuLbg=h?Qve}+27RQ`q9{q#}6%e?nR8Sht}V`=fk|)`*nczF3;W{U*`~A!W)cM z{~(pT{(iSCBR>5bud?C@#|HYX|Fcc@>4^@<=C(jfy`s}vZeY)jg z$#+)&cg*tAHgM!xEdOVBU;2-ScT`H;sUz{z)$w^O_d!NQ_0SZ6ij$=y*iN`p3Ql=f zPN1|8E@-Rcm)Dt*>fGA+<<831uE42`l|2>J!&8f?F(a92IsBF>8LY1#E~a8chg(?W z2-PUhQkb%qj3EwXrN{rDQ{U^5TYTGe;&}3bo-N{)!h=o1qI#;+M>4+8-M7xbnfyD<8NV%Cwx=QS={Ul+(Aa zI`?`JN zYbWxKHvW93tW@sBeN?>ssnss;Q~Z>WgaehezGTMYCyOwLybpeJ3vQ0J8I4$VKd}0B zcUmgXpXiTP=%M0%OBUv&PAec)<3GO7h|)?c+*nRH7O@8oReoU zmSSL*CqdTkDE<*b_nMYafH{P6s7A{A(kKw%L=(y=05b!lo6aJ2v(3>I5ayzT=8WcR zjCrO}RBh-J3>%{|;|7>6OJ+9(4cxj>nt@z!)`41Z09Bkr9@NI@y3uGdu$Aac=q$B~ zS*RK{fkU`qG^&`!Oe26{G$J&WVN^*c{h}-+{T#mU19ilHQt~y%(}BpByvDaLdG5U6 z6WcCNAAdvIc_J<_BJ5hSQ;o~6{l^2EeAuoQotKifc@)Xwwu$WAw^Z}&T!t)G9S4Sr z_W3;B>rmvQy&G}dH9al*@Rn~RM|vWnD4s=8CvQ(4f3L>nhF#A`{tnGW<8;zinnGtv{Ruzw-2a<#0j6E{imud@)J_6$j?dkSwpSv zIRYzczhBV?`9S*4rI!NBzT|7K-FNIeSkzoA-y#l};4?=yBLmQReIKFB9@itpE#$~lBAHFsvZ^^$!Z|Gh{ao-%XUl=r_!OBJWD$`ALCcxm(B^r~J&pg>JY%S-eNp$3OB}GLN+(aK5 zydTA8ug6|;tXn)KmN4+Lg5M!N8}0NE>sv3G&Dj6;%@^B#7k&HkTf$6b$>SRyL{HnR zV z4xfaQPu`bj$%5A_W?Emr@Vq+!P#EuB^(*l@eAe#Y=bt|^JgF|A4$mN*>Wfyx{_4TS zKjY;{1_(Ips4%R*L-1+02M+ z%=GJ&>Zx)CvKAqrL5w3S&j`hhYJ(O9Q9BJg3A*ALEuc`zVnHkhbXN{>nAu1keAq$& z!KlE-LaN#B2A#E0V+=Knau^`Zazf>W`5cZKm6okh;Gv^*%A(PTRfwrjV`Q6PC$_;f z70OKbMflU_!+VsmL1PEYPFZ&%sLq;OYPng=l{EPlu0 zz{z6r6Zfq@l$|8o4wZ#?*nKy2mTY}H`DBv#^*71(yIv1p&LtgAvNm%f4tTg$Sp<*{ zlixIcaLr1Q{F%e;$uP;~gI9gj$q7m2mqSmM@jwQ&Tx@O)N0%!UAJH{B(gX6mg>7jU zN-lNImIR8wJ%71#lXyOL8=@Va_MRZ8PV*X?^dFR{58f^2txp{*v-mS0y= z$FaCU?+n#bD++gapeeh4?U@Kg> z(X{JV>vcNQns61_m%7ys6A!g?%~W5v3daut+`_sbBTM%;8!DIZ@5)hAr@AOas5s#OVMvGcb)PEb`a`UL;5!2$qLf; z_LN`C{5*1jO+?%Y9A`qkaj8qLsAHYQ_a?IQ7V;R6SFYZuL^@GM=CxQX{*%j(3{5W3 z+~Erv>pZ@Gw9()sE_CakE{%1|U}JAdclYq0n8B;bHhMi!e;auUuS*|PIjJa!B{r?N znKzv1eWTxAugy%2c!L-9sPZU zvLo%HAmT~pfI9j~R;2pfFfY`rF)76Q1JbR6q+MSWM}O?UcmAgJcjT}{yWcLKAG;tX zq}gn1_z&m4xBHBQc#dxhMV(y+glcIiE7me^3&H9idxm+ZkS(W<9I@SJ9XW(|GN@eO z9Zx4vD(~0VkjkfEBfck865WkIdw4BBROwROwfT|@TOEHuE=v)$5J(<~;px$brthw3^+EV$a~M3)}Q zWD0{)mMM$Rl(`aIDNhpA$-?PskLj9##B5cZu8g;{O#J#qDvwYFkaeMPL74RQi^spJ zlP?9Iy!%r8`cKK%9vAGtdm`gu|GwmJ$pJy6pa9u+nIec8-mD|&8#mJRdQX+(>|Isd zTq`HYxow#<|KH2MZ*EO!zrC!qyRLJIQh19RZ9S60Hp=#;7$aF=Fv{Je&stcC%!K&HM!;Y>n{-myVK*v9_k&U z+~VtU%1(KZl}s{rkD{_(r#lL6mTDFXGq4<|Di}{koN7rw>>Ts-IUKh9Wd%PlSsdcE zqqz0!UHua`?#xIUQ*_zCk0l?yDe%n^-&+}fr=I^!m;Fz~wW)*e9US9)pYKVPJJ;7Z z3b=<}@T<3jatU(IqJO9_SD$iiDKic3|5AzYCfq-@Z9*(IQU!&Qv=+(A3gztxK!6Rk zF-y)^f3A!jWz%tsG-hg;@XhpXRechA~_zHfCfNu-ee^Jra;qk#@!K(<*zhPo; z7>uE8L<=&b*eX~J(6P-(i+&!GaJ_c8+w2sRat#geQ|^(G5z>8rOPjlUJlm^D7*v76 zmhj2KLq={51w%CR6fXB=gEP-Jn_tR670IJ}O)3~qmOCrNKtlI$uj(RSwGoJu3v91R zm>X%^cD&*RqLU1KfZv^zdsnn}R>#LTz6k1H&L2=5Z8SQ!<`2fW#ZL_kM9AIA%*a^h z)~}xg<7%%aB`)OA3GrEj3pDgc>$vi>7YEghWP?|eJ`jaVZ1BeH&qNITx1sJCS&=6z z1);JF~=>3Vl+z^7Rdj_2_sII7{aIeJ@&%1j~$%te=gmC}^ zNQqA<4B+sC9N&nDpnhW0zB*Fbo8|R_Q%I$nit;+YhehNfp{k9dR{5SM_|*(e;3ep( zk+30&r6`FH0VIoxzBioFt~)(p+?ao5b`?idoU7ep@rKhoBwACdcJOJ?|<6qQ#B zkS#g6lAoQo#3)D^dti3!75L8xU}jORg)tv8gH&!D*yu#&4RSvY)p(8 zdkIWnz*q@|z#5RLk-Dy;!onNq6&Bc9h*;~TeAsX0N?;6wnMZG0(-BSevrxby!B1zJ zGh97KwWT@EM5qfildBim`F^rTSiwWA|Gz}iWHF?g6_`t&vhl4 z{ReJ6i%9fDpJ?G1H&phS2eEAQ3ptv61QMI-8&5qu`UiG)2#W&y1e%qV)Y#?(!W&M}*|LSZ@ zwW!=+W*$Jp$KX!ITJW$shY}1TMU8rEsI)c<-uPdQ zeSv4fC!96!qvbym3s>RUzZ4>&HBbpvoEV>nezgD|>p-w14pHop|!Sr?PJLKDw zTXa>8*sb}We(wD>-2Ozyq2H!rwh!Yvm=RcEaqq=F65hg}%8IqeIfHiH4HVXd&*heZ z+z>Z%`Jv(zxBhFd_}q_X%K4PnKb0+BZ4ux=!-RK{D+#6=*uk)04Z#SvhtGe=C_kcx z5HxYagtVwcq@!>+a!wvpO;Ezqn5K=qXUar_tdNOmL&eHef>sKO-v)qtj>$}@8Z{VA zp+@XPSsWRr2M%~B9FBqU3yWzSRk4HfnM$w#Sg<8p|5=uA43Go)VUNn0Y@`nuG>T^^ zb8I%E4h#tZ>uGE(vjPeWgXkRUdN(rJLi0dL70r}MX6yKmq%bmB!3;Vh3HE&?3HF+j zAy-tfXw|E%1mks-C68tGo10%8L zAU;sUn9HLW-C%o7#&l&X`WqcaSr&m(5QrzGJYgh0UM)@hc>G03sp2U4c%^x z$rH~!h%U01x5sM-PW-Fl@lP)vzxV}xnV0>>}fOWtb2R)r-S^EqA4 z^ggfUgog@0cJFTwqHNb%f7c>#VfT4-y(bihvik}t-s18@x%{!e=*Lvkx7`DJTjsB4 zs&~_S|ERKI#g*{Koz)-G`<_weTNHjFeRWxVe@t&y%ok^aBuy5$i^k7P3tUF1CvcYn z?Vf7%okPesEA!YC=hd{tJ+DyUiZQu7hIoT(^HLJ1ryq~ zi5Wrm5}t;9>=fl#j=(o%rgVh#!*n z3y%w3=dBEEdOd6&%%)G>Ydp+~9WeJZ51O4YddHEFu67ICO|vU2sd&tq z2`2{!OmE)Bbbw_)J0~#Du-%>0IDX*W3s%1BGXK^l?9WzB_2BDcb^TAve_fX(t-D*F z)82MdINPb~hPPDb=1S_S1aWIT=ws$KADq#ecsKd5Zx!X@ios54awIe~SQDG2RvUuN z8>Prstp=}2Ibkq^fs2itxMXf)CIV{Mg9|d|E8Ae%s?ty6_&GDTET-0(p}m1 z^k4dnj;*XaI(OdA@suk+_fLgqbGFNz#O#6jP2Z+$cE9*bMs%_O3ow5&Z7}iJxcX@0 zdO)B0Lk~}EqYc~V|6V$;11OjDhwyCH-TCLv0`GpIXaA$pC`WPL8wxYLrJ7QHRp&n` zcaP7C1b8R(9#CNH-O1a@Bh%5)b?GhswAl-zv>%4waqe$aUv9dVw1fx7N zZ51&O_|r9Q+GKTiA>FH8q?4px&`G$dLY$VBT-_RP6G?nsTAUXch+veIVj7i7>ru0$ z`dKL=CA#3*G>6P(d{=LdOeqB!JV2md(Tf0;;89S10RHHZZeCoQtuskxC3Ye5)($j$``zQxqb5&eUV~vpPS!GAuxBGh8?dv6P8h3r0bnS6Y(n<1S-{IbekB7cR zYeqve3|-IIc(v*h=t%i4LtVWl3OyoNtZyMzuxsjfk`)|ToZ+U#;z-4r_b z3ni)B+b8^m_Q4)4t#5nT$|#nV{z6see3VElY(1cOMvbNH5MI(k(JKTV<8D(kXtQL+(yLEfwm=l7*duWuh`Gzn9p&y8J%N6n9#> za((PSE=cCYe)$_}w=cQ{(ev>-Zg=sTY((Bu2? zDR1S(Y^rZyYArE4oQT_F+TM&QBL+~(iZEta3%DwT6I#eQCkKa8KAL4v8$!KnvzW1@ zw*#>OU`K4oI9W{QC`WZ}X_QLm(3CQS(FUmK@K`cHbWYpA%&1~K8x3F^4NRuVC}*)W zm>10r8p7!5c^inpQ_&0Arb2q&XyNE63?)@6K)fJGFu|YT(DM~|9yoz0jY}$-CJIxB z3hZ<$O*N`Q_*CLEWt1^MV*uY;qux-;)yiPt$yqe@B5E19Neb}asAt;npRuNYgmFs% zI|ZcCpTd-rTAh#_#9z8hg}&7x31rOO7if8@wNy1|rm3Jkg@pQ=J^tK@;3!nqP=0u` zf-e_u)X3%PC8J#a3R|a;tk^8X;4CSJqzH!K=Sq%5QB)jIn$$lJcUM!F@)pwy*7R%B z_4RF-Oy_30yFftD>0vg0N#EIBS7#1dXzvtVKq_6gPLJ$8{Y9x>ip|>ErpY7)wzer+ z>y*OnxoZv+(^^OOT(@2eC-Y6%JR?&I^}H*mv$Ja&(WZ{}WmIbBP7M!dkIG9tJ+)wFn; z%$>A2D3cLLPX{9yhXXHGIrExJ=^li-*00mZ$H)s!sSm=N+pb_ei*eHBrO0I<*2Ar? zHJl#z)6a^7s_jvXpiD);`a$N2#gRT!zb?&F<+KU(E$!kR-SnLt@ww$t8Of(D)kVhe z5w?aIZD+?FS=*F}KGUt?=j8TL)V^P2_UgsH=eT1o^%bUJ>&K=A{ujl01`2)Qh$cc#cVlPJsDq5>Zs*j;X3O~^PY%8x zkmu!UE4s1^N1OzDKgsyJmjZ(1i#uAqr#$`NjrL3PEw1S~=)x+u^Un(FkqGm_O# zJ*0oR`iq@9JfaJqKUN3$^f%$^1}8(TXffZncOiZUiHp+iIfRsS%))HETZp+}O$T3? zj#<6PwA$6KGp!VCuFS1nDP_5X(r62MhGcP%vhQx7J%Ib=;(z3CMK*q z&Bc&yVrDLC5RPmrcC{7@~*C$tq+&aLgG8R(a8m^-O^6Jn0@xY7xm&sHfG0D;li zM>c(dT}7pU`l=OV7=Af^W55@t0?Ch?D9>Gl=)HVnP_$li%Hy*8F2F*Q#K?_f#0I5o9n^;~Nl2GL^1tBp970i)yaMzxve_|;a;z$gOAYR@?Qcc7e96PY7&JKxw^&xya$QDC{n8u7NGyeiCP}z zY%t})ayCrbIdo2_VM7BZp;G_aF=AWEfTbtV?YUfsPazpYB3G#e{e4{XcPG6X9E70m5(4w4X zd7DIX%9Vc$vP<3V;S}Xib~}~)xGE|x1t8klrUN2kgC>eZh;pd1(Gk&*MTQMu-5=(K zuiQ`G{yg#VH;?J0l*BDG!0yEK-_ljO8_R(a~JVinIh0kW6+u?Y2(2Qh{8QHK35FxfGVrtiNg-AtS7 zYWV%;=ImPct=)#81nh`t>HBX$wlOPgB?Q$^^A48rE=q3+=HST}{qvGW?S}An{S=@y z%KSyD+LRB;fA3LD`cmnAY4P9rQ!!|W5k1(Wn9pPOqYF1e;@p)Y$vC0|LUlC**lETDkfkr&+1=8>Q1V$`OklU*rapuvnpUT+9B zR(_75=GTJjRd$VbO1z$1gEc6a9==V}m9=n&R4~D0$gPAukJzxklDD^CoR8s@IhYs& z+$nvvT;MyY&M8&U24kHj()^P}o^j8euxjOsFtY_Vi6v&%WOFGh%B130Q%k8j7BCVu zQijr2oxxx^^B}HX$O9%)&PSa%Hj(BXAL16L!&n;Y?i)B2<&LlA&aHX#dhY=>bMRz^ z7{HwbV7-}N;cl+Hl>5*b63n$LqfzBNaiiM9lN1B`cRXi*<%I5F1f3s|ci=&te6nuBY`d^Hfw1K1!M_$nh`XNcZwzR<^h|| za@~wR*Bomk(9TKg+x2Tav_ago+56qn>AAX<)RkOOcUKoUq=MNl*7W8D^kFO4=Ma;H zP+26tbm??!7$gy805A-}@9k^SIlWZBHrI{F0`y=4+#WGfuEEF>0a_j1qBVqYcVhTN z_q95EziktN27pB1Lm-7KK?gEsxPlZYJs;_>QoerBJ+qw#1J$-C|I6L~7z&M`w~U&f=>b2>K^usYIwi*M2eVWZWX zmJ&m7pZ9pmwJ3W{loO9Qo-4U>wkobz*LZCDHU%qA(m_t&^Z3T|THbrJx4q*^g|`Vo zw!+jBW038dIGo;>rw(AAsLS(Xr@fW(y-8S7`71O)irQ??h+yz2+$@0azJ2?Uu zP5ND7!^X>T=lyp&$5U4C=r5xA3j;vscV{J)(Dop_L!7UeU+ePB5P6RbXkyL#OmRMZ z`_%|q4G9?3EU*UDFJc(;YwdS6>SHq#Y3IjBDMwSsjhyYnW#cJpch%)repbvsTk6xO zKW?tEZMnYgD2&n3 z0F6fOP{YJTsaRAzmoZ}v@R6pW9)WR5Q!Hv|%uHoI9RY%Hoi+s_c#Wf>L&(HrE8$xP zMU)wyH()QYk*_r3e{^gxvosrsYBw5a%#96N;xktM#s=1$a*UHQ6U_HGOy#0-gGr~c zRmf@Aa1`A#@Q@ICNu|-5at<9YM6Qts$eBuJloAT6(5#mNt}ZRs%v4sPNgAx7sZ+F$lnnd2D z=!J8GX5=b*eyzN<&5W;i-K_P9NNz7x&;j7@O<$|YyipxbBTyKRf4g$_PBwdR#66yI zY$DY+h2Y!N_}y<0P-hq)C;fi>#mD1M`yk{o_ie2L|4A$lTDuOGi@T5s7l;k#1FTgU z(NE3|i49nI`X>p?TknDO0gNs(hCnLtZ_;~{j&b*Juc+K8iHoqun74W4Q_Ej7=j#TJN3aTBfF5rlxfzx<*XnkeSlpU5T#OPDxNXX zl;a@pbtZPU__Tjhg4La4^px! zOOBr#Ga*&dz!+!LSXgLeX*hHvJ6^<{UDj~eFV}dhRa67Wak%x48d!PHXmZkXYF@5R zM^4%CvbFp=&zl6=SmQIA$Afe*HMm#(_r^<7iAk4|%HGLbNq(fr6Ng{&u$25JrTO-Q z6T7eMdMk5{%ORsx+JD0C_4hY<#3U-|tH~%Ec0lIKv3`uk+tKY5*Rc{5@A-?`$LcrO z=xF9Ti~rW%s-BOUt_lsZ{yF0TGsdb*(p<*d_EafPkX!v(bV*zMRY2^^)__`?Cee-i zIfvSNbXqbk+Gdi|)8i)-7uN}LPQpNXf_6<<{BLrjnm}WP#<2>iEbD_^tTAc*Tx%dQ zvP!&XfX)5UxY1j!e$KI>Dh%^j+V%c%ls$hle`M6|Q~gctZ30ylY8Nyk6^CXqZt9&i z3XR%Z3jTvL!$=Rcm39hJ#x_PWQ*%o$)&|E0_x?LS&Ydu+S(M3{z{IE{6nxD~m%kLl{h|R)z1m&F?Bc~`494X~Pqo8fu!>>KZ zP=!)sNKAlNl1B*T1W(Nyz}Dh`x_nZG*26#WA)^4YtIX7xQNZi9HC!1n#As327;d--h8L|j>waI zY~1KRsAkGX;yHwY`G36r<%G(O%p&~%>&Iky&D(ZLLTC~>BB;ei|zTScXWu)lf;FS?TF>FUS_ z)$X78-a|A(j3@ek61_fk8|DQ*RF}OCJVhXtH;LEpebEMWUNCSeOwAdXrYYRd<-w;W z^ytzO%dbw?1k2GNg0X^*kMRVZequ#!Ru-eOIxF$sR3hjLW>QY#y1JrfgaRSb@DxK3 zi+wPd$YzV$Q)Lwr_*(>z$TL%;pMHw!H3E2552qWf&Cfs7WPw%&2o-Q8P0nHBBO9Tv zt8enB3iF=3X39c?lpuL#nwZSQjZByzLm8~0FDl{X5d@8J5N(4Edt%@PqlH*T!FqWE zZGZj-3}#W5(B#VsHK<9Ze5iyW5r*jvdi^Ei#s-kve4_!}+Pr50s(dzbDHlicRZ1mO zg{}7}GnTR5J&^dbOuaa&Fa^mCs=ViHI*e~f&l3#tcwuhUkWi}^i5~HE*~-l8!dm)J zn=U#fHTrpchUA(|p(H&8#nnCIVHD{JJE1=nLP7Xg99BHmHwr6n!{mKlIk&Y{efC|>?A}>kgG%SjBKcHN zeh8zJyxP(nruhwTAF4gr={Gee?0F3vjK@E2gy~Tp7JFM<$sUPHyFf8D?{Udp#=f?X z8l64H&ll`0<2RmZ`Xua}!l!fmsG#wfnBI9OQ0T(gPw^6EhYgS4tV-{o+bidvQ%|hF zI2gajZzS$-^`-g!Y0LN9@90E3+p;gr-B}SMrc5Gck2+3^In;hFzi~;~-!X0)Fpxj1 z?boMykGcgjn3{g96GazG89(>yx#RCnirFksz9u?rodwGvN6fTM4ofS#CiMD)+VW2E zJ^fUcnyDP(r%Y-Xm1wmx&Z*%nN4$xhyNW=?>3i-T>g$`5=ASY(IE><@F4szq$SL=l z1(fwgKM)i>;3vN(NHxcTbwlE{{7z$3KTXZ;Zm-vNOiR0hWf|?fhXYZnOqFNnDi+rw z$<0kjxlXBMc8=FM?sYyj`z<7zbgBum1PvNEk`XhuoMQ+@Yb6KGT*k@Q$bw_hOQxE~ z!a)Y;TzTK#tjYXB%O~T)j65v?L^CWAVpX^2-qZs6hE^%+SoBgNF7rstmiHk)3w*HX zSM#dMEk2k4Cl>$lrGW))CB`uZ$g!`75*iD33J9%vKk$Km3HEvr#@`Lhuice9j;ox- z{7P|tgu-0WvM+!_q4*fT{IXPiEa~1y%U`;m;5Z5P0?fw`)pR$rPE~yGYMQqPfd^RM ze?o*U!`_U{tEt|U=wyIfUD1oF{!QS+nG0>KUv+|+B0-DHpm|NH2pO(<$AEz8f~qt9 zs!8a5Fu?_qxl@aux`EI2MJ*{8B4#SuqVh*=lpo$PlwOaZ7%(#i&7?W4KKm_-_7qHL zGE>vC-zk7mnGt=BVtrjnoRLbJ`HyChv2F}Lqn(jx%>yUM6KT;;xqEV|(UUV{&zVKU z`J*2Cv|J2kMc8suj-?Q6AJwFeZ;_%r2I^;|Gi!piB0`I_TieyKLN#VBnK+y+#hWHR_K@Hzt>L+-njQxZ zCxioEM$TZqTr(`u!X#bN!9GM>=(86)NT*pW=dDL6Cgn*7BUPUII6 zPZf^n;hM+JwklmoskfW!%Gy%5ti%g?@lBkj-|J#TD5oVeA_}8y@_3a8Sa^A-kYtp~sJz)3V0CIRYQ;{a# zGOEAOUbe{^!&F1t*f?(U-S_T2Gq&lU-TK(}yU%X@Y3tWpU%2jvqtRFW?}s^MdooPJ`(13kH49x zLl)iJJu58vj=uGWJv}=2TOaS;LWoj@l5zx%zq4LG>FwsnUlp9sk&3rGuWuKOiFTHi zJa$Sy&|~=~og$!=5$AV)ck)(!l?Rc0YD;bvDS2Dj&dYx!+X<_EJt@rIvy@@{VVNR2C;+2Oazs7$YgKLPmf^T2(5xkrwBw^WmlMQ~jnzI`76 zPIiDG`pvo10+%y2zOZC$h!*ciwG!B6xP*<}A~^|d7JL3)+*$J6W-=V^VL>LPY7}|} zI4F!#M-WQ@(gn8lFb!LQ-2b0E5D`LdrR{V2_SLQ~sc2OQvB4Z{<~Q4UsJnnn5Q10N zF>9*Zgu{$}ONSQ=@@aH;m|1cA#~qwAYE-dIOcphtB?n`7gUOO*%9j$mO@<%^l#DQO zG9_$8;ZIP|K!$gwDW6TJZKwj2=uOY50%Vy6SK_1YHhuT_u1nfg@jDp^Ep>HOL9|T8 zix)r3WC@1MMT3l%ph#^ONd*_{;FoZrjyDZR2pv8k>=&t3G#WczcwTp*icabd=nHWW z6%b61bB{NVn01@V)Q$dk{&Gf2ZfBt;|X0Ovt#a!@2?I4y;vxe8^as11SpPg|0QdzT=;JS9~=m z=+JM1YyxJS-qd!})B!3AB1|u~anm|ME)jOtZ#H*b%E2NQTqo_cR(vDxRFH4uj|&f9 z>ZZ$D=Uv|R(Mg9xNMG@SSsu;PyzDnS40ei&DzJguVZW$Ur0IfbQS_IhrUSwl`Ea?FkaIvAV=(nD=cOH#p{sL6F<6K$=>-P^@ z2yDu%t-OmRW1pO?P4|zo5)j7MX|V%__+YkIM>Q>@@Kr~(ir5vq7+S3&a58hJ7R15g z9iMDZvyc~Ei`{Y+Rv~##`?4=s_}Tfqw3yg>+y2l+>$o#TGRyxivLU_OqG=*1`>}s$p$nT45QonQSvG4r#)~l?3pbR3&h*o zC1XeBL6#nY*2%B$PUv^mwvao-O1#G_ajZzI*X=ufh;h{xojnsE>2n_f8* zLhvil??^FGai;|~AAD-_9m_o-ReLKf+g4__TJ#U+SOrlVL=>+A0|IFP6XIC`JCvi@a0d_sWGJ$9g2tHu3PM6R zsI6;4G+Zh$EE`lZ>C7g>O*+YLxHcQkd$R47XORdBuy$o(xc&GBOJm>X*9LZFVr_eU zWu{L&qEwJwxl@H=U80}+M95y`X(2H6!YYg!r6{E#FlR;1N|Qwy{D$U+;g_FF&TbQ{ z)j$yBWr>$i=?uf1Q76X7X!{~@bv8ClnxH*{bf9VHH42Pb#L~9CwWVFX)Gk403q8Aw z**ZLkLSySvt9Dt}y4;GkMpWvefR~4MFL*gk!gh7bGN;aFw#yVb)NV3GnUpUAO{*bk zrhWxQ&kSI(u{A;JX6RN0B~qCjSLNW0N{%7D!%*UlNdOkxmOup)&G+SVtaY-$d~yl zGk5c7$IBmrXB}`&ZIXy5xld4&_7O-l&CVxoc_EZdau`Ik@=m;EZnn+`d=gbt&@+6 zb??|@8HV4vWb9Kja-RNDdbr@{-35iA&%dsA@f}Pr5@h)A0-NSg=Ht`COLtCf^S97j z7_2@yU+KKDcwb3R$;p2>Ot(n^B`dF%MeeH{Agz^L#ICau& zr~MxD1A&2nP%85vGtxfxR={=)+4jtNKBxt0-q|q1WRgSYzUCx|55AgByTQFxJc0Fc z=AkP<(*@7nEQ6y8PI+CXF1uVJzR4X0O>w%6m|i_Hb@4hf94spe01ITSG^K$>xTy%( zT$-Lip}k<}8B8cYlwpNlH5`aktO}4s8vFq&HhUh#L>7aoQb4g(g`}^l47s@&7@#eJ zBNGVakq#LV|H_Avm|Zb=4MUCu4c8>2>jNWK3Dt2m1Xd}g9Bd}TJKVhl0!CZAsYcKy zbP!AnZs)x4q4@X^OvT41zs^72VQ&BF$%NY}J3Xupb#!*rbe^a?KT;X-#d!2S2WM); zf5tx!iTDerg%fk*tS&~rak7OIcPILFh>ycXfgs-Z1%t9tpsuhQ+Uxnk!Bg*bRs@Ui zsXA5A^}ROTBi@>=rTHHtu9+##8ypY;+_L+xt`_&qUje-Fi@y{3>4q#$9fO}_{w>Wp zt>1bmsq&)0Nh542PKwixW(jyw=VhUYrKfbWr6RV0ZQdp+2wbo+jcf=^*^*9sx@qoc zsnsvur3Ffdx+5p6EmAg?Zr;r@|7F{w-3NnyaTZ!doG2s@o~pJJeHnAO`TF3&PIrQ8 zw%+_J}L1hRzaINT6BI%%?KHjyRQR#*JNAJlFXpZXhEM4LkUkyL?~R zj;9=Ccv|)soLDnp@qG8^Z-d;uEi(22r&eGJ6F0xf#1CUZe_~J z+k>x=r*%uU53IC&+P*$nw|qrhBq={kW*{x5$siZ0a#xuF5VTjJkms@} zK7i9QnGE1SaqP+DSR~7H6<$~|8w?EUu%5|e=@|+(YLNOFrqx`wA`m#QT=_7SV#+l! z5IUon`elGEue>%;eH138DGJMZ)OZDiqz%oX$g3Z7D}yD|>nTjCN)^takY7-Iyv~kC zC;OfD*zTD~4nDDdW{drvD}(=RG%eK#VC+fwK3*UYDPMTyGSM=_NRxSmFe!RHXkqUJ zrX~KQIIF@Cs3anq!qDUEBYxcH=w$iq;DM7t9gLHi`Pwz=wLzBmHqUIoynm$J>}Qvt zANL>{`)HELh2jeRTUvLg!goHz_jywIei(7^4!gyjnu3oPJ+^A%jXZ(@p-HMnx>=V! z_Q|P(e{c$ZSMcMy>l^Qp^W|n{YnRNpTk`+y_Pw2#Hx9a9nfdJWW%8+mz9;)!Jp6Z* z)@a!JN! zEbF1&9Q&C&qsK;LM_<~)sgvZPu9XUe4IyTh>w=GOF5?D-rH4gzQLTeRJQGK1HWvOW z3L?1&Kfukur1O`Y1Di@z18Mti&jc5|vi_))lYAdcO;zkl)R7b5()OO1hj5xXRqcl} zkr(L3bGND6HTv{M+5m{gp3krD{%_|~gAH3oHrQV%NZyEmTH@gW&+Ov*ZC{1bfIas- z;p<mezgHjTO<4E{zcSJ2H7$)> znluS?jZmFX*a(0|YfZaaVL zQYmmrgg%uF-2f?ZA+p}nCb%17Z?#XiENRs`wXl(=onES`MM;=Y+PWmrwYG7Znuq(T zsjfqZgmwQzKlxXo?~(0m6zfXY{?!)v?q2gyn!^Zt&0~-hC+zBub?7WV^|zSW-|0;X zPOO8&7gE-IKbHSs%7sZ)7cXBeW^OCpkvfTDb zcU`Vo!)NB_zgchfjpfm=%)WFI#jR=N?$7^ms`0U*M>n<%&u8|6UN7rDF52j9L0kX#dSr z$g{0L`Seee-f@|OJE}jz=HJql$CE_0qKCQzys@SQZu4N;`oTL12P-Y3ROh(a6}~jN zuWPzC%uQ0>4h+ZLgwj#?yVAoW+H_PmLsf z&CPi5$h}ytz6<@fN#~Kx#cMoS?;E{x|2u>4iR46bRwV{6z@Pv)O7DT?RRanXF_VcG zg~{Rmv-VgWG2V7{AaC91g{&?!Pw)WcAHZ3fI;^pN#0fM)fwpzY zL^voQ!kSuON5nQr7!N^J(<+s=n~ZG|%%zLp)~Y9Qb86KtNw6zI$%082Cy+=@0@N>< zcx~+xai(T^sn*zPY~q-Vlcpt3d?O$XG^`V-E~t6nLz!(A2so3I{A59V?0EErLZvN8jFYB&b-QQkE?%cdK zd)<G_dT8*%=&F&$GK}~@M`Qie~yFR=UwB{`JhBjS(>}Zh^Sn#FwVCciD^}APS$Gy+=Nw}oU(R+(yjdz<_ zw%pK<^QnmCWH!j$8{k+k#|yid{NQ?;cM>la;NM8lU&e>ZM*clxzI&=F(A_)qc!t>i zihJmLfR-s(ovV5F=}>fU@e(I!D|;lo(2tDtaNzA;&}9Y^GyqYp7$+s~7LMC&czUv> z?EqdF<8Vd^S}PP3ud@Q7AdU!@2zBuGWD21ZNMV&VV%xRcZmLBs4iN!{6(n1_EEeLR zMZ6@NXM{uYl6VTjKwdTsxb09Qbg1n?_bsk)Y~CfEme(#_Om3N;o^GoF2uZk@)heiI z*S0L;syAsQ2_A=dT^J8>=#QX6MUKN={%M-;UeC3kIarQN*j~Ap?7QX|42;JdHkRh^ z+9I`!y#{{&oGJdq>T>m>r$Jd^QvuytAn8w)IJe8LCA@ z?7Clfo}Aj$dDfxmTF2jG4u~ww&kQ;B7V<}5LEEDZnom3!{qSb~T}e-s!@Ht7$ERtD zj_0dLR`!MVT~8Oji4HYOb0~UzdS6YECico7cK{=v>_-X8dA9jYh)c`{UhI{Beb?-K zEQ!=ofI6VNoG1Jw2Q3mP!4#HahtOJ2OBUn_uu6wM&2E*Nqt0S&p<1h1*FCw_QHJzCYfe7HKKIAjfRl>_Ji9-eoyjat<`PcSaeyrvnXjh#Ji4vnUsdj9B=38 zMn2O)c@$UsF7MEU8y%mLE{A|+c*6Fie~@Ua z$Zs@$%#d|f(t}3OF%~OvwMkHH4@BW$_peH4Km2gn( z*ozHA;$hj!cBFT$S+obE-gW=JvHpF7>g?@fxU%ZZx4cigyr*vM!v2#^_be8+)ztvi znc{uwV5Rk&w<(1&;)B1=stank8Ehxgrt3Y`PUMg1<`X}{@Nl7uNFCcm^>NAbVSWRt zhfx`dPwt71Lvx?J86ah>;8MpsgWGV~RqvBJg0#qn@uq7cKbAURvA zfvE>7RvubCHF@y6X=_dVZy^k{n_x(jwrLmH8V0d}Cyr?pf^xLF$yQ0bPKcyvtEqMg zX;&Q-1Cvo~Y|@%)If51=$T6+PY`7qKI-wTnP|OLf+BT_Buq2#rHEAZr*~@}8_plfpR^eC?3$t5 zB1+r&<6xohXO33h8%6~7-Mhh`_Sk$o=1a@_J5M&PTiCE|cSl&zFBU)Bj#&S4Y)yS> z{vV1*Y)XH(_NL{?mZy|8jU^r4=!s!j%<>jK>>Q#FB3X-8MGf%`-6K?}lvXL_6UqzRP05EpDE4`|mu_|U^p0t3{rGc*jo z*bi$d37$y7Egq(6wP>*SJNrHN7It(Dtjlmn5Bd`S&(V7d(t;hXcai|+LO3}1gFAki zMB-@%u@L_(Pjhv`M8dwrgVku}$Gz)8WDa)Rvu)exwymT04$OFlna6zNI68A{SLkPJ zjq7&qDhZ&N1bjGkA2Pm{P5XI=KvXL5xB(nI$f}yShv*@itRGCU%aM=l4`?# z4#LtZlP3z8hrzNn=viz!+hB@RzLux0GEs^n52qlFAVYedN{7n8f>~)E76K}K1ZXHR zvBB`f;#CFgB2|S8;3|lUa>H<78t7c~l~n^3oW)f-eAblTe*7@i3lRslBEV$wlI!&W zxo3rHh-fgK8zp@9Vm9`{jff-qzI3p9Hy#|a@y?mx=!9k^>}`KsKgsdkjVNDXaKeG2 z%U4SaUOjak58CW&-jLw>=H&U1w2ows`BMJfk%ErBjRh;gW`{Pt?aM#c@ps3f%T@>O zoXtNU^vkThIi*izb(gm}KjdA}=k_+gmL4v;Z1cqOj%(!1Yw9Ll%(pD~6Ie6< z%yadcS*k5tx~qvk2Jk~5bmNL<&(SyBEHa4WZ0`Wh?_UWm7ZsCICNX43vpl_@r zUYSMiFr&GgY{88q_`ZAi2AOq#K-Vr(LO{fyyT>WrS)zly_37Pp0&DW7$*Qy~=UoOi zjfdMwMBmNhe<2f;-Z-0)+Z#){uAA~drrgPh&xm^*O@+hyRi>ZsrV+DJ$NGdrg$thy zARjTh{U(P@-E_AQ1m-GoSU=fkKY_BTHz<#XT_{%9-sR~6hTcb|_egkX!U_E_V8>G< zv{ND*a110P61V6$08HrDNY45PfHw}h3*PjTrO&5_xML5&(PT-pYk5y}P18#}jc##K zxV$XHUCvYs_=mQwR*2p)E%;k)Ez3(hzA2ghWLuXk(@bp?Q~OFGtYf2!{F>mo0ECY z{7bOyc&BB;?stWvFC1)(cjtogx2Jg2rE2|^klQIgZ)(8$bz{m8mbO;+{$Q8!h2>-^ zH+J(|h_!pG`X&b48))U#7uA(-Q~p7IfUGr(gsZbOa2H177%q(HNVoJ&5%h$|L*Iuc zNXkVLbik0GBT(ExyIalyRRW$bD8RU5Lo@{*H~^>A1ssq5q{nl~za6*;eM4DKOHOPU zl0zd13An()k?aYVl!vdEgVmmG2P-T-{nBctcROh}y?HA}<(RFc-DhV!-xX|+@C-Nm zqQK2}BXiw3ier2f)}>#q`@t&1yKtk+sX;hnj}wXSsq&)wSbJ@e&XKdV+K%fz zderTt%tXNLRaY(>{TWuN)`PYg%PZajm_`f6aikwxb2aefY-$TB3K#=EqTsyrtsPD?Q>t=JL z&81ZvZ>RKH|5EDjU~{&pF<5d_Lqn^juG|bn;?QEKUeCakoFa!C7kd zMaxRvN6pfRi9Xy34@bQnMKyEVxK!k%U-=9s-~Gd?nZ(?a^2)aTC;I)7iZj>&8kl1EiLgr%91kQI0qLS_WY%H$u;F*?8 zI2(09%$XLoZJcGS{skC)wb0en`1O{h#8nTMxVH8dzShJ~Vwm_kkO8FH)@fZU_$mTT zydX{^&2A~>l1T77ClP{)vixB=pNtm90SDjt*~xNXXkbkT`)k((4R}|A9ZedWyeoR} zCYM|hcbw=x|iEX3KClEB$xTVrydJimtyMAo49L`bz ziN|Kyx*w6}b`J!6;p!)}@6S13jC?Z`?$|AKOZn#&<%tYI8OJ>a(27OpU5_-t zAEFkI;D55qVoH9p5T}TvIAb-|KGGBE6t6D_Z-P4VRgSHYz{P z_orP}e%8Y~x62pFB!s36&Mw{=D}hxcV0*zem*L?d5uCC}?^{s3$$8`iDZc-BHgAV- z@ZjH?#LnqGT;ZAdKIw!^M#{@1&-Y1qnbTE%v@*?vidLrP`$zRHSLgP%NO;2RbB3m*I%wh{D zDguFaoJzk?Q^VJCG?_`U4vnQ%E|Zl$o^<9ezkV1Icep%0jVt+`cjKq8rFCBmcX9P` zaYHz$BCWfzdGJ?d!dHI5r&4z&1nnp--W%qy;hm`To0pzPE^(asNjrSH8{10KhXMs9 z@|5XjCezNiLw-tm=F|(8)rmTNdZ$J-nCF*LD5(EZP~MLpw-yW33<)O#7@r5ij0cPL^FuR> zn$Y7}TvxyICE?@`8M6*)Z_%aFRLgzBM?(tGQhb3fi9U68EKRJun z4th_Uv>m1bJY`_ZPGi~Tu9m2$_^oQbCW{;Eh)HY(a2LW=eodM2KIePy$HZv?^#x(( z@6lD2lQ|J$H)?Q54t~B5qNI}} z0q5yXa^^YnwvJ|wK6{kx?t7vhrV6`JK({Eth#EylzXy$i_MV9Zp`*)wuqZG>WmTq5 zR;K2_R>6b(i1Vsvyn8PIggxU*xKTl36gxTh5Fr1HQRl$aZq_4v_!gtlE@T&R!@sC5 zhagJwfTyYgp`Pq2;C}8_le3Uj41Er(yEo9TL*FBTM4qKMr!ph2pWgG*s;9JKNbXjd zcJw~6dUQ*TdJEF{;hyoiYv(~N>?4e;u-8|n0&_;$UnLtr^oKMN;@*&s9K2YUEzJwN z0KhteRVkYWe{^{}#I0hv#|-}HT1;wqSLHy8ksiQ-j1HMu{1_TBARZd<{VWcDfb4g)7D-p_0t^e1Kf5d#~y7*{cKARK231OnGAr789E3Dq-msMB+ zZwTz`7x)?)5qnW?;9@g*CuH5PgFhr3;B0qUd)Q$k#}+@wjjhV-+gJs`GrBDP^5SM1Md*L7uN{dmX@ZcMW<0Xsa#R`b)20K3uJ+G zjt`}LPnmnh-1RWWw42rG;cGu~6XpkcF*Qj>5_HIq>Za5qQ9P`0b@;2ME3>7^!T{6` zXBYeq_fvHx$|77^1iB^Rqlu1~3+U6azUMmPo@`y6-t}AwP+i=X@)Fv|uDPqoLx#?f zn$V;3;p>uczg{)Uyc59$7`?>J;2+vwu!H`66wH#c-}aU#qmWB&A$D)OFcCm^4& z5T(thM=O;pN?gjK&;o*>P86cM@W8lFNtdyBlmamZrWbmv#M68Y5xqv*c3GnlEG;I% zxZm1}9nn%7wwA4}rq*^?OQAiYqXj)JMz+=jrm%JyvtEl7V_usCkJ++R+SH2M@3gVe zWXxWw)h+@3C;%1Ph#W8e6)duGyyoGMZR3a`S@Lw)Q?*J!GM>~-FQYmHcL<&^N^q+qNAk;lpcbSN)H~w5qCO+4X!oiw1yz) zcpitz=fJ`I)Voi7%a(Q&^SRkV>^>eKX?Q> zzwN`aitsuG1S2R8Fk1Mg(lT*_!joHLOuVTsMYm#BE$Y2s&l-rl0TxRm%&8o-hKlQh?r`nvpI#~ul*bqXS z2ume8`^g4+p_-yScYx)9VqHw67~0S`aR~Uy$#O1o0^xYrt8)8zO;v$ph=Rz!sjJgu z?s=L1Q9ZR{q3&A=u9s>pH-j)}1kw!*I?E)N57Sv`DhBdGbPCf4iGK!4+?Xb`wkn3% z`dl}wJ z`{hs*>A4EZ9BNt6ak%o5_8R<6JxlqTHSC2d0Xed^OUo@h3Cw(PZIf-{>#2=ZJ}$vY z9P+DYl#7q$Lo`&;h_Bw@#C-P(z-kZ=#aEtT+3pv7x^c4P9WUm-^66qyOwKKibA^M8 zBfb}n#g3)C?Kb^P8wE(#Lnm4QuKwH2W>&xLbcv$dRIDA9(|m?Ete!GOv=8A2_%Ljk z5r1Y{dH4nW_#!1sShtc0q_umUJ|yC%Pv^f;g8rFYw=y@A*VOzwslCbSz^EPVGga50 zXKIJsxN${0^){@O+L=bC%@Bb1EE^SVkN9ShA9<#hYIOpY0oHR$H*rS5iks0M3GqN1 z+O1+up0m+eTm1FHB}uV#^jqE*QF)Q0!G_xPGO83np7IgEg}bQ;a%#7d=lz9+9xqTl-=LiEE6JQ8Twzi!!m zCI4B0#V(JGnIBLdcPizIyCc`BmbaBR(e+b^uNM$~DqLj8rZ}J=5=}fTPdG8gZt};T zsP8`H0@v^A-q8Jomy!FfdK0%vnwJO5i(0H%Mt%D&-gGSwLCvO?+7`YhTcQJM4huT4 z^5Le$E1Xb>CX;D#k{}KTlL<#uix5YY4uw<8fF7D!jatDpHYo@}VvK@R0)m~S%?Nd_ zT`;L@(FkzRpfP1xx1?SCR%{ds9;btgg1%QhMWJBgl{Em$H4tP}gm~F2$YHMH>69Y_ z-cRjE8NE*W$_Xhg@`j_lkFS&49P=O(D^`B z!y!g~7S-20j|FgE=v)Tqg}BYdk`kId zcCi5O5I~3=hBKxrhFiHvc%ZUdltUaU@fqLE7>Nle#CG)67eGzGz|+jKYmSd$*>c>- zCxX8K-nD+dTjv&`0nPRF!oxegpk15^$9?rJjp5V{P%FM!fXveSv^O)agK?6PkH`GJ zI|-x=?w#4g!7}_N_yq*6DLRkwey3ifr#u1L5LAM`gIu}JBLht=IVcTya(Ao&N1A(# z_;ZYU!L$C(%$iN}(Q0@ES+Bi6FIMKaJ}qAudbWTXG#6 z1d&V^kDo08^;I*C#rMOu8ckSk2<_oA$M1q*oD!YIiEEbIj_u?49*SWGhvX!coV^n= zlz1hn0>jVd&c&W;t#N1^Ewhkt@F6%2*!k8ODKRo?I!u5o* z%e5#fNkFa1mc6Gw-+bdvh$Z|MfEU(!g-5=-y-O;n`%7bcMZ^sT)5nrw>ijd6N!zCH zmj{Ii?l2=RMaokf3;z)xFZx8^uP~1T!^LXu9bcof!hxT1mY*jWskkVWT042s`y(!h zn*=q5^qg0(HhV89X3iVgBj8BO0y_5_rwjd5iW~=V(k-1eg=XqJyl5)CnNFcvSSL-; zm5#uyjr@D|;)Kk?ju-m6^OTya)7Yek_lX^Ye*CC>C@Dg4OP11_t5SZ(8V=}U`6IpE zh>z*6mjNn#J6Ru~+z$ec!Rn@(qZS3SsR5m}tjM{ps`_2zj0g5v85?ITkFK|}Kj=M^ zxc*AU%=Rlszq|h_>TLLjt_L-g`IFS5$O)N0dFH=>{#9(^g#v0)e+=8PYVv2~2gplW zLD~K30?)p$(r4^CNZGmeo z%63*&Eai$pHav{?k&L#w@`3093PX;DqMDCJdytw>o|lp5-~gXXf`Mf@k#`*|5IooCm`*XZ4Mn15EE;dr^r?hPi~i(@1}n^3~2 zo*06^GV}pixgz)f|BG~$-zR(^d`kqF$Kd;toR^o38q2hsx?6x%sCk#T%Uql;*~F8| zf?1s9XK-JB2nzvtIdB@b;Ly@!bJW^pqu^3|Te~zqyQXF_OWKauZktdm5Vp6pnP70# zT|!zcyS7cb3=vJ(3Q)kb77!j`Ya0v=xIdsMWEzqh?2>>Vs-f6|rKQWlv@g*Z#oxB^ zmbH-GmelyE0QML!wV(~B2A0Wo#1nMWZ5%bM39Y(tmnX(aUXvzPBS_4uru`Js?;h`4 zmR05BnVIq7h9;2W*bi3#L`ps0G#Xk1ll|t5a@LQxfGtP2^w~+NnY~a%OI-I)S+kNe`;Yabct+8Z2KhEuq_eAqt{* z{@c5ee*VZ`gmr&~kx@LlN#3b#EAkIGm40?)mC|LMRpbbckLSz_dUx`zyAqLPp+)Bw zK%<1C!5Q;e?pqlZdsd;&n_bCDWqG|Z_=bVagU32{RG~bQH9DN~T8_95a_4!mqxX7C zzano+POyq`yd2Q+kL{MJ2RGT@v^ZA(@su^xyQ)+Gr4C2W9+tluPSxqiE3?x)*br&T z*(m5{Vvv36KO`ma)m-l^t{XY=;9|r1N~Q`~Y6w=$bd27)G#neba{0NHEq+A zIzfEPA}+ATwiZ0WS-dHM6i@4tG*82Z(wCf#lb8>O82GIdvl`53a~{BN+<)(ntpKWu zAm4+GgB~RebKeJ|kXYya=t|Y$x3sX+lO-}I3}B9LX<2uh#h@yB+t))NyhVz`7Zc) z2G3*kr_*3fYn(Y-Mq{rWyK>=RL+}Ce{)m#|n|J!aqTV30Nom@U^5e{_nSWk+^_ja9 z^f@;N8+^k*oJSK!aZrA9$cgLBb48{25jgQ4jH!!9x5b497K^@$`6?!E^i~(K1lA$CpuRun4tF19x8ON~a!Z(M(ud@zltB=P zKv-b1nxKJ0ByLc?ZvY}=4CJvi?{m8*qNzYhW?o77aAlBur_(Ivy4_SEQbwN_iaswU z^h2EgJ857Z23){YJB6s`Jl3Q5)yXiXOikbgCn_!Z%!Ov0cnu+Ll@8$J`um+u#vw}# zgCHWFn1-QDsUWF|(nM#!fnk4ehVy_!#IdHvtc9t687+k^9EfFY#_g-Q@>Mo;G|I}W zWRV=L%yRae0kW&ofN>z5wyIJgFQ8YcU}a&g&Kq(eo*7tv!z>75R4|H(FAaDc@de(L zDbVT33c%y4S}{>s{TMI|DwB^P*JMC+YgjLT$J0!!q2O_hOPXvoTv|p(>!NG)OFc6Z zSTOMUMb{IP9Dw{{0(XerXDQx1)E{xn^H9AtTo$JXZacE3UQg_XH8Bv zJM4V*>NN{KTXWCnvu!PlH?##C^j-!!?WNKxIrdukYA!Qk&$MP4LQTO}M2>!X!Ra{a zrF0^v_8NhNm!&O;$J!34hPqe@Z!sw#M6T-ShFOS&L`Zfa74!Am`FUvm%e*P%nikay zpDOWwCo=sOJ0Ncd%KXtUw^jKuEM4XUkxgC5K;5`*ucRuSw$WL6mz&`sskswGv%MTW zx@}L_Hv5#Y;>)W!yzIhnKo*`7Ar3>D5*;LjPGBoRPqa8T1!MK$cSN)rrO1d^yQS^t zH-y_EC3vW6Z#g&dptj208@^GvMTOJ6Y3&kT?!;<5L8jVv2=730w+R_&z~rdgq|!X0 z=35S)F>Q;mMVzH=nFO2_n@Ek30b{CArxjr0Z*SK$Ex~oiUIOB7$+Wx-RSdG3K)VF7 z%p?>rmfNQ!5>1=Qv}i<6vQeU0BF?Nfjej7HJtjJi^(>&R11YHSgPY1rA1SAzzFrY1 zpHl%sO&OA-nC_=l6RWR+gjJTBqniW=0X<1NV%X%sC64M!I5}br&b?yPoRhy#Lc#F- zc|PGN2TqF+;5U|pIFhNh9M`w7R(R6SM27{>ERNyC5C%E{rt@~dw)p`rR$mk^{Jux* zhZzGO!GY(BWXp3+sjL0?-{Y5&uY!hVG^OD2vQI z+$VM>2BxH24Oh?L?xEJKonX*WH!zz`{hM>ll^{Z$<4JBtSF7vTE#|tGX z@dhW1a-K5@bu1txGAZ*qCCx)XzQyqt)XzQ+*>oq|Wyg-vl6RuB9`TQE@ULE@oW8+> zEY_U!>Y3lW5_z;&W_GFb~L%MXyI;=^UR$l&K7lZxMqvAKAf{f!r9VVj<_?P!mcfH zO({ebNOO2?u|VL|Hu}ZF>0xMUtuw8({R$gJ*UOpKzzyNVXN)en%!Uorw5`8QTE->m zR-VQc6>n+E{rz39fFm4zRXV7Zl%&aQr;ByM9Z|Fs%YfzN%HODz43@Jjm8GPs&ahJb zeVkNpWXcnoF-%&T44%3~rKAr#k`Ik40v%bN$Fq1l3>%d5-7Q>4k#diyY~(@2Cn?S! zPZ~3}k9WMtnRVW!86PH>Eus_7z}hvQn%Ob4|cP(eXzBa7@oe@{UPoaPd^x6p__J85Ou?$zrD}WEG0b5St=NdJ(o($2O#-phPUM0F?b=DLsRB(4bV@%y)8i2X8q+`x zfR`!Jbpi8_2I! z4CZhy0EPWH$=Jh81B>aRFAt zJx_bqq_ckY8oS3oQ&b`3twS#uDPfw{3QtG(#jKG(_q$AwycA|8jnq?AIa|b;OCxRB zOBE4x*=h=1MmJa>h22@PqMD(n&lytarhpKOmDg-b#(?JwOLWRwveOKme;j5L0K=J^ ztaN4yIy!Xrtn}3`Yc){E_7G!Ww#}TBjBXkZ%-JIbOnTHp#vJIf%?9}E3tpyVWMw{# z>Tl(ptjiqH%b4iWtqDYW!N%FI^ZJLI1&=bD3jZ{(3>zQ^e@J#^<;*LOPt}UyyST-f z!uk*y-Hf;?-jixyFY?1F$XD(t$$c<-VSIo}3O+%AsK_wfN@oSI@Ds}heMxgkuo$!_z5L7Em%EkvGvix=4EO#wpT>TR_07m5HRRWR;2RQqK45{ zGtOLj;XWSyeLTAcUYj;vY^D=)ZyQ$_d#FEh-(-19g=MR^Z*biGU0hva zN^?-d5!VcaDM~uLqC;PL;%m&b?#Z^YmS(Qhol(3zn#HM)t0_L-9OwHA$*}!tL%TtC z*CadgHI6)gtK%c9)b&fF79m~Mn)+kOq5GqT_O#xX2LvtgMocavt2w(TN7oPS;c4uK zUaVhCvWxzyvz57idDLRI-fl+``F*|J%nQn?XU?9Vk#E5gwp&}Y+L0>|odmM^{t*X-A}x#Ka5V26P!_{;Re`m7hJF~Y{zYamj%FPi4fA%3AHF^PI*F7qMUzgAcrX&QKW%t zVi8sWz`W+puj2TnIvwengOzDi}#Y3-2%F1|13 z-c70n+BPlB_)vdv$$}&#T;ikJxK$v<5(ZUhnijx+a7#sj$Fym238gM@l1{gn5DZ8L zc1XLpG`%Dd7;!rm;=6z%M7=lPC~W0nxzl2rmf``G2u(aB2#oNv!fi4+PG_Oikx5Z8 zb93emdMf3j6m}VL^0ZnjY+90LPpffAsrjIv17MB?XEuKkznM-qi9|12D9A*0j>kC4 zx)4mAn8&;6F|aha$pA8ip!{KYR_|iT4<%}>5tY;3Vyz27jxD!H&(fU{aPt0RH5O6lPQoJreX4xlRFuRQYe{iqe$ z392c^Ah_j;#%^fonF7>c^zacd1?b!NdB$81Z?TcM%);~m3s~mooA;HYf`bgMy;zu+ zm&9vODh=eH2oIc6fp-?(quFpp__;R`x;r7o0h|?uentW+Y2G*6xHoSgYYhYTw! z)SNL;5d^{}?uflJ;kA;8k@-g)bvl1*lITN!L`BFOO0*L=XD>DiCN(1#?Dj=0`^NCY z7X5P!Icvl~ld%-TB&ZD!@h8qdi3KCVDs)^shI1amswG-`2j5ob5R$bV_>Z* z0{IK@Lm8__3{YX~lvuR(u{2xqa9R2UcgyKo>Dmn}!XzQb?J(ESpJH`MBLD$wkqyv_ zRv=xwp3=W^jt;V4WcQw`%^iPzor3Y|`mKBZgjURlGmx<2$erG~PDQ+G>#mLu3M57DpL!Bq4+(EI6~ZMt^A5%M>V zZaYx$%65cpcHM3_%OYjdQ#X>&IVUU$u0C}!sj)d{z!{0lDdqcy8k>!98P1w*!V@%gz` zYqs@!-r`P{@iiMBjgU|Jbzf~z&GkdllhM|yB^9icqa~+zPpo@oSGTtLJL{-3Z#%!p zH<shIspH>z;B^SMP`Kw=IYj*I9rV43iFA$d>1f9$$#^;M$(nookv%zxdK zV!5y>V*Po$gSULwCRyIMvw!7Q*z7#=95Aey-`StC+86Zer$I&AI{$DaW9@Na%BrZd-*(Pi4&w83Kn)^CnZe$+U{U6PRjZ*-N}e zep38iQ)|l-bV%5;v&1(!nVHG4<4jsGWx$UaNSW*2bE|gj=tq>=x6kQaE>)z>0kIW1 zbc>UP%~xEF@KTLtN*E8xR*s9XS1vt-o;E8^!q-2(No|DLJDdM)OQ}dcU_jF%Zvm+O z&Tnk%HXroqa7-y&o4Ib|`rc1Ee`j+M_FJVQkd>nc#dLl?JTSd)e0HL53b!w8Atp^@ zpMd@Hbiv>3f35Fr8Fab4W$)C=gG^cRSLk|2b!8jdFRQ;tRhdLPV zu#di%pC*BrcLMk3khd<`UI3P_KYC~V3++|wMiu5@qL!O)V^dpl`6 z?p!Fn6HMFZ>qHCQRM+NVccDn;24%T3Ocp#8I99)IH$%#~3?TwVjLwqymx zUl!=#Y|C@1_I2(&Wx2hv%5&2}i&Bg0=H})lRW^$rNEnYl4G=^KJ9+#YUkYC762AiQ>{$ zL5*B_4l-eLSstmi!Yjn;P0n5ipNl&WkuFuZIMi^qE02e6svm4Tx#VVs1jpvOPj|1Z z3*DGvEef+09bB%olBZByCQg>;C+3^g`S%p=+}>MFK76P1@1ozYoY0n}V`*Pr_zx@p zwPRcIb3qi_Uz0zo>Ie__yl?mPR~yl;&W72A)AFFjwP_7OX$y$#toQtnPr3R4(PY2R`!GDmhNoAGSwoM*PA94k;- z9}Fq@59f|kK}XE;HeKGe=csjDVc^;^@-L5znZi@;#W+!xUD|~ z_a%1hCRD4~2HJx~H4k+tdjrWk9`bAl(t~Kq51C{*D)>9|yc?pw5BHtNf5fL%`T#Wm zRU&BBft`0?pbyL~)VU+WfbhyPZbedE7H;`Ohp)N?yh}{N!mQe5lT?EWc_G0pLef>B zonFMP$s}l5T9yh;H9Vb3BamuZp9g{7=fYD$*pM+IEz!(zAqE?#KqKgk- z?N2Ap%_tep+>l7qs!Z?IZ;DiS<-Gby&Q_%>`U6$rk@9!&<@r(0ya|@2%PDCyf8oS8 zEoRkfwYaRquMJXcNI!JReu{$jI!#Z0^#TMtHr10jE?2HA^|^DB*71L9!_BUn{kz%!nbS63F@Fd@>@l)RX@Js?h9fm{tjJGc zm6*+ob#kjj%qc^7;O<0-?_B00`9m)!Q9aT3|JZx;zozee?Vo+$5vi!4SV57cRVb*z z2!dJxK}aU3iD;`;u|rv!3CI#<5rN_|YN%iXs1y=rg4*$6mNt>ijT_@aPI~4fC#N-CcC59DiLWSufiDv7HpRA>w(P0MV>D za0lY!2a@7Y!gAuxx~4x{+{|CWw?^hs=;m=ecK^k1+_HC~jLvr$clyV!TtvJ`52-Fg zB`a!e+#<^+KYzg?-F9!j5hF%4@Bmg_=o`uxMR&d^iVF;0&s`mG);;@&&B41u100&~ z;39Io#161ZM-r07q`woEGg?R zw3Tfta(eT{g;QPE)(_tEv~l~Q-n~>b(na&}FZg=k_RywiGscDO1Fp?ZA!HW7LPR@M z+aj9rHr8;rz-o-`FTJ`ReJRKY51+n4CQ*JomOuTGUfA0o@&a4vxM>1s^Z}le{08hl z^r3~if=E|UYn?t|f{RA&Rd(g_IgX_qtf@X@vNGR1&dqRkxaceJJB zhZY*OTqITzmljC%xbQDvg9hdcOj8pAjM588vO)zQ+;*Y~X@^cLZg14<(36eEL@~Yu z#$`~ECSM1cV{*F?2bP}GzSeTT&JbsQuZ{uOL< z#%!zE`ipzb?>Yt+H(IWR~CEprCBy2qJP?_=3YH?mW(Y*0zr$ zuDg`00K71D;4wY!F-_?nlDD^_pX~n4 ze7(gv0G( z%t66i3{f(&TWM7KW&>+ri+U-0@3Lj!q!)A2x`S(88_{PrvfC=Lk|NYa?h_;QN1eOU6|N)M!PCz!Txf%9 zZZ@R);;vA;cgIhZe!rn70YbzlNA4$CoU{{e++X^yt?=C!dHxJ-$o9>7@6yKTUb&C$ z+mv%IgJbIM(+&ETxaoj$1al$-hOFKNa0>)$N$*wDr=f@gW#|=N#V^?QB@A3wKbjlhrFyjS>T~qP zkyJC-91{L6!0-iVVF$a3CxT5Bw@-*)KjZsCDcmGlh?YA2dOi%_3eCVp;j!L(3$kh_&HEH zOwi^bjm}S)mib5O=IfSQdHM{|n8`HO0nuU}Y4C9gZ-kLR|Mf}mqv`2^_#S%vo(&0@ z;Evc{TRpfZ_RSq`Nh|jH?clz8Ez#&tf*%)C(K!PL4jQRx85j^k`g-s$({ciKj8B8n zuAi)$G5D^v{QdilK$d%7{F7O?`Tt`TY<<$Q+^Wa3$@?>(l zCO0KEIw(m%JHmT;u&N$AQ&?g0D8@*j7$E<(0<(qK)a{e-$V@?oEh$8vy~yb9yUJ$p z-@8^>XPa3$y`h<@&Huylq}BiIvZy~}L$O+8oBzG-A4e>E)|8SwPF2_D2&=MYh2F>4 z>+S1M@g!`h%>HMEU`1PMv#b4NqXT_&_~w+B*y$6;R)AgTYL_459@*%Di{ zx$B8DfipzSDZ6N~>Nt6~ncHko^>yFF{6u4pw-d)7XYic|_ut5TQTinB9{l$gUYEMF zK8)@yo7~uW(qH^kZ{@Om$0YEWf4y2I)8jvg;;t`e=kIDtqU}0%AnwuF^l7BlQz%Mc!AD>sGKdceefPj%nRr-Q$ z>KELI7Yp$F6^gu78q)2tQ4LAS3P1n}XQr0BybJ~>q^tR5SqHS$Iy7`l@aMtL9+!0x zP-DHca9qEztQSZmm}>PMjUcQDFf%OR3>}9-UV_#jOtvrpvSFo{$)K}K3rdLWZ3g-| zI%Iq#Vf8JhMtwoaVZqVyd4WD-49&LsG1(Y@Ue+NxC26}N@E4s3st3g|hCx4b#NS+C z>t>f9Y4+YyXchP4hNu6JS^b6g7Aku;uWOMPFS(b_I$hg$X@6~i?V7h<{{Cp1Z|67i zh3`?khN~FmY*u>JcbxVySzGl8%r&Gld(YRVs;)Tj6IWk7#I*GvJW?7CJiPbh-)!u@ zwfbLS=A0E5C|!@%pRvZ1t0^eUvE|}dO;>{h4^bWa2EGtibyO{{r5=hXN@B$Kg?WY@ z$~eTNdS!Ssdf8q^@5S@8QXyX&CaIC$M*#%Adj7-kj=@A>&>*SLOii-2cl%`4zHN`i zqWv51lzo*k2P|}U*&7C#7OEA^4+6V#+)5k{j|Z zq0;WuY>d$^i}{@iWw(OJ1)5RFPr|xxs(3WM}{Nb@~`_s<{*RFHP zPAl8o)MD4=8__w-ueq8s_tl58heJ-&D)-@aqq|52r)#*{8eJk~Cdn)O>}UU|=fYh- z+=>c6ygH%phJ!SaXb1=@IqQY;r*Cdx{esrIyyx1{86%DdL^JbL)BW-^3cBCqGyq1D zgBO)*g=%RjiN(%AF{5sxaRPuAv@N#-58uK2u1=bP{>BagWY}d4vy;iLOOP# zV_C=7!|NwAp+NvCa1#fNmwCQ;L9av6jEpzO7G)iopAat)IkvI!WwA`qve2ko*3NUx zI@SQI9sHY|iuNNW_K`HET2YK0Rw~>#OjH+u5gXAJX){7ud#TUrch7ay^3Ocqd~)2w z%_iOamDh#!$Hf*0*P7>9+zjyzAkit8-{;W$L|G=@94?rWg?4<|@Wy##?aSx6qc7_d zAN9%mBd9r^aeLPN`^@DkkDjj2{;}o)|H6M+_}E-_gi-U;!D6$!W!D_jH|;Jc{e45{ z50u2JZ+L4MMCR>&0;WlE4XR4fv;xcZR`f%mYlXsa*0jD`P5);X};6^g!vxF-K}O%FNn*sH+Q}9TlbB) zvd`jIUyjw~qP11kgtmT*gtuLBD|j`Ap4$K`}D2tpzr#roZNoj z4V9h3Hf}4&=0QqT*#m-Scx`X}XpAu0%lWD|>9*iZmG;se)DzaDUZ-GNKS?LagnRX!CnQ3H$G)qfR9asiZx1K3EtcH(}AN${~1%H9S4fex= zZF&8q8t%|ej1SmyL%9uOsg_fnLK^bP};U;%I<+{X|MnLxxBJdAmeqNVud68wNpUx;>j{WADP6A~_RA|)7l z1tkK^9gP4U9+foAYuhDaSY4%a%dnO;E`u*OH_-t`3>Qt}s87Y-15uIqgft5Bv|-lN z)t4w|PuKc1jmB+Fw>S{KrtI2|OEJPXX8(qr@ZWJ8oGz>$i4h(D-(4wxiCc^I2utC~ zmAn7L+}k=k`a-Y8Nw25>bZBY~-nz>6#3$=Ig%&#;*9mMUqf1|`e)=B{pWWOTU^CsB zVaB~Y>(g)h%_T!8FyYnnJr=NFx=<&Kv|Xp#=>GKZ z(;H6Q^eg%9seQp)oqH%7nM%5~k@1q-gm`pcnE6pnk}i1_7p36qWuH6lua=d8{w9VoWa-4$DG$sI{i`fNmoj8>9^-lQQ+m}e^zfj z!Ml53wK;kwZ}MQ^-sy^n+x~rsiBT|5A#D-onLt_EQyLh9O-kuUAx6557z1x&1yJc=4~J*xlTZx&Px# z9J0s(pcN%ff7%}qHK;U!sM3Iib}2e5BSi|BYSHshI1pth?)dLw8q5BphCxG4E(0d$hI!< zm;jQngFBCLVPQe0lZXWpnNG@Io}ZUtV{e-KZh`}D3`W=qee1kwLN{kJA&Utt2Z{+N z>hy4Lis#d+H|_Cn_ec9(aWRaZ3GiU!@U z{bBykCcEFiwJ33Yx<97K`rA)-ci1+yI+tzQX&056o=RHRwzVw#&PH24>GF>UoY!C5 zigy_2wl1it%&hC;uQuRzZfY7e-)1xD`Q~@)zlE5K9hx?*`?u9ex&>xD-{Y;8PAi7a z{K@9cU$#{7z^Zh2FM_Rz{t!*`bKbxOa01ZLjc#TZNKyFw8-3yWStKY8vj#tOi|zCO zKt$HooJUF|Grm8MUJrL+UqAs`V$ly9jbRW$T`wdz^iz-w@*@bfebc~;hX<` zTu91KF|%*dYOQA9Bn{q5NvkT062|1kzVRFUEaq=J90pZ5Ei$<)F84V2W+fg7X2pIb=_&|~i$dbB|z5iDzE=0LB8l% z?hxxGX#Bt{n$JBi<4g2MW#f%}0=ldNaR%9X-1v^kbUK|>AfD&V73d<8d{+6~+spCy z5Asic`?*J-L!Vur?Y#td`|kCB1=8E0dDE&B*S_Fb_MGugFyHKBoASc?7SmkZ^!b3r z=~C<6ZthJN-OXysa!)>W`z*4e`I?>H-{0&~*~6SByQ-6Nvpvu1SDD=!NSFR#(ISK9t&Y!~njbdR zXh%#N8m}AuW{oN~#7f~Bil^Q|u(m6;bNQHaR88qVyU7GjQp|VJoh-8T`=pc%qfG?M zCNj)s%`ct1@>QO#qQQpZ&VYQC`vljX8PL?Iv38sd z7mmm5vJ7``vX9!d`?{yOr(=wZ^YOUdkx%>m?P(6Gbv(V>z}e4A)03+>hns(+f0AtZ za_9}!s^Q{4&XO)4wE1OENBwFhDI~hzLACfr30|Kz4|bPl=KpE6nW7U5atCHG`9{vf z@CRN~p4p10_Z(w>@Q(30>nRU&s?E9AGZ5x!dHA!1`ZK6>9GyP#J~I$gMI-W%-Xwxqm^jn#85w}|bA0@C5IOKFZqK9r^1R#veO7^1gvMeynH&jF zLo&%2qQ+)T-tPlb6MOkQtTSvFTX14R9)kyF0k5xd5V{w}1#mS8#$j}kF3TV==)oZ* zBBrMK`8tV?&0l~X(GK4_6bYEEWPF`O##t_ubWDin`5hd+7%KAlG42tBrsw84I$6i^ zd5IX{aBfSZsg4JEE<==AvSEV9Kf0{L@3}zG2MTmZfE^WO3aj}5#u09y7Q#MMaAK-U|-F?S|Ur=0Hmd(3#5(N^y{bOwGgL=UG(6oQt6hDrK^^WmGlNn6Zw z%*M?B>H2hA(@*AKTmI-^chc$yn;)%ITg+cu?+@&BaB5Ef&bjS76hNXXX8oo=#U;hE z>?i**_4&v^pylkU<|wD^I}Ywm2QK#R`4j}OQIPG-&6t)HYMowm}b6L*khh~j_z?c;f`WMDu z02^=LpGJY>40>o)XW>l6@GZx~iw8CY;Ds-*M<@H|lfw%T!gYjwqZP^hesUgsh9AF! ze&;FAlSlWm5Wgq^aXVhkF#01F$ky^@uz(4`4OuoxAw!7q>O#LKM=X#*B$&Y3O^-F) z1ai~F8pDLZH0ECnxC>IiyD|2{{Id*w7e9{9Fj!a-wt)<+!`4jzMHzW7v8c6Uc^>{T zWPrG)b{$-UoDPf)^JDYiA!84MR3lOsC?Dk!~E&%vSX z(r4Gb&O7Di>RSKsHJ2~kuCPd&mqkH`S*6M2!g)IEnJN z@!Jw+!}z@OgOb{iDZV3`#eACX^`*jWdM{Nm@au|}T3QGN4K`$|^$w1d?@&xjo%}l0 z$O>MS+oz`Z7-)+=207cdy{opN@l4W%T)=7`!0bduHZFYDn zv$nGn&h+elXnpOlAA2i+Z zi_TvS?f-Vwm)CP>;ghGstL`Ovhp+1Q#@PMlT)^2;U^4#%AoB-6B@&G=sczpI$SH`l zm*e=1dsA}OqhRz)^p2Lm`kXI7gENs%LrtfW$bzRa;8q0#WJACi?SuAoVr9sgElP;`>W&(g~F(y4j7dNlL}XgX$2KN_a!w|k+Jy12wE|M zkBEG+utQK77k@JWG{P{kLKJAILDfbAD}zBbhM9`l(P|XfWEfCm6OzzJ4Kt~pz+e1$ zz@$Wkhgqyi?GItfVV4EOnWsK;6J%<_?7sV zqBTU<6bPe-wsyBxas9I4=7KSP3P8bOejM6dU-A%H%+a5_;R9%E?siW!-3Fvjxkm!p z^_5WcF|!O4>M(NEr>YS3xhrhHiu__a9C!^VW;rv1V9?upDhAlv0O-J6wwfp|1h@uq zA0o2@cG&Y_JsAW;(v%)NSXe`mEjkY?KZ*c?sV_zofIkd{^=pQXebDwb=P|UAy>h7j z`EVwjzk%`v{N^yN>f+-Q{H}-B<70r)J@19cqgxkn_jGleAa3%B!`_NdV?v$RC6tnE zes8lF-D>{MX7ZD!e>*p+_k22F3`jTnhFAT3`NJ4Et9P5Pe}KrmsPOjjD$71CD6#wMqea;Tq`M*o1NR;m@0i z^g4alIqqsw+>gf!v{-Wamp~JE(KxD3_SY8(^vNue@dZ~94~+H=7W~^xk}~Aw0%60) z7OP}ljOcWSuDwGC1m-;GG&l^$m!TI)k4ccH2J@|502iwcJa;XjC)7#wC3DCT=?i&; zCa^G3hqbH;7meAZ<8`r7~9e0GG57c)(t7bU|@Yh zbyPiZUS69!sn`2G6COLyUAwqTj33ARhW75dhS zIlcEnMOOG^D{1m?Gs!kN!%@EbFFN{~n{JRSldX1^{kjr2n0=n|Lt71=Y14s{%A_zy z4JoX+`&2Dy9YBT+*%5ZO_l6-KTlY1?YuSz4?cN6heU<^SpT(0MuZZjEO;&6X5QllS%kG={R4kM^uq_PK=@rz8Dkb1OnD` znd5l&E{ucQiU%()K!qmWgr2=1mY6`x6o7S$Nl3#)@D?QI`)$+FW=Otj??FTJ74{~M+ z*{_BWHJ^*Nz2Lxf?(KhfR(ut3FI43D0aH`eMkEmz8-U7(GZr^Z?4Q~ei4%voS3~n` zsE0BBD?WHMm3u3b9Q&My-gmcLn$%UI=sWm}^gs8gb`4F#qMr-{Kci=WGDRe{NxSaC z={hn%^U6U{E0+7-gf%{Nl)~6I@(chT_*tc)!?QTGNQ~Y--+beSnl{l$pKk$1OQz=` zsy8x-fOn;CkMOb0-JfJx|zj0^;S@U>G!6=3P za4Q`BkaO{@MCxqd{R;xzJp(60QfgK*4)vPrINUefc=|S+{x+?jw5vdX42c`SbKJrz zg3FU^gSQrfP+fq;uD@2az)e#3%Sm5gX^*SNTD3tFZOCv(Ibkt7mo1&k6tOQ+bCumY zMo{S6M?JKbX`mGw`w^g4sDVJd0^Jig6)nLH(`(_xDv-hYIwujqgu>O2F3DMJGUydc zYE87tM3_3@Qvp*YUX4&3t5~HrF7A_)XBbAc0-;C^JG&3|ZQeP+DDYm;ih|ihO@QVnie)$7ZyU@9VSC5vLc~4W{=>= zY&EBa&#~a%Lzuhz61=p1xqUu&gAfz%p`7D{TPHq#CLUV#H6?!W3R>x$_yYC?O~z_L zB$YvTJ;gDJ&WrhN_Pg2_5JDan>CSNr+eNYq8UtCALCfnRD@>p=sI2VjYSSepH~Bz zhRH<@qS501=*jh=xU~*XXp!M)nFF%4rrz@A0kr@z%406o}9bT{s9v)*r@sM(I;n!X3Q9u z@<5MZ(+p|K3>qzrtfYk~X&HtPWgjhMsTfJgFjEE%EFESvRh_}^GofFQFs33WNkf1O zQ8qTUPZ`a^ORYGWwqyjDB4LT4$jIrCwF^oR`~C=_2FheX2MGvoLs=GICvMc~4x3C` zWP9Ntl<4&tz$MDYmIc}lU8|T2)eA(v+|#%(^E2z=^C!xhK49~Z6hBZxhMB%#D{<+Z z&XHp%%OQ^FH|9$ASbeoIV^7w6N4!a=7@X}B^ht<%!zOna*=7eePX5; zAp@kwXOUnkA^;fP!IX!@2|I>UH2QgcD3Gq00Qq3W;w0q_y>;K#AILk2wp%%FmT2(9 znK<&Yx{Z%L5-Pd2d-;OQfdv*Q9DYb$@?m!aekk|rFGr`hdqqam&kFP<@$vF(y_x0p zV8#n+kaOpY&&wX%-w^=n^afMwft9q4Tzq!{F0icweHIwz}Nkj`mq)oe7c;zfYnAQ~xi zr;UBZ#t^y2L}k#H5Uyr|eUhPMvyC%IK%&@$MPsp9N&{1^oB`d$G+LrC?EKUK`j5*#L-J^8Gy%Kct~cSs*BpwX0vhxkM`iO5NQ5$Wz+o7(+V z)SG{0h3|-Tcj1ie8RMo4l|Ft{v?@O8!fwbR4!|6r^f!05CdO?EOL>g6x9Y*Xbam?F zKXt2V!DOi)V~?!D1k&L@rQV26AGu^qk^+`zQU-vBN{hX8%l|>> zWJ?cli24<}p`9PfJq-@>v_1L{l@KDx;4prut7OPsczSRB(2^=SqF2uHCiiH1i{8?T zlI5NQQ$$qA=I%by+x~3s5b6P7H9#gtQWav)eJyQ47uvPqDV~!HF$AJ6Fei= z$c;3(Oc9$Tp)Q~RPSYFxh!rNqgM_03;zkbf2%L@Sjn(zAsmPI5B3+5^nW7g~b5=w8 z+S&v43+bQweF5pj${HE#+#VI>T@)rF+-Nc35m-?I7a7%g^k%E!qJ*3*v z{H{wC794p=wJS04!Edn&!o$;cB>&IdS*ufi+cQ=%vghZ@mz9%qCsav*?E|0~H>vmQ zKDno*YRKr-)8|E^d12BU&S$v{<-eJ`+fu0_1H}(gPFww@$%V=RawUbpfdP^zWbP5H z#+t0wGrXq$K7|@jPwG`s-fr!koF{@zNJ%AlhVQt0zn9L4o67y_o9bHsVVEBR+$-*P zy>ea0;=c2DA$X zO{_#PWJqy0Simbyk5DH(aDekQV4uyIkj~-7tzFP9=;!dd=0&{Oy@ibGWpZ z_^Y)1%*psY>o>hQ#PQ&qJ@Lt#6RFBW!q3Lq$2d-W$=vZilvTEcFP8|nO8M<&LHzN- zXVM6ob&$ZH-_$@wFvK(TNUb8qkJCQu6C~E9`<$xd3i5q|j_Hn;20xPLDmuNP(AlXe z#xScZg`AebTOk?6oxTel!cY5D2FySDacyZbs=M6_wCz&6C)5Zw zO6}ZnB!D>t-ZU;d8pea_?fb zDv-recm0CmAk}(cM~GDTLSy;1nZ7)gpG5RcLs{rNpW}%l?R=rgtu)`J&GMw>4|bc} z@A!27{-;pO@b#7b)~iNUl-nNbUY+#>aK|p|>?kYz3$%WntI*cxBLd_F!eO}5AvqvZ zw5@B^{UqRXJa71Bo%QU>i{IZ4PAk8pu%Q9{$MssM6I{VLP6-+prDCz9fveROWbv`F zkW6rp{OwqlX+KEE7x?qb=Lxs3WI}LWTqhPw`9}l@RkS0qiD)K&9)4dq)exxw8fO`& z9ira)2^}Bmh>5QS1HB!d2E2(t>2zR?h5``OH`IY`jxeQ+Xv#av5rTyhhPG#%a?!LU zCe5JKs5JzvX#jx3=!|0eZLf|C5ynGeht0`S5uvbCp z#(=Y=l29VGNV@$sjE$+pIY1&`C6R%T#tjs>LIHsEq3K9&YY7*6!HKuc(_aZtS9;ZzVMxCRWEpQ;Qfg=>@Y=+m`Tc%^ zv)Yf<58~tbMnJi*pe+yJYi680yvqIO!O>Mu_dF%NtlWJge21meCoz9W+5h=r zSTNf@ds{zTGCVHi3bsf9J>)G^iL^BfHPT^$AYhE^H?wDg>(`$>HwV1of_|;MYewTL z9y)q?aPgOJfB*GoCbNb%Sq8f`k@}p<_7_0Mjacmfe)xc5bH~_;9lY2 zk*7b}2%G?H*;+AR)jFL>3m4Y0F<4y?Gwhfj8x!kbMTA=gyIADB#0%pNv_sW3EdY;?#<+HY*tC39inNYoIWiB`h}2?`S*e)IFpQ~Wv?VHorCegGiyoV3 zMIdM>FtFjCK?m!pc*&4KRW3fJvWgdf{e`*&z^B5bWWnb*Gc!Xmq7Nb(^n9cfp$<(4 zp^b-fo0NYNGYz<;LHlq!dRfhh#$^$R_wA9m+qu>{4+|y00j8kfC=;|C8o(1?;qQn^~ty`{>@-GY4+I9bYX>$B(Fc@WvRbT0d`VW76POv2+>^C^xuv_tt#?_Mk z4ufvwhO4>x#QigWy0;$)$ZAl@h{7U_cZ=0kJP`YZZ~l4R)AILYcJ3{uR+JT)whvBO zrhi^O^d18z9Pv46#Br7e4HIKoBpHYiVk7+vJS$M?P-g&mE(rZd*I?CJg3|iQ|GGh{ z(jq5{MjG5&{0pQ7f@G{%(C|FNf=qy}kK%qL#>wnrHc)v>)M6I!ISM6H(H$s|nxs;+ zpCLIIK||&buzPk`G47RlGnzd7=9n8znqmWPm>S4ukjv!Y@XTZwL4#$nBJp>$dP!ws zvGR-xwOnJ&!}D-Ptu0i#LB+g^Cd0aMXgpFz*RH%{sLw!73jNY65ZS6Q1Rg;KZ>8#k!2??t^j>cYDg# z^pzuVl&yC+O8vv*l@|YgH02etUw1c%Z{s35)!xqMv|sd7&{Q%&?d|?48e8^gALC_O z9}z@``A9fYD>D7pU!P3((JVX;X8{s=YT=?SH5F(8)wx->-x7k=OwFR*IdDd#3l}{J z;I5i8BHe{Ju1N1=LdqrLHIoXK}Ua!Cpz!TDBT2#2JPhR#d0Z#2?{6^(ola zD-RfKf!Xi$^~mt5yI@d%BKJ?EygS)59Z(~YNiz{k#dwBEN(6~mKR3oDy4S~uT-`hjc=K~?8NXd1g_PH^EW&ZNEJZ8ZxB!3Vd9ipNcTnjVU#1l=WZh1d~Cbq*BRL6HRHz zFwkbyRLT?jIev$@LnjzRBPM70=meiIxN!?kx69_n>hH^om*lIl0hwroCnlPa49tuk z+qeY(0#EqKX0g0PcSi#v&m_ja`b_KtwxL!CIIf&#%uKiM&a#pDfH*ZWg*WU1el>@M z3oDw;8S%LGVXKLXwtrrlVg=ln2ZWbkYN3GFdWbM=#M1$)76cLcJSIJ;@WZ-*d`V>= zFdMkg5|qO&)tt3C+i>aJGh4jp-kz+0GZLf^0E?cM!y)ITT^&7nC97wizlvf!nbM9L8})%A(_8&Qvp!nn*tbO~M0!#^JtL?}Yu;mX9DgI4gSNQ*2%>@f75MT}!(2*asJkbKKtNbkVhtx~BKaWyS zB8t~)1g<1iCjmOVAzw7I!VN^AkCm4!k3gvm>Af*RAy*NH4Cq%GEadcyl^R9{nXND_ zvB`7;P9-+*QPin+^ludM&Iw?JgV#=i*P1|&6)3(T5se@!5jdFu|6aDxh`%v}EeKFC z(;+>NXw)1!Vx$PB2=Hq#xWRX~j9QxZiLqs|SkNvT2S5t%39Bh^fwW@+B(nv_7DV5f zv=Uik9S}aV3i6Dik33+YGILrv=^#tTJx<*?@+F_Q)eykHr#v)pg?ma%ZkK&#`koBCjbr5zMS--GOGh}MMjw%` zX7d#%8x-6NODR`hP{HaSsmNx<0)?2 zL+f(nW6ik-JfMj(V@S-r1Vb7aiPV(8F9t4d);Q2oS~LzBU5#CjX%3pMCj%kT*&UNl zXpY5QKV`Sj?DAEcUk*K<{MF;nv8^KC=jTb?JWf_@D4dE+h7Bik9!z{{bk&FrS=e0bv zX^u7OkIm`Mex0CXutT7UG6$Z(Ucol?5o~-KRYPSe4AeeT2D6x@S%R7mL@yLAYB7~Y z)Wo2YI%5eWe3JqYHaxL1Otj*RFar|DYL=48V5w}3-*{#+PJBGuvClF@ z?$BgStW^&zW#CqjU{mHy?gl)wH6BVaq9N1s8v6S@>?RWQiC-swv0?)v?+k%SOTL+ z1h%wXzZYEgM~rR#dDNfNmu4_96-3~6iQ@;RUjip7z7c!{hP`K3K}7roqO(TN|icSog+aKRDa$yQ73Jcm@q$q zfyXQ4^Tc($Q`dttIqB8aZSNEbDf65b4k(y_9rg@l^1e=R=yUgh9tO>8gFBUQ2JhL} z_U4B;KaP7{t<&KDoH>a+q=$W&f$XmO$zE>fx}is(PQh-2nID~ zkCvC&bT7k@k|H?I2fL6NLidC#V6c%Ke0JIKyuPI^M6i6c^uLer;kz}ke9{AwsEq8R z15Xw`=L&gQ4qkM_WGb(KFAN3UInr9KF<2X!WR*AEXf|X`bSG02#$1P3=R%DUh$pg+{t%!6S z$S`<*DU7P$n8LA&()*t|}=Fuyzw2gSU= z)Cdq^2KXUIF-HOa4SU7dm>v%$)WAsRm-)!^BK1IY6X}oXP(#|jEE}I+kZF52;klT(;Z~?EATQAK6(;(UC4%w?yZ5V;vRay z^A|tv5eu&$O2x4EYDte)uwQv%!|*SuM(n24ec3lGT;ga z9BU+DgGdJs8suqg4D@ji;qxbOu~;)UPr1`JU_g4=#=n|_7BrX^l6})f!}tQPuMw?j zxJt}DFpv#h`xh}>eGI;V<(QipzaMbK4d5A~#XRv5QgS>{_>BRFMaWU45|tfbxT^3c zAwATiof}Kk{h|o(L)ylM8ctY^2x$dfC?IN=Z-0d=iaeNdQ{q+;cx({SEH0K8I;8|szSYFFageL zU;_*ut$Jc&G1!nMP52+qgjZFOVNxW}e`BkuC>(<8m6{NZ9wPW%HOy(_5)E$!DVpVj z?rr!4+viSS9_tX#6$;upx@B~{bnwQ7Dp?;*&ejb4M*l^D2k5}!vo@!*l{a z*w!v1E_Uh6*u|=QeVzm;>tp{dqgMqyJ?1ca!DtSVH~>-=IR(s74{5%$MknHf8kvp~ zeJw}8Ydh%*&NY8<5^xZ|tpDg_*|u+Fdb>By}dkEiX&-?331@OwB+n4kYnLS zvQG&-GKIX!O^%ZGO~w`P>tQ?W8z_qQbDi>i8*fz@zoIzKa%s;$@Xm_jUESlX z4(+x|+4m2#eUo_qZf^f~ANT+Nv;M#I8JPX_(Eqj~eSBfKNBh1px7v;W+q?fKImYT> z6pF=0u11Yq2_LyadVeMAed1?beZi0VHr@SlU8(ISCo0^+cVGBaq}o6H+(sK!w{mbt zls;IS{bcR)*MpYZpXUtdH$Kn#PGro}?i5Vi+U+zU(a#>#!h3H}8z#-KDvM2~?1Lg^ zXDFYMZS@mnBXxH&)rvp3+7L8HGftW_jqKiuEBc|VB34m?q>*Lz347`Fgyg4gy|(x{ z>-EG1=T_`mkLg)OJ0(A9)~a7?pW0G)EeL_ssA8!#JM}D<*;@6CM5v!r&HiR&6F*`0tfXtMN9ARl6TuVD3)zFiW#H|LmI0`gJSHFRW`ja3R&hBgH1uBgIp( z+j6@6rqzH)iVJ_Nc(*;D9vDQoZ#l40`!x>;n-`CGX<{zCa!>clNg zP>CZ;MB2lB+X zvKX(b2 zRZ-EsSIlQ>qDVPkUvz)o_1q)M?5{bm*Nj|o%sK5c{MZC!nBDA6OW;xQLzRxQdtbzPI0>o#%Y0WoX=kn zj6-W3gS-KVK7(ys1lStL&M}*ztdO|2qAz>f za;u{ByFP7J9jMBARW$c%ZBN^s;O417k3*q>9?hp+s}2RNY@T`@wg2(Ts9(3oszSPj zzg)dD+!;E;n;8*4UfX+fY`eU1<)Qq#HTQ(XXHU2DD!$d^OlsF?dxZG3ZDRTD()z7T zLugI1Al=!xUClViX6625%#$>}-aeq+`IGwK1njBqVOE1JYg8B7WZd zP>^I?-!US-#VGER%w?Efw=k%SVtzZXIAIPagE34nE=A8C!X4vt8M$JaejfRGD4lrq zVAqW{QGv90NzGUUq`Hy$dZ7axAD#w2MDb4=_KegdTae=3I*&znyXlsOL3=e{x20nY zO(AS{nl599AXm#|YPRT>`PwbRYWjSK>5fcWyf5Y24PIA9DDzb@MW@@ITSMLSXlyNW zB_pm}_bER`*W$s5(b=bvL@_6}#~i=jx$e=;PkqvE`nBy37kI2}$qp>P>6~`cm6}3k zS__&U5BOvsv{D6HWa@3TPt_FXMIR5XrM%6TwJBHU9F=fUC$MQBJU1Is|tj@eB``K!Y%KE-wW|UsLc0h|q zg(hQ%=(WHj$AsZ~T0{%Yu1nS9QN!G+(|5?qg$F&`4oK;yt4I-vdGuB)^%YX)aNF?l z>Y15uWHNKbw~DFg;LDi7AtI9?+pa_F0)S-`I>8+3&x+M*s>ZNHRPiyWR91%O70xLh z3i-(6CKH5q;K`)ojxlNo*op@fG!KNNkhVpfMDs3}OqhueN zn)&H99*U;d9p!h54X-E|>w&02ZmE*5k9!B$qnc482-;m!447GxTb0cbl)h z#ff#vmK6Itu1}4YuNVpSK#{iK?7}jNYgG7PVOchI)LE`M zs>5-24#y6B*?n`DtG#2?Un>sQSRd%50r&8`(zq#Bb--nqG+nf3ho>HmKRb2P%eDN3PrxsfIRa z^R#(-t%g;k(=m6{s07Qp;CrHhgG?4a#rRZ%3cf2>TC8}@TDdDv8})>?U6ZHR=RBYJ zN&TFRT4Bl9b|b3`&ocA}Q34syced-`!qv}F1QQFlXmq_N|0ilJm8rC+zDnig3R+SQ z%oTTKXwq)IUY-|e@M$nG?sCo zX-CRmODP^6<_3$d11X+X7mAuzZ+_+BVX?LJ!2iq9y8x$kr+K21Ez6$Ki2L9VdYxTXLy1s>12CxYTsPNU}>; z=uicdq+I6VfFxU`z348wcLs{RyN3cnQPdvl9we(MUJ~3N-R^Q?Vh|GNeE;wJy*@&I zD#Udx^WTyhi25Iyl=71o4f{XNDUMEFJf3X0m)^BC4i>s}Co>ktQGYD1#5=+URdd-y z)@_N$!t#@Uqp|zy-;2rQCyt5mrOvERTK9t@V4u1!B|}R;ja_)XSoZ>3luk5xt+Kio z0jKR$S|;7+q&S?5u2aa7n7S6^b4pW}FHCA2^{uhc63>^qaQU4MPThaIp>#Fgw|nD% z;Z1s6jbHjaQ!`YG1{O`8OZQg0WF+n%0gJXXiYzS(fc&BNW6>O}wF-g~Z+X6Z{E$Jt;mh;I) zUfYNLiR;@u)rW~7DV$XYSKAVj{xl!mlU1%Ys%hw|jH^OBf;B5%pI&jDB&9=@ifm9i z?y;9rWNz43n&$+#`e;sNRav0Tt1?b#qX@l>qhb88hq*N=f(R6<$Rw&DaFzjti4A*B zuLl{2wVl~MSH~4%+EN>9_k6Qjhqf-)mAG|$#~al%6cK}YN;Hcj&b*-4`6=A z6tZV=fl(Q+DUgB~Au4gODVZXl7m>r>@>)=*-?hp+$rIbdR_GjxWL_F_fRmPL7IBO( zwo9_cXYg2tUi$bvf<*X{z^lI;cj-QaeA@Qyt4Eh9bSdA=zH;frwKCQ;T6ihxZhpMC|J`F3$z8t3>%V!`W#)0a zw)XZ5MAK`R>8z??8J>G>`+olQf^ZJC?QueWZtQnI4|X(-KIblfjMnSr$XrnU@o8h% zmHn^I^jvJ>f5>a%pN~hHdVc*>^5;r~wyAkD*xd8}2HSCV`F=C!#K&iOBv-xh^AK`ux@2{^wWs?sx9pZ%Z7|+W8(6Z(>X0to_ADJtys}ve#qr%0BX{qXc-MJOjU+oQ^_PXBoKKz-EhL}at=8wI^SB2F6|;$r2aB-D?zWe zumholPW}18yI14`(bx0Bd7(Xr)N>>Ap)to_g1bYp3TmcG~_Mv;yM^M0|5oun3EMe^?4Z1Lgf#gWR3+2c_ z#!4vlIYO?NJY!b{+r-2Sa#_7@s2{2V$+L8OJ~$TW#+{Mg4kl=wt`Pr2zWoqowUgWa zdg12Q!i{8ltg9K#3)wQKD!hNqqT73C|3=3nTGILSI(K=$@PhkP+|7HcVtDHN1J@D< z4+_tl=Uyd3AEIACd8!RH-Mr=d?N>rWr}ut8*P}bXbT~WqN$dP8G!{cm>h9***RBt5 z+};chwbqK)+4%mAU)}ii@-}*{?xt6k+WKERI(~cbDC$jJl09O(?OD=iwk6+oOEMr;jouwD(r+e`^1yA{n6New;1eK z-zNpz{2#W3KCO|#K}8C!y9Hhr3475@2Su@?Uk`{!8R?0TTg+YhLulzG#o00U&QP{93!O_JnZ%_UDjKQH{EwjNg|@F_hyPYxr3^U)!IX4 zsIEd~7MzrcsGEpL`Oo?34qg3kRt7PfG~it|xo%z-OrBra5XkGR6y+urLDgvyORm3& z=`Z~(Oh_l}dxFV&*Uc+p?9$D~&+Pc9q;?(d)9R>U5gaI0oceyX(}dh)Hu- z`i%XmQ^yB@(+*vQhQmU-jb+p1`bBmCAP=pzu8W+r{H*JHBOC7(CM1GrjK@#czirtI z_A_ik&YtY%?FHKyT-^NhiQmt*o#%f)ueFRARbQ!Jd(%Rj_VL_l3&~AdNYRkc$Q90y z1$7vQm{eX{VhlmA7d=VFpGBI2jB^2bpm{lts7VKRg}4x$pv)X17i5^ENqHP#%8(sp zJViNzniVjT;=`&^Ss!#tR>QFpbW3k-=7MI4^_Sp`MFy`EYwlPnwt0?Iti4>Nu&&y( zIH-uo-4d=B;&cQuF&fsbWmcuc@nnjSN+TR>MVCdYw9{i;2}6d$Ivt7-!-pb7YO@wZ4v1~%zJo=(}{`FpHWWJ|RFmd z?H}yjrUM+}?V~6g8gM)j7 zXsG>_g=oJ%c3-e-OoVQ4j|GViQU6nIG94rMS>qV}(c^{ld%ri9?H((>yiTVzJ`HJ% zY5J*h?34CgkH@$g1+*t=0nSqPQvNwXP_QXalLEM?Yn31xbfh^lQO1<(GevY1>&$p& zU>~qksem1akwQylicu{-WvdJe5DDzGmm1{79QM{I6tsivvzx+)c2o~P@@b(gFINl- zg+hs|EqV*GONZZjRKa2qua7|}Gvwq>FGlWNpg8a*I0|BijCPbVX0oq{QR&E!pj*%0 zpuOVItVQ~e8setA0%dlhf;QKnh{_a(P}Ou@$)#kRSnpb7t1@tXHzjmvQmxc%sPm@c z8asAxg~y>kq8{(@(b!0@?AXpzqrdVzT! zH6NYXeOMGX`j&2S)(!HvlJOT@)vq`M!s&?o8}d zrFkoU92!ub1ycWt-TNX=lg&OJqhbm#CLa5iqx3+8o0-+W^0@B0Nc#NVV6%W0x?fv# zw-09p!A^GUlc1>ZZAks4(Y!3Y%eo`dz8Tw%8)D>51yYeg$HbZ;EDN#eor*(73_qC{X0u7V9~J;l$&>M$ z3^{!m`ZK}+VSiO2ryNoU*Q8a5*WREuRV%PZd1?ie!Lk^t1f`_6;!wsSAZ6B;3VDc3?txZPPxQfFRmphwhM$zsL+|yj90#gU?v~sVW?<5paY(~E z9&frQ5mrbh(x5ETG?PI)o$h%q^a1I#?#^>~>O&ht3U;3upCuDluDad_uKM=$e!(JX zuZ=7X__GY5dV z?J3@mB)PUt=u|f>g+dzj!9Z%R#-Nj_$gn zbh_4>q}yY(E@{pSazbCFrFgtC{78&*RuSFkiiMVkQ-+-ESYEZ8cXkrNhhW^);oM)(!t9 zS{M=nN1dZu7%9d!4YK`3P1#JwcB+M&P^~F*3S|zkjZ$&ME?@NT366KuLZ=@Xql1+W zrIfUN#&kV=MNAEhcov?4ubN0n15d@{=D{L<2tv@D#lim8s#>tgF`&5uAxqfWYc;mk~Hq`$G$1~$~y`>Ky zHS{9I`^5VS{iUm=lC{Dc#TuWW*rj9Gz?zm*T^i zGy0|+!;kcr6r0jFGgJTb(o3TE;V(@DeL6SpjbA^wFFUD-=r2V=h_=XjW)^tMLT2e= zQNK50$EmN?R~Rox|{@S)c&DZHm71?yE^BG{my z-Sp{bP8tW&h#Fon} z>P+*~ykc})5Ok1dla^<5ZCYc}eTZ(ezo<||W3pbi1n7-HV{`+Q_dzX~f$^5Z(Ug2{ zsf4>iuc(H`o%Uiltk3E73W==GB`ig2!q-QnbTI+u^+l^3(AFs^`Og?Lqoj)T|%tP++gCK;L35O6CD zv2Yc%Q(qA-kr{8jatfylE6rjOAQna;W98?Rm@m82|Q_ zU}QkbCW0mQG#gi18Vl z2C3Y?{Ci{ijq&Nh=YZO+sCKu5N!y=$6Y;G26W*%U1|;q}-tpamT@MNxeP>>Mf7r+s z2^ZV1Z>v8)4ojSV4`A}D0eo8l-?*^67GenrD4q%I+YW5`k+xrqfk&!X2#m(#MKPN6V8(3f1DU zf5xPY$#S?w#+j62hgI%4D7oLPAo39fnVKK42J47ow%E_(NbDMEvI02mrseLf0b z8SG|JCFDUtkV!04g}0+@x0mc#WVRS|x??ffzG?r~-q0GJW+r0!oI#lnxm6gT>`s5V zT*)i~^P9+kwIH{NLuUqin5hayA$FzBIkT|Rwjw>w?^qQIsoZa03w85+1Cvjs;{Mg1 z=e={i3pXXpp_bXc1$}6MXo_A=NgM694o9P7=?mlN7sloAc~7w)XR_PSVhAiRAzPpH4Zn9^3ZhLXNys_o7ixT4}~tEB7j zu~pK{zqyfa-=_DrFKnbI=7knMlk~cSs~5dKK~zH0YD_>-Wr-M;gi}ifG>y#dxFx{s z5jkS$k$9Yb=bD_mJiLH>FBZw&ihyRpbu*yAr|+{@41iFGTr8K9>YQj}t6nD>kdz#d zs)Eq0Nn{);A|gg8k4Y*M=@mI?L_vF?Q>|psrlyW42cnu%yVkr6ntLf3J929*E6N?S zG%Pa&m@Gr&j`fm>88;!-hXKIlM2>P%BAsxoD&!?2Q?@EJ4z$0=hk#}2j9CFT8Z5XM zOqEZA$$NX(Ya9!a!`554@q!?!7dv>|p;jYE*HD2Gi9|vm85U9ZSvB8`qLA2UyIW^R z*-qdpBXezcyW5`ag?Oi?Y0>a(kC_KB^GGlpoFTHG8QP{>tZ1E<9-akuM(z5Oqz_rGcd4k8f^?AUliXO{zATj`#U=6E>Z<-@Zo zrX6T1>d}j~r|M|1)_RQHX9_GRV6BhU)T#olD}XXb6)QY%j|QI}9J)hFDI57=SA(=?lOZx6HRnv%w$ zwxsQO2ig9?T5v3+j}4n5^5t&rj4usxpK*G8SV8D5v5uu6;E8wXn?q%mbgI>dO)I8cV9;C}q& zM-ab9NW2Ipw9mZRhxbA6am$N(y#rv8Md5QA=FQ)81*bysVo zsIT-$S8^lq^U_-#$AqF9hW!yTAvh$ZN+QP*rp8T^9#8yasi+X2vUPTH9V$j{kN*|B zB3*y$Uo%f^k`*aO78w1JSb8EVi`D(wIpc}H9azaV#bnxtU#!Vj>V7-CK*?LyNa55! z!3SKi#~X~Ej9IBm`@H)%W* z-LhyOvcku1nwJQ6CW38R*AMi05>gpNf-u0+Gv$huqj9W~`b~LPuFpJyX~9mdDM_g| z<1D%LJ9ZO9CBE8yzB7}Ggfh$kK^#)fnZ->xf+s7Piu=&%E0qvc1Ll~}RQv#9Q{jj# z9E(NrvdFP)5F>5Ila}W6xiA53)ZoNSYTu+WfeCy=mAu&$(P!2$ar08?b>g5KG9n;TfOk1fJ!F&WD^Sg5?&#jn zA86-ZCk_`_w(A4iw||S#M@YFOo<|aHzt#dMLU&hp`&vRBk_bJ<#D0pKd}7;sSKPJT zA-%9ENrJUIK#|$r-dY%06rsY<#So(-gn%-1=^looOF_Ic#CwKOh8#AgNm=xW;BrCn z*j}_63G%|hA?P$xf4FuK&}mUs@$?%vZohV=+PC>H?+WK9Y$^T0Jwa@EzF{_q$w|Zx z`+cQF@(qeT8ptf9PFWvSc&-C~rg7uk5xRE)ZjwP6&EQ0+WGh^%Y_F{BI+WoNM($hD zUt1$Ly5wC^d>qQ-{lyqy^QyMwRuW23Lo#JP4`c&=tWAY{mG-4B&UjK}0O&8EjtW~m z#*h=-8_xKgFirTJsiQ6>htOnA`9J?2J|+x`yezN+Hd?RGBdEv}Rs?WyD|wB4IeYGS*9+rIu*w3@6k&6aUV4CcN?S@S``b4$o{T{+0ZU;bb_} z>Npc?OmDb4Y`~}_WIPDG%C^lRp(;*#zi~5}4%I5Wr>f_YL z7YjV?-lu!v{V!*115!u;`03uvba3>>MJbnaVe`;-SN?Y4F4HC5@VL&Cs!u^A%EgOk zE1k}HsjBQ6hyh|TiMkJZ9c_-MVi_+H8lh&#f?eXcU&W-Gc4s3^GF4^IFsHJNc;+_c z9j47Nt=x+q3=%wjt^06vG6|yXZE(OOx6)sJ?6C~QcxnFk3wyhk5wI zvHQve35VE082xH*DiS#sQv_m>m62RuwdzbMYAec>ps~X-O^GVw@TuZ%ho`fYF+~u5 zKs&}wn+S?bNh8?*oFj%S?n$@U@4y}%Y|;?`WsMS=8AsNqj2#EGrHY4wKF(V;OUJQG z!DA-%`s)FM#x16BAukY|2^+8M4(EnF_B8EeeH5i64TBEH>V(&)ajwy;hGB-#A9~YK zNqX#Xjb=eF+KLC8AcddOx^j9f#DeBzgo_;_qN8PQG{pt*0Dd+>qXQc@%GB%Me!^Xb z7>+(S%Hnn%yEnuGH}HY$CHP-r#R1X+6fs*4p|UVbNk7QjR= z&UANl^Sai>G;??fRqAYF^E#wJ^F1%ljs-da(e^U~R}(dCMLe*rKG^>@Sn^u0*`Bu% z{JriT04O9VokUP~`#O+}Fs)+*AGp$7k|(&XO^<<6Ssb3@oCdTZG_H;X@D6*Pw_@Tb zyo5+a2mFFg_Y`T(>{fchO+%k{Xa2%A$V_Hndl#BPeO|l3%0l=?pV`vwlBaUA4ebfW zm|@Ee7A4e{bxWg|^+Y=SGzKO!SqDB*OBOPOcCaWxK_Fd|(6R;p=pR`(?*1GR2Fh^6 zrKq7zMee9euaLJ;Pr$l3UEMi7KnZe)p;nKTyQQ^BrLR(2Lq2visyGf}kj%?2dQ;j8 zuCZzZO9WLT?ud??7J)}zG;9dV-WjjI(8=O5Gz1H`(nnpfJ|8mnXfKGvD%u>oPp>Ka z_qr8T&JoLUNCPR;f#H=x!D#@C1G5<(iRgi-lxGlrRk-D|vo2;l)nE#1tbs0>Oxi;E z#^=YOgL&y$6Z2MZ5S?F9_D%=5E8_syMV@K!l(zRq7FISspY+84t)qpS3QBY9pK&fk zM&Bl)!T2v%j_Ff`I;VO&AEp}*7xVRoaQM<^kyyRfdaU{xz1Lg7O45?;OqOvq9Bi-u zhFbashtp&3oeFYgM~zacf^PUkinix%)z^RcWWQs_Zc)8`VnYyconh?h6TMQw$W+;H zqB)ge(`95pLv`0u_$iXjPv&ExGe;#?i$36d_@371IE~)xx3LEI=B4Tp+Xxu>#HW7W zY5x=Nqp(u2|7D>ZkB}3d$H=>BQU}p8oCbJR7!tC&b4ja&Y&!I?U|c4Qs!wq(#1Dc+ zsA>gUYnOi)Gzj+5xe2x`B%S{AU*V^PMv_$52rrV<$AlhV$-j%U~M23c<+kWKfQBl}jMZ>vy7X>k4_O!<)2 z-*U^L6%RVfi^ET#dr(8^X&f8Wdc5xIP`Bag-|k^`L`Me(5D1n#?+(w7G6P$QgM(n_ zQm$nljlDQ`&zQ zxAR%f$^Gw*d}vrvRn)y*+Y`3uXVnjgMvHiQ6SxODmsH2;$MZczGo}cgc%ENiS!Q^C zYe6Tr;B5)r0euMENHB>qrqj%AE!-UlKJ#W#skFwi-p;fjAbheY2t1qD6+>tiMp=MyBE70*J@b&FsiVw9PjumdfHuumTVD{`#eEv%0&|4XyKSb zf2?)ZRovc?TkiCvBf65uzqngGHppb?9j+A`J zQ!ZW)OD5JLW8u(k`LmM6v8rsL<~au+Pdo_ zwBIBf)sq3&P}}mprDN>_ zW+}%}eI?_{)P1(+2ZL05zDe1%``Df!Us0AJT2vrqr#$f-J){%KhZlL;gmk~;TI{*A z5U;-W>_ycA+Q9#I2Qx^?^*lHf>^S%;am&aX#vjeH4pOkG9-NpXw_h2%0*PG z0j)KNpJ7n2UH3nH81+2vT^HJmg|sf+q4BzJ6;h(IHBVmHMRMMSZ8;0Gz7&83s4RjA zkEBR>Co_&{2ZwWpRRI3SvPV+{;tb_s#_J#yVF(&WkfRB6x7QBKE#l;Q)$~Murf&pT%0p$hap{1w5+5y^oI3Z^23^BRt< zK(CncLMnWP=w-04mrX0^&P)+e7vmX7@mZN3ZSJwG*OXI4gkD<;Hw{DqT2oAF2+B4F z%bARyHE0%VtxwR-j)l;dKMfVbhqeMkh;)1JhVG!#e_~uEw-y9fZ8}ji|D^=_joyBU zK!8&An7PX#N&jt3HrH(67AAEcf%Cxa`^xpA?HSZsS0TFZnCEL|#~{yu%Xm0t?lNuq(ygUj)PY4CE!u5aj5+|I24}a z8DQ9M2;Q`bP}!TP@f!!900L15lM8pyf~56_?ijxSxK(|)ph5Y=lo362AKGW;d4$kc zNOh3BRkSz|pgx@>HBUfsYvw0#WYkFCuCo+we4%4?D_Qm~wiynBmH3B06#&(yv(CM4 zXvhRux-sbvq-4sAPT%l!5Gp*9Y)b^$4)UzFh`u0NjdjR+sfPxCI*=)Fta+)%TXk_N zeseX{kPlE)m09{LPnsNeqNpDmFk4oKJXiplfn?0?HKt+n@tVo@T@duH5$qJt8p%yu zZWq1oyvi&RS`jA^00x^j2)%Y63sEX53qbpE9HpJ^VAbp26G4WbEp*p%$B!Cz%G8r= zcIr=*dv^wJLHoqIQ}*ul`6_HV&A)|V%IRFVTh6Wl%&_J{ON%`cs_SSaKb}H1Tkj(= zj=bew=}9rXNhyF6E2HhwQL*U?0USf#DAiDF#O9>yM{BA@$T&F4y8k+2FGwoB`oGaD zwEk?I8?E9NS?{8|ghP}Y%tZXGay&Kq&CJYu z%Ok~=D*-@-{02zCclUb7r!JDZuHdMpvwN_{W+7-s?1v8}*7+EY!N^hrC$G6>y{Nfe%ChtS4r3Ri>*E#+m8Q$5}^J(*w3oF z+w-DM7=O~-q^mQDQ!`jIqO?czdL(lXV+qEpX3-$K=s@SMB*J25MJgTgRHW5$i>DYW zIe;KYLyX{x;?My(yFMQ%t&#cmNM4u1xnqzHx!aBmqFy$**LrmeOLIxkU>=0!_F9-diDeVn8 z(Aqc%;zE?ZO$22e{5*#|^G!-o*5Tj)Z#`CG{3)3aeZ?Si(NngwWGt_TfqQomFagP9 zi;~*7;tnyG^znleiAYiHIA0V2aU8~&LbL|~{NUD`nGYsS^hdShUO2zyNAn|9O6Ee>$HWvcMs?k_nEI$xHB!U z5wXF&(A}Z-tweADFoEvQ?3U{r<0#YJ)N_$1pb75Npa>c+UUCiYneRYpXczSn0eS)y za*0gn_ZcRVPps%RSfYLR$^3++c!)OU4&HQawCZ0Dl5OeW>NbGi8uDADi@6E>P(6!! zMT8_&-=y@hKB!t^2;m4p0Z7P*V|mGX%MRbm+5znl9L}r$Xk=wQNefXcwG~#&R1gOVloKCj=7wU}Pp zZHuqnsuz9J`OT2)y?l#zc6T;^+O7IZIJ-L^Ys#NZ(2~N7kVyWPt&u)O6DN8#j}A63 zuk=0MTmSaK&5hLt*HGxZY|F+rx+D!++qp6NoO7C3KbC+wxhHE1E*(z}=-#$ZA1xbZ zqgN{(^ngxWsLmb@`=HOr%#FiamQ&}qcjhClzOERr(iLmpnFFM${$!tZv@2e_<>ym; z_%=&G#s9D=|H%Eytx6WYx6g}`Nf*1y%g*7VRtIIkyHg@`(a_wOK&xVwoIvP8G?=EF zz|rGlk&vIOd^Nhw_5T zc#wT;3Ndfc8FG^LVtSp`b%UuVc9oH|$!{YM9dqG!akcwQDpp)X@R4!}Ip- zEf*&J?X9)EAjC4bIp?=*x7YW-{xG3_@CV2%+Allcq3s>w_22$rfuB$ZMa{2mzwSIU z`flfWE-(Hs4;h^L;<@e7G*@sCx?_a#1mnQ%>thdIbfBi(8`!lx4G4l{aO}QFRKp#% z7TAohc3XqLxIR(x7{@*lLYB4w7SxJr6yx7u+d9wV%@W*aNgxVfzXz_OhNT3Ow040U z2Au_l27g!ehiFdTg`b|&C(QxhrJu3bnWdNRn*mGZWf$-b!5GuVl0Y1`z>-4Ip{uRx zlpRugLQmQ0sBpbLowLlg_l`fBk`UxU&9icds1|P(jYkCh28uU4d%j8$-~pVQ@JSWA^v9P6S*P1T z$YBCS1gJt@i7%@2n+h6$M_TK}siOyx9y4qzqGVO$EmKCy};$=}H zH_ki_C?=_PF}2)Nks?#x2S5E@?I$ie1t2^oXMOBNT{zuLQ*IQS_ zzU6lYSH`}Ii=;$g={KF;>=b2!V)QRB$e)HU{e5nD?b1K_#%;~1ungu~wM-%^rh&_Xcpaz6%BTxU?cmnLoAVfQF4XU>< z?0VhNU5omY3tPsj&jHasNr-TgB6-z4Ni7RDVR)Nl?W_WH!?**VyvO4xQw}*!eS0NC znZi0y5+a1MuJp|?()KG#sUojbqB$I@l$<4xHRQ#eTD9tBvZ_oP88wYpybJbJ&Jdx! z8DC}rK+7=+a#I$v7ZJ>I0V^Hy%Di+eav=bvGAI#;0!b?)tc=jOZsHA4&t}X~Ib*MY zXZ1T)!-PU>hH`h8@tcV7aR|mOoa#97K!%b%kO3|a7elP-5BXqaH5?8V^U-`j1Tv8w zUL?r&7y;oy(p$0{pKiQMqaZvD2rH|*K1MW*s?omdz6E_26kI7$F*Zo*R%110l+3(*izgt4E$6K;mTP749I`hI`s?JXDdQzVbuU-aLQh{M}s2UU^;CytL(15v5hnDzOd zVFE9~8(l<48NIGY8ywav^rWC>MnZ?NO15ELddVF0GLB_nbjpZA<236P8o&9C2?|UP zlnzuTr3~kQK1(%OXG#_!h5(K6CEjesr>M&C91#nlgCCnDA(sI(s;>$c2hSY6P0d{hkx)C#DM2=rY*qs*Erk)6nS(ByK4I zp?oToaXK<4*@fJMN%qD`c^z2`oF$ zP^{mrj2ZYhUJ}FB>q0l6EV5mZ*B{!-kPWn;+0=S`{c7~xBv{L04J8mRcC<^<{31pG zks#L5A*`k%7idD14nQ)o*sc_Mpc=@76&3-Ko3U;S=tA+}co#i`M4Onz%52{#FMAq@-EG8o@VCgca5ySAf%?`xFw>fTv^&-J6J|J+&k zf7Bcx|0}otpKqKv%Uxq~1w=YlXvO5#7p&ke&3R5P)^l@ z?a(=0iEE2>rvsEM1p3Ez>SEb=3Sx9u-4JBzElB?B3Q;Q;M)ro{b;^{qr?EO@IeDLv z$>bCzuR9roZUmFluHPLjM)&J5$}UFftZz8WYQ65`_4-z!QR_0d;lpY~B@=ZBjcU5? z=O`T=O@f2p8*7>d5DiUq;(Gh~PYx|)`(}`AkFM*3i71V84b`yjy!)WLP4%lp5ORn5 zf84U1=YPAdOIrF9+jTz;_J7!~O056mYZuwRE>Gh3T3iT>Vz_D*G=fMV%SX+@xd~t~ zud!1`Cvc@q#`!&0_zs z4<=SfKAdHLg+V*v%$8Ep?cEZ>Kq@{8qsn3*unB`y1T}6X5)C8fJ&gPsDIv;G6(YPt zrH_wytC@uqk(1l)eyc*eknZ)3?fPJlvsYFXxY^*ULL@G`3e7UELy-<0HT%p>GbE40 z<-*&_1BA$f0?|Hawm(Cy&z?|u%?*dJj;g%Gpmx;OLG^vK*OOkM}QfC^0=Esvps zfB}P-d_2!f@jK7gys>wAs;`=0g6B5D+5TYYWa!-9y*7ACFFzQ#baYO2XIj#vy7Mq~ za{vAr{)c-ov)*s3rFbxBR1a-OdJ>6wSF^YNEw zMt>du*Eznad3y3({NVMYD?wiTt240HkJfZqu)5aF`=^aMl^JFYjvb4Hlq2cYOmLw~34YKc%5bhyU*8HkcDYnEdaJ z>DLc`Z@Kxw!d>`+=VcRangG3WUnKNcca7{bTj9_D`*wE`ZI6xMCf#}Uq1_$QE{f2> z0M*4`bknPPfZGIq39T_flpuA*==NcnJoI5zYfh70?DaCF;!U;UaJb*tt0^SbVr$qS z<<{fO3fVnQ#6(uS6)5fu2=XJVSQm^GS|q(4M=|d};u+3g%=rD_wrhtYK^{jV`B;7k zJUmj94=avx87z32Mc3=`!`E6}9LhNSGi6gG7p@^(Fc+(j${@dYo<>sZ2b*J7~PSL4)h3YS5oO*(&*YE#K+S5eEf0>>7??~`S)tmPJ zGI~j&opJ}3esLjmD&O-Yo{sd1&s;y%#CzGL!PU`r zb?7bs)c=@?|MD6B<0!l(2he6*h8Fx*{8q?J$HK zk%>p!QV2uYf)awM4R*EB;%I7@qpA6vjz{;ue5ASV9q+s- zamym~&JSB|hhO_3tGG3PR%02URq1W6{*@)&POG=I^BN<+xGhpgak5+U&|@QY)FC4c z5LH5asWs(xeG}GD7X4^(vYRt*eq+bd5?P?OG#zQ8wOu(#Z1?Fh3~B{hTLrd8XnaLO z{y0q1b~7-pH)#@~9tOzytI)+n71d*H#tb(JnG5+#)wMhv3CD+kBTAWUm168`Vjl$! z9%2B)3Oxj$nv6M2z(-$&W*lq^JlMEy$UfLZjb*5(d?mL;S6$S@;gA}J8Ogz}MuE=+ zQFFH8uF|d@z_bNrG<;;uv!iMB5TOqfA>g%7#n6C{eZmD|(AJ5;FEJ^&xB*A8ovI|BUMP#{H&^|M2y%&rii)o{t|5Z0>(CRfyM$)O#EE&mgnogU+`WIPvcv z7S1ib6ug?5zJ7M;;9YU&heYeNxJ`BM5zUL=M+V4gM0lVo!BX!yvr)JUYb{;)4ixFo zS-4^MK0cdJcX8ESr)+m-H{pM|y@mLs*9kE+ULVcAf?}*wjM~}0Wiz)RxC0)=iJCI3?TePIi}hONS-)ww>^4LY82QG*ki@Y9?oLK# z$PfBi1&J2J07W>?Y{u2CBsZ0$BEVKE=^FiZybDKt2}Nfyikz!x6f)xDC~}VJG*osm z8e+{Ve4+?LMuv;F7EN3BE<#!n)_VL<-33s4uqezNBQ;gua$m`(%s~-qI(1dAyl`n? zP%pjo^EJcx)a5spex@#;>vf&{qsMh~vc9NEYo|s=ErhJ@|5~{a-REF2M!YHArovRz z1ej)IYD5u528h>V5})|x!{5EDUl&yr`#(O8?f*DW^u~WNn4;rkC7c--{d3r zTf>UossEB)3&wjPemnUIseLl_e*)|*G+8q4{kH|Xtg1ukJ_t7YVe9Nq56`!+=K)Zj zM(T>?ToOSa?{DK0Xf*bht(&wqIfqmTWCp4JZd<=);i-eGTt2D{M8^CV5YwhbT8Gmr zDPpaRl;|y`r9{nRrjK0ML0Pd=koiFdE|^>%@>MXf!qPr$&$wYX()heHt`QR^WSlyx zk(sqyJ_w;aC_;9B7520o4lXb+N({z^NVOWxX92w?J7%6jciPdOFU)L7gsTe3T3$PiJ60rJ(*Q>FgU;{l3x&2iDbP#Cd7ERLz` zsG3q&NxkB_3=kvW0stF_=+T&TgK_Nhg8*-o?ck!zsF}(Tr2%gUS{yj+n@7L>9esBQ zPF6`b54kzBW4&Eh!Hj-C*Yo}+E%;zT(t*{0773iA@Q?L&y>gK#S&(igRB*-PDdP6m z35d-h_Y%+Y-@mfmxX|%v^XmNR_~(}nr|GvJg*s+89(>4om_Dg`mviFYnVxf;6Q49% z`j=jw@Bc6!Zp6Ta43VDquLRpV&#BNfJ-BgKu(9;@fBNTNck=k}!)xCJf%2O3z;&D z!=P*!dkSVG_@Siagah6sk6TiL1Lu%R?-(6VajVLo1~gBoxjq}AnHz+&><579@QSDc2;aqAxtUD82`gVig!88TqpE9Sxq~v?vURw|MKJ4H(JEuy8=9KDZ z!|{Jds{S&%E|t}rMJHFdJ;N%bpk>SSzv-hAR-p+&!bR2YD{cE6i#h|;`D(^0d=*qDre>O1XQ`nz1C zeJ{2dl=Ji(_vPoAy)TT-7pJc!A5|MI1v4!~=hE!n@+GdBUilK&NuHrSfRTv|4$z5h zj_O6Qn}%fTV0#Q&F78kpn}#UVeQmCz?M52xOWA9M7MNv8S#@xQtcIfjeMIMk0i?dh{&C zQ1Re*zi`uqmp5NqKJ2`?m5>af)yF-C>1d2ka`*PXz8 znN|=u32@TH0qmhj4ESl%V=S7=ek*?8_O|M{Owl`RQfRTrYaP-Z4Y7+_TC zRyb5(nsxPep5tHSCggXK+k*l1_T_ucuaKqEU{rUDc%yfw>DF1U>c-1{ytQVd>OXNF zZNXvRA3u2Ix-&N>I9zCc?Yey>z*8}p$QZcbCWo5%w>Rmxf%J}k@wg^7gM_?;I~xD` zMHAveMn9c{WE~1G;5|^8)m*M!&GWo|Zs*gjMY63EV!aT9n9Z8*)0m(cMzdHezX*f! z+AN|0Ob$wEK(b&E@e~5|M)-9?oDvz4NksVxP@w*fdU8xEJ9LOHSGU-sZHq3YzX@CQd z6wtIn#=#OtY{_W_u@D3SOdhf5T7cAgt$9=|fW-M@8XZbnua8y-LEM6(*hGeuzs#KS zmCOF>)Zb&dgQya>=5u{4<>FDLs&|oW@M-ZRt#>B7i12L%wl_Oizcn%_6(W+AnjbYa zEKpnsDJ;~Pz>J+{+#P5gf8- z7n*ube*JYV$Kxi!>ER)|2tBVFCfxL=(4`LqqK}pya7#H|Cue_G3?W&>t4bw3Dk=v>vr*ZKm?hDO68zN z_o?%$*0#cn_B?JdOQW3a!pF}J38(%`(q-l{>QCTLssGoxw*I8+CNLTGyX!~l-)#%0 zMJ0Ai#1a0p<&KBiYlW6?;cYs3LjU1S{TJp6oA*^g=uN}PYHV5|jgHMnkhC_YxU=Y=V#*A?f z>P`6CV7Sl158pC}01h#7E|3C*?40x5fe&mik$Zuu;r0hM^6XPu1m~D&;F^mO8sSr` zRRAq!k6N-%k9Mxd{Qr~mHn3^k`QE3UHxJ`UyhCd^-_xg6(?xL&G#jhXw;i&;a(6+h-%;>+JZxlD5Gjt_SE~)+m z9!EV7FfoYQ1qASBVc{pho^c%<{y{PEsI6{()Mlz@iXLAY8WeN)CqJG(+KHAOc<{S7 zUEf(7puE!HL>*roH8Zrh>qYz^9zSA)ICxuYX6Gi`h>+8OrT(pUMuhe8aGq@UkK)^Q zj89Oq%7hA;tX#$#brL5Cg(d}4SM<8FZFkJ!CW0jdbnbLu^RELYUp9iGzyw!x#o;Jn zOM5IWC&D1GRP6p^k}Z>Yg#z*dQw-Qt{FRq+{$^+aU`laj(}_d^bC(EwjQwCoQBEx5 z6#8FAfbV(>uFOOEub;{wRU$}vYywuM@<3*yPP*!@KPafsOsj9TLFe3Ly)`c&*Tqsp4^ zz(6k%_#25vVeCGN$bh&jtubf4>3&$C?bzb6MK|=tavwD^3(gaE>x4R!%BkJbVAdLq zYqbzF?=i57 zCxF`6+~cfZ!x(4CMGN@tj^0`TS1(`z*mTpT#gTz|DU54o^csZ;fFRLn;2^ly(hEw^ z^d}31XpVPrL5PZ`cK9=5+jb0^pd|((*#tIIpV&&CMeKD1zvnmIz`Fm`ANFsLb7(|B z$T$ir#zFyf9ZjFu)R3W~T|%W*`2L?I+6lq#{@>;W-_4zwzW|Hf{=M(pG>d!&V~^(M z`r)J9@HooO7YC1ocdgBRSV6s+=z|-t19uG}3-Pd{H6hqma{>n0Pz@Y3lA(a(@V+yE-I#Gb^^q>&t3FC4^qL-K=x^xc2ft++l2?&m( zjFszfKqK=~I{cULvX&Eg=Xfn!-V%pFTwhm}(P;HhH*C8% zypF<8E6m80J6gVqbP!cTdCXN?S<#;PlC}NRup&H~T9rn~>PznE(9biD!cWRF*{L?N zKBa013>m}cBvSFvH%;w>7U@-;R3Tj>rJc-a{WI!JJe*B_bxf`iiH;`^Pt$yt%qLC(6fYOOjuNXeOB_V}M=v%G`gNHf3E>ek+t^0-K)r9EavoE^CMJB<3U5}s^`O~RDKV9U};-`wOw zSnnYtkEJ&xJQOM&%@hHByEC|y+9v%S%dFZ^iQ;|o-O`OShbq^32`vK~{Y%gw5Na01{^mnq>u5GKT6 zE-)nr9>uITi%*qhU9>MXWa=S$-QXez0o?B_y2G60#EFU`mG7aHWr(~y8N0t?r@%R= z;M3WC@Ig0iC=NYtC85tlkxFhOu_6nEM!v`C%cSwl=gTa_OL0OB7zZvRd>t9EO6{ox zFULa;8z3CI6Sl;fNI2IaK2jw58AT!pKXsN-TSFNJ4R?w5U{hd}!}-`jfwk-7Qe7#0 zZDMJI=NT-fHw|>R^b?2e+r<}SE@OCtsDUB=OSXxO;3VCvWh~ z9J)TfJHyt{ODx79pS!xH>vmT^Z~bB4jJ_s4Gyyoj>yi^hu=3TswB{J|2A*5h92)w{ zio(Oyx^|K}o1K{qzvp;TG&ES?v&Tw6H8?}8KmO}CB|{%PNA|i6mD2ED1-q8g&R4sK zu5n6C@>2p*dtdS*I@}>|cFbsO{rh@Xf9G^g^`!%F4(|^4VGRZS#$ob=sPXQ^n1;dx z)fC$K(tLL}A{Q0N%kRvL$k&~3$~T=%z~z1?5+DUxS7q|i=$!5yGugAk&&~-!s*`u^ z#&mLFP?dFMcLVbKTVQio0)p2X4A$KVkp;`R1WtQ|$$Pz+b~Dg?ANf-#4&zD`3UTBu zT^``Fzn!z#50_TAOr1t$3`;oBxL{sC%y@iQwICh>VpOu6i?GrW2v8|bmj-NlE~0k& z-1qpY}+NvWwKD$)+bigQDJ`*H|Lauw`@)E?KMNt(x+ zF~Qjrd?_M7y2^%J45iIbO7uYGZFh9kfeI)H{(>*-lQl(08F|qsrjPTTJ}hmv%~`bl z1qR|WC{walI7{weoVbfn2oA;9x{1dG@^F+8W-M0C6XKTLgN{D%`%qDEBhs$jAiPBB z8XLi_Jm6GqyYNDyL8d~>%RVpwv?`j>kU0W{sx0NxC0 z;H?0>@~+nq#P1ojsrt!>EiGCsOOAGfO&c*uZ&NI51z`RW>Vr{W5yc{_4*E1~t86GQ zAcScOtq6l+5$5fC&*q089v^(Qh@$HV0^(VBT(_MLfD-JMWTE?jsFdA#B!r*(z&KR(VVD$3Z_;*U(T1+6=1;so;!hOB9+S%IaUGfWG;G&?VzN} z6F!T$2!?4R0riL{VrN2_u`a{|2?ctaUVKqW$WXisA~$q^q-e3^H7Au`3r5@G5^^RM zG47~>lO=tE$XvgLjjUb9<_3)h8;@!jU-jLR8AXU&hosazS@j=YAJvB)Q-vFl#N+#S zeQD((l{88_jMw#W(Eyv?wY=t+2A7(YP=`#|FH+SXE^mz4tN$ycDE6iIJCYv^4NUz{ z_Mz^9r8EC##6)AMDY#^mR(0BK1IDYrTl(g9{qv5h|6|Bpi!IH+VHl!=I`c;p%i0CI zvq<@vWvL#}-Jw(G<2lUm%6E$nmMK&R5O$)+9%&YgQOI>PGHDL8ljohT2b2_0kdaE& zP1>&SJ%*Bh=)A$zKzoxl`Yq}#4XB1f8cSNnJhtjfv{bs@&lDQGNF}fr5H#UQl|xLg zp6s(bnX0c)>Xk~M*MN;y%1(Vm3fZO*nSgkloW%5ksmfz|F(=@@{TjGWYBjqD|1$M} zK~I5r8K7WE{BS9a`CIj(U^md1%%k5>?JcU9F4YJ4ofg4g$5B?q-rI9AqNip?To?x! zjGK%zD_}FQ2{5#-E5#XD4h@vo)uP~uD)G9G4} z*a|PBz>$<#PC*7i$!L0XHI9cSQNI#b9styXwwV<9w8sarzNdy}wC>>eL3JU(+yeL< z0RO@R3+-Alx(OAKpf2tG*dBOS!vd`Y(A7t9@f_LAObYID+}k)}Fgk^|g%qp4;kaDj z_3Xy(+%Q15h64zOGQn+QJvPEGLZV}Zde-uZc^rQ*ta%2UVE4_v-7!W>HeX!W^e>!2 zSC9x(qfmxGy@3lJuJ_}EgBV_HERUMuHwtUB&y2AYwj?m7K=_Il1NP)?8{O41fm08u z6b$}vLwNDxL|%j8gSPOhF}*$CK0Y59#q2^$!}bE}?4n+6Zg~KH{6RUiLXskb+B#BU zSYSG>I;DPyv8J&I%J~;I%<2PwN z7=JJ_Ib15-a;_}5U&I&08Zxt4h?H_i*sB0>n@T{%MSD@a;(c*s_qye7WjJHfP<&=3 z*Mr;{XcBO_ZD@6q`CpRsmAw)%oMJJT91BM(tf2p`D=Gphn;MB7E0nee0a>+#RC;rp(Nbx{R@Ara2}N z&7E{B-a@M?A2jic^J*8_IUf%Ylc-a*WQ_ZJ+%X>%d>r4%W9lICK84VgvvUjOcOqDt z#N~HRNS=UmIW&`hTX`Bf#cOd ztzeWMQOL0(Q!2tVzk;F2pg*0EitLuFZ|EY8_l}g5!_GpO%d}Tr{2~Ng7zL!W%-|)goyGzZ~B07m}u%xV7mvao0;1 z(83(19LP;t+>LHScoP9G?;sE1z&ZjGY)ajLWOTj2&ScX?T!5{$eksp2@~G1>@atO$ zy>#7c8RuFJ%__HTd5HKIx<&9eSWYqL9PVk|Gz>rmkBycXB#hm$$*%=>Fnxj%wUwXN z><);m%}o+`P7D)}TTBLFr~zvfheHR(5pNjL;!DFg4iq?U@Jt_s2N%=ia3lmsw+;VK zG|~T97MxkgoNA|tX628hnl4Cf`yVWgOY zC-H=$b!z%$3F0q!*-#R23y>0>hV_h$F)~sE39{F+-s`vWG}hQoPqsKsvg>U+<^$VAJ*Nw|TO(s)Mwr6B}=H#4|Q-kL2h7W4Wwu1qH5eC!DvAk-=2|Y&149ALOlw@EFd(g&6UHTdb5Q80zW=oZ>Se(mBMgY#abRF2C7`-P7!5hCA%z@o zEllDPU}0(dUmruS&WahZ{qiGF zm13TdE%~s9P2gZq5OJvA0l|O?78+`ln`IQ79T-3Y3M$R@(Nbj@(FMr8u;9pHp#w@G z%tjhc!DL*576TyR-eM`$L4)9B9FX=Dixw$yfF%hKUWeGm(kf$*1_BYde9Iw154e)o zZUwLnSE9%gO3Tn)KSHOBZlaMU#TtfayjRd-!gpsQC=759+@_0e-E_TyxM?YehyGC; zSY^1RIoUeQ=75&H*hGrcBQ#`#pk)B=r-6{l+zY7ZLsSap2bLQK!R_8!VTc=c)=tJA zJcFw2Q$A>+wZpBoLup_RUI}mE`#vUV-Ne)bf4cBmxV`w&_P2$1aEgkoT3q){cxE}@ zN(?&oKmIyA_{fR4ZaKUL5YA zrN=Wlc3!*mSL;~=QLDI7oKd?zmOTGVcCJ#F!*w3No?rhL>by)aK5LLrSWD2_OLX$T z`Oa@m=TS!V$}dq^Y}85D_olgIx#}EodQ4jvTZ)L!OV{-?L8eYN^ttQG$@}15Bp;`E zr?*^P%j*nL8?CcH1@!_u5;#?f2C6k$S9nk$3BGfIP!(PM+Wwum>JihV?cdeC%PwZ; zPWpHA!+kE|;E~HDe*jdPpwde=V_ZOr=JO3q7p+Q%#Ei)C%)6)tK* zKGz@&Yg?3uvZGQbghVth0b4a=)JI1%mO-~p-)AIcM_xL^;MIaSr{XM?V9B?aFvyNE z(2QgF0|+0&=PfL}Y9kpqk$`TdC_9@5(*gof3=QDIM9R37jX?Cdg`O%GCKQpwEI{)a zq$tUx{eCc@DUTzi067#E!R46O7=^u#t27kKc(90?XEh)`&WnstgX#87&7>k4L`)P0 zX3#;)A_rlOPi?t{YI@WiL<;5f4FewTJ8yTm1jbM=G8JDVzeKdSf_xLeGeg}sp{|Ci z=_cfpyFm^=gYq6!U+#f@10mON8?xdc3@^$Txs0*{CaTln7&D9X+ zcNY|l0E{=iThPqabLN&!!#K1XuD>k|Z*ohINAT6c3x}1#EpWS_0e>Xc!m3X9_8|oU z2(AF9XB~bh#E_>S&{;eU^;&}jHIK*Jr18mGyu%Spz_kFg~h%{7!RSE#2j4% z#sq`*`0r|a(!?rQ76~S-jy+X|VO@7kMBA5Ha=nuxbLA7N0-wohB5_!9V6_QTIhh#s zTDB``&{bUETE)S_!d@ZBE8VBL7rjkF8N7P2RWlr72{FF5;{X8NA;)jr*64{MqER9t^`5eAaf8jJlhWJ0V zR*|dMi4nu|){bP|VbV18O{#~dPX4?cjYt@2$J99X!fN9fpI+iiL6ob|Y6El<%TcXe zImCn015X)Mec}Pq!?*Msu>;X!)M;@lhh@&xe|WtzAYEv(O}O@-z5ZjcIMb&?U>wMV7%$9(MDP|dyGO|%7L!K{ zBJ(}`Hhf4S&2j9`Hg3Kq7}N`D(4-g(wji0r0N}0X<|GDvclk--8DVaT!csX-(Fmh)AfHX^o5NGeZlJ zpFB!$PTvw@F@RF}g`{QAEC%leh7HW|AKwqO|MfQaEh0I{1SolX2BP@_zBOPL?(K$= z8G_~>hBKTQlLuiS=+sbdetg3`z5q}TY-^_aG#LOSFQ{ z#+qi7{h#9JpI(5<@&=cBhd=$zT63qTLQu(2J2Z^;Am}1WWjgcOrwFzL7l)LYkAw@^QFZ4n18p7 ztFfHNIl*aJ*qpd4HTfCY6bYp}4H|PQ0O_m_@rvF|#u)7@vZ+KaDpnkNSM`b#&NRV5WEsvdoBLvxb{w%B^J_b+xp^vU`ZdSZsv0d;1^sURozyvWNC$ zOZBkuQMkc@Wd*f!WK$aYuqJ45gJL1Aqg$i3(TfbZUve1_grI6jH4Zl`cIP%Ly<)u8 zN&r-p=8xvL7j-w?@(11p;w`Y5@pm3q3Bv;-gbWdlE9-hKgLBP?d2lz*bYT!{EH&4J1yMRgUJf&YK`-ieX$0W0p!yira~M$} zPPZ4`ih+y|&h|_Ou*bXZ$+AK+WFm=v%XPVYJ|gE}m_;lgoGj(d zP}o*^p=nrA`?DE~B2mthom}#(`{+1Ma6%{AEq6GP5wP>CPS9x s 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%0DeXL87xYUzOsmm5XOanC6!y&B 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 bundled studio HDRI all package screenshots are lit with.

v3ASgNR`hVP$~l^4Sp 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⪙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�!l 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 z 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�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

+ /// The HDRI is missing from the package. + private static Cubemap LoadHdri() + { + var hdri = AssetDatabase.LoadAssetAtPath(HdriAssetPath); + if (!hdri) { + throw new InvalidOperationException($"The screenshot HDRI is missing at {HdriAssetPath}."); + } + return hdri; + } + + /// + /// Removes previously generated screenshots and their bounds, so a package never picks up images of an + /// earlier state of the table. + /// + internal static void DeleteGenerated(string outputFolderPath) + { + foreach (var fileName in ScreenshotFileNames) { + AssetDatabase.DeleteAsset($"{outputFolderPath}/{fileName}"); + } + AssetDatabase.DeleteAsset($"{outputFolderPath}/{FilenameBounds}"); + } + + /// + /// as a task, completing once all shots are written. Await it on the main thread + /// without blocking it, since the shots render across several editor frames. + /// + internal static Task GenerateAsync(TableComponent tableComponent, string outputFolderPath) + { + var completion = new TaskCompletionSource(); + try { + Generate(tableComponent, outputFolderPath, + result => completion.TrySetResult(result), + error => completion.TrySetException(error)); + } catch (Exception error) { + completion.TrySetException(error); + } + return completion.Task; + } + + internal static void Generate(TableComponent tableComponent, string outputFolderPath, Action onComplete, Action onError = null) { if (!tableComponent) { @@ -105,11 +149,7 @@ internal static void Generate(TableComponent tableComponent, string outputFolder throw new InvalidOperationException("No PlayfieldComponent was found under the selected table."); } - if (!hdriCubemap) { - throw new ArgumentNullException(nameof(hdriCubemap), "A HDRI cubemap is required."); - } - - var routine = GenerateRoutine(tableComponent, outputFolderPath, hdriCubemap, hdriExposure, playfield, onComplete); + var routine = GenerateRoutine(tableComponent, outputFolderPath, LoadHdri(), HdriExposure, playfield, onComplete); EditorApplication.CallbackFunction driver = null; driver = () => { bool moveNext; @@ -194,6 +234,10 @@ private static IEnumerator GenerateRoutine(TableComponent tableComponent, string var camera = InstantiateCamera(); try { + // The render target is portrait, but a fresh camera takes its aspect from the Game view. With the + // prefab's gate-fitted physical lens the field of view depends on the aspect, so fix it before + // framing; otherwise the table shrinks whenever the Game view is landscape. + camera.aspect = PortraitWidth / (float)PortraitHeight; var cameraDistance = CalculateTopDownDistance(playfieldWidth, playfieldHeight, PortraitWidth / (float)PortraitHeight, camera); var cameraPosition = new Vector3(playfieldCenter.x, playfieldCenter.y + cameraDistance, playfieldCenter.z); camera.transform.SetPositionAndRotation(cameraPosition, TopDownRotation); diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs index a16e4b324..890215ce1 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageWriter.cs @@ -62,6 +62,12 @@ public class PackageWriter : IPackagedContentResolver private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + /// The folder the package screenshots are read from, or null for a package without screenshots. + internal string ScreenshotFolder => _screenshotFolder; + + /// The table being exported. + internal GameObject Table => _table; + public PackageWriter( GameObject table, bool runtimeCompressSideChannelTextures = true, diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/VpeExportRunner.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/VpeExportRunner.cs index e6fa0c999..880951a07 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/VpeExportRunner.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/VpeExportRunner.cs @@ -62,6 +62,10 @@ public static async Task RunAsync(PackageWriter writer, string path, strin }); try { + if (!await RenderScreenshotsAsync(writer, title)) { + Logger.Info("Export canceled: screenshots could not be rendered."); + return false; + } await writer.WritePackageAsync(path, progress, cts.Token); return true; @@ -80,5 +84,41 @@ public static async Task RunAsync(PackageWriter writer, string path, strin EditorUtility.ClearProgressBar(); } } + + /// + /// Renders the package screenshots afresh, so the player's table images always show the table as exported. + /// Earlier screenshots are removed first; if rendering fails, the user decides whether to export without them. + /// Returns false when the export should stop. + /// + private static async Task RenderScreenshotsAsync(PackageWriter writer, string title) + { + var folder = writer.ScreenshotFolder; + var table = writer.Table ? writer.Table.GetComponent() : null; + if (string.IsNullOrEmpty(folder) || !table) { + return true; + } + EditorUtility.DisplayProgressBar(title, "Rendering screenshots…", 0f); + string failure; + try { + PackageScreenshotGenerator.DeleteGenerated(folder); + await PackageScreenshotGenerator.GenerateAsync(table, folder); + return true; + } catch (Exception ex) { + Logger.Error(ex, "Rendering the package screenshots failed."); + // Shots are written one by one; drop the ones that made it, so a partial set is never packed. + try { + PackageScreenshotGenerator.DeleteGenerated(folder); + } catch (Exception cleanup) { + Logger.Warn(cleanup, "Could not remove the partial screenshots."); + } + Debug.LogException(ex); + failure = ex.Message; + } finally { + EditorUtility.ClearProgressBar(); + } + return EditorUtility.DisplayDialog(title, + $"The table screenshots could not be rendered:\n{failure}\n\nWithout them, the player shows no image of the table.", + "Export Without Screenshots", "Cancel"); + } } } diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Table/TableInspector.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Table/TableInspector.cs index 4f6b2454c..7e4f673ad 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Table/TableInspector.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/VPT/Table/TableInspector.cs @@ -37,18 +37,11 @@ public class TableInspector : ItemInspector private bool _packageFoldout; private bool _runtimeCompressSideChannelTextures = true; private bool _runtimeCompressNormalMaps = true; - private string _screenshotHdriPath; - private Cubemap _screenshotHdri; - private float _screenshotHdriExposure; private const string RuntimeCompressSideChannelTexturesKey = "VisualPinball.Unity.Editor.TableInspector.RuntimeCompressSideChannelTextures"; private const string RuntimeCompressNormalMapsKey = "VisualPinball.Unity.Editor.TableInspector.RuntimeCompressNormalMaps"; - private const string ScreenshotHdriPathKey = - "VisualPinball.Unity.Editor.TableInspector.ScreenshotHdriPath"; - private const string ScreenshotHdriExposureKey = - "VisualPinball.Unity.Editor.TableInspector.ScreenshotHdriExposure"; protected override void OnEnable() { @@ -59,13 +52,6 @@ protected override void OnEnable() _metadataProperty = serializedObject.FindProperty(nameof(TableComponent.Metadata)); _runtimeCompressSideChannelTextures = EditorPrefs.GetBool(RuntimeCompressSideChannelTexturesKey, true); _runtimeCompressNormalMaps = EditorPrefs.GetBool(RuntimeCompressNormalMapsKey, true); - _screenshotHdriPath = EditorPrefs.GetString(ScreenshotHdriPathKey, PackageScreenshotGenerator.DefaultHdriAssetPath); - _screenshotHdri = AssetDatabase.LoadAssetAtPath(_screenshotHdriPath); - _screenshotHdriExposure = EditorPrefs.GetFloat(ScreenshotHdriExposureKey, 4f); - if (!_screenshotHdri && _screenshotHdriPath != PackageScreenshotGenerator.DefaultHdriAssetPath) { - _screenshotHdriPath = PackageScreenshotGenerator.DefaultHdriAssetPath; - _screenshotHdri = AssetDatabase.LoadAssetAtPath(_screenshotHdriPath); - } } public override void OnInspectorGUI() @@ -94,17 +80,6 @@ public override void OnInspectorGUI() $"Screenshot preset: {PackageScreenshotGenerator.PortraitWidth}x{PackageScreenshotGenerator.PortraitHeight} PNG using a computed top-down shot at {PackageScreenshotGenerator.FieldOfView:F1} deg FOV.", MessageType.None); EditorGUI.BeginChangeCheck(); - _screenshotHdri = (Cubemap)EditorGUILayout.ObjectField("Screenshot HDRI", _screenshotHdri, typeof(Cubemap), false); - if (EditorGUI.EndChangeCheck()) { - _screenshotHdriPath = _screenshotHdri ? AssetDatabase.GetAssetPath(_screenshotHdri) : string.Empty; - EditorPrefs.SetString(ScreenshotHdriPathKey, _screenshotHdriPath); - } - EditorGUI.BeginChangeCheck(); - _screenshotHdriExposure = EditorGUILayout.FloatField("Screenshot HDRI Exposure", _screenshotHdriExposure); - if (EditorGUI.EndChangeCheck()) { - EditorPrefs.SetFloat(ScreenshotHdriExposureKey, _screenshotHdriExposure); - } - EditorGUI.BeginChangeCheck(); _runtimeCompressSideChannelTextures = EditorGUILayout.ToggleLeft( "Compress sidecar textures (Unity runtime compression)", _runtimeCompressSideChannelTextures); @@ -119,44 +94,37 @@ public override void OnInspectorGUI() EditorPrefs.SetBool(RuntimeCompressNormalMapsKey, _runtimeCompressNormalMaps); } if (GUILayout.Button("Generate Screenshot")) { - if (!_screenshotHdri) { - EditorUtility.DisplayDialog("Generate Screenshot Failed", - "Please assign a screenshot HDRI cubemap before generating a screenshot.", "OK"); - } else { - // Run the capture *outside* OnInspectorGUI. Rendering - // (SubmitRenderRequest/ReadPixels) inside the IMGUI event uses the - // GUI frame's stale culling, so the per-shot lamp/bulb toggles - // don't take effect and later shots leak the first shot's state. - var table = tableComponent; - var hdri = _screenshotHdri; - var exposure = _screenshotHdriExposure; - EditorApplication.delayCall += () => { - try { - // Generate runs across several editor frames (it lets HDRP settle - // lamp state between shots), so the result arrives via callback. - PackageScreenshotGenerator.Generate(table, @"Assets/Screenshots", hdri, exposure, - result => { - Debug.Log( - $"Generated package screenshot at {result.AssetPath} " + - $"(camera Y: {result.CameraPosition.y:F3}m, distance: {result.CameraDistance:F3}m, " + - table); + // Run the capture *outside* OnInspectorGUI. Rendering + // (SubmitRenderRequest/ReadPixels) inside the IMGUI event uses the + // GUI frame's stale culling, so the per-shot lamp/bulb toggles + // don't take effect and later shots leak the first shot's state. + var table = tableComponent; + EditorApplication.delayCall += () => { + try { + // Generate runs across several editor frames (it lets HDRP settle + // lamp state between shots), so the result arrives via callback. + PackageScreenshotGenerator.Generate(table, @"Assets/Screenshots", + result => { + Debug.Log( + $"Generated package screenshot at {result.AssetPath} " + + $"(camera Y: {result.CameraPosition.y:F3}m, distance: {result.CameraDistance:F3}m, " + + table); - var screenshot = AssetDatabase.LoadAssetAtPath(result.AssetPath); - if (screenshot) { - Selection.activeObject = screenshot; - EditorGUIUtility.PingObject(screenshot); - } - }, - exception => { - Debug.LogException(exception, table); - EditorUtility.DisplayDialog("Generate Screenshot Failed", exception.Message, "OK"); - }); - } catch (Exception exception) { - Debug.LogException(exception, table); - EditorUtility.DisplayDialog("Generate Screenshot Failed", exception.Message, "OK"); - } - }; - } + var screenshot = AssetDatabase.LoadAssetAtPath(result.AssetPath); + if (screenshot) { + Selection.activeObject = screenshot; + EditorGUIUtility.PingObject(screenshot); + } + }, + exception => { + Debug.LogException(exception, table); + EditorUtility.DisplayDialog("Generate Screenshot Failed", exception.Message, "OK"); + }); + } catch (Exception exception) { + Debug.LogException(exception, table); + EditorUtility.DisplayDialog("Generate Screenshot Failed", exception.Message, "OK"); + } + }; } if (GUILayout.Button($"Save as .{ext}")) { var tableContainer = tableComponent.TableContainer; From a20c268bbb5bd5ed93fcd814ba05c880d8bdd9bf Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 17:32:10 +0200 Subject: [PATCH 23/26] packaging: light only the inserts in the lights-on screenshot Hybrid inserts show their authored intensity outside play mode and were never switched, so the lights-on and lights-off shots came out the same. The generator now drives every insert, including its extra bulbs, on for the lights-on shot and off otherwise, restoring them afterwards. GI, bulbs and flashers stay off in all shots. --- .../Packaging/PackageScreenshotGenerator.cs | 82 ++++++++++++++++--- 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs index e4a28c031..800f168cd 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs @@ -62,11 +62,11 @@ internal static class PackageScreenshotGenerator private const float MinFarPadding = 0.5f; private const byte BackgroundTolerance = 4; - // Shot 1: env (directional) + HDRI, playfield lamps on (flashers off). + // Shot 1: env (directional) + HDRI, inserts lit, every other lamp (GI, bulbs, flashers) off. internal const string FilenameLightsOn = "table-lights-on.png"; - // Shot 2: env (directional) + HDRI, all playfield lamps off (incl. faux bulb). + // Shot 2: env (directional) + HDRI, all playfield lamps and inserts off (incl. faux bulb). internal const string FilenameLightsOff = "table-lights-off.png"; - // Shot 3: HDRI only, no directional, playfield lamps on (flashers off). + // Shot 3: HDRI only, no directional, all playfield lamps and inserts off. internal const string FilenameHdriOnly = "table-hdri.png"; // The generated screenshot files the packager pulls into the .vpe (in shot order). @@ -313,9 +313,9 @@ private static IEnumerator GenerateRoutine(TableComponent tableComponent, string private enum PlayfieldLightMode { - // Non-flasher lamps (inserts + GI) on, flashers off. + // Inserts on; GI, bulbs and flashers off. On, - // All lamps off and their bulbs hidden. + // All lamps and inserts off, their bulbs hidden. Off, } @@ -359,6 +359,8 @@ private readonly struct Entry { public readonly LightComponent Lamp; public readonly bool IsFlasher; + // A lamp under a playfield insert (a VPX-style insert mesh, or a hybrid insert it drives). + public readonly bool IsInsert; public readonly Light[] Lights; public readonly bool[] LightEnabled; // Emissive meshes that LightComponent.Enabled drives (the faux bulbs), @@ -366,10 +368,11 @@ private readonly struct Entry public readonly Renderer[] EmissiveRenderers; public readonly bool[] RendererActive; - public Entry(LightComponent lamp, bool isFlasher, Light[] lights, bool[] lightEnabled, Renderer[] emissiveRenderers, bool[] rendererActive) + public Entry(LightComponent lamp, bool isFlasher, bool isInsert, Light[] lights, bool[] lightEnabled, Renderer[] emissiveRenderers, bool[] rendererActive) { Lamp = lamp; IsFlasher = isFlasher; + IsInsert = isInsert; Lights = lights; LightEnabled = lightEnabled; EmissiveRenderers = emissiveRenderers; @@ -379,6 +382,26 @@ public Entry(LightComponent lamp, bool isFlasher, Light[] lights, bool[] lightEn private readonly List _entries = new(); + // Hybrid inserts show their authored intensity outside the game, per bulb. + private readonly struct InsertEntry + { + public readonly InsertLightComponent Insert; + public readonly float Intensity; + public readonly float[] ExtraIntensities; + + public InsertEntry(InsertLightComponent insert) + { + Insert = insert; + Intensity = insert.Intensity; + ExtraIntensities = new float[insert.ExtraBulbs?.Length ?? 0]; + for (var i = 0; i < ExtraIntensities.Length; i++) { + ExtraIntensities[i] = insert.ExtraBulbs[i]?.Intensity ?? 0f; + } + } + } + + private readonly List _inserts = new(); + public PlayfieldLightSnapshot(TableComponent tableComponent) { // Flashers are identified via the lamp mapping (IsCoil), resolving @@ -420,6 +443,12 @@ public PlayfieldLightSnapshot(TableComponent tableComponent) AddEntry(lamp, isFlasher); } + foreach (var insert in tableComponent.GetComponentsInChildren(true)) { + if (insert) { + _inserts.Add(new InsertEntry(insert)); + } + } + // Group lamps (LightGroupComponent) can drive raw Lights + faux-bulb meshes // that are NOT wrapped in their own LightComponent (e.g. L51's two skull-eye // sub-lamps). The LightComponent pass above misses those, leaving their bulbs @@ -459,7 +488,8 @@ private void AddEntry(LightComponent lamp, bool isFlasher) rendererActive[i] = emissiveRenderers[i].gameObject.activeSelf; } - _entries.Add(new Entry(lamp, isFlasher, lights, lightEnabled, emissiveRenderers, rendererActive)); + var isInsert = lamp.GetComponentInChildren(true) || lamp.GetComponentInChildren(true); + _entries.Add(new Entry(lamp, isFlasher, isInsert, lights, lightEnabled, emissiveRenderers, rendererActive)); } // A LightGroupComponent whose member sub-lamps have no LightComponent of their @@ -501,7 +531,7 @@ private void AddOrphanGroupEntry(LightGroupComponent group, bool isFlasher) rendererActive[i] = emissiveRenderers[i].gameObject.activeSelf; } - _entries.Add(new Entry(null, isFlasher, lights, lightEnabled, emissiveRenderers, rendererActive)); + _entries.Add(new Entry(null, isFlasher, false, lights, lightEnabled, emissiveRenderers, rendererActive)); } // Resolve a mapped lamp device to the concrete LightComponents it drives, @@ -525,9 +555,9 @@ private static void ResolveLightComponents(ILampDeviceComponent device, List Date: Wed, 14 Oct 2026 17:46:42 +0200 Subject: [PATCH 24/26] packaging: keep flasher-driven insert bulbs off in screenshots Coil mappings are resolved to the insert bulbs they drive, directly by bulb, through a lamp above the insert, or per group member, and those bulbs stay off in the lights-on shot. --- .../Packaging/PackageScreenshotGenerator.cs | 65 +++++++++++++++++-- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs index 800f168cd..6876c94fd 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs @@ -388,10 +388,13 @@ private readonly struct InsertEntry public readonly InsertLightComponent Insert; public readonly float Intensity; public readonly float[] ExtraIntensities; + // Per bulb (0 is the main one): driven by a flasher, so it stays off in every shot. + public readonly bool[] Flasher; - public InsertEntry(InsertLightComponent insert) + public InsertEntry(InsertLightComponent insert, bool[] flasher) { Insert = insert; + Flasher = flasher; Intensity = insert.Intensity; ExtraIntensities = new float[insert.ExtraBulbs?.Length ?? 0]; for (var i = 0; i < ExtraIntensities.Length; i++) { @@ -413,6 +416,7 @@ public PlayfieldLightSnapshot(TableComponent tableComponent) // non-deterministic since the uncovered set shifts with table state). var flashers = new HashSet(); var flasherDevices = new HashSet(); + var flasherBulbs = new Dictionary(); var lamps = tableComponent.MappingConfig?.Lamps; if (lamps != null) { var resolved = new List(); @@ -423,6 +427,7 @@ public PlayfieldLightSnapshot(TableComponent tableComponent) if (lampMapping.Device is Component devComp && devComp) { flasherDevices.Add(devComp); } + ResolveInsertBulbs(lampMapping.Device, lampMapping.DeviceItem, flasherBulbs); resolved.Clear(); ResolveLightComponents(lampMapping.Device, resolved); foreach (var lc in resolved) { @@ -444,9 +449,20 @@ public PlayfieldLightSnapshot(TableComponent tableComponent) } foreach (var insert in tableComponent.GetComponentsInChildren(true)) { - if (insert) { - _inserts.Add(new InsertEntry(insert)); + if (!insert) { + continue; + } + if (!flasherBulbs.TryGetValue(insert, out var flasher)) { + flasher = new bool[InsertLightComponent.MaxBulbs]; } + // An insert under a flasher lamp, or named like one (the VPX "F_" fallback), is a flasher as a whole. + var parentLamp = insert.GetComponentInParent(true); + if (parentLamp && (flashers.Contains(parentLamp) || parentLamp.name.StartsWith("F_", StringComparison.Ordinal)) || insert.name.StartsWith("F_", StringComparison.Ordinal)) { + for (var i = 0; i < flasher.Length; i++) { + flasher[i] = true; + } + } + _inserts.Add(new InsertEntry(insert, flasher)); } // Group lamps (LightGroupComponent) can drive raw Lights + faux-bulb meshes @@ -536,6 +552,40 @@ private void AddOrphanGroupEntry(LightGroupComponent group, bool isFlasher) // Resolve a mapped lamp device to the concrete LightComponents it drives, // expanding LightGroupComponent references recursively. + // Mark the insert bulbs a coil (flasher) mapping drives: one bulb of an insert mapped directly, + // every bulb of an insert under a mapped lamp or group. + private static void ResolveInsertBulbs(ILampDeviceComponent device, string deviceItem, Dictionary result) + { + bool[] Bulbs(InsertLightComponent insert) + { + if (!result.TryGetValue(insert, out var bulbs)) { + result[insert] = bulbs = new bool[InsertLightComponent.MaxBulbs]; + } + return bulbs; + } + switch (device) { + case InsertLightComponent insert when insert: + Bulbs(insert)[InsertLightComponent.BulbIndex(deviceItem)] = true; + break; + case LightComponent lamp when lamp: + foreach (var insert in lamp.GetComponentsInChildren(true)) { + var bulbs = Bulbs(insert); + for (var i = 0; i < bulbs.Length; i++) { + bulbs[i] = true; + } + } + break; + case LightGroupComponent group when group: + // Each member takes part with one of its lamps, such as a single bulb of an insert. + for (var index = 0; index < group._lights.Length; index++) { + if (group._lights[index] && group._lights[index] is ILampDeviceComponent child) { + ResolveInsertBulbs(child, group.LampId(index), result); + } + } + break; + } + } + private static void ResolveLightComponents(ILampDeviceComponent device, List result) { switch (device) { @@ -601,11 +651,12 @@ public void Apply(PlayfieldLightMode mode) if (!entry.Insert) { continue; } - entry.Insert.Intensity = level; + entry.Insert.Intensity = entry.Flasher[0] ? 0f : level; if (entry.Insert.ExtraBulbs != null) { - foreach (var bulb in entry.Insert.ExtraBulbs) { - if (bulb != null) { - bulb.Intensity = level; + for (var i = 0; i < entry.Insert.ExtraBulbs.Length; i++) { + if (entry.Insert.ExtraBulbs[i] != null) { + var flasher = i + 1 < entry.Flasher.Length && entry.Flasher[i + 1]; + entry.Insert.ExtraBulbs[i].Intensity = flasher ? 0f : level; } } } From 8f19b605030ebd5c7c71fa29e675690a9827bceb Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 17:58:44 +0200 Subject: [PATCH 25/26] packaging: leave inserts under light groups to the insert pass The orphan group pass picked up the spill lights and meshes of inserts parented under a light group and switched them off in the lights-on shot, overriding the insert pass. --- .../Packaging/PackageScreenshotGenerator.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs index 6876c94fd..0c4192125 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs @@ -517,8 +517,10 @@ private void AddOrphanGroupEntry(LightGroupComponent group, bool isFlasher) var converter = RenderPipeline.Current?.MaterialConverter; var lightList = new List(); + // Inserts under the group (such as L51's eye inserts) are switched by the insert pass; leave + // their spill lights and meshes alone, or this would turn them off again in the lit shot. foreach (var l in group.GetComponentsInChildren(true)) { - if (l && l.GetComponentInParent(true) == null) { + if (l && l.GetComponentInParent(true) == null && !BelongsToInsert(l)) { lightList.Add(l); } } @@ -526,7 +528,7 @@ private void AddOrphanGroupEntry(LightGroupComponent group, bool isFlasher) var rendererList = new List(); if (converter != null) { foreach (var mr in group.GetComponentsInChildren(true)) { - if (mr && converter.GetEmissiveIntensity(mr.sharedMaterial) > 0f && mr.GetComponentInParent(true) == null) { + if (mr && converter.GetEmissiveIntensity(mr.sharedMaterial) > 0f && mr.GetComponentInParent(true) == null && !BelongsToInsert(mr)) { rendererList.Add(mr); } } @@ -552,6 +554,9 @@ private void AddOrphanGroupEntry(LightGroupComponent group, bool isFlasher) // Resolve a mapped lamp device to the concrete LightComponents it drives, // expanding LightGroupComponent references recursively. + private static bool BelongsToInsert(Component component) => + component.GetComponentInParent(true) || component.GetComponentInParent(true); + // Mark the insert bulbs a coil (flasher) mapping drives: one bulb of an insert mapped directly, // every bulb of an insert under a mapped lamp or group. private static void ResolveInsertBulbs(ILampDeviceComponent device, string deviceItem, Dictionary result) From def5ab7de626fbd7ec092ce628e3719f4c206dcb Mon Sep 17 00:00:00 2001 From: freezy Date: Wed, 14 Oct 2026 18:01:34 +0200 Subject: [PATCH 26/26] packaging: resolve flasher insert bulbs like the game does A lamp above an insert drives only its main bulb, and only while no mapping takes that bulb over; extra bulbs follow their own mappings. The screenshot generator now records the bulbs mappings drive directly and applies a flasher lamp above an insert to the main bulb only, so normal bulbs no longer stay dark. --- .../Packaging/PackageScreenshotGenerator.cs | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs index 0c4192125..95fe76669 100644 --- a/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs +++ b/VisualPinball.Unity/VisualPinball.Unity.Editor/Packaging/PackageScreenshotGenerator.cs @@ -416,18 +416,22 @@ public PlayfieldLightSnapshot(TableComponent tableComponent) // non-deterministic since the uncovered set shifts with table state). var flashers = new HashSet(); var flasherDevices = new HashSet(); - var flasherBulbs = new Dictionary(); + var insertBulbs = new Dictionary(); var lamps = tableComponent.MappingConfig?.Lamps; if (lamps != null) { var resolved = new List(); foreach (var lampMapping in lamps) { - if (lampMapping == null || !lampMapping.IsCoil || lampMapping.Device == null) { + if (lampMapping == null || lampMapping.Device == null) { + continue; + } + // Every mapping that reaches an insert bulb directly takes that bulb over, lamp or coil. + ResolveInsertBulbs(lampMapping.Device, lampMapping.DeviceItem, lampMapping.IsCoil, insertBulbs); + if (!lampMapping.IsCoil) { continue; } if (lampMapping.Device is Component devComp && devComp) { flasherDevices.Add(devComp); } - ResolveInsertBulbs(lampMapping.Device, lampMapping.DeviceItem, flasherBulbs); resolved.Clear(); ResolveLightComponents(lampMapping.Device, resolved); foreach (var lc in resolved) { @@ -452,15 +456,14 @@ public PlayfieldLightSnapshot(TableComponent tableComponent) if (!insert) { continue; } - if (!flasherBulbs.TryGetValue(insert, out var flasher)) { - flasher = new bool[InsertLightComponent.MaxBulbs]; - } - // An insert under a flasher lamp, or named like one (the VPX "F_" fallback), is a flasher as a whole. - var parentLamp = insert.GetComponentInParent(true); - if (parentLamp && (flashers.Contains(parentLamp) || parentLamp.name.StartsWith("F_", StringComparison.Ordinal)) || insert.name.StartsWith("F_", StringComparison.Ordinal)) { - for (var i = 0; i < flasher.Length; i++) { - flasher[i] = true; - } + var mapping = insertBulbs.TryGetValue(insert, out var found) ? found : new InsertBulbMapping(); + var flasher = (bool[])mapping.Flasher.Clone(); + // A lamp above the insert drives its main bulb only, and only while no mapping takes that bulb + // over (the extra bulbs follow their own mappings), as InsertLightComponent does in the game. + if (!mapping.Mapped[0]) { + var parentLamp = insert.GetComponentInParent(true); + flasher[0] = parentLamp && (flashers.Contains(parentLamp) || parentLamp.name.StartsWith("F_", StringComparison.Ordinal)) + || insert.name.StartsWith("F_", StringComparison.Ordinal); } _inserts.Add(new InsertEntry(insert, flasher)); } @@ -557,34 +560,31 @@ private void AddOrphanGroupEntry(LightGroupComponent group, bool isFlasher) private static bool BelongsToInsert(Component component) => component.GetComponentInParent(true) || component.GetComponentInParent(true); - // Mark the insert bulbs a coil (flasher) mapping drives: one bulb of an insert mapped directly, - // every bulb of an insert under a mapped lamp or group. - private static void ResolveInsertBulbs(ILampDeviceComponent device, string deviceItem, Dictionary result) + // Which bulbs of an insert mappings drive directly, and which of those are flashers (coil mappings). + private sealed class InsertBulbMapping + { + public readonly bool[] Mapped = new bool[InsertLightComponent.MaxBulbs]; + public readonly bool[] Flasher = new bool[InsertLightComponent.MaxBulbs]; + } + + // Record the insert bulb a mapping drives directly: the bulb of an insert mapped itself, or of an insert + // taking part in a mapped group. A mapped lamp above an insert is resolved per insert instead. + private static void ResolveInsertBulbs(ILampDeviceComponent device, string deviceItem, bool isCoil, Dictionary result) { - bool[] Bulbs(InsertLightComponent insert) - { - if (!result.TryGetValue(insert, out var bulbs)) { - result[insert] = bulbs = new bool[InsertLightComponent.MaxBulbs]; - } - return bulbs; - } switch (device) { case InsertLightComponent insert when insert: - Bulbs(insert)[InsertLightComponent.BulbIndex(deviceItem)] = true; - break; - case LightComponent lamp when lamp: - foreach (var insert in lamp.GetComponentsInChildren(true)) { - var bulbs = Bulbs(insert); - for (var i = 0; i < bulbs.Length; i++) { - bulbs[i] = true; - } + if (!result.TryGetValue(insert, out var mapping)) { + result[insert] = mapping = new InsertBulbMapping(); } + var bulb = InsertLightComponent.BulbIndex(deviceItem); + mapping.Mapped[bulb] = true; + mapping.Flasher[bulb] |= isCoil; break; case LightGroupComponent group when group: // Each member takes part with one of its lamps, such as a single bulb of an insert. for (var index = 0; index < group._lights.Length; index++) { if (group._lights[index] && group._lights[index] is ILampDeviceComponent child) { - ResolveInsertBulbs(child, group.LampId(index), result); + ResolveInsertBulbs(child, group.LampId(index), isCoil, result); } } break;