Files
Towbar/42.20/media/lua/server/TowingCommands.lua
T
2026-08-19 12:12:12 -04:00

690 lines
26 KiB
Lua

if isClient() then return end
require("TowBar/Persistence")
local TowingCommands = {}
local Commands = {}
local TowBarItemType = "TowBar.TowBar"
local SnapshotIntervalTicks = 120
local BrokenPairAuditIntervalTicks = 5
local snapshotTickCounter = 0
local brokenPairAuditTickCounter = 0
local confirmedTowPairs = {}
local towPairRuntime = {}
local RestoreRetryMs = 2000
local SustainedBreakMs = 5000
local giveTowBar
TowingCommands.wantNoise = getDebug() or false
local noise = function(msg)
if TowingCommands.wantNoise then
print("TowBarCommands: " .. msg)
end
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 getPersistentVehicleId(vehicle)
if not vehicle or not vehicle.getSqlId then return nil end
local ok, sqlId = pcall(function() return vehicle:getSqlId() end)
if ok and type(sqlId) == "number" and sqlId >= 0 then return sqlId end
return nil
end
local function matchesSavedVehicle(runtimeId, sqlId, vehicle)
if not vehicle then return false end
local stableId = tonumber(sqlId)
if stableId and stableId >= 0 then
return getPersistentVehicleId(vehicle) == stableId
end
return tonumber(runtimeId) == vehicle:getId()
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
local vehicleAId = getPersistentVehicleId(vehicleA) or vehicleA:getId()
local vehicleBId = getPersistentVehicleId(vehicleB) or vehicleB:getId()
return tostring(vehicleAId) .. ":" .. tostring(vehicleBId)
end
local function markTowBarPairConfirmed(vehicleA, vehicleB)
local key = getTowBarPairKey(vehicleA, vehicleB)
if key then
confirmedTowPairs[key] = true
towPairRuntime[key] = { confirmed = 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
towPairRuntime[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["towBarTowedVehicleSqlId"] = getPersistentVehicleId(vehicleB)
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarTowingVehicleSqlId"] = nil
towingModData["towBarExpectedAttachment"] = attachmentA
towedModData["isTowingByTowBar"] = true
towedModData["towed"] = true
towedModData["towBarTowedVehicleId"] = nil
towedModData["towBarTowedVehicleSqlId"] = nil
towedModData["towBarTowingVehicleId"] = vehicleA:getId()
towedModData["towBarTowingVehicleSqlId"] = getPersistentVehicleId(vehicleA)
towedModData["towBarExpectedAttachment"] = attachmentB
vehicleA:transmitModData()
vehicleB:transmitModData()
TowBarMod.Persistence.savePair("towbar", vehicleA, vehicleB, attachmentA, attachmentB)
end
local function clearExpectedTowBarPair(vehicleA, vehicleB)
if vehicleA and vehicleB then
TowBarMod.Persistence.removePair("towbar", vehicleA, vehicleB)
end
if vehicleA then
local towingModData = vehicleA:getModData()
if towingModData then
towingModData["isTowingByTowBar"] = false
towingModData["towed"] = false
towingModData["towBarTowedVehicleId"] = nil
towingModData["towBarTowedVehicleSqlId"] = nil
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarTowingVehicleSqlId"] = 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["towBarTowedVehicleSqlId"] = nil
towedModData["towBarTowingVehicleId"] = nil
towedModData["towBarTowingVehicleSqlId"] = 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 matchesSavedVehicle(
towingModData["towBarTowedVehicleId"], towingModData["towBarTowedVehicleSqlId"], vehicleB
) and matchesSavedVehicle(
towedModData["towBarTowingVehicleId"], towedModData["towBarTowingVehicleSqlId"], vehicleA
)
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 findLoadedVehicle(runtimeId, sqlId)
local stableId = tonumber(sqlId)
if stableId and stableId >= 0 then
local cell = getCell()
local vehicles = cell and cell:getVehicles() or nil
local match
forEachCollectionItem(vehicles, function(vehicle)
if not match and getPersistentVehicleId(vehicle) == stableId then match = vehicle end
end)
return match
end
return runtimeId and getVehicleById(tonumber(runtimeId)) or nil
end
local function broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
if not vehicleA or not vehicleB then return end
local args = {
vehicleA = vehicleA:getId(),
vehicleB = vehicleB:getId(),
attachmentA = attachmentA,
attachmentB = attachmentB
}
if isServer() then
sendServerCommand("towbar", "forceAttachSync", args)
elseif not isClient() and TowBarMod and TowBarMod.Sync
and TowBarMod.Sync.applyAttachSync then
TowBarMod.Sync.applyAttachSync(args)
end
end
local function broadcastDetach(vehicleAId, vehicleBId)
local args = {
vehicleA = vehicleAId,
vehicleB = vehicleBId
}
if isServer() then
sendServerCommand("towbar", "forceDetachSync", args)
elseif not isClient() and TowBarMod and TowBarMod.Sync
and TowBarMod.Sync.applyDetachSync then
TowBarMod.Sync.applyDetachSync(args)
end
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 restorePersistedTowBarPair(towingVehicle, towedVehicle)
if not isExpectedTowBarPair(towingVehicle, towedVehicle)
or hasAnyTowLink(towingVehicle) or hasAnyTowLink(towedVehicle) then
return false
end
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
local attachmentA = towingModData["towBarExpectedAttachment"] or "trailer"
local attachmentB = towedModData["towBarExpectedAttachment"] or "trailerfront"
if not towingVehicle:attachmentExist(attachmentA)
or not towedVehicle:attachmentExist(attachmentB) then
return false
end
markExpectedTowBarPair(towingVehicle, towedVehicle, attachmentA, attachmentB)
if isServer() then
towingVehicle:addPointConstraint(nil, towedVehicle, attachmentA, attachmentB)
end
broadcastAttach(towingVehicle, towedVehicle, attachmentA, attachmentB)
if isLinked(towingVehicle, towedVehicle) then
markTowBarPairConfirmed(towingVehicle, towedVehicle)
return true
end
return false
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
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
local loadedBySqlId = {}
forEachCollectionItem(vehicles, function(vehicle)
local sqlId = getPersistentVehicleId(vehicle)
if sqlId then loadedBySqlId[sqlId] = vehicle end
end)
local processed = {}
local function reconcilePair(towingVehicle, towedVehicle, attachmentA, attachmentB)
if not towingVehicle or not towedVehicle then return end
local key = getTowBarPairKey(towingVehicle, towedVehicle)
if not key or processed[key] then return end
processed[key] = true
if not isExpectedTowBarPair(towingVehicle, towedVehicle) then
markExpectedTowBarPair(
towingVehicle, towedVehicle,
attachmentA or "trailer", attachmentB or "trailerfront"
)
end
if not isExpectedTowBarPair(towingVehicle, towedVehicle) then return end
local now = getTimestampMs()
local state = towPairRuntime[key] or {}
local linked = isLinked(towingVehicle, towedVehicle)
local occupied = hasAnyTowLink(towingVehicle) or hasAnyTowLink(towedVehicle)
local action
state, action = TowBarMod.Persistence.advanceRecoveryState(
state, now, linked, occupied, RestoreRetryMs, SustainedBreakMs
)
towPairRuntime[key] = state
if action == "adopt" then
local towingMd = towingVehicle:getModData()
local towedMd = towedVehicle:getModData()
local savedAttachmentA = towingMd["towBarExpectedAttachment"] or attachmentA or "trailer"
local savedAttachmentB = towedMd["towBarExpectedAttachment"] or attachmentB or "trailerfront"
if tonumber(towingMd["towBarTowedVehicleId"]) ~= towedVehicle:getId()
or tonumber(towedMd["towBarTowingVehicleId"]) ~= towingVehicle:getId() then
markExpectedTowBarPair(
towingVehicle, towedVehicle,
savedAttachmentA, savedAttachmentB
)
end
TowBarMod.Persistence.savePair(
"towbar", towingVehicle, towedVehicle, savedAttachmentA, savedAttachmentB
)
confirmedTowPairs[key] = true
elseif action == "break" then
finalizeBrokenTowBarPair(towingVehicle, towedVehicle, "sustained-physical-link-audit")
elseif action == "restore" then
restorePersistedTowBarPair(towingVehicle, towedVehicle)
end
end
TowBarMod.Persistence.forEachPair("towbar", function(record)
local towingVehicle = loadedBySqlId[tonumber(record.towingSqlId)]
local towedVehicle = loadedBySqlId[tonumber(record.towedSqlId)]
local key = tostring(record.towingSqlId) .. ":" .. tostring(record.towedSqlId)
towPairRuntime[key] = TowBarMod.Persistence.notePeerAvailability(
towPairRuntime[key], towingVehicle ~= nil and towedVehicle ~= nil
)
reconcilePair(
towingVehicle,
towedVehicle,
record.attachmentA,
record.attachmentB
)
end)
forEachCollectionItem(vehicles, function(towingVehicle)
local towingModData = towingVehicle and towingVehicle:getModData() or nil
local towedVehicleId = towingModData and towingModData["towBarTowedVehicleId"] or nil
local towedVehicleSqlId = towingModData and towingModData["towBarTowedVehicleSqlId"] or nil
local towedVehicle = findLoadedVehicle(towedVehicleId, towedVehicleSqlId)
if towingVehicle and towedVehicle then reconcilePair(towingVehicle, towedVehicle) end
end)
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 processTowBarServerTick()
reconcileBrokenTowBarPairsServer()
snapshotTickCounter = snapshotTickCounter + 1
if snapshotTickCounter >= SnapshotIntervalTicks then
snapshotTickCounter = 0
snapshotActiveTowbarLinksServer()
end
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) 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
-- Match the working wrecker path: persist the accepted logical pair, ask
-- the server for its native relation, then immediately tell clients to
-- replace that relation with the local rigid fake-trailer constraint.
markExpectedTowBarPair(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
if isServer() then
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
end
broadcastAttach(vehicleA, 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 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)
broadcastDetach(towingVehicle:getId(), towedVehicle:getId())
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(processTowBarServerTick)