This commit is contained in:
2026-02-13 13:00:40 -05:00
parent e694f746f8
commit a9c1d8201b
4 changed files with 317 additions and 4 deletions

View File

@@ -3,7 +3,7 @@ if not TowBarMod.Config then TowBarMod.Config = {} end
TowBarMod.Config.lowLevelAnimation = "RemoveGrass" TowBarMod.Config.lowLevelAnimation = "RemoveGrass"
TowBarMod.Config.rigidTowbarDistance = 1.0 TowBarMod.Config.rigidTowbarDistance = 1.0
TowBarMod.Config.devMode = true TowBarMod.Config.devMode = false
TowBarMod.Config.vanillaTowbarModelScaleMin = 1.5 TowBarMod.Config.vanillaTowbarModelScaleMin = 1.5
TowBarMod.Config.vanillaTowbarModelScaleMax = 2.0 TowBarMod.Config.vanillaTowbarModelScaleMax = 2.0
TowBarMod.Config.smallScaleTowbarIndexOffset = 2 TowBarMod.Config.smallScaleTowbarIndexOffset = 2

View File

@@ -0,0 +1,125 @@
if isServer() then return end
if not TowBarMod then TowBarMod = {} end
TowBarMod.Sync = TowBarMod.Sync or {}
if TowBarMod.Sync._towSyncClientLoaded then return end
TowBarMod.Sync._towSyncClientLoaded = true
local function resolveVehicle(id)
if not id then return nil end
return getVehicleById(id)
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 isLinked(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return false end
return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
end
local function reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
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
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
local attachmentA = args.attachmentA or "trailer"
local attachmentB = args.attachmentB or "trailerfront"
if not ensureAttachment(vehicleA, attachmentA) or not ensureAttachment(vehicleB, attachmentB) then
return
end
if not isLinked(vehicleA, vehicleB) then
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB)
end
reconcilePairState(vehicleA, 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 ~= "towbar" then return end
if command == "forceAttachSync" then
applyAttachSync(args)
elseif command == "forceDetachSync" then
applyDetachSync(args)
end
end
Events.OnServerCommand.Add(onServerCommand)

View File

@@ -33,6 +33,19 @@ local function getTowBarItem(playerObj)
return inventory:getItemFromTypeRecurse("TowBar.TowBar") return inventory:getItemFromTypeRecurse("TowBar.TowBar")
end end
local function sendTowAttachCommand(playerObj, args)
if not playerObj or not args then return end
-- MP-safe/server-authoritative attach path (Landtrain style).
if isClient() and isMultiplayer() then
sendClientCommand(playerObj, "towbar", "attachTowBar", args)
return
end
-- Keep vanilla attach path for SP/local behavior.
sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
end
local TowbarVariantSize = 24 local TowbarVariantSize = 24
local TowbarNormalStart = 0 local TowbarNormalStart = 0
local TowbarLargeStart = 24 local TowbarLargeStart = 24
@@ -278,11 +291,38 @@ local function reattachTowBarPair(playerObj, towingVehicle, towedVehicle, requir
attachmentA = attachmentA, attachmentA = attachmentA,
attachmentB = attachmentB attachmentB = attachmentB
} }
sendClientCommand(playerObj, "vehicle", "attachTrailer", args) sendTowAttachCommand(playerObj, args)
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)) ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
return true return true
end end
local function reattachTowBarPairAfterCleanDetach(playerObj, towingVehicle, towedVehicle, requireDriver)
if not playerObj or not towingVehicle or not towedVehicle then
return false
end
if requireDriver and not towingVehicle:isDriver(playerObj) then
return false
end
local detachArgs = {
towingVehicle = towingVehicle:getId(),
vehicle = towedVehicle:getId()
}
sendClientCommand(playerObj, "towbar", "detachTowBar", detachArgs)
-- World load/spawn can restore constraints in a bad state. Reattach one
-- short tick later so the detach is fully applied first.
ISTimedActionQueue.add(TowBarScheduleAction:new(
playerObj,
1,
reattachTowBarPair,
towingVehicle,
towedVehicle,
requireDriver
))
return true
end
local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft) local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft)
if not vehicle then return end if not vehicle then return end
@@ -295,8 +335,14 @@ local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft)
local localPlayer = playerObj or getPlayer() local localPlayer = playerObj or getPlayer()
local towingVehicle = vehicle:getVehicleTowedBy() local towingVehicle = vehicle:getVehicleTowedBy()
if towingVehicle then
-- Apply rigid spacing as soon as the tow link exists to avoid a visible
-- bumper-to-bumper snap while waiting for reattach recovery.
TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
end
if localPlayer and towingVehicle then if localPlayer and towingVehicle then
if reattachTowBarPair(localPlayer, towingVehicle, vehicle, false) then if reattachTowBarPairAfterCleanDetach(localPlayer, towingVehicle, vehicle, false) then
return return
end end
end end
@@ -376,7 +422,7 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
attachmentA = attachmentA, attachmentA = attachmentA,
attachmentB = attachmentB attachmentB = attachmentB
} }
sendClientCommand(playerObj, "vehicle", "attachTrailer", args) sendTowAttachCommand(playerObj, args)
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle)) ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
end end

View File

@@ -3,6 +3,10 @@ if isClient() then return end
local TowingCommands = {} local TowingCommands = {}
local Commands = {} local Commands = {}
local TowBarItemType = "TowBar.TowBar" local TowBarItemType = "TowBar.TowBar"
local SyncDelayTicks = 2
local SnapshotIntervalTicks = 120
local pendingSync = {}
local snapshotTickCounter = 0
TowingCommands.wantNoise = getDebug() or false TowingCommands.wantNoise = getDebug() or false
@@ -12,6 +16,133 @@ local noise = function(msg)
end end
end end
local function queueSync(kind, player, args)
if not args then return end
table.insert(pendingSync, {
kind = kind,
ticks = SyncDelayTicks,
player = player,
args = args
})
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 isLinked(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return false end
return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
end
local function hasTowBarState(vehicle)
if not vehicle then return false end
local md = vehicle:getModData()
if not md then return false end
return md["isTowingByTowBar"] == true
end
local function broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
if not vehicleA or not vehicleB then return end
sendServerCommand("towbar", "forceAttachSync", {
vehicleA = vehicleA:getId(),
vehicleB = vehicleB:getId(),
attachmentA = attachmentA,
attachmentB = attachmentB
})
end
local function broadcastDetach(vehicleAId, vehicleBId)
sendServerCommand("towbar", "forceDetachSync", {
vehicleA = vehicleAId,
vehicleB = vehicleBId
})
end
local function processAttachSync(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
noise("attach sync skipped missing vehicles A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB))
return
end
local attachmentA = resolveAttachmentA(args, vehicleA)
local attachmentB = resolveAttachmentB(args, vehicleB)
if not isLinked(vehicleA, vehicleB) then
vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB)
end
if isLinked(vehicleA, vehicleB) then
broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
end
end
local function processDetachSync(item)
local args = item.args or {}
local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle
local vehicleBId = args.vehicleB
broadcastDetach(vehicleAId, vehicleBId)
end
local function snapshotActiveTowbarLinksServer()
local cell = getCell()
if not cell then return end
local list = cell:getVehicles()
if not list then return end
for i = 0, list:size() - 1 do
local towingVehicle = list:get(i)
local towedVehicle = towingVehicle and towingVehicle:getVehicleTowing() or nil
if towingVehicle and towedVehicle and towedVehicle:getVehicleTowedBy() == towingVehicle then
if hasTowBarState(towingVehicle) or hasTowBarState(towedVehicle) then
local attachmentA = resolveAttachmentA(nil, towingVehicle)
local towedMd = towedVehicle:getModData()
local attachmentB = (towedMd and towedMd["towBarChangedAttachmentId"]) or resolveAttachmentB(nil, towedVehicle)
if attachmentA == attachmentB then
attachmentA = "trailer"
attachmentB = "trailerfront"
end
broadcastAttach(towingVehicle, towedVehicle, attachmentA, attachmentB)
end
end
end
end
local function processPendingSync()
snapshotTickCounter = snapshotTickCounter + 1
if snapshotTickCounter >= SnapshotIntervalTicks then
snapshotTickCounter = 0
snapshotActiveTowbarLinksServer()
end
if #pendingSync == 0 then return end
local remaining = {}
for i = 1, #pendingSync do
local item = pendingSync[i]
item.ticks = item.ticks - 1
if item.ticks <= 0 then
if item.kind == "attach" then
processAttachSync(item)
elseif item.kind == "detach" then
processDetachSync(item)
end
else
table.insert(remaining, item)
end
end
pendingSync = remaining
end
function Commands.attachTowBar(player, args) function Commands.attachTowBar(player, args)
local vehicleA = getVehicleById(args.vehicleA) local vehicleA = getVehicleById(args.vehicleA)
local vehicleB = getVehicleById(args.vehicleB) local vehicleB = getVehicleById(args.vehicleB)
@@ -92,6 +223,16 @@ Commands.attachConstraint = Commands.attachTowBar
Commands.detachConstraint = Commands.detachTowBar Commands.detachConstraint = Commands.detachTowBar
TowingCommands.OnClientCommand = function(module, command, player, args) TowingCommands.OnClientCommand = function(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 == "attachTowBar" then
queueSync("attach", player, args)
elseif module == "towbar" and command == "detachTowBar" then
queueSync("detach", player, args)
end
if module == "towbar" and Commands[command] then if module == "towbar" and Commands[command] then
local argStr = "" local argStr = ""
args = args or {} args = args or {}
@@ -104,3 +245,4 @@ TowingCommands.OnClientCommand = function(module, command, player, args)
end end
Events.OnClientCommand.Add(TowingCommands.OnClientCommand) Events.OnClientCommand.Add(TowingCommands.OnClientCommand)
Events.OnTick.Add(processPendingSync)