if isServer() then return end if not TowBarMod then TowBarMod = {} end TowBarMod.Sync = TowBarMod.Sync or {} if TowBarMod.Sync._towSyncClientLoaded then return end TowBarMod.Sync._towSyncClientLoaded = true TowBarMod.Sync.appliedPairs = TowBarMod.Sync.appliedPairs or {} local function pairKeyContainsVehicle(key, vehicleId) local id = tostring(vehicleId) return string.sub(key, 1, #id + 1) == id .. ":" or string.sub(key, -(#id + 1)) == ":" .. id end local function clearAppliedPairForVehicle(vehicle) if not vehicle then return end local vehicleId = vehicle:getId() for key in pairs(TowBarMod.Sync.appliedPairs) do if pairKeyContainsVehicle(key, vehicleId) then TowBarMod.Sync.appliedPairs[key] = nil end end end local function resolveVehicle(id) if not id then return nil end return getVehicleById(id) end local function ensureAttachment(vehicle, attachmentId) if not vehicle or not attachmentId then return false end local script = vehicle:getScript() if not script then return false end if script:getAttachmentById(attachmentId) ~= nil then return true end local wheelCount = script:getWheelCount() local yOffset = -0.5 if wheelCount > 0 then local wheel = script:getWheel(0) if wheel and wheel:getOffset() then yOffset = wheel:getOffset():y() + 0.1 end end local chassis = script:getPhysicsChassisShape() if not chassis then return false end local attach = ModelAttachment.new(attachmentId) if attachmentId == "trailer" then attach:getOffset():set(0, yOffset, -chassis:z() / 2 - 0.1) attach:setZOffset(-1) else attach:getOffset():set(0, yOffset, chassis:z() / 2 + 0.1) attach:setZOffset(1) end script:addAttachment(attach) return true end local function isLinked(vehicleA, vehicleB) if not vehicleA or not vehicleB then return false end return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA end local function reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB) if TowBarMod.Utils and TowBarMod.Utils.updateAttachmentsForRigidTow then TowBarMod.Utils.updateAttachmentsForRigidTow(vehicleA, vehicleB, attachmentA, attachmentB) end local towingMd = vehicleA:getModData() local towedMd = vehicleB:getModData() local currentScript = vehicleB:getScriptName() if towingMd then towingMd["isTowingByTowBar"] = true towingMd["towed"] = false towingMd["towBarTowedVehicleId"] = vehicleB:getId() towingMd["towBarTowingVehicleId"] = nil towingMd["towBarExpectedAttachment"] = attachmentA vehicleA:transmitModData() end if towedMd then if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then towedMd.towBarOriginalScriptName = currentScript end if towedMd.towBarOriginalMass == nil then towedMd.towBarOriginalMass = vehicleB:getMass() end if towedMd.towBarOriginalBrakingForce == nil then towedMd.towBarOriginalBrakingForce = vehicleB:getBrakingForce() end towedMd["isTowingByTowBar"] = true towedMd["towed"] = true towedMd["towBarTowedVehicleId"] = nil towedMd["towBarTowingVehicleId"] = vehicleA:getId() towedMd["towBarExpectedAttachment"] = attachmentB vehicleB:transmitModData() end if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicleB, "notTowingA_Trailer") end if TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then pcall(TowBarMod.Hook.setVehiclePostAttach, nil, vehicleB) end end local breakTowBarPair local function applyAttachSync(args) if not args then return end local vehicleA = resolveVehicle(args.vehicleA) local vehicleB = resolveVehicle(args.vehicleB) if not vehicleA or not vehicleB then return end local attachmentA = args.attachmentA or "trailer" local attachmentB = args.attachmentB or "trailerfront" if not ensureAttachment(vehicleA, attachmentA) or not ensureAttachment(vehicleB, attachmentB) then return end local key = tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId()) if TowBarMod.Sync.appliedPairs[key] and not isLinked(vehicleA, vehicleB) then -- Vehicle streaming can remove the native constraint while the Lua -- module remains loaded. Allow the authoritative recovery sync to -- rebuild local physics for the returning pair. TowBarMod.Sync.appliedPairs[key] = nil end if not TowBarMod.Sync.appliedPairs[key] then -- The server initially creates a normal Build 42 tow relation. It may -- also restore one from the save. Replace that native rope exactly -- once with this mod's rigid fake-trailer constraint. breakTowBarPair(vehicleA, vehicleB) if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicleB, "notTowingA_Trailer") end -- The final true keeps this a local physics rebuild. The server owns -- the persisted logical relation and must not receive a detach/attach race. vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB, true) TowBarMod.Sync.appliedPairs[key] = true end reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB) end local function hasConflictingTowLink(vehicle, expectedOther) if not vehicle or not expectedOther then return false end local towing = vehicle:getVehicleTowing() local towedBy = vehicle:getVehicleTowedBy() if (towing ~= nil and towing ~= expectedOther) or (towedBy ~= nil and towedBy ~= expectedOther) then return true end -- Also reject a delayed detach while a new tow is being established but -- has not created its physical constraint yet. local modData = vehicle:getModData() if not modData then return false end local expectedOtherId = expectedOther:getId() return (modData["towBarTowedVehicleId"] ~= nil and modData["towBarTowedVehicleId"] ~= expectedOtherId) or (modData["towBarTowingVehicleId"] ~= nil and modData["towBarTowingVehicleId"] ~= expectedOtherId) end breakTowBarPair = function(vehicleA, vehicleB) if not vehicleA or not vehicleB then return end local vehicleAReferencesB = vehicleA:getVehicleTowing() == vehicleB or vehicleA:getVehicleTowedBy() == vehicleB local vehicleBReferencesA = vehicleB:getVehicleTowing() == vehicleA or vehicleB:getVehicleTowedBy() == vehicleA if vehicleAReferencesB then vehicleA:breakConstraint(true, true) elseif vehicleBReferencesA then vehicleB:breakConstraint(true, true) end end local function applyDetachSync(args) if not args then return end local vehicleA = resolveVehicle(args.vehicleA) local vehicleB = resolveVehicle(args.vehicleB) if not vehicleA or not vehicleB then return end if hasConflictingTowLink(vehicleA, vehicleB) or hasConflictingTowLink(vehicleB, vehicleA) then return end breakTowBarPair(vehicleA, vehicleB) TowBarMod.Sync.appliedPairs[tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())] = nil if TowBarMod.Hook and TowBarMod.Hook.cleanupDetachedTowBar then pcall(TowBarMod.Hook.cleanupDetachedTowBar, vehicleA, vehicleB) end end local function onServerCommand(module, command, args) if module ~= "towbar" then return end if command == "forceAttachSync" then applyAttachSync(args) elseif command == "forceDetachSync" or command == "spontaneousDetachSync" then applyDetachSync(args) end end TowBarMod.Sync.applyAttachSync = applyAttachSync TowBarMod.Sync.applyDetachSync = applyDetachSync Events.OnServerCommand.Add(onServerCommand) if Events.OnSpawnVehicleEnd then Events.OnSpawnVehicleEnd.Add(clearAppliedPairForVehicle) end