Saving and Loading Fixed

This commit is contained in:
2026-02-06 22:21:31 -05:00
parent bd5a9ca990
commit 8e2c12500a

View File

@@ -6,17 +6,28 @@ local LANDTRAIN_DEBUG = true
local LANDTRAIN_FALLBACK_MAX_DIST_SQ = 3.25 -- ~1.8 tiles
local LANDTRAIN_FALLBACK_MAX_DZ = 0.9
local LANDTRAIN_FRONT_SQL_ID_KEY = "landtrainTowbarFrontSqlId"
local LANDTRAIN_FRONT_ID_KEY_LEGACY = "landtrainTowbarFrontVehicleId"
local LANDTRAIN_FRONT_ATTACHMENT_A_KEY = "landtrainTowbarAttachmentA"
local LANDTRAIN_FRONT_ATTACHMENT_B_KEY = "landtrainTowbarAttachmentB"
local LANDTRAIN_REAR_SQL_ID_KEY = "landtrainTowbarRearSqlId"
local LANDTRAIN_SAVED_POS_X_KEY = "landtrainSavedPosX"
local LANDTRAIN_SAVED_POS_Y_KEY = "landtrainSavedPosY"
local LANDTRAIN_SAVED_POS_Z_KEY = "landtrainSavedPosZ"
local LANDTRAIN_SAVED_DIR_KEY = "landtrainSavedDir"
local function emitLandtrainLog(line)
print(line)
if type(writeLog) == "function" then
pcall(writeLog, "Landtrain", line)
end
end
local function ltLog(msg)
if not LANDTRAIN_DEBUG then return end
print("[Landtrain] " .. tostring(msg))
emitLandtrainLog("[Landtrain] " .. tostring(msg))
end
local function ltInfo(msg)
print("[Landtrain][Info] " .. tostring(msg))
emitLandtrainLog("[Landtrain][Info] " .. tostring(msg))
end
ltInfo("UnlimitedTowbarChains loaded. debug=" .. tostring(LANDTRAIN_DEBUG))
@@ -27,6 +38,115 @@ local function vehLabel(vehicle)
return tostring(vehicle:getId()) .. ":" .. tostring(name)
end
local _landtrainPairPickPosA = Vector3f.new()
local _landtrainPairPickPosB = Vector3f.new()
local function hasAttachmentById(vehicle, attachmentId)
if vehicle == nil or attachmentId == nil then return false end
local script = vehicle:getScript()
if script == nil then return false end
return script:getAttachmentById(attachmentId) ~= nil
end
local function getTowbarStyleAttachmentPair(towingVehicle, towedVehicle)
local attachmentA = nil
local attachmentB = nil
if hasAttachmentById(towingVehicle, "trailer") then
attachmentA = "trailer"
elseif towingVehicle ~= nil then
attachmentA = towingVehicle:getTowAttachmentSelf()
end
if hasAttachmentById(towedVehicle, "trailerfront") then
attachmentB = "trailerfront"
elseif towedVehicle ~= nil then
attachmentB = towedVehicle:getTowAttachmentSelf()
end
if attachmentA == attachmentB then
if attachmentA == "trailer" and hasAttachmentById(towedVehicle, "trailerfront") then
attachmentB = "trailerfront"
elseif attachmentA == "trailerfront" and hasAttachmentById(towingVehicle, "trailer") then
attachmentA = "trailer"
end
end
if attachmentA == nil then attachmentA = "trailer" end
if attachmentB == nil then attachmentB = "trailerfront" end
return attachmentA, attachmentB
end
local function chooseLandtrainAttachmentPair(towingVehicle, towedVehicle, preferredA, preferredB)
local seen = {}
local candidates = {}
local function addCandidate(a, b)
if a == nil or b == nil or a == b then return end
local key = tostring(a) .. "|" .. tostring(b)
if seen[key] then return end
seen[key] = true
table.insert(candidates, { a = a, b = b })
end
addCandidate(preferredA, preferredB)
addCandidate(towingVehicle and towingVehicle:getTowAttachmentSelf() or nil, towedVehicle and towedVehicle:getTowAttachmentSelf() or nil)
addCandidate("trailer", "trailerfront")
addCandidate("trailerfront", "trailer")
local bestPair = nil
local bestDistSq = nil
local bestDz = nil
for _, pair in ipairs(candidates) do
if hasAttachmentById(towingVehicle, pair.a) and hasAttachmentById(towedVehicle, pair.b) then
local posA = towingVehicle:getAttachmentWorldPos(pair.a, _landtrainPairPickPosA)
local posB = towedVehicle:getAttachmentWorldPos(pair.b, _landtrainPairPickPosB)
if posA ~= nil and posB ~= nil then
local dx = posA:x() - posB:x()
local dy = posA:y() - posB:y()
local dz = math.abs(posA:z() - posB:z())
local distSq = dx * dx + dy * dy + (posA:z() - posB:z()) * (posA:z() - posB:z())
if bestPair == nil or distSq < bestDistSq or (distSq == bestDistSq and dz < bestDz) then
bestPair = pair
bestDistSq = distSq
bestDz = dz
end
elseif bestPair == nil then
bestPair = pair
end
end
end
if bestPair ~= nil then
return bestPair.a, bestPair.b
end
local fallbackA = preferredA or (towingVehicle and towingVehicle:getTowAttachmentSelf()) or "trailer"
local fallbackB = preferredB or (towedVehicle and towedVehicle:getTowAttachmentSelf()) or "trailerfront"
if fallbackA == fallbackB then
if fallbackA == "trailerfront" then
fallbackA = "trailer"
else
fallbackB = "trailerfront"
end
end
return fallbackA, fallbackB
end
local function chooseLandtrainTowbarPair(towingVehicle, towedVehicle, preferredA, preferredB)
local towbarA, towbarB = getTowbarStyleAttachmentPair(towingVehicle, towedVehicle)
if towbarA ~= towbarB and hasAttachmentById(towingVehicle, towbarA) and hasAttachmentById(towedVehicle, towbarB) then
return towbarA, towbarB
end
if preferredA ~= nil and preferredB ~= nil and preferredA ~= preferredB
and hasAttachmentById(towingVehicle, preferredA)
and hasAttachmentById(towedVehicle, preferredB) then
return preferredA, preferredB
end
return chooseLandtrainAttachmentPair(towingVehicle, towedVehicle, preferredA, preferredB)
end
local function dumpTowState(prefix, vehicle)
if not LANDTRAIN_DEBUG then return end
if vehicle == nil then
@@ -131,7 +251,7 @@ local function getVehicleBySqlIdSafe(sqlId)
for i = 0, vehicles:size() - 1 do
local vehicle = vehicles:get(i)
if vehicle ~= nil and vehicle:getSqlId and vehicle:getSqlId() == targetSqlId then
if vehicle ~= nil and vehicle.getSqlId and vehicle:getSqlId() == targetSqlId then
return vehicle
end
end
@@ -140,11 +260,13 @@ end
local function storeLandtrainFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB)
if towingVehicle == nil or towedVehicle == nil then return end
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
if towedModData == nil then return end
if towingModData == nil or towedModData == nil then return end
if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then
towingModData[LANDTRAIN_REAR_SQL_ID_KEY] = nil
towingVehicle:transmitModData()
towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
towedModData[LANDTRAIN_FRONT_ID_KEY_LEGACY] = nil
towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil
towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil
towedVehicle:transmitModData()
@@ -152,15 +274,22 @@ local function storeLandtrainFrontLinkData(towingVehicle, towedVehicle, attachme
return
end
local towingSqlId = towingVehicle:getSqlId and towingVehicle:getSqlId() or -1
local towingSqlId = towingVehicle.getSqlId and towingVehicle:getSqlId() or -1
if towingSqlId ~= nil and towingSqlId >= 0 then
towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = towingSqlId
else
towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
end
towedModData[LANDTRAIN_FRONT_ID_KEY_LEGACY] = nil
towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = attachmentA or towingVehicle:getTowAttachmentSelf() or "trailer"
towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = attachmentB or towedVehicle:getTowAttachmentSelf() or "trailerfront"
local towedSqlId = towedVehicle.getSqlId and towedVehicle:getSqlId() or -1
if towedSqlId ~= nil and towedSqlId >= 0 then
towingModData[LANDTRAIN_REAR_SQL_ID_KEY] = towedSqlId
else
towingModData[LANDTRAIN_REAR_SQL_ID_KEY] = nil
end
towingVehicle:transmitModData()
local resolvedA, resolvedB = chooseLandtrainTowbarPair(towingVehicle, towedVehicle, attachmentA, attachmentB)
towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = resolvedA
towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = resolvedB
towedVehicle:transmitModData()
end
@@ -168,8 +297,19 @@ local function clearLandtrainFrontLinkData(vehicle)
if vehicle == nil then return end
local modData = vehicle:getModData()
if modData == nil then return end
local frontSqlId = tonumber(modData[LANDTRAIN_FRONT_SQL_ID_KEY])
local vehicleSqlId = vehicle.getSqlId and vehicle:getSqlId() or nil
if frontSqlId ~= nil and vehicleSqlId ~= nil and vehicleSqlId >= 0 then
local frontVehicle = getVehicleBySqlIdSafe(frontSqlId)
if frontVehicle ~= nil then
local frontModData = frontVehicle:getModData()
if frontModData ~= nil and tonumber(frontModData[LANDTRAIN_REAR_SQL_ID_KEY]) == vehicleSqlId then
frontModData[LANDTRAIN_REAR_SQL_ID_KEY] = nil
frontVehicle:transmitModData()
end
end
end
modData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
modData[LANDTRAIN_FRONT_ID_KEY_LEGACY] = nil
modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil
modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil
vehicle:transmitModData()
@@ -187,6 +327,7 @@ local function isTowbarManagedVehicle(vehicle)
or modData["isChangedTowedAttachment"] == true
or modData["towBarChangedAttachmentId"] ~= nil
or modData[LANDTRAIN_FRONT_SQL_ID_KEY] ~= nil
or modData[LANDTRAIN_REAR_SQL_ID_KEY] ~= nil
end
local function restoreVehicleTowbarDefaults(vehicle)
@@ -244,9 +385,9 @@ local function restoreVehicleTowbarDefaults(vehicle)
modData.towBarOriginalMass = nil
modData.towBarOriginalBrakingForce = nil
modData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
modData[LANDTRAIN_FRONT_ID_KEY_LEGACY] = nil
modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil
modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil
modData[LANDTRAIN_REAR_SQL_ID_KEY] = nil
if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.clearReapplied then
TowBarMod.Hook.clearReapplied(vehicle)
@@ -277,7 +418,7 @@ local function reconcileTowbarSplitVehicle(vehicle)
end
modData["towed"] = (frontVehicle ~= nil)
modData["isTowingByTowBar"] = (frontVehicle ~= nil) or (hasTowbarRear == true)
modData["isTowingByTowBar"] = (modData["towed"] == true) or (hasTowbarRear == true)
vehicle:transmitModData()
end
@@ -344,7 +485,7 @@ end
local function sanitizeLoadedTowLinks()
local vehicles = getLoadedVehicles()
if #vehicles == 0 then return end
if #vehicles == 0 then return vehicles end
for _, vehicle in ipairs(vehicles) do
local front = vehicle:getVehicleTowedBy()
@@ -357,252 +498,382 @@ local function sanitizeLoadedTowLinks()
end
end
for _, vehicle in ipairs(vehicles) do
reconcileTowbarSplitAround(vehicle)
refreshTowBarState(vehicle)
local frontVehicle = vehicle:getVehicleTowedBy()
if frontVehicle ~= nil and isTowbarManagedVehicle(vehicle) then
storeLandtrainFrontLinkData(frontVehicle, vehicle)
end
end
return vehicles
end
local _landtrainLoadSanityTicks = 0
local function onLandtrainLoadSanityTick()
if _landtrainLoadSanityTicks <= 0 then
Events.OnTick.Remove(onLandtrainLoadSanityTick)
return
local function setModDataNumberIfChanged(modData, key, value)
if modData == nil or key == nil or value == nil then return false end
local current = tonumber(modData[key])
if current ~= nil and math.abs(current - value) < 0.0001 then
return false
end
modData[key] = value
return true
end
_landtrainLoadSanityTicks = _landtrainLoadSanityTicks - 1
if (_landtrainLoadSanityTicks % 30) ~= 0 then
return
end
sanitizeLoadedTowLinks()
end
local function scheduleLandtrainLoadSanity()
_landtrainLoadSanityTicks = 300 -- run for ~5 seconds
Events.OnTick.Remove(onLandtrainLoadSanityTick)
Events.OnTick.Add(onLandtrainLoadSanityTick)
sanitizeLoadedTowLinks()
end
local landtrainPendingRestoreVehicleIds = {}
local landtrainReapplyTickCounter = 0
local function hasLandtrainFrontLinkData(vehicle)
local function saveLandtrainVehiclePosition(vehicle)
if vehicle == nil then return false end
local modData = vehicle:getModData()
if modData == nil then return false end
return modData[LANDTRAIN_FRONT_SQL_ID_KEY] ~= nil
local changed = false
changed = setModDataNumberIfChanged(modData, LANDTRAIN_SAVED_POS_X_KEY, vehicle:getX()) or changed
changed = setModDataNumberIfChanged(modData, LANDTRAIN_SAVED_POS_Y_KEY, vehicle:getY()) or changed
changed = setModDataNumberIfChanged(modData, LANDTRAIN_SAVED_POS_Z_KEY, vehicle:getZ()) or changed
if vehicle.getDirectionAngle then
local ok, angle = pcall(function()
return vehicle:getDirectionAngle()
end)
if ok and angle ~= nil then
changed = setModDataNumberIfChanged(modData, LANDTRAIN_SAVED_DIR_KEY, angle) or changed
end
end
local function queueLandtrainTowbarRestore(vehicle)
if vehicle == nil then return end
if changed then
vehicle:transmitModData()
end
return changed
end
local function isLandtrainTowbarLink(frontVehicle, rearVehicle)
if frontVehicle == nil or rearVehicle == nil then return false end
if frontVehicle == rearVehicle or frontVehicle:getId() == rearVehicle:getId() then return false end
if frontVehicle:getVehicleTowing() ~= rearVehicle or rearVehicle:getVehicleTowedBy() ~= frontVehicle then
return false
end
local frontModData = frontVehicle:getModData()
local rearModData = rearVehicle:getModData()
if frontModData == nil or rearModData == nil then return false end
return isTowbarManagedVehicle(frontVehicle)
or isTowbarManagedVehicle(rearVehicle)
or tonumber(rearModData[LANDTRAIN_FRONT_SQL_ID_KEY]) ~= nil
or tonumber(frontModData[LANDTRAIN_REAR_SQL_ID_KEY]) ~= nil
end
local function saveLandtrainTowbarLink(frontVehicle, rearVehicle, attachmentA, attachmentB)
if not isLandtrainTowbarLink(frontVehicle, rearVehicle) then return false end
storeLandtrainFrontLinkData(frontVehicle, rearVehicle, attachmentA, attachmentB)
local changed = false
changed = saveLandtrainVehiclePosition(frontVehicle) or changed
changed = saveLandtrainVehiclePosition(rearVehicle) or changed
return changed
end
local function saveActiveLandtrainTowbarSnapshot(vehicles)
local loaded = vehicles or getLoadedVehicles()
local savedLinks = 0
for _, rearVehicle in ipairs(loaded) do
local frontVehicle = rearVehicle:getVehicleTowedBy()
if isLandtrainTowbarLink(frontVehicle, rearVehicle) then
local rearModData = rearVehicle:getModData()
local attachmentA, attachmentB = chooseLandtrainTowbarPair(
frontVehicle,
rearVehicle,
rearModData and rearModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] or nil,
rearModData and rearModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] or nil
)
if saveLandtrainTowbarLink(frontVehicle, rearVehicle, attachmentA, attachmentB) then
savedLinks = savedLinks + 1
end
end
end
return savedLinks
end
local function callVehicleMethodSafe(vehicle, methodName, ...)
if vehicle == nil then return false end
local method = vehicle[methodName]
if type(method) ~= "function" then return false end
local ok = pcall(method, vehicle, ...)
return ok
end
local function restoreSavedVehiclePosition(vehicle)
if vehicle == nil then return false end
local modData = vehicle:getModData()
if modData == nil then return end
if modData == nil then return false end
-- Drop legacy runtime-id metadata; it is not stable across save/load sessions.
if modData[LANDTRAIN_FRONT_ID_KEY_LEGACY] ~= nil then
modData[LANDTRAIN_FRONT_ID_KEY_LEGACY] = nil
vehicle:transmitModData()
end
local vehicleSqlId = vehicle.getSqlId and vehicle:getSqlId() or nil
local frontVehicleSqlId = tonumber(modData[LANDTRAIN_FRONT_SQL_ID_KEY])
if frontVehicleSqlId ~= nil and vehicleSqlId ~= nil and vehicleSqlId >= 0 and frontVehicleSqlId == vehicleSqlId then
modData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil
modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil
modData["towed"] = false
vehicle:transmitModData()
ltLog("queueLandtrainTowbarRestore cleared invalid self front sqlId for " .. vehLabel(vehicle))
end
if modData["towed"] == true
or modData["isTowingByTowBar"] == true
or modData[LANDTRAIN_FRONT_SQL_ID_KEY] ~= nil
or modData.towBarOriginalScriptName ~= nil
or modData.towBarOriginalMass ~= nil
or modData.towBarOriginalBrakingForce ~= nil then
landtrainPendingRestoreVehicleIds[vehicle:getId()] = true
end
end
local function tryLandtrainTowbarRestore(vehicle, playerObj)
if vehicle == nil then return true end
local modData = vehicle:getModData()
if modData == nil then return true end
local frontVehicle = vehicle:getVehicleTowedBy()
if frontVehicle == vehicle then
ltLog("tryLandtrainTowbarRestore cleared self front link for " .. vehLabel(vehicle))
clearLandtrainFrontLinkData(vehicle)
modData["towed"] = false
vehicle:transmitModData()
return true
end
if frontVehicle ~= nil then
if modData["isTowingByTowBar"] == true or modData["towed"] == true then
storeLandtrainFrontLinkData(frontVehicle, vehicle)
if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
TowBarMod.Hook.setVehiclePostAttach(playerObj, vehicle)
end
if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.markReapplied then
TowBarMod.Hook.markReapplied(vehicle)
end
setTowBarModelVisibleForVehicle(vehicle, true)
refreshTowBarState(frontVehicle)
refreshTowBarState(vehicle)
end
return true
end
if modData["towed"] ~= true and not hasLandtrainFrontLinkData(vehicle) then
clearLandtrainFrontLinkData(vehicle)
return true
end
local frontVehicleSqlId = tonumber(modData[LANDTRAIN_FRONT_SQL_ID_KEY])
if frontVehicleSqlId == nil then
clearLandtrainFrontLinkData(vehicle)
if modData["towed"] == true then
modData["towed"] = false
vehicle:transmitModData()
end
return true
end
local vehicleSqlId = vehicle.getSqlId and vehicle:getSqlId() or nil
if vehicleSqlId ~= nil and vehicleSqlId >= 0 and frontVehicleSqlId == vehicleSqlId then
ltLog("tryLandtrainTowbarRestore dropped self metadata sqlId for " .. vehLabel(vehicle))
clearLandtrainFrontLinkData(vehicle)
modData["towed"] = false
vehicle:transmitModData()
return true
end
local savedFrontVehicle = getVehicleBySqlIdSafe(frontVehicleSqlId)
if savedFrontVehicle == nil then
return false
end
if savedFrontVehicle == vehicle or savedFrontVehicle:getId() == vehicle:getId() then
ltLog("tryLandtrainTowbarRestore dropped self resolved front for " .. vehLabel(vehicle))
clearLandtrainFrontLinkData(vehicle)
modData["towed"] = false
vehicle:transmitModData()
return true
end
local currentRear = savedFrontVehicle:getVehicleTowing()
if currentRear ~= nil and currentRear ~= vehicle then
local x = tonumber(modData[LANDTRAIN_SAVED_POS_X_KEY])
local y = tonumber(modData[LANDTRAIN_SAVED_POS_Y_KEY])
local z = tonumber(modData[LANDTRAIN_SAVED_POS_Z_KEY])
if x == nil or y == nil or z == nil then
return false
end
local attachmentA = modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] or savedFrontVehicle:getTowAttachmentSelf() or "trailer"
local attachmentB = modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] or vehicle:getTowAttachmentSelf() or "trailerfront"
local scriptA = savedFrontVehicle:getScript()
local scriptB = vehicle:getScript()
if scriptA == nil or scriptB == nil then
return false
end
if scriptA:getAttachmentById(attachmentA) == nil then
attachmentA = savedFrontVehicle:getTowAttachmentSelf() or "trailer"
end
if scriptB:getAttachmentById(attachmentB) == nil then
attachmentB = vehicle:getTowAttachmentSelf() or "trailerfront"
end
if scriptA:getAttachmentById(attachmentA) == nil or scriptB:getAttachmentById(attachmentB) == nil then
return false
local moved = false
moved = callVehicleMethodSafe(vehicle, "setX", x) or moved
moved = callVehicleMethodSafe(vehicle, "setY", y) or moved
moved = callVehicleMethodSafe(vehicle, "setZ", z) or moved
callVehicleMethodSafe(vehicle, "setLx", x)
callVehicleMethodSafe(vehicle, "setLy", y)
callVehicleMethodSafe(vehicle, "setLz", z)
local dir = tonumber(modData[LANDTRAIN_SAVED_DIR_KEY])
if dir ~= nil then
callVehicleMethodSafe(vehicle, "setDirectionAngle", dir)
end
playerObj = playerObj or getPlayer()
if playerObj == nil then
return false
local cell = getCell()
if cell ~= nil then
local square = cell:getGridSquare(math.floor(x), math.floor(y), math.floor(z))
if square ~= nil then
callVehicleMethodSafe(vehicle, "setCurrent", square)
end
end
callVehicleMethodSafe(vehicle, "transmitPosition")
return moved
end
local function collectSavedLandtrainLinks(vehicles)
local links = {}
local seenRear = {}
local frontSqlByRearSql = {}
for _, vehicle in ipairs(vehicles) do
local modData = vehicle and vehicle:getModData() or nil
local rearSqlId = vehicle and vehicle.getSqlId and vehicle:getSqlId() or nil
local frontSqlId = modData and tonumber(modData[LANDTRAIN_FRONT_SQL_ID_KEY]) or nil
if rearSqlId ~= nil and rearSqlId >= 0 and frontSqlId ~= nil and frontSqlId >= 0 and frontSqlId ~= rearSqlId then
frontSqlByRearSql[rearSqlId] = frontSqlId
end
end
local function getSavedDepthForRearSql(rearSqlId)
if rearSqlId == nil or rearSqlId < 0 then return 0 end
local depth = 0
local seen = {}
local cursorSqlId = rearSqlId
while cursorSqlId ~= nil do
local frontSqlId = frontSqlByRearSql[cursorSqlId]
if frontSqlId == nil or frontSqlId < 0 then
break
end
if seen[frontSqlId] then
break
end
seen[frontSqlId] = true
depth = depth + 1
cursorSqlId = frontSqlId
end
return depth
end
for _, rearVehicle in ipairs(vehicles) do
if rearVehicle ~= nil then
local rearId = rearVehicle:getId()
if not seenRear[rearId] then
local rearModData = rearVehicle:getModData()
local frontSqlId = rearModData and tonumber(rearModData[LANDTRAIN_FRONT_SQL_ID_KEY]) or nil
local frontVehicle = nil
if frontSqlId ~= nil and frontSqlId >= 0 then
frontVehicle = getVehicleBySqlIdSafe(frontSqlId)
end
if frontVehicle == nil then
frontVehicle = rearVehicle:getVehicleTowedBy()
end
if frontVehicle ~= nil and frontVehicle ~= rearVehicle and frontVehicle:getId() ~= rearVehicle:getId() then
local attachmentA, attachmentB = chooseLandtrainTowbarPair(
frontVehicle,
rearVehicle,
rearModData and rearModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] or nil,
rearModData and rearModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] or nil
)
table.insert(links, {
frontVehicle = frontVehicle,
rearVehicle = rearVehicle,
attachmentA = attachmentA,
attachmentB = attachmentB,
depth = getSavedDepthForRearSql(rearVehicle.getSqlId and rearVehicle:getSqlId() or nil)
})
seenRear[rearId] = true
end
end
end
end
table.sort(links, function(a, b)
if a.depth == b.depth then
return a.rearVehicle:getId() < b.rearVehicle:getId()
end
return a.depth < b.depth
end)
return links
end
local function detachSavedLandtrainLinks(links)
local detached = 0
for i = #links, 1, -1 do
local link = links[i]
local frontVehicle = link.frontVehicle
local rearVehicle = link.rearVehicle
if frontVehicle ~= nil and rearVehicle ~= nil
and (frontVehicle:getVehicleTowing() == rearVehicle or rearVehicle:getVehicleTowedBy() == frontVehicle) then
if breakConstraintSafe(frontVehicle, "load-rebuild-detach") then
detached = detached + 1
end
end
end
return detached
end
local function attachSavedLandtrainLink(playerObj, link, delayTicks)
local frontVehicle = link.frontVehicle
local rearVehicle = link.rearVehicle
if playerObj == nil or frontVehicle == nil or rearVehicle == nil then return false end
if TowBarMod == nil or TowBarMod.Utils == nil or TowBarMod.Utils.updateAttachmentsForRigidTow == nil then
return false
end
if frontVehicle == rearVehicle or frontVehicle:getId() == rearVehicle:getId() then
return false
end
TowBarMod.Utils.updateAttachmentsForRigidTow(savedFrontVehicle, vehicle, attachmentA, attachmentB)
vehicle:setScriptName("notTowingA_Trailer")
local scriptA = frontVehicle:getScript()
local scriptB = rearVehicle:getScript()
if scriptA == nil or scriptB == nil then
return false
end
if scriptA:getAttachmentById(link.attachmentA) == nil or scriptB:getAttachmentById(link.attachmentB) == nil then
return false
end
local originalScriptName = rearVehicle:getScriptName()
TowBarMod.Utils.updateAttachmentsForRigidTow(frontVehicle, rearVehicle, link.attachmentA, link.attachmentB)
rearVehicle:setScriptName("notTowingA_Trailer")
local args = {
vehicleA = savedFrontVehicle:getId(),
vehicleB = vehicle:getId(),
attachmentA = attachmentA,
attachmentB = attachmentB
vehicleA = frontVehicle:getId(),
vehicleB = rearVehicle:getId(),
attachmentA = link.attachmentA,
attachmentB = link.attachmentB
}
if args.vehicleA == args.vehicleB then
ltLog("tryLandtrainTowbarRestore blocked self attach args for " .. vehLabel(vehicle))
clearLandtrainFrontLinkData(vehicle)
modData["towed"] = false
vehicle:transmitModData()
return true
return false
end
sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
local frontModData = frontVehicle:getModData()
local rearModData = rearVehicle:getModData()
if frontModData == nil or rearModData == nil then return false end
frontModData["isTowingByTowBar"] = true
rearModData["isTowingByTowBar"] = true
rearModData["towed"] = true
if rearModData.towBarOriginalScriptName == nil and originalScriptName ~= "notTowingA_Trailer" then
rearModData.towBarOriginalScriptName = originalScriptName
end
if rearModData.towBarOriginalMass == nil then
rearModData.towBarOriginalMass = rearVehicle:getMass()
end
if rearModData.towBarOriginalBrakingForce == nil then
rearModData.towBarOriginalBrakingForce = rearVehicle:getBrakingForce()
end
frontVehicle:transmitModData()
rearVehicle:transmitModData()
storeLandtrainFrontLinkData(frontVehicle, rearVehicle, link.attachmentA, link.attachmentB)
if TowBarScheduleAction and TowBarMod and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, vehicle))
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, delayTicks, TowBarMod.Hook.setVehiclePostAttach, rearVehicle))
elseif TowBarMod and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
TowBarMod.Hook.setVehiclePostAttach(playerObj, vehicle)
TowBarMod.Hook.setVehiclePostAttach(playerObj, rearVehicle)
end
if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.markReapplied then
TowBarMod.Hook.markReapplied(rearVehicle)
end
savedFrontVehicle:getModData()["isTowingByTowBar"] = true
modData["isTowingByTowBar"] = true
modData["towed"] = true
savedFrontVehicle:transmitModData()
vehicle:transmitModData()
storeLandtrainFrontLinkData(savedFrontVehicle, vehicle, attachmentA, attachmentB)
setTowBarModelVisibleForVehicle(vehicle, true)
refreshTowBarState(savedFrontVehicle)
refreshTowBarState(vehicle)
if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.markReapplied then
TowBarMod.Hook.markReapplied(vehicle)
end
setTowBarModelVisibleForVehicle(rearVehicle, true)
refreshTowBarState(frontVehicle)
refreshTowBarState(rearVehicle)
return true
end
local function processLandtrainPendingRestores()
landtrainReapplyTickCounter = landtrainReapplyTickCounter + 1
if landtrainReapplyTickCounter < 15 then
local function runLandtrainSingleLoadRestorePass()
local vehicles = sanitizeLoadedTowLinks()
if vehicles == nil or #vehicles == 0 then
ltInfo("Landtrain load rebuild: no loaded vehicles")
return
end
landtrainReapplyTickCounter = 0
local resolvedVehicleIds = {}
local playerObj = getPlayer()
for vehicleId, _ in pairs(landtrainPendingRestoreVehicleIds) do
local vehicle = getVehicleByIdSafe(vehicleId)
if vehicle == nil or tryLandtrainTowbarRestore(vehicle, playerObj) then
table.insert(resolvedVehicleIds, vehicleId)
local links = collectSavedLandtrainLinks(vehicles)
if #links == 0 then
ltInfo("Landtrain load rebuild: no saved towbar links")
return
end
local playerObj = getPlayer() or getSpecificPlayer(0)
if playerObj == nil then
ltInfo("Landtrain load rebuild skipped: no player")
return
end
if TowBarMod == nil or TowBarMod.Utils == nil or TowBarMod.Utils.updateAttachmentsForRigidTow == nil then
ltInfo("Landtrain load rebuild skipped: TowBar utilities unavailable")
return
end
local movedVehicles = 0
local movedSeen = {}
for _, link in ipairs(links) do
local frontId = link.frontVehicle:getId()
local rearId = link.rearVehicle:getId()
if not movedSeen[frontId] then
if restoreSavedVehiclePosition(link.frontVehicle) then
movedVehicles = movedVehicles + 1
end
movedSeen[frontId] = true
end
if not movedSeen[rearId] then
if restoreSavedVehiclePosition(link.rearVehicle) then
movedVehicles = movedVehicles + 1
end
movedSeen[rearId] = true
end
end
for _, vehicleId in ipairs(resolvedVehicleIds) do
landtrainPendingRestoreVehicleIds[vehicleId] = nil
local detachedCount = detachSavedLandtrainLinks(links)
local attachedCount = 0
for index, link in ipairs(links) do
local delay = 10 + ((index - 1) * 5)
if attachSavedLandtrainLink(playerObj, link, delay) then
attachedCount = attachedCount + 1
end
end
local function queueLandtrainLoadedTowbarRestores()
local vehicles = getLoadedVehicles()
for _, vehicle in ipairs(vehicles) do
local frontVehicle = vehicle:getVehicleTowedBy()
if frontVehicle ~= nil and isTowbarManagedVehicle(vehicle) then
storeLandtrainFrontLinkData(frontVehicle, vehicle)
end
queueLandtrainTowbarRestore(vehicle)
end
saveActiveLandtrainTowbarSnapshot(vehicles)
ltInfo("Landtrain load rebuild complete moved=" .. tostring(movedVehicles)
.. " detached=" .. tostring(detachedCount)
.. " attached=" .. tostring(attachedCount)
.. " links=" .. tostring(#links))
end
local function onLandtrainSpawnVehicleEnd(vehicle)
if vehicle == nil then return end
local frontVehicle = vehicle:getVehicleTowedBy()
if frontVehicle ~= nil and isTowbarManagedVehicle(vehicle) then
storeLandtrainFrontLinkData(frontVehicle, vehicle)
local landtrainLoadRebuildDone = false
local landtrainSaveSnapshotTickCounter = 0
local function onLandtrainSaveSnapshotTick()
if not landtrainLoadRebuildDone then
return
end
queueLandtrainTowbarRestore(vehicle)
queueLandtrainTowbarRestore(vehicle:getVehicleTowedBy())
queueLandtrainTowbarRestore(vehicle:getVehicleTowing())
landtrainSaveSnapshotTickCounter = landtrainSaveSnapshotTickCounter + 1
if landtrainSaveSnapshotTickCounter < 60 then
return
end
landtrainSaveSnapshotTickCounter = 0
saveActiveLandtrainTowbarSnapshot()
end
local landtrainLoadRestoreDelayTicks = 0
local function onLandtrainSingleLoadRestoreTick()
if landtrainLoadRestoreDelayTicks > 0 then
landtrainLoadRestoreDelayTicks = landtrainLoadRestoreDelayTicks - 1
return
end
Events.OnTick.Remove(onLandtrainSingleLoadRestoreTick)
runLandtrainSingleLoadRestorePass()
landtrainLoadRebuildDone = true
end
local function scheduleLandtrainSingleLoadRestore()
-- Single delayed load pass: restore saved positions, detach all saved links, reattach in chain order.
landtrainLoadRebuildDone = false
landtrainSaveSnapshotTickCounter = 0
landtrainLoadRestoreDelayTicks = 90
Events.OnTick.Remove(onLandtrainSingleLoadRestoreTick)
Events.OnTick.Add(onLandtrainSingleLoadRestoreTick)
end
local _landtrainHookPosA = Vector3f.new()
@@ -700,11 +971,12 @@ local function captureTowbarFrontLink(towingVehicle)
return nil
end
local attachmentA, attachmentB = chooseLandtrainTowbarPair(frontVehicle, towingVehicle, nil, nil)
local link = {
frontVehicle = frontVehicle,
towingVehicle = towingVehicle,
attachmentA = frontVehicle:getTowAttachmentSelf() or "trailer",
attachmentB = towingVehicle:getTowAttachmentSelf() or "trailerfront"
attachmentA = attachmentA,
attachmentB = attachmentB
}
ltLog("captureTowbarFrontLink captured front=" .. vehLabel(frontVehicle) .. " middle=" .. vehLabel(towingVehicle)
.. " attA=" .. tostring(link.attachmentA) .. " attB=" .. tostring(link.attachmentB))
@@ -809,9 +1081,16 @@ end
local function menuHasTowbarAttachSlice(menu)
if menu == nil or menu.slices == nil then return false end
local attachAction = TowBarMod and TowBarMod.Hook and TowBarMod.Hook.attachByTowBarAction or nil
local chooseAction = TowBarMod and TowBarMod.UI and TowBarMod.UI.showChooseVehicleMenu or nil
if attachAction == nil and chooseAction == nil then
return false
end
for _, slice in ipairs(menu.slices) do
local command = slice.command and slice.command[1]
if command == TowBarMod.Hook.attachByTowBarAction or command == TowBarMod.UI.showChooseVehicleMenu then
if (attachAction ~= nil and command == attachAction)
or (chooseAction ~= nil and command == chooseAction) then
return true
end
end
@@ -820,9 +1099,13 @@ end
local function menuHasTowbarDetachSlice(menu)
if menu == nil or menu.slices == nil then return false end
local detachAction = TowBarMod and TowBarMod.Hook and TowBarMod.Hook.deattachTowBarAction or nil
if detachAction == nil then
return false
end
for _, slice in ipairs(menu.slices) do
local command = slice.command and slice.command[1]
if command == TowBarMod.Hook.deattachTowBarAction then
if command == detachAction then
return true
end
end
@@ -971,7 +1254,18 @@ end
local function addLandtrainHookOptionToMenu(playerObj, vehicle)
if playerObj == nil or vehicle == nil then return end
if not playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") then
local hasTowBarItem = false
if playerObj.getInventory and playerObj:getInventory() ~= nil then
hasTowBarItem = playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") ~= nil
end
if not hasTowBarItem and TowBarMod and TowBarMod.Hook and TowBarMod.Hook.getTowBarInventoryItem then
hasTowBarItem = TowBarMod.Hook.getTowBarInventoryItem(playerObj) ~= nil
end
if not hasTowBarItem and TowBarMod and TowBarMod.Utils and TowBarMod.Utils.getTowBarInventoryItem then
hasTowBarItem = TowBarMod.Utils.getTowBarInventoryItem(playerObj) ~= nil
end
if not hasTowBarItem then
ltLog("addLandtrainHookOptionToMenu no TowBar item for " .. vehLabel(vehicle))
return
end
@@ -1093,7 +1387,7 @@ local function installLandtrainTowbarPatch()
if towedVehicle then
refreshTowBarState(towedVehicle:getVehicleTowing())
end
queueLandtrainTowbarRestore(towedVehicle)
saveActiveLandtrainTowbarSnapshot()
end
TowBarMod.Hook.performAttachTowBar = performAttachWrapper
@@ -1166,8 +1460,7 @@ local function installLandtrainTowbarPatch()
refreshTowBarState(resolvedTowingVehicle:getVehicleTowing())
refreshTowBarState(resolvedTowedVehicle:getVehicleTowedBy())
refreshTowBarState(resolvedTowedVehicle:getVehicleTowing())
queueLandtrainTowbarRestore(resolvedTowingVehicle)
queueLandtrainTowbarRestore(resolvedTowedVehicle)
saveActiveLandtrainTowbarSnapshot()
dumpTowState("performDeattachTowBar post-reconcile towing", resolvedTowingVehicle)
dumpTowState("performDeattachTowBar post-reconcile towed", resolvedTowedVehicle)
@@ -1202,7 +1495,7 @@ local function installLandtrainTowbarPatch()
ltLog("installLandtrainTowbarPatch patched ISVehicleMenu.onDetachTrailer")
end
-- Towbar UI only adds attach when fully unlinked; add attach option for linked vehicles too.
-- Towbar UI only adds attach when fully unlinked; add attach for linked/chain-related interactions too.
if ISVehicleMenu and ISVehicleMenu.showRadialMenu and ISVehicleMenu.showRadialMenu ~= TowBarMod.UI._landtrainShowRadialWrapper then
local originalShowRadialMenu = ISVehicleMenu.showRadialMenu
local showRadialWrapper = function(playerObj)
@@ -1228,7 +1521,15 @@ local function installLandtrainTowbarPatch()
return
end
if (vehicle:getVehicleTowing() or vehicle:getVehicleTowedBy()) then
local vehicleLinked = vehicle:getVehicleTowing() ~= nil or vehicle:getVehicleTowedBy() ~= nil
local chainRelated = isTowbarChainVehicleOrNeighbor(vehicle)
local shouldInjectAttach = vehicleLinked or chainRelated
ltLog("showRadial attach check vehicle=" .. vehLabel(vehicle)
.. " linked=" .. tostring(vehicleLinked)
.. " chainRelated=" .. tostring(chainRelated)
.. " shouldInject=" .. tostring(shouldInjectAttach))
if shouldInjectAttach then
addLandtrainHookOptionToMenu(playerObj, vehicle)
else
ltLog("showRadial vehicle not linked, Landtrain attach not injected for " .. vehLabel(vehicle))
@@ -1299,7 +1600,5 @@ end
Events.OnGameBoot.Add(ensureTowAttachmentsForTrailers)
Events.OnGameBoot.Add(startLandtrainInstallWatchdog)
Events.OnGameStart.Add(startLandtrainInstallWatchdog)
Events.OnGameStart.Add(scheduleLandtrainLoadSanity)
Events.OnGameStart.Add(queueLandtrainLoadedTowbarRestores)
Events.OnSpawnVehicleEnd.Add(onLandtrainSpawnVehicleEnd)
Events.OnTick.Add(processLandtrainPendingRestores)
Events.OnGameStart.Add(scheduleLandtrainSingleLoadRestore)
Events.OnTick.Add(onLandtrainSaveSnapshotTick)