42.20 and Towtrucks
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
local failures = 0
|
||||
|
||||
local function expect(condition, message)
|
||||
if not condition then
|
||||
failures = failures + 1
|
||||
io.stderr:write("FAIL: " .. message .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
package.path = "42.20/media/lua/shared/?.lua;" .. package.path
|
||||
|
||||
local globalData = {}
|
||||
ModData = {
|
||||
getOrCreate = function(name)
|
||||
globalData[name] = globalData[name] or {}
|
||||
return globalData[name]
|
||||
end,
|
||||
transmit = function() end
|
||||
}
|
||||
|
||||
TowBarMod = {}
|
||||
require("TowBar/Persistence")
|
||||
|
||||
local function collection(items)
|
||||
return {
|
||||
iterator = function()
|
||||
local index = 0
|
||||
return {
|
||||
hasNext = function() return index < #items end,
|
||||
next = function()
|
||||
index = index + 1
|
||||
return items[index]
|
||||
end
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
local function newMetrics()
|
||||
return {
|
||||
addConstraint = 0,
|
||||
breakConstraint = 0,
|
||||
attachSync = 0,
|
||||
detachSync = 0,
|
||||
worldDrops = 0
|
||||
}
|
||||
end
|
||||
|
||||
local function newVehicle(id, sqlId, metrics)
|
||||
local vehicle = {
|
||||
id = id,
|
||||
sqlId = sqlId,
|
||||
modData = {},
|
||||
towing = nil,
|
||||
towedBy = nil
|
||||
}
|
||||
|
||||
function vehicle:getId() return self.id end
|
||||
function vehicle:getSqlId() return self.sqlId end
|
||||
function vehicle:getModData() return self.modData end
|
||||
function vehicle:transmitModData() end
|
||||
function vehicle:getVehicleTowing() return self.towing end
|
||||
function vehicle:getVehicleTowedBy() return self.towedBy end
|
||||
function vehicle:attachmentExist() return true end
|
||||
function vehicle:addPointConstraint()
|
||||
-- Deliberately do not establish the link. Build 42 may acknowledge a
|
||||
-- new native constraint on a later tick.
|
||||
metrics.addConstraint = metrics.addConstraint + 1
|
||||
end
|
||||
function vehicle:breakConstraint()
|
||||
metrics.breakConstraint = metrics.breakConstraint + 1
|
||||
self.towing = nil
|
||||
self.towedBy = nil
|
||||
end
|
||||
function vehicle:getSquare()
|
||||
return {
|
||||
AddWorldInventoryItem = function()
|
||||
metrics.worldDrops = metrics.worldDrops + 1
|
||||
return {}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
return vehicle
|
||||
end
|
||||
|
||||
local function linkPair(towing, towed)
|
||||
towing.towing = towed
|
||||
towing.towedBy = nil
|
||||
towed.towing = nil
|
||||
towed.towedBy = towing
|
||||
end
|
||||
|
||||
local function unlinkPair(towing, towed)
|
||||
towing.towing = nil
|
||||
towing.towedBy = nil
|
||||
towed.towing = nil
|
||||
towed.towedBy = nil
|
||||
end
|
||||
|
||||
local function installEvents()
|
||||
local callbacks = {}
|
||||
Events = {
|
||||
OnClientCommand = {
|
||||
Add = function(callback) callbacks.clientCommand = callback end
|
||||
},
|
||||
OnTick = {
|
||||
Add = function(callback) callbacks.tick = callback end
|
||||
}
|
||||
}
|
||||
return callbacks
|
||||
end
|
||||
|
||||
local function fireTicks(callback, count)
|
||||
for _ = 1, count do callback() end
|
||||
end
|
||||
|
||||
local function resetRegistry()
|
||||
globalData["TowBar.PersistentPairsV2"] = { pairs = {} }
|
||||
end
|
||||
|
||||
local function runPortableHandlerSpec()
|
||||
resetRegistry()
|
||||
local metrics = newMetrics()
|
||||
local callbacks = installEvents()
|
||||
local now = 1000
|
||||
local towing = newVehicle(101, 1001, metrics)
|
||||
local towed = newVehicle(202, 2002, metrics)
|
||||
local loaded = { towing, towed }
|
||||
local byId = { [101] = towing, [202] = towed }
|
||||
|
||||
isClient = function() return false end
|
||||
isServer = function() return true end
|
||||
getDebug = function() return false end
|
||||
getTimestampMs = function() return now end
|
||||
getCell = function()
|
||||
return { getVehicles = function() return collection(loaded) end }
|
||||
end
|
||||
getVehicleById = function(id) return byId[id] end
|
||||
sendServerCommand = function(_, command)
|
||||
if command == "forceAttachSync" then
|
||||
metrics.attachSync = metrics.attachSync + 1
|
||||
elseif command == "forceDetachSync" or command == "spontaneousDetachSync" then
|
||||
metrics.detachSync = metrics.detachSync + 1
|
||||
end
|
||||
end
|
||||
|
||||
expect(
|
||||
TowBarMod.Persistence.savePair(
|
||||
"towbar", towing, towed, "trailer", "trailerfront"
|
||||
),
|
||||
"portable setup must persist a stable pair"
|
||||
)
|
||||
dofile("42.20/media/lua/server/TowingCommands.lua")
|
||||
expect(type(callbacks.tick) == "function", "portable server must register its audit tick")
|
||||
|
||||
fireTicks(callbacks.tick, 5)
|
||||
expect(metrics.addConstraint == 1, "portable restart must request one native restore")
|
||||
expect(metrics.attachSync == 1, "portable restart must broadcast one restore")
|
||||
|
||||
now = 1500
|
||||
fireTicks(callbacks.tick, 5)
|
||||
expect(metrics.addConstraint == 1, "portable pending restore must not duplicate its constraint")
|
||||
expect(metrics.attachSync == 1, "portable pending restore must not duplicate its sync")
|
||||
|
||||
linkPair(towing, towed)
|
||||
now = 2000
|
||||
fireTicks(callbacks.tick, 5)
|
||||
expect(metrics.addConstraint == 1, "portable delayed acknowledgement must be adopted without re-adding")
|
||||
expect(towing.modData.towBarTowedVehicleId == towed:getId(), "portable adoption must refresh the towing live ID")
|
||||
expect(towed.modData.towBarTowingVehicleId == towing:getId(), "portable adoption must refresh the towed live ID")
|
||||
|
||||
unlinkPair(towing, towed)
|
||||
loaded = { towing }
|
||||
byId[202] = nil
|
||||
now = 10000
|
||||
fireTicks(callbacks.tick, 5)
|
||||
now = 16000
|
||||
fireTicks(callbacks.tick, 5)
|
||||
expect(metrics.worldDrops == 0, "an unloaded portable peer must not drop a towbar")
|
||||
expect(metrics.detachSync == 0, "an unloaded portable peer must not broadcast destructive cleanup")
|
||||
expect(towing.modData.isTowingByTowBar == true, "an unloaded portable peer must keep active metadata")
|
||||
expect(towing.modData.towBarTowedVehicleSqlId == towed:getSqlId(), "an unloaded portable peer must keep stable identity")
|
||||
|
||||
loaded = { towing, towed }
|
||||
byId[202] = towed
|
||||
now = 17000
|
||||
fireTicks(callbacks.tick, 5)
|
||||
expect(metrics.addConstraint == 2, "a reloaded portable peer must re-enter recovery")
|
||||
expect(metrics.attachSync == 2, "a reloaded portable peer must receive one fresh restore sync")
|
||||
expect(metrics.worldDrops == 0, "portable peer reload must not be mistaken for a break")
|
||||
expect(metrics.breakConstraint == 0, "portable peer reload must not break an unrelated constraint")
|
||||
end
|
||||
|
||||
local function runWreckerHandlerSpec()
|
||||
resetRegistry()
|
||||
local metrics = newMetrics()
|
||||
local callbacks = installEvents()
|
||||
local now = 1000
|
||||
local wrecker = newVehicle(303, 3003, metrics)
|
||||
local target = newVehicle(404, 4004, metrics)
|
||||
local loaded = { wrecker, target }
|
||||
local byId = { [303] = wrecker, [404] = target }
|
||||
|
||||
TowBarMod.Wrecker = {
|
||||
getWorldVehicles = function() return collection(loaded) end,
|
||||
isSupportedWrecker = function(vehicle) return vehicle == wrecker end,
|
||||
normalizeHeightLevel = function(level)
|
||||
return math.max(0, math.min(2, tonumber(level) or 0))
|
||||
end,
|
||||
getHeightAttachmentId = function(level)
|
||||
return ({ [0] = "towbarWreckerHookLow", [1] = "towbarWreckerHookMid", [2] = "towbarWreckerHookHigh" })[level]
|
||||
end
|
||||
}
|
||||
|
||||
isClient = function() return false end
|
||||
isServer = function() return true end
|
||||
getTimestampMs = function() return now end
|
||||
getVehicleById = function(id) return byId[id] end
|
||||
sendServerCommand = function(_, command)
|
||||
if command == "wreckerAttachSync" then
|
||||
metrics.attachSync = metrics.attachSync + 1
|
||||
elseif command == "wreckerDetachSync" then
|
||||
metrics.detachSync = metrics.detachSync + 1
|
||||
end
|
||||
end
|
||||
|
||||
expect(
|
||||
TowBarMod.Persistence.savePair(
|
||||
"wrecker", wrecker, target,
|
||||
"towbarWreckerHookHigh", "trailerfront", 2
|
||||
),
|
||||
"wrecker setup must persist a stable pair"
|
||||
)
|
||||
dofile("42.20/media/lua/server/WreckerCommands.lua")
|
||||
expect(type(callbacks.tick) == "function", "wrecker server must register its audit tick")
|
||||
|
||||
fireTicks(callbacks.tick, 30)
|
||||
expect(metrics.addConstraint == 1, "wrecker restart must request one native restore")
|
||||
expect(metrics.attachSync == 1, "wrecker restart must broadcast one restore")
|
||||
|
||||
now = 1500
|
||||
fireTicks(callbacks.tick, 30)
|
||||
expect(metrics.addConstraint == 1, "wrecker pending restore must not duplicate its constraint")
|
||||
expect(metrics.attachSync == 1, "wrecker pending restore must not duplicate its sync")
|
||||
|
||||
linkPair(wrecker, target)
|
||||
now = 2000
|
||||
fireTicks(callbacks.tick, 30)
|
||||
expect(metrics.addConstraint == 1, "wrecker delayed acknowledgement must be adopted without re-adding")
|
||||
expect(wrecker.modData.wreckerHeightLevel == 2, "wrecker adoption must preserve the saved hook height")
|
||||
expect(wrecker.modData.wreckerTargetAttachment == "trailerfront", "wrecker adoption must preserve the target endpoint")
|
||||
|
||||
unlinkPair(wrecker, target)
|
||||
loaded = { wrecker }
|
||||
byId[404] = nil
|
||||
now = 10000
|
||||
fireTicks(callbacks.tick, 30)
|
||||
now = 16000
|
||||
fireTicks(callbacks.tick, 30)
|
||||
expect(metrics.detachSync == 0, "an unloaded wrecker peer must not broadcast destructive cleanup")
|
||||
expect(wrecker.modData.wreckerTowActive == true, "an unloaded wrecker peer must keep active metadata")
|
||||
expect(wrecker.modData.wreckerTowedVehicleSqlId == target:getSqlId(), "an unloaded wrecker peer must keep stable identity")
|
||||
|
||||
loaded = { wrecker, target }
|
||||
byId[404] = target
|
||||
now = 17000
|
||||
fireTicks(callbacks.tick, 30)
|
||||
expect(metrics.addConstraint == 2, "a reloaded wrecker peer must re-enter recovery")
|
||||
expect(metrics.attachSync == 2, "a reloaded wrecker peer must receive one fresh restore sync")
|
||||
expect(metrics.detachSync == 0, "wrecker peer reload must not be mistaken for a break")
|
||||
expect(metrics.worldDrops == 0, "wrecker recovery must never create a portable towbar item")
|
||||
expect(metrics.breakConstraint == 0, "wrecker peer reload must not break an unrelated constraint")
|
||||
end
|
||||
|
||||
runPortableHandlerSpec()
|
||||
runWreckerHandlerSpec()
|
||||
|
||||
if failures > 0 then os.exit(1) end
|
||||
print("PASS: production persistence handlers debounce recovery and survive peer unload")
|
||||
@@ -0,0 +1,69 @@
|
||||
local failures = 0
|
||||
local function expect(condition, message)
|
||||
if not condition then
|
||||
failures = failures + 1
|
||||
io.stderr:write("FAIL: " .. message .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
local globalData = {}
|
||||
local transmitted = 0
|
||||
ModData = {
|
||||
getOrCreate = function(name)
|
||||
globalData[name] = globalData[name] or {}
|
||||
return globalData[name]
|
||||
end,
|
||||
transmit = function() transmitted = transmitted + 1 end
|
||||
}
|
||||
function isServer() return true end
|
||||
TowBarMod = {}
|
||||
|
||||
dofile("42.20/media/lua/shared/TowBar/Persistence.lua")
|
||||
|
||||
local function vehicle(sqlId)
|
||||
return { getSqlId = function() return sqlId end }
|
||||
end
|
||||
|
||||
local towing, towed = vehicle(101), vehicle(202)
|
||||
expect(TowBarMod.Persistence.savePair("towbar", towing, towed, "trailer", "trailerfront"), "valid SQL pair must save")
|
||||
local pair = TowBarMod.Persistence.getPair("towbar", towing, towed)
|
||||
expect(pair and pair.towingSqlId == 101 and pair.towedSqlId == 202, "saved pair must use stable SQL IDs")
|
||||
expect(pair and pair.attachmentA == "trailer" and pair.attachmentB == "trailerfront", "saved pair must retain attachments")
|
||||
|
||||
expect(TowBarMod.Persistence.savePair("wrecker", towing, towed, "towbarWreckerHookHigh", "trailer", 2), "wrecker pair must save")
|
||||
local wreckerPair = TowBarMod.Persistence.getPair("wrecker", towing, towed)
|
||||
expect(wreckerPair and wreckerPair.heightLevel == 2, "wrecker pair must retain hook height")
|
||||
|
||||
local seen = 0
|
||||
TowBarMod.Persistence.forEachPair("towbar", function(record)
|
||||
if record.towingSqlId == 101 and record.towedSqlId == 202 then seen = seen + 1 end
|
||||
end)
|
||||
expect(seen == 1, "registry iteration must return the saved pair exactly once")
|
||||
expect(TowBarMod.Persistence.removePair("towbar", towing, towed), "saved pair must be removable")
|
||||
expect(TowBarMod.Persistence.getPair("towbar", towing, towed) == nil, "removed pair must stay absent")
|
||||
expect(transmitted >= 3, "server registry changes must transmit")
|
||||
expect(not TowBarMod.Persistence.savePair("towbar", vehicle(-1), towed, "trailer", "trailerfront"), "unsaved vehicles must not create unstable records")
|
||||
|
||||
local state, action = TowBarMod.Persistence.advanceRecoveryState(nil, 1000, false, false, 2000, 5000)
|
||||
expect(action == "restore", "first missing-link observation must start one restore")
|
||||
state, action = TowBarMod.Persistence.advanceRecoveryState(state, 1500, false, false, 2000, 5000)
|
||||
expect(action == "wait", "an in-flight restore must not be duplicated")
|
||||
state, action = TowBarMod.Persistence.advanceRecoveryState(state, 3000, true, false, 2000, 5000)
|
||||
expect(action == "adopt" and state.confirmed, "a restored link must be adopted")
|
||||
state, action = TowBarMod.Persistence.advanceRecoveryState(state, 3100, false, false, 2000, 5000)
|
||||
expect(action == "wait", "one missing sample must not destroy an adopted link")
|
||||
state, action = TowBarMod.Persistence.advanceRecoveryState(state, 8099, false, false, 2000, 5000)
|
||||
expect(action == "wait", "cleanup must wait for the complete break grace period")
|
||||
state, action = TowBarMod.Persistence.advanceRecoveryState(state, 8100, false, false, 2000, 5000)
|
||||
expect(action == "break", "a continuously missing confirmed link must eventually break")
|
||||
|
||||
state = TowBarMod.Persistence.notePeerAvailability({ confirmed = true }, false)
|
||||
expect(state.peerMissing, "an unloaded peer must be remembered")
|
||||
state = TowBarMod.Persistence.notePeerAvailability(state, true)
|
||||
expect(state.confirmed == false, "a reloaded pair must re-enter recovery instead of break cleanup")
|
||||
expect(state.peerMissing == nil, "the peer-missing marker must clear after both vehicles load")
|
||||
state, action = TowBarMod.Persistence.advanceRecoveryState(state, 9000, false, false, 2000, 5000)
|
||||
expect(action == "restore", "a reloaded pair must rebuild its constraint")
|
||||
|
||||
if failures > 0 then os.exit(1) end
|
||||
print("PASS: persistent pair registry behavior")
|
||||
@@ -0,0 +1,183 @@
|
||||
local failures = 0
|
||||
|
||||
local function expect(condition, message)
|
||||
if not condition then
|
||||
failures = failures + 1
|
||||
io.stderr:write("FAIL: " .. message .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
local serverCommand
|
||||
local vehicleSpawned
|
||||
Events = {
|
||||
OnServerCommand = {
|
||||
Add = function(callback) serverCommand = callback end
|
||||
},
|
||||
OnSpawnVehicleEnd = {
|
||||
Add = function(callback) vehicleSpawned = callback end
|
||||
}
|
||||
}
|
||||
|
||||
isServer = function() return false end
|
||||
|
||||
local trace = {}
|
||||
local function record(value) trace[#trace + 1] = value end
|
||||
|
||||
local function script()
|
||||
return {
|
||||
getAttachmentById = function(_, id)
|
||||
if id == "trailer" or id == "trailerfront" then return {} end
|
||||
return nil
|
||||
end,
|
||||
getWheelCount = function() return 0 end,
|
||||
getPhysicsChassisShape = function()
|
||||
return { z = function() return 4 end }
|
||||
end,
|
||||
addAttachment = function() end
|
||||
}
|
||||
end
|
||||
|
||||
local rigidAdds = 0
|
||||
local breaks = 0
|
||||
local towing
|
||||
local towed
|
||||
|
||||
local function vehicle(id)
|
||||
local value = {
|
||||
id = id,
|
||||
modData = {},
|
||||
script = script(),
|
||||
towing = nil,
|
||||
towedBy = nil
|
||||
}
|
||||
|
||||
function value:getId() return self.id end
|
||||
function value:getScript() return self.script end
|
||||
function value:getScriptName() return "Base.TestVehicle" end
|
||||
function value:getModData() return self.modData end
|
||||
function value:transmitModData() end
|
||||
function value:getMass() return 1200 end
|
||||
function value:getBrakingForce() return 25 end
|
||||
function value:getVehicleTowing() return self.towing end
|
||||
function value:getVehicleTowedBy() return self.towedBy end
|
||||
function value:addPointConstraint(_, other, attachmentA, attachmentB, localOnly)
|
||||
rigidAdds = rigidAdds + 1
|
||||
record("rigid-add")
|
||||
expect(self == towing and other == towed, "rigid rebuild must use the original pair")
|
||||
expect(attachmentA == "trailer" and attachmentB == "trailerfront", "rigid rebuild must preserve endpoints")
|
||||
expect(localOnly == true, "rigid rebuild must be local-only to avoid an attach/detach command race")
|
||||
self.towing = other
|
||||
other.towedBy = self
|
||||
end
|
||||
function value:breakConstraint()
|
||||
breaks = breaks + 1
|
||||
record("break-native")
|
||||
if self == towing or self == towed then
|
||||
towing.towing = nil
|
||||
towing.towedBy = nil
|
||||
towed.towing = nil
|
||||
towed.towedBy = nil
|
||||
end
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
towing = vehicle(101)
|
||||
towed = vehicle(202)
|
||||
local vehicles = { [101] = towing, [202] = towed }
|
||||
getVehicleById = function(id) return vehicles[id] end
|
||||
|
||||
TowBarMod = {
|
||||
Utils = {
|
||||
updateAttachmentsForRigidTow = function()
|
||||
record("rigid-offsets")
|
||||
end
|
||||
},
|
||||
Hook = {
|
||||
cleanupDetachedTowBar = function()
|
||||
record("cleanup")
|
||||
towing.modData = {}
|
||||
towed.modData = {}
|
||||
end,
|
||||
setVehicleScriptWithTowBarHidden = function(_, scriptName)
|
||||
record("script:" .. tostring(scriptName))
|
||||
return true
|
||||
end,
|
||||
setVehiclePostAttach = function()
|
||||
record("post-attach")
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
dofile("42.20/media/lua/client/TowBar/TowSyncClient.lua")
|
||||
expect(type(serverCommand) == "function", "client sync must register its server-command handler")
|
||||
|
||||
-- Begin with the old physical pair so spontaneous cleanup exercises the same
|
||||
-- path as a real Build 42 break notification.
|
||||
towing.towing = towed
|
||||
towed.towedBy = towing
|
||||
towing.modData.towBarTowedVehicleId = towed:getId()
|
||||
towed.modData.towBarTowingVehicleId = towing:getId()
|
||||
serverCommand("towbar", "spontaneousDetachSync", {
|
||||
vehicleA = towing:getId(),
|
||||
vehicleB = towed:getId()
|
||||
})
|
||||
expect(breaks > 0, "spontaneous cleanup must remove the old native relation")
|
||||
expect(towing:getVehicleTowing() == nil and towed:getVehicleTowedBy() == nil, "spontaneous cleanup must leave the pair unlinked")
|
||||
|
||||
-- A direct reattach can arrive from the server as a native B42 relationship
|
||||
-- before forceAttachSync reaches the client. That native relationship is the
|
||||
-- rope-like constraint; forceAttachSync must replace it with the rigid path.
|
||||
towing.towing = towed
|
||||
towed.towedBy = towing
|
||||
local breaksBeforeReattach = breaks
|
||||
serverCommand("towbar", "forceAttachSync", {
|
||||
vehicleA = towing:getId(),
|
||||
vehicleB = towed:getId(),
|
||||
attachmentA = "trailer",
|
||||
attachmentB = "trailerfront"
|
||||
})
|
||||
|
||||
expect(breaks == breaksBeforeReattach + 1, "direct reattach must replace the native B42 rope relation")
|
||||
expect(rigidAdds == 1, "direct reattach must create exactly one rigid local constraint")
|
||||
|
||||
local fakeScriptIndex
|
||||
local rigidAddIndex
|
||||
for index, value in ipairs(trace) do
|
||||
if value == "script:notTowingA_Trailer" and not fakeScriptIndex then fakeScriptIndex = index end
|
||||
if value == "rigid-add" and not rigidAddIndex then rigidAddIndex = index end
|
||||
end
|
||||
expect(fakeScriptIndex ~= nil, "rigid reattach must select the fake-trailer script")
|
||||
expect(rigidAddIndex ~= nil and fakeScriptIndex < rigidAddIndex, "fake-trailer selection must precede rigid constraint creation")
|
||||
expect(towing:getVehicleTowing() == towed and towed:getVehicleTowedBy() == towing, "rigid rebuild must finish with a reciprocal tow relation")
|
||||
|
||||
local rigidAddsAfterFirstSync = rigidAdds
|
||||
local breaksAfterFirstSync = breaks
|
||||
serverCommand("towbar", "forceAttachSync", {
|
||||
vehicleA = towing:getId(),
|
||||
vehicleB = towed:getId(),
|
||||
attachmentA = "trailer",
|
||||
attachmentB = "trailerfront"
|
||||
})
|
||||
expect(rigidAdds == rigidAddsAfterFirstSync, "a repeated attach snapshot must not layer a second rigid constraint")
|
||||
expect(breaks == breaksAfterFirstSync, "a repeated attach snapshot must adopt the already-rigid pair")
|
||||
|
||||
-- Streaming can recreate a native relationship with the same runtime IDs
|
||||
-- while the Lua module and its applied cache survive. The spawn notification
|
||||
-- must invalidate that stale cache so the next snapshot replaces the rope.
|
||||
expect(type(vehicleSpawned) == "function", "client sync must observe vehicle streaming")
|
||||
vehicleSpawned(towed)
|
||||
local rigidAddsBeforeStreamRestore = rigidAdds
|
||||
local breaksBeforeStreamRestore = breaks
|
||||
serverCommand("towbar", "forceAttachSync", {
|
||||
vehicleA = towing:getId(),
|
||||
vehicleB = towed:getId(),
|
||||
attachmentA = "trailer",
|
||||
attachmentB = "trailerfront"
|
||||
})
|
||||
expect(rigidAdds == rigidAddsBeforeStreamRestore + 1, "stream-restored native rope must be rebuilt rigid")
|
||||
expect(breaks == breaksBeforeStreamRestore + 1, "stream-restored native rope must be removed exactly once")
|
||||
|
||||
if failures > 0 then os.exit(1) end
|
||||
print("PASS: spontaneous break followed by direct attach rebuilds a rigid towbar")
|
||||
+110
-9
@@ -29,14 +29,23 @@ 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 "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.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."
|
||||
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
|
||||
Assert-True (-not (Compare-Object $baselineFiles $releaseFiles)) "42.20 file layout must match the latest baseline."
|
||||
$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 {
|
||||
@@ -52,18 +61,42 @@ $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.*?towBarTowedVehicleId.*?vehicleB:getId\(\).*?towBarTowingVehicleId.*?vehicleA:getId\(\)') "Expected towbar pair validation must be reciprocal."
|
||||
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."
|
||||
@@ -83,13 +116,30 @@ Assert-True ($serverCommands -match 'if item\.reservedTowBar then') "Failed mult
|
||||
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 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 function breakTowBarPair') "Client detach sync must break only the requested pair."
|
||||
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."
|
||||
@@ -97,13 +147,63 @@ Assert-True ($hooking -match '(?s)if modData\.towBarOriginalParkingBrakeOn ~= ni
|
||||
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 '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."
|
||||
@@ -130,6 +230,7 @@ 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."
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
$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."
|
||||
@@ -0,0 +1,89 @@
|
||||
local failures = 0
|
||||
|
||||
local function expect(condition, message)
|
||||
if not condition then
|
||||
failures = failures + 1
|
||||
io.stderr:write("FAIL: " .. message .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
Events = { OnGameBoot = { Add = function() end } }
|
||||
Vector3f = { new = function() return {} end }
|
||||
|
||||
local function point(x, y, z)
|
||||
return {
|
||||
x = function() return x end,
|
||||
y = function() return y end,
|
||||
z = function() return z or 0 end
|
||||
}
|
||||
end
|
||||
|
||||
local function vehicle(id, fullName, attachments)
|
||||
local linkedTowing
|
||||
local linkedTowedBy
|
||||
local script = {
|
||||
getFullName = function() return fullName end,
|
||||
getName = function() return string.gsub(fullName, "^Base%.", "") end
|
||||
}
|
||||
return {
|
||||
getId = function() return id end,
|
||||
getScript = function() return script end,
|
||||
attachmentExist = function(_, name) return attachments[name] ~= nil end,
|
||||
getAttachmentWorldPos = function(_, name) return attachments[name] end,
|
||||
getVehicleTowing = function() return linkedTowing end,
|
||||
getVehicleTowedBy = function() return linkedTowedBy end,
|
||||
setLinks = function(_, towing, towedBy) linkedTowing, linkedTowedBy = towing, towedBy end
|
||||
}
|
||||
end
|
||||
|
||||
dofile("42.20/media/lua/shared/TowBar/WreckerUtils.lua")
|
||||
|
||||
local supported = {
|
||||
"Base.76chevyC30CCwrecker",
|
||||
"Base.76chevyC30SCwrecker",
|
||||
"Base.76chevyK30CCwrecker",
|
||||
"Base.76chevyK30SCwrecker",
|
||||
"Base.93chevySilveradoK3500wrecker",
|
||||
"Base.93chevySilveradoK3500mechanic",
|
||||
"Base.78amgeneralM62"
|
||||
}
|
||||
for index, fullName in ipairs(supported) do
|
||||
expect(TowBarMod.Wrecker.isSupportedWrecker(vehicle(index, fullName, { hook = point(0, 0) })), fullName .. " should be supported")
|
||||
expect(not TowBarMod.Wrecker.isSupportedWrecker(vehicle(index + 100, fullName, {})), fullName .. " without a hook must be rejected")
|
||||
end
|
||||
expect(not TowBarMod.Wrecker.isSupportedWrecker(vehicle(10, supported[1], {})), "exact variant without hook must be rejected")
|
||||
expect(not TowBarMod.Wrecker.isSupportedWrecker(vehicle(11, "Base.76chevyK30CC", { hook = point(0, 0) })), "regular pickup must be rejected")
|
||||
expect(not TowBarMod.Wrecker.isSupportedWrecker(vehicle(12, supported[1] .. "Burnt", { hook = point(0, 0) })), "suffixed lookalike must be rejected")
|
||||
expect(not TowBarMod.Wrecker.isSupportedWrecker(vehicle(13, "Base.78amgeneralM35A2", { hook = point(0, 0) })), "ordinary M35A2 must be rejected")
|
||||
expect(not TowBarMod.Wrecker.isSupportedWrecker(vehicle(14, "Base.Chevalier_Rhino_TowTruck", { hook = point(0, 0) })), "competing Rhino tow truck must be rejected")
|
||||
|
||||
expect(TowBarMod.Wrecker.nextHeightLevel(0, 1) == 1, "height should raise once from bottom")
|
||||
expect(TowBarMod.Wrecker.nextHeightLevel(1, 1) == 2, "height should raise twice from bottom")
|
||||
expect(TowBarMod.Wrecker.nextHeightLevel(2, 1) == 2, "height must clamp at the second upward step")
|
||||
expect(TowBarMod.Wrecker.nextHeightLevel(0, -1) == 0, "height must not move below the initial hook position")
|
||||
expect(TowBarMod.Wrecker.nextHeightLevel(2, -1) == 1, "height should lower one step")
|
||||
expect(TowBarMod.Wrecker.nextHeightLevel(0, 0) == nil, "invalid height direction must be rejected")
|
||||
expect(TowBarMod.Wrecker.getHeightAttachmentId(0) == "towbarWreckerHookLow", "level 0 must use the bottom hook")
|
||||
expect(TowBarMod.Wrecker.getHeightAttachmentId(1) == "towbarWreckerHookMid", "level 1 must use the first upward step")
|
||||
expect(TowBarMod.Wrecker.getHeightAttachmentId(2) == "towbarWreckerHookHigh", "level 2 must use the second upward step")
|
||||
expect(TowBarMod.Wrecker.normalizeHeightLevel(-1) == 0, "old below-bottom state must migrate to bottom")
|
||||
|
||||
local wrecker = vehicle(20, supported[1], { hook = point(0, 0, 9) })
|
||||
local nearRear = vehicle(21, "Base.CarA", {
|
||||
trailer = point(0.599, 0, -50), trailerfront = point(4, 0, 50)
|
||||
})
|
||||
local nearFront = vehicle(22, "Base.CarB", {
|
||||
trailer = point(5, 0), trailerfront = point(0.4, 0.1)
|
||||
})
|
||||
local result = TowBarMod.Wrecker.resolveNearestTarget(wrecker, { nearRear, nearFront })
|
||||
expect(result and result.vehicle == nearFront and result.attachment == "trailerfront", "nearest endpoint across all vehicles must win")
|
||||
|
||||
result = TowBarMod.Wrecker.resolveNearestTarget(wrecker, { nearRear })
|
||||
expect(result and result.attachment == "trailer", "0.599 endpoint must be accepted and world Z ignored")
|
||||
local tooFar = vehicle(23, "Base.CarC", { trailer = point(0.601, 0) })
|
||||
expect(TowBarMod.Wrecker.resolveNearestTarget(wrecker, { tooFar }) == nil, "0.601 endpoint must be rejected")
|
||||
local trailer = vehicle(24, "Base.UtilityTrailer", { trailerfront = point(0.1, 0) })
|
||||
expect(TowBarMod.Wrecker.resolveNearestTarget(wrecker, { trailer }) == nil, "trailers must be excluded from wrecker vehicle towing")
|
||||
|
||||
if failures > 0 then os.exit(1) end
|
||||
print("PASS: wrecker helper behavior")
|
||||
@@ -0,0 +1,54 @@
|
||||
local failures = 0
|
||||
|
||||
local function expect(condition, message)
|
||||
if not condition then
|
||||
failures = failures + 1
|
||||
io.stderr:write("FAIL: " .. message .. "\n")
|
||||
end
|
||||
end
|
||||
|
||||
package.preload["TimedActions/ISBaseTimedAction"] = function() return true end
|
||||
ISBaseTimedAction = {}
|
||||
function ISBaseTimedAction:derive()
|
||||
local derived = {}
|
||||
setmetatable(derived, { __index = self })
|
||||
return derived
|
||||
end
|
||||
function ISBaseTimedAction.new(class, character)
|
||||
local action = { character = character }
|
||||
setmetatable(action, { __index = class })
|
||||
return action
|
||||
end
|
||||
function ISBaseTimedAction.perform(action)
|
||||
action.completed = true
|
||||
end
|
||||
function ISBaseTimedAction.stop(action)
|
||||
action.stopped = true
|
||||
end
|
||||
|
||||
dofile("42.20/media/lua/client/TowBar/WreckerTimedAction.lua")
|
||||
|
||||
local sent = 0
|
||||
local valid = true
|
||||
local function validate() return valid end
|
||||
local function perform() sent = sent + 1 end
|
||||
|
||||
local attach = WreckerTimedAction:new({}, 300, validate, perform, {}, {})
|
||||
expect(attach.maxTime == 300, "attach duration must be 300 action units")
|
||||
expect(sent == 0, "constructing an action must not send a command")
|
||||
attach:perform()
|
||||
expect(sent == 1 and attach.completed, "valid attach must send once after its timer completes")
|
||||
|
||||
local detach = WreckerTimedAction:new({}, 200, validate, perform, {}, {})
|
||||
expect(detach.maxTime == 200, "detach duration must be 200 action units")
|
||||
valid = false
|
||||
detach:perform()
|
||||
expect(sent == 1, "an invalidated timed action must not send its command")
|
||||
|
||||
valid = true
|
||||
local cancelled = WreckerTimedAction:new({}, 300, validate, perform, {}, {})
|
||||
cancelled:stop()
|
||||
expect(cancelled.stopped and sent == 1, "cancelling an action must not send its command")
|
||||
|
||||
if failures > 0 then os.exit(1) end
|
||||
print("PASS: wrecker timed action behavior")
|
||||
Reference in New Issue
Block a user