exchanger-themes/tools/Test-PackagedBindingContract.ps1
Danil Iarosh ca52efbedc Initial commit: Exchanger Theme DLC Builder
Godot 4.7 project for authoring, previewing, validating, and building Exchanger PCK themes. Includes the example and space-pixel themes, binding contracts, editor tooling, validation/build scripts, and project documentation.
2026-09-22 11:43:13 +03:00

148 lines
7.7 KiB
PowerShell

param(
[string]$ThemeId = "*"
)
$ErrorActionPreference = "Stop"
$builderRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$authorityPath = Join-Path $builderRoot "contracts\screen_bindings.v2.json"
if (-not (Test-Path -LiteralPath $authorityPath)) {
throw "The authoritative binding contract is missing: $authorityPath"
}
$contract = Get-Content -LiteralPath $authorityPath -Raw | ConvertFrom-Json
if ($contract.Format -ne "ExchangerThemeBindingContract" -or $contract.Version -ne 2) {
throw "contracts\screen_bindings.v2.json has an unsupported format or version."
}
$screenCount = @($contract.Screens).Count
$bindingCount = @($contract.Screens | ForEach-Object { $_.Bindings }).Count
if ($screenCount -ne 9 -or $bindingCount -ne 481) {
throw "Binding contract v2 must contain 9 screens and 481 elements; found $screenCount/$bindingCount."
}
$allBindings = @($contract.Screens | ForEach-Object { $_.Bindings })
$bindingIds = @($allBindings | ForEach-Object { $_.Id })
if (@($bindingIds | Select-Object -Unique).Count -ne 481) {
throw "Binding contract v2 contains duplicate element IDs."
}
$requiredServiceBindings = [ordered]@{
"settings.scroll.content.section_tabs.service_button" = "Button"
"settings.scroll.content.service_inventory_panel" = "Control"
"settings.scroll.content.service_inventory_panel.margin.content.count" = "Label"
"settings.scroll.content.service_inventory_panel.margin.content.hopper1_label" = "Label"
"settings.scroll.content.service_inventory_panel.margin.content.hopper2_label" = "Label"
"settings.scroll.content.service_inventory_panel.margin.content.recount_button" = "Button"
"settings.scroll.content.service_inventory_panel.margin.content.manual_add_button" = "Button"
"settings.scroll.content.service_inventory_panel.margin.content.status" = "Label"
"settings.service_dialog_overlay" = "Control"
"settings.service_dialog_overlay.dialog.margin.content.title" = "Label"
"settings.service_dialog_overlay.dialog.margin.content.description" = "Label"
"settings.service_dialog_overlay.dialog.margin.content.amount" = "SpinBox"
"settings.service_dialog_overlay.dialog.margin.content.buttons.cancel_button" = "Button"
"settings.service_dialog_overlay.dialog.margin.content.buttons.confirm_button" = "Button"
}
foreach ($entry in $requiredServiceBindings.GetEnumerator()) {
$binding = @($allBindings | Where-Object { $_.Id -eq $entry.Key })
if ($binding.Count -ne 1 -or @($binding[0].Types) -notcontains $entry.Value) {
throw "Binding contract v2 is missing service binding '$($entry.Key)' with type '$($entry.Value)'."
}
}
$supplementalRuntimeBindings = @(
"home.scroll.content.advertisement_panel",
"home.scroll.content.stock_status.meter.empty",
"home.scroll.content.stock_status.meter.low",
"home.scroll.content.stock_status.meter.enough",
"home.scroll.content.stock_status.meter.much",
"home.scroll.content.stock_status.approximate_count",
"card_amount.content.reward_panel.content.bonus",
"cash_payment.content.banner_text",
"cash_payment.content.top_bar.home_navigation_button",
"card_amount.content.top_bar.home_navigation_button",
"card_custom_amount.content.top_bar.home_navigation_button",
"card_terminal.content.top_bar.home_navigation_button",
"success.content.home_navigation_button",
"error.content.home_navigation_button",
"service_access.content.home_navigation_button",
"settings.footer",
"settings.footer.save_and_exit_button",
"settings.scroll.content.footer_clearance",
"settings.scroll.content.service_inventory_panel.margin.content.description",
"settings.scroll.content.service_inventory_panel.margin.content.inventory_enabled",
"settings.scroll.content.service_inventory_panel.margin.content.purchase_limit_enabled",
"settings.scroll.content.menu_visibility_panel",
"settings.scroll.content.menu_visibility_panel.margin.content.show_stock_status",
"settings.scroll.content.menu_visibility_panel.margin.content.show_right_character",
"settings.scroll.content.menu_visibility_panel.margin.content.show_promotion_block",
"settings.scroll.content.menu_visibility_panel.margin.content.show_interactive_pet",
"settings.scroll.content.advertisement_poster_panel.margin.content.select_button",
"settings.scroll.content.animated_banner_texts_panel"
)
foreach ($supplementalRuntimeBinding in $supplementalRuntimeBindings) {
if ($bindingIds -contains $supplementalRuntimeBinding) {
throw "Supplemental runtime binding '$supplementalRuntimeBinding' must not change the immutable 481-element JSON contract."
}
}
$cardAmountValueBinding = @($allBindings | Where-Object {
$_.Id -eq "card_amount.content.reward_panel.content.value"
})
if ($cardAmountValueBinding.Count -ne 1 -or @($cardAmountValueBinding[0].Types) -notcontains "Label") {
throw "The immutable CardAmount Value binding must remain a Label at its original ID/path."
}
$requiredShortTextBindings = @(
"home.scroll.content.speech_text",
"settings.scroll.content.branding_panel.margin.content.short_text",
"settings.scroll.content.branding_panel.margin.content.short_text_error",
"settings.scroll.content.branding_preview_panel.margin.content.short_text"
)
foreach ($bindingId in $requiredShortTextBindings) {
if ($bindingIds -notcontains $bindingId) {
throw "Binding contract v2 is missing required short-text binding '$bindingId'."
}
}
if (@($bindingIds | Where-Object { $_ -like "*subtitle*" }).Count -ne 0) {
throw "Binding contract v2 must not restore deprecated subtitle bindings."
}
$expectedFixedCollections = @{
"card_amount.content.amount_panel.margin.content.preset_grid.slot_" = 16
"settings.scroll.content.bonus_panel.margin.content.rows.slot_" = 80
"settings.scroll.content.card_options_panel.margin.content.preset_rows.slot_" = 64
"settings.scroll.content.advertisement_playlist_panel.margin.content.rows.slot_" = 48
}
foreach ($entry in $expectedFixedCollections.GetEnumerator()) {
$actual = @($bindingIds | Where-Object { $_.StartsWith($entry.Key, [System.StringComparison]::Ordinal) }).Count
if ($actual -ne $entry.Value) {
throw "Fixed collection '$($entry.Key)' must expose $($entry.Value) bindings; found $actual."
}
}
$authorityHash = (Get-FileHash -LiteralPath $authorityPath -Algorithm SHA256).Hash
$themeDirectories = Get-ChildItem -LiteralPath (Join-Path $builderRoot "themes") -Directory | Where-Object {
$ThemeId -eq "*" -or $_.Name -eq $ThemeId
}
if (@($themeDirectories).Count -eq 0) {
throw "Theme '$ThemeId' was not found in themes/."
}
foreach ($themeDirectory in $themeDirectories) {
$manifestPath = Join-Path $themeDirectory.FullName "package\manifest.json"
$packageContractPath = Join-Path $themeDirectory.FullName "package\contracts\screen_bindings.v2.json"
if (-not (Test-Path -LiteralPath $manifestPath)) {
throw "Manifest for theme '$($themeDirectory.Name)' is missing: $manifestPath"
}
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
if ($manifest.Format -ne "ExchangerThemeDlc" -or $manifest.FormatVersion -ne 2) {
throw "Theme '$($themeDirectory.Name)' does not use ExchangerThemeDlc v2."
}
if (-not (Test-Path -LiteralPath $packageContractPath)) {
throw "Binding contract copy for theme '$($themeDirectory.Name)' is missing."
}
$packageHash = (Get-FileHash -LiteralPath $packageContractPath -Algorithm SHA256).Hash
if ($packageHash -ne $authorityHash) {
throw "Binding contract copy for theme '$($themeDirectory.Name)' differs from contracts\screen_bindings.v2.json."
}
}
Write-Host "Binding contract copies are synchronized: $(@($themeDirectories).Count) theme(s), SHA-256 $authorityHash."