157 lines
15 KiB
PowerShell
157 lines
15 KiB
PowerShell
$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) }
|
|
}
|
|
|
|
$expectedFiles = @(
|
|
"media/lua/shared/TowBar/WreckerUtils.lua",
|
|
"media/lua/client/TowBar/WreckerTimedAction.lua",
|
|
"media/lua/client/TowBar/WreckerUI.lua",
|
|
"media/lua/client/TowBar/WreckerSyncClient.lua",
|
|
"media/lua/server/WreckerCommands.lua"
|
|
)
|
|
foreach ($relativePath in $expectedFiles) {
|
|
Assert-True (Test-Path -LiteralPath (Join-Path $releaseRoot $relativePath)) "Missing wrecker runtime file: $relativePath"
|
|
}
|
|
|
|
$sharedPath = Join-Path $releaseRoot "media/lua/shared/TowBar/WreckerUtils.lua"
|
|
$timedActionPath = Join-Path $releaseRoot "media/lua/client/TowBar/WreckerTimedAction.lua"
|
|
$uiPath = Join-Path $releaseRoot "media/lua/client/TowBar/WreckerUI.lua"
|
|
$syncPath = Join-Path $releaseRoot "media/lua/client/TowBar/WreckerSyncClient.lua"
|
|
$serverPath = Join-Path $releaseRoot "media/lua/server/WreckerCommands.lua"
|
|
$persistencePath = Join-Path $releaseRoot "media/lua/shared/TowBar/Persistence.lua"
|
|
$menuPath = Join-Path $releaseRoot "media/lua/client/TowBar/TowingUI.lua"
|
|
|
|
$shared = if (Test-Path $sharedPath) { Get-Content $sharedPath -Raw } else { "" }
|
|
$timedAction = if (Test-Path $timedActionPath) { Get-Content $timedActionPath -Raw } else { "" }
|
|
$ui = if (Test-Path $uiPath) { Get-Content $uiPath -Raw } else { "" }
|
|
$sync = if (Test-Path $syncPath) { Get-Content $syncPath -Raw } else { "" }
|
|
$server = if (Test-Path $serverPath) { Get-Content $serverPath -Raw } else { "" }
|
|
$persistence = if (Test-Path $persistencePath) { Get-Content $persistencePath -Raw } else { "" }
|
|
$menu = Get-Content $menuPath -Raw
|
|
|
|
$supportedWreckers = @(
|
|
"Base.76chevyC30CCwrecker",
|
|
"Base.76chevyC30SCwrecker",
|
|
"Base.76chevyK30CCwrecker",
|
|
"Base.76chevyK30SCwrecker",
|
|
"Base.93chevySilveradoK3500wrecker",
|
|
"Base.93chevySilveradoK3500mechanic",
|
|
"Base.78amgeneralM62"
|
|
)
|
|
foreach ($fullName in $supportedWreckers) {
|
|
Assert-True ($shared.Contains($fullName)) "Supported wrecker missing from exact allowlist: $fullName"
|
|
}
|
|
$allowlistStart = $shared.IndexOf("local SupportedWreckers = {")
|
|
$allowlistEnd = $shared.IndexOf("local HeightAttachmentIds", $allowlistStart)
|
|
Assert-True ($allowlistStart -ge 0 -and $allowlistEnd -gt $allowlistStart) "Could not isolate the supported wrecker allowlist."
|
|
if ($allowlistStart -ge 0 -and $allowlistEnd -gt $allowlistStart) {
|
|
$allowlistBody = $shared.Substring($allowlistStart, $allowlistEnd - $allowlistStart)
|
|
$actualWreckers = [regex]::Matches($allowlistBody, '\["(Base\.[^"]+)"\]\s*=\s*true') |
|
|
ForEach-Object { $_.Groups[1].Value } | Sort-Object
|
|
Assert-True (-not (Compare-Object ($supportedWreckers | Sort-Object) $actualWreckers)) "Supported wrecker allowlist must contain exactly the seven verified KI5 vehicle scripts."
|
|
}
|
|
Assert-True ($shared -match '(?s)function Wrecker\.isSupportedWrecker.*?SupportedWreckers\[fullName\].*?attachmentExist\("hook"\)') "Wrecker detection must require an exact script name and a hook attachment."
|
|
Assert-True ($shared -notmatch 'Chevalier_Rhino_TowTruck') "The competing Rhino tow-truck implementation must not be supported by this wrecker system."
|
|
Assert-True ($shared -match 'Wrecker\.MaxAttachDistance = 0\.6') "Wrecker target selection must use the discovered 0.6-unit hook range."
|
|
Assert-True ($shared -match 'Wrecker\.HeightStep = 0\.30') "Hook height must move in 0.30-unit steps."
|
|
Assert-True ($shared -match 'Wrecker\.MinHeightLevel = 0' -and $shared -match 'Wrecker\.MaxHeightLevel = 2') "Hook height must begin at bottom and allow exactly two upward steps."
|
|
Assert-True ($shared -match 'towbarWreckerHookLow' -and $shared -match 'towbarWreckerHookMid' -and $shared -match 'towbarWreckerHookHigh') "Wreckers must use immutable low/mid/high hook attachment IDs."
|
|
Assert-True ($shared -match '(?s)registerHookAttachments.*?ModelAttachment\.new.*?baseOffset:y\(\) \+ level \* Wrecker\.HeightStep') "Hook banks must copy each wrecker's own hook geometry and vary only height."
|
|
Assert-True ($shared -match '(?s)resolveNearestTarget.*?MaxAttachDistance.*?trailerfront.*?trailer.*?distanceSquared') "Target selection must server-recompute the nearest front/rear tow point."
|
|
|
|
Assert-True ($menu -match '(?s)playerObj:getVehicle\(\).*?TowBarMod\.WreckerUI\.addOptionsToMenu.*?return') "The seated driver radial path must run before the existing early return."
|
|
Assert-True ($ui -match 'media/textures/tow_car_icon\.png') "Attach radial action must use the supplied tow-car icon."
|
|
Assert-True ($ui -match 'media/textures/untow_car_icon\.png') "Detach radial action must use the supplied untow-car icon."
|
|
Assert-True ($ui -match 'media/textures/arrow_up\.png' -and $ui -match 'media/textures/arrow_down\.png') "Height controls must use the supplied arrow icons."
|
|
Assert-True ($ui -match 'sendClientCommand\(playerObj, "towbar", "attachWrecker"' -and $ui -match 'sendClientCommand\(playerObj, "towbar", "adjustWreckerHeight"') "Wrecker radial actions must request server-authoritative commands."
|
|
Assert-True ($ui -match 'local AttachDuration = 300' -and $ui -match 'local DetachDuration = 200') "Wrecker attach and detach must take about three and two seconds respectively."
|
|
Assert-True ($ui -match '(?s)requestAttach.*?WreckerTimedAction:new.*?AttachDuration') "Attach must complete through the three-second timed action."
|
|
Assert-True ($ui -match '(?s)requestDetach.*?WreckerTimedAction:new.*?DetachDuration') "Detach must complete through the two-second timed action."
|
|
Assert-True ($timedAction -match 'ISBaseTimedAction:derive\("WreckerTimedAction"\)' -and $timedAction -match 'self\.isValidFunc') "Wrecker timed actions must revalidate while waiting."
|
|
|
|
Assert-True ($server -match 'function Commands\.attachWrecker') "Missing server-authoritative wrecker attach command."
|
|
Assert-True ($server -match 'function Commands\.detachWrecker') "Missing server-authoritative wrecker detach command."
|
|
Assert-True ($server -match 'function Commands\.adjustWreckerHeight') "Missing server-authoritative hook-height command."
|
|
Assert-True ($server -match '(?s)attachWrecker.*?isDriver\(player\).*?isSupportedWrecker.*?resolveNearestTarget.*?syncPairState\(wrecker, target, hookAttachment, targetAttachment, heightLevel, player\)') "Attach must validate driver, wrecker, geometry, and canonical target before synchronizing a constraint."
|
|
Assert-True ($server -match '(?s)local function syncPairState.*?if isServer\(\) then.*?addPointConstraint\(player, target, hookAttachment, targetAttachment\).*?broadcastWreckerSync') "Dedicated servers must own the logical towing relation while SP uses the client rigid sync path."
|
|
Assert-True ($server -match '(?s)auditWreckerPairs.*?shouldSnapshot.*?wreckerAttachSync') "The server must periodically reconcile active wrecker pairs for joining clients."
|
|
Assert-True ($server -match 'local confirmedWreckerPairs = \{\}') "Wrecker persistence must distinguish restored pairs from live constraint breaks."
|
|
Assert-True ($server -match 'wreckerTowedVehicleSqlId' -and $server -match 'wreckerTowingVehicleSqlId' -and $server -match 'getSqlId') "Wrecker persistence must use stable save IDs in addition to live network IDs."
|
|
Assert-True ($server -match 'Persistence\.savePair\(\s*"wrecker"' -and $server -match 'Persistence\.forEachPair\("wrecker"') "Wrecker persistence must use the server-owned saved-pair registry."
|
|
Assert-True ($server -match '(?s)auditWreckerPairs.*?isLinked\(wrecker, target\).*?wreckerTowedVehicleId.*?setPairState') "Adopting an engine-restored pair must refresh stale live IDs immediately."
|
|
Assert-True ($server -match '(?s)auditWreckerPairs.*?Persistence\.advanceRecoveryState.*?RestoreRetryMs, SustainedBreakMs.*?action == "break".*?detachPair.*?action == "restore".*?syncPairState') "Missing wrecker constraints 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"') "Wrecker recovery must debounce restores and require a sustained break before cleanup."
|
|
Assert-True ($server -notmatch '(?s)elseif not target then.*?wreckerTowActive = nil') "Temporarily unloaded targets must not erase saved wrecker state."
|
|
Assert-True ($server -match '(?s)auditWreckerPairs.*?wreckerTowingVehicleId.*?if towingVehicle and not isExpectedPair.*?wreckerDetachSync') "Loaded reciprocal mismatches must receive orphan cleanup."
|
|
Assert-True ($server -notmatch '(?s)wreckerTowingVehicleId.*?if not towingVehicle.*?wreckerTowingVehicleId = nil') "Temporarily unloaded wreckers must not erase saved target state."
|
|
Assert-True ($server -match '(?s)adjustWreckerHeight.*?direction ~= -1 and direction ~= 1.*?nextHeightLevel.*?syncPairState') "Height changes must validate direction, clamp on the server, and broadcast canonical state."
|
|
Assert-True ($server -notmatch 'TowBar\.TowBar|consumeTowBar|giveTowBar|dropTowBarOnGround|inventory:AddItem') "Wrecker commands must not mutate the separate towbar item lifecycle."
|
|
Assert-True ($server -match '(?s)onClientCommand.*?getTimestampMs.*?CommandCooldownMs') "Wrecker commands must be rate-limited at the server boundary."
|
|
|
|
Assert-True ($sync -match 'notTowingA_Trailer') "Client sync must use the existing fake-trailer path to select vanilla rigid constraints."
|
|
Assert-True ($sync -match '(?s)applyAttachSync.*?breakWreckerPair.*?setScriptSafely\(target, "notTowingA_Trailer"\).*?addPointConstraint.*?setScriptSafely\(target, originalScript\)') "Client sync must rebuild the constraint while the target uses the fake trailer script, then restore it."
|
|
Assert-True ($sync -match 'addPointConstraint\(nil, target, hookAttachment, targetAttachment, true\)') "Wrecker local physics rebuilds must suppress server detach/attach traffic."
|
|
Assert-True ($sync -match '(?s)local appliedLevel = Sync\.appliedLevels\[key\].*?if isPairLinked\(wrecker, target\).*?and appliedLevel == canonicalLevel then.*?return') "Only a locally applied wrecker constraint at the requested height may be reused."
|
|
Assert-True ($sync -match '(?s)local appliedLevel = Sync\.appliedLevels\[key\].*?appliedLevel == canonicalLevel.*?return\s+end.*?breakWreckerPair\(wrecker, target\).*?addPointConstraint') "A native-restored wrecker rope without a locally applied height must be rebuilt through the rigid local constraint path."
|
|
Assert-True ($sync -match '(?s)local function clearAppliedLevelForVehicle.*?Sync\.appliedLevels\[key\] = nil.*?Events\.OnSpawnVehicleEnd\.Add\(clearAppliedLevelForVehicle\)') "Vehicle streaming must invalidate cached local wrecker constraints."
|
|
Assert-True ($sync -match '(?s)if isPairLinked\(wrecker, target\).*?appliedLevel.*?canonicalLevel.*?breakWreckerPair\(wrecker, target\).*?addPointConstraint') "A requested wrecker height change must rebuild the local constraint at the new hook attachment."
|
|
Assert-True ($sync -match '(?s)local function applyFreeRollingState.*?wreckerOriginalMass.*?wreckerOriginalBrakingForce.*?setMass\(200\).*?setBrakingForce\(0\)') "Attached vehicles must be made free-rolling like trailers."
|
|
Assert-True ($sync -match '(?s)local function restoreFreeRollingState.*?setMass.*?setBrakingForce.*?wreckerOriginalMass = nil') "Detach must restore the target vehicle's pre-tow mass and braking state."
|
|
Assert-True ($sync -match '(?s)local function applyDetachSync.*?if not target then return end.*?if wrecker then breakWreckerPair.*?restoreFreeRollingState\(target\)') "Detach sync must restore a reloaded target even when the former wrecker is unavailable."
|
|
Assert-True ($sync -match '(?s)applyDetachSync.*?wreckerTowingVehicleSqlId.*?args\.wreckerSqlId.*?expectedWreckerSqlId ~= commandWreckerSqlId') "Detach sync must validate stable IDs when live IDs changed across restart."
|
|
|
|
foreach ($name in @("tow_car_icon.png", "untow_car_icon.png", "arrow_up.png", "arrow_down.png")) {
|
|
$iconPath = Join-Path $repositoryRoot "common/media/textures/$name"
|
|
Assert-True (Test-Path -LiteralPath $iconPath) "Missing runtime icon: $name"
|
|
if (Test-Path -LiteralPath $iconPath) {
|
|
$bytes = [IO.File]::ReadAllBytes($iconPath)
|
|
Assert-True ($bytes.Length -gt 24) "$name is not a valid PNG payload."
|
|
if ($bytes.Length -gt 24) {
|
|
Assert-True ($bytes[0] -eq 0x89 -and $bytes[1] -eq 0x50 -and $bytes[2] -eq 0x4E -and $bytes[3] -eq 0x47) "$name lacks a PNG signature."
|
|
$width = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($bytes, 16))
|
|
$height = [Net.IPAddress]::NetworkToHostOrder([BitConverter]::ToInt32($bytes, 20))
|
|
Assert-True ($width -eq 64 -and $height -eq 64) "$name must be 64x64."
|
|
}
|
|
}
|
|
}
|
|
|
|
$luaFiles = @($sharedPath, $timedActionPath, $uiPath, $syncPath, $serverPath) | Where-Object { Test-Path $_ }
|
|
if ($luaFiles.Count -gt 0) {
|
|
& npx --yes luaparse --quiet @luaFiles
|
|
Assert-True ($LASTEXITCODE -eq 0) "One or more wrecker Lua files failed to parse."
|
|
}
|
|
|
|
$releaseInfo = Get-Content -LiteralPath (Join-Path $releaseRoot "mod.info")
|
|
$rootInfo = Get-Content -LiteralPath (Join-Path $repositoryRoot "mod.info")
|
|
Assert-True ($releaseInfo -contains "incompatible=\STowTruck_B42") "42.20 must mark the competing Tow Truck [B42] implementation incompatible."
|
|
Assert-True ($rootInfo -contains "incompatible=\STowTruck_B42") "Root metadata must mark the competing Tow Truck [B42] implementation incompatible."
|
|
Assert-True (@($releaseInfo | Where-Object { $_ -like "incompatible=*" }).Count -eq 1) "42.20 must contain exactly one incompatibility declaration."
|
|
Assert-True (@($rootInfo | Where-Object { $_ -like "incompatible=*" }).Count -eq 1) "Root metadata must contain exactly one incompatibility declaration."
|
|
|
|
$readme = Get-Content -LiteralPath (Join-Path $repositoryRoot "README.md") -Raw
|
|
Assert-True ($readme -match '## Supported wrecker mods') "README must publish the supported-wrecker mod list."
|
|
foreach ($workshopId in @("3161951724", "3152529790", "2799152995")) {
|
|
Assert-True ($readme.Contains($workshopId)) "README supported list must include Workshop ID $workshopId."
|
|
}
|
|
foreach ($modId in @("76chevyKseries", "93chevySuburban", "93chevySuburbanExpanded", "78amgeneralM35A2")) {
|
|
Assert-True ($readme.Contains($modId)) "README supported list must include Mod ID $modId."
|
|
}
|
|
foreach ($fullName in $supportedWreckers) {
|
|
Assert-True ($readme.Contains($fullName)) "README supported list must include vehicle $fullName."
|
|
}
|
|
Assert-True ($readme.Contains("3446203945") -and $readme.Contains("STowTruck_B42") -and $readme -match 'incompatible') "README must identify and explain the competing Tow Truck mod incompatibility."
|
|
|
|
if ($failures.Count -gt 0) {
|
|
$failures | ForEach-Object { Write-Error $_ }
|
|
exit 1
|
|
}
|
|
|
|
Write-Output "PASS: Build 42.20 wrecker compatibility contracts."
|