Bump mod version to 1.0.5

This commit is contained in:
2026-08-12 13:36:01 -04:00
parent 40f0296080
commit a6ba4877d0
23 changed files with 6650 additions and 1 deletions
+89
View File
@@ -0,0 +1,89 @@
BTtow = {}
BTtow.Create = {}
BTtow.Init = {}
local TowbarVariantSize = 24
local TowbarNormalStart = 0
local TowbarLargeStart = 24
local TowbarMaxIndex = TowbarVariantSize - 1
local VanillaScaleMin = 1.5
local VanillaScaleMax = 2.0
local function getVehicleModelScale(script)
if not script then return nil end
local ok, result = pcall(function()
return script:getModelScale()
end)
if ok and type(result) == "number" then
return result
end
ok, result = pcall(function()
local model = script:getModel()
if model then
return model:getScale()
end
return nil
end)
if ok and type(result) == "number" then
return result
end
return nil
end
local function isVanillaScale(script)
local modelScale = getVehicleModelScale(script)
if modelScale == nil then
return true
end
local configuredMin = TowBarMod and TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMin)
local configuredMax = TowBarMod and TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMax)
local minScale = configuredMin or VanillaScaleMin
local maxScale = configuredMax or VanillaScaleMax
return modelScale >= minScale and modelScale <= maxScale
end
local function getTowbarIndexVanilla(script)
local z = script:getPhysicsChassisShape():z() / 2 - 0.1
local index = math.floor((z * 2 / 3 - 1) * 10)
return math.max(0, math.min(TowbarMaxIndex, index))
end
local function getTowbarModelSlot(script)
if not isVanillaScale(script) then
-- KI5/small-scale vehicles use the normal towbar0 mesh at Z=1.
return 0
end
return getTowbarIndexVanilla(script)
end
function BTtow.Create.towbar(vehicle, part)
if part == nil then return end
for j=0, TowbarVariantSize - 1 do
part:setModelVisible("towbar" .. j, false)
end
end
function BTtow.Init.towbar(vehicle, part)
if part == nil then return end
for j=0, TowbarVariantSize - 1 do
part:setModelVisible("towbar" .. j, false)
end
local modData = vehicle:getModData()
if modData and modData.towBarModelSwapInProgress then
return
end
if modData and modData["isTowingByTowBar"] and modData["towed"] then
local script = vehicle:getScript()
if script then
local index = getTowbarModelSlot(script)
local shouldShowOnThisPart = part:getId() == "towbar"
if shouldShowOnThisPart then
part:setModelVisible("towbar" .. index, true)
end
end
end
end
@@ -0,0 +1,71 @@
require 'Items/ProceduralDistributions'
require 'Items/SuburbsDistributions'
require 'Items/Distributions'
require 'Items/Distribution_BinJunk'
require 'Items/Distribution_ClosetJunk'
require 'Items/Distribution_DeskJunk'
require 'Items/Distribution_ShelfJunk'
require 'Items/Distribution_CounterJunk'
require 'Items/Distribution_SideTableJunk'
require 'Vehicles/VehicleDistributions'
require 'Vehicles/VehicleDistribution_GloveBoxJunk'
require 'Vehicles/VehicleDistribution_SeatJunk'
require 'Vehicles/VehicleDistribution_TrunkJunk'
----------------- TOW BAR -----------------------
-- Mirror Jack spawn chance into TowBar in container distributions (world + vehicle containers).
-- Intentionally excludes story-clutter floor placement tables (RandomizedWorldContent/StoryClutter).
local TOWBAR_ITEM_TYPE = "TowBar.TowBar"
local JACK_ITEM_TYPES = {
["Jack"] = true,
["Base.Jack"] = true,
}
local function addMissingTowBarsForJack(items)
if type(items) ~= "table" then return end
local jackCountByChance = {}
local towBarCountByChance = {}
for i = 1, #items, 2 do
local itemType = items[i]
local chance = tonumber(items[i + 1])
if type(itemType) == "string" and chance ~= nil then
if JACK_ITEM_TYPES[itemType] then
jackCountByChance[chance] = (jackCountByChance[chance] or 0) + 1
elseif itemType == TOWBAR_ITEM_TYPE then
towBarCountByChance[chance] = (towBarCountByChance[chance] or 0) + 1
end
end
end
for chance, jackCount in pairs(jackCountByChance) do
local missing = jackCount - (towBarCountByChance[chance] or 0)
for _ = 1, missing do
table.insert(items, TOWBAR_ITEM_TYPE)
table.insert(items, chance)
end
end
end
local function walkContainerDistributions(root, seen)
if type(root) ~= "table" or seen[root] then return end
seen[root] = true
for key, value in pairs(root) do
if key == "items" and type(value) == "table" then
addMissingTowBarsForJack(value)
elseif type(value) == "table" then
walkContainerDistributions(value, seen)
end
end
end
local seen = {}
walkContainerDistributions(ProceduralDistributions, seen)
walkContainerDistributions(SuburbsDistributions, seen)
walkContainerDistributions(Distributions, seen)
walkContainerDistributions(VehicleDistributions, seen)
walkContainerDistributions(ClutterTables, seen)
+669
View File
@@ -0,0 +1,669 @@
if isClient() then return end
local TowingCommands = {}
local Commands = {}
local TowBarItemType = "TowBar.TowBar"
local SyncDelayTicks = 2
local SnapshotIntervalTicks = 120
local BrokenPairAuditIntervalTicks = 5
local pendingSync = {}
local snapshotTickCounter = 0
local brokenPairAuditTickCounter = 0
local confirmedTowPairs = {}
local giveTowBar
TowingCommands.wantNoise = getDebug() or false
local noise = function(msg)
if TowingCommands.wantNoise then
print("TowBarCommands: " .. msg)
end
end
local function queueSync(kind, player, args, reservedTowBar)
if not args then return end
table.insert(pendingSync, {
kind = kind,
ticks = SyncDelayTicks,
attempts = 0,
reservedTowBar = reservedTowBar == true,
player = player,
args = args
})
end
local function cancelPendingAttach(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return end
local vehicleAId = vehicleA:getId()
local vehicleBId = vehicleB:getId()
local remaining = {}
for i = 1, #pendingSync do
local item = pendingSync[i]
local itemArgs = item.args or {}
local isThisAttach = item.kind == "attach"
and itemArgs.vehicleA == vehicleAId
and itemArgs.vehicleB == vehicleBId
if isThisAttach then
-- The reserved item is about to become the single ground drop.
item.reservedTowBar = false
else
table.insert(remaining, item)
end
end
pendingSync = remaining
end
local function hasPendingAttach(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return false end
local vehicleAId = vehicleA:getId()
local vehicleBId = vehicleB:getId()
for i = 1, #pendingSync do
local item = pendingSync[i]
local itemArgs = item.args or {}
if item.kind == "attach"
and itemArgs.vehicleA == vehicleAId
and itemArgs.vehicleB == vehicleBId then
return true
end
end
return false
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 hasAnyTowLink(vehicle)
if not vehicle then return false end
return vehicle:getVehicleTowing() ~= nil or vehicle:getVehicleTowedBy() ~= nil
end
local function getTowBarPairKey(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return nil end
return tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())
end
local function markTowBarPairConfirmed(vehicleA, vehicleB)
local key = getTowBarPairKey(vehicleA, vehicleB)
if key then confirmedTowPairs[key] = true end
end
local function isTowBarPairConfirmed(vehicleA, vehicleB)
local key = getTowBarPairKey(vehicleA, vehicleB)
return key ~= nil and confirmedTowPairs[key] == true
end
local function forgetTowBarPair(vehicleA, vehicleB)
local key = getTowBarPairKey(vehicleA, vehicleB)
if key then confirmedTowPairs[key] = nil end
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 markExpectedTowBarPair(vehicleA, vehicleB, attachmentA, attachmentB)
if not vehicleA or not vehicleB then return end
local towingModData = vehicleA:getModData()
local towedModData = vehicleB:getModData()
if not towingModData or not towedModData then return end
towingModData["isTowingByTowBar"] = true
towingModData["towed"] = false
towingModData["towBarTowedVehicleId"] = vehicleB:getId()
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarExpectedAttachment"] = attachmentA
towedModData["isTowingByTowBar"] = true
towedModData["towed"] = true
towedModData["towBarTowedVehicleId"] = nil
towedModData["towBarTowingVehicleId"] = vehicleA:getId()
towedModData["towBarExpectedAttachment"] = attachmentB
vehicleA:transmitModData()
vehicleB:transmitModData()
end
local function clearExpectedTowBarPair(vehicleA, vehicleB)
if vehicleA then
local towingModData = vehicleA:getModData()
if towingModData then
towingModData["isTowingByTowBar"] = false
towingModData["towed"] = false
towingModData["towBarTowedVehicleId"] = nil
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarExpectedAttachment"] = nil
vehicleA:transmitModData()
end
end
if vehicleB then
local towedModData = vehicleB:getModData()
if towedModData then
towedModData["isTowingByTowBar"] = false
towedModData["towed"] = false
towedModData["towBarTowedVehicleId"] = nil
towedModData["towBarTowingVehicleId"] = nil
towedModData["towBarExpectedAttachment"] = nil
vehicleB:transmitModData()
end
end
end
local function isExpectedTowBarPair(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return false end
local towingModData = vehicleA:getModData()
local towedModData = vehicleB:getModData()
if not towingModData or not towedModData then return false end
return towingModData["towBarTowedVehicleId"] == vehicleB:getId()
and towedModData["towBarTowingVehicleId"] == vehicleA:getId()
end
local function isLegacyTowBarPair(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return false end
local towingModData = vehicleA:getModData()
local towedModData = vehicleB:getModData()
if not towingModData or not towedModData then return false end
return hasTowBarState(vehicleA) and hasTowBarState(vehicleB)
and towedModData["towed"] == true
and towingModData["towBarTowedVehicleId"] == nil
and towingModData["towBarTowingVehicleId"] == nil
and towedModData["towBarTowedVehicleId"] == nil
and towedModData["towBarTowingVehicleId"] == nil
end
local function resolveExpectedTowBarPair(vehicle)
if not vehicle then return nil, nil end
local towingVehicle = vehicle:getVehicleTowing() and vehicle or vehicle:getVehicleTowedBy()
local towedVehicle = vehicle:getVehicleTowing() or (vehicle:getVehicleTowedBy() and vehicle or nil)
if towingVehicle and towedVehicle
and (isExpectedTowBarPair(towingVehicle, towedVehicle)
or isLegacyTowBarPair(towingVehicle, towedVehicle)) then
return towingVehicle, towedVehicle
end
local modData = vehicle:getModData()
if not modData then return nil, nil end
if modData["towBarTowedVehicleId"] ~= nil then
local towedVehicle = getVehicleById(modData["towBarTowedVehicleId"])
if isExpectedTowBarPair(vehicle, towedVehicle) then
return vehicle, towedVehicle
end
end
if modData["towBarTowingVehicleId"] ~= nil then
local towingVehicle = getVehicleById(modData["towBarTowingVehicleId"])
if isExpectedTowBarPair(towingVehicle, vehicle) then
return towingVehicle, vehicle
end
end
return nil, nil
end
local function isPlayerAuthorizedForPair(player, vehicleA, vehicleB)
if not player or not vehicleA or not vehicleB then return false end
local playerVehicle = player:getVehicle()
if playerVehicle == vehicleA or playerVehicle == vehicleB then
return true
end
local playerX = player:getX()
local playerY = player:getY()
local function isNear(vehicle)
local deltaX = playerX - vehicle:getX()
local deltaY = playerY - vehicle:getY()
return deltaX * deltaX + deltaY * deltaY <= 64
end
return isNear(vehicleA) or isNear(vehicleB)
end
local function findTowBarItem(player, itemId)
if not player then return nil end
local inventory = player:getInventory()
if not inventory then return nil end
local towBarItem = itemId and inventory:getItemWithID(itemId) or nil
if towBarItem and towBarItem:getFullType() ~= TowBarItemType then
towBarItem = nil
end
return towBarItem or inventory:getFirstTypeRecurse(TowBarItemType)
end
local function removeTowBarItem(player, towBarItem)
if not player or not towBarItem then return false end
local inventory = player:getInventory()
if not inventory or not inventory:contains(towBarItem, true) then return false 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
return true
end
local function forEachCollectionItem(collection, callback)
if not collection then return end
local ok, iterator = pcall(function()
return collection:iterator()
end)
if ok and iterator then
while iterator:hasNext() do
callback(iterator:next())
end
return
end
local size
ok, size = pcall(function()
return collection:size()
end)
if not ok or type(size) ~= "number" then return end
for i = 0, size - 1 do
callback(collection:get(i))
end
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 broadcastSpontaneousDetach(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return end
sendServerCommand("towbar", "spontaneousDetachSync", {
vehicleA = vehicleA:getId(),
vehicleB = vehicleB:getId()
})
end
local function breakTowBarConstraint(towingVehicle, towedVehicle)
if not towingVehicle or not towedVehicle then return end
-- Only break a relation between this exact pair. Stale towbar metadata must
-- never disconnect either vehicle from a different trailer.
local towingReferencesTowed = towingVehicle:getVehicleTowing() == towedVehicle
or towingVehicle:getVehicleTowedBy() == towedVehicle
local towedReferencesTowing = towedVehicle:getVehicleTowing() == towingVehicle
or towedVehicle:getVehicleTowedBy() == towingVehicle
if towedReferencesTowing then
towedVehicle:breakConstraint(true, false)
end
if towingReferencesTowed then
towingVehicle:breakConstraint(true, false)
end
end
local function dropTowBarOnGround(towingVehicle, towedVehicle)
local square = towedVehicle and towedVehicle:getSquare() or nil
if not square and towingVehicle then
square = towingVehicle:getSquare()
end
if not square then
noise("could not drop broken towbar because neither vehicle has a square")
return false
end
local item = square:AddWorldInventoryItem(TowBarItemType, 0.5, 0.5, 0)
if not item then
noise("failed to drop broken towbar on the ground")
return false
end
return true
end
local function finalizeBrokenTowBarPair(towingVehicle, towedVehicle, reason)
if not towingVehicle or not towedVehicle then return false end
if not isExpectedTowBarPair(towingVehicle, towedVehicle)
and not isLegacyTowBarPair(towingVehicle, towedVehicle) then
return false
end
local shouldDrop = hasTowBarState(towingVehicle) or hasTowBarState(towedVehicle)
if shouldDrop and not dropTowBarOnGround(towingVehicle, towedVehicle) then
-- Keep the reciprocal IDs intact so the audit can retry when a square is available.
return false
end
cancelPendingAttach(towingVehicle, towedVehicle)
forgetTowBarPair(towingVehicle, towedVehicle)
breakTowBarConstraint(towingVehicle, towedVehicle)
clearExpectedTowBarPair(towingVehicle, towedVehicle)
if isServer() then
broadcastSpontaneousDetach(towingVehicle, towedVehicle)
elseif not isClient() and TowBarMod and TowBarMod.Hook
and TowBarMod.Hook.cleanupDetachedTowBar then
local ok, err = pcall(TowBarMod.Hook.cleanupDetachedTowBar, towingVehicle, towedVehicle)
if not ok then
noise("single-player broken towbar cleanup failed: " .. tostring(err))
return false
end
end
noise("finalized broken towbar pair " .. tostring(towingVehicle:getId())
.. ":" .. tostring(towedVehicle:getId()) .. " reason=" .. tostring(reason)
.. " dropped=" .. tostring(shouldDrop))
return true
end
local function reconcileBrokenTowBarPairsServer()
brokenPairAuditTickCounter = brokenPairAuditTickCounter + 1
if brokenPairAuditTickCounter < BrokenPairAuditIntervalTicks then return end
brokenPairAuditTickCounter = 0
local cell = getCell()
if not cell then return end
local vehicles = cell:getVehicles()
if not vehicles then return end
forEachCollectionItem(vehicles, function(towingVehicle)
local towingModData = towingVehicle and towingVehicle:getModData() or nil
local towedVehicleId = towingModData and towingModData["towBarTowedVehicleId"] or nil
local towedVehicle = towedVehicleId and getVehicleById(towedVehicleId) or nil
if towingVehicle and towedVehicle and isExpectedTowBarPair(towingVehicle, towedVehicle) then
if isLinked(towingVehicle, towedVehicle) then
markTowBarPairConfirmed(towingVehicle, towedVehicle)
elseif isTowBarPairConfirmed(towingVehicle, towedVehicle)
and not hasAnyTowLink(towingVehicle)
and not hasAnyTowLink(towedVehicle) then
finalizeBrokenTowBarPair(towingVehicle, towedVehicle, "physical-link-audit")
end
end
end)
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 "failed"
end
local attachmentA = resolveAttachmentA(args, vehicleA)
local attachmentB = resolveAttachmentB(args, vehicleB)
if not isLinked(vehicleA, vehicleB) then
if isTowBarPairConfirmed(vehicleA, vehicleB) then return "broken" end
if item.attempts >= 3 then return "failed" end
vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB)
return "retry"
end
markExpectedTowBarPair(vehicleA, vehicleB, attachmentA, attachmentB)
markTowBarPairConfirmed(vehicleA, vehicleB)
broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
return "complete"
end
local function failAttachSync(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 isLinked(vehicleA, vehicleB) then
vehicleA:breakConstraint(true, false)
end
clearExpectedTowBarPair(vehicleA, vehicleB)
broadcastDetach(args.vehicleA, args.vehicleB)
if item.reservedTowBar then
giveTowBar(item.player, true)
item.reservedTowBar = false
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 or args.vehicle
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
forEachCollectionItem(list, function(towingVehicle)
local towedVehicle = towingVehicle and towingVehicle:getVehicleTowing() or nil
if towingVehicle and towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle then
-- Preserve reciprocal modern pairs and clean legacy pairs only.
-- Stale modern IDs must not promote an unrelated current trailer.
if isExpectedTowBarPair(towingVehicle, towedVehicle)
or isLegacyTowBarPair(towingVehicle, 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
markExpectedTowBarPair(towingVehicle, towedVehicle, attachmentA, attachmentB)
markTowBarPairConfirmed(towingVehicle, towedVehicle)
broadcastAttach(towingVehicle, towedVehicle, attachmentA, attachmentB)
end
end
end)
end
local function processPendingSync()
reconcileBrokenTowBarPairsServer()
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
local status = processAttachSync(item)
if status == "retry" and item.attempts < 3 then
item.attempts = item.attempts + 1
item.ticks = SyncDelayTicks
table.insert(remaining, item)
elseif status == "broken" then
finalizeBrokenTowBarPair(
item.args.vehicleA and getVehicleById(item.args.vehicleA) or nil,
item.args.vehicleB and getVehicleById(item.args.vehicleB) or nil,
"attach-confirmation-break"
)
elseif status ~= "complete" then
failAttachSync(item)
end
elseif item.kind == "detach" then
processDetachSync(item)
end
else
table.insert(remaining, item)
end
end
pendingSync = remaining
end
function Commands.attachTowBar(player, args)
if not player or type(args) ~= "table" then return end
if type(args.attachmentA) ~= "string" or type(args.attachmentB) ~= "string" then return end
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
if vehicleA == vehicleB or not isPlayerAuthorizedForPair(player, vehicleA, vehicleB) then
noise("rejected invalid or remote towbar attach")
return
end
if hasAnyTowLink(vehicleA) or hasAnyTowLink(vehicleB) then
noise("rejected conflicting towbar attach")
return
end
if isExpectedTowBarPair(vehicleA, vehicleB)
or isExpectedTowBarPair(vehicleB, vehicleA)
or hasPendingAttach(vehicleA, vehicleB)
or hasPendingAttach(vehicleB, vehicleA) then
noise("rejected duplicate pending towbar attach")
return
end
local towBarItem = findTowBarItem(player, args.itemId)
if not towBarItem then
noise("rejected towbar attach without item")
return
end
if not removeTowBarItem(player, towBarItem) then
noise("rejected towbar attach because item reservation failed")
return
end
-- Constraint creation may complete on a later server tick in multiplayer.
-- Record and queue the reserved item first so an immediate Build 42.20
-- spontaneous break can resolve the pair and consume the reservation once.
markExpectedTowBarPair(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
queueSync("attach", player, args, true)
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
if isLinked(vehicleA, vehicleB) then
markTowBarPairConfirmed(vehicleA, vehicleB)
end
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 not isPlayerAuthorizedForPair(player, towingVehicle, towedVehicle) then
noise("rejected unauthorized towbar detach")
return
end
if not isLinked(towingVehicle, towedVehicle) then
noise("rejected mismatched or stale towbar detach")
return
end
if not isExpectedTowBarPair(towingVehicle, towedVehicle)
and not isLegacyTowBarPair(towingVehicle, towedVehicle) then
noise("rejected detach for a non-towbar or stale linked pair")
return
end
local shouldRefund = hasTowBarState(towingVehicle) or hasTowBarState(towedVehicle)
breakTowBarConstraint(towingVehicle, towedVehicle)
forgetTowBarPair(towingVehicle, towedVehicle)
clearExpectedTowBarPair(towingVehicle, towedVehicle)
queueSync("detach", player, args)
if shouldRefund then
giveTowBar(player, true)
end
end
function Commands.consumeTowBar(player, args)
local itemId = args and args.itemId
removeTowBarItem(player, findTowBarItem(player, itemId))
end
giveTowBar = function(player, equipPrimary)
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 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)
if module == "vehicle" and command == "detachTrailerSpontaneous" then
args = args or {}
local vehicle = args.vehicle and getVehicleById(args.vehicle) or nil
local towingVehicle, towedVehicle = resolveExpectedTowBarPair(vehicle)
if towingVehicle and towedVehicle then
finalizeBrokenTowBarPair(towingVehicle, towedVehicle, "detachTrailerSpontaneous")
end
return
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)