diff --git a/42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua b/42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua index d533793..8dd5060 100644 --- a/42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua +++ b/42.13/media/lua/client/Landtrain/LandtrainTowSyncClient.lua @@ -45,14 +45,51 @@ local function resolveVehicle(id) return getVehicleById(id) end +local function reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB) + if not vehicleA or not vehicleB then return end + + if TowBarMod.Utils and TowBarMod.Utils.updateAttachmentsForRigidTow then + TowBarMod.Utils.updateAttachmentsForRigidTow(vehicleA, vehicleB, attachmentA, attachmentB) + end + + local towingMd = vehicleA:getModData() + local towedMd = vehicleB:getModData() + local currentScript = vehicleB:getScriptName() + + if towingMd then + towingMd["isTowingByTowBar"] = true + vehicleA:transmitModData() + end + if towedMd then + if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then + towedMd.towBarOriginalScriptName = currentScript + end + if towedMd.towBarOriginalMass == nil then + towedMd.towBarOriginalMass = vehicleB:getMass() + end + if towedMd.towBarOriginalBrakingForce == nil then + towedMd.towBarOriginalBrakingForce = vehicleB:getBrakingForce() + end + towedMd["isTowingByTowBar"] = true + towedMd["towed"] = true + vehicleB:transmitModData() + end + + vehicleB:setScriptName("notTowingA_Trailer") + if TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then + pcall(TowBarMod.Hook.setVehiclePostAttach, nil, vehicleB) + end + log("reconcilePairState A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()) + .. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB)) +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 + if not vehicleA or not vehicleB then + log("applyAttachSync skipped: missing vehicle A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB)) return end @@ -63,7 +100,15 @@ local function applyAttachSync(args) return end - vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB) + if vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA then + log("applyAttachSync already linked A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())) + else + log("applyAttachSync addPointConstraint A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()) + .. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB)) + vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB) + end + + reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB) end local function safeBreak(vehicle) diff --git a/42.13/media/lua/client/Landtrain/LandtrainTowbarChainsRebuild.lua b/42.13/media/lua/client/Landtrain/LandtrainTowbarChainsRebuild.lua index 4739296..371273d 100644 --- a/42.13/media/lua/client/Landtrain/LandtrainTowbarChainsRebuild.lua +++ b/42.13/media/lua/client/Landtrain/LandtrainTowbarChainsRebuild.lua @@ -20,6 +20,10 @@ local function log(msg) end end +local function vehicleId(vehicle) + return vehicle and vehicle:getId() or "nil" +end + local function hasAttachment(vehicle, attachmentId) if vehicle == nil or attachmentId == nil then return false end local script = vehicle:getScript() @@ -180,6 +184,18 @@ local function restoreDefaultsIfLeadLost(vehicle) clearChangedOffset(vehicle) restoreTowBarDefaults(vehicle) + local md = vehicle:getModData() + if md then + md["towed"] = false + if vehicle:getVehicleTowing() == nil then + md["isTowingByTowBar"] = false + md.towBarOriginalScriptName = nil + md.towBarOriginalMass = nil + md.towBarOriginalBrakingForce = nil + end + vehicle:transmitModData() + end + setTowBarModelVisible(vehicle, false) end @@ -208,12 +224,104 @@ local function sendAttach(playerObj, towingVehicle, towedVehicle, attachmentA, a end end -local function captureFrontLink(middleVehicle) +local function isLinkedPair(towingVehicle, towedVehicle) + if not towingVehicle or not towedVehicle then return false end + return towingVehicle:getVehicleTowing() == towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle +end + +local function markTowBarPair(towingVehicle, towedVehicle) + local towingMd = towingVehicle and towingVehicle:getModData() or nil + local towedMd = towedVehicle and towedVehicle:getModData() or nil + + if towingMd then + towingMd["isTowingByTowBar"] = true + towingVehicle:transmitModData() + end + if towedMd then + local currentScript = towedVehicle and towedVehicle:getScriptName() or nil + if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then + towedMd.towBarOriginalScriptName = currentScript + end + if towedMd.towBarOriginalMass == nil and towedVehicle then + towedMd.towBarOriginalMass = towedVehicle:getMass() + end + if towedMd.towBarOriginalBrakingForce == nil and towedVehicle then + towedMd.towBarOriginalBrakingForce = towedVehicle:getBrakingForce() + end + towedMd["isTowingByTowBar"] = true + towedMd["towed"] = true + towedVehicle:transmitModData() + end +end + +local function ensurePairAttached(playerObj, towingVehicle, towedVehicle, preferredA, preferredB, alwaysSend) + if not playerObj or not towingVehicle or not towedVehicle then + log("ensurePairAttached skipped: missing refs A=" .. tostring(vehicleId(towingVehicle)) .. " B=" .. tostring(vehicleId(towedVehicle))) + return false + end + if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then + log("ensurePairAttached skipped: identical vehicles id=" .. tostring(vehicleId(towingVehicle))) + return false + end + if wouldCreateTowLoop(towingVehicle, towedVehicle) then + log("ensurePairAttached skipped: loop A=" .. tostring(vehicleId(towingVehicle)) .. " B=" .. tostring(vehicleId(towedVehicle))) + return false + end + + local attachmentA, attachmentB = resolvePair(towingVehicle, towedVehicle, preferredA, preferredB) + if not hasAttachment(towingVehicle, attachmentA) or not hasAttachment(towedVehicle, attachmentB) then + log("ensurePairAttached skipped: missing attachment A=" .. tostring(attachmentA) .. " B=" .. tostring(attachmentB) + .. " ids=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle))) + return false + end + + applyRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB) + towedVehicle:setScriptName("notTowingA_Trailer") + markTowBarPair(towingVehicle, towedVehicle) + + local linked = isLinkedPair(towingVehicle, towedVehicle) + log("ensurePairAttached pair=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle)) + .. " linked=" .. tostring(linked) .. " alwaysSend=" .. tostring(alwaysSend) + .. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB)) + + if linked and TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then + pcall(TowBarMod.Hook.setVehiclePostAttach, nil, towedVehicle) + end + + if alwaysSend or not linked then + sendAttach(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) + elseif TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then + queueAction(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle) + end + + setTowBarModelVisible(towedVehicle, true) + refreshAround(towingVehicle) + refreshAround(towedVehicle) + return true +end + +local function captureFrontLinks(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 } + + local links = {} + local cursor = middleVehicle + local visited = {} + + while cursor do + if visited[cursor] then break end + visited[cursor] = true + + local frontVehicle = cursor:getVehicleTowedBy() + if not frontVehicle then break end + if frontVehicle == cursor or visited[frontVehicle] then break end + + local a, b = resolvePair(frontVehicle, cursor) + table.insert(links, { front = frontVehicle, middle = cursor, a = a, b = b }) + cursor = frontVehicle + end + + if #links == 0 then return nil end + return links end local function restoreFrontLink(playerObj, link) @@ -225,23 +333,21 @@ local function restoreFrontLink(playerObj, link) 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) - - setTowBarModelVisible(middle, true) - refreshAround(front) - refreshAround(middle) + ensurePairAttached(playerObj, front, middle, link.a, link.b, false) 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) +local function restoreFrontLinks(playerObj, links) + if not links then return end + for _, link in ipairs(links) do + restoreFrontLink(playerObj, link) + end +end + +local function queueRestoreFrontLinks(playerObj, links, delay) + if not playerObj or not links then return end + queueAction(playerObj, delay or 12, function(character, linksArg) + restoreFrontLinks(character, linksArg) + end, links) end local function resolveDetachPair(towingVehicle, towedVehicle) @@ -411,16 +517,31 @@ LT.performAttachWrapper = function(playerObj, towingVehicle, towedVehicle, attac if towingVehicle == towedVehicle or towingVehicle:getId() == towedVehicle:getId() then return end if wouldCreateTowLoop(towingVehicle, towedVehicle) then return end - local frontLink = captureFrontLink(towingVehicle) + local frontLinks = captureFrontLinks(towingVehicle) + log("performAttachWrapper begin pair=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle)) + .. " frontLinks=" .. tostring(frontLinks and #frontLinks or 0)) original(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB) - restoreFrontLink(playerObj, frontLink) - queueRestoreFront(playerObj, frontLink, 12) - queueRestoreFront(playerObj, frontLink, 30) + restoreFrontLinks(playerObj, frontLinks) + queueRestoreFrontLinks(playerObj, frontLinks, 12) + queueRestoreFrontLinks(playerObj, frontLinks, 30) - setTowBarModelVisible(towedVehicle, true) - refreshAround(towingVehicle) - refreshAround(towedVehicle) + ensurePairAttached(playerObj, towingVehicle, towedVehicle, attachmentA, attachmentB, false) + + local attachState = { + towing = towingVehicle, + towed = towedVehicle, + attachmentA = attachmentA, + attachmentB = attachmentB + } + queueAction(playerObj, 14, function(character, state) + if not state then return end + ensurePairAttached(character, state.towing, state.towed, state.attachmentA, state.attachmentB, true) + end, attachState) + queueAction(playerObj, 34, function(character, state) + if not state then return end + ensurePairAttached(character, state.towing, state.towed, state.attachmentA, state.attachmentB, true) + end, attachState) end LT.performDetachWrapper = function(playerObj, towingVehicle, towedVehicle) @@ -430,12 +551,12 @@ LT.performDetachWrapper = function(playerObj, towingVehicle, towedVehicle) local resolvedTowing, resolvedTowed = resolveDetachPair(towingVehicle, towedVehicle) if resolvedTowing == nil or resolvedTowed == nil then return end - local frontLink = captureFrontLink(resolvedTowing) + local frontLinks = captureFrontLinks(resolvedTowing) original(playerObj, resolvedTowing, resolvedTowed) - restoreFrontLink(playerObj, frontLink) - queueRestoreFront(playerObj, frontLink, 12) - queueRestoreFront(playerObj, frontLink, 30) + restoreFrontLinks(playerObj, frontLinks) + queueRestoreFrontLinks(playerObj, frontLinks, 12) + queueRestoreFrontLinks(playerObj, frontLinks, 30) restoreDefaultsIfLeadLost(resolvedTowing) restoreDefaultsIfLeadLost(resolvedTowed) diff --git a/42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua b/42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua index ddbe444..8043bb1 100644 --- a/42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua +++ b/42.13/media/lua/server/Landtrain/LandtrainTowSyncServer.lua @@ -70,8 +70,12 @@ local function processAttach(item) log("attach sync failed: server link not established A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())) return end + else + log("attach sync already linked A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())) end + log("attach sync broadcast A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()) + .. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB)) sendServerCommand("landtrain", "forceAttachSync", { vehicleA = vehicleA:getId(), vehicleB = vehicleB:getId(), @@ -94,7 +98,8 @@ end local function processPending() if #pending == 0 then return end - for i = #pending, 1, -1 do + local remaining = {} + for i = 1, #pending do local item = pending[i] item.ticks = item.ticks - 1 if item.ticks <= 0 then @@ -103,17 +108,24 @@ local function processPending() elseif item.kind == "detach" then processDetach(item) end - table.remove(pending, i) + else + table.insert(remaining, item) end end + pending = remaining end local function onClientCommand(module, command, player, args) if module == "vehicle" and command == "attachTrailer" then + log("queue attach from " .. tostring(player and player:getUsername() or "unknown") + .. " A=" .. tostring(args and args.vehicleA) .. " B=" .. tostring(args and args.vehicleB) + .. " attachmentA=" .. tostring(args and args.attachmentA) .. " attachmentB=" .. tostring(args and args.attachmentB)) queueSync("attach", player, args) elseif module == "vehicle" and command == "detachTrailer" then + log("queue detach(vehicle) from " .. tostring(player and player:getUsername() or "unknown")) queueSync("detach", player, args) elseif module == "towbar" and command == "detachTowBar" then + log("queue detach(towbar) from " .. tostring(player and player:getUsername() or "unknown")) queueSync("detach", player, args) end end diff --git a/tools/java/BaseVehicleConstraintPatch.java b/tools/java/BaseVehicleConstraintPatch.java index e392c68..45e217c 100644 --- a/tools/java/BaseVehicleConstraintPatch.java +++ b/tools/java/BaseVehicleConstraintPatch.java @@ -4,6 +4,7 @@ import java.nio.file.Paths; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnList; @@ -11,6 +12,7 @@ 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.tree.VarInsnNode; /** * Patches zombie.vehicles.BaseVehicle for Landtrain chain support: @@ -20,10 +22,21 @@ import org.objectweb.asm.tree.MethodNode; public final class BaseVehicleConstraintPatch { private static final String TARGET_NAME = "addPointConstraint"; private static final String CONSTRAINT_CHANGED_NAME = "constraintChanged"; + private static final String AUTHORIZATION_CHANGED_NAME = "authorizationChanged"; + private static final String AUTHORIZATION_CLIENT_COLLIDE_NAME = "authorizationClientCollide"; + private static final String AUTHORIZATION_SERVER_COLLIDE_NAME = "authorizationServerCollide"; + private static final String AUTHORIZATION_SERVER_ON_SEAT_NAME = "authorizationServerOnSeat"; private static final String IS_ENTER_BLOCKED_NAME = "isEnterBlocked"; private static final String IS_ENTER_BLOCKED2_NAME = "isEnterBlocked2"; private static final String CLINIT_NAME = ""; private static final String VOID_NOARG_DESC = "()V"; + private static final String AUTHORIZATION_CHANGED_DESC = + "(Lzombie/characters/IsoGameCharacter;)V"; + private static final String AUTHORIZATION_CLIENT_COLLIDE_DESC = + "(Lzombie/characters/IsoPlayer;)V"; + private static final String AUTHORIZATION_SERVER_COLLIDE_DESC = "(SZ)V"; + private static final String AUTHORIZATION_SERVER_ON_SEAT_DESC = + "(Lzombie/characters/IsoPlayer;Z)V"; private static final String ENTER_BLOCKED_DESC = "(Lzombie/characters/IsoGameCharacter;I)Z"; private static final String EXIT_BLOCKED_DESC = "(Lzombie/characters/IsoGameCharacter;I)Z"; private static final String EXIT_BLOCKED2_DESC = "(I)Z"; @@ -31,6 +44,11 @@ public final class BaseVehicleConstraintPatch { 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 BULLET_OWNER = "zombie/core/physics/Bullet"; + private static final String BULLET_ADD_ROPE = "addRopeConstraint"; + private static final String BULLET_ADD_POINT = "addPointConstraint"; + private static final String BULLET_ADD_ROPE_DESC = "(IIFFFFFFF)I"; + private static final String BULLET_ADD_POINT_DESC = "(IIFFFFFF)I"; 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"; @@ -41,6 +59,27 @@ public final class BaseVehicleConstraintPatch { "(Lzombie/vehicles/BaseVehicle;Lzombie/characters/IsoGameCharacter;I)Z"; private static final String HELPER_ENTER_BLOCKED2 = "isEnterBlocked2Landtrain"; private static final String HELPER_ENTER_BLOCKED2_DESC = "(Lzombie/vehicles/BaseVehicle;I)Z"; + private static final String HELPER_AUTHORIZATION_CHANGED = + "authorizationChangedLandtrain"; + private static final String HELPER_AUTHORIZATION_CHANGED_DESC = + "(Lzombie/vehicles/BaseVehicle;Lzombie/characters/IsoGameCharacter;)V"; + private static final String HELPER_AUTHORIZATION_CLIENT_COLLIDE = + "authorizationClientCollideLandtrain"; + private static final String HELPER_AUTHORIZATION_CLIENT_COLLIDE_DESC = + "(Lzombie/vehicles/BaseVehicle;Lzombie/characters/IsoPlayer;)V"; + private static final String HELPER_AUTHORIZATION_SERVER_COLLIDE = + "authorizationServerCollideLandtrain"; + private static final String HELPER_AUTHORIZATION_SERVER_COLLIDE_DESC = + "(Lzombie/vehicles/BaseVehicle;SZ)V"; + private static final String HELPER_AUTHORIZATION_SERVER_ON_SEAT = + "authorizationServerOnSeatLandtrain"; + private static final String HELPER_AUTHORIZATION_SERVER_ON_SEAT_DESC = + "(Lzombie/vehicles/BaseVehicle;Lzombie/characters/IsoPlayer;Z)V"; + + private static final class AddPointPatchStats { + int removedBreakCalls; + int forcedRigidCalls; + } private BaseVehicleConstraintPatch() { } @@ -59,18 +98,49 @@ public final class BaseVehicleConstraintPatch { new ClassReader(original).accept(classNode, 0); int removedCalls = 0; + int forcedRigidCalls = 0; int inspectedAddPointMethods = 0; int patchedConstraintDriverCalls = 0; int patchedEnterBlockedCalls = 0; int patchedEnterBlocked2Calls = 0; + int patchedAuthorizationChangedMethods = 0; + int patchedAuthorizationClientCollideMethods = 0; + int patchedAuthorizationServerCollideMethods = 0; + int patchedAuthorizationServerOnSeatMethods = 0; for (MethodNode method : classNode.methods) { if (TARGET_NAME.equals(method.name) && isTargetAddPointConstraint(method.desc)) { inspectedAddPointMethods++; - removedCalls += patchAddPointConstraint(method); + AddPointPatchStats stats = patchAddPointConstraint(method); + removedCalls += stats.removedBreakCalls; + forcedRigidCalls += stats.forcedRigidCalls; } else if (CONSTRAINT_CHANGED_NAME.equals(method.name) && VOID_NOARG_DESC.equals(method.desc)) { patchedConstraintDriverCalls += patchConstraintChangedDriverCalls(method); + } else if (AUTHORIZATION_CHANGED_NAME.equals(method.name) + && AUTHORIZATION_CHANGED_DESC.equals(method.desc)) { + patchedAuthorizationChangedMethods += patchMethodDelegateToHelper( + method, + HELPER_AUTHORIZATION_CHANGED, + HELPER_AUTHORIZATION_CHANGED_DESC); + } else if (AUTHORIZATION_CLIENT_COLLIDE_NAME.equals(method.name) + && AUTHORIZATION_CLIENT_COLLIDE_DESC.equals(method.desc)) { + patchedAuthorizationClientCollideMethods += patchMethodDelegateToHelper( + method, + HELPER_AUTHORIZATION_CLIENT_COLLIDE, + HELPER_AUTHORIZATION_CLIENT_COLLIDE_DESC); + } else if (AUTHORIZATION_SERVER_COLLIDE_NAME.equals(method.name) + && AUTHORIZATION_SERVER_COLLIDE_DESC.equals(method.desc)) { + patchedAuthorizationServerCollideMethods += patchMethodDelegateToHelper( + method, + HELPER_AUTHORIZATION_SERVER_COLLIDE, + HELPER_AUTHORIZATION_SERVER_COLLIDE_DESC); + } else if (AUTHORIZATION_SERVER_ON_SEAT_NAME.equals(method.name) + && AUTHORIZATION_SERVER_ON_SEAT_DESC.equals(method.desc)) { + patchedAuthorizationServerOnSeatMethods += patchMethodDelegateToHelper( + method, + HELPER_AUTHORIZATION_SERVER_ON_SEAT, + HELPER_AUTHORIZATION_SERVER_ON_SEAT_DESC); } else if (IS_ENTER_BLOCKED_NAME.equals(method.name) && ENTER_BLOCKED_DESC.equals(method.desc)) { patchedEnterBlockedCalls += patchEnterBlockedCall( @@ -98,11 +168,36 @@ public final class BaseVehicleConstraintPatch { + inspectedAddPointMethods + ")"); } + if (forcedRigidCalls < 1) { + throw new IllegalStateException( + "Expected to force at least 1 rope->point constraint call, patched " + + forcedRigidCalls); + } if (patchedConstraintDriverCalls < 1) { throw new IllegalStateException( "Expected to patch at least 1 constraintChanged getDriver call, patched " + patchedConstraintDriverCalls); } + if (patchedAuthorizationChangedMethods < 1) { + throw new IllegalStateException( + "Expected to patch authorizationChanged, patched " + + patchedAuthorizationChangedMethods); + } + if (patchedAuthorizationClientCollideMethods < 1) { + throw new IllegalStateException( + "Expected to patch authorizationClientCollide, patched " + + patchedAuthorizationClientCollideMethods); + } + if (patchedAuthorizationServerCollideMethods < 1) { + throw new IllegalStateException( + "Expected to patch authorizationServerCollide, patched " + + patchedAuthorizationServerCollideMethods); + } + if (patchedAuthorizationServerOnSeatMethods < 1) { + throw new IllegalStateException( + "Expected to patch authorizationServerOnSeat, patched " + + patchedAuthorizationServerOnSeatMethods); + } if (patchedEnterBlockedCalls < 1) { throw new IllegalStateException( "Expected to patch isEnterBlocked call, patched " + patchedEnterBlockedCalls); @@ -123,8 +218,18 @@ public final class BaseVehicleConstraintPatch { System.out.println( "Patched BaseVehicle.class; removed breakConstraint calls: " + removedCalls + + ", forced rigid constraints: " + + forcedRigidCalls + ", constraint driver hooks: " + patchedConstraintDriverCalls + + ", auth hooks (changed/client/server/seat): " + + patchedAuthorizationChangedMethods + + "/" + + patchedAuthorizationClientCollideMethods + + "/" + + patchedAuthorizationServerCollideMethods + + "/" + + patchedAuthorizationServerOnSeatMethods + ", enter-block hooks: " + patchedEnterBlockedCalls + "/" @@ -139,8 +244,8 @@ public final class BaseVehicleConstraintPatch { .equals(methodDesc); } - private static int patchAddPointConstraint(MethodNode method) { - int patched = 0; + private static AddPointPatchStats patchAddPointConstraint(MethodNode method) { + AddPointPatchStats stats = new AddPointPatchStats(); InsnList insns = method.instructions; for (AbstractInsnNode node = insns.getFirst(); node != null; ) { @@ -160,12 +265,27 @@ public final class BaseVehicleConstraintPatch { replacement.add(new InsnNode(Opcodes.POP)); insns.insert(node, replacement); insns.remove(node); - patched++; + stats.removedBreakCalls++; + } else if (node instanceof MethodInsnNode call + && BULLET_OWNER.equals(call.owner) + && BULLET_ADD_ROPE.equals(call.name) + && BULLET_ADD_ROPE_DESC.equals(call.desc)) { + // Drop the rope-length float and call Bullet.addPointConstraint(...) instead. + insns.insertBefore(call, new InsnNode(Opcodes.POP)); + MethodInsnNode replacement = + new MethodInsnNode( + Opcodes.INVOKESTATIC, + BULLET_OWNER, + BULLET_ADD_POINT, + BULLET_ADD_POINT_DESC, + false); + insns.set(call, replacement); + stats.forcedRigidCalls++; } node = next; } - return patched; + return stats; } private static int patchConstraintChangedDriverCalls(MethodNode method) { @@ -198,6 +318,45 @@ public final class BaseVehicleConstraintPatch { return patched; } + private static int patchMethodDelegateToHelper( + MethodNode method, String helperMethod, String helperDesc) { + InsnList insns = new InsnList(); + + int localIndex = 0; + int maxStack = 0; + if ((method.access & Opcodes.ACC_STATIC) == 0) { + insns.add(new VarInsnNode(Opcodes.ALOAD, 0)); + localIndex = 1; + maxStack = 1; + } + + Type[] argumentTypes = Type.getArgumentTypes(method.desc); + for (Type argumentType : argumentTypes) { + insns.add(new VarInsnNode(argumentType.getOpcode(Opcodes.ILOAD), localIndex)); + localIndex += argumentType.getSize(); + maxStack += argumentType.getSize(); + } + + insns.add(new MethodInsnNode(Opcodes.INVOKESTATIC, HELPER_OWNER, helperMethod, helperDesc, false)); + + Type returnType = Type.getReturnType(method.desc); + if (returnType.getSort() == Type.VOID) { + insns.add(new InsnNode(Opcodes.RETURN)); + } else { + insns.add(new InsnNode(returnType.getOpcode(Opcodes.IRETURN))); + } + + method.instructions.clear(); + method.instructions.add(insns); + method.tryCatchBlocks.clear(); + if (method.localVariables != null) { + method.localVariables.clear(); + } + method.maxLocals = Math.max(method.maxLocals, localIndex); + method.maxStack = Math.max(method.maxStack, maxStack); + return 1; + } + private static int patchEnterBlockedCall( MethodNode method, String targetCallName, diff --git a/tools/java/LandtrainConstraintAuthHelper.java b/tools/java/LandtrainConstraintAuthHelper.java index a5e594b..016e22b 100644 --- a/tools/java/LandtrainConstraintAuthHelper.java +++ b/tools/java/LandtrainConstraintAuthHelper.java @@ -1,8 +1,12 @@ package zombie.vehicles; +import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import zombie.characters.IsoGameCharacter; +import zombie.characters.IsoPlayer; /** * Resolves the effective driver for constraint auth in chained towing. @@ -62,4 +66,133 @@ public final class LandtrainConstraintAuthHelper { private static boolean isVehicleBeingTowedInLandtrain(BaseVehicle vehicle) { return vehicle != null && vehicle.getVehicleTowedBy() != null; } + + public static void authorizationChangedLandtrain(BaseVehicle vehicle, IsoGameCharacter character) { + if (vehicle == null) { + return; + } + + if (character != null) { + applyAuthorizationAcrossChain( + vehicle, BaseVehicle.Authorization.Local, character.getOnlineID(), false); + } else { + applyAuthorizationAcrossChain(vehicle, BaseVehicle.Authorization.Server, -1, false); + } + } + + public static void authorizationClientCollideLandtrain(BaseVehicle vehicle, IsoPlayer driver) { + if (vehicle == null || driver == null || vehicle.getDriver() != null) { + return; + } + + applyAuthorizationAcrossChain( + vehicle, BaseVehicle.Authorization.LocalCollide, driver.getOnlineID(), true); + } + + public static void authorizationServerCollideLandtrain( + BaseVehicle vehicle, short playerID, boolean isCollide) { + if (vehicle == null) { + return; + } + if (vehicle.isNetPlayerAuthorization(BaseVehicle.Authorization.Local)) { + return; + } + + if (isCollide) { + applyAuthorizationAcrossChain( + vehicle, BaseVehicle.Authorization.LocalCollide, playerID, false); + return; + } + + BaseVehicle.Authorization auth = + playerID == -1 ? BaseVehicle.Authorization.Server : BaseVehicle.Authorization.Local; + applyAuthorizationAcrossChain(vehicle, auth, playerID, false); + } + + public static void authorizationServerOnSeatLandtrain( + BaseVehicle vehicle, IsoPlayer player, boolean enter) { + if (vehicle == null || player == null) { + return; + } + + BaseVehicle vehicleA = vehicle.getVehicleTowing(); + BaseVehicle vehicleB = vehicle.getVehicleTowedBy(); + if (vehicle.isNetPlayerId((short) -1) && enter) { + if (vehicleA != null && vehicleA.getDriver() == null) { + vehicle.addPointConstraint( + null, vehicleA, vehicle.getTowAttachmentSelf(), vehicleA.getTowAttachmentSelf()); + } else if (vehicleB != null && vehicleB.getDriver() == null) { + vehicle.addPointConstraint( + null, vehicleB, vehicle.getTowAttachmentSelf(), vehicleB.getTowAttachmentSelf()); + } else { + applyAuthorizationAcrossChain( + vehicle, BaseVehicle.Authorization.Local, player.getOnlineID(), false); + } + } else if (vehicle.isNetPlayerId(player.getOnlineID()) && !enter) { + if (vehicleA != null && vehicleA.getDriver() != null) { + vehicleA.addPointConstraint( + null, vehicle, vehicleA.getTowAttachmentSelf(), vehicle.getTowAttachmentSelf()); + } else if (vehicleB != null && vehicleB.getDriver() != null) { + vehicleB.addPointConstraint( + null, vehicle, vehicleB.getTowAttachmentSelf(), vehicle.getTowAttachmentSelf()); + } else { + applyAuthorizationAcrossChain(vehicle, BaseVehicle.Authorization.Server, -1, false); + } + } + } + + private static List collectConnectedChain(BaseVehicle start) { + ArrayList chain = new ArrayList<>(); + if (start == null) { + return chain; + } + + ArrayDeque pending = new ArrayDeque<>(); + Set visitedIds = new HashSet<>(); + pending.add(start); + + while (!pending.isEmpty()) { + BaseVehicle cursor = pending.removeFirst(); + if (cursor == null) { + continue; + } + + int id = cursor.getId(); + if (!visitedIds.add(id)) { + continue; + } + + chain.add(cursor); + BaseVehicle front = cursor.getVehicleTowedBy(); + BaseVehicle rear = cursor.getVehicleTowing(); + if (front != null && front != cursor) { + pending.add(front); + } + if (rear != null && rear != cursor) { + pending.add(rear); + } + } + + return chain; + } + + private static void applyAuthorizationAcrossChain( + BaseVehicle start, + BaseVehicle.Authorization authorization, + int playerId, + boolean refreshSimulation) { + long now = System.currentTimeMillis(); + for (BaseVehicle vehicle : collectConnectedChain(start)) { + if (vehicle == null) { + continue; + } + vehicle.setNetPlayerAuthorization(authorization, playerId); + if (refreshSimulation) { + vehicle.authSimulationTime = now; + if (vehicle.interpolation != null) { + vehicle.interpolation.clear(); + } + } + } + } } diff --git a/zombie/vehicles/BaseVehicle.class b/zombie/vehicles/BaseVehicle.class index 7ac58c8..a93c7ad 100644 Binary files a/zombie/vehicles/BaseVehicle.class and b/zombie/vehicles/BaseVehicle.class differ diff --git a/zombie/vehicles/LandtrainConstraintAuthHelper.class b/zombie/vehicles/LandtrainConstraintAuthHelper.class index 6907135..b092a3c 100644 Binary files a/zombie/vehicles/LandtrainConstraintAuthHelper.class and b/zombie/vehicles/LandtrainConstraintAuthHelper.class differ