if isClient() then return end local TowingCommands = {} local Commands = {} local TowBarItemType = "TowBar.TowBar" local SyncDelayTicks = 2 local SnapshotIntervalTicks = 120 local pendingSync = {} local snapshotTickCounter = 0 TowingCommands.wantNoise = getDebug() or false local noise = function(msg) if TowingCommands.wantNoise then print("TowBarCommands: " .. msg) end end local function queueSync(kind, player, args) if not args then return end table.insert(pendingSync, { kind = kind, ticks = SyncDelayTicks, player = player, args = args }) end local function resolveAttachmentA(args, vehicleA) if args and args.attachmentA then return args.attachmentA end if vehicleA and vehicleA:getTowAttachmentSelf() then return vehicleA:getTowAttachmentSelf() end return "trailer" end local function resolveAttachmentB(args, vehicleB) if args and args.attachmentB then return args.attachmentB end if vehicleB and vehicleB:getTowAttachmentSelf() then return vehicleB:getTowAttachmentSelf() end return "trailerfront" 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 hasTowBarState(vehicle) if not vehicle then return false end local md = vehicle:getModData() if not md then return false end return md["isTowingByTowBar"] == true end local function broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB) if not vehicleA or not vehicleB then return end sendServerCommand("towbar", "forceAttachSync", { vehicleA = vehicleA:getId(), vehicleB = vehicleB:getId(), attachmentA = attachmentA, attachmentB = attachmentB }) end local function broadcastDetach(vehicleAId, vehicleBId) sendServerCommand("towbar", "forceDetachSync", { vehicleA = vehicleAId, vehicleB = vehicleBId }) end local function processAttachSync(item) local args = item.args or {} local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil local vehicleB = args.vehicleB and getVehicleById(args.vehicleB) or nil if not vehicleA or not vehicleB then noise("attach sync skipped missing vehicles A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB)) return end local attachmentA = resolveAttachmentA(args, vehicleA) local attachmentB = resolveAttachmentB(args, vehicleB) if not isLinked(vehicleA, vehicleB) then vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB) end if isLinked(vehicleA, vehicleB) then broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB) end end local function processDetachSync(item) local args = item.args or {} local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle local vehicleBId = args.vehicleB broadcastDetach(vehicleAId, vehicleBId) end local function snapshotActiveTowbarLinksServer() local cell = getCell() if not cell then return end local list = cell:getVehicles() if not list then return end for i = 0, list:size() - 1 do local towingVehicle = list:get(i) local towedVehicle = towingVehicle and towingVehicle:getVehicleTowing() or nil if towingVehicle and towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle then if hasTowBarState(towingVehicle) or hasTowBarState(towedVehicle) then local attachmentA = resolveAttachmentA(nil, towingVehicle) local towedMd = towedVehicle:getModData() local attachmentB = (towedMd and towedMd["towBarChangedAttachmentId"]) or resolveAttachmentB(nil, towedVehicle) if attachmentA == attachmentB then attachmentA = "trailer" attachmentB = "trailerfront" end broadcastAttach(towingVehicle, towedVehicle, attachmentA, attachmentB) end end end end local function processPendingSync() snapshotTickCounter = snapshotTickCounter + 1 if snapshotTickCounter >= SnapshotIntervalTicks then snapshotTickCounter = 0 snapshotActiveTowbarLinksServer() end if #pendingSync == 0 then return end local remaining = {} for i = 1, #pendingSync do local item = pendingSync[i] item.ticks = item.ticks - 1 if item.ticks <= 0 then if item.kind == "attach" then processAttachSync(item) elseif item.kind == "detach" then processDetachSync(item) end else table.insert(remaining, item) end end pendingSync = remaining end function Commands.attachTowBar(player, args) local vehicleA = getVehicleById(args.vehicleA) local vehicleB = getVehicleById(args.vehicleB) if not vehicleA then noise("no such vehicle (A) id=" .. tostring(args.vehicleA)) return end if not vehicleB then noise("no such vehicle (B) id=" .. tostring(args.vehicleB)) return end vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB) end function Commands.detachTowBar(player, args) local towingVehicle = args.towingVehicle and getVehicleById(args.towingVehicle) or nil local towedVehicle = args.vehicle and getVehicleById(args.vehicle) or nil if not towingVehicle and towedVehicle then towingVehicle = towedVehicle:getVehicleTowedBy() end if not towedVehicle and towingVehicle then towedVehicle = towingVehicle:getVehicleTowing() end if towedVehicle then towedVehicle:breakConstraint(true, false) end if towingVehicle and towingVehicle ~= towedVehicle then towingVehicle:breakConstraint(true, false) end end function Commands.consumeTowBar(player, args) if not player then return end local inventory = player:getInventory() if not inventory then return end local towBarItem = nil local itemId = args and args.itemId if itemId then towBarItem = inventory:getItemWithID(itemId) end if not towBarItem then towBarItem = inventory:getFirstTypeRecurse(TowBarItemType) end if not towBarItem then return end local wasPrimary = player:isPrimaryHandItem(towBarItem) local wasSecondary = player:isSecondaryHandItem(towBarItem) player:removeFromHands(towBarItem) inventory:Remove(towBarItem) sendRemoveItemFromContainer(inventory, towBarItem) if wasPrimary or wasSecondary then sendEquip(player) end end function Commands.giveTowBar(player, args) if not player then return end local inventory = player:getInventory() if not inventory then return end local towBarItem = inventory:AddItem(TowBarItemType) if not towBarItem then return end sendAddItemToContainer(inventory, towBarItem) if args and args.equipPrimary then player:setPrimaryHandItem(towBarItem) sendEquip(player) end end -- Compatibility aliases for older command names. Commands.attachConstraint = Commands.attachTowBar Commands.detachConstraint = Commands.detachTowBar TowingCommands.OnClientCommand = function(module, command, player, args) -- Only sync explicit towbar commands so vanilla towing stays untouched. if module == "towbar" and command == "attachTowBar" then queueSync("attach", player, args) elseif module == "towbar" and command == "detachTowBar" then queueSync("detach", player, args) end if module == "towbar" and Commands[command] then local argStr = "" args = args or {} for k, v in pairs(args) do argStr = argStr .. " " .. tostring(k) .. "=" .. tostring(v) end noise("received " .. module .. " " .. command .. " " .. tostring(player) .. argStr) Commands[command](player, args) end end Events.OnClientCommand.Add(TowingCommands.OnClientCommand) Events.OnTick.Add(processPendingSync)