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 {} TowBarMod.Sync.desiredPairs = TowBarMod.Sync.desiredPairs or {} if not TowBarMod.RigidTow or not TowBarMod.RigidTow.attach then require("TowBar/RigidTow") end local Sync = TowBarMod.Sync local function argsPairKey(args) if type(args) ~= "table" then return nil end local vehicleA = tonumber(args.vehicleA) local vehicleB = tonumber(args.vehicleB) if not vehicleA or not vehicleB then return nil end return tostring(vehicleA) .. ":" .. tostring(vehicleB) end local function copyAttachArgs(args) return { vehicleA = tonumber(args.vehicleA), vehicleB = tonumber(args.vehicleB), attachmentA = args.attachmentA, attachmentB = args.attachmentB } end local function pairKey(vehicleA, vehicleB) return tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId()) end 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(Sync.appliedPairs) do if pairKeyContainsVehicle(key, vehicleId) then Sync.appliedPairs[key] = nil end end end local function resolvePair(args) if type(args) ~= "table" then return nil, nil end local vehicleA = args.vehicleA and getVehicleById(tonumber(args.vehicleA)) or nil local vehicleB = args.vehicleB and getVehicleById(tonumber(args.vehicleB)) or nil return vehicleA, vehicleB end local function isPairLinked(vehicleA, vehicleB) return vehicleA and vehicleB and vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA end local function hasConflictingLink(vehicle, expectedOther) if not vehicle or not expectedOther then return false end local towing = vehicle:getVehicleTowing() local towedBy = vehicle:getVehicleTowedBy() return (towing ~= nil and towing ~= expectedOther) or (towedBy ~= nil and towedBy ~= expectedOther) end local function preparePairState(vehicleA, vehicleB, attachmentA, attachmentB) local towingMd = vehicleA:getModData() local towedMd = vehicleB:getModData() if not towingMd or not towedMd then return false end local currentScript = vehicleB:getScriptName() 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 towingMd.isTowingByTowBar = true towingMd.towed = false towingMd.towBarTowedVehicleId = vehicleB:getId() towingMd.towBarTowingVehicleId = nil towingMd.towBarExpectedAttachment = attachmentA towedMd.isTowingByTowBar = true towedMd.towed = true towedMd.towBarTowedVehicleId = nil towedMd.towBarTowingVehicleId = vehicleA:getId() towedMd.towBarExpectedAttachment = attachmentB vehicleA:transmitModData() vehicleB:transmitModData() return true end local function isLocalDriver(vehicleA, playerObj) if not vehicleA or not playerObj then return false end return vehicleA:isDriver(playerObj) end local function applyAttachSync(args, playerObj, forceReattach) local desiredKey = argsPairKey(args) if not desiredKey then return false end Sync.desiredPairs[desiredKey] = copyAttachArgs(args) local vehicleA, vehicleB = resolvePair(args) if not vehicleA or not vehicleB then return false end local attachmentA = args.attachmentA or "trailer" local attachmentB = args.attachmentB or "trailerfront" if not preparePairState(vehicleA, vehicleB, attachmentA, attachmentB) then return false end local localPlayer = playerObj if not localPlayer and type(getPlayer) == "function" then localPlayer = getPlayer() end if not isLocalDriver(vehicleA, localPlayer) then -- Portable installation happens while the player is outside. Keep the -- server/native relation until this client becomes the towing vehicle's -- physics owner; otherwise the local-only rigid add is discarded. return true end if hasConflictingLink(vehicleA, vehicleB) or hasConflictingLink(vehicleB, vehicleA) then return false end local key = pairKey(vehicleA, vehicleB) if not forceReattach and isPairLinked(vehicleA, vehicleB) and Sync.appliedPairs[key] then return true end -- This is deliberately the same one-shot method as WreckerSyncClient: -- replace any exact native relation with one client-local rigid relation. if TowBarMod.RigidTow.attach(vehicleA, vehicleB, attachmentA, attachmentB) ~= true then return false end Sync.appliedPairs[key] = true return true end local function applyDetachSync(args) local desiredKey = argsPairKey(args) local wasApplied = desiredKey and Sync.appliedPairs[desiredKey] == true if desiredKey then Sync.desiredPairs[desiredKey] = nil Sync.appliedPairs[desiredKey] = nil end local vehicleA, vehicleB = resolvePair(args) if not vehicleA or not vehicleB then return end if hasConflictingLink(vehicleA, vehicleB) or hasConflictingLink(vehicleB, vehicleA) then return end local key = pairKey(vehicleA, vehicleB) Sync.appliedPairs[key] = nil if wasApplied and TowBarMod.RigidTow and TowBarMod.RigidTow.breakExactPair then TowBarMod.RigidTow.breakExactPair(vehicleA, vehicleB) elseif wasApplied and isPairLinked(vehicleA, vehicleB) then vehicleA:breakConstraint(true, true) end if TowBarMod.Hook and TowBarMod.Hook.cleanupDetachedTowBar then TowBarMod.Hook.cleanupDetachedTowBar(vehicleA, vehicleB) end end Sync.applyAttachSync = applyAttachSync Sync.applyDetachSync = applyDetachSync local function retryDesiredPairsForDriver(character) if not character then return end local desired = {} for _, args in pairs(Sync.desiredPairs) do local vehicleA = args.vehicleA and getVehicleById(tonumber(args.vehicleA)) or nil if vehicleA and vehicleA:isDriver(character) then desired[#desired + 1] = args end end for i = 1, #desired do applyAttachSync(desired[i], character) end end local function getPersistedPairArgsForDriver(character) if not character or type(character.getVehicle) ~= "function" then return nil end local vehicleA = character:getVehicle() if not vehicleA or not vehicleA:isDriver(character) then return nil end local towingMd = vehicleA:getModData() if not towingMd or towingMd.isTowingByTowBar ~= true or towingMd.towed == true then return nil end local vehicleBId = tonumber(towingMd.towBarTowedVehicleId) local vehicleB = vehicleBId and getVehicleById(vehicleBId) or nil if not vehicleB then return nil end local towedMd = vehicleB:getModData() if not towedMd or towedMd.isTowingByTowBar ~= true or towedMd.towed ~= true or tonumber(towedMd.towBarTowingVehicleId) ~= vehicleA:getId() then return nil end return { vehicleA = vehicleA:getId(), vehicleB = vehicleB:getId(), attachmentA = towingMd.towBarExpectedAttachment or "trailer", attachmentB = towedMd.towBarExpectedAttachment or "trailerfront" } end local function forceReattachForDriver(character) if not character then return end local persistedArgs = getPersistedPairArgsForDriver(character) if persistedArgs then applyAttachSync(persistedArgs, character, true) return end -- Initial installation may have the authoritative command before all -- reciprocal modData reaches this client. Retained server args are still -- safe because only a driver-owned exact pair can pass applyAttachSync. local desired = {} for _, args in pairs(Sync.desiredPairs) do local vehicleA = args.vehicleA and getVehicleById(tonumber(args.vehicleA)) or nil if vehicleA and vehicleA:isDriver(character) then desired[#desired + 1] = args end end for i = 1, #desired do applyAttachSync(desired[i], character, true) end end local function onSpawnVehicle(vehicle) clearAppliedPairForVehicle(vehicle) if type(getPlayer) == "function" then retryDesiredPairsForDriver(getPlayer()) end end Events.OnServerCommand.Add(function(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) if Events.OnSpawnVehicleEnd then Events.OnSpawnVehicleEnd.Add(onSpawnVehicle) end if Events.OnEnterVehicle then Events.OnEnterVehicle.Add(forceReattachForDriver) end if Events.OnSwitchVehicleSeat then Events.OnSwitchVehicleSeat.Add(forceReattachForDriver) end