Files
Towbar/42.20/media/lua/client/TowBar/TowSyncClient.lua
T
2026-08-13 18:59:54 -04:00

216 lines
8.0 KiB
Lua

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
TowBarMod.Sync.appliedPairs = TowBarMod.Sync.appliedPairs or {}
local function pairKeyContainsVehicle(key, vehicleId)
local id = tostring(vehicleId)
return string.sub(key, 1, #id + 1) == id .. ":"
or string.sub(key, -(#id + 1)) == ":" .. id
end
local function clearAppliedPairForVehicle(vehicle)
if not vehicle then return end
local vehicleId = vehicle:getId()
for key in pairs(TowBarMod.Sync.appliedPairs) do
if pairKeyContainsVehicle(key, vehicleId) then
TowBarMod.Sync.appliedPairs[key] = nil
end
end
end
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
towingMd["towed"] = false
towingMd["towBarTowedVehicleId"] = vehicleB:getId()
towingMd["towBarTowingVehicleId"] = nil
towingMd["towBarExpectedAttachment"] = attachmentA
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
towedMd["towBarTowedVehicleId"] = nil
towedMd["towBarTowingVehicleId"] = vehicleA:getId()
towedMd["towBarExpectedAttachment"] = attachmentB
vehicleB:transmitModData()
end
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicleB, "notTowingA_Trailer")
end
if TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
pcall(TowBarMod.Hook.setVehiclePostAttach, nil, vehicleB)
end
end
local breakTowBarPair
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
local key = tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())
if TowBarMod.Sync.appliedPairs[key] and not isLinked(vehicleA, vehicleB) then
-- Vehicle streaming can remove the native constraint while the Lua
-- module remains loaded. Allow the authoritative recovery sync to
-- rebuild local physics for the returning pair.
TowBarMod.Sync.appliedPairs[key] = nil
end
if not TowBarMod.Sync.appliedPairs[key] then
-- The server initially creates a normal Build 42 tow relation. It may
-- also restore one from the save. Replace that native rope exactly
-- once with this mod's rigid fake-trailer constraint.
breakTowBarPair(vehicleA, vehicleB)
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicleB, "notTowingA_Trailer")
end
-- The final true keeps this a local physics rebuild. The server owns
-- the persisted logical relation and must not receive a detach/attach race.
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB, true)
TowBarMod.Sync.appliedPairs[key] = true
end
reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
end
local function hasConflictingTowLink(vehicle, expectedOther)
if not vehicle or not expectedOther then return false end
local towing = vehicle:getVehicleTowing()
local towedBy = vehicle:getVehicleTowedBy()
if (towing ~= nil and towing ~= expectedOther)
or (towedBy ~= nil and towedBy ~= expectedOther) then
return true
end
-- Also reject a delayed detach while a new tow is being established but
-- has not created its physical constraint yet.
local modData = vehicle:getModData()
if not modData then return false end
local expectedOtherId = expectedOther:getId()
return (modData["towBarTowedVehicleId"] ~= nil
and modData["towBarTowedVehicleId"] ~= expectedOtherId)
or (modData["towBarTowingVehicleId"] ~= nil
and modData["towBarTowingVehicleId"] ~= expectedOtherId)
end
breakTowBarPair = function(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return end
local vehicleAReferencesB = vehicleA:getVehicleTowing() == vehicleB
or vehicleA:getVehicleTowedBy() == vehicleB
local vehicleBReferencesA = vehicleB:getVehicleTowing() == vehicleA
or vehicleB:getVehicleTowedBy() == vehicleA
if vehicleAReferencesB then
vehicleA:breakConstraint(true, true)
elseif vehicleBReferencesA then
vehicleB:breakConstraint(true, true)
end
end
local function applyDetachSync(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 hasConflictingTowLink(vehicleA, vehicleB) or hasConflictingTowLink(vehicleB, vehicleA) then
return
end
breakTowBarPair(vehicleA, vehicleB)
TowBarMod.Sync.appliedPairs[tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())] = nil
if TowBarMod.Hook and TowBarMod.Hook.cleanupDetachedTowBar then
pcall(TowBarMod.Hook.cleanupDetachedTowBar, vehicleA, vehicleB)
end
end
local function onServerCommand(module, command, args)
if module ~= "towbar" then return end
if command == "forceAttachSync" then
applyAttachSync(args)
elseif command == "forceDetachSync" or command == "spontaneousDetachSync" then
applyDetachSync(args)
end
end
TowBarMod.Sync.applyAttachSync = applyAttachSync
TowBarMod.Sync.applyDetachSync = applyDetachSync
Events.OnServerCommand.Add(onServerCommand)
if Events.OnSpawnVehicleEnd then
Events.OnSpawnVehicleEnd.Add(clearAppliedPairForVehicle)
end