diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..289042d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,18 @@
+.tools/
+
+# Logs and local patch leftovers
+*.log
+*.tmp
+*.bak
+*.orig
+*.rej
+
+# Editor/IDE
+.vscode/
+.idea/
+*.iml
+
+# OS metadata
+.DS_Store
+Thumbs.db
+desktop.ini
diff --git a/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua b/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua
new file mode 100644
index 0000000..2dd76d1
--- /dev/null
+++ b/42.13/media/lua/client/Landtrain/UnlimitedTowbarChains.lua
@@ -0,0 +1,483 @@
+if not TowBarMod then TowBarMod = {} end
+if not TowBarMod.Utils then TowBarMod.Utils = {} end
+if not TowBarMod.UI then TowBarMod.UI = {} end
+
+local LANDTRAIN_DEBUG = false
+local LANDTRAIN_ATTACH_LABEL = "Landtrain attach"
+local LANDTRAIN_FALLBACK_MAX_DIST_SQ = 3.25 -- ~1.8 tiles
+local LANDTRAIN_FALLBACK_MAX_DZ = 0.9
+
+local function ltLog(msg)
+ if not LANDTRAIN_DEBUG then return end
+ print("[Landtrain] " .. tostring(msg))
+end
+
+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 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 _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
+
+ -- 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)
+ local isLandtrainLink = (vehicleA and (vehicleA:getVehicleTowing() or vehicleA:getVehicleTowedBy()))
+ or (vehicleB and (vehicleB:getVehicleTowing() or vehicleB:getVehicleTowedBy()))
+ if isLandtrainLink then
+ return LANDTRAIN_ATTACH_LABEL
+ end
+ return getText("UI_Text_Towing_byTowBar")
+end
+
+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
+
+ local link = {
+ frontVehicle = frontVehicle,
+ towingVehicle = towingVehicle,
+ attachmentA = frontVehicle:getTowAttachmentSelf() or "trailer",
+ attachmentB = towingVehicle:getTowAttachmentSelf() or "trailerfront"
+ }
+ 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 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))
+ TowBarMod.Utils.updateAttachmentsForRigidTow(frontVehicle, towingVehicle, link.attachmentA, link.attachmentB)
+ towingVehicle:setScriptName("notTowingA_Trailer")
+
+ local args = {
+ vehicleA = frontVehicle:getId(),
+ vehicleB = towingVehicle:getId(),
+ attachmentA = link.attachmentA,
+ attachmentB = link.attachmentB
+ }
+ 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
+ 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 menuHasTowbarAttachSlice(menu)
+ if menu == nil or menu.slices == nil then return false end
+ for _, slice in ipairs(menu.slices) do
+ local command = slice.command and slice.command[1]
+ if command == TowBarMod.Hook.attachByTowBarAction or command == TowBarMod.UI.showChooseVehicleMenu then
+ return true
+ end
+ end
+ return false
+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" .. LANDTRAIN_ATTACH_LABEL
+ 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" .. LANDTRAIN_ATTACH_LABEL
+ hookType.func = TowBarMod.Hook.attachByTowBarAction
+ hookType.towingVehicle = vehicleA
+ hookType.towedVehicle = vehicleB
+ hookType.textureName = "tow_bar_icon"
+ table.insert(hookTypeVariants, hookType)
+ end
+
+ return hookTypeVariants
+end
+
+local function getNearbyLandtrainTargets(mainVehicle)
+ local vehicles = {}
+ local square = mainVehicle and mainVehicle:getSquare() or nil
+ if square == nil then 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 })
+ end
+ end
+ end
+ end
+ end
+ end
+ return vehicles
+end
+
+local function addLandtrainHookOptionToMenu(playerObj, vehicle)
+ if playerObj == nil or vehicle == nil then return end
+ if not playerObj:getInventory():getItemFromTypeRecurse("TowBar.TowBar") then return end
+
+ 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(
+ LANDTRAIN_ATTACH_LABEL .. "...",
+ getTexture("media/textures/tow_bar_attach.png"),
+ TowBarMod.UI.showChooseVehicleMenu,
+ playerObj,
+ vehicle,
+ vehicleList,
+ true
+ )
+end
+
+local function installLandtrainTowbarPatch()
+ if not TowBarMod or not TowBarMod.Utils then
+ return
+ end
+
+ if TowBarMod.Utils._landtrainUnlimitedChainsInstalled then
+ return
+ end
+
+ -- Override Towbar's single-link limitation so a vehicle can be part of a chain.
+ function TowBarMod.Utils.getHookTypeVariants(vehicleA, vehicleB, hasTowBar)
+ local hookTypeVariants = {}
+ if not hasTowBar then return hookTypeVariants end
+ if vehicleA == nil or vehicleB == nil then return hookTypeVariants end
+ if vehicleA == vehicleB then return hookTypeVariants end
+
+ -- 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
+
+ -- Keep towbar state valid for middle links in a chain after detach/attach.
+ local originalPerformAttach = TowBarMod.Hook and TowBarMod.Hook.performAttachTowBar
+ if originalPerformAttach then
+ TowBarMod.Hook.performAttachTowBar = function(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB)
+ 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)
+ 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
+ end
+ end
+
+ local originalPerformDetach = TowBarMod.Hook and TowBarMod.Hook.performDeattachTowBar
+ if originalPerformDetach then
+ TowBarMod.Hook.performDeattachTowBar = function(playerObj, towingVehicle, towedVehicle)
+ originalPerformDetach(playerObj, towingVehicle, towedVehicle)
+
+ setTowBarModelVisibleForVehicle(towedVehicle, false)
+ refreshTowBarState(towingVehicle)
+ refreshTowBarState(towedVehicle)
+ if towingVehicle then
+ refreshTowBarState(towingVehicle:getVehicleTowedBy())
+ refreshTowBarState(towingVehicle:getVehicleTowing())
+ end
+ if towedVehicle then
+ refreshTowBarState(towedVehicle:getVehicleTowedBy())
+ refreshTowBarState(towedVehicle:getVehicleTowing())
+ end
+ end
+ end
+
+ -- Towbar UI only adds attach when fully unlinked; add attach option for linked vehicles too.
+ if ISVehicleMenu and ISVehicleMenu.showRadialMenu and not TowBarMod.UI._landtrainShowRadialPatched then
+ local originalShowRadialMenu = ISVehicleMenu.showRadialMenu
+ ISVehicleMenu.showRadialMenu = 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())
+ if menuHasTowbarAttachSlice(menu) then
+ return
+ end
+
+ if (vehicle:getVehicleTowing() or vehicle:getVehicleTowedBy()) then
+ addLandtrainHookOptionToMenu(playerObj, vehicle)
+ end
+ end
+ TowBarMod.UI._landtrainShowRadialPatched = true
+ end
+
+ TowBarMod.Utils._landtrainUnlimitedChainsInstalled = true
+end
+
+Events.OnGameBoot.Add(ensureTowAttachmentsForTrailers)
+Events.OnGameBoot.Add(installLandtrainTowbarPatch)
+Events.OnGameStart.Add(installLandtrainTowbarPatch)
diff --git a/42.13/mod.info b/42.13/mod.info
new file mode 100644
index 0000000..e983b0b
--- /dev/null
+++ b/42.13/mod.info
@@ -0,0 +1,9 @@
+name=Landtrain
+id=hrsys_landtrain
+require=\hrsys_towbars
+description=Extends Towbars to allow chained vehicle towing.
+author=Riggs0
+category=vehicle
+versionMin=42.13.0
+url=https://hudsonriggs.systems
+modversion=1.0.0
diff --git a/README.md b/README.md
index 4e89afd..a6218ae 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,30 @@
# Landtrain
-Yargh I be a land pirate, or g'day chap wanna vb with that land train
\ No newline at end of file
+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:
+
+- `zombie/vehicles/BaseVehicle.class`
+
+This is the same override pattern used by mods like Realistic Car Physics (manual `zombie` folder copy).
+
+## Apply patch to game
+
+1. Run:
+
+```powershell
+.\tools\patch-game-basevehicle.ps1
+```
+
+2. Ensure both mods are enabled:
+ - `hrsys_towbars`
+ - `hrsys_landtrain`
+
+## Restore vanilla class
+
+```powershell
+.\tools\restore-game-basevehicle.ps1
+```
diff --git a/mod.info b/mod.info
new file mode 100644
index 0000000..e983b0b
--- /dev/null
+++ b/mod.info
@@ -0,0 +1,9 @@
+name=Landtrain
+id=hrsys_landtrain
+require=\hrsys_towbars
+description=Extends Towbars to allow chained vehicle towing.
+author=Riggs0
+category=vehicle
+versionMin=42.13.0
+url=https://hudsonriggs.systems
+modversion=1.0.0
diff --git a/tools/java/BaseVehicleConstraintPatch.java b/tools/java/BaseVehicleConstraintPatch.java
new file mode 100644
index 0000000..b3dc692
--- /dev/null
+++ b/tools/java/BaseVehicleConstraintPatch.java
@@ -0,0 +1,105 @@
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+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.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.
+ */
+public final class BaseVehicleConstraintPatch {
+ private static final String TARGET_NAME = "addPointConstraint";
+ 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 BaseVehicleConstraintPatch() {
+ }
+
+ public static void main(String[] args) throws Exception {
+ if (args.length != 2) {
+ System.err.println("Usage: BaseVehicleConstraintPatch