From d4114eb6c681b1ca458997a966eb0a1d01392215 Mon Sep 17 00:00:00 2001 From: HRiggs Date: Thu, 12 Feb 2026 09:02:55 -0500 Subject: [PATCH] working --- .../Landtrain/LandtrainTowSyncClient.lua | 93 + .../LandtrainTowbarChainsRebuild.lua | 651 ++++++ .../Landtrain/UnlimitedTowbarChains.lua | 1756 +---------------- .../LandtrainTowAttachmentsServer.lua | 77 + .../Landtrain/LandtrainTowSyncServer.lua | 124 ++ README.md | 33 +- tools/check-game-basevehicle.ps1 | 104 + tools/java/BaseVehicleConstraintPatch.java | 69 +- tools/java/LandtrainConstraintAuthHelper.java | 38 + tools/patch-game-basevehicle.ps1 | 62 +- tools/restore-game-basevehicle.ps1 | 42 +- zombie/vehicles/BaseVehicle.class | Bin 282960 -> 283117 bytes .../LandtrainConstraintAuthHelper.class | Bin 0 -> 763 bytes 13 files changed, 1249 insertions(+), 1800 deletions(-) create mode 100644 42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua create mode 100644 42.13/media/lua/client/Landtrain/LandtrainTowbarChainsRebuild.lua create mode 100644 42.13/media/lua/server/Landtrain/LandtrainTowAttachmentsServer.lua create mode 100644 42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua create mode 100644 tools/check-game-basevehicle.ps1 create mode 100644 tools/java/LandtrainConstraintAuthHelper.java create mode 100644 zombie/vehicles/LandtrainConstraintAuthHelper.class diff --git a/42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua b/42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua new file mode 100644 index 0000000..d533793 --- /dev/null +++ b/42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua @@ -0,0 +1,93 @@ +if isServer() then return end + +if not TowBarMod then TowBarMod = {} end +TowBarMod.Landtrain = TowBarMod.Landtrain or {} +if TowBarMod.Landtrain._towSyncClientLoaded then return end +TowBarMod.Landtrain._towSyncClientLoaded = true + +local function log(msg) + print("[Landtrain][TowSyncClient] " .. tostring(msg)) +end + +local function ensureAttachment(vehicle, attachmentId) + if not vehicle or not attachmentId then return false end + + local script = vehicle:getScript() + if not script then return false end + if script:getAttachmentById(attachmentId) ~= nil then return true end + + local wheelCount = script:getWheelCount() + local yOffset = -0.5 + if wheelCount > 0 then + local wheel = script:getWheel(0) + if wheel and wheel:getOffset() then + yOffset = wheel:getOffset():y() + 0.1 + end + end + + local chassis = script:getPhysicsChassisShape() + if not chassis then return false end + + local attach = ModelAttachment.new(attachmentId) + if attachmentId == "trailer" then + attach:getOffset():set(0, yOffset, -chassis:z() / 2 - 0.1) + attach:setZOffset(-1) + else + attach:getOffset():set(0, yOffset, chassis:z() / 2 + 0.1) + attach:setZOffset(1) + end + script:addAttachment(attach) + return true +end + +local function resolveVehicle(id) + if not id then return nil end + return getVehicleById(id) +end + +local function applyAttachSync(args) + if not args then return end + + local vehicleA = resolveVehicle(args.vehicleA) + local vehicleB = resolveVehicle(args.vehicleB) + if not vehicleA or not vehicleB then return end + + if vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA then + return + end + + local attachmentA = args.attachmentA or "trailer" + local attachmentB = args.attachmentB or "trailerfront" + if not ensureAttachment(vehicleA, attachmentA) or not ensureAttachment(vehicleB, attachmentB) then + log("attach sync skipped: missing attachment A=" .. tostring(attachmentA) .. " B=" .. tostring(attachmentB)) + return + end + + vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB) +end + +local function safeBreak(vehicle) + if not vehicle then return end + if vehicle:getVehicleTowing() == nil and vehicle:getVehicleTowedBy() == nil then return end + vehicle:breakConstraint(true, true) +end + +local function applyDetachSync(args) + if not args then return end + safeBreak(resolveVehicle(args.vehicleA)) + safeBreak(resolveVehicle(args.vehicleB)) +end + +local function onServerCommand(module, command, args) + if module ~= "landtrain" then return end + + if command == "forceAttachSync" then + applyAttachSync(args) + elseif command == "forceDetachSync" then + applyDetachSync(args) + end +end + +Events.OnServerCommand.Add(onServerCommand) + +log("LandtrainTowSyncClient loaded") diff --git a/42.13/media/lua/client/Landtrain/LandtrainTowbarChainsRebuild.lua b/42.13/media/lua/client/Landtrain/LandtrainTowbarChainsRebuild.lua new file mode 100644 index 0000000..719ee10 --- /dev/null +++ b/42.13/media/lua/client/Landtrain/LandtrainTowbarChainsRebuild.lua @@ -0,0 +1,651 @@ +if not TowBarMod then TowBarMod = {} end +TowBarMod.Landtrain = TowBarMod.Landtrain or {} +if TowBarMod.Landtrain._rebuildLoaded then return end +TowBarMod.Landtrain._rebuildLoaded = true + +TowBarMod.Utils = TowBarMod.Utils or {} +TowBarMod.UI = TowBarMod.UI or {} +TowBarMod.Hook = TowBarMod.Hook or {} + +local LT = TowBarMod.Landtrain +local MAX_DIST_SQ = 3.25 +local MAX_DZ = 0.9 +local tmpA = Vector3f.new() +local tmpB = Vector3f.new() + +local function log(msg) + print("[Landtrain] " .. tostring(msg)) + if type(writeLog) == "function" then + pcall(writeLog, "Landtrain", "[Landtrain] " .. tostring(msg)) + end +end + +local function hasAttachment(vehicle, attachmentId) + if vehicle == nil or attachmentId == nil then return false end + local script = vehicle:getScript() + return script ~= nil and script:getAttachmentById(attachmentId) ~= nil +end + +local function sideFree(vehicle, attachmentId) + if vehicle == nil then return false end + if attachmentId == "trailer" then return vehicle:getVehicleTowing() == nil end + if attachmentId == "trailerfront" then return vehicle:getVehicleTowedBy() == nil end + return true +end + +local function wouldCreateTowLoop(towingVehicle, towedVehicle) + if towingVehicle == nil or towedVehicle == nil then return false end + if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then return true end + + local visited = {} + local cursor = towedVehicle + while cursor do + local id = cursor:getId() + if visited[id] then return true end + if id == towingVehicle:getId() then return true end + visited[id] = true + cursor = cursor:getVehicleTowing() + end + + visited = {} + cursor = towingVehicle + while cursor do + local id = cursor:getId() + if visited[id] then return true end + if id == towedVehicle:getId() then return true end + visited[id] = true + cursor = cursor:getVehicleTowedBy() + end + + return false +end + +local function closeEnoughFallback(vehicleA, vehicleB, attachmentA, attachmentB) + local posA = vehicleA:getAttachmentWorldPos(attachmentA, tmpA) + local posB = vehicleB:getAttachmentWorldPos(attachmentB, tmpB) + if not posA or not posB then return false end + local dx = posA:x() - posB:x() + local dy = posA:y() - posB:y() + local dz = posA:z() - posB:z() + local distSq = dx * dx + dy * dy + dz * dz + return distSq <= MAX_DIST_SQ and math.abs(dz) <= MAX_DZ +end + +local function canTowByLandtrain(vehicleA, vehicleB, attachmentA, attachmentB, allowOccupiedSides) + if not vehicleA or not vehicleB or vehicleA == vehicleB then return false end + if wouldCreateTowLoop(vehicleA, vehicleB) then return false end + if not hasAttachment(vehicleA, attachmentA) or not hasAttachment(vehicleB, attachmentB) then return false end + if not allowOccupiedSides and (not sideFree(vehicleA, attachmentA) or not sideFree(vehicleB, attachmentB)) then return false end + if vehicleA:getVehicleTowing() == vehicleB or vehicleA:getVehicleTowedBy() == vehicleB then return false end + + if vehicleA:canAttachTrailer(vehicleB, attachmentA, attachmentB) then return true end + + local eitherLinked = vehicleA:getVehicleTowing() ~= nil or vehicleA:getVehicleTowedBy() ~= nil + or vehicleB:getVehicleTowing() ~= nil or vehicleB:getVehicleTowedBy() ~= nil + return eitherLinked and closeEnoughFallback(vehicleA, vehicleB, attachmentA, attachmentB) +end + +local function resolvePair(towingVehicle, towedVehicle, preferredA, preferredB) + local pairs = { + { preferredA, preferredB }, + { towingVehicle and towingVehicle:getTowAttachmentSelf() or nil, towedVehicle and towedVehicle:getTowAttachmentSelf() or nil }, + { "trailer", "trailerfront" }, + { "trailerfront", "trailer" } + } + + for _, pair in ipairs(pairs) do + local a, b = pair[1], pair[2] + if a and b and a ~= b and canTowByLandtrain(towingVehicle, towedVehicle, a, b, true) then + return a, b + end + end + + for _, pair in ipairs(pairs) do + local a, b = pair[1], pair[2] + if a and b and a ~= b and hasAttachment(towingVehicle, a) and hasAttachment(towedVehicle, b) then + return a, b + end + end + + return "trailer", "trailerfront" +end + +local function clearChangedOffset(vehicle) + if not vehicle then return end + local md = vehicle:getModData() + if not md or md["isChangedTowedAttachment"] ~= true then return end + + local script = vehicle:getScript() + local changedId = md["towBarChangedAttachmentId"] or vehicle:getTowAttachmentSelf() + local attachment = script and script:getAttachmentById(changedId) or nil + if attachment then + local offset = attachment:getOffset() + local storedShift = tonumber(md["towBarChangedOffsetZShift"]) + if storedShift ~= nil then + attachment:getOffset():set(offset:x(), offset:y(), offset:z() - storedShift) + else + local zShift = offset:z() > 0 and -1 or 1 + attachment:getOffset():set(offset:x(), offset:y(), offset:z() + zShift) + end + end + + md["isChangedTowedAttachment"] = false + md["towBarChangedAttachmentId"] = nil + md["towBarChangedOffsetZShift"] = nil + vehicle:transmitModData() +end + +local function applyRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) + if not TowBarMod.Utils or not TowBarMod.Utils.updateAttachmentsForRigidTow then return end + clearChangedOffset(towedVehicle) + TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) +end + +local function setTowBarModelVisible(vehicle, isVisible) + if not vehicle then return end + local part = vehicle:getPartById("towbar") + if part == nil then return end + + for j = 0, 23 do + part:setModelVisible("towbar" .. j, false) + end + + if isVisible then + local script = vehicle:getScript() + if script then + local scale = script:getModelScale() + if scale >= 1.5 and scale <= 2 then + local z = script:getPhysicsChassisShape():z() / 2 - 0.1 + local index = math.floor((z * 2 / 3 - 1) * 10) + if index < 0 then index = 0 end + if index > 23 then index = 23 end + part:setModelVisible("towbar" .. index, true) + end + end + end + + vehicle:doDamageOverlay() +end + +local function refreshTowBarState(vehicle) + if not vehicle then return end + local md = vehicle:getModData() + if not md then return end + + local front = vehicle:getVehicleTowedBy() + local rear = vehicle:getVehicleTowing() + + local isTowed = false + if front then + local frontMd = front:getModData() + if md["towed"] == true or (frontMd and frontMd["isTowingByTowBar"] == true) then + isTowed = true + end + end + + local isTowing = false + if rear then + local rearMd = rear:getModData() + if rearMd and rearMd["towed"] == true and rearMd["isTowingByTowBar"] == true then + isTowing = true + end + end + + md["towed"] = isTowed + md["isTowingByTowBar"] = isTowed or isTowing + vehicle:transmitModData() +end + +local function refreshAround(vehicle) + if not vehicle then return end + refreshTowBarState(vehicle) + refreshTowBarState(vehicle:getVehicleTowedBy()) + refreshTowBarState(vehicle:getVehicleTowing()) +end + +local function restoreDefaultsIfLeadLost(vehicle) + if not vehicle then return end + local md = vehicle:getModData() + if not md then return end + if vehicle:getVehicleTowedBy() ~= nil then return end + if md["towed"] ~= true and md.towBarOriginalMass == nil and md.towBarOriginalBrakingForce == nil then return end + + if md.towBarOriginalScriptName then vehicle:setScriptName(md.towBarOriginalScriptName) end + if md.towBarOriginalMass ~= nil then vehicle:setMass(md.towBarOriginalMass) end + if md.towBarOriginalBrakingForce ~= nil then vehicle:setBrakingForce(md.towBarOriginalBrakingForce) end + vehicle:constraintChanged() + vehicle:updateTotalMass() + + md["towed"] = false + md.towBarOriginalScriptName = nil + md.towBarOriginalMass = nil + md.towBarOriginalBrakingForce = nil + if vehicle:getVehicleTowing() == nil then + md["isTowingByTowBar"] = false + end + vehicle:transmitModData() + + setTowBarModelVisible(vehicle, false) +end + +local function queueAction(playerObj, delay, fn, arg) + if not playerObj or not fn then return end + if TowBarScheduleAction == nil then + fn(playerObj, arg) + return + end + ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, delay or 10, fn, arg)) +end + +local function sendAttach(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) + if not playerObj or not towingVehicle or not towedVehicle then return end + if towingVehicle:getId() == towedVehicle:getId() then return end + + sendClientCommand(playerObj, "vehicle", "attachTrailer", { + vehicleA = towingVehicle:getId(), + vehicleB = towedVehicle:getId(), + attachmentA = attachmentA, + attachmentB = attachmentB + }) + + if TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then + queueAction(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle) + end +end + +local function captureFrontLink(middleVehicle) + if not middleVehicle then return nil end + local frontVehicle = middleVehicle:getVehicleTowedBy() + if not frontVehicle or frontVehicle == middleVehicle then return nil end + local a, b = resolvePair(frontVehicle, middleVehicle) + return { front = frontVehicle, middle = middleVehicle, a = a, b = b } +end + +local function restoreFrontLink(playerObj, link) + if not link then return end + local front = link.front + local middle = link.middle + if not front or not middle then return end + if middle:getVehicleTowedBy() ~= nil then return end + if front == middle or front:getId() == middle:getId() then return end + if wouldCreateTowLoop(front, middle) then return end + + local a, b = resolvePair(front, middle, link.a, link.b) + if not hasAttachment(front, a) or not hasAttachment(middle, b) then return end + + applyRigidTow(front, middle, a, b) + middle:setScriptName("notTowingA_Trailer") + sendAttach(playerObj, front, middle, a, b) + + local frontMd = front:getModData() + local middleMd = middle:getModData() + if frontMd then + frontMd["isTowingByTowBar"] = true + front:transmitModData() + end + if middleMd then + middleMd["isTowingByTowBar"] = true + middleMd["towed"] = true + middle:transmitModData() + end + + setTowBarModelVisible(middle, true) + refreshAround(front) + refreshAround(middle) +end + +local function queueRestoreFront(playerObj, link, delay) + if not playerObj or not link then return end + queueAction(playerObj, delay or 12, function(character, linkArg) + restoreFrontLink(character, linkArg) + end, link) +end + +local function resolveDetachPair(towingVehicle, towedVehicle) + local resolvedTowing = towingVehicle + local resolvedTowed = resolvedTowing and resolvedTowing:getVehicleTowing() or nil + + if resolvedTowed == nil and towedVehicle and towedVehicle:getVehicleTowedBy() then + resolvedTowing = towedVehicle:getVehicleTowedBy() + resolvedTowed = towedVehicle + elseif resolvedTowed == nil then + resolvedTowed = towedVehicle + end + + if resolvedTowing == nil and resolvedTowed then + resolvedTowing = resolvedTowed:getVehicleTowedBy() + end + + if resolvedTowing and resolvedTowed and resolvedTowed:getVehicleTowedBy() ~= resolvedTowing then + local directRear = resolvedTowing:getVehicleTowing() + if directRear and directRear:getVehicleTowedBy() == resolvedTowing then + resolvedTowed = directRear + else + return nil, nil + end + end + + return resolvedTowing, resolvedTowed +end + +local function getDetachAction() + return TowBarMod.Hook and (TowBarMod.Hook.deattachTowBarAction or TowBarMod.Hook.detachTowBarAction) or nil +end + +local function getPerformDetachHook() + return TowBarMod.Hook and (TowBarMod.Hook.performDetachTowBar or TowBarMod.Hook.performDeattachTowBar) or nil +end + +local function getLandtrainHookTypeVariants(vehicleA, vehicleB, hasTowBar) + local variants = {} + if not hasTowBar or not vehicleA or not vehicleB or vehicleA == vehicleB then return variants end + + if canTowByLandtrain(vehicleA, vehicleB, "trailerfront", "trailer", false) then + table.insert(variants, { + name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar"), + func = TowBarMod.Hook.attachByTowBarAction, + towingVehicle = vehicleB, + towedVehicle = vehicleA, + textureName = "tow_bar_icon" + }) + elseif canTowByLandtrain(vehicleA, vehicleB, "trailer", "trailerfront", false) then + table.insert(variants, { + name = getText("UI_Text_Towing_attach") .. "\n" .. ISVehicleMenu.getVehicleDisplayName(vehicleB) .. "\n" .. getText("UI_Text_Towing_byTowBar"), + func = TowBarMod.Hook.attachByTowBarAction, + towingVehicle = vehicleA, + towedVehicle = vehicleB, + textureName = "tow_bar_icon" + }) + end + + return variants +end + +local function menuHasTowbarAttachSlice(menu) + if menu == nil or menu.slices == nil then return false end + local attachAction = TowBarMod.Hook and TowBarMod.Hook.attachByTowBarAction or nil + local chooseAction = 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 == attachAction or command == chooseAction then + return true + end + end + return false +end + +local function menuHasTowbarDetachSlice(menu) + if menu == nil or menu.slices == nil then return false end + local detachAction = getDetachAction() + if detachAction == nil then return false end + + for _, slice in ipairs(menu.slices) do + local command = slice.command and slice.command[1] + if command == detachAction then + return true + end + end + return false +end + +local function getNearbyTargets(mainVehicle) + local targets = {} + local square = mainVehicle and mainVehicle:getSquare() or nil + if square == nil then return targets end + + for y = square:getY() - 6, square:getY() + 6 do + for x = square:getX() - 6, square:getX() + 6 do + local square2 = getCell():getGridSquare(x, y, square:getZ()) + if square2 then + for i = 1, square2:getMovingObjects():size() do + local obj = square2:getMovingObjects():get(i - 1) + if obj and instanceof(obj, "BaseVehicle") and obj ~= mainVehicle then + local variants = getLandtrainHookTypeVariants(mainVehicle, obj, true) + if #variants > 0 then + table.insert(targets, { vehicle = obj, variants = variants }) + end + end + end + end + end + end + + return targets +end + +local function addAttachOptionToMenu(playerObj, vehicle) + if playerObj == nil or vehicle == nil then return end + local inventory = playerObj:getInventory() + if inventory == nil or inventory:getItemFromTypeRecurse("TowBar.TowBar") == nil then return end + + local menu = getPlayerRadialMenu(playerObj:getPlayerNum()) + if menu == nil then return end + + local targets = getNearbyTargets(vehicle) + if #targets == 0 then return end + + if #targets == 1 then + local hookType = targets[1].variants[1] + menu:addSlice(hookType.name, getTexture("media/textures/tow_bar_attach.png"), hookType.func, playerObj, hookType.towingVehicle, hookType.towedVehicle, hookType.towingPoint, hookType.towedPoint) + return + end + + local vehicleList = {} + for _, entry in ipairs(targets) do table.insert(vehicleList, entry.vehicle) end + menu:addSlice(getText("UI_Text_Towing_attach") .. "...", getTexture("media/textures/tow_bar_attach.png"), TowBarMod.UI.showChooseVehicleMenu, playerObj, vehicle, vehicleList, true) +end + +local function addDetachOptionToMenu(playerObj, vehicle) + if playerObj == nil or vehicle == nil then return end + local detachAction = getDetachAction() + if detachAction == nil then return end + + local menu = getPlayerRadialMenu(playerObj:getPlayerNum()) + if menu == nil or menuHasTowbarDetachSlice(menu) then return end + if vehicle:getVehicleTowing() == nil and vehicle:getVehicleTowedBy() == nil then return end + + local target = vehicle:getVehicleTowing() or vehicle + menu:addSlice(getText("ContextMenu_Vehicle_DetachTrailer", ISVehicleMenu.getVehicleDisplayName(target)), getTexture("media/textures/tow_bar_detach.png"), detachAction, playerObj, target) +end + +LT.getHookTypeVariantsWrapper = function(vehicleA, vehicleB, hasTowBar) + return getLandtrainHookTypeVariants(vehicleA, vehicleB, hasTowBar) +end + +LT.performAttachWrapper = function(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) + local original = TowBarMod.Hook and TowBarMod.Hook._ltOriginalPerformAttach or nil + if original == nil or towingVehicle == nil or towedVehicle == nil then return end + if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then return end + if wouldCreateTowLoop(towingVehicle, towedVehicle) then return end + + local frontLink = captureFrontLink(towingVehicle) + original(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) + + restoreFrontLink(playerObj, frontLink) + queueRestoreFront(playerObj, frontLink, 12) + queueRestoreFront(playerObj, frontLink, 30) + + setTowBarModelVisible(towedVehicle, true) + refreshAround(towingVehicle) + refreshAround(towedVehicle) +end + +LT.performDetachWrapper = function(playerObj, towingVehicle, towedVehicle) + local original = TowBarMod.Hook and TowBarMod.Hook._ltOriginalPerformDetach or nil + if original == nil then return end + + local resolvedTowing, resolvedTowed = resolveDetachPair(towingVehicle, towedVehicle) + if resolvedTowing == nil or resolvedTowed == nil then return end + + local frontLink = captureFrontLink(resolvedTowing) + original(playerObj, resolvedTowing, resolvedTowed) + + restoreFrontLink(playerObj, frontLink) + queueRestoreFront(playerObj, frontLink, 12) + queueRestoreFront(playerObj, frontLink, 30) + + restoreDefaultsIfLeadLost(resolvedTowing) + restoreDefaultsIfLeadLost(resolvedTowed) + refreshAround(resolvedTowing) + refreshAround(resolvedTowed) + + queueAction(playerObj, 12, function(_, v) restoreDefaultsIfLeadLost(v); refreshAround(v) end, resolvedTowing) + queueAction(playerObj, 12, function(_, v) restoreDefaultsIfLeadLost(v); refreshAround(v) end, resolvedTowed) + queueAction(playerObj, 30, function(_, v) restoreDefaultsIfLeadLost(v); refreshAround(v) end, resolvedTowing) + queueAction(playerObj, 30, function(_, v) restoreDefaultsIfLeadLost(v); refreshAround(v) end, resolvedTowed) + + setTowBarModelVisible(resolvedTowed, false) +end + +LT.showRadialWrapper = function(playerObj) + local original = TowBarMod.UI and TowBarMod.UI._ltOriginalShowRadial or nil + if original then original(playerObj) end + + if playerObj == nil or playerObj:getVehicle() then return end + if ISVehicleMenu == nil or ISVehicleMenu.getVehicleToInteractWith == nil then return end + + local vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj) + if vehicle == nil then return end + + local linked = vehicle:getVehicleTowing() ~= nil or vehicle:getVehicleTowedBy() ~= nil + if not linked then return end + + if TowBarMod.UI and TowBarMod.UI.removeDefaultDetachOption then + TowBarMod.UI.removeDefaultDetachOption(playerObj) + end + addDetachOptionToMenu(playerObj, vehicle) + + local menu = getPlayerRadialMenu(playerObj:getPlayerNum()) + if menu and not menuHasTowbarAttachSlice(menu) then + addAttachOptionToMenu(playerObj, vehicle) + end +end + +LT.onDetachTrailerWrapper = function(playerObj, vehicle, ...) + local original = TowBarMod.UI and TowBarMod.UI._ltOriginalOnDetachTrailer or nil + local detachAction = getDetachAction() + + local vehicleToCheck = vehicle + if vehicleToCheck == nil and playerObj and ISVehicleMenu and ISVehicleMenu.getVehicleToInteractWith then + vehicleToCheck = ISVehicleMenu.getVehicleToInteractWith(playerObj) + end + + if playerObj and vehicleToCheck and detachAction then + if vehicleToCheck:getVehicleTowing() or vehicleToCheck:getVehicleTowedBy() then + detachAction(playerObj, vehicleToCheck) + return + end + end + + if original then + return original(playerObj, vehicle, ...) + end +end + +local function ensureTrailerAttachments() + local sm = getScriptManager() + if sm == nil then return end + local scripts = sm:getAllVehicleScripts() + if scripts == nil then return end + + for i = 0, scripts:size() - 1 do + local script = scripts:get(i) + local name = script and script:getName() or nil + if script and name and string.match(string.lower(name), "trailer") then + local wheelCount = script:getWheelCount() + local yOffset = -0.5 + if wheelCount > 0 then + yOffset = script:getWheel(0):getOffset():y() + 0.1 + end + + if script:getAttachmentById("trailer") == nil then + local rearAttach = ModelAttachment.new("trailer") + rearAttach:getOffset():set(0, yOffset, -script:getPhysicsChassisShape():z() / 2 - 0.1) + rearAttach:setZOffset(-1) + script:addAttachment(rearAttach) + end + + if script:getAttachmentById("trailerfront") == nil then + local frontAttach = ModelAttachment.new("trailerfront") + frontAttach:getOffset():set(0, yOffset, script:getPhysicsChassisShape():z() / 2 + 0.1) + frontAttach:setZOffset(1) + script:addAttachment(frontAttach) + end + end + end +end + +local function installPatch() + if TowBarMod.Utils == nil or TowBarMod.UI == nil or TowBarMod.Hook == nil then return false end + if TowBarMod.Hook.attachByTowBarAction == nil or ISVehicleMenu == nil then return false end + if TowBarMod.Hook.performAttachTowBar == nil then return false end + if getPerformDetachHook() == nil then return false end + + if TowBarMod.Utils._ltOriginalGetHookTypeVariants == nil then + TowBarMod.Utils._ltOriginalGetHookTypeVariants = TowBarMod.Utils.getHookTypeVariants + end + TowBarMod.Utils.getHookTypeVariants = LT.getHookTypeVariantsWrapper + + if TowBarMod.Hook._ltOriginalPerformAttach == nil then + TowBarMod.Hook._ltOriginalPerformAttach = TowBarMod.Hook.performAttachTowBar + end + if TowBarMod.Hook._ltOriginalPerformDetach == nil then + TowBarMod.Hook._ltOriginalPerformDetach = getPerformDetachHook() + end + if TowBarMod.Hook._ltOriginalPerformAttach == nil or TowBarMod.Hook._ltOriginalPerformDetach == nil then + return false + end + + TowBarMod.Hook.performAttachTowBar = LT.performAttachWrapper + TowBarMod.Hook.performDetachTowBar = LT.performDetachWrapper + TowBarMod.Hook.performDeattachTowBar = LT.performDetachWrapper + + if TowBarMod.UI._ltOriginalShowRadial == nil then + TowBarMod.UI._ltOriginalShowRadial = ISVehicleMenu.showRadialMenu + end + ISVehicleMenu.showRadialMenu = LT.showRadialWrapper + + if TowBarMod.UI._ltOriginalOnDetachTrailer == nil then + TowBarMod.UI._ltOriginalOnDetachTrailer = ISVehicleMenu.onDetachTrailer + end + ISVehicleMenu.onDetachTrailer = LT.onDetachTrailerWrapper + + TowBarMod.Utils._landtrainUnlimitedChainsInstalled = true + return true +end + +local watchTicks = 0 +local readyLogged = false + +local function watchdogTick() + if watchTicks <= 0 then + Events.OnTick.Remove(watchdogTick) + return + end + watchTicks = watchTicks - 1 + + if installPatch() then + if not readyLogged then + log("Towbar chain hooks active") + readyLogged = true + end + Events.OnTick.Remove(watchdogTick) + end +end + +local function startWatchdog() + readyLogged = false + watchTicks = 1800 + Events.OnTick.Remove(watchdogTick) + Events.OnTick.Add(watchdogTick) + installPatch() +end + +Events.OnGameBoot.Add(ensureTrailerAttachments) +Events.OnGameBoot.Add(startWatchdog) +Events.OnGameStart.Add(startWatchdog) + +log("LandtrainTowbarChainsRebuild loaded") + diff --git a/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua b/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua index c6443fa..10043fa 100644 --- a/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua +++ b/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua @@ -1,1754 +1,2 @@ -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 = 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_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 LANDTRAIN_ATTACHMENT_BASE_OFFSETS = {} - -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 - emitLandtrainLog("[Landtrain] " .. tostring(msg)) -end - -local function ltInfo(msg) - emitLandtrainLog("[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" - 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 isUsableAttachmentPair(towingVehicle, towedVehicle, attachmentA, attachmentB) - if attachmentA == nil or attachmentB == nil or attachmentA == attachmentB then - return false - end - return hasAttachmentById(towingVehicle, attachmentA) and hasAttachmentById(towedVehicle, attachmentB) -end - -local function getAttachmentBaseOffsetKey(script, attachmentId) - if script == nil or attachmentId == nil then return nil end - local scriptName = script.getName and script:getName() or tostring(script) - return tostring(scriptName) .. "|" .. tostring(attachmentId) -end - -local function cacheAttachmentBaseOffset(script, attachmentId) - local key = getAttachmentBaseOffsetKey(script, attachmentId) - if key == nil or LANDTRAIN_ATTACHMENT_BASE_OFFSETS[key] ~= nil then - return - end - local attachment = script and script:getAttachmentById(attachmentId) or nil - if attachment == nil then - return - end - local offset = attachment:getOffset() - LANDTRAIN_ATTACHMENT_BASE_OFFSETS[key] = { - x = offset:x(), - y = offset:y(), - z = offset:z() - } -end - -local function getAttachmentBaseOffset(script, attachmentId) - local key = getAttachmentBaseOffsetKey(script, attachmentId) - if key == nil then return nil end - return LANDTRAIN_ATTACHMENT_BASE_OFFSETS[key] -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) - if isUsableAttachmentPair(towingVehicle, towedVehicle, preferredA, preferredB) then - return preferredA, preferredB - end - - -- Match Towbar's load reapply pair selection before Landtrain-specific fallbacks. - local towbarA = (towingVehicle and towingVehicle:getTowAttachmentSelf()) or "trailer" - local towbarB = (towedVehicle and towedVehicle:getTowAttachmentSelf()) or "trailerfront" - if isUsableAttachmentPair(towingVehicle, towedVehicle, towbarA, towbarB) then - return towbarA, towbarB - end - - local styleA, styleB = getTowbarStyleAttachmentPair(towingVehicle, towedVehicle) - if isUsableAttachmentPair(towingVehicle, towedVehicle, styleA, styleB) then - return styleA, styleB - end - - return chooseLandtrainAttachmentPair(towingVehicle, towedVehicle, preferredA, preferredB) -end - -local function clearChangedTowedAttachmentOffset(vehicle) - if vehicle == nil then return end - local modData = vehicle:getModData() - if modData == nil or modData["isChangedTowedAttachment"] ~= true then return end - - local script = vehicle:getScript() - local changedAttachmentId = modData["towBarChangedAttachmentId"] - if changedAttachmentId == nil and vehicle.getTowAttachmentSelf then - changedAttachmentId = vehicle:getTowAttachmentSelf() - end - - if script ~= nil and changedAttachmentId ~= nil then - cacheAttachmentBaseOffset(script, changedAttachmentId) - local baseOffset = getAttachmentBaseOffset(script, changedAttachmentId) - local towedAttachment = script:getAttachmentById(changedAttachmentId) - if towedAttachment ~= nil then - if baseOffset ~= nil then - towedAttachment:getOffset():set(baseOffset.x, baseOffset.y, baseOffset.z) - else - 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 - - modData["isChangedTowedAttachment"] = false - modData["towBarChangedAttachmentId"] = nil - modData["towBarChangedOffsetZShift"] = nil - vehicle:transmitModData() -end - -local function updateAttachmentsForRigidTowNoStack(towingVehicle, towedVehicle, attachmentA, attachmentB) - if TowBarMod == nil or TowBarMod.Utils == nil or TowBarMod.Utils.updateAttachmentsForRigidTow == nil then - return false - end - local towedScript = towedVehicle and towedVehicle:getScript() or nil - cacheAttachmentBaseOffset(towedScript, attachmentB) - clearChangedTowedAttachmentOffset(towedVehicle) - TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) - return true -end - -local function dumpTowState(prefix, vehicle) - if not LANDTRAIN_DEBUG then return end - if vehicle == nil then - ltLog(prefix .. " vehicle=nil") - return - end - local modData = vehicle:getModData() or {} - local front = vehicle:getVehicleTowedBy() - local rear = vehicle:getVehicleTowing() - ltLog(prefix - .. " v=" .. vehLabel(vehicle) - .. " front=" .. vehLabel(front) - .. " rear=" .. vehLabel(rear) - .. " md.isTowingByTowBar=" .. tostring(modData["isTowingByTowBar"]) - .. " md.towed=" .. tostring(modData["towed"])) -end - -local function refreshTowBarState(vehicle) - if vehicle == nil then return end - - local modData = vehicle:getModData() - if modData == nil then return end - - local frontVehicle = vehicle:getVehicleTowedBy() - local rearVehicle = vehicle:getVehicleTowing() - - local isTowedByTowBar = false - if frontVehicle ~= nil then - local frontModData = frontVehicle:getModData() - if modData["towed"] == true or (frontModData and frontModData["isTowingByTowBar"] == true) then - isTowedByTowBar = true - end - end - - local isTowingByTowBar = false - if rearVehicle ~= nil then - local rearModData = rearVehicle:getModData() - if rearModData and rearModData["towed"] == true and rearModData["isTowingByTowBar"] == true then - isTowingByTowBar = true - end - end - - modData["towed"] = isTowedByTowBar - modData["isTowingByTowBar"] = (isTowedByTowBar or isTowingByTowBar) - vehicle:transmitModData() -end - -local function setTowBarModelVisibleForVehicle(vehicle, visible) - if vehicle == nil then return end - local part = vehicle:getPartById("towbar") - if part == nil then return end - - for j = 0, 23 do - part:setModelVisible("towbar" .. j, false) - end - - if visible then - local z = vehicle:getScript():getPhysicsChassisShape():z() / 2 - 0.1 - local index = math.floor((z * 2 / 3 - 1) * 10) - if index < 0 then index = 0 end - if index > 23 then index = 23 end - part:setModelVisible("towbar" .. index, true) - end - - 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 towingModData = towingVehicle:getModData() - local towedModData = towedVehicle:getModData() - 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_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 - 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 preferredA = attachmentA or towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] - local preferredB = attachmentB or towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] - local resolvedA, resolvedB = chooseLandtrainTowbarPair(towingVehicle, towedVehicle, preferredA, preferredB) - towedModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] = resolvedA - towedModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] = resolvedB - towedVehicle:transmitModData() -end - -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_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 - or modData[LANDTRAIN_REAR_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_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) - 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"] = (modData["towed"] == true) 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 - local towingVehicle = vehicle - local towedVehicle = vehicle:getVehicleTowing() - if towedVehicle == nil then - local frontVehicle = vehicle:getVehicleTowedBy() - if frontVehicle ~= nil then - towingVehicle = frontVehicle - towedVehicle = vehicle - end - end - - local detachCommand = "detachTowBar" - if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.performDetachTowBar == nil then - detachCommand = "detachConstraint" - end - - local args = {} - if towingVehicle ~= nil then - args.towingVehicle = towingVehicle:getId() - end - if towedVehicle ~= nil then - args.vehicle = towedVehicle:getId() - else - args.vehicle = vehicle:getId() - end - - sendClientCommand(playerObj, "towbar", detachCommand, args) - 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 vehicles 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 - - return vehicles -end - -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 - -local function saveLandtrainVehiclePosition(vehicle) - if vehicle == nil then return false end - local modData = vehicle:getModData() - if modData == nil then return false end - - 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 - -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 false end - - 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 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 - - 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 - - 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() - updateAttachmentsForRigidTowNoStack(frontVehicle, rearVehicle, link.attachmentA, link.attachmentB) - rearVehicle:setScriptName("notTowingA_Trailer") - local args = { - vehicleA = frontVehicle:getId(), - vehicleB = rearVehicle:getId(), - attachmentA = link.attachmentA, - attachmentB = link.attachmentB - } - if args.vehicleA == args.vehicleB then - 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, delayTicks, TowBarMod.Hook.setVehiclePostAttach, rearVehicle)) - elseif TowBarMod and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then - TowBarMod.Hook.setVehiclePostAttach(playerObj, rearVehicle) - end - if TowBarMod and TowBarMod.Hook and TowBarMod.Hook.markReapplied then - TowBarMod.Hook.markReapplied(rearVehicle) - end - - setTowBarModelVisibleForVehicle(rearVehicle, true) - refreshTowBarState(frontVehicle) - refreshTowBarState(rearVehicle) - return true -end - -local function runLandtrainSingleLoadRestorePass() - local vehicles = sanitizeLoadedTowLinks() - if vehicles == nil or #vehicles == 0 then - ltInfo("Landtrain load rebuild: no loaded vehicles") - return - end - - 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 - - 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 - - saveActiveLandtrainTowbarSnapshot(vehicles) - ltInfo("Landtrain load rebuild complete moved=" .. tostring(movedVehicles) - .. " detached=" .. tostring(detachedCount) - .. " attached=" .. tostring(attachedCount) - .. " links=" .. tostring(#links)) -end - -local landtrainLoadRebuildDone = false -local landtrainSaveSnapshotTickCounter = 0 -local function onLandtrainSaveSnapshotTick() - if not landtrainLoadRebuildDone then - return - end - 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() -local _landtrainHookPosB = Vector3f.new() - -local function hasTowAttachment(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 isAttachmentSideFree(vehicle, attachmentId) - if vehicle == nil then return false end - if attachmentId == "trailer" then - return vehicle:getVehicleTowing() == nil - end - if attachmentId == "trailerfront" then - return vehicle:getVehicleTowedBy() == nil - end - return true -end - -local function canTowByLandtrain(vehicleA, vehicleB, attachmentA, attachmentB) - if vehicleA == nil or vehicleB == nil then return false end - if vehicleA == vehicleB then return false end - if not hasTowAttachment(vehicleA, attachmentA) then return false end - if not hasTowAttachment(vehicleB, attachmentB) then return false end - if not isAttachmentSideFree(vehicleA, attachmentA) then return false end - if not isAttachmentSideFree(vehicleB, attachmentB) then return false end - if vehicleA:getVehicleTowing() == vehicleB or vehicleA:getVehicleTowedBy() == vehicleB then - return false - end - - -- Keep vanilla behavior when possible. - if vehicleA:canAttachTrailer(vehicleB, attachmentA, attachmentB) then - ltLog("canTowByLandtrain vanilla=true A=" .. vehLabel(vehicleA) .. " B=" .. vehLabel(vehicleB) .. " attA=" .. tostring(attachmentA) .. " attB=" .. tostring(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) - if posA == nil or posB == nil then return false end - - local dx = posA:x() - posB:x() - local dy = posA:y() - posB:y() - local dz = posA:z() - posB:z() - local distSq = dx * dx + dy * dy + dz * dz - local allow = distSq <= LANDTRAIN_FALLBACK_MAX_DIST_SQ and math.abs(dz) <= LANDTRAIN_FALLBACK_MAX_DZ - ltLog("canTowByLandtrain fallback=" .. tostring(allow) - .. " A=" .. vehLabel(vehicleA) - .. " B=" .. vehLabel(vehicleB) - .. " attA=" .. tostring(attachmentA) - .. " attB=" .. tostring(attachmentB) - .. " distSq=" .. string.format("%.3f", distSq) - .. " dz=" .. string.format("%.3f", dz)) - return allow -end - -local function getAttachLabel(vehicleA, vehicleB) - return getText("UI_Text_Towing_byTowBar") -end - -local function captureTowbarFrontLink(towingVehicle) - if towingVehicle == nil then - ltLog("captureTowbarFrontLink towingVehicle=nil") - return nil - end - - local frontVehicle = towingVehicle:getVehicleTowedBy() - if frontVehicle == nil then - 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 towingModData = towingVehicle:getModData() - local savedAttachmentA = towingModData and towingModData[LANDTRAIN_FRONT_ATTACHMENT_A_KEY] or nil - local savedAttachmentB = towingModData and towingModData[LANDTRAIN_FRONT_ATTACHMENT_B_KEY] or nil - local attachmentA, attachmentB = chooseLandtrainTowbarPair(frontVehicle, towingVehicle, savedAttachmentA, savedAttachmentB) - local link = { - frontVehicle = frontVehicle, - towingVehicle = towingVehicle, - attachmentA = attachmentA, - attachmentB = attachmentB - } - ltLog("captureTowbarFrontLink captured front=" .. vehLabel(frontVehicle) .. " middle=" .. vehLabel(towingVehicle) - .. " attA=" .. tostring(link.attachmentA) .. " attB=" .. tostring(link.attachmentB)) - return link -end - -local function restoreTowbarFrontLink(playerObj, link) - if link == nil then - ltLog("restoreTowbarFrontLink skipped (no captured link)") - return - end - local frontVehicle = link.frontVehicle - local towingVehicle = link.towingVehicle - if frontVehicle == nil or towingVehicle == nil then - 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 - end - - ltLog("restoreTowbarFrontLink restoring front=" .. vehLabel(frontVehicle) .. " middle=" .. vehLabel(towingVehicle)) - updateAttachmentsForRigidTowNoStack(frontVehicle, towingVehicle, link.attachmentA, link.attachmentB) - towingVehicle:setScriptName("notTowingA_Trailer") - - local args = { - vehicleA = frontVehicle:getId(), - vehicleB = towingVehicle:getId(), - 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)) - - local frontModData = frontVehicle:getModData() - local towingModData = towingVehicle:getModData() - 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) - dumpTowState("restoreTowbarFrontLink after middle", towingVehicle) -end - -local function queueTowbarFrontLinkRestore(playerObj, link, delayTicks) - if link == nil or playerObj == nil then return end - local delay = delayTicks or 15 - ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, delay, function(character, linkArg) - ltLog("queueTowbarFrontLinkRestore fire delay=" .. tostring(delay) .. " middle=" .. vehLabel(linkArg and linkArg.towingVehicle or nil)) - restoreTowbarFrontLink(character, linkArg) - end, link)) -end - -local function ensureTowAttachmentsForTrailers() - local scriptManager = getScriptManager() - if scriptManager == nil then return end - - local vehicleScripts = scriptManager:getAllVehicleScripts() - if vehicleScripts == nil then return end - - for i = 0, vehicleScripts:size() - 1 do - local script = vehicleScripts:get(i) - local scriptName = script and script:getName() or nil - if script and scriptName and string.match(string.lower(scriptName), "trailer") then - local wheelCount = script:getWheelCount() - local attachHeightOffset = -0.5 - if wheelCount > 0 then - attachHeightOffset = script:getWheel(0):getOffset():y() + 0.1 - end - - local rearTow = script:getAttachmentById("trailer") - if rearTow == nil then - local attach = ModelAttachment.new("trailer") - attach:getOffset():set(0, attachHeightOffset, -script:getPhysicsChassisShape():z() / 2 - 0.1) - attach:setZOffset(-1) - script:addAttachment(attach) - end - - local frontTow = script:getAttachmentById("trailerfront") - if frontTow == nil then - local attach = ModelAttachment.new("trailerfront") - attach:getOffset():set(0, attachHeightOffset, script:getPhysicsChassisShape():z() / 2 + 0.1) - attach:setZOffset(1) - script:addAttachment(attach) - end - end - end -end - -local function captureTowbarAttachmentDefaults() - local scriptManager = getScriptManager() - if scriptManager == nil then return end - - local vehicleScripts = scriptManager:getAllVehicleScripts() - if vehicleScripts == nil then return end - - for i = 0, vehicleScripts:size() - 1 do - local script = vehicleScripts:get(i) - if script ~= nil then - cacheAttachmentBaseOffset(script, "trailer") - cacheAttachmentBaseOffset(script, "trailerfront") - end - end -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 (attachAction ~= nil and command == attachAction) - or (chooseAction ~= nil and command == chooseAction) then - return true - end - end - return false -end - -local function getTowbarDetachAction() - if TowBarMod == nil or TowBarMod.Hook == nil then return nil end - return TowBarMod.Hook.deattachTowBarAction or TowBarMod.Hook.detachTowBarAction -end - -local function getTowbarPerformDetachHook() - if TowBarMod == nil or TowBarMod.Hook == nil then return nil end - return TowBarMod.Hook.performDetachTowBar or TowBarMod.Hook.performDeattachTowBar -end - -local function menuHasTowbarDetachSlice(menu) - if menu == nil or menu.slices == nil then return false end - local detachAction = getTowbarDetachAction() - if detachAction == nil then - return false - end - for _, slice in ipairs(menu.slices) do - local command = slice.command and slice.command[1] - if command == detachAction 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 - local detachAction = getTowbarDetachAction() - if detachAction == 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"), - detachAction, - playerObj, - towedVehicle - ) -end - -local function getLandtrainHookTypeVariants(vehicleA, vehicleB) - local hookTypeVariants = {} - if vehicleA == nil or vehicleB == nil or vehicleA == vehicleB then - return hookTypeVariants - end - - 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 - - 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 - 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 - local square2 = getCell():getGridSquare(x, y, square:getZ()) - if square2 then - for i = 1, square2:getMovingObjects():size() do - local obj = square2:getMovingObjects():get(i - 1) - if obj ~= nil and instanceof(obj, "BaseVehicle") and obj ~= mainVehicle then - 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 - 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 - ltLog("addLandtrainHookOptionToMenu vehicle=" .. vehLabel(vehicle)) - - local menu = getPlayerRadialMenu(playerObj:getPlayerNum()) - if menu == nil then return end - - local targets = getNearbyLandtrainTargets(vehicle) - if #targets == 0 then - ltLog("addLandtrainHookOptionToMenu no nearby valid targets for " .. vehLabel(vehicle)) - return - end - - if #targets == 1 then - local hookType = targets[1].variants[1] - menu:addSlice( - hookType.name, - getTexture("media/textures/tow_bar_attach.png"), - hookType.func, - playerObj, - hookType.towingVehicle, - hookType.towedVehicle, - hookType.towingPoint, - hookType.towedPoint - ) - return - end - - -- Reuse Towbar's chooser UI by passing only the candidate vehicles. - local vehicleList = {} - for _, entry in ipairs(targets) do - table.insert(vehicleList, entry.vehicle) - end - menu:addSlice( - getText("UI_Text_Towing_attach") .. "...", - getTexture("media/textures/tow_bar_attach.png"), - TowBarMod.UI.showChooseVehicleMenu, - 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 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. - 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) - end - - return hookTypeVariants - end - - 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.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) - dumpTowState("performAttachTowBar pre towed", towedVehicle) - local frontLink = captureTowbarFrontLink(towingVehicle) - 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) - dumpTowState("performAttachTowBar post-restore towing", towingVehicle) - dumpTowState("performAttachTowBar post-restore towed", towedVehicle) - - setTowBarModelVisibleForVehicle(towedVehicle, true) - refreshTowBarState(towingVehicle) - refreshTowBarState(towedVehicle) - if towingVehicle then - refreshTowBarState(towingVehicle:getVehicleTowedBy()) - end - if towedVehicle then - refreshTowBarState(towedVehicle:getVehicleTowing()) - end - saveActiveLandtrainTowbarSnapshot() - end - - TowBarMod.Hook.performAttachTowBar = performAttachWrapper - TowBarMod.Hook._landtrainPerformAttachWrapper = performAttachWrapper - ltLog("installLandtrainTowbarPatch patched TowBarMod.Hook.performAttachTowBar") - end - - local function resolveTowbarDetachPair(towingVehicle, towedVehicle) - local resolvedTowing = towingVehicle - local resolvedTowed = nil - - 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 = getTowbarPerformDetachHook() - 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("performDetachTowBar pre towing", resolvedTowingVehicle) - dumpTowState("performDetachTowBar pre towed", resolvedTowedVehicle) - originalPerformDetach(playerObj, resolvedTowingVehicle, resolvedTowedVehicle) - dumpTowState("performDetachTowBar post-original towing", resolvedTowingVehicle) - dumpTowState("performDetachTowBar 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()) - saveActiveLandtrainTowbarSnapshot() - - dumpTowState("performDetachTowBar post-reconcile towing", resolvedTowingVehicle) - dumpTowState("performDetachTowBar post-reconcile towed", resolvedTowedVehicle) - end - - TowBarMod.Hook.performDetachTowBar = performDetachWrapper - TowBarMod.Hook.performDeattachTowBar = performDetachWrapper - TowBarMod.Hook._landtrainPerformDetachWrapper = performDetachWrapper - ltLog("installLandtrainTowbarPatch patched TowBarMod.Hook.performDetachTowBar") - 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) - local detachAction = getTowbarDetachAction() - if playerObj ~= nil and towbarDetachTarget ~= nil and detachAction ~= nil then - detachAction(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 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) - originalShowRadialMenu(playerObj) - - if playerObj == nil or playerObj:getVehicle() then return end - local vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj) - 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 - - 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)) - end - end - - 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(captureTowbarAttachmentDefaults) -Events.OnGameBoot.Add(startLandtrainInstallWatchdog) -Events.OnGameStart.Add(captureTowbarAttachmentDefaults) -Events.OnGameStart.Add(startLandtrainInstallWatchdog) -Events.OnGameStart.Add(scheduleLandtrainSingleLoadRestore) -Events.OnTick.Add(onLandtrainSaveSnapshotTick) +-- Legacy entrypoint kept for compatibility. +require "Landtrain/LandtrainTowbarChainsRebuild" diff --git a/42.13/media/lua/server/Landtrain/LandtrainTowAttachmentsServer.lua b/42.13/media/lua/server/Landtrain/LandtrainTowAttachmentsServer.lua new file mode 100644 index 0000000..5df7828 --- /dev/null +++ b/42.13/media/lua/server/Landtrain/LandtrainTowAttachmentsServer.lua @@ -0,0 +1,77 @@ +if isClient() then return end + +if not TowBarMod then TowBarMod = {} end +TowBarMod.Landtrain = TowBarMod.Landtrain or {} +if TowBarMod.Landtrain._serverTowAttachmentsLoaded then return end +TowBarMod.Landtrain._serverTowAttachmentsLoaded = true + +local function log(msg) + print("[Landtrain][Server] " .. tostring(msg)) +end + +local function addMissingTowAttachments(script) + if not script then return 0 end + + local wheelCount = script:getWheelCount() + local yOffset = -0.5 + if wheelCount > 0 then + local wheel = script:getWheel(0) + if wheel and wheel:getOffset() then + yOffset = wheel:getOffset():y() + 0.1 + end + end + + local chassis = script:getPhysicsChassisShape() + if not chassis then return 0 end + + local changed = 0 + + if script:getAttachmentById("trailer") == nil then + local rearAttach = ModelAttachment.new("trailer") + rearAttach:getOffset():set(0, yOffset, -chassis:z() / 2 - 0.1) + rearAttach:setZOffset(-1) + script:addAttachment(rearAttach) + changed = changed + 1 + end + + if script:getAttachmentById("trailerfront") == nil then + local frontAttach = ModelAttachment.new("trailerfront") + frontAttach:getOffset():set(0, yOffset, chassis:z() / 2 + 0.1) + frontAttach:setZOffset(1) + script:addAttachment(frontAttach) + changed = changed + 1 + end + + return changed +end + +local function ensureTowAttachmentsServer() + local sm = getScriptManager() + if sm == nil then + log("ScriptManager unavailable; skipping tow attachment bootstrap") + return + end + + local scripts = sm:getAllVehicleScripts() + if scripts == nil then + log("Vehicle script list unavailable; skipping tow attachment bootstrap") + return + end + + local patchedScripts = 0 + local addedAttachments = 0 + for i = 0, scripts:size() - 1 do + local script = scripts:get(i) + local added = addMissingTowAttachments(script) + if added > 0 then + patchedScripts = patchedScripts + 1 + addedAttachments = addedAttachments + added + end + end + + log("Tow attachment bootstrap complete: scripts=" .. tostring(patchedScripts) .. ", attachments=" .. tostring(addedAttachments)) +end + +Events.OnGameBoot.Add(ensureTowAttachmentsServer) + +log("LandtrainTowAttachmentsServer loaded") diff --git a/42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua b/42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua new file mode 100644 index 0000000..ddbe444 --- /dev/null +++ b/42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua @@ -0,0 +1,124 @@ +if isClient() then return end + +if not TowBarMod then TowBarMod = {} end +TowBarMod.Landtrain = TowBarMod.Landtrain or {} +if TowBarMod.Landtrain._towSyncServerLoaded then return end +TowBarMod.Landtrain._towSyncServerLoaded = true + +local SYNC_DELAY_TICKS = 2 +local pending = {} + +local function log(msg) + print("[Landtrain][TowSyncServer] " .. tostring(msg)) +end + +local function queueSync(kind, player, args) + if not args then return end + table.insert(pending, { + kind = kind, + ticks = SYNC_DELAY_TICKS, + player = player, + args = args + }) +end + +local function vehicleHasAttachment(vehicle, attachmentId) + if not vehicle or not attachmentId then return false end + local script = vehicle:getScript() + return script ~= nil and script:getAttachmentById(attachmentId) ~= nil +end + +local function isLinked(vehicleA, vehicleB) + if not vehicleA or not vehicleB then return false end + return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA +end + +local function resolveAttachmentA(args, vehicleA) + if args and args.attachmentA then return args.attachmentA end + if vehicleA and vehicleA:getTowAttachmentSelf() then return vehicleA:getTowAttachmentSelf() end + return "trailer" +end + +local function resolveAttachmentB(args, vehicleB) + if args and args.attachmentB then return args.attachmentB end + if vehicleB and vehicleB:getTowAttachmentSelf() then return vehicleB:getTowAttachmentSelf() end + return "trailerfront" +end + +local function processAttach(item) + local args = item.args or {} + local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil + local vehicleB = args.vehicleB and getVehicleById(args.vehicleB) or nil + if not vehicleA or not vehicleB then + log("attach sync skipped: missing vehicle A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB)) + return + end + + local attachmentA = resolveAttachmentA(args, vehicleA) + local attachmentB = resolveAttachmentB(args, vehicleB) + + local linked = isLinked(vehicleA, vehicleB) + if not linked then + if not vehicleHasAttachment(vehicleA, attachmentA) or not vehicleHasAttachment(vehicleB, attachmentB) then + log("attach sync failed: missing attachment A=" .. tostring(attachmentA) .. " B=" .. tostring(attachmentB)) + return + end + + vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB) + linked = isLinked(vehicleA, vehicleB) + if not linked then + log("attach sync failed: server link not established A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())) + return + end + end + + sendServerCommand("landtrain", "forceAttachSync", { + vehicleA = vehicleA:getId(), + vehicleB = vehicleB:getId(), + attachmentA = attachmentA, + attachmentB = attachmentB + }) +end + +local function processDetach(item) + local args = item.args or {} + local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle + local vehicleBId = args.vehicleB + + sendServerCommand("landtrain", "forceDetachSync", { + vehicleA = vehicleAId, + vehicleB = vehicleBId + }) +end + +local function processPending() + if #pending == 0 then return end + + for i = #pending, 1, -1 do + local item = pending[i] + item.ticks = item.ticks - 1 + if item.ticks <= 0 then + if item.kind == "attach" then + processAttach(item) + elseif item.kind == "detach" then + processDetach(item) + end + table.remove(pending, i) + end + end +end + +local function onClientCommand(module, command, player, args) + if module == "vehicle" and command == "attachTrailer" then + queueSync("attach", player, args) + elseif module == "vehicle" and command == "detachTrailer" then + queueSync("detach", player, args) + elseif module == "towbar" and command == "detachTowBar" then + queueSync("detach", player, args) + end +end + +Events.OnClientCommand.Add(onClientCommand) +Events.OnTick.Add(processPending) + +log("LandtrainTowSyncServer loaded") diff --git a/README.md b/README.md index a6218ae..9336441 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,46 @@ Landtrain extends Towbars to support chained towing. ## Important -Project Zomboid base `BaseVehicle.addPointConstraint()` force-breaks existing constraints. -To keep `1 -> 2` while attaching `2 -> 3`, Landtrain includes a Java class override: +Project Zomboid base `BaseVehicle.addPointConstraint()` force-breaks existing constraints and chain state in MP. +To keep `1 -> 2` while attaching `2 -> 3` (and preserve movement replication), Landtrain includes a Java class override: -- `zombie/vehicles/BaseVehicle.class` +- client install: `zombie/vehicles/BaseVehicle.class` +- dedicated install: `java/zombie/vehicles/BaseVehicle.class` +- helper class (both): `LandtrainConstraintAuthHelper.class` in the same `zombie/vehicles` folder This is the same override pattern used by mods like Realistic Car Physics (manual `zombie` folder copy). ## Apply patch to game -1. Run: +1. Patch your local client install: ```powershell .\tools\patch-game-basevehicle.ps1 ``` -2. Ensure both mods are enabled: - - `hrsys_towbars` - - `hrsys_landtrain` +2. Patch the dedicated server install: + +```powershell +.\tools\patch-game-basevehicle.ps1 -GameRoot "D:\SteamLibrary\steamapps\common\Project Zomboid Dedicated Server" +``` + +3. Ensure both mods are enabled: + - `\hrsys_towbars` + - `\hrsys_landtrain` + +## MP requirement + +For multiplayer, every connecting client and the dedicated server must run the patched class. +If only one side is patched, towing links can desync with no explicit Lua error. ## Restore vanilla class ```powershell .\tools\restore-game-basevehicle.ps1 ``` + +To restore dedicated server too: + +```powershell +.\tools\restore-game-basevehicle.ps1 -GameRoot "D:\SteamLibrary\steamapps\common\Project Zomboid Dedicated Server" +``` diff --git a/tools/check-game-basevehicle.ps1 b/tools/check-game-basevehicle.ps1 new file mode 100644 index 0000000..17c6f3b --- /dev/null +++ b/tools/check-game-basevehicle.ps1 @@ -0,0 +1,104 @@ +param( + [string]$GameRoot = "D:\SteamLibrary\steamapps\common\ProjectZomboid" +) + +$ErrorActionPreference = "Stop" + +function Get-Sha256([string]$Path) { + if (-not (Test-Path $Path)) { return $null } + return (Get-FileHash $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Get-JarEntrySha256([string]$JarPath, [string]$EntryName) { + if (-not (Test-Path $JarPath)) { return $null } + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [System.IO.Compression.ZipFile]::OpenRead($JarPath) + try { + $entry = $zip.GetEntry($EntryName) + if ($null -eq $entry) { return $null } + $stream = $entry.Open() + $memory = New-Object System.IO.MemoryStream + try { + $stream.CopyTo($memory) + } finally { + $stream.Close() + } + $bytes = $memory.ToArray() + $sha = [System.Security.Cryptography.SHA256]::Create() + return (($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString("x2") }) -join "") + } finally { + $zip.Dispose() + } +} + +$repoRoot = Split-Path -Parent $PSScriptRoot +$knownPatchedClass = Join-Path $repoRoot "zombie\vehicles\BaseVehicle.class" +$patchedHash = Get-Sha256 $knownPatchedClass +$patchedHashText = if ($null -eq $patchedHash) { "missing" } else { $patchedHash } +$knownHelperClass = Join-Path $repoRoot "zombie\vehicles\LandtrainConstraintAuthHelper.class" +$helperHash = Get-Sha256 $knownHelperClass +$helperHashText = if ($null -eq $helperHash) { "missing" } else { $helperHash } + +$targets = @( + @{ + Name = "Client override path" + Path = Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class" + }, + @{ + Name = "Dedicated override path" + Path = Join-Path $GameRoot "java\zombie\vehicles\BaseVehicle.class" + } +) + +$jarCandidates = @( + (Join-Path $GameRoot "projectzomboid.jar"), + (Join-Path $GameRoot "java\projectzomboid.jar") +) + +Write-Output "GameRoot: $GameRoot" +Write-Output "Known patched hash (repo): $patchedHashText" +Write-Output "Known helper hash (repo): $helperHashText" +Write-Output "" + +foreach ($target in $targets) { + $path = $target.Path + $hash = Get-Sha256 $path + if ($null -eq $hash) { + Write-Output "$($target.Name): MISSING ($path)" + continue + } + + $status = if ($patchedHash -and $hash -eq $patchedHash) { "PATCHED" } else { "NOT_MATCHING_PATCHED_HASH" } + Write-Output "$($target.Name): $status" + Write-Output " path: $path" + Write-Output " hash: $hash" +} + +Write-Output "" +foreach ($helperPath in @( + (Join-Path $GameRoot "zombie\vehicles\LandtrainConstraintAuthHelper.class"), + (Join-Path $GameRoot "java\zombie\vehicles\LandtrainConstraintAuthHelper.class") +) | Select-Object -Unique) { + $hash = Get-Sha256 $helperPath + if ($null -eq $hash) { + Write-Output "Helper class: MISSING ($helperPath)" + continue + } + $status = if ($helperHash -and $hash -eq $helperHash) { "PATCHED" } else { "NOT_MATCHING_HELPER_HASH" } + Write-Output "Helper class: $status" + Write-Output " path: $helperPath" + Write-Output " hash: $hash" +} + +Write-Output "" +foreach ($jar in $jarCandidates | Select-Object -Unique) { + $jarHash = Get-JarEntrySha256 $jar "zombie/vehicles/BaseVehicle.class" + if ($null -eq $jarHash) { + Write-Output "Jar class: MISSING ($jar)" + continue + } + Write-Output "Jar class hash:" + Write-Output " jar: $jar" + Write-Output " hash: $jarHash" +} diff --git a/tools/java/BaseVehicleConstraintPatch.java b/tools/java/BaseVehicleConstraintPatch.java index 445e768..a6a8c34 100644 --- a/tools/java/BaseVehicleConstraintPatch.java +++ b/tools/java/BaseVehicleConstraintPatch.java @@ -3,27 +3,34 @@ import java.nio.file.Path; import java.nio.file.Paths; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.Opcodes; /** - * Patches zombie.vehicles.BaseVehicle so addPointConstraint() no longer force-breaks - * both vehicles before creating a new constraint. + * Patches zombie.vehicles.BaseVehicle for Landtrain chain support: + * 1) remove forced breakConstraint() in addPointConstraint() + * 2) route constraintChanged() driver lookups through helper that handles chain middle vehicles */ public final class BaseVehicleConstraintPatch { private static final String TARGET_NAME = "addPointConstraint"; + private static final String CONSTRAINT_CHANGED_NAME = "constraintChanged"; private static final String CLINIT_NAME = ""; private static final String VOID_NOARG_DESC = "()V"; private static final String PATCH_LOG_LINE = "[Landtrain][BaseVehiclePatch] BaseVehicle override enabled"; private static final String BREAK_DESC_OBJECT_BOOL = "(ZLjava/lang/Boolean;)V"; private static final String BREAK_DESC_PRIMITIVE_BOOL = "(ZZ)V"; private static final String BASE_VEHICLE_OWNER = "zombie/vehicles/BaseVehicle"; + private static final String GET_DRIVER_DESC = "()Lzombie/characters/IsoGameCharacter;"; + private static final String HELPER_OWNER = "zombie/vehicles/LandtrainConstraintAuthHelper"; + private static final String HELPER_METHOD = "resolveConstraintDriver"; + private static final String HELPER_DESC = + "(Lzombie/vehicles/BaseVehicle;)Lzombie/characters/IsoGameCharacter;"; private BaseVehicleConstraintPatch() { } @@ -43,12 +50,16 @@ public final class BaseVehicleConstraintPatch { int removedCalls = 0; int inspectedAddPointMethods = 0; + int patchedConstraintDriverCalls = 0; + for (MethodNode method : classNode.methods) { - if (!TARGET_NAME.equals(method.name) || !isTargetAddPointConstraint(method.desc)) { - continue; + if (TARGET_NAME.equals(method.name) && isTargetAddPointConstraint(method.desc)) { + inspectedAddPointMethods++; + removedCalls += patchAddPointConstraint(method); + } else if (CONSTRAINT_CHANGED_NAME.equals(method.name) + && VOID_NOARG_DESC.equals(method.desc)) { + patchedConstraintDriverCalls += patchConstraintChangedDriverCalls(method); } - inspectedAddPointMethods++; - removedCalls += patchAddPointConstraint(method); } if (removedCalls < 2) { @@ -59,6 +70,11 @@ public final class BaseVehicleConstraintPatch { + inspectedAddPointMethods + ")"); } + if (patchedConstraintDriverCalls < 1) { + throw new IllegalStateException( + "Expected to patch at least 1 constraintChanged getDriver call, patched " + + patchedConstraintDriverCalls); + } if (!ensureClassInitLog(classNode)) { throw new IllegalStateException("Failed to inject BaseVehicle class-init debug log"); } @@ -71,12 +87,12 @@ public final class BaseVehicleConstraintPatch { System.out.println( "Patched BaseVehicle.class; removed breakConstraint calls: " + removedCalls + + ", constraint driver hooks: " + + patchedConstraintDriverCalls + ", class-init debug log: enabled"); } private static boolean isTargetAddPointConstraint(String methodDesc) { - // We only want the 5-arg overload: - // (IsoPlayer, BaseVehicle, String, String, boolean|Boolean) -> void return "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Z)V" .equals(methodDesc) || "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V" @@ -97,9 +113,8 @@ public final class BaseVehicleConstraintPatch { node = next; continue; } - // Keep stack-map frames valid by preserving stack effect: + // breakConstraint(...) consumes objectref + 2 args and returns void. - // Replace invoke with POP2 + POP (consume 3 category-1 stack slots). InsnList replacement = new InsnList(); replacement.add(new InsnNode(Opcodes.POP2)); replacement.add(new InsnNode(Opcodes.POP)); @@ -113,6 +128,36 @@ public final class BaseVehicleConstraintPatch { return patched; } + private static int patchConstraintChangedDriverCalls(MethodNode method) { + int patched = 0; + InsnList insns = method.instructions; + for (AbstractInsnNode node = insns.getFirst(); node != null; ) { + AbstractInsnNode next = node.getNext(); + if (!(node instanceof MethodInsnNode call)) { + node = next; + continue; + } + if (!BASE_VEHICLE_OWNER.equals(call.owner) + || !"getDriver".equals(call.name) + || !GET_DRIVER_DESC.equals(call.desc)) { + node = next; + continue; + } + + MethodInsnNode replacement = + new MethodInsnNode( + Opcodes.INVOKESTATIC, + HELPER_OWNER, + HELPER_METHOD, + HELPER_DESC, + false); + insns.set(call, replacement); + patched++; + node = next; + } + return patched; + } + private static boolean ensureClassInitLog(ClassNode classNode) { MethodNode clinit = null; for (MethodNode method : classNode.methods) { diff --git a/tools/java/LandtrainConstraintAuthHelper.java b/tools/java/LandtrainConstraintAuthHelper.java new file mode 100644 index 0000000..42f156e --- /dev/null +++ b/tools/java/LandtrainConstraintAuthHelper.java @@ -0,0 +1,38 @@ +package zombie.vehicles; + +import zombie.characters.IsoGameCharacter; + +/** + * Resolves the effective driver for constraint auth in chained towing. + * For middle vehicles in a chain, prefer the front/lead driver's authority. + */ +public final class LandtrainConstraintAuthHelper { + private LandtrainConstraintAuthHelper() { + } + + public static IsoGameCharacter resolveConstraintDriver(BaseVehicle vehicle) { + if (vehicle == null) { + return null; + } + + IsoGameCharacter driver = vehicle.getDriver(); + if (driver != null) { + return driver; + } + + BaseVehicle front = vehicle.getVehicleTowedBy(); + if (front != null) { + driver = front.getDriver(); + if (driver != null) { + return driver; + } + } + + BaseVehicle rear = vehicle.getVehicleTowing(); + if (rear != null) { + return rear.getDriver(); + } + + return null; + } +} diff --git a/tools/patch-game-basevehicle.ps1 b/tools/patch-game-basevehicle.ps1 index 48c32eb..7093345 100644 --- a/tools/patch-game-basevehicle.ps1 +++ b/tools/patch-game-basevehicle.ps1 @@ -14,9 +14,27 @@ New-Item -ItemType Directory -Force -Path $classPatchDir | Out-Null New-Item -ItemType Directory -Force -Path $buildDir | Out-Null $javaExe = Join-Path $GameRoot "jre64\bin\java.exe" -$gameJar = Join-Path $GameRoot "projectzomboid.jar" if (-not (Test-Path $javaExe)) { throw "java.exe not found at $javaExe" } -if (-not (Test-Path $gameJar)) { throw "projectzomboid.jar not found at $gameJar" } + +$clientJar = Join-Path $GameRoot "projectzomboid.jar" +$dedicatedJar = Join-Path $GameRoot "java\projectzomboid.jar" +$gameJar = $null +$targetClasses = @() + +if (Test-Path $clientJar) { + $gameJar = $clientJar + $targetClasses += (Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class") +} +if (Test-Path $dedicatedJar) { + $gameJar = $dedicatedJar + $targetClasses += (Join-Path $GameRoot "java\zombie\vehicles\BaseVehicle.class") +} +if ($null -eq $gameJar) { + throw "projectzomboid.jar not found at either $clientJar or $dedicatedJar" +} +if ($targetClasses.Count -eq 0) { + throw "No valid BaseVehicle.class deployment targets found under $GameRoot" +} $ecjJar = Join-Path $toolsDir "ecj.jar" $asmJar = Join-Path $toolsDir "asm.jar" @@ -34,12 +52,15 @@ if (-not (Test-Path $asmTreeJar)) { $patcherSource = Join-Path $PSScriptRoot "java\BaseVehicleConstraintPatch.java" if (-not (Test-Path $patcherSource)) { throw "Missing patcher source: $patcherSource" } +$helperSource = Join-Path $PSScriptRoot "java\LandtrainConstraintAuthHelper.java" +if (-not (Test-Path $helperSource)) { throw "Missing helper source: $helperSource" } -& $javaExe -jar $ecjJar -17 -cp "$asmJar;$asmTreeJar" -d $buildDir $patcherSource -if ($LASTEXITCODE -ne 0) { throw "Failed to compile BaseVehicleConstraintPatch.java" } +& $javaExe -jar $ecjJar -17 -cp "$asmJar;$asmTreeJar;$gameJar" -d $buildDir $patcherSource $helperSource +if ($LASTEXITCODE -ne 0) { throw "Failed to compile BaseVehicleConstraintPatch.java/LandtrainConstraintAuthHelper.java" } $inputClass = Join-Path $classPatchDir "BaseVehicle.original.class" $patchedClass = Join-Path $classPatchDir "BaseVehicle.patched.class" +$helperClass = Join-Path $buildDir "zombie\vehicles\LandtrainConstraintAuthHelper.class" Add-Type -AssemblyName System.IO.Compression.FileSystem $zip = [System.IO.Compression.ZipFile]::OpenRead($gameJar) @@ -61,26 +82,33 @@ try { & $javaExe -cp "$buildDir;$asmJar;$asmTreeJar" BaseVehicleConstraintPatch $inputClass $patchedClass if ($LASTEXITCODE -ne 0) { throw "BaseVehicle class patch failed" } +if (-not (Test-Path $helperClass)) { throw "Missing compiled helper class: $helperClass" } -$targetDir = Join-Path $GameRoot "zombie\vehicles" -$targetClass = Join-Path $targetDir "BaseVehicle.class" -$backupClass = "$targetClass.landtrain.original" +foreach ($targetClass in $targetClasses | Select-Object -Unique) { + $targetDir = Split-Path -Parent $targetClass + $backupClass = "$targetClass.landtrain.original" + $targetHelperClass = Join-Path $targetDir "LandtrainConstraintAuthHelper.class" -New-Item -ItemType Directory -Force -Path $targetDir | Out-Null -if (-not (Test-Path $backupClass)) { - if (Test-Path $targetClass) { - Copy-Item $targetClass $backupClass -Force - } else { - Copy-Item $inputClass $backupClass -Force + New-Item -ItemType Directory -Force -Path $targetDir | Out-Null + if (-not (Test-Path $backupClass)) { + if (Test-Path $targetClass) { + Copy-Item $targetClass $backupClass -Force + } else { + Copy-Item $inputClass $backupClass -Force + } } + Copy-Item $patchedClass $targetClass -Force + Copy-Item $helperClass $targetHelperClass -Force + Write-Output "Patched BaseVehicle.class deployed to $targetClass" + Write-Output "Deployed LandtrainConstraintAuthHelper.class to $targetHelperClass" + Write-Output "Backup stored at $backupClass" } -Copy-Item $patchedClass $targetClass -Force -Write-Output "Patched BaseVehicle.class deployed to $targetClass" -Write-Output "Backup stored at $backupClass" - $distDir = Join-Path $repoRoot "zombie\vehicles" $distClass = Join-Path $distDir "BaseVehicle.class" +$distHelperClass = Join-Path $distDir "LandtrainConstraintAuthHelper.class" New-Item -ItemType Directory -Force -Path $distDir | Out-Null Copy-Item $patchedClass $distClass -Force +Copy-Item $helperClass $distHelperClass -Force Write-Output "Distribution class updated at $distClass" +Write-Output "Distribution helper class updated at $distHelperClass" diff --git a/tools/restore-game-basevehicle.ps1 b/tools/restore-game-basevehicle.ps1 index 6221915..6e96060 100644 --- a/tools/restore-game-basevehicle.ps1 +++ b/tools/restore-game-basevehicle.ps1 @@ -4,15 +4,37 @@ param( $ErrorActionPreference = "Stop" -$targetClass = Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class" -$backupClass = "$targetClass.landtrain.original" +$targets = @( + (Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class"), + (Join-Path $GameRoot "java\zombie\vehicles\BaseVehicle.class") +) +$helperTargets = @( + (Join-Path $GameRoot "zombie\vehicles\LandtrainConstraintAuthHelper.class"), + (Join-Path $GameRoot "java\zombie\vehicles\LandtrainConstraintAuthHelper.class") +) -if (Test-Path $backupClass) { - Copy-Item $backupClass $targetClass -Force - Write-Output "Restored BaseVehicle.class from $backupClass" -} elseif (Test-Path $targetClass) { - Remove-Item $targetClass -Force - Write-Output "Removed override class at $targetClass (game will use class from projectzomboid.jar)" -} else { - Write-Output "No override or backup found. Nothing to restore." +$handled = $false +foreach ($targetClass in $targets | Select-Object -Unique) { + $backupClass = "$targetClass.landtrain.original" + if (Test-Path $backupClass) { + Copy-Item $backupClass $targetClass -Force + Write-Output "Restored BaseVehicle.class from $backupClass" + $handled = $true + } elseif (Test-Path $targetClass) { + Remove-Item $targetClass -Force + Write-Output "Removed override class at $targetClass (game will use class from projectzomboid.jar)" + $handled = $true + } +} + +foreach ($helperClass in $helperTargets | Select-Object -Unique) { + if (Test-Path $helperClass) { + Remove-Item $helperClass -Force + Write-Output "Removed helper override class at $helperClass" + $handled = $true + } +} + +if (-not $handled) { + Write-Output "No override or backup found in known client/dedicated paths. Nothing to restore." } diff --git a/zombie/vehicles/BaseVehicle.class b/zombie/vehicles/BaseVehicle.class index 3d1d73f3eddca92afd6e7e38fb9f2922c9627c41..3319ada2fa3462b40b9d02f799f81b8804009ad9 100644 GIT binary patch delta 38904 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 P=q`D;{E;zZIoJOWPa<|= delta 39332 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 La+mq~9o+a2el34+ diff --git a/zombie/vehicles/LandtrainConstraintAuthHelper.class b/zombie/vehicles/LandtrainConstraintAuthHelper.class new file mode 100644 index 0000000000000000000000000000000000000000..42c2fcd46bc1f632d087b67f2ee0140ec3269b89 GIT binary patch literal 763 zcma)4OK;Oa5dJ20;>I|SHl-;92$YA?w5f#<7Yax;h=-JxL#y65&M0wl>}u@3u@`vpQ^Hbh>x5u5!wy?m zLe@qa8A74u1$;Q@w^<*nw$FsJ<|y=sd|u_b_J*tp&06bUO#3S0livXMw&u5PPif`G ztRv@j6uwk_Zq7Y>7V;>XC=e~#!it2?z#WJ0xU>JB zP(4rcXRv=ak>J~~aV3c_UWoUCE+KamEBEbzI-3s5#5Ka^td9j<6Re|fpk039B@^CU z$mDK9BJM*dlivWOWC>MrHsy3nG>J~vzhL>37?NA0C65wm;1aU3?b-8>q7AuuSpSYR zjq0C~Yi*B_-cHQY!Oj@@oeRG5VPlNthWJJ_lEBn7{UezQWE&_5l2s{C5^GIZDq|fR txGDpyioA^q?uu2%P3#KR2e>0%2pvXd0?R~Yf|7~+1UZp{?fSIDjbCPIyo&$; literal 0 HcmV?d00001