working
This commit is contained in:
93
42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua
Normal file
93
42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua
Normal 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")
|
||||
@@ -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
@@ -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")
|
||||
124
42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua
Normal file
124
42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua
Normal 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")
|
||||
33
README.md
33
README.md
@@ -4,27 +4,46 @@ Landtrain extends Towbars to support chained towing.
|
||||
|
||||
## Important
|
||||
|
||||
Project Zomboid base `BaseVehicle.addPointConstraint()` force-breaks existing constraints.
|
||||
To keep `1 -> 2` while attaching `2 -> 3`, Landtrain includes a Java class override:
|
||||
Project Zomboid base `BaseVehicle.addPointConstraint()` force-breaks existing constraints and chain state in MP.
|
||||
To keep `1 -> 2` while attaching `2 -> 3` (and preserve movement replication), Landtrain includes a Java class override:
|
||||
|
||||
- `zombie/vehicles/BaseVehicle.class`
|
||||
- client install: `zombie/vehicles/BaseVehicle.class`
|
||||
- dedicated install: `java/zombie/vehicles/BaseVehicle.class`
|
||||
- helper class (both): `LandtrainConstraintAuthHelper.class` in the same `zombie/vehicles` folder
|
||||
|
||||
This is the same override pattern used by mods like Realistic Car Physics (manual `zombie` folder copy).
|
||||
|
||||
## Apply patch to game
|
||||
|
||||
1. Run:
|
||||
1. Patch your local client install:
|
||||
|
||||
```powershell
|
||||
.\tools\patch-game-basevehicle.ps1
|
||||
```
|
||||
|
||||
2. Ensure both mods are enabled:
|
||||
- `hrsys_towbars`
|
||||
- `hrsys_landtrain`
|
||||
2. Patch the dedicated server install:
|
||||
|
||||
```powershell
|
||||
.\tools\patch-game-basevehicle.ps1 -GameRoot "D:\SteamLibrary\steamapps\common\Project Zomboid Dedicated Server"
|
||||
```
|
||||
|
||||
3. Ensure both mods are enabled:
|
||||
- `\hrsys_towbars`
|
||||
- `\hrsys_landtrain`
|
||||
|
||||
## MP requirement
|
||||
|
||||
For multiplayer, every connecting client and the dedicated server must run the patched class.
|
||||
If only one side is patched, towing links can desync with no explicit Lua error.
|
||||
|
||||
## Restore vanilla class
|
||||
|
||||
```powershell
|
||||
.\tools\restore-game-basevehicle.ps1
|
||||
```
|
||||
|
||||
To restore dedicated server too:
|
||||
|
||||
```powershell
|
||||
.\tools\restore-game-basevehicle.ps1 -GameRoot "D:\SteamLibrary\steamapps\common\Project Zomboid Dedicated Server"
|
||||
```
|
||||
|
||||
104
tools/check-game-basevehicle.ps1
Normal file
104
tools/check-game-basevehicle.ps1
Normal 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"
|
||||
}
|
||||
@@ -3,27 +3,34 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.InsnList;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.LdcInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
/**
|
||||
* Patches zombie.vehicles.BaseVehicle so addPointConstraint() no longer force-breaks
|
||||
* both vehicles before creating a new constraint.
|
||||
* Patches zombie.vehicles.BaseVehicle for Landtrain chain support:
|
||||
* 1) remove forced breakConstraint() in addPointConstraint()
|
||||
* 2) route constraintChanged() driver lookups through helper that handles chain middle vehicles
|
||||
*/
|
||||
public final class BaseVehicleConstraintPatch {
|
||||
private static final String TARGET_NAME = "addPointConstraint";
|
||||
private static final String CONSTRAINT_CHANGED_NAME = "constraintChanged";
|
||||
private static final String CLINIT_NAME = "<clinit>";
|
||||
private static final String VOID_NOARG_DESC = "()V";
|
||||
private static final String PATCH_LOG_LINE = "[Landtrain][BaseVehiclePatch] BaseVehicle override enabled";
|
||||
private static final String BREAK_DESC_OBJECT_BOOL = "(ZLjava/lang/Boolean;)V";
|
||||
private static final String BREAK_DESC_PRIMITIVE_BOOL = "(ZZ)V";
|
||||
private static final String BASE_VEHICLE_OWNER = "zombie/vehicles/BaseVehicle";
|
||||
private static final String GET_DRIVER_DESC = "()Lzombie/characters/IsoGameCharacter;";
|
||||
private static final String HELPER_OWNER = "zombie/vehicles/LandtrainConstraintAuthHelper";
|
||||
private static final String HELPER_METHOD = "resolveConstraintDriver";
|
||||
private static final String HELPER_DESC =
|
||||
"(Lzombie/vehicles/BaseVehicle;)Lzombie/characters/IsoGameCharacter;";
|
||||
|
||||
private BaseVehicleConstraintPatch() {
|
||||
}
|
||||
@@ -43,12 +50,16 @@ public final class BaseVehicleConstraintPatch {
|
||||
|
||||
int removedCalls = 0;
|
||||
int inspectedAddPointMethods = 0;
|
||||
int patchedConstraintDriverCalls = 0;
|
||||
|
||||
for (MethodNode method : classNode.methods) {
|
||||
if (!TARGET_NAME.equals(method.name) || !isTargetAddPointConstraint(method.desc)) {
|
||||
continue;
|
||||
}
|
||||
if (TARGET_NAME.equals(method.name) && isTargetAddPointConstraint(method.desc)) {
|
||||
inspectedAddPointMethods++;
|
||||
removedCalls += patchAddPointConstraint(method);
|
||||
} else if (CONSTRAINT_CHANGED_NAME.equals(method.name)
|
||||
&& VOID_NOARG_DESC.equals(method.desc)) {
|
||||
patchedConstraintDriverCalls += patchConstraintChangedDriverCalls(method);
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCalls < 2) {
|
||||
@@ -59,6 +70,11 @@ public final class BaseVehicleConstraintPatch {
|
||||
+ inspectedAddPointMethods
|
||||
+ ")");
|
||||
}
|
||||
if (patchedConstraintDriverCalls < 1) {
|
||||
throw new IllegalStateException(
|
||||
"Expected to patch at least 1 constraintChanged getDriver call, patched "
|
||||
+ patchedConstraintDriverCalls);
|
||||
}
|
||||
if (!ensureClassInitLog(classNode)) {
|
||||
throw new IllegalStateException("Failed to inject BaseVehicle class-init debug log");
|
||||
}
|
||||
@@ -71,12 +87,12 @@ public final class BaseVehicleConstraintPatch {
|
||||
System.out.println(
|
||||
"Patched BaseVehicle.class; removed breakConstraint calls: "
|
||||
+ removedCalls
|
||||
+ ", constraint driver hooks: "
|
||||
+ patchedConstraintDriverCalls
|
||||
+ ", class-init debug log: enabled");
|
||||
}
|
||||
|
||||
private static boolean isTargetAddPointConstraint(String methodDesc) {
|
||||
// We only want the 5-arg overload:
|
||||
// (IsoPlayer, BaseVehicle, String, String, boolean|Boolean) -> void
|
||||
return "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Z)V"
|
||||
.equals(methodDesc)
|
||||
|| "(Lzombie/characters/IsoPlayer;Lzombie/vehicles/BaseVehicle;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V"
|
||||
@@ -97,9 +113,8 @@ public final class BaseVehicleConstraintPatch {
|
||||
node = next;
|
||||
continue;
|
||||
}
|
||||
// Keep stack-map frames valid by preserving stack effect:
|
||||
|
||||
// breakConstraint(...) consumes objectref + 2 args and returns void.
|
||||
// Replace invoke with POP2 + POP (consume 3 category-1 stack slots).
|
||||
InsnList replacement = new InsnList();
|
||||
replacement.add(new InsnNode(Opcodes.POP2));
|
||||
replacement.add(new InsnNode(Opcodes.POP));
|
||||
@@ -113,6 +128,36 @@ public final class BaseVehicleConstraintPatch {
|
||||
return patched;
|
||||
}
|
||||
|
||||
private static int patchConstraintChangedDriverCalls(MethodNode method) {
|
||||
int patched = 0;
|
||||
InsnList insns = method.instructions;
|
||||
for (AbstractInsnNode node = insns.getFirst(); node != null; ) {
|
||||
AbstractInsnNode next = node.getNext();
|
||||
if (!(node instanceof MethodInsnNode call)) {
|
||||
node = next;
|
||||
continue;
|
||||
}
|
||||
if (!BASE_VEHICLE_OWNER.equals(call.owner)
|
||||
|| !"getDriver".equals(call.name)
|
||||
|| !GET_DRIVER_DESC.equals(call.desc)) {
|
||||
node = next;
|
||||
continue;
|
||||
}
|
||||
|
||||
MethodInsnNode replacement =
|
||||
new MethodInsnNode(
|
||||
Opcodes.INVOKESTATIC,
|
||||
HELPER_OWNER,
|
||||
HELPER_METHOD,
|
||||
HELPER_DESC,
|
||||
false);
|
||||
insns.set(call, replacement);
|
||||
patched++;
|
||||
node = next;
|
||||
}
|
||||
return patched;
|
||||
}
|
||||
|
||||
private static boolean ensureClassInitLog(ClassNode classNode) {
|
||||
MethodNode clinit = null;
|
||||
for (MethodNode method : classNode.methods) {
|
||||
|
||||
38
tools/java/LandtrainConstraintAuthHelper.java
Normal file
38
tools/java/LandtrainConstraintAuthHelper.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,27 @@ New-Item -ItemType Directory -Force -Path $classPatchDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $buildDir | Out-Null
|
||||
|
||||
$javaExe = Join-Path $GameRoot "jre64\bin\java.exe"
|
||||
$gameJar = Join-Path $GameRoot "projectzomboid.jar"
|
||||
if (-not (Test-Path $javaExe)) { throw "java.exe not found at $javaExe" }
|
||||
if (-not (Test-Path $gameJar)) { throw "projectzomboid.jar not found at $gameJar" }
|
||||
|
||||
$clientJar = Join-Path $GameRoot "projectzomboid.jar"
|
||||
$dedicatedJar = Join-Path $GameRoot "java\projectzomboid.jar"
|
||||
$gameJar = $null
|
||||
$targetClasses = @()
|
||||
|
||||
if (Test-Path $clientJar) {
|
||||
$gameJar = $clientJar
|
||||
$targetClasses += (Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class")
|
||||
}
|
||||
if (Test-Path $dedicatedJar) {
|
||||
$gameJar = $dedicatedJar
|
||||
$targetClasses += (Join-Path $GameRoot "java\zombie\vehicles\BaseVehicle.class")
|
||||
}
|
||||
if ($null -eq $gameJar) {
|
||||
throw "projectzomboid.jar not found at either $clientJar or $dedicatedJar"
|
||||
}
|
||||
if ($targetClasses.Count -eq 0) {
|
||||
throw "No valid BaseVehicle.class deployment targets found under $GameRoot"
|
||||
}
|
||||
|
||||
$ecjJar = Join-Path $toolsDir "ecj.jar"
|
||||
$asmJar = Join-Path $toolsDir "asm.jar"
|
||||
@@ -34,12 +52,15 @@ if (-not (Test-Path $asmTreeJar)) {
|
||||
|
||||
$patcherSource = Join-Path $PSScriptRoot "java\BaseVehicleConstraintPatch.java"
|
||||
if (-not (Test-Path $patcherSource)) { throw "Missing patcher source: $patcherSource" }
|
||||
$helperSource = Join-Path $PSScriptRoot "java\LandtrainConstraintAuthHelper.java"
|
||||
if (-not (Test-Path $helperSource)) { throw "Missing helper source: $helperSource" }
|
||||
|
||||
& $javaExe -jar $ecjJar -17 -cp "$asmJar;$asmTreeJar" -d $buildDir $patcherSource
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to compile BaseVehicleConstraintPatch.java" }
|
||||
& $javaExe -jar $ecjJar -17 -cp "$asmJar;$asmTreeJar;$gameJar" -d $buildDir $patcherSource $helperSource
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to compile BaseVehicleConstraintPatch.java/LandtrainConstraintAuthHelper.java" }
|
||||
|
||||
$inputClass = Join-Path $classPatchDir "BaseVehicle.original.class"
|
||||
$patchedClass = Join-Path $classPatchDir "BaseVehicle.patched.class"
|
||||
$helperClass = Join-Path $buildDir "zombie\vehicles\LandtrainConstraintAuthHelper.class"
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$zip = [System.IO.Compression.ZipFile]::OpenRead($gameJar)
|
||||
@@ -61,26 +82,33 @@ try {
|
||||
|
||||
& $javaExe -cp "$buildDir;$asmJar;$asmTreeJar" BaseVehicleConstraintPatch $inputClass $patchedClass
|
||||
if ($LASTEXITCODE -ne 0) { throw "BaseVehicle class patch failed" }
|
||||
if (-not (Test-Path $helperClass)) { throw "Missing compiled helper class: $helperClass" }
|
||||
|
||||
$targetDir = Join-Path $GameRoot "zombie\vehicles"
|
||||
$targetClass = Join-Path $targetDir "BaseVehicle.class"
|
||||
$backupClass = "$targetClass.landtrain.original"
|
||||
foreach ($targetClass in $targetClasses | Select-Object -Unique) {
|
||||
$targetDir = Split-Path -Parent $targetClass
|
||||
$backupClass = "$targetClass.landtrain.original"
|
||||
$targetHelperClass = Join-Path $targetDir "LandtrainConstraintAuthHelper.class"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
|
||||
if (-not (Test-Path $backupClass)) {
|
||||
New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
|
||||
if (-not (Test-Path $backupClass)) {
|
||||
if (Test-Path $targetClass) {
|
||||
Copy-Item $targetClass $backupClass -Force
|
||||
} else {
|
||||
Copy-Item $inputClass $backupClass -Force
|
||||
}
|
||||
}
|
||||
Copy-Item $patchedClass $targetClass -Force
|
||||
Copy-Item $helperClass $targetHelperClass -Force
|
||||
Write-Output "Patched BaseVehicle.class deployed to $targetClass"
|
||||
Write-Output "Deployed LandtrainConstraintAuthHelper.class to $targetHelperClass"
|
||||
Write-Output "Backup stored at $backupClass"
|
||||
}
|
||||
|
||||
Copy-Item $patchedClass $targetClass -Force
|
||||
Write-Output "Patched BaseVehicle.class deployed to $targetClass"
|
||||
Write-Output "Backup stored at $backupClass"
|
||||
|
||||
$distDir = Join-Path $repoRoot "zombie\vehicles"
|
||||
$distClass = Join-Path $distDir "BaseVehicle.class"
|
||||
$distHelperClass = Join-Path $distDir "LandtrainConstraintAuthHelper.class"
|
||||
New-Item -ItemType Directory -Force -Path $distDir | Out-Null
|
||||
Copy-Item $patchedClass $distClass -Force
|
||||
Copy-Item $helperClass $distHelperClass -Force
|
||||
Write-Output "Distribution class updated at $distClass"
|
||||
Write-Output "Distribution helper class updated at $distHelperClass"
|
||||
|
||||
@@ -4,15 +4,37 @@ param(
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$targetClass = Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class"
|
||||
$backupClass = "$targetClass.landtrain.original"
|
||||
$targets = @(
|
||||
(Join-Path $GameRoot "zombie\vehicles\BaseVehicle.class"),
|
||||
(Join-Path $GameRoot "java\zombie\vehicles\BaseVehicle.class")
|
||||
)
|
||||
$helperTargets = @(
|
||||
(Join-Path $GameRoot "zombie\vehicles\LandtrainConstraintAuthHelper.class"),
|
||||
(Join-Path $GameRoot "java\zombie\vehicles\LandtrainConstraintAuthHelper.class")
|
||||
)
|
||||
|
||||
if (Test-Path $backupClass) {
|
||||
$handled = $false
|
||||
foreach ($targetClass in $targets | Select-Object -Unique) {
|
||||
$backupClass = "$targetClass.landtrain.original"
|
||||
if (Test-Path $backupClass) {
|
||||
Copy-Item $backupClass $targetClass -Force
|
||||
Write-Output "Restored BaseVehicle.class from $backupClass"
|
||||
} elseif (Test-Path $targetClass) {
|
||||
$handled = $true
|
||||
} elseif (Test-Path $targetClass) {
|
||||
Remove-Item $targetClass -Force
|
||||
Write-Output "Removed override class at $targetClass (game will use class from projectzomboid.jar)"
|
||||
} else {
|
||||
Write-Output "No override or backup found. Nothing to restore."
|
||||
$handled = $true
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($helperClass in $helperTargets | Select-Object -Unique) {
|
||||
if (Test-Path $helperClass) {
|
||||
Remove-Item $helperClass -Force
|
||||
Write-Output "Removed helper override class at $helperClass"
|
||||
$handled = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $handled) {
|
||||
Write-Output "No override or backup found in known client/dedicated paths. Nothing to restore."
|
||||
}
|
||||
|
||||
Binary file not shown.
BIN
zombie/vehicles/LandtrainConstraintAuthHelper.class
Normal file
BIN
zombie/vehicles/LandtrainConstraintAuthHelper.class
Normal file
Binary file not shown.
Reference in New Issue
Block a user