Files
Towbar/tests/towbar-rigid-reattach-42.20-spec.lua
T
2026-08-13 18:59:54 -04:00

184 lines
6.5 KiB
Lua

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")