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 3d1d73f..3319ada 100644 Binary files a/zombie/vehicles/BaseVehicle.class and b/zombie/vehicles/BaseVehicle.class differ diff --git a/zombie/vehicles/LandtrainConstraintAuthHelper.class b/zombie/vehicles/LandtrainConstraintAuthHelper.class new file mode 100644 index 0000000..42c2fcd Binary files /dev/null and b/zombie/vehicles/LandtrainConstraintAuthHelper.class differ