Files
Towbar/tests/validate-42.20.ps1
T
2026-08-12 13:36:01 -04:00

169 lines
17 KiB
PowerShell

$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") "42.20 must use the stable mod id."
Assert-True ($releaseInfo -contains "versionMin=42.20.0") "42.20 must require Build 42.20."
Assert-True ($releaseInfo -contains "modversion=1.0.5") "42.20 must declare mod version 1.0.5."
Assert-True ($rootInfo -contains "modversion=1.0.5") "Root and release mod versions must match."
$baselineFiles = Get-RelativeFileNames $baselineRoot
$releaseFiles = Get-RelativeFileNames $releaseRoot
Assert-True (-not (Compare-Object $baselineFiles $releaseFiles)) "42.20 file layout must match the latest baseline."
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"
$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
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.*?towBarTowedVehicleId.*?vehicleB:getId\(\).*?towBarTowingVehicleId.*?vehicleA:getId\(\)') "Expected towbar pair validation must be reciprocal."
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.*?towBarTowedVehicleId.*?getVehicleById.*?isExpectedTowBarPair.*?isLinked.*?markTowBarPairConfirmed.*?isTowBarPairConfirmed.*?finalizeBrokenTowBarPair') "The physical-link audit must confirm reciprocal pairs and finalize a missing constraint."
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 function breakTowBarPair') "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 '(?s)local function getTowbarModelSlot\(script\).*?if not isVanillaScale\(script\) then.*?return 0\s+end') "KI5 client rendering must select towbar slot 0."
Assert-True ($btTow -match '(?s)local function getTowbarModelSlot\(script\).*?if not isVanillaScale\(script\) then.*?return 0\s+end') "KI5 part initialization must select towbar slot 0."
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 ($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."
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 '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."