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")
|
||||
Reference in New Issue
Block a user