$ErrorActionPreference = "Stop" $repositoryRoot = Split-Path -Parent $PSScriptRoot $releaseRoot = Join-Path $repositoryRoot "42.20" $failures = [Collections.Generic.List[string]]::new() function Assert-True { param([bool]$Condition, [string]$Message) if (-not $Condition) { $script:failures.Add($Message) } } function Read-Source { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { $script:failures.Add("Missing dual-mode source: $Path") return "" } return Get-Content -LiteralPath $Path -Raw } $compatibilityPath = Join-Path $releaseRoot "media/lua/shared/TowBar/VehicleCompatibility.lua" $hooking = Read-Source (Join-Path $releaseRoot "media/lua/client/TowBar/TowingHooking.lua") $btTow = Read-Source (Join-Path $releaseRoot "media/lua/server/BTTow.lua") $template = Read-Source (Join-Path $releaseRoot "media/scripts/vehicles/template_towbar.txt") $compatibility = Read-Source $compatibilityPath # The boundary must be an exact mod-ID + exact VehicleScript full-name map. # Model scale remains part of the legacy renderer only; it cannot decide KI5 ownership. Assert-True ($compatibility -match 'TowBarMod\.Compatibility') "Classifier must expose TowBarMod.Compatibility." Assert-True ($compatibility -match 'function Compatibility\.isKi5Vehicle\(vehicle\)') "Classifier must expose isKi5Vehicle(vehicle)." foreach ($identity in @( '91range', 'Base.91range', '87toyotaCorolla', 'Base.87toyotaCorollaAE92levin', '76chevyKseries', 'Base.76chevyK30CCwrecker' )) { Assert-True ($compatibility.Contains($identity)) "Classifier is missing verified exact KI5 identity: $identity" } Assert-True ($compatibility -match '(?s)getActivatedMods\(\).*?contains') "KI5 mode must require an exact active mod ID." Assert-True ($compatibility -match 'getFullName\(\)') "KI5 mode must match the complete VehicleScript name." Assert-True ($compatibility -notmatch 'getModelScale|modelScale|attachmentExist|string\.find|string\.match') "KI5 ownership must not be inferred from scale, attachments, or substring patterns." # The visual carrier is classified independently. KI5 keeps current B42.20 # dynamic hitbox placement; every other vehicle uses the exact B42.13 selector. foreach ($source in @($hooking, $btTow)) { Assert-True ($source -match 'Compatibility\.isKi5Vehicle\(vehicle\)') "Each visual path must classify the vehicle carrying the towbar." Assert-True ($source -match 'getTowbarFrontEdgeZ') "KI5 visual path must retain hitbox-derived placement." Assert-True ($source -match 'TowbarVisualScale\s*=\s*2\.5') "KI5 visual path must retain 2.5x scale compensation." Assert-True ($source -match 'getTowbarIndexVanilla') "Legacy visual path must restore the B42.13 vanilla slot formula." Assert-True ($source -match 'getTowbarIndexSmallScale') "Legacy non-KI5 path must restore B42.13 small-scale slot selection." Assert-True ($source -match 'VanillaScaleMin\s*=\s*1\.5' -and $source -match 'VanillaScaleMax\s*=\s*2\.0') "Legacy visual classification must preserve the B42.13 scale window." } # One bank cannot represent both scales. Restore the original `towbar` bank for # legacy vehicles and isolate today's enlarged KI5 mesh in its own part. Assert-True ($template -match '(?s)model towbarModel\s*\{.*?scale\s*=\s*0\.01,') "Vanilla and normal-scale non-KI5 vehicles must restore the B42.13 0.01 model." Assert-True ($template -match '(?s)model towbarModelKI5\s*\{.*?scale\s*=\s*0\.025,') "KI5 towbar bank must remain exactly 2.5x its original 0.01 scale." Assert-True ($template -match '(?s)model towbarModelLarge\s*\{.*?scale\s*=\s*0\.02022,') "Small-scale non-KI5 vehicles must retain the B42.13 large model scale." Assert-True ($template -match 'part towbarKI5') "Template must provide an isolated KI5 part without resizing the legacy towbar part." function Get-Ki5DynamicSlot { param([double]$ShapeZ, [double]$CenterZ) $meshHalfLength = (0.9714089036 * 2.5) / 2 $center = $CenterZ + ($ShapeZ / 2) + $meshHalfLength return [Math]::Max(0, [Math]::Min(23, [int][Math]::Floor((($center - 1.0) / 0.1) + 0.5))) } function Get-LegacyVanillaSlot { param([double]$ShapeZ) $z = ($ShapeZ / 2) - 0.1 return [Math]::Max(0, [Math]::Min(23, [int][Math]::Floor((($z * 2 / 3) - 1) * 10))) } function Get-LegacySmallScaleSlot { param([double]$MaxAbsAttachmentZ) return [Math]::Max(0, [Math]::Min(23, [int][Math]::Floor(($MaxAbsAttachmentZ + 0.1 - 1.0) * 10))) } Assert-True ((Get-Ki5DynamicSlot 4.3111 -0.0444) -eq 23) "KI5 Corolla must retain B42.20 dynamic slot 23." Assert-True ((Get-Ki5DynamicSlot 4.4444 -0.2556) -eq 22) "KI5 Range Rover must retain B42.20 dynamic slot 22." Assert-True ((Get-LegacyVanillaSlot 2.6044) -eq 0) "B42 Ranger must return to the B42.13 legacy-normal slot 0, not dynamic slot 15." Assert-True ((Get-LegacySmallScaleSlot 2.3222) -eq 14) "Non-KI5 small-scale geometry must retain the B42.13 attachment-derived slot." if ($failures.Count -gt 0) { $failures | ForEach-Object { Write-Host "FAIL: $_" -ForegroundColor Red } exit 1 } Write-Output "PASS: Build 42.20 exact-KI5 dual-mode contracts."