Working 42.20 MP
This commit is contained in:
@@ -5,6 +5,34 @@ 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)
|
||||
@@ -15,201 +43,218 @@ end
|
||||
local function clearAppliedPairForVehicle(vehicle)
|
||||
if not vehicle then return end
|
||||
local vehicleId = vehicle:getId()
|
||||
for key in pairs(TowBarMod.Sync.appliedPairs) do
|
||||
for key in pairs(Sync.appliedPairs) do
|
||||
if pairKeyContainsVehicle(key, vehicleId) then
|
||||
TowBarMod.Sync.appliedPairs[key] = nil
|
||||
Sync.appliedPairs[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function resolveVehicle(id)
|
||||
if not id then return nil end
|
||||
return getVehicleById(id)
|
||||
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 ensureAttachment(vehicle, attachmentId)
|
||||
if not vehicle or not attachmentId then return false end
|
||||
local function isPairLinked(vehicleA, vehicleB)
|
||||
return vehicleA and vehicleB
|
||||
and vehicleA:getVehicleTowing() == vehicleB
|
||||
and vehicleB:getVehicleTowedBy() == vehicleA
|
||||
end
|
||||
|
||||
local script = vehicle:getScript()
|
||||
if not script then return false end
|
||||
if script:getAttachmentById(attachmentId) ~= nil then return true 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 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
|
||||
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
|
||||
|
||||
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)
|
||||
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 isLinked(vehicleA, vehicleB)
|
||||
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
|
||||
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
|
||||
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 = 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
|
||||
local key = pairKey(vehicleA, vehicleB)
|
||||
if not forceReattach and isPairLinked(vehicleA, vehicleB) and Sync.appliedPairs[key] 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)
|
||||
-- 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)
|
||||
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
|
||||
local desiredKey = argsPairKey(args)
|
||||
local wasApplied = desiredKey and Sync.appliedPairs[desiredKey] == true
|
||||
if desiredKey then
|
||||
Sync.desiredPairs[desiredKey] = nil
|
||||
Sync.appliedPairs[desiredKey] = nil
|
||||
end
|
||||
breakTowBarPair(vehicleA, vehicleB)
|
||||
TowBarMod.Sync.appliedPairs[tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())] = nil
|
||||
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
|
||||
pcall(TowBarMod.Hook.cleanupDetachedTowBar, vehicleA, vehicleB)
|
||||
TowBarMod.Hook.cleanupDetachedTowBar(vehicleA, vehicleB)
|
||||
end
|
||||
end
|
||||
|
||||
local function onServerCommand(module, command, args)
|
||||
if module ~= "towbar" then return 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
|
||||
end)
|
||||
|
||||
TowBarMod.Sync.applyAttachSync = applyAttachSync
|
||||
TowBarMod.Sync.applyDetachSync = applyDetachSync
|
||||
|
||||
Events.OnServerCommand.Add(onServerCommand)
|
||||
if Events.OnSpawnVehicleEnd then
|
||||
Events.OnSpawnVehicleEnd.Add(clearAppliedPairForVehicle)
|
||||
Events.OnSpawnVehicleEnd.Add(onSpawnVehicle)
|
||||
end
|
||||
if Events.OnEnterVehicle then
|
||||
Events.OnEnterVehicle.Add(forceReattachForDriver)
|
||||
end
|
||||
if Events.OnSwitchVehicleSeat then
|
||||
Events.OnSwitchVehicleSeat.Add(forceReattachForDriver)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user