Files
2026-08-19 12:12:12 -04:00

261 lines
9.2 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 {}
TowBarMod.Sync.desiredPairs = TowBarMod.Sync.desiredPairs or {}
if not TowBarMod.RigidTow or not TowBarMod.RigidTow.attach then
require("TowBar/RigidTow")
end
local Sync = TowBarMod.Sync
local function argsPairKey(args)
if type(args) ~= "table" then return nil end
local vehicleA = tonumber(args.vehicleA)
local vehicleB = tonumber(args.vehicleB)
if not vehicleA or not vehicleB then return nil end
return tostring(vehicleA) .. ":" .. tostring(vehicleB)
end
local function copyAttachArgs(args)
return {
vehicleA = tonumber(args.vehicleA),
vehicleB = tonumber(args.vehicleB),
attachmentA = args.attachmentA,
attachmentB = args.attachmentB
}
end
local function pairKey(vehicleA, vehicleB)
return tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())
end
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(Sync.appliedPairs) do
if pairKeyContainsVehicle(key, vehicleId) then
Sync.appliedPairs[key] = nil
end
end
end
local function resolvePair(args)
if type(args) ~= "table" then return nil, nil end
local vehicleA = args.vehicleA and getVehicleById(tonumber(args.vehicleA)) or nil
local vehicleB = args.vehicleB and getVehicleById(tonumber(args.vehicleB)) or nil
return vehicleA, vehicleB
end
local function isPairLinked(vehicleA, vehicleB)
return vehicleA and vehicleB
and vehicleA:getVehicleTowing() == vehicleB
and vehicleB:getVehicleTowedBy() == vehicleA
end
local function hasConflictingLink(vehicle, expectedOther)
if not vehicle or not expectedOther then return false end
local towing = vehicle:getVehicleTowing()
local towedBy = vehicle:getVehicleTowedBy()
return (towing ~= nil and towing ~= expectedOther)
or (towedBy ~= nil and towedBy ~= expectedOther)
end
local function preparePairState(vehicleA, vehicleB, attachmentA, attachmentB)
local towingMd = vehicleA:getModData()
local towedMd = vehicleB:getModData()
if not towingMd or not towedMd then return false end
local currentScript = vehicleB:getScriptName()
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
towingMd.isTowingByTowBar = true
towingMd.towed = false
towingMd.towBarTowedVehicleId = vehicleB:getId()
towingMd.towBarTowingVehicleId = nil
towingMd.towBarExpectedAttachment = attachmentA
towedMd.isTowingByTowBar = true
towedMd.towed = true
towedMd.towBarTowedVehicleId = nil
towedMd.towBarTowingVehicleId = vehicleA:getId()
towedMd.towBarExpectedAttachment = attachmentB
vehicleA:transmitModData()
vehicleB:transmitModData()
return true
end
local function isLocalDriver(vehicleA, playerObj)
if not vehicleA or not playerObj then return false end
return vehicleA:isDriver(playerObj)
end
local function applyAttachSync(args, playerObj, forceReattach)
local desiredKey = argsPairKey(args)
if not desiredKey then return false end
Sync.desiredPairs[desiredKey] = copyAttachArgs(args)
local vehicleA, vehicleB = resolvePair(args)
if not vehicleA or not vehicleB then return false end
local attachmentA = args.attachmentA or "trailer"
local attachmentB = args.attachmentB or "trailerfront"
if not preparePairState(vehicleA, vehicleB, attachmentA, attachmentB) then return false end
local localPlayer = playerObj
if not localPlayer and type(getPlayer) == "function" then localPlayer = getPlayer() end
if not isLocalDriver(vehicleA, localPlayer) then
-- Portable installation happens while the player is outside. Keep the
-- server/native relation until this client becomes the towing vehicle's
-- physics owner; otherwise the local-only rigid add is discarded.
return true
end
if hasConflictingLink(vehicleA, vehicleB) or hasConflictingLink(vehicleB, vehicleA) then
return false
end
local key = pairKey(vehicleA, vehicleB)
if not forceReattach and isPairLinked(vehicleA, vehicleB) and Sync.appliedPairs[key] then
return true
end
-- This is deliberately the same one-shot method as WreckerSyncClient:
-- replace any exact native relation with one client-local rigid relation.
if TowBarMod.RigidTow.attach(vehicleA, vehicleB, attachmentA, attachmentB) ~= true then
return false
end
Sync.appliedPairs[key] = true
return true
end
local function applyDetachSync(args)
local desiredKey = argsPairKey(args)
local wasApplied = desiredKey and Sync.appliedPairs[desiredKey] == true
if desiredKey then
Sync.desiredPairs[desiredKey] = nil
Sync.appliedPairs[desiredKey] = nil
end
local vehicleA, vehicleB = resolvePair(args)
if not vehicleA or not vehicleB then return end
if hasConflictingLink(vehicleA, vehicleB) or hasConflictingLink(vehicleB, vehicleA) then return end
local key = pairKey(vehicleA, vehicleB)
Sync.appliedPairs[key] = nil
if wasApplied and TowBarMod.RigidTow and TowBarMod.RigidTow.breakExactPair then
TowBarMod.RigidTow.breakExactPair(vehicleA, vehicleB)
elseif wasApplied and isPairLinked(vehicleA, vehicleB) then
vehicleA:breakConstraint(true, true)
end
if TowBarMod.Hook and TowBarMod.Hook.cleanupDetachedTowBar then
TowBarMod.Hook.cleanupDetachedTowBar(vehicleA, vehicleB)
end
end
Sync.applyAttachSync = applyAttachSync
Sync.applyDetachSync = applyDetachSync
local function retryDesiredPairsForDriver(character)
if not character then return end
local desired = {}
for _, args in pairs(Sync.desiredPairs) do
local vehicleA = args.vehicleA and getVehicleById(tonumber(args.vehicleA)) or nil
if vehicleA and vehicleA:isDriver(character) then
desired[#desired + 1] = args
end
end
for i = 1, #desired do
applyAttachSync(desired[i], character)
end
end
local function getPersistedPairArgsForDriver(character)
if not character or type(character.getVehicle) ~= "function" then return nil end
local vehicleA = character:getVehicle()
if not vehicleA or not vehicleA:isDriver(character) then return nil end
local towingMd = vehicleA:getModData()
if not towingMd or towingMd.isTowingByTowBar ~= true or towingMd.towed == true then return nil end
local vehicleBId = tonumber(towingMd.towBarTowedVehicleId)
local vehicleB = vehicleBId and getVehicleById(vehicleBId) or nil
if not vehicleB then return nil end
local towedMd = vehicleB:getModData()
if not towedMd or towedMd.isTowingByTowBar ~= true or towedMd.towed ~= true
or tonumber(towedMd.towBarTowingVehicleId) ~= vehicleA:getId() then
return nil
end
return {
vehicleA = vehicleA:getId(),
vehicleB = vehicleB:getId(),
attachmentA = towingMd.towBarExpectedAttachment or "trailer",
attachmentB = towedMd.towBarExpectedAttachment or "trailerfront"
}
end
local function forceReattachForDriver(character)
if not character then return end
local persistedArgs = getPersistedPairArgsForDriver(character)
if persistedArgs then
applyAttachSync(persistedArgs, character, true)
return
end
-- Initial installation may have the authoritative command before all
-- reciprocal modData reaches this client. Retained server args are still
-- safe because only a driver-owned exact pair can pass applyAttachSync.
local desired = {}
for _, args in pairs(Sync.desiredPairs) do
local vehicleA = args.vehicleA and getVehicleById(tonumber(args.vehicleA)) or nil
if vehicleA and vehicleA:isDriver(character) then desired[#desired + 1] = args end
end
for i = 1, #desired do
applyAttachSync(desired[i], character, true)
end
end
local function onSpawnVehicle(vehicle)
clearAppliedPairForVehicle(vehicle)
if type(getPlayer) == "function" then
retryDesiredPairsForDriver(getPlayer())
end
end
Events.OnServerCommand.Add(function(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)
if Events.OnSpawnVehicleEnd then
Events.OnSpawnVehicleEnd.Add(onSpawnVehicle)
end
if Events.OnEnterVehicle then
Events.OnEnterVehicle.Add(forceReattachForDriver)
end
if Events.OnSwitchVehicleSeat then
Events.OnSwitchVehicleSeat.Add(forceReattachForDriver)
end