Landtrain!!!!

This commit is contained in:
2026-02-06 16:04:34 -05:00
parent 9194c5e96d
commit fc14fe2b57
9 changed files with 750 additions and 1 deletions

18
.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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)

9
42.13/mod.info Normal file
View File

@@ -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

View File

@@ -1,3 +1,30 @@
# Landtrain
Yargh I be a land pirate, or g'day chap wanna vb with that land train
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
```

9
mod.info Normal file
View File

@@ -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

View File

@@ -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 <input BaseVehicle.class> <output BaseVehicle.class>");
System.exit(2);
}
Path input = Paths.get(args[0]);
Path output = Paths.get(args[1]);
byte[] original = Files.readAllBytes(input);
ClassNode classNode = new ClassNode();
new ClassReader(original).accept(classNode, 0);
int removedCalls = 0;
int inspectedAddPointMethods = 0;
for (MethodNode method : classNode.methods) {
if (!TARGET_NAME.equals(method.name) || !isTargetAddPointConstraint(method.desc)) {
continue;
}
inspectedAddPointMethods++;
removedCalls += patchAddPointConstraint(method);
}
if (removedCalls < 2) {
throw new IllegalStateException(
"Expected to remove 2 breakConstraint calls, removed "
+ removedCalls
+ " (inspected addPoint methods: "
+ inspectedAddPointMethods
+ ")");
}
ClassWriter writer = new ClassWriter(0);
classNode.accept(writer);
Files.createDirectories(output.getParent());
Files.write(output, writer.toByteArray());
System.out.println("Patched BaseVehicle.class; removed breakConstraint calls: " + removedCalls);
}
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"
.equals(methodDesc);
}
private static int patchAddPointConstraint(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
&& BASE_VEHICLE_OWNER.equals(call.owner)
&& "breakConstraint".equals(call.name)) {
if (!(BREAK_DESC_OBJECT_BOOL.equals(call.desc)
|| BREAK_DESC_PRIMITIVE_BOOL.equals(call.desc))) {
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));
insns.insert(node, replacement);
insns.remove(node);
patched++;
}
node = next;
}
return patched;
}
}

View File

@@ -0,0 +1,80 @@
param(
[string]$GameRoot = "D:\SteamLibrary\steamapps\common\ProjectZomboid"
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$toolsDir = Join-Path $repoRoot ".tools"
$classPatchDir = Join-Path $toolsDir "classpatch"
$buildDir = Join-Path $toolsDir "patcher-build"
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
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" }
$ecjJar = Join-Path $toolsDir "ecj.jar"
$asmJar = Join-Path $toolsDir "asm.jar"
$asmTreeJar = Join-Path $toolsDir "asm-tree.jar"
if (-not (Test-Path $ecjJar)) {
Invoke-WebRequest -Uri "https://repo1.maven.org/maven2/org/eclipse/jdt/ecj/3.38.0/ecj-3.38.0.jar" -OutFile $ecjJar
}
if (-not (Test-Path $asmJar)) {
Invoke-WebRequest -Uri "https://repo1.maven.org/maven2/org/ow2/asm/asm/9.9/asm-9.9.jar" -OutFile $asmJar
}
if (-not (Test-Path $asmTreeJar)) {
Invoke-WebRequest -Uri "https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.9/asm-tree-9.9.jar" -OutFile $asmTreeJar
}
$patcherSource = Join-Path $PSScriptRoot "java\BaseVehicleConstraintPatch.java"
if (-not (Test-Path $patcherSource)) { throw "Missing patcher source: $patcherSource" }
& $javaExe -jar $ecjJar -17 -cp "$asmJar;$asmTreeJar" -d $buildDir $patcherSource
if ($LASTEXITCODE -ne 0) { throw "Failed to compile BaseVehicleConstraintPatch.java" }
$inputClass = Join-Path $classPatchDir "BaseVehicle.original.class"
$patchedClass = Join-Path $classPatchDir "BaseVehicle.patched.class"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($gameJar)
try {
$entry = $zip.GetEntry("zombie/vehicles/BaseVehicle.class")
if ($null -eq $entry) { throw "zombie/vehicles/BaseVehicle.class not found in $gameJar" }
$entryStream = $entry.Open()
$fileStream = [System.IO.File]::Create($inputClass)
try {
$entryStream.CopyTo($fileStream)
} finally {
$fileStream.Close()
$entryStream.Close()
}
} finally {
$zip.Dispose()
}
& $javaExe -cp "$buildDir;$asmJar;$asmTreeJar" BaseVehicleConstraintPatch $inputClass $patchedClass
if ($LASTEXITCODE -ne 0) { throw "BaseVehicle class patch failed" }
$targetDir = Join-Path $GameRoot "zombie\vehicles"
$targetClass = Join-Path $targetDir "BaseVehicle.class"
$backupClass = "$targetClass.landtrain.original"
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
Write-Output "Patched BaseVehicle.class deployed to $targetClass"
Write-Output "Backup stored at $backupClass"

View File

@@ -0,0 +1,18 @@
param(
[string]$GameRoot = "D:\SteamLibrary\steamapps\common\ProjectZomboid"
)
$ErrorActionPreference = "Stop"
$targetClass = Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class"
$backupClass = "$targetClass.landtrain.original"
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."
}

Binary file not shown.