Working 42.20 MP

This commit is contained in:
2026-08-19 12:12:12 -04:00
parent e403323017
commit e99f3bbff9
22 changed files with 2262 additions and 825 deletions
+201 -156
View File
@@ -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