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_DIST_SQ = 3.25 -- ~1.8 tiles
local LANDTRAIN_FALLBACK_MAX_DZ = 0.9 local LANDTRAIN_FALLBACK_MAX_DZ = 0.9
local LANDTRAIN_FRONT_SQL_ID_KEY = "landtrainTowbarFrontSqlId" 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_A_KEY = "landtrainTowbarAttachmentA"
local LANDTRAIN_FRONT_ATTACHMENT_B_KEY = "landtrainTowbarAttachmentB" 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) local function ltLog(msg)
if not LANDTRAIN_DEBUG then return end if not LANDTRAIN_DEBUG then return end
print("[Landtrain] " .. tostring(msg)) emitLandtrainLog("[Landtrain] " .. tostring(msg))
end end
local function ltInfo(msg) local function ltInfo(msg)
print("[Landtrain][Info] " .. tostring(msg)) emitLandtrainLog("[Landtrain][Info] " .. tostring(msg))
end end
ltInfo("UnlimitedTowbarChains loaded. debug=" .. tostring(LANDTRAIN_DEBUG)) ltInfo("UnlimitedTowbarChains loaded. debug=" .. tostring(LANDTRAIN_DEBUG))
@@ -27,6 +38,115 @@ local function vehLabel(vehicle)
return tostring(vehicle:getId()) .. ":" .. tostring(name) return tostring(vehicle:getId()) .. ":" .. tostring(name)
end 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) local function dumpTowState(prefix, vehicle)
if not LANDTRAIN_DEBUG then return end if not LANDTRAIN_DEBUG then return end
if vehicle == nil then if vehicle == nil then
@@ -131,7 +251,7 @@ local function getVehicleBySqlIdSafe(sqlId)
for i = 0, vehicles:size() - 1 do for i = 0, vehicles:size() - 1 do
local vehicle = vehicles:get(i) 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 return vehicle
end end
end end
@@ -140,11 +260,13 @@ end
local function storeLandtrainFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB) local function storeLandtrainFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB)
if towingVehicle == nil or towedVehicle == nil then return end if towingVehicle == nil or towedVehicle == nil then return end
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle: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 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_SQL_ID_KEY] = nil
towedModData[LANDTRAIN_FRONT_ID_KEY_LEGACY] = nil
towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil
towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil
towedVehicle:transmitModData() towedVehicle:transmitModData()
@@ -152,15 +274,22 @@ local function storeLandtrainFrontLinkData(towingVehicle, towedVehicle, attachme
return return
end 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 if towingSqlId ~= nil and towingSqlId >= 0 then
towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = towingSqlId towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = towingSqlId
else else
towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil towedModData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil
end end
towedModData[LANDTRAIN_FRONT_ID_KEY_LEGACY] = nil local towedSqlId = towedVehicle.getSqlId and towedVehicle:getSqlId() or -1
towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = attachmentA or towingVehicle:getTowAttachmentSelf() or "trailer" if towedSqlId ~= nil and towedSqlId >= 0 then
towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = attachmentB or towedVehicle:getTowAttachmentSelf() or "trailerfront" 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() towedVehicle:transmitModData()
end end
@@ -168,8 +297,19 @@ local function clearLandtrainFrontLinkData(vehicle)
if vehicle == nil then return end if vehicle == nil then return end
local modData = vehicle:getModData() local modData = vehicle:getModData()
if modData == nil then return end 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_SQL_ID_KEY] = nil
modData[LANDTRAIN_FRONT_ID_KEY_LEGACY] = nil
modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil
modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil
vehicle:transmitModData() vehicle:transmitModData()
@@ -187,6 +327,7 @@ local function isTowbarManagedVehicle(vehicle)
or modData["isChangedTowedAttachment"] == true or modData["isChangedTowedAttachment"] == true
or modData["towBarChangedAttachmentId"] ~= nil or modData["towBarChangedAttachmentId"] ~= nil
or modData[LANDTRAIN_FRONT_SQL_ID_KEY] ~= nil or modData[LANDTRAIN_FRONT_SQL_ID_KEY] ~= nil
or modData[LANDTRAIN_REAR_SQL_ID_KEY] ~= nil
end end
local function restoreVehicleTowbarDefaults(vehicle) local function restoreVehicleTowbarDefaults(vehicle)
@@ -244,9 +385,9 @@ local function restoreVehicleTowbarDefaults(vehicle)
modData.towBarOriginalMass = nil modData.towBarOriginalMass = nil
modData.towBarOriginalBrakingForce = nil modData.towBarOriginalBrakingForce = nil
modData[LANDTRAIN_FRONT_SQL_ID_KEY] = 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_A_KEY] = nil
modData[LANDTRAIN_FRONT_ATTACHMENT_B_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 if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.clearReapplied then
TowBarMod.Hook.clearReapplied(vehicle) TowBarMod.Hook.clearReapplied(vehicle)
@@ -277,7 +418,7 @@ local function reconcileTowbarSplitVehicle(vehicle)
end end
modData["towed"] = (frontVehicle ~= nil) modData["towed"] = (frontVehicle ~= nil)
modData["isTowingByTowBar"] = (frontVehicle ~= nil) or (hasTowbarRear == true) modData["isTowingByTowBar"] = (modData["towed"] == true) or (hasTowbarRear == true)
vehicle:transmitModData() vehicle:transmitModData()
end end
@@ -344,7 +485,7 @@ end
local function sanitizeLoadedTowLinks() local function sanitizeLoadedTowLinks()
local vehicles = getLoadedVehicles() local vehicles = getLoadedVehicles()
if #vehicles == 0 then return end if #vehicles == 0 then return vehicles end
for _, vehicle in ipairs(vehicles) do for _, vehicle in ipairs(vehicles) do
local front = vehicle:getVehicleTowedBy() local front = vehicle:getVehicleTowedBy()
@@ -357,252 +498,382 @@ local function sanitizeLoadedTowLinks()
end end
end end
for _, vehicle in ipairs(vehicles) do return vehicles
reconcileTowbarSplitAround(vehicle)
refreshTowBarState(vehicle)
local frontVehicle = vehicle:getVehicleTowedBy()
if frontVehicle ~= nil and isTowbarManagedVehicle(vehicle) then
storeLandtrainFrontLinkData(frontVehicle, vehicle)
end
end
end end
local _landtrainLoadSanityTicks = 0 local function setModDataNumberIfChanged(modData, key, value)
local function onLandtrainLoadSanityTick() if modData == nil or key == nil or value == nil then return false end
if _landtrainLoadSanityTicks <= 0 then local current = tonumber(modData[key])
Events.OnTick.Remove(onLandtrainLoadSanityTick) if current ~= nil and math.abs(current - value) < 0.0001 then
return return false
end end
modData[key] = value
_landtrainLoadSanityTicks = _landtrainLoadSanityTicks - 1 return true
if (_landtrainLoadSanityTicks % 30) ~= 0 then
return
end
sanitizeLoadedTowLinks()
end end
local function scheduleLandtrainLoadSanity() local function saveLandtrainVehiclePosition(vehicle)
_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)
if vehicle == nil then return false end if vehicle == nil then return false end
local modData = vehicle:getModData() local modData = vehicle:getModData()
if modData == nil then return false end 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
if changed then
vehicle:transmitModData()
end
return changed
end end
local function queueLandtrainTowbarRestore(vehicle) local function isLandtrainTowbarLink(frontVehicle, rearVehicle)
if vehicle == nil then return end if frontVehicle == nil or rearVehicle == nil then return false end
local modData = vehicle:getModData() if frontVehicle == rearVehicle or frontVehicle:getId() == rearVehicle:getId() then return false end
if modData == nil then return end if frontVehicle:getVehicleTowing() ~= rearVehicle or rearVehicle:getVehicleTowedBy() ~= frontVehicle then
return false
-- 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 end
local vehicleSqlId = vehicle.getSqlId and vehicle:getSqlId() or nil local frontModData = frontVehicle:getModData()
local frontVehicleSqlId = tonumber(modData[LANDTRAIN_FRONT_SQL_ID_KEY]) local rearModData = rearVehicle:getModData()
if frontVehicleSqlId ~= nil and vehicleSqlId ~= nil and vehicleSqlId >= 0 and frontVehicleSqlId == vehicleSqlId then if frontModData == nil or rearModData == nil then return false end
modData[LANDTRAIN_FRONT_SQL_ID_KEY] = nil return isTowbarManagedVehicle(frontVehicle)
modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = nil or isTowbarManagedVehicle(rearVehicle)
modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = nil or tonumber(rearModData[LANDTRAIN_FRONT_SQL_ID_KEY]) ~= nil
modData["towed"] = false or tonumber(frontModData[LANDTRAIN_REAR_SQL_ID_KEY]) ~= nil
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 end
local function tryLandtrainTowbarRestore(vehicle, playerObj) local function saveLandtrainTowbarLink(frontVehicle, rearVehicle, attachmentA, attachmentB)
if vehicle == nil then return true end 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() local modData = vehicle:getModData()
if modData == nil then return true end if modData == nil then return false end
local frontVehicle = vehicle:getVehicleTowedBy() local x = tonumber(modData[LANDTRAIN_SAVED_POS_X_KEY])
if frontVehicle == vehicle then local y = tonumber(modData[LANDTRAIN_SAVED_POS_Y_KEY])
ltLog("tryLandtrainTowbarRestore cleared self front link for " .. vehLabel(vehicle)) local z = tonumber(modData[LANDTRAIN_SAVED_POS_Z_KEY])
clearLandtrainFrontLinkData(vehicle) if x == nil or y == nil or z == nil then
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
return false return false
end end
local attachmentA = modData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] or savedFrontVehicle:getTowAttachmentSelf() or "trailer" local moved = false
local attachmentB = modData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] or vehicle:getTowAttachmentSelf() or "trailerfront" moved = callVehicleMethodSafe(vehicle, "setX", x) or moved
local scriptA = savedFrontVehicle:getScript() moved = callVehicleMethodSafe(vehicle, "setY", y) or moved
local scriptB = vehicle:getScript() moved = callVehicleMethodSafe(vehicle, "setZ", z) or moved
if scriptA == nil or scriptB == nil then callVehicleMethodSafe(vehicle, "setLx", x)
return false callVehicleMethodSafe(vehicle, "setLy", y)
end callVehicleMethodSafe(vehicle, "setLz", z)
if scriptA:getAttachmentById(attachmentA) == nil then
attachmentA = savedFrontVehicle:getTowAttachmentSelf() or "trailer" local dir = tonumber(modData[LANDTRAIN_SAVED_DIR_KEY])
end if dir ~= nil then
if scriptB:getAttachmentById(attachmentB) == nil then callVehicleMethodSafe(vehicle, "setDirectionAngle", dir)
attachmentB = vehicle:getTowAttachmentSelf() or "trailerfront"
end
if scriptA:getAttachmentById(attachmentA) == nil or scriptB:getAttachmentById(attachmentB) == nil then
return false
end end
playerObj = playerObj or getPlayer() local cell = getCell()
if playerObj == nil then if cell ~= nil then
return false local square = cell:getGridSquare(math.floor(x), math.floor(y), math.floor(z))
if square ~= nil then
callVehicleMethodSafe(vehicle, "setCurrent", square)
end 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 if TowBarMod == nil or TowBarMod.Utils == nil or TowBarMod.Utils.updateAttachmentsForRigidTow == nil then
return false return false
end end
if frontVehicle == rearVehicle or frontVehicle:getId() == rearVehicle:getId() then
return false
end
TowBarMod.Utils.updateAttachmentsForRigidTow(savedFrontVehicle, vehicle, attachmentA, attachmentB) local scriptA = frontVehicle:getScript()
vehicle:setScriptName("notTowingA_Trailer") 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 = { local args = {
vehicleA = savedFrontVehicle:getId(), vehicleA = frontVehicle:getId(),
vehicleB = vehicle:getId(), vehicleB = rearVehicle:getId(),
attachmentA = attachmentA, attachmentA = link.attachmentA,
attachmentB = attachmentB attachmentB = link.attachmentB
} }
if args.vehicleA == args.vehicleB then if args.vehicleA == args.vehicleB then
ltLog("tryLandtrainTowbarRestore blocked self attach args for " .. vehLabel(vehicle)) return false
clearLandtrainFrontLinkData(vehicle)
modData["towed"] = false
vehicle:transmitModData()
return true
end end
sendClientCommand(playerObj, "vehicle", "attachTrailer", args) 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 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 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 end
savedFrontVehicle:getModData()["isTowingByTowBar"] = true setTowBarModelVisibleForVehicle(rearVehicle, true)
modData["isTowingByTowBar"] = true refreshTowBarState(frontVehicle)
modData["towed"] = true refreshTowBarState(rearVehicle)
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
return true return true
end end
local function processLandtrainPendingRestores() local function runLandtrainSingleLoadRestorePass()
landtrainReapplyTickCounter = landtrainReapplyTickCounter + 1 local vehicles = sanitizeLoadedTowLinks()
if landtrainReapplyTickCounter < 15 then if vehicles == nil or #vehicles == 0 then
ltInfo("Landtrain load rebuild: no loaded vehicles")
return return
end end
landtrainReapplyTickCounter = 0
local resolvedVehicleIds = {} local links = collectSavedLandtrainLinks(vehicles)
local playerObj = getPlayer() if #links == 0 then
for vehicleId, _ in pairs(landtrainPendingRestoreVehicleIds) do ltInfo("Landtrain load rebuild: no saved towbar links")
local vehicle = getVehicleByIdSafe(vehicleId) return
if vehicle == nil or tryLandtrainTowbarRestore(vehicle, playerObj) then end
table.insert(resolvedVehicleIds, vehicleId)
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
end end
for _, vehicleId in ipairs(resolvedVehicleIds) do local detachedCount = detachSavedLandtrainLinks(links)
landtrainPendingRestoreVehicleIds[vehicleId] = nil 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
end
saveActiveLandtrainTowbarSnapshot(vehicles)
ltInfo("Landtrain load rebuild complete moved=" .. tostring(movedVehicles)
.. " detached=" .. tostring(detachedCount)
.. " attached=" .. tostring(attachedCount)
.. " links=" .. tostring(#links))
end end
local function queueLandtrainLoadedTowbarRestores() local landtrainLoadRebuildDone = false
local vehicles = getLoadedVehicles() local landtrainSaveSnapshotTickCounter = 0
for _, vehicle in ipairs(vehicles) do local function onLandtrainSaveSnapshotTick()
local frontVehicle = vehicle:getVehicleTowedBy() if not landtrainLoadRebuildDone then
if frontVehicle ~= nil and isTowbarManagedVehicle(vehicle) then return
storeLandtrainFrontLinkData(frontVehicle, vehicle)
end end
queueLandtrainTowbarRestore(vehicle) landtrainSaveSnapshotTickCounter = landtrainSaveSnapshotTickCounter + 1
if landtrainSaveSnapshotTickCounter < 60 then
return
end end
landtrainSaveSnapshotTickCounter = 0
saveActiveLandtrainTowbarSnapshot()
end end
local function onLandtrainSpawnVehicleEnd(vehicle) local landtrainLoadRestoreDelayTicks = 0
if vehicle == nil then return end local function onLandtrainSingleLoadRestoreTick()
local frontVehicle = vehicle:getVehicleTowedBy() if landtrainLoadRestoreDelayTicks > 0 then
if frontVehicle ~= nil and isTowbarManagedVehicle(vehicle) then landtrainLoadRestoreDelayTicks = landtrainLoadRestoreDelayTicks - 1
storeLandtrainFrontLinkData(frontVehicle, vehicle) return
end end
queueLandtrainTowbarRestore(vehicle) Events.OnTick.Remove(onLandtrainSingleLoadRestoreTick)
queueLandtrainTowbarRestore(vehicle:getVehicleTowedBy()) runLandtrainSingleLoadRestorePass()
queueLandtrainTowbarRestore(vehicle:getVehicleTowing()) 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 end
local _landtrainHookPosA = Vector3f.new() local _landtrainHookPosA = Vector3f.new()
@@ -700,11 +971,12 @@ local function captureTowbarFrontLink(towingVehicle)
return nil return nil
end end
local attachmentA, attachmentB = chooseLandtrainTowbarPair(frontVehicle, towingVehicle, nil, nil)
local link = { local link = {
frontVehicle = frontVehicle, frontVehicle = frontVehicle,
towingVehicle = towingVehicle, towingVehicle = towingVehicle,
attachmentA = frontVehicle:getTowAttachmentSelf() or "trailer", attachmentA = attachmentA,
attachmentB = towingVehicle:getTowAttachmentSelf() or "trailerfront" attachmentB = attachmentB
} }
ltLog("captureTowbarFrontLink captured front=" .. vehLabel(frontVehicle) .. " middle=" .. vehLabel(towingVehicle) ltLog("captureTowbarFrontLink captured front=" .. vehLabel(frontVehicle) .. " middle=" .. vehLabel(towingVehicle)
.. " attA=" .. tostring(link.attachmentA) .. " attB=" .. tostring(link.attachmentB)) .. " attA=" .. tostring(link.attachmentA) .. " attB=" .. tostring(link.attachmentB))
@@ -809,9 +1081,16 @@ end
local function menuHasTowbarAttachSlice(menu) local function menuHasTowbarAttachSlice(menu)
if menu == nil or menu.slices == nil then return false end 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 for _, slice in ipairs(menu.slices) do
local command = slice.command and slice.command[1] 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 return true
end end
end end
@@ -820,9 +1099,13 @@ end
local function menuHasTowbarDetachSlice(menu) local function menuHasTowbarDetachSlice(menu)
if menu == nil or menu.slices == nil then return false end 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 for _, slice in ipairs(menu.slices) do
local command = slice.command and slice.command[1] local command = slice.command and slice.command[1]
if command == TowBarMod.Hook.deattachTowBarAction then if command == detachAction then
return true return true
end end
end end
@@ -971,7 +1254,18 @@ end
local function addLandtrainHookOptionToMenu(playerObj, vehicle) local function addLandtrainHookOptionToMenu(playerObj, vehicle)
if playerObj == nil or vehicle == nil then return end 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)) ltLog("addLandtrainHookOptionToMenu no TowBar item for " .. vehLabel(vehicle))
return return
end end
@@ -1093,7 +1387,7 @@ local function installLandtrainTowbarPatch()
if towedVehicle then if towedVehicle then
refreshTowBarState(towedVehicle:getVehicleTowing()) refreshTowBarState(towedVehicle:getVehicleTowing())
end end
queueLandtrainTowbarRestore(towedVehicle) saveActiveLandtrainTowbarSnapshot()
end end
TowBarMod.Hook.performAttachTowBar = performAttachWrapper TowBarMod.Hook.performAttachTowBar = performAttachWrapper
@@ -1166,8 +1460,7 @@ local function installLandtrainTowbarPatch()
refreshTowBarState(resolvedTowingVehicle:getVehicleTowing()) refreshTowBarState(resolvedTowingVehicle:getVehicleTowing())
refreshTowBarState(resolvedTowedVehicle:getVehicleTowedBy()) refreshTowBarState(resolvedTowedVehicle:getVehicleTowedBy())
refreshTowBarState(resolvedTowedVehicle:getVehicleTowing()) refreshTowBarState(resolvedTowedVehicle:getVehicleTowing())
queueLandtrainTowbarRestore(resolvedTowingVehicle) saveActiveLandtrainTowbarSnapshot()
queueLandtrainTowbarRestore(resolvedTowedVehicle)
dumpTowState("performDeattachTowBar post-reconcile towing", resolvedTowingVehicle) dumpTowState("performDeattachTowBar post-reconcile towing", resolvedTowingVehicle)
dumpTowState("performDeattachTowBar post-reconcile towed", resolvedTowedVehicle) dumpTowState("performDeattachTowBar post-reconcile towed", resolvedTowedVehicle)
@@ -1202,7 +1495,7 @@ local function installLandtrainTowbarPatch()
ltLog("installLandtrainTowbarPatch patched ISVehicleMenu.onDetachTrailer") ltLog("installLandtrainTowbarPatch patched ISVehicleMenu.onDetachTrailer")
end 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 if ISVehicleMenu and ISVehicleMenu.showRadialMenu and ISVehicleMenu.showRadialMenu ~= TowBarMod.UI._landtrainShowRadialWrapper then
local originalShowRadialMenu = ISVehicleMenu.showRadialMenu local originalShowRadialMenu = ISVehicleMenu.showRadialMenu
local showRadialWrapper = function(playerObj) local showRadialWrapper = function(playerObj)
@@ -1228,7 +1521,15 @@ local function installLandtrainTowbarPatch()
return return
end 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) addLandtrainHookOptionToMenu(playerObj, vehicle)
else else
ltLog("showRadial vehicle not linked, Landtrain attach not injected for " .. vehLabel(vehicle)) ltLog("showRadial vehicle not linked, Landtrain attach not injected for " .. vehLabel(vehicle))
@@ -1299,7 +1600,5 @@ end
Events.OnGameBoot.Add(ensureTowAttachmentsForTrailers) Events.OnGameBoot.Add(ensureTowAttachmentsForTrailers)
Events.OnGameBoot.Add(startLandtrainInstallWatchdog) Events.OnGameBoot.Add(startLandtrainInstallWatchdog)
Events.OnGameStart.Add(startLandtrainInstallWatchdog) Events.OnGameStart.Add(startLandtrainInstallWatchdog)
Events.OnGameStart.Add(scheduleLandtrainLoadSanity) Events.OnGameStart.Add(scheduleLandtrainSingleLoadRestore)
Events.OnGameStart.Add(queueLandtrainLoadedTowbarRestores) Events.OnTick.Add(onLandtrainSaveSnapshotTick)
Events.OnSpawnVehicleEnd.Add(onLandtrainSpawnVehicleEnd)
Events.OnTick.Add(processLandtrainPendingRestores)