This commit is contained in:
2026-02-12 09:02:55 -05:00
parent c01fe187d0
commit d4114eb6c6
13 changed files with 1249 additions and 1800 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -4,27 +4,46 @@ Landtrain extends Towbars to support chained towing.
## Important ## Important
Project Zomboid base `BaseVehicle.addPointConstraint()` force-breaks existing constraints. Project Zomboid base `BaseVehicle.addPointConstraint()` force-breaks existing constraints and chain state in MP.
To keep `1 -> 2` while attaching `2 -> 3`, Landtrain includes a Java class override: 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). This is the same override pattern used by mods like Realistic Car Physics (manual `zombie` folder copy).
## Apply patch to game ## Apply patch to game
1. Run: 1. Patch your local client install:
```powershell ```powershell
.\tools\patch-game-basevehicle.ps1 .\tools\patch-game-basevehicle.ps1
``` ```
2. Ensure both mods are enabled: 2. Patch the dedicated server install:
- `hrsys_towbars`
- `hrsys_landtrain` ```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 ## Restore vanilla class
```powershell ```powershell
.\tools\restore-game-basevehicle.ps1 .\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"
```

View File

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

View File

@@ -3,27 +3,34 @@ import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter; import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.Opcodes;
/** /**
* Patches zombie.vehicles.BaseVehicle so addPointConstraint() no longer force-breaks * Patches zombie.vehicles.BaseVehicle for Landtrain chain support:
* both vehicles before creating a new constraint. * 1) remove forced breakConstraint() in addPointConstraint()
* 2) route constraintChanged() driver lookups through helper that handles chain middle vehicles
*/ */
public final class BaseVehicleConstraintPatch { public final class BaseVehicleConstraintPatch {
private static final String TARGET_NAME = "addPointConstraint"; private static final String TARGET_NAME = "addPointConstraint";
private static final String CONSTRAINT_CHANGED_NAME = "constraintChanged";
private static final String CLINIT_NAME = "<clinit>"; private static final String CLINIT_NAME = "<clinit>";
private static final String VOID_NOARG_DESC = "()V"; private static final String VOID_NOARG_DESC = "()V";
private static final String PATCH_LOG_LINE = "[Landtrain][BaseVehiclePatch] BaseVehicle override enabled"; 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_OBJECT_BOOL = "(ZLjava/lang/Boolean;)V";
private static final String BREAK_DESC_PRIMITIVE_BOOL = "(ZZ)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 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() { private BaseVehicleConstraintPatch() {
} }
@@ -43,12 +50,16 @@ public final class BaseVehicleConstraintPatch {
int removedCalls = 0; int removedCalls = 0;
int inspectedAddPointMethods = 0; int inspectedAddPointMethods = 0;
int patchedConstraintDriverCalls = 0;
for (MethodNode method : classNode.methods) { for (MethodNode method : classNode.methods) {
if (!TARGET_NAME.equals(method.name) || !isTargetAddPointConstraint(method.desc)) { if (TARGET_NAME.equals(method.name) && isTargetAddPointConstraint(method.desc)) {
continue;
}
inspectedAddPointMethods++; inspectedAddPointMethods++;
removedCalls += patchAddPointConstraint(method); removedCalls += patchAddPointConstraint(method);
} else if (CONSTRAINT_CHANGED_NAME.equals(method.name)
&& VOID_NOARG_DESC.equals(method.desc)) {
patchedConstraintDriverCalls += patchConstraintChangedDriverCalls(method);
}
} }
if (removedCalls < 2) { if (removedCalls < 2) {
@@ -59,6 +70,11 @@ public final class BaseVehicleConstraintPatch {
+ inspectedAddPointMethods + inspectedAddPointMethods
+ ")"); + ")");
} }
if (patchedConstraintDriverCalls < 1) {
throw new IllegalStateException(
"Expected to patch at least 1 constraintChanged getDriver call, patched "
+ patchedConstraintDriverCalls);
}
if (!ensureClassInitLog(classNode)) { if (!ensureClassInitLog(classNode)) {
throw new IllegalStateException("Failed to inject BaseVehicle class-init debug log"); throw new IllegalStateException("Failed to inject BaseVehicle class-init debug log");
} }
@@ -71,12 +87,12 @@ public final class BaseVehicleConstraintPatch {
System.out.println( System.out.println(
"Patched BaseVehicle.class; removed breakConstraint calls: " "Patched BaseVehicle.class; removed breakConstraint calls: "
+ removedCalls + removedCalls
+ ", constraint driver hooks: "
+ patchedConstraintDriverCalls
+ ", class-init debug log: enabled"); + ", class-init debug log: enabled");
} }
private static boolean isTargetAddPointConstraint(String methodDesc) { 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" return "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Z)V"
.equals(methodDesc) .equals(methodDesc)
|| "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V" || "(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; node = next;
continue; continue;
} }
// Keep stack-map frames valid by preserving stack effect:
// breakConstraint(...) consumes objectref + 2 args and returns void. // breakConstraint(...) consumes objectref + 2 args and returns void.
// Replace invoke with POP2 + POP (consume 3 category-1 stack slots).
InsnList replacement = new InsnList(); InsnList replacement = new InsnList();
replacement.add(new InsnNode(Opcodes.POP2)); replacement.add(new InsnNode(Opcodes.POP2));
replacement.add(new InsnNode(Opcodes.POP)); replacement.add(new InsnNode(Opcodes.POP));
@@ -113,6 +128,36 @@ public final class BaseVehicleConstraintPatch {
return patched; 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) { private static boolean ensureClassInitLog(ClassNode classNode) {
MethodNode clinit = null; MethodNode clinit = null;
for (MethodNode method : classNode.methods) { for (MethodNode method : classNode.methods) {

View File

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

View File

@@ -14,9 +14,27 @@ New-Item -ItemType Directory -Force -Path $classPatchDir | Out-Null
New-Item -ItemType Directory -Force -Path $buildDir | Out-Null New-Item -ItemType Directory -Force -Path $buildDir | Out-Null
$javaExe = Join-Path $GameRoot "jre64\bin\java.exe" $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 $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" $ecjJar = Join-Path $toolsDir "ecj.jar"
$asmJar = Join-Path $toolsDir "asm.jar" $asmJar = Join-Path $toolsDir "asm.jar"
@@ -34,12 +52,15 @@ if (-not (Test-Path $asmTreeJar)) {
$patcherSource = Join-Path $PSScriptRoot "java\BaseVehicleConstraintPatch.java" $patcherSource = Join-Path $PSScriptRoot "java\BaseVehicleConstraintPatch.java"
if (-not (Test-Path $patcherSource)) { throw "Missing patcher source: $patcherSource" } 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 & $javaExe -jar $ecjJar -17 -cp "$asmJar;$asmTreeJar;$gameJar" -d $buildDir $patcherSource $helperSource
if ($LASTEXITCODE -ne 0) { throw "Failed to compile BaseVehicleConstraintPatch.java" } if ($LASTEXITCODE -ne 0) { throw "Failed to compile BaseVehicleConstraintPatch.java/LandtrainConstraintAuthHelper.java" }
$inputClass = Join-Path $classPatchDir "BaseVehicle.original.class" $inputClass = Join-Path $classPatchDir "BaseVehicle.original.class"
$patchedClass = Join-Path $classPatchDir "BaseVehicle.patched.class" $patchedClass = Join-Path $classPatchDir "BaseVehicle.patched.class"
$helperClass = Join-Path $buildDir "zombie\vehicles\LandtrainConstraintAuthHelper.class"
Add-Type -AssemblyName System.IO.Compression.FileSystem Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [System.IO.Compression.ZipFile]::OpenRead($gameJar) $zip = [System.IO.Compression.ZipFile]::OpenRead($gameJar)
@@ -61,10 +82,12 @@ try {
& $javaExe -cp "$buildDir;$asmJar;$asmTreeJar" BaseVehicleConstraintPatch $inputClass $patchedClass & $javaExe -cp "$buildDir;$asmJar;$asmTreeJar" BaseVehicleConstraintPatch $inputClass $patchedClass
if ($LASTEXITCODE -ne 0) { throw "BaseVehicle class patch failed" } 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" foreach ($targetClass in $targetClasses | Select-Object -Unique) {
$targetClass = Join-Path $targetDir "BaseVehicle.class" $targetDir = Split-Path -Parent $targetClass
$backupClass = "$targetClass.landtrain.original" $backupClass = "$targetClass.landtrain.original"
$targetHelperClass = Join-Path $targetDir "LandtrainConstraintAuthHelper.class"
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
if (-not (Test-Path $backupClass)) { if (-not (Test-Path $backupClass)) {
@@ -74,13 +97,18 @@ if (-not (Test-Path $backupClass)) {
Copy-Item $inputClass $backupClass -Force Copy-Item $inputClass $backupClass -Force
} }
} }
Copy-Item $patchedClass $targetClass -Force Copy-Item $patchedClass $targetClass -Force
Copy-Item $helperClass $targetHelperClass -Force
Write-Output "Patched BaseVehicle.class deployed to $targetClass" Write-Output "Patched BaseVehicle.class deployed to $targetClass"
Write-Output "Deployed LandtrainConstraintAuthHelper.class to $targetHelperClass"
Write-Output "Backup stored at $backupClass" Write-Output "Backup stored at $backupClass"
}
$distDir = Join-Path $repoRoot "zombie\vehicles" $distDir = Join-Path $repoRoot "zombie\vehicles"
$distClass = Join-Path $distDir "BaseVehicle.class" $distClass = Join-Path $distDir "BaseVehicle.class"
$distHelperClass = Join-Path $distDir "LandtrainConstraintAuthHelper.class"
New-Item -ItemType Directory -Force -Path $distDir | Out-Null New-Item -ItemType Directory -Force -Path $distDir | Out-Null
Copy-Item $patchedClass $distClass -Force Copy-Item $patchedClass $distClass -Force
Copy-Item $helperClass $distHelperClass -Force
Write-Output "Distribution class updated at $distClass" Write-Output "Distribution class updated at $distClass"
Write-Output "Distribution helper class updated at $distHelperClass"

View File

@@ -4,15 +4,37 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$targetClass = Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class" $targets = @(
$backupClass = "$targetClass.landtrain.original" (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")
)
$handled = $false
foreach ($targetClass in $targets | Select-Object -Unique) {
$backupClass = "$targetClass.landtrain.original"
if (Test-Path $backupClass) { if (Test-Path $backupClass) {
Copy-Item $backupClass $targetClass -Force Copy-Item $backupClass $targetClass -Force
Write-Output "Restored BaseVehicle.class from $backupClass" Write-Output "Restored BaseVehicle.class from $backupClass"
$handled = $true
} elseif (Test-Path $targetClass) { } elseif (Test-Path $targetClass) {
Remove-Item $targetClass -Force Remove-Item $targetClass -Force
Write-Output "Removed override class at $targetClass (game will use class from projectzomboid.jar)" Write-Output "Removed override class at $targetClass (game will use class from projectzomboid.jar)"
} else { $handled = $true
Write-Output "No override or backup found. Nothing to restore." }
}
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."
} }

Binary file not shown.

Binary file not shown.