Bump mod version to 1.0.5
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user