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