Files
Towbar/tests/rigid-tow-contract-42.20-spec.lua
2026-08-19 12:12:12 -04:00

179 lines
7.8 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
isServer = function() return false end
local metrics = {
offsets = 0,
breaks = 0,
adds = 0,
hides = 0,
postAttach = 0,
scriptSwaps = 0,
freeRolling = 0
}
local trace = {}
local function record(event) trace[#trace + 1] = event end
local function script()
return {
getAttachmentById = function(_, id)
if id == "trailer" or id == "trailerfront" then return {} end
return nil
end
}
end
local acknowledgeAdd = true
local function unlink(a, b)
a.towing, a.towedBy = nil, nil
b.towing, b.towedBy = nil, nil
end
local function link(a, b)
a.towing, a.towedBy = b, nil
b.towing, b.towedBy = nil, a
end
local function vehicle(id)
local value = {
id = id,
towing = nil,
towedBy = nil,
script = script(),
scriptName = "Base.TestVehicle",
modData = {}
}
function value:getId() return self.id end
function value:getScript() return self.script end
function value:getScriptName() return self.scriptName end
function value:getModData() return self.modData end
function value:getVehicleTowing() return self.towing end
function value:getVehicleTowedBy() return self.towedBy end
function value:breakConstraint()
metrics.breaks = metrics.breaks + 1
record("break")
local other = self.towing or self.towedBy
if other then unlink(self, other) end
end
function value:addPointConstraint(player, other, attachmentA, attachmentB, localOnly)
metrics.adds = metrics.adds + 1
record("add")
expect(player == nil, "rigid primitive must not impersonate a player")
expect(attachmentA == "trailer" and attachmentB == "trailerfront",
"rigid primitive must preserve selected attachment IDs")
expect(localOnly == true, "rigid primitive must create the client-local rigid constraint")
if acknowledgeAdd then link(self, other) end
end
return value
end
TowBarMod = {
Utils = {
updateAttachmentsForRigidTow = function(a, b, attachmentA, attachmentB)
metrics.offsets = metrics.offsets + 1
expect(a ~= nil and b ~= nil, "rigid offset update must receive both vehicles")
expect(attachmentA == "trailer" and attachmentB == "trailerfront",
"rigid offset update must receive the selected endpoints")
end
},
Hook = {
applyFreeRollingTowState = function(vehicle)
metrics.freeRolling = metrics.freeRolling + 1
record("free")
expect(vehicle ~= nil, "rigid primitive must receive the towed vehicle for free-roll setup")
end,
setVehicleScriptWithTowBarHidden = function(vehicle, scriptName)
metrics.hides = metrics.hides + 1
metrics.scriptSwaps = metrics.scriptSwaps + 1
vehicle.scriptName = scriptName
record("script:" .. tostring(scriptName))
expect(vehicle ~= nil,
"rigid primitive must receive a vehicle for each hidden script swap")
end,
setVehiclePostAttach = function(player, vehicle, towingVehicle)
metrics.postAttach = metrics.postAttach + 1
record("post")
expect(player == nil and vehicle ~= nil and towingVehicle ~= nil,
"rigid primitive must restore post-attach state after acknowledgement")
expect(vehicle:getScriptName() == "Base.TestVehicle",
"rigid primitive must restore the real script before post-attach finalization")
end
}
}
dofile("42.20/media/lua/client/TowBar/RigidTow.lua")
expect(TowBarMod.RigidTow ~= nil and type(TowBarMod.RigidTow.attach) == "function",
"production rigid module must expose attach")
-- First attach with no physical relation uses the exact rigid path once.
local a, b = vehicle(101), vehicle(102)
trace = {}
local attached = TowBarMod.RigidTow.attach(a, b, "trailer", "trailerfront")
expect(attached == true, "acknowledged initial rigid add must succeed")
expect(a:getVehicleTowing() == b and b:getVehicleTowedBy() == a,
"acknowledged rigid add must leave an exact reciprocal relation")
expect(metrics.offsets == 1 and metrics.adds == 1 and metrics.breaks == 0,
"initial rigid add must update offsets and add once without a needless break")
expect(metrics.scriptSwaps == 2 and metrics.postAttach == 1,
"successful rigid add must select the fake script and restore the real script exactly once")
expect(table.concat(trace, ",") == "free,script:notTowingA_Trailer,add,script:Base.TestVehicle,post",
"initial rigid sequence must be free-roll, fake script, local add, real script, then post-attach")
-- A restored native relation is replaced inside this primitive. The lifecycle
-- controller must not grow its own second copy of this physics sequence.
local restoredA, restoredB = vehicle(201), vehicle(202)
link(restoredA, restoredB)
local beforeOffsets, beforeAdds = metrics.offsets, metrics.adds
local beforeBreaks, beforePost = metrics.breaks, metrics.postAttach
trace = {}
attached = TowBarMod.RigidTow.attach(restoredA, restoredB, "trailer", "trailerfront")
expect(attached == true, "restored native relation must be replaced by rigid mechanics")
expect(metrics.offsets == beforeOffsets + 1 and metrics.adds == beforeAdds + 1,
"native replacement must update offsets and add one rigid constraint")
expect(metrics.breaks == beforeBreaks + 1,
"native replacement must break the exact old pair once")
expect(metrics.postAttach == beforePost + 1,
"native replacement must run post-attach only after the new link exists")
expect(table.concat(trace, ",") == "break,free,script:notTowingA_Trailer,add,script:Base.TestVehicle,post",
"native replacement sequence must break, free-roll, use the fake script, add, restore the real script, then finalize")
-- A link to a different vehicle is never collateral damage.
local conflictA, expectedB, unrelated = vehicle(301), vehicle(302), vehicle(399)
link(conflictA, unrelated)
local beforeConflictBreaks, beforeConflictAdds = metrics.breaks, metrics.adds
attached = TowBarMod.RigidTow.attach(conflictA, expectedB, "trailer", "trailerfront")
expect(attached == false, "conflicting physical link must reject rigid attachment")
expect(metrics.breaks == beforeConflictBreaks and metrics.adds == beforeConflictAdds,
"conflicting physical link must not break or layer another constraint")
-- Invalid endpoints are rejected before modifying a valid existing pair.
local missingA, missingB = vehicle(351), vehicle(352)
link(missingA, missingB)
local beforeMissingBreaks, beforeMissingAdds = metrics.breaks, metrics.adds
attached = TowBarMod.RigidTow.attach(missingA, missingB, "missing", "trailerfront")
expect(attached == false, "missing attachment must reject rigid attachment")
expect(metrics.breaks == beforeMissingBreaks and metrics.adds == beforeMissingAdds,
"missing attachment must not break the existing pair or attempt an add")
-- In multiplayer Java/Bullet may accept the local constraint before the Lua
-- reciprocal getters update. Submission is success; waiting for those getters
-- here strands an already-streamed pair with only its visual state applied.
local delayedA, delayedB = vehicle(401), vehicle(402)
acknowledgeAdd = false
local beforeDelayedPost = metrics.postAttach
attached = TowBarMod.RigidTow.attach(delayedA, delayedB, "trailer", "trailerfront")
expect(attached == true, "submitted rigid add must tolerate delayed multiplayer getters")
expect(metrics.postAttach == beforeDelayedPost + 1,
"delayed multiplayer getters must not block post-attach restoration")
if failures > 0 then os.exit(1) end
print("PASS: single rigid-tow contract preserves exact-pair mechanics and delayed MP submission")