$ErrorActionPreference = "Stop" $repositoryRoot = Split-Path -Parent $PSScriptRoot $releaseRoot = Join-Path $repositoryRoot "42.20" $baselineRoot = Join-Path $repositoryRoot "42.18" $gameRoot = "D:\SteamLibrary\steamapps\common\ProjectZomboid" $failures = [Collections.Generic.List[string]]::new() function Assert-True { param( [bool]$Condition, [string]$Message ) if (-not $Condition) { $script:failures.Add($Message) } } function Get-RelativeFileNames { param([string]$Root) return Get-ChildItem -LiteralPath $Root -Recurse -File | ForEach-Object { $_.FullName.Substring($Root.Length + 1).Replace("\", "/") } | Sort-Object } Assert-True (Test-Path -LiteralPath $releaseRoot) "Missing 42.20 release folder." $releaseInfo = Get-Content -LiteralPath (Join-Path $releaseRoot "mod.info") $rootInfo = Get-Content -LiteralPath (Join-Path $repositoryRoot "mod.info") Assert-True ($releaseInfo -contains "id=hrsys_towbars_testing") "42.20 must retain the current testing mod id." Assert-True ($releaseInfo -contains "versionMin=42.20.0") "42.20 must require Build 42.20." Assert-True ($releaseInfo -contains "modversion=1.0.12") "42.20 must declare mod version 1.0.12." Assert-True ($rootInfo -contains "modversion=1.0.12") "Root and release mod versions must match." $baselineFiles = Get-RelativeFileNames $baselineRoot $releaseFiles = Get-RelativeFileNames $releaseRoot $allowedAdditions = @( "media/lua/client/TowBar/WreckerSyncClient.lua", "media/lua/client/TowBar/WreckerTimedAction.lua", "media/lua/client/TowBar/WreckerUI.lua", "media/lua/server/WreckerCommands.lua", "media/lua/shared/TowBar/Persistence.lua", "media/lua/shared/TowBar/WreckerUtils.lua" ) $expectedReleaseFiles = @($baselineFiles) + $allowedAdditions | Sort-Object Assert-True (-not (Compare-Object $expectedReleaseFiles $releaseFiles)) "42.20 file layout must match baseline plus declared wrecker compatibility files." foreach ($jsonFile in Get-ChildItem -LiteralPath $releaseRoot -Recurse -Filter "*.json") { try { $null = Get-Content -LiteralPath $jsonFile.FullName -Raw | ConvertFrom-Json } catch { $failures.Add("Invalid JSON: $($jsonFile.FullName)") } } $serverCommandsPath = Join-Path $releaseRoot "media/lua/server/TowingCommands.lua" $btTowPath = Join-Path $releaseRoot "media/lua/server/BTTow.lua" $clientSyncPath = Join-Path $releaseRoot "media/lua/client/TowBar/TowSyncClient.lua" $hookingPath = Join-Path $releaseRoot "media/lua/client/TowBar/TowingHooking.lua" $towbarTemplatePath = Join-Path $releaseRoot "media/scripts/vehicles/template_towbar.txt" $itemNamePath = Join-Path $releaseRoot "media/lua/shared/Translate/EN/ItemName.json" $persistencePath = Join-Path $releaseRoot "media/lua/shared/TowBar/Persistence.lua" $legacyItemNamePath = Join-Path $repositoryRoot "common/media/lua/shared/Translate/EN/ItemName_EN.txt" $serverCommands = Get-Content -LiteralPath $serverCommandsPath -Raw $btTow = Get-Content -LiteralPath $btTowPath -Raw $clientSync = Get-Content -LiteralPath $clientSyncPath -Raw $hooking = Get-Content -LiteralPath $hookingPath -Raw $towbarTemplate = Get-Content -LiteralPath $towbarTemplatePath -Raw $itemNames = Get-Content -LiteralPath $itemNamePath -Raw | ConvertFrom-Json $persistence = Get-Content -LiteralPath $persistencePath -Raw $legacyItemNames = Get-Content -LiteralPath $legacyItemNamePath -Raw $towbarVisualScale = 2.5 $towbarBaseScriptScale = 0.01 $towbarScaledScriptScale = $towbarBaseScriptScale * $towbarVisualScale $towbarMeasuredLengthAtBaseScale = 0.9714089036 $towbarScaledHalfLength = ($towbarMeasuredLengthAtBaseScale * $towbarVisualScale) / 2 Assert-True ($towbarTemplate -match ('(?s)model towbarModel\s*\{{.*?scale = {0},' -f [regex]::Escape($towbarScaledScriptScale.ToString('0.000', [Globalization.CultureInfo]::InvariantCulture)))) "The normal towbar visual must render at 2.5x its measured 0.01 script scale." Assert-True ($hooking -match 'local TowbarVisualScale = 2\.5') "Client placement must declare the 2.5x visual scale." Assert-True ($btTow -match 'local TowbarVisualScale = 2\.5') "Part initialization must declare the 2.5x visual scale." Assert-True ($hooking -match '(?s)local TowbarScaledModelLength = TowbarModelLength \* TowbarVisualScale.*?local TowbarModelHalfLength = TowbarScaledModelLength / 2') "Client placement must compensate offsets using the scaled mesh length." Assert-True ($btTow -match '(?s)local TowbarScaledModelLength = TowbarModelLength \* TowbarVisualScale.*?local TowbarModelHalfLength = TowbarScaledModelLength / 2') "Part initialization must compensate offsets using the scaled mesh length." Assert-True ([Math]::Abs((0.9714089036 / 2) - ($towbarScaledHalfLength / $towbarVisualScale)) -lt 0.000000001) "Scaling and offset compensation must retain the original mesh-relative half-length contact point." $towBarDisplayName = $itemNames.PSObject.Properties["TowBar.TowBar"] Assert-True ($null -ne $towBarDisplayName -and $towBarDisplayName.Value -ceq "Tow Bar") "B42.20 must display TowBar.TowBar as Tow Bar." Assert-True ($null -eq $itemNames.PSObject.Properties["ItemName_TowBar.TowBar"]) "B42 JSON must not use the legacy ItemName_ prefix." Assert-True ($legacyItemNames -match 'ItemName_TowBar\.TowBar\s*=\s*"Tow Bar"') "Legacy translations must retain their prefixed item-name key." Assert-True ($serverCommands -match 'detachTrailerSpontaneous') "Server must handle Build 42.20 spontaneous towing detach." Assert-True ($serverCommands -match 'towBarTowedVehicleId') "Server must persist the expected towbar pair." Assert-True ($serverCommands -match '(?s)local function markExpectedTowBarPair.*?towingModData\["towBarTowingVehicleId"\] = nil.*?towedModData\["towBarTowedVehicleId"\] = nil') "Pair marking must clear stale opposite-role IDs." Assert-True ($serverCommands -match '(?s)local function clearExpectedTowBarPair.*?towingModData\["towBarTowedVehicleId"\] = nil.*?towingModData\["towBarTowingVehicleId"\] = nil.*?towedModData\["towBarTowedVehicleId"\] = nil.*?towedModData\["towBarTowingVehicleId"\] = nil') "Pair cleanup must clear both role IDs from both vehicles." Assert-True ($serverCommands -match 'local function isExpectedTowBarPair') "Server must validate reciprocal expected towbar IDs." Assert-True ($serverCommands -match '(?s)isExpectedTowBarPair.*?matchesSavedVehicle\(.*?towBarTowedVehicleId.*?towBarTowedVehicleSqlId.*?vehicleB.*?matchesSavedVehicle\(.*?towBarTowingVehicleId.*?towBarTowingVehicleSqlId.*?vehicleA') "Expected towbar pair validation must use reciprocal stable vehicle identities." Assert-True ($serverCommands -match 'local function isLegacyTowBarPair') "Server must isolate legacy pair recovery from stale modern pair IDs." Assert-True ($serverCommands -match '(?s)isLegacyTowBarPair.*?hasTowBarState\(vehicleA\) and hasTowBarState\(vehicleB\).*?towed.*?== true.*?towingModData\["towBarTowedVehicleId"\].*?== nil.*?towingModData\["towBarTowingVehicleId"\].*?== nil.*?towedModData\["towBarTowedVehicleId"\].*?== nil.*?towedModData\["towBarTowingVehicleId"\].*?== nil') "Legacy recovery must require both role markers and no modern pair IDs on either vehicle." Assert-True ($serverCommands -match '(?s)local function resolveExpectedTowBarPair.*?towingVehicle and towedVehicle.*?isExpectedTowBarPair\(towingVehicle, towedVehicle\)') "Current-link spontaneous resolution must still require reciprocal towbar identity." Assert-True ($serverCommands -match '(?s)local function snapshotActiveTowbarLinksServer.*?isExpectedTowBarPair\(towingVehicle, towedVehicle\).*?isLegacyTowBarPair\(towingVehicle, towedVehicle\)') "Server snapshots must only preserve reciprocal or clean legacy towbar pairs." Assert-True ($serverCommands -match 'spontaneousDetachSync') "Server must broadcast spontaneous towbar cleanup." Assert-True ($serverCommands -match 'local vehicleBId = args\.vehicleB or args\.vehicle') "Manual detach sync must include the towed vehicle id." Assert-True ($serverCommands -notmatch 'function Commands\.giveTowBar') "Clients must not be able to invoke the towbar refund helper." Assert-True ($serverCommands -match 'isPlayerAuthorizedForPair') "Server refunds must validate the requesting player." Assert-True ($serverCommands -match 'rejected towbar attach without item') "Multiplayer attach must require server-side towbar ownership." Assert-True ($serverCommands -match '(?s)function Commands\.attachTowBar.*?if hasAnyTowLink\(vehicleA\) or hasAnyTowLink\(vehicleB\) then.*?return.*?if isExpectedTowBarPair\(vehicleA, vehicleB\).*?or isExpectedTowBarPair\(vehicleB, vehicleA\).*?or hasPendingAttach\(vehicleA, vehicleB\).*?or hasPendingAttach\(vehicleB, vehicleA\).*?then.*?return.*?local towBarItem = findTowBarItem') "Existing or reversed pending tow pairs must be rejected before consuming another towbar." Assert-True ($serverCommands -match 'queueSync\("attach", player, args, true\)') "Multiplayer attach must reserve the towbar for delayed confirmation." Assert-True ($serverCommands -match '(?s)function Commands\.attachTowBar.*?markExpectedTowBarPair\(vehicleA, vehicleB, args\.attachmentA, args\.attachmentB\).*?queueSync\("attach", player, args, true\).*?vehicleA:addPointConstraint') "Attach must record and queue the pair before constraint creation can break spontaneously." Assert-True ($serverCommands -match 'local function failAttachSync') "Failed multiplayer attach must have a cleanup/refund path." Assert-True ($serverCommands -match '(?s)local function processAttachSync.*?if isTowBarPairConfirmed\(vehicleA, vehicleB\) then return "broken" end') "A confirmed pair that breaks during attach confirmation must not be reattached." Assert-True ($serverCommands -match '(?s)local function processPendingSync.*?status == "broken".*?finalizeBrokenTowBarPair') "A break during attach confirmation must use the shared ground-drop finalizer." Assert-True ($serverCommands -match 'if item\.reservedTowBar then') "Failed multiplayer attach must refund the reserved towbar exactly once." Assert-True ($serverCommands -match 'Constraint creation may complete on a later server tick') "Multiplayer attach must preserve delayed constraint confirmation." Assert-True ($serverCommands -match 'local function finalizeBrokenTowBarPair') "Broken towbar cleanup must use one idempotent finalizer." Assert-True ($serverCommands -match 'local function reconcileBrokenTowBarPairsServer') "Server and single-player must audit confirmed towbar constraints for silent breaks." Assert-True ($serverCommands -match '(?s)local function reconcileBrokenTowBarPairsServer.*?Persistence\.advanceRecoveryState.*?RestoreRetryMs, SustainedBreakMs.*?action == "break".*?finalizeBrokenTowBarPair.*?action == "restore".*?restorePersistedTowBarPair') "The physical-link audit must use the shared bounded recovery state machine." Assert-True ($persistence -match '(?s)function Persistence\.advanceRecoveryState.*?state\.unlinkedSince = state\.unlinkedSince or now.*?now - state\.unlinkedSince >= breakMs.*?return state, "break"' -and $persistence -match '(?s)if not state\.pendingUntil or now >= state\.pendingUntil then.*?state\.pendingUntil = now \+ retryMs.*?return state, "restore"') "Persistent-pair recovery must debounce restores and require a sustained break before cleanup." Assert-True ($serverCommands -match '(?s)local function restorePersistedTowBarPair.*?towBarExpectedAttachment.*?addPointConstraint.*?broadcastAttach') "A saved towbar pair must rebuild its physical constraint without consuming another item." Assert-True ($persistence -match '(?s)if not state\.pendingUntil or now >= state\.pendingUntil then.*?return state, "restore"') "An unconfirmed saved pair must schedule a bounded restoration attempt." Assert-True ($clientSync -match 'TowBarMod\.Sync\.applyAttachSync = applyAttachSync') "Single-player must expose the existing attach synchronizer for saved-pair recovery." Assert-True ($clientSync -match '(?s)local key = tostring\(vehicleA:getId\(\)\).*?if TowBarMod\.Sync\.appliedPairs\[key\] and not isLinked\(vehicleA, vehicleB\) then.*?TowBarMod\.Sync\.appliedPairs\[key\] = nil.*?if not TowBarMod\.Sync\.appliedPairs\[key\] then.*?breakTowBarPair\(vehicleA, vehicleB\).*?setVehicleScriptWithTowBarHidden\(vehicleB, "notTowingA_Trailer"\).*?addPointConstraint\(nil, vehicleB, attachmentA, attachmentB, true\)') "Every fresh or recovered towbar pair must replace the native rope with one local rigid constraint." Assert-True ($clientSync -notmatch '(?s)if linked then\s*--.*?appliedPairs\[key\] = true') "A newly observed native tow link must not be accepted as rigid without rebuilding it." Assert-True ($clientSync -match '(?s)local function clearAppliedPairForVehicle.*?TowBarMod\.Sync\.appliedPairs\[key\] = nil.*?Events\.OnSpawnVehicleEnd\.Add\(clearAppliedPairForVehicle\)') "Vehicle streaming must invalidate cached rigid towbar constraints." Assert-True ($serverCommands -match '(?s)local function broadcastAttach.*?if isServer\(\).*?sendServerCommand.*?elseif not isClient\(\).*?TowBarMod\.Sync\.applyAttachSync') "Saved towbar recovery must synchronize both dedicated-server and single-player sessions." Assert-True ($serverCommands -match 'towBarTowedVehicleSqlId' -and $serverCommands -match 'towBarTowingVehicleSqlId' -and $serverCommands -match 'getSqlId') "Towbar persistence must use stable save IDs in addition to live network IDs." Assert-True ($serverCommands -match 'Persistence\.savePair\("towbar"' -and $serverCommands -match 'Persistence\.forEachPair\("towbar"') "Towbar persistence must use the server-owned saved-pair registry." Assert-True ($hooking -match '(?s)local function recoverTowBarVehicleAfterLoad.*?authoritative audit will.*?reconnect') "Client load recovery must defer missing constraints to the authoritative saved-pair audit." $loadRecoveryStart = $hooking.IndexOf('local function recoverTowBarVehicleAfterLoad') $loadRecoveryEnd = $hooking.IndexOf('function TowBarMod.Hook.setVehiclePostAttach', $loadRecoveryStart) if ($loadRecoveryStart -ge 0 -and $loadRecoveryEnd -gt $loadRecoveryStart) { $loadRecovery = $hooking.Substring($loadRecoveryStart, $loadRecoveryEnd - $loadRecoveryStart) Assert-True ($loadRecovery -notmatch 'detachTowBar|attachTowBar|reattachTowBarPairAfterCleanDetach') "Loading a saved towbar pair must not refund or consume the item." } Assert-True ($serverCommands -match '(?s)local function processPendingSync.*?reconcileBrokenTowBarPairsServer\(\)') "The physical-link audit must run from the server/SP tick handler." Assert-True ($serverCommands -match '(?s)local function finalizeBrokenTowBarPair.*?dropTowBarOnGround.*?cancelPendingAttach.*?breakTowBarConstraint.*?clearExpectedTowBarPair.*?broadcastSpontaneousDetach') "Broken-pair finalization must drop, disconnect, clear state, and synchronize cleanup." Assert-True ($serverCommands -match '(?s)local function finalizeBrokenTowBarPair.*?if isServer\(\) then.*?elseif not isClient\(\).*?TowBarMod\.Hook\.cleanupDetachedTowBar') "Single-player breaks must directly restore the vehicle without relying on a server command." Assert-True ($clientSync -match 'spontaneousDetachSync') "Client sync must receive spontaneous towbar cleanup." Assert-True ($clientSync -match 'local function hasConflictingTowLink') "Client detach sync must detect links to unrelated vehicles." Assert-True ($clientSync -match 'local breakTowBarPair' -and $clientSync -match 'breakTowBarPair = function') "Client detach sync must break only the requested pair." Assert-True ($clientSync -notmatch 'local function safeBreak') "Client detach sync must not use an unrestricted per-vehicle constraint break." Assert-True ($clientSync -match '(?s)local function applyDetachSync.*?hasConflictingTowLink\(vehicleA, vehicleB\).*?return.*?breakTowBarPair\(vehicleA, vehicleB\).*?cleanupDetachedTowBar') "Client detach sync must reject conflicting links before cleanup." Assert-True ($hooking -match 'function TowBarMod\.Hook\.cleanupDetachedTowBar') "Client must expose idempotent towbar cleanup." Assert-True ($hooking -match '(?s)if modData\.towBarOriginalParkingBrakeOn ~= nil then.*?setParkingBrakeOn.*?end.*?if modData\.towBarOriginalParkingBrake ~= nil then.*?setParkingBrake.*?end.*?if modData\.towBarOriginalHandbrake ~= nil then.*?setHandbrake.*?end') "Free-rolling state may only change brake controls whose original values were captured." Assert-True ($hooking -notmatch 'tryVehicleCall\(vehicle, "setBrake"') "Free-rolling state must not change an unrestorable brake control." Assert-True ($hooking -notmatch 'tryVehicleCall\(vehicle, "setBraking"') "Free-rolling state must not change an unrestorable braking control." Assert-True ($hooking -match 'local TowbarFirstZ = 1\.0') "Client hitbox placement must use the first model's Z position." Assert-True ($hooking -match 'local TowbarSlotStep = 0\.1') "Client hitbox placement must use the model bank's Z spacing." Assert-True ($hooking -match 'local TowbarVisualScale = 2\.5') "Client placement must account for the enlarged visual." Assert-True ($hooking -match 'local TowbarScaledModelLength = TowbarModelLength \* TowbarVisualScale') "Client placement must derive the enlarged mesh length." Assert-True ($hooking -match 'local TowbarModelHalfLength = TowbarScaledModelLength / 2') "Client placement must offset the mesh center by half its enlarged length." Assert-True ($btTow -match 'local TowbarVisualScale = 2\.5') "Part initialization must account for the enlarged visual." Assert-True ($btTow -match 'local TowbarScaledModelLength = TowbarModelLength \* TowbarVisualScale') "Part initialization must derive the enlarged mesh length." Assert-True ($btTow -match 'local TowbarModelHalfLength = TowbarScaledModelLength / 2') "Part initialization must offset the mesh center by half its enlarged length." Assert-True ($hooking -match '(?s)local function getTowbarFrontEdgeZ\(script\).*?getPhysicsChassisShape\(\).*?getCenterOfMassOffset\(\).*?centerZ.*?shapeZ / 2') "Client visual position must derive from the hitbox front edge." Assert-True ($btTow -match '(?s)local function getTowbarFrontEdgeZ\(script\).*?getPhysicsChassisShape\(\).*?getCenterOfMassOffset\(\).*?centerZ.*?shapeZ / 2') "Part initialization must derive visual position from the hitbox front edge." Assert-True ($hooking -match '(?s)local centerOk, center = pcall.*?if not centerOk or not center then return nil end.*?if not zOk or type\(centerZ\) ~= "number" then return nil end') "Client must not show a dynamically placed towbar when center-of-mass geometry is invalid." Assert-True ($btTow -match '(?s)local centerOk, center = pcall.*?if not centerOk or not center then return nil end.*?if not zOk or type\(centerZ\) ~= "number" then return nil end') "Part initialization must not show a towbar when center-of-mass geometry is invalid." Assert-True ($hooking -match '(?s)local function getTowbarModelSlot\(script\).*?local frontEdgeZ = getTowbarFrontEdgeZ\(script\).*?local modelCenterZ = frontEdgeZ \+ TowbarModelHalfLength.*?math\.floor\(\(\(modelCenterZ - TowbarFirstZ\) / TowbarSlotStep\) \+ 0\.5\).*?math\.max\(0, math\.min\(TowbarMaxIndex, index\)\)') "Client must place the towbar's inner end at the hitbox edge." Assert-True ($btTow -match '(?s)local function getTowbarModelSlot\(script\).*?local frontEdgeZ = getTowbarFrontEdgeZ\(script\).*?local modelCenterZ = frontEdgeZ \+ TowbarModelHalfLength.*?math\.floor\(\(\(modelCenterZ - TowbarFirstZ\) / TowbarSlotStep\) \+ 0\.5\).*?math\.max\(0, math\.min\(TowbarMaxIndex, index\)\)') "Part initialization must place the towbar's inner end at the hitbox edge." Assert-True ($hooking -notmatch 'isVanillaScale|modelScale') "Dynamic hitbox placement must not apply a second model-scale correction." Assert-True ($btTow -notmatch 'isVanillaScale|modelScale') "Part initialization must not apply a second model-scale correction." Assert-True ($hooking -match '(?s)local function setTowBarModelVisible.*?local part = normalPart.*?part:setModelVisible\("towbar" \.\. index, true\)') "Automatic rendering must use the normal towbar part." Assert-True ($btTow -match '(?s)function BTtow\.Init\.towbar.*?local shouldShowOnThisPart = part:getId\(\) == "towbar"') "Automatic part initialization must only show the normal towbar part." Assert-True ($hooking -notmatch 'local part = isVanilla and normalPart or largePart') "KI5 automatic rendering must not select the large towbar part." $normalTowbarPart = $towbarTemplate.Substring($towbarTemplate.IndexOf('part towbar'), $towbarTemplate.IndexOf('part towbarLarge') - $towbarTemplate.IndexOf('part towbar')) Assert-True ($towbarTemplate -match '(?s)model towbarModel\s*\{.*?scale = 0\.025,') "The rendered towbar mesh must be exactly 2.5 times its original 0.01 scale." Assert-True ($normalTowbarPart -match '(?s)model towbar0\s*\{\s*file = towbarModel,\s*offset = 0 -0\.3 1\.0,') "Normal towbar0 must retain the radial-menu Z=1.0 geometry." $normalTowbarModels = [regex]::Matches($normalTowbarPart, '(?s)model towbar(\d+)\s*\{\s*file = towbarModel,\s*offset = ([\d.-]+) ([\d.-]+) ([\d.-]+),') Assert-True ($normalTowbarModels.Count -eq 24) "Normal towbar part must provide all 24 dynamic Z slots." for ($slot = 0; $slot -lt $normalTowbarModels.Count; $slot++) { $match = $normalTowbarModels[$slot] Assert-True ([int]$match.Groups[1].Value -eq $slot) "Normal towbar slots must be sequential." Assert-True ([double]$match.Groups[2].Value -eq 0) "Dynamic placement must not alter lateral X." Assert-True ([double]$match.Groups[3].Value -eq -0.3) "Dynamic placement must not alter vertical Y." $expectedZ = 1.0 + ($slot * 0.1) Assert-True ([Math]::Abs(([double]$match.Groups[4].Value) - $expectedZ) -lt 0.000001) "Towbar slot $slot must map to Z=$expectedZ." } function Get-ExpectedTowbarSlot { param([double]$ShapeZ, [double]$CenterZ) $towbarModelHalfLength = (0.9714089036 * 2.5) / 2 $targetCenterZ = $CenterZ + $ShapeZ / 2 + $towbarModelHalfLength $raw = [Math]::Floor((($targetCenterZ - 1.0) / 0.1) + 0.5) return [Math]::Max(0, [Math]::Min(23, [int]$raw)) } $geometryCases = @( @{ Name = "B42 Ranger"; ShapeZ = 2.6044; CenterZ = 0.0; Slot = 15 }, @{ Name = "KI5 Corolla"; ShapeZ = 4.3111; CenterZ = -0.0444; Slot = 23 }, @{ Name = "KI5 Range"; ShapeZ = 4.4444; CenterZ = -0.2556; Slot = 22 }, @{ Name = "short vehicle"; ShapeZ = 1.2; CenterZ = 0.0; Slot = 8 }, @{ Name = "long clamp"; ShapeZ = 7.2; CenterZ = 0.0; Slot = 23 } ) foreach ($case in $geometryCases) { Assert-True ((Get-ExpectedTowbarSlot $case.ShapeZ $case.CenterZ) -eq $case.Slot) "$($case.Name) hitbox must map to towbar slot $($case.Slot)." $frontEdgeZ = $case.CenterZ + ($case.ShapeZ / 2) $chosenCenterZ = 1.0 + ($case.Slot * 0.1) $chosenInnerEndZ = $chosenCenterZ - $towbarScaledHalfLength if ($case.Slot -gt 0 -and $case.Slot -lt 23) { Assert-True ([Math]::Abs($chosenInnerEndZ - $frontEdgeZ) -le 0.0500001) "$($case.Name) enlarged visual must retain contact with the hitbox edge within half a model slot." } } Assert-True ($hooking -match 'function TowBarMod\.Hook\.setVehicleScriptWithTowBarHidden') "Vehicle script swaps must use the anti-flicker helper." Assert-True ($hooking -match '(?s)setVehicleScriptWithTowBarHidden.*?setTowBarModelVisible\(vehicle, false\).*?towBarModelSwapInProgress.*?vehicle:setScriptName\(scriptName\).*?setTowBarModelVisible\(vehicle, false\)') "The anti-flicker helper must hide towbar models for the full script swap." Assert-True ($btTow -match '(?s)function BTtow\.Init\.towbar.*?setModelVisible\("towbar" \.\. j, false\).*?towBarModelSwapInProgress.*?return') "Part initialization must not show a towbar during a script swap." Assert-True ($hooking -notmatch 'towedVehicle:setScriptName\(') "All hook script swaps must use the anti-flicker helper." Assert-True ($clientSync -notmatch 'vehicleB:setScriptName\(') "Client reconciliation must not bypass the anti-flicker helper." Assert-True ($clientSync -match 'TowBarMod\.Hook\.setVehicleScriptWithTowBarHidden\(vehicleB, "notTowingA_Trailer"\)') "Client reconciliation must hide towbar models while changing scripts." $spontaneousStart = $serverCommands.IndexOf('if module == "vehicle" and command == "detachTrailerSpontaneous" then') $spontaneousEnd = $serverCommands.IndexOf('if module == "towbar" and Commands[command] then', $spontaneousStart) Assert-True ($spontaneousStart -ge 0 -and $spontaneousEnd -gt $spontaneousStart) "Could not isolate the spontaneous detach handler." if ($spontaneousStart -ge 0 -and $spontaneousEnd -gt $spontaneousStart) { $spontaneousBranch = $serverCommands.Substring($spontaneousStart, $spontaneousEnd - $spontaneousStart) Assert-True ([regex]::Matches($spontaneousBranch, 'finalizeBrokenTowBarPair\(towingVehicle, towedVehicle').Count -eq 1) "The multiplayer spontaneous event must use the shared broken-pair finalizer." Assert-True ($spontaneousBranch -notmatch 'dropTowBarOnGround\(|breakTowBarConstraint\(|clearExpectedTowBarPair\(') "The spontaneous event must not bypass exactly-once finalization." Assert-True ($spontaneousBranch -notmatch 'giveTowBar\(|inventory:AddItem|sendAddItemToContainer') "A spontaneously broken towbar must not be refunded to inventory." } Assert-True ($serverCommands -match '(?s)local function dropTowBarOnGround\(towingVehicle, towedVehicle\).*?getSquare\(\).*?AddWorldInventoryItem\(TowBarItemType, 0\.5, 0\.5, 0\)') "A broken towbar must be placed on the ground beside the tow pair." Assert-True ([regex]::Matches($serverCommands, 'dropTowBarOnGround\(').Count -eq 2) "Only the shared finalizer may call the one towbar ground-drop helper." $cleanupStart = $hooking.IndexOf('function TowBarMod.Hook.cleanupDetachedTowBar') $cleanupEnd = $hooking.IndexOf('function TowBarMod.Hook.performDetachTowBar', $cleanupStart) Assert-True ($cleanupStart -ge 0 -and $cleanupEnd -gt $cleanupStart) "Could not isolate client towbar cleanup." if ($cleanupStart -ge 0 -and $cleanupEnd -gt $cleanupStart) { $cleanupBranch = $hooking.Substring($cleanupStart, $cleanupEnd - $cleanupStart) Assert-True ($cleanupBranch -match 'updateAttachmentsOnDefaultValues') "Break cleanup must restore attachment offsets." Assert-True ($cleanupBranch -match 'setVehicleScriptWithTowBarHidden.*?towBarOriginalScriptName') "Break cleanup must restore the original vehicle script." Assert-True ($cleanupBranch -match '(?s)setVehicleScriptWithTowBarHidden.*?towBarOriginalScriptName.*?updateAttachmentsOnDefaultValues') "Break cleanup must restore the real vehicle script before restoring its attachment offsets." Assert-True ($cleanupBranch -match 'restoreFreeRollingTowState') "Break cleanup must restore mass, brakes, and parking-brake state." Assert-True ($cleanupBranch -match 'setTowBarModelVisible\(towedVehicle, false\)') "Break cleanup must hide the towbar model." Assert-True ($cleanupBranch -match '(?s)towingModData\["towBarTowedVehicleId"\] = nil.*?towingModData\["towBarTowingVehicleId"\] = nil.*?towedModData\["towBarTowedVehicleId"\] = nil.*?towedModData\["towBarTowingVehicleId"\] = nil') "Client cleanup must remove stale IDs for either tow role." } $manualDetachStart = $serverCommands.IndexOf('function Commands.detachTowBar') $manualDetachEnd = $serverCommands.IndexOf('function Commands.consumeTowBar', $manualDetachStart) Assert-True ($manualDetachStart -ge 0 -and $manualDetachEnd -gt $manualDetachStart) "Could not isolate the manual detach handler." if ($manualDetachStart -ge 0 -and $manualDetachEnd -gt $manualDetachStart) { $manualDetachBranch = $serverCommands.Substring($manualDetachStart, $manualDetachEnd - $manualDetachStart) Assert-True ($manualDetachBranch -match 'if not isLinked\(towingVehicle, towedVehicle\) then') "Manual detach must reject mismatched or stale vehicle pairs." Assert-True ($manualDetachBranch -match '(?s)not isExpectedTowBarPair\(towingVehicle, towedVehicle\).*?not isLegacyTowBarPair\(towingVehicle, towedVehicle\)') "Manual detach must reject non-towbar or stale linked pairs." Assert-True ($manualDetachBranch -match 'breakTowBarConstraint\(towingVehicle, towedVehicle\)') "Manual detach must only break the validated tow pair." Assert-True ($manualDetachBranch -match 'giveTowBar\(player, true\)') "Manual detach must continue returning the towbar to inventory." Assert-True ($manualDetachBranch -notmatch 'dropTowBarOnGround') "Manual detach must not drop the towbar on the ground." } $vanillaVehicleCommandsPath = Join-Path $gameRoot "media/lua/server/Vehicles/VehicleCommands.lua" if (Test-Path -LiteralPath $vanillaVehicleCommandsPath) { $vanillaVehicleCommands = Get-Content -LiteralPath $vanillaVehicleCommandsPath -Raw Assert-True ($vanillaVehicleCommands -match 'function Commands\.attachTrailer') "Installed game lacks the expected attach command." Assert-True ($vanillaVehicleCommands -match 'function Commands\.detachTrailerSpontaneous') "Installed game lacks the Build 42.20 spontaneous detach command." Assert-True ($vanillaVehicleCommands -match 'addPointConstraint') "Installed game lacks the expected towing constraint API." } $luaFiles = Get-ChildItem -LiteralPath (Join-Path $releaseRoot "media/lua") -Recurse -Filter "*.lua" | ForEach-Object FullName & npx --yes luaparse --quiet @luaFiles Assert-True ($LASTEXITCODE -eq 0) "One or more 42.20 Lua files failed to parse." if ($failures.Count -gt 0) { $failures | ForEach-Object { Write-Error $_ } exit 1 } Write-Output "PASS: Build 42.20 metadata, layout, JSON, Lua, and towing compatibility checks."