if isServer() then return end if not TowBarMod then TowBarMod = {} end TowBarMod.WreckerSync = TowBarMod.WreckerSync or {} local Sync = TowBarMod.WreckerSync Sync.appliedLevels = Sync.appliedLevels or {} Sync.desiredPairs = Sync.desiredPairs or {} local function argsPairKey(args) if type(args) ~= "table" then return nil end local wreckerId = tonumber(args.wrecker) local targetId = tonumber(args.target) if not wreckerId or not targetId then return nil end return tostring(wreckerId) .. ":" .. tostring(targetId) end local function copyAttachArgs(args) return { wrecker = tonumber(args.wrecker), target = tonumber(args.target), wreckerSqlId = tonumber(args.wreckerSqlId), targetSqlId = tonumber(args.targetSqlId), targetAttachment = args.targetAttachment, heightLevel = tonumber(args.heightLevel) or 0 } 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 clearAppliedLevelForVehicle(vehicle) if not vehicle then return end local vehicleId = vehicle:getId() for key in pairs(Sync.appliedLevels) do if pairKeyContainsVehicle(key, vehicleId) then Sync.appliedLevels[key] = nil end end end local function pairKey(wrecker, target) return tostring(wrecker:getId()) .. ":" .. tostring(target:getId()) end local function callVehicle(vehicle, methodName, value) local method = vehicle and vehicle[methodName] or nil if not method then return false, nil end return pcall(function() if value ~= nil then return method(vehicle, value) end return method(vehicle) end) end local function applyFreeRollingState(vehicle) local md = vehicle and vehicle:getModData() or nil if not md then return end local capturedOriginal = false if md.wreckerOriginalMass == nil then md.wreckerOriginalMass = vehicle:getMass() capturedOriginal = true end if md.wreckerOriginalBrakingForce == nil then md.wreckerOriginalBrakingForce = vehicle:getBrakingForce() capturedOriginal = true end if md.wreckerOriginalParkingBrakeOn == nil then local ok, value = callVehicle(vehicle, "isParkingBrakeOn") if ok then md.wreckerOriginalParkingBrakeOn = value capturedOriginal = true end end if md.wreckerOriginalParkingBrake == nil then local ok, value = callVehicle(vehicle, "getParkingBrake") if ok then md.wreckerOriginalParkingBrake = value capturedOriginal = true end end if md.wreckerOriginalHandbrake == nil then local ok, value = callVehicle(vehicle, "isHandbrake") if ok then md.wreckerOriginalHandbrake = value capturedOriginal = true end end vehicle:setMass(200) vehicle:setBrakingForce(0) if md.wreckerOriginalParkingBrakeOn ~= nil then callVehicle(vehicle, "setParkingBrakeOn", false) end if md.wreckerOriginalParkingBrake ~= nil then callVehicle(vehicle, "setParkingBrake", false) end if md.wreckerOriginalHandbrake ~= nil then callVehicle(vehicle, "setHandbrake", false) end if capturedOriginal then vehicle:transmitModData() end end local function restoreFreeRollingState(vehicle) local md = vehicle and vehicle:getModData() or nil if not md then return end if md.wreckerOriginalMass ~= nil then vehicle:setMass(md.wreckerOriginalMass) end if md.wreckerOriginalBrakingForce ~= nil then vehicle:setBrakingForce(md.wreckerOriginalBrakingForce) end if md.wreckerOriginalParkingBrakeOn ~= nil then callVehicle(vehicle, "setParkingBrakeOn", md.wreckerOriginalParkingBrakeOn) end if md.wreckerOriginalParkingBrake ~= nil then callVehicle(vehicle, "setParkingBrake", md.wreckerOriginalParkingBrake) end if md.wreckerOriginalHandbrake ~= nil then callVehicle(vehicle, "setHandbrake", md.wreckerOriginalHandbrake) end md.wreckerOriginalMass = nil md.wreckerOriginalBrakingForce = nil md.wreckerOriginalParkingBrakeOn = nil md.wreckerOriginalParkingBrake = nil md.wreckerOriginalHandbrake = nil vehicle:transmitModData() end local function isPairLinked(wrecker, target) return wrecker and target and wrecker:getVehicleTowing() == target and target:getVehicleTowedBy() == wrecker end local function hasConflictingLink(vehicle, other) return vehicle and ((vehicle:getVehicleTowing() and vehicle:getVehicleTowing() ~= other) or (vehicle:getVehicleTowedBy() and vehicle:getVehicleTowedBy() ~= other)) end local function breakWreckerPair(wrecker, target) if not wrecker or not target then return end if isPairLinked(wrecker, target) then wrecker:breakConstraint(true, true) end end local function setScriptSafely(vehicle, scriptName) if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then return TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicle, scriptName) end vehicle:setScriptName(scriptName) return true end local function applyAttachSync(args, forceReattach) if not args then return end local desiredKey = argsPairKey(args) if desiredKey then Sync.desiredPairs[desiredKey] = copyAttachArgs(args) end local wrecker = getVehicleById(args.wrecker) local target = getVehicleById(args.target) if not wrecker or not target then return end if hasConflictingLink(wrecker, target) or hasConflictingLink(target, wrecker) then return end local hookAttachment = TowBarMod.Wrecker.getHeightAttachmentId(args.heightLevel) local targetAttachment = args.targetAttachment if not hookAttachment or (targetAttachment ~= "trailer" and targetAttachment ~= "trailerfront") then return end if not wrecker:attachmentExist(hookAttachment) or not target:attachmentExist(targetAttachment) then return end local key = pairKey(wrecker, target) local canonicalLevel = tonumber(args.heightLevel) or 0 local appliedLevel = Sync.appliedLevels[key] if not forceReattach and isPairLinked(wrecker, target) and appliedLevel == canonicalLevel then Sync.appliedLevels[key] = canonicalLevel applyFreeRollingState(target) return end breakWreckerPair(wrecker, target) local targetMd = target:getModData() local originalScript = targetMd.wreckerOriginalScriptName or target:getScriptName() targetMd.wreckerOriginalScriptName = originalScript applyFreeRollingState(target) setScriptSafely(target, "notTowingA_Trailer") wrecker:addPointConstraint(nil, target, hookAttachment, targetAttachment, true) setScriptSafely(target, originalScript) local wreckerMd = wrecker:getModData() wreckerMd.wreckerTowActive = true wreckerMd.wreckerTowedVehicleId = target:getId() wreckerMd.wreckerTowedVehicleSqlId = tonumber(args.targetSqlId) wreckerMd.wreckerTargetAttachment = targetAttachment wreckerMd.wreckerHeightLevel = canonicalLevel targetMd.wreckerTowingVehicleId = wrecker:getId() targetMd.wreckerTowingVehicleSqlId = tonumber(args.wreckerSqlId) Sync.appliedLevels[key] = canonicalLevel end local function applyDetachSync(args) if not args then return end local desiredKey = argsPairKey(args) if desiredKey then Sync.desiredPairs[desiredKey] = nil end local wrecker = getVehicleById(args.wrecker) local target = getVehicleById(args.target) if not target then return end local targetMd = target:getModData() local expectedWreckerId = tonumber(targetMd.wreckerTowingVehicleId) local expectedWreckerSqlId = tonumber(targetMd.wreckerTowingVehicleSqlId) local commandWreckerSqlId = tonumber(args.wreckerSqlId) if expectedWreckerSqlId and commandWreckerSqlId then if expectedWreckerSqlId ~= commandWreckerSqlId then return end elseif expectedWreckerId and expectedWreckerId ~= tonumber(args.wrecker) then return end if wrecker and (hasConflictingLink(wrecker, target) or hasConflictingLink(target, wrecker)) then return end if wrecker then breakWreckerPair(wrecker, target) end if targetMd.wreckerOriginalScriptName and target:getScriptName() ~= targetMd.wreckerOriginalScriptName then setScriptSafely(target, targetMd.wreckerOriginalScriptName) end restoreFreeRollingState(target) if wrecker then Sync.appliedLevels[pairKey(wrecker, target)] = nil end if wrecker then local wreckerMd = wrecker:getModData() wreckerMd.wreckerTowActive = nil wreckerMd.wreckerTowedVehicleId = nil wreckerMd.wreckerTowedVehicleSqlId = nil wreckerMd.wreckerTargetAttachment = nil wreckerMd.wreckerHeightLevel = nil end targetMd.wreckerTowingVehicleId = nil targetMd.wreckerTowingVehicleSqlId = nil targetMd.wreckerOriginalScriptName = nil end Sync.applyAttachSync = applyAttachSync Sync.applyDetachSync = applyDetachSync local function forceReattachForDriver(character) if not character or type(character.getVehicle) ~= "function" then return end local wrecker = character:getVehicle() if not wrecker or not wrecker:isDriver(character) then return end local wreckerMd = wrecker:getModData() if not wreckerMd or wreckerMd.wreckerTowActive ~= true then return end local targetId = tonumber(wreckerMd.wreckerTowedVehicleId) local target = targetId and getVehicleById(targetId) or nil if not target then return end local targetMd = target:getModData() if not targetMd or tonumber(targetMd.wreckerTowingVehicleId) ~= wrecker:getId() then return end applyAttachSync({ wrecker = wrecker:getId(), target = target:getId(), wreckerSqlId = wrecker:getSqlId(), targetSqlId = wreckerMd.wreckerTowedVehicleSqlId, targetAttachment = wreckerMd.wreckerTargetAttachment, heightLevel = tonumber(wreckerMd.wreckerHeightLevel) or 0 }, true) end Events.OnServerCommand.Add(function(module, command, args) if module ~= "towbar" then return end if command == "wreckerAttachSync" then applyAttachSync(args) elseif command == "wreckerDetachSync" then applyDetachSync(args) end end) if Events.OnSpawnVehicleEnd then Events.OnSpawnVehicleEnd.Add(clearAppliedLevelForVehicle) end if Events.OnEnterVehicle then Events.OnEnterVehicle.Add(forceReattachForDriver) end if Events.OnSwitchVehicleSeat then Events.OnSwitchVehicleSeat.Add(forceReattachForDriver) end