diff --git a/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua b/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua index 2dd76d1..e369314 100644 --- a/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua +++ b/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua @@ -2,16 +2,25 @@ if not TowBarMod then TowBarMod = {} end if not TowBarMod.Utils then TowBarMod.Utils = {} end if not TowBarMod.UI then TowBarMod.UI = {} end -local LANDTRAIN_DEBUG = false -local LANDTRAIN_ATTACH_LABEL = "Landtrain attach" +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 function ltLog(msg) if not LANDTRAIN_DEBUG then return end print("[Landtrain] " .. tostring(msg)) end +local function ltInfo(msg) + print("[Landtrain][Info] " .. tostring(msg)) +end + +ltInfo("UnlimitedTowbarChains loaded. debug=" .. tostring(LANDTRAIN_DEBUG)) + local function vehLabel(vehicle) if vehicle == nil then return "nil" end local name = vehicle:getScriptName() or "unknown" @@ -85,6 +94,517 @@ local function setTowBarModelVisibleForVehicle(vehicle, visible) vehicle:doDamageOverlay() end +local function getVehicleByIdSafe(vehicleId) + if vehicleId == nil then return nil end + if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.getVehicleByIdSafe then + return TowBarMod.Hook.getVehicleByIdSafe(vehicleId) + end + if getVehicleById then + return getVehicleById(vehicleId) + end + + local cell = getCell() + if cell == nil then return nil end + local vehicles = cell:getVehicles() + if vehicles == nil then return nil end + + for i = 0, vehicles:size() - 1 do + local vehicle = vehicles:get(i) + if vehicle ~= nil and vehicle:getId() == vehicleId then + return vehicle + end + end + return nil +end + +local function getVehicleBySqlIdSafe(sqlId) + if sqlId == nil then return nil end + local targetSqlId = tonumber(sqlId) + if targetSqlId == nil or targetSqlId < 0 then + return nil + end + + local cell = getCell() + if cell == nil then return nil end + local vehicles = cell:getVehicles() + if vehicles == nil then return nil end + + for i = 0, vehicles:size() - 1 do + local vehicle = vehicles:get(i) + if vehicle ~= nil and vehicle:getSqlId and vehicle:getSqlId() == targetSqlId then + return vehicle + end + end + return nil +end + +local function storeLandtrainFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB) + if towingVehicle == nil or towedVehicle == nil then return end + local towedModData = towedVehicle:getModData() + if towedModData == nil then return end + if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then + 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() + ltLog("storeLandtrainFrontLinkData rejected self-link vehicle=" .. vehLabel(towedVehicle)) + return + end + + 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" + towedVehicle:transmitModData() +end + +local function clearLandtrainFrontLinkData(vehicle) + if vehicle == nil then return end + local modData = vehicle:getModData() + if modData == nil then return 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() +end + +local function isTowbarManagedVehicle(vehicle) + if vehicle == nil then return false end + local modData = vehicle:getModData() + if modData == nil then return false end + return modData["isTowingByTowBar"] == true + or modData["towed"] == true + or modData.towBarOriginalScriptName ~= nil + or modData.towBarOriginalMass ~= nil + or modData.towBarOriginalBrakingForce ~= nil + or modData["isChangedTowedAttachment"] == true + or modData["towBarChangedAttachmentId"] ~= nil + or modData[LANDTRAIN_FRONT_SQL_ID_KEY] ~= nil +end + +local function restoreVehicleTowbarDefaults(vehicle) + if vehicle == nil then return end + local modData = vehicle:getModData() + if modData == nil then return end + + local script = vehicle:getScript() + local towingAttachmentId = vehicle:getTowAttachmentSelf() + if script and towingAttachmentId then + local towingAttachment = script:getAttachmentById(towingAttachmentId) + if towingAttachment ~= nil then + towingAttachment:setUpdateConstraint(true) + towingAttachment:setZOffset((towingAttachmentId == "trailer") and -1 or 1) + end + end + + local changedAttachmentId = modData["towBarChangedAttachmentId"] or towingAttachmentId + if script and changedAttachmentId then + local towedAttachment = script:getAttachmentById(changedAttachmentId) + if towedAttachment ~= nil then + towedAttachment:setUpdateConstraint(true) + towedAttachment:setZOffset((changedAttachmentId == "trailer") and -1 or 1) + + if modData["isChangedTowedAttachment"] then + local offset = towedAttachment:getOffset() + local storedShift = tonumber(modData["towBarChangedOffsetZShift"]) + if storedShift ~= nil then + towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() - storedShift) + else + local zShift = offset:z() > 0 and -1 or 1 + towedAttachment:getOffset():set(offset:x(), offset:y(), offset:z() + zShift) + end + end + end + end + + if modData.towBarOriginalScriptName then + vehicle:setScriptName(modData.towBarOriginalScriptName) + end + if modData.towBarOriginalMass ~= nil then + vehicle:setMass(modData.towBarOriginalMass) + end + if modData.towBarOriginalBrakingForce ~= nil then + vehicle:setBrakingForce(modData.towBarOriginalBrakingForce) + end + vehicle:constraintChanged() + vehicle:updateTotalMass() + + modData["towed"] = false + modData["isChangedTowedAttachment"] = false + modData["towBarChangedAttachmentId"] = nil + modData["towBarChangedOffsetZShift"] = nil + modData.towBarOriginalScriptName = nil + 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 + + if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.clearReapplied then + TowBarMod.Hook.clearReapplied(vehicle) + end + if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.lastForcedReapplyAtByVehicle then + TowBarMod.Hook.lastForcedReapplyAtByVehicle[vehicle:getId()] = nil + end + setTowBarModelVisibleForVehicle(vehicle, false) +end + +local function reconcileTowbarSplitVehicle(vehicle) + if vehicle == nil then return end + if not isTowbarManagedVehicle(vehicle) then return end + + local modData = vehicle:getModData() + if modData == nil then return end + + local frontVehicle = vehicle:getVehicleTowedBy() + local rearVehicle = vehicle:getVehicleTowing() + local rearModData = rearVehicle and rearVehicle:getModData() or nil + local hasTowbarRear = rearModData and rearModData["towed"] == true and rearModData["isTowingByTowBar"] == true + + if frontVehicle == nil and (modData["towed"] == true or modData.towBarOriginalMass ~= nil or modData.towBarOriginalBrakingForce ~= nil) then + ltLog("reconcileTowbarSplitVehicle restoring new lead " .. vehLabel(vehicle)) + restoreVehicleTowbarDefaults(vehicle) + elseif frontVehicle ~= nil and (modData["isTowingByTowBar"] == true or modData["towed"] == true) then + storeLandtrainFrontLinkData(frontVehicle, vehicle) + end + + modData["towed"] = (frontVehicle ~= nil) + modData["isTowingByTowBar"] = (frontVehicle ~= nil) or (hasTowbarRear == true) + vehicle:transmitModData() +end + +local function reconcileTowbarSplitAround(vehicle) + if vehicle == nil then return end + reconcileTowbarSplitVehicle(vehicle) + reconcileTowbarSplitVehicle(vehicle:getVehicleTowedBy()) + reconcileTowbarSplitVehicle(vehicle:getVehicleTowing()) +end + +local function queueTowbarSplitReconcile(playerObj, vehicle, delayTicks) + if playerObj == nil or vehicle == nil then return end + local delay = delayTicks or 12 + ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, delay, function(_, vehicleArg) + reconcileTowbarSplitAround(vehicleArg) + end, vehicle)) +end + +local function getLoadedVehicles() + local vehicles = {} + local cell = getCell() + if cell == nil then return vehicles end + local list = cell:getVehicles() + if list == nil then return vehicles end + for i = 0, list:size() - 1 do + local vehicle = list:get(i) + if vehicle ~= nil then + table.insert(vehicles, vehicle) + end + end + return vehicles +end + +local function breakConstraintSafe(vehicle, reason) + if vehicle == nil then return false end + ltLog("breakConstraintSafe reason=" .. tostring(reason) .. " vehicle=" .. vehLabel(vehicle)) + local ok = pcall(function() + vehicle:breakConstraint(true, false) + end) + if ok then return true end + + local playerObj = getSpecificPlayer(0) + if playerObj ~= nil then + sendClientCommand(playerObj, "towbar", "detachConstraint", { vehicle = vehicle:getId() }) + return true + end + return false +end + +local function towingLoopDetected(startVehicle) + if startVehicle == nil then return false end + local visited = {} + local vehicle = startVehicle + while vehicle ~= nil do + local id = vehicle:getId() + if visited[id] then + return true + end + visited[id] = true + vehicle = vehicle:getVehicleTowing() + end + return false +end + +local function sanitizeLoadedTowLinks() + local vehicles = getLoadedVehicles() + if #vehicles == 0 then return end + + for _, vehicle in ipairs(vehicles) do + local front = vehicle:getVehicleTowedBy() + local rear = vehicle:getVehicleTowing() + + if front == vehicle or rear == vehicle then + breakConstraintSafe(vehicle, "self-link") + elseif towingLoopDetected(vehicle) then + breakConstraintSafe(vehicle, "loop") + 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 +end + +local _landtrainLoadSanityTicks = 0 +local function onLandtrainLoadSanityTick() + if _landtrainLoadSanityTicks <= 0 then + Events.OnTick.Remove(onLandtrainLoadSanityTick) + return + 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) + 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 +end + +local function queueLandtrainTowbarRestore(vehicle) + if vehicle == nil then return end + local modData = vehicle:getModData() + if modData == nil then return 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 + 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 + end + + playerObj = playerObj or getPlayer() + if playerObj == nil then + return false + end + if TowBarMod == nil or TowBarMod.Utils == nil or TowBarMod.Utils.updateAttachmentsForRigidTow == nil then + return false + end + + TowBarMod.Utils.updateAttachmentsForRigidTow(savedFrontVehicle, vehicle, attachmentA, attachmentB) + vehicle:setScriptName("notTowingA_Trailer") + local args = { + vehicleA = savedFrontVehicle:getId(), + vehicleB = vehicle:getId(), + attachmentA = attachmentA, + attachmentB = 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 + end + sendClientCommand(playerObj, "vehicle", "attachTrailer", args) + if TowBarScheduleAction and TowBarMod and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then + ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, vehicle)) + elseif TowBarMod and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then + TowBarMod.Hook.setVehiclePostAttach(playerObj, vehicle) + 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 + return true +end + +local function processLandtrainPendingRestores() + landtrainReapplyTickCounter = landtrainReapplyTickCounter + 1 + if landtrainReapplyTickCounter < 15 then + 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) + end + end + + for _, vehicleId in ipairs(resolvedVehicleIds) do + landtrainPendingRestoreVehicleIds[vehicleId] = nil + 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 +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) + end + queueLandtrainTowbarRestore(vehicle) + queueLandtrainTowbarRestore(vehicle:getVehicleTowedBy()) + queueLandtrainTowbarRestore(vehicle:getVehicleTowing()) +end + local _landtrainHookPosA = Vector3f.new() local _landtrainHookPosB = Vector3f.new() @@ -123,6 +643,14 @@ local function canTowByLandtrain(vehicleA, vehicleB, attachmentA, attachmentB) return true end + -- Keep first hookup identical to Towbar/vanilla distance checks. + local vehicleALinked = (vehicleA:getVehicleTowing() ~= nil or vehicleA:getVehicleTowedBy() ~= nil) + local vehicleBLinked = (vehicleB:getVehicleTowing() ~= nil or vehicleB:getVehicleTowedBy() ~= nil) + if not vehicleALinked and not vehicleBLinked then + ltLog("canTowByLandtrain fallback=false (unlinked pair) A=" .. vehLabel(vehicleA) .. " B=" .. vehLabel(vehicleB)) + return false + end + -- Vanilla blocks chained towing here; allow only near-identical close-range hookups. local posA = vehicleA:getAttachmentWorldPos(attachmentA, _landtrainHookPosA) local posB = vehicleB:getAttachmentWorldPos(attachmentB, _landtrainHookPosB) @@ -144,11 +672,6 @@ local function canTowByLandtrain(vehicleA, vehicleB, attachmentA, attachmentB) end local function getAttachLabel(vehicleA, vehicleB) - local isLandtrainLink = (vehicleA and (vehicleA:getVehicleTowing() or vehicleA:getVehicleTowedBy())) - or (vehicleB and (vehicleB:getVehicleTowing() or vehicleB:getVehicleTowedBy())) - if isLandtrainLink then - return LANDTRAIN_ATTACH_LABEL - end return getText("UI_Text_Towing_byTowBar") end @@ -163,6 +686,19 @@ local function captureTowbarFrontLink(towingVehicle) ltLog("captureTowbarFrontLink no front link for " .. vehLabel(towingVehicle)) return nil end + if frontVehicle == towingVehicle then + ltLog("captureTowbarFrontLink invalid self front for " .. vehLabel(towingVehicle)) + return nil + end + if towingVehicle:getVehicleTowing() == frontVehicle then + ltLog("captureTowbarFrontLink invalid two-way loop front=" .. vehLabel(frontVehicle) .. " middle=" .. vehLabel(towingVehicle)) + return nil + end + local frontRear = frontVehicle:getVehicleTowing() + if frontRear ~= nil and frontRear ~= towingVehicle then + ltLog("captureTowbarFrontLink invalid front rear mismatch front=" .. vehLabel(frontVehicle) .. " rear=" .. vehLabel(frontRear) .. " middle=" .. vehLabel(towingVehicle)) + return nil + end local link = { frontVehicle = frontVehicle, @@ -186,6 +722,11 @@ local function restoreTowbarFrontLink(playerObj, link) ltLog("restoreTowbarFrontLink invalid captured refs") return end + if frontVehicle == towingVehicle or frontVehicle:getId() == towingVehicle:getId() then + ltLog("restoreTowbarFrontLink rejected self-link for " .. vehLabel(towingVehicle)) + clearLandtrainFrontLinkData(towingVehicle) + return + end if towingVehicle:getVehicleTowedBy() ~= nil then ltLog("restoreTowbarFrontLink not needed; front still connected for middle=" .. vehLabel(towingVehicle)) return @@ -201,6 +742,11 @@ local function restoreTowbarFrontLink(playerObj, link) attachmentA = link.attachmentA, attachmentB = link.attachmentB } + if args.vehicleA == args.vehicleB then + ltLog("restoreTowbarFrontLink blocked self attach args for " .. vehLabel(towingVehicle)) + clearLandtrainFrontLinkData(towingVehicle) + return + end sendClientCommand(playerObj, "vehicle", "attachTrailer", args) ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towingVehicle)) @@ -209,6 +755,7 @@ local function restoreTowbarFrontLink(playerObj, link) frontModData["isTowingByTowBar"] = true towingModData["isTowingByTowBar"] = true towingModData["towed"] = true + storeLandtrainFrontLinkData(frontVehicle, towingVehicle, link.attachmentA, link.attachmentB) frontVehicle:transmitModData() towingVehicle:transmitModData() dumpTowState("restoreTowbarFrontLink after front", frontVehicle) @@ -271,6 +818,100 @@ local function menuHasTowbarAttachSlice(menu) return false end +local function menuHasTowbarDetachSlice(menu) + if menu == nil or menu.slices == 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 + return true + end + end + return false +end + +local function hasTowbarPart(vehicle) + if vehicle == nil then return false end + return vehicle:getPartById("towbar") ~= nil +end + +local function getDetachPairFromVehicle(vehicle) + if vehicle == nil then return nil, nil end + if vehicle:getVehicleTowedBy() ~= nil then + return vehicle:getVehicleTowedBy(), vehicle + end + if vehicle:getVehicleTowing() ~= nil then + return vehicle, vehicle:getVehicleTowing() + end + return nil, nil +end + +local function isLikelyTowbarDetachPair(towingVehicle, towedVehicle) + if towingVehicle == nil or towedVehicle == nil then return false end + + local towingModData = towingVehicle:getModData() + local towedModData = towedVehicle:getModData() + if towingModData ~= nil then + if towingModData["isTowingByTowBar"] == true or towingModData["towed"] == true then + return true + end + end + if towedModData ~= nil then + if towedModData["isTowingByTowBar"] == true + or towedModData["towed"] == true + or towedModData.towBarOriginalScriptName ~= nil + or towedModData.towBarOriginalMass ~= nil + or towedModData.towBarOriginalBrakingForce ~= nil + or towedModData["isChangedTowedAttachment"] == true + or towedModData["towBarChangedAttachmentId"] ~= nil then + return true + end + end + + return hasTowbarPart(towingVehicle) and hasTowbarPart(towedVehicle) +end + +local function getTowbarDetachTargetVehicle(vehicle) + local towingVehicle, towedVehicle = getDetachPairFromVehicle(vehicle) + if isLikelyTowbarDetachPair(towingVehicle, towedVehicle) then + return towedVehicle + end + return nil +end + +local function isTowbarChainVehicleOrNeighbor(vehicle) + if vehicle == nil then return false end + if getTowbarDetachTargetVehicle(vehicle) ~= nil then return true end + if isTowbarManagedVehicle(vehicle) then return true end + + local front = vehicle:getVehicleTowedBy() + local rear = vehicle:getVehicleTowing() + if getTowbarDetachTargetVehicle(front) ~= nil then return true end + if getTowbarDetachTargetVehicle(rear) ~= nil then return true end + if isTowbarManagedVehicle(front) then return true end + if isTowbarManagedVehicle(rear) then return true end + return false +end + +local function addLandtrainUnhookOptionToMenu(playerObj, vehicle) + if playerObj == nil or vehicle == nil then return end + if TowBarMod == nil or TowBarMod.Hook == nil or TowBarMod.Hook.deattachTowBarAction == nil then return end + + local menu = getPlayerRadialMenu(playerObj:getPlayerNum()) + if menu == nil then return end + if menuHasTowbarDetachSlice(menu) then return end + + local towedVehicle = getTowbarDetachTargetVehicle(vehicle) + if towedVehicle == nil then return end + + menu:addSlice( + getText("ContextMenu_Vehicle_DetachTrailer", ISVehicleMenu.getVehicleDisplayName(towedVehicle)), + getTexture("media/textures/tow_bar_detach.png"), + TowBarMod.Hook.deattachTowBarAction, + playerObj, + towedVehicle + ) +end + local function getLandtrainHookTypeVariants(vehicleA, vehicleB) local hookTypeVariants = {} if vehicleA == nil or vehicleB == nil or vehicleA == vehicleB then @@ -279,7 +920,7 @@ local function getLandtrainHookTypeVariants(vehicleA, vehicleB) if canTowByLandtrain(vehicleA, vehicleB, "trailerfront", "trailer") then local hookType = {} - hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. LANDTRAIN_ATTACH_LABEL + hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB) hookType.func = TowBarMod.Hook.attachByTowBarAction hookType.towingVehicle = vehicleB hookType.towedVehicle = vehicleA @@ -287,7 +928,7 @@ local function getLandtrainHookTypeVariants(vehicleA, vehicleB) table.insert(hookTypeVariants, hookType) elseif canTowByLandtrain(vehicleA, vehicleB, "trailer", "trailerfront") then local hookType = {} - hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. LANDTRAIN_ATTACH_LABEL + hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB) hookType.func = TowBarMod.Hook.attachByTowBarAction hookType.towingVehicle = vehicleA hookType.towedVehicle = vehicleB @@ -295,13 +936,17 @@ local function getLandtrainHookTypeVariants(vehicleA, vehicleB) table.insert(hookTypeVariants, hookType) end + ltLog("getLandtrainHookTypeVariants A=" .. vehLabel(vehicleA) .. " B=" .. vehLabel(vehicleB) .. " count=" .. tostring(#hookTypeVariants)) return hookTypeVariants end local function getNearbyLandtrainTargets(mainVehicle) local vehicles = {} local square = mainVehicle and mainVehicle:getSquare() or nil - if square == nil then return vehicles end + if square == nil then + ltLog("getNearbyLandtrainTargets no square for " .. vehLabel(mainVehicle)) + return vehicles + end for y = square:getY() - 6, square:getY() + 6 do for x = square:getX() - 6, square:getX() + 6 do @@ -313,18 +958,24 @@ local function getNearbyLandtrainTargets(mainVehicle) local variants = getLandtrainHookTypeVariants(mainVehicle, obj) if #variants > 0 then table.insert(vehicles, { vehicle = obj, variants = variants }) + ltLog("getNearbyLandtrainTargets candidate main=" .. vehLabel(mainVehicle) .. " target=" .. vehLabel(obj) .. " variants=" .. tostring(#variants)) end end end end end end + ltLog("getNearbyLandtrainTargets total main=" .. vehLabel(mainVehicle) .. " targets=" .. tostring(#vehicles)) return vehicles end local function addLandtrainHookOptionToMenu(playerObj, vehicle) if playerObj == nil or vehicle == nil then return end - if not playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") then return end + if not playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") then + ltLog("addLandtrainHookOptionToMenu no TowBar item for " .. vehLabel(vehicle)) + return + end + ltLog("addLandtrainHookOptionToMenu vehicle=" .. vehLabel(vehicle)) local menu = getPlayerRadialMenu(playerObj:getPlayerNum()) if menu == nil then return end @@ -356,7 +1007,7 @@ local function addLandtrainHookOptionToMenu(playerObj, vehicle) table.insert(vehicleList, entry.vehicle) end menu:addSlice( - LANDTRAIN_ATTACH_LABEL .. "...", + getText("UI_Text_Towing_attach") .. "...", getTexture("media/textures/tow_bar_attach.png"), TowBarMod.UI.showChooseVehicleMenu, playerObj, @@ -364,50 +1015,60 @@ local function addLandtrainHookOptionToMenu(playerObj, vehicle) vehicleList, true ) + ltLog("addLandtrainHookOptionToMenu added chooser with " .. tostring(#vehicleList) .. " targets for " .. vehLabel(vehicle)) end local function installLandtrainTowbarPatch() - if not TowBarMod or not TowBarMod.Utils then - return - end - - if TowBarMod.Utils._landtrainUnlimitedChainsInstalled then - return + if not TowBarMod or not TowBarMod.Utils or not TowBarMod.UI or not TowBarMod.Hook then + ltLog("installLandtrainTowbarPatch waiting for Towbar globals") + return false end -- Override Towbar's single-link limitation so a vehicle can be part of a chain. - function TowBarMod.Utils.getHookTypeVariants(vehicleA, vehicleB, hasTowBar) - local hookTypeVariants = {} - if not hasTowBar then return hookTypeVariants end - if vehicleA == nil or vehicleB == nil then return hookTypeVariants end - if vehicleA == vehicleB then return hookTypeVariants end + if TowBarMod.Utils.getHookTypeVariants ~= TowBarMod.Utils._landtrainGetHookVariantsWrapper then + local hookVariantsWrapper = function(vehicleA, vehicleB, hasTowBar) + local hookTypeVariants = {} + if not hasTowBar then return hookTypeVariants end + if vehicleA == nil or vehicleB == nil then return hookTypeVariants end + if vehicleA == vehicleB then return hookTypeVariants end - -- Allow trailer <-> vehicle and trailer <-> trailer links for landtrains. - if canTowByLandtrain(vehicleA, vehicleB, "trailerfront", "trailer") then - local hookType = {} - hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB) - hookType.func = TowBarMod.Hook.attachByTowBarAction - hookType.towingVehicle = vehicleB - hookType.towedVehicle = vehicleA - hookType.textureName = "tow_bar_icon" - table.insert(hookTypeVariants, hookType) - elseif canTowByLandtrain(vehicleA, vehicleB, "trailer", "trailerfront") then - local hookType = {} - hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB) - hookType.func = TowBarMod.Hook.attachByTowBarAction - hookType.towingVehicle = vehicleA - hookType.towedVehicle = vehicleB - hookType.textureName = "tow_bar_icon" - table.insert(hookTypeVariants, hookType) + -- Allow trailer <-> vehicle and trailer <-> trailer links for landtrains. + if canTowByLandtrain(vehicleA, vehicleB, "trailerfront", "trailer") then + local hookType = {} + hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB) + hookType.func = TowBarMod.Hook.attachByTowBarAction + hookType.towingVehicle = vehicleB + hookType.towedVehicle = vehicleA + hookType.textureName = "tow_bar_icon" + table.insert(hookTypeVariants, hookType) + elseif canTowByLandtrain(vehicleA, vehicleB, "trailer", "trailerfront") then + local hookType = {} + hookType.name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getAttachLabel(vehicleA, vehicleB) + hookType.func = TowBarMod.Hook.attachByTowBarAction + hookType.towingVehicle = vehicleA + hookType.towedVehicle = vehicleB + hookType.textureName = "tow_bar_icon" + table.insert(hookTypeVariants, hookType) + end + + return hookTypeVariants end - return hookTypeVariants + TowBarMod.Utils.getHookTypeVariants = hookVariantsWrapper + TowBarMod.Utils._landtrainGetHookVariantsWrapper = hookVariantsWrapper + ltLog("installLandtrainTowbarPatch patched TowBarMod.Utils.getHookTypeVariants") end -- Keep towbar state valid for middle links in a chain after detach/attach. - local originalPerformAttach = TowBarMod.Hook and TowBarMod.Hook.performAttachTowBar - if originalPerformAttach then - TowBarMod.Hook.performAttachTowBar = function(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) + local originalPerformAttach = TowBarMod.Hook.performAttachTowBar + if originalPerformAttach and originalPerformAttach ~= TowBarMod.Hook._landtrainPerformAttachWrapper then + local performAttachWrapper = function(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) + if towingVehicle == nil or towedVehicle == nil then return end + if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then + ltLog("performAttachTowBar blocked self-link args towing=" .. vehLabel(towingVehicle)) + clearLandtrainFrontLinkData(towedVehicle) + return + end ltLog("performAttachTowBar begin towing=" .. vehLabel(towingVehicle) .. " towed=" .. vehLabel(towedVehicle) .. " attA=" .. tostring(attachmentA) .. " attB=" .. tostring(attachmentB)) dumpTowState("performAttachTowBar pre towing", towingVehicle) @@ -416,6 +1077,7 @@ local function installLandtrainTowbarPatch() originalPerformAttach(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) dumpTowState("performAttachTowBar post-original towing", towingVehicle) dumpTowState("performAttachTowBar post-original towed", towedVehicle) + storeLandtrainFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB) restoreTowbarFrontLink(playerObj, frontLink) queueTowbarFrontLinkRestore(playerObj, frontLink, 12) queueTowbarFrontLinkRestore(playerObj, frontLink, 30) @@ -431,32 +1093,119 @@ local function installLandtrainTowbarPatch() if towedVehicle then refreshTowBarState(towedVehicle:getVehicleTowing()) end + queueLandtrainTowbarRestore(towedVehicle) end + + TowBarMod.Hook.performAttachTowBar = performAttachWrapper + TowBarMod.Hook._landtrainPerformAttachWrapper = performAttachWrapper + ltLog("installLandtrainTowbarPatch patched TowBarMod.Hook.performAttachTowBar") end - local originalPerformDetach = TowBarMod.Hook and TowBarMod.Hook.performDeattachTowBar - if originalPerformDetach then - TowBarMod.Hook.performDeattachTowBar = function(playerObj, towingVehicle, towedVehicle) - originalPerformDetach(playerObj, towingVehicle, towedVehicle) + local function resolveTowbarDetachPair(towingVehicle, towedVehicle) + local resolvedTowing = towingVehicle + local resolvedTowed = nil - setTowBarModelVisibleForVehicle(towedVehicle, false) - refreshTowBarState(towingVehicle) - refreshTowBarState(towedVehicle) - if towingVehicle then - refreshTowBarState(towingVehicle:getVehicleTowedBy()) - refreshTowBarState(towingVehicle:getVehicleTowing()) - end - if towedVehicle then - refreshTowBarState(towedVehicle:getVehicleTowedBy()) - refreshTowBarState(towedVehicle:getVehicleTowing()) + if resolvedTowing ~= nil then + resolvedTowed = resolvedTowing:getVehicleTowing() + end + + if resolvedTowed == nil and towedVehicle ~= nil and towedVehicle:getVehicleTowedBy() ~= nil then + resolvedTowing = towedVehicle:getVehicleTowedBy() + resolvedTowed = towedVehicle + elseif resolvedTowed == nil then + resolvedTowed = towedVehicle + end + + if resolvedTowing == nil and resolvedTowed ~= nil then + resolvedTowing = resolvedTowed:getVehicleTowedBy() + end + + if resolvedTowing ~= nil and resolvedTowed ~= nil and resolvedTowed:getVehicleTowedBy() ~= resolvedTowing then + local directRear = resolvedTowing:getVehicleTowing() + if directRear ~= nil and directRear:getVehicleTowedBy() == resolvedTowing then + resolvedTowed = directRear + else + resolvedTowing = nil + resolvedTowed = nil end end + + return resolvedTowing, resolvedTowed + end + + local originalPerformDetach = TowBarMod.Hook.performDeattachTowBar + if originalPerformDetach and originalPerformDetach ~= TowBarMod.Hook._landtrainPerformDetachWrapper then + local performDetachWrapper = function(playerObj, towingVehicle, towedVehicle) + local resolvedTowingVehicle, resolvedTowedVehicle = resolveTowbarDetachPair(towingVehicle, towedVehicle) + if resolvedTowingVehicle == nil or resolvedTowedVehicle == nil then + resolvedTowingVehicle = towingVehicle + resolvedTowedVehicle = towedVehicle + end + if resolvedTowingVehicle == nil or resolvedTowedVehicle == nil then + return + end + + dumpTowState("performDeattachTowBar pre towing", resolvedTowingVehicle) + dumpTowState("performDeattachTowBar pre towed", resolvedTowedVehicle) + originalPerformDetach(playerObj, resolvedTowingVehicle, resolvedTowedVehicle) + dumpTowState("performDeattachTowBar post-original towing", resolvedTowingVehicle) + dumpTowState("performDeattachTowBar post-original towed", resolvedTowedVehicle) + clearLandtrainFrontLinkData(resolvedTowedVehicle) + + reconcileTowbarSplitAround(resolvedTowingVehicle) + reconcileTowbarSplitAround(resolvedTowedVehicle) + queueTowbarSplitReconcile(playerObj, resolvedTowingVehicle, 12) + queueTowbarSplitReconcile(playerObj, resolvedTowedVehicle, 12) + queueTowbarSplitReconcile(playerObj, resolvedTowingVehicle, 30) + queueTowbarSplitReconcile(playerObj, resolvedTowedVehicle, 30) + + setTowBarModelVisibleForVehicle(resolvedTowedVehicle, false) + refreshTowBarState(resolvedTowingVehicle) + refreshTowBarState(resolvedTowedVehicle) + refreshTowBarState(resolvedTowingVehicle:getVehicleTowedBy()) + refreshTowBarState(resolvedTowingVehicle:getVehicleTowing()) + refreshTowBarState(resolvedTowedVehicle:getVehicleTowedBy()) + refreshTowBarState(resolvedTowedVehicle:getVehicleTowing()) + queueLandtrainTowbarRestore(resolvedTowingVehicle) + queueLandtrainTowbarRestore(resolvedTowedVehicle) + + dumpTowState("performDeattachTowBar post-reconcile towing", resolvedTowingVehicle) + dumpTowState("performDeattachTowBar post-reconcile towed", resolvedTowedVehicle) + end + + TowBarMod.Hook.performDeattachTowBar = performDetachWrapper + TowBarMod.Hook._landtrainPerformDetachWrapper = performDetachWrapper + ltLog("installLandtrainTowbarPatch patched TowBarMod.Hook.performDeattachTowBar") + end + + -- If vanilla detach sneaks into the radial menu, redirect it to Towbar timed detach. + if ISVehicleMenu and ISVehicleMenu.onDetachTrailer and ISVehicleMenu.onDetachTrailer ~= TowBarMod.UI._landtrainDetachRedirectWrapper then + local originalOnDetachTrailer = ISVehicleMenu.onDetachTrailer + local detachRedirectWrapper = function(playerObj, vehicle, ...) + local vehicleToCheck = vehicle + if vehicleToCheck == nil and playerObj and ISVehicleMenu.getVehicleToInteractWith then + vehicleToCheck = ISVehicleMenu.getVehicleToInteractWith(playerObj) + end + + local towbarDetachTarget = getTowbarDetachTargetVehicle(vehicleToCheck) + if playerObj ~= nil and towbarDetachTarget ~= nil + and TowBarMod and TowBarMod.Hook and TowBarMod.Hook.deattachTowBarAction then + TowBarMod.Hook.deattachTowBarAction(playerObj, towbarDetachTarget) + return + end + + return originalOnDetachTrailer(playerObj, vehicle, ...) + end + + ISVehicleMenu.onDetachTrailer = detachRedirectWrapper + TowBarMod.UI._landtrainDetachRedirectWrapper = detachRedirectWrapper + ltLog("installLandtrainTowbarPatch patched ISVehicleMenu.onDetachTrailer") end -- Towbar UI only adds attach when fully unlinked; add attach option for linked vehicles too. - if ISVehicleMenu and ISVehicleMenu.showRadialMenu and not TowBarMod.UI._landtrainShowRadialPatched then + if ISVehicleMenu and ISVehicleMenu.showRadialMenu and ISVehicleMenu.showRadialMenu ~= TowBarMod.UI._landtrainShowRadialWrapper then local originalShowRadialMenu = ISVehicleMenu.showRadialMenu - ISVehicleMenu.showRadialMenu = function(playerObj) + local showRadialWrapper = function(playerObj) originalShowRadialMenu(playerObj) if playerObj == nil or playerObj:getVehicle() then return end @@ -464,20 +1213,93 @@ local function installLandtrainTowbarPatch() if vehicle == nil then return end local menu = getPlayerRadialMenu(playerObj:getPlayerNum()) + ltLog("showRadial vehicle=" .. vehLabel(vehicle) + .. " towing=" .. vehLabel(vehicle:getVehicleTowing()) + .. " towedBy=" .. vehLabel(vehicle:getVehicleTowedBy())) + if (vehicle:getVehicleTowing() or vehicle:getVehicleTowedBy()) and isTowbarChainVehicleOrNeighbor(vehicle) then + if TowBarMod and TowBarMod.UI and TowBarMod.UI.removeDefaultDetachOption then + TowBarMod.UI.removeDefaultDetachOption(playerObj) + end + addLandtrainUnhookOptionToMenu(playerObj, vehicle) + end + if menuHasTowbarAttachSlice(menu) then + ltLog("showRadial attach slice already present for " .. vehLabel(vehicle) .. ", skipping Landtrain add") return end if (vehicle:getVehicleTowing() or vehicle:getVehicleTowedBy()) then addLandtrainHookOptionToMenu(playerObj, vehicle) + else + ltLog("showRadial vehicle not linked, Landtrain attach not injected for " .. vehLabel(vehicle)) end end - TowBarMod.UI._landtrainShowRadialPatched = true + + ISVehicleMenu.showRadialMenu = showRadialWrapper + TowBarMod.UI._landtrainShowRadialWrapper = showRadialWrapper + ltLog("installLandtrainTowbarPatch patched ISVehicleMenu.showRadialMenu") end TowBarMod.Utils._landtrainUnlimitedChainsInstalled = true + return true +end + +local landtrainInstallWatchTicks = 0 +local landtrainInstallReadyLogged = false + +local function isLandtrainRadialPatchActive() + return TowBarMod ~= nil + and TowBarMod.UI ~= nil + and TowBarMod.UI._landtrainShowRadialWrapper ~= nil + and ISVehicleMenu ~= nil + and ISVehicleMenu.showRadialMenu == TowBarMod.UI._landtrainShowRadialWrapper +end + +local function landtrainInstallWatchdogTick() + if landtrainInstallWatchTicks <= 0 then + Events.OnTick.Remove(landtrainInstallWatchdogTick) + if not isLandtrainRadialPatchActive() then + ltInfo("Landtrain install watchdog expired before radial hook was active") + end + return + end + landtrainInstallWatchTicks = landtrainInstallWatchTicks - 1 + + local installed = installLandtrainTowbarPatch() + if installed and isLandtrainRadialPatchActive() then + if not landtrainInstallReadyLogged then + ltInfo("Landtrain Towbar hooks active") + landtrainInstallReadyLogged = true + end + Events.OnTick.Remove(landtrainInstallWatchdogTick) + return + end + + if (landtrainInstallWatchTicks % 120) == 0 then + ltInfo("Landtrain waiting for Towbar hooks. ticksLeft=" .. tostring(landtrainInstallWatchTicks)) + end +end + +local function startLandtrainInstallWatchdog() + landtrainInstallReadyLogged = false + landtrainInstallWatchTicks = 1800 -- 30 seconds at 60fps. + Events.OnTick.Remove(landtrainInstallWatchdogTick) + Events.OnTick.Add(landtrainInstallWatchdogTick) + + local installed = installLandtrainTowbarPatch() + if installed and isLandtrainRadialPatchActive() then + ltInfo("Landtrain Towbar hooks active") + landtrainInstallReadyLogged = true + Events.OnTick.Remove(landtrainInstallWatchdogTick) + else + ltInfo("Landtrain install watchdog started") + end end Events.OnGameBoot.Add(ensureTowAttachmentsForTrailers) -Events.OnGameBoot.Add(installLandtrainTowbarPatch) -Events.OnGameStart.Add(installLandtrainTowbarPatch) +Events.OnGameBoot.Add(startLandtrainInstallWatchdog) +Events.OnGameStart.Add(startLandtrainInstallWatchdog) +Events.OnGameStart.Add(scheduleLandtrainLoadSanity) +Events.OnGameStart.Add(queueLandtrainLoadedTowbarRestores) +Events.OnSpawnVehicleEnd.Add(onLandtrainSpawnVehicleEnd) +Events.OnTick.Add(processLandtrainPendingRestores) diff --git a/42.13/mod.info b/42.13/mod.info index e983b0b..1227f50 100644 --- a/42.13/mod.info +++ b/42.13/mod.info @@ -7,3 +7,5 @@ category=vehicle versionMin=42.13.0 url=https://hudsonriggs.systems modversion=1.0.0 +icon=../common/media/textures/landtrain_icon.png +poster=../common/media/textures/preview.png diff --git a/art/banner.png b/art/banner.png new file mode 100644 index 0000000..7daff6c Binary files /dev/null and b/art/banner.png differ diff --git a/art/banner.psd b/art/banner.psd new file mode 100644 index 0000000..85fc5d9 Binary files /dev/null and b/art/banner.psd differ diff --git a/art/landtrain_icon.png b/art/landtrain_icon.png new file mode 100644 index 0000000..c344641 Binary files /dev/null and b/art/landtrain_icon.png differ diff --git a/art/landtrain_icon.psd b/art/landtrain_icon.psd new file mode 100644 index 0000000..3349423 Binary files /dev/null and b/art/landtrain_icon.psd differ diff --git a/art/preview.png b/art/preview.png new file mode 100644 index 0000000..223859b Binary files /dev/null and b/art/preview.png differ diff --git a/art/preview.psd b/art/preview.psd new file mode 100644 index 0000000..dc0f82c Binary files /dev/null and b/art/preview.psd differ diff --git a/common/media/textures/banner.png b/common/media/textures/banner.png new file mode 100644 index 0000000..7daff6c Binary files /dev/null and b/common/media/textures/banner.png differ diff --git a/common/media/textures/landtrain_icon.png b/common/media/textures/landtrain_icon.png new file mode 100644 index 0000000..c344641 Binary files /dev/null and b/common/media/textures/landtrain_icon.png differ diff --git a/common/media/textures/preview.png b/common/media/textures/preview.png new file mode 100644 index 0000000..223859b Binary files /dev/null and b/common/media/textures/preview.png differ diff --git a/mod.info b/mod.info index e983b0b..f4bd964 100644 --- a/mod.info +++ b/mod.info @@ -7,3 +7,5 @@ category=vehicle versionMin=42.13.0 url=https://hudsonriggs.systems modversion=1.0.0 +icon=common/media/textures/landtrain_icon.png +poster=common/media/textures/preview.png