This commit is contained in:
2026-02-12 17:55:25 -05:00
parent 0127c1d0c7
commit 9e98c54057
7 changed files with 512 additions and 42 deletions

View File

@@ -45,14 +45,51 @@ local function resolveVehicle(id)
return getVehicleById(id)
end
local function reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
if not vehicleA or not vehicleB then return end
if TowBarMod.Utils and TowBarMod.Utils.updateAttachmentsForRigidTow then
TowBarMod.Utils.updateAttachmentsForRigidTow(vehicleA, vehicleB, attachmentA, attachmentB)
end
local towingMd = vehicleA:getModData()
local towedMd = vehicleB:getModData()
local currentScript = vehicleB:getScriptName()
if towingMd then
towingMd["isTowingByTowBar"] = true
vehicleA:transmitModData()
end
if towedMd then
if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then
towedMd.towBarOriginalScriptName = currentScript
end
if towedMd.towBarOriginalMass == nil then
towedMd.towBarOriginalMass = vehicleB:getMass()
end
if towedMd.towBarOriginalBrakingForce == nil then
towedMd.towBarOriginalBrakingForce = vehicleB:getBrakingForce()
end
towedMd["isTowingByTowBar"] = true
towedMd["towed"] = true
vehicleB:transmitModData()
end
vehicleB:setScriptName("notTowingA_Trailer")
if TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
pcall(TowBarMod.Hook.setVehiclePostAttach, nil, vehicleB)
end
log("reconcilePairState A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())
.. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB))
end
local function applyAttachSync(args)
if not args then return end
local vehicleA = resolveVehicle(args.vehicleA)
local vehicleB = resolveVehicle(args.vehicleB)
if not vehicleA or not vehicleB then return end
if vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA then
if not vehicleA or not vehicleB then
log("applyAttachSync skipped: missing vehicle A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB))
return
end
@@ -63,7 +100,15 @@ local function applyAttachSync(args)
return
end
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB)
if vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA then
log("applyAttachSync already linked A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()))
else
log("applyAttachSync addPointConstraint A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())
.. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB))
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB)
end
reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
end
local function safeBreak(vehicle)

View File

@@ -20,6 +20,10 @@ local function log(msg)
end
end
local function vehicleId(vehicle)
return vehicle and vehicle:getId() or "nil"
end
local function hasAttachment(vehicle, attachmentId)
if vehicle == nil or attachmentId == nil then return false end
local script = vehicle:getScript()
@@ -180,6 +184,18 @@ local function restoreDefaultsIfLeadLost(vehicle)
clearChangedOffset(vehicle)
restoreTowBarDefaults(vehicle)
local md = vehicle:getModData()
if md then
md["towed"] = false
if vehicle:getVehicleTowing() == nil then
md["isTowingByTowBar"] = false
md.towBarOriginalScriptName = nil
md.towBarOriginalMass = nil
md.towBarOriginalBrakingForce = nil
end
vehicle:transmitModData()
end
setTowBarModelVisible(vehicle, false)
end
@@ -208,12 +224,104 @@ local function sendAttach(playerObj, towingVehicle, towedVehicle, attachmentA, a
end
end
local function captureFrontLink(middleVehicle)
local function isLinkedPair(towingVehicle, towedVehicle)
if not towingVehicle or not towedVehicle then return false end
return towingVehicle:getVehicleTowing() == towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle
end
local function markTowBarPair(towingVehicle, towedVehicle)
local towingMd = towingVehicle and towingVehicle:getModData() or nil
local towedMd = towedVehicle and towedVehicle:getModData() or nil
if towingMd then
towingMd["isTowingByTowBar"] = true
towingVehicle:transmitModData()
end
if towedMd then
local currentScript = towedVehicle and towedVehicle:getScriptName() or nil
if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then
towedMd.towBarOriginalScriptName = currentScript
end
if towedMd.towBarOriginalMass == nil and towedVehicle then
towedMd.towBarOriginalMass = towedVehicle:getMass()
end
if towedMd.towBarOriginalBrakingForce == nil and towedVehicle then
towedMd.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
end
towedMd["isTowingByTowBar"] = true
towedMd["towed"] = true
towedVehicle:transmitModData()
end
end
local function ensurePairAttached(playerObj, towingVehicle, towedVehicle, preferredA, preferredB, alwaysSend)
if not playerObj or not towingVehicle or not towedVehicle then
log("ensurePairAttached skipped: missing refs A=" .. tostring(vehicleId(towingVehicle)) .. " B=" .. tostring(vehicleId(towedVehicle)))
return false
end
if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then
log("ensurePairAttached skipped: identical vehicles id=" .. tostring(vehicleId(towingVehicle)))
return false
end
if wouldCreateTowLoop(towingVehicle, towedVehicle) then
log("ensurePairAttached skipped: loop A=" .. tostring(vehicleId(towingVehicle)) .. " B=" .. tostring(vehicleId(towedVehicle)))
return false
end
local attachmentA, attachmentB = resolvePair(towingVehicle, towedVehicle, preferredA, preferredB)
if not hasAttachment(towingVehicle, attachmentA) or not hasAttachment(towedVehicle, attachmentB) then
log("ensurePairAttached skipped: missing attachment A=" .. tostring(attachmentA) .. " B=" .. tostring(attachmentB)
.. " ids=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle)))
return false
end
applyRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
towedVehicle:setScriptName("notTowingA_Trailer")
markTowBarPair(towingVehicle, towedVehicle)
local linked = isLinkedPair(towingVehicle, towedVehicle)
log("ensurePairAttached pair=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle))
.. " linked=" .. tostring(linked) .. " alwaysSend=" .. tostring(alwaysSend)
.. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB))
if linked and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
pcall(TowBarMod.Hook.setVehiclePostAttach, nil, towedVehicle)
end
if alwaysSend or not linked then
sendAttach(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
elseif TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
queueAction(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)
end
setTowBarModelVisible(towedVehicle, true)
refreshAround(towingVehicle)
refreshAround(towedVehicle)
return true
end
local function captureFrontLinks(middleVehicle)
if not middleVehicle then return nil end
local frontVehicle = middleVehicle:getVehicleTowedBy()
if not frontVehicle or frontVehicle == middleVehicle then return nil end
local a, b = resolvePair(frontVehicle, middleVehicle)
return { front = frontVehicle, middle = middleVehicle, a = a, b = b }
local links = {}
local cursor = middleVehicle
local visited = {}
while cursor do
if visited[cursor] then break end
visited[cursor] = true
local frontVehicle = cursor:getVehicleTowedBy()
if not frontVehicle then break end
if frontVehicle == cursor or visited[frontVehicle] then break end
local a, b = resolvePair(frontVehicle, cursor)
table.insert(links, { front = frontVehicle, middle = cursor, a = a, b = b })
cursor = frontVehicle
end
if #links == 0 then return nil end
return links
end
local function restoreFrontLink(playerObj, link)
@@ -225,23 +333,21 @@ local function restoreFrontLink(playerObj, link)
if front == middle or front:getId() == middle:getId() then return end
if wouldCreateTowLoop(front, middle) then return end
local a, b = resolvePair(front, middle, link.a, link.b)
if not hasAttachment(front, a) or not hasAttachment(middle, b) then return end
applyRigidTow(front, middle, a, b)
middle:setScriptName("notTowingA_Trailer")
sendAttach(playerObj, front, middle, a, b)
setTowBarModelVisible(middle, true)
refreshAround(front)
refreshAround(middle)
ensurePairAttached(playerObj, front, middle, link.a, link.b, false)
end
local function queueRestoreFront(playerObj, link, delay)
if not playerObj or not link then return end
queueAction(playerObj, delay or 12, function(character, linkArg)
restoreFrontLink(character, linkArg)
end, link)
local function restoreFrontLinks(playerObj, links)
if not links then return end
for _, link in ipairs(links) do
restoreFrontLink(playerObj, link)
end
end
local function queueRestoreFrontLinks(playerObj, links, delay)
if not playerObj or not links then return end
queueAction(playerObj, delay or 12, function(character, linksArg)
restoreFrontLinks(character, linksArg)
end, links)
end
local function resolveDetachPair(towingVehicle, towedVehicle)
@@ -411,16 +517,31 @@ LT.performAttachWrapper = function(playerObj, towingVehicle, towedVehicle, attac
if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then return end
if wouldCreateTowLoop(towingVehicle, towedVehicle) then return end
local frontLink = captureFrontLink(towingVehicle)
local frontLinks = captureFrontLinks(towingVehicle)
log("performAttachWrapper begin pair=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle))
.. " frontLinks=" .. tostring(frontLinks and #frontLinks or 0))
original(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
restoreFrontLink(playerObj, frontLink)
queueRestoreFront(playerObj, frontLink, 12)
queueRestoreFront(playerObj, frontLink, 30)
restoreFrontLinks(playerObj, frontLinks)
queueRestoreFrontLinks(playerObj, frontLinks, 12)
queueRestoreFrontLinks(playerObj, frontLinks, 30)
setTowBarModelVisible(towedVehicle, true)
refreshAround(towingVehicle)
refreshAround(towedVehicle)
ensurePairAttached(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB, false)
local attachState = {
towing = towingVehicle,
towed = towedVehicle,
attachmentA = attachmentA,
attachmentB = attachmentB
}
queueAction(playerObj, 14, function(character, state)
if not state then return end
ensurePairAttached(character, state.towing, state.towed, state.attachmentA, state.attachmentB, true)
end, attachState)
queueAction(playerObj, 34, function(character, state)
if not state then return end
ensurePairAttached(character, state.towing, state.towed, state.attachmentA, state.attachmentB, true)
end, attachState)
end
LT.performDetachWrapper = function(playerObj, towingVehicle, towedVehicle)
@@ -430,12 +551,12 @@ LT.performDetachWrapper = function(playerObj, towingVehicle, towedVehicle)
local resolvedTowing, resolvedTowed = resolveDetachPair(towingVehicle, towedVehicle)
if resolvedTowing == nil or resolvedTowed == nil then return end
local frontLink = captureFrontLink(resolvedTowing)
local frontLinks = captureFrontLinks(resolvedTowing)
original(playerObj, resolvedTowing, resolvedTowed)
restoreFrontLink(playerObj, frontLink)
queueRestoreFront(playerObj, frontLink, 12)
queueRestoreFront(playerObj, frontLink, 30)
restoreFrontLinks(playerObj, frontLinks)
queueRestoreFrontLinks(playerObj, frontLinks, 12)
queueRestoreFrontLinks(playerObj, frontLinks, 30)
restoreDefaultsIfLeadLost(resolvedTowing)
restoreDefaultsIfLeadLost(resolvedTowed)

View File

@@ -70,8 +70,12 @@ local function processAttach(item)
log("attach sync failed: server link not established A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()))
return
end
else
log("attach sync already linked A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()))
end
log("attach sync broadcast A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())
.. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB))
sendServerCommand("landtrain", "forceAttachSync", {
vehicleA = vehicleA:getId(),
vehicleB = vehicleB:getId(),
@@ -94,7 +98,8 @@ end
local function processPending()
if #pending == 0 then return end
for i = #pending, 1, -1 do
local remaining = {}
for i = 1, #pending do
local item = pending[i]
item.ticks = item.ticks - 1
if item.ticks <= 0 then
@@ -103,17 +108,24 @@ local function processPending()
elseif item.kind == "detach" then
processDetach(item)
end
table.remove(pending, i)
else
table.insert(remaining, item)
end
end
pending = remaining
end
local function onClientCommand(module, command, player, args)
if module == "vehicle" and command == "attachTrailer" then
log("queue attach from " .. tostring(player and player:getUsername() or "unknown")
.. " A=" .. tostring(args and args.vehicleA) .. " B=" .. tostring(args and args.vehicleB)
.. " attachmentA=" .. tostring(args and args.attachmentA) .. " attachmentB=" .. tostring(args and args.attachmentB))
queueSync("attach", player, args)
elseif module == "vehicle" and command == "detachTrailer" then
log("queue detach(vehicle) from " .. tostring(player and player:getUsername() or "unknown"))
queueSync("detach", player, args)
elseif module == "towbar" and command == "detachTowBar" then
log("queue detach(towbar) from " .. tostring(player and player:getUsername() or "unknown"))
queueSync("detach", player, args)
end
end