216 lines
8.0 KiB
Lua
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
|