Working 42.20 MP
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
if isServer() then return end
|
||||
|
||||
if not TowBarMod then TowBarMod = {} end
|
||||
TowBarMod.RigidTow = TowBarMod.RigidTow or {}
|
||||
|
||||
local RigidTow = TowBarMod.RigidTow
|
||||
|
||||
local function isExactPairLinked(towingVehicle, towedVehicle)
|
||||
return towingVehicle and towedVehicle
|
||||
and towingVehicle:getVehicleTowing() == towedVehicle
|
||||
and towedVehicle:getVehicleTowedBy() == towingVehicle
|
||||
end
|
||||
|
||||
local function hasConflictingLink(vehicle, expectedOther)
|
||||
if not vehicle or not expectedOther then return true end
|
||||
local towing = vehicle:getVehicleTowing()
|
||||
local towedBy = vehicle:getVehicleTowedBy()
|
||||
return (towing ~= nil and towing ~= expectedOther)
|
||||
or (towedBy ~= nil and towedBy ~= expectedOther)
|
||||
end
|
||||
|
||||
local function attachmentExists(vehicle, attachmentId)
|
||||
if not vehicle or type(attachmentId) ~= "string" then return false end
|
||||
local script = vehicle:getScript()
|
||||
return script ~= nil and script:getAttachmentById(attachmentId) ~= nil
|
||||
end
|
||||
|
||||
local function breakExactPair(towingVehicle, towedVehicle)
|
||||
if not towingVehicle or not towedVehicle then return end
|
||||
if towingVehicle:getVehicleTowing() == towedVehicle
|
||||
or towingVehicle:getVehicleTowedBy() == towedVehicle then
|
||||
towingVehicle:breakConstraint(true, true)
|
||||
elseif towedVehicle:getVehicleTowing() == towingVehicle
|
||||
or towedVehicle:getVehicleTowedBy() == towingVehicle then
|
||||
towedVehicle:breakConstraint(true, true)
|
||||
end
|
||||
end
|
||||
|
||||
function RigidTow.attach(towingVehicle, towedVehicle, attachmentA, attachmentB)
|
||||
if not towingVehicle or not towedVehicle or towingVehicle == towedVehicle then return false end
|
||||
if hasConflictingLink(towingVehicle, towedVehicle)
|
||||
or hasConflictingLink(towedVehicle, towingVehicle) then
|
||||
return false
|
||||
end
|
||||
if not attachmentExists(towingVehicle, attachmentA)
|
||||
or not attachmentExists(towedVehicle, attachmentB) then
|
||||
return false
|
||||
end
|
||||
|
||||
if TowBarMod.Utils and TowBarMod.Utils.updateAttachmentsForRigidTow then
|
||||
TowBarMod.Utils.updateAttachmentsForRigidTow(
|
||||
towingVehicle, towedVehicle, attachmentA, attachmentB
|
||||
)
|
||||
end
|
||||
|
||||
-- Match the proven wrecker sequence exactly. The fake trailer script is
|
||||
-- used only while Bullet creates the rigid constraint; leaving it active
|
||||
-- until the broader post-attach hook is unreliable in multiplayer because
|
||||
-- that hook may return early while vehicle state is still replicating.
|
||||
local towedModData = towedVehicle:getModData()
|
||||
local currentScript = towedVehicle:getScriptName()
|
||||
local originalScript = towedModData and towedModData.towBarOriginalScriptName or nil
|
||||
if originalScript == nil and currentScript ~= "notTowingA_Trailer" then
|
||||
originalScript = currentScript
|
||||
if towedModData then
|
||||
towedModData.towBarOriginalScriptName = originalScript
|
||||
end
|
||||
end
|
||||
if originalScript == nil or originalScript == "notTowingA_Trailer" then
|
||||
return false
|
||||
end
|
||||
|
||||
breakExactPair(towingVehicle, towedVehicle)
|
||||
if TowBarMod.Hook and TowBarMod.Hook.applyFreeRollingTowState then
|
||||
TowBarMod.Hook.applyFreeRollingTowState(towedVehicle)
|
||||
elseif TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
|
||||
-- Compatibility fallback for older hook modules and the regression
|
||||
-- harness. Current builds expose the narrower free-roll helper.
|
||||
TowBarMod.Hook.setVehiclePostAttach(nil, towedVehicle, towingVehicle)
|
||||
end
|
||||
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
|
||||
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, "notTowingA_Trailer")
|
||||
else
|
||||
towedVehicle:setScriptName("notTowingA_Trailer")
|
||||
end
|
||||
|
||||
towingVehicle:addPointConstraint(nil, towedVehicle, attachmentA, attachmentB, true)
|
||||
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
|
||||
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, originalScript)
|
||||
else
|
||||
towedVehicle:setScriptName(originalScript)
|
||||
end
|
||||
if TowBarMod.Hook and TowBarMod.Hook.setVehiclePostAttach then
|
||||
-- Multiplayer updates the reciprocal towing getters after this Lua
|
||||
-- call. The authoritative sync already validated the pair, so finalize
|
||||
-- with the known towing vehicle instead of treating that delay as a
|
||||
-- failed constraint submission.
|
||||
TowBarMod.Hook.setVehiclePostAttach(nil, towedVehicle, towingVehicle)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
RigidTow.isExactPairLinked = isExactPairLinked
|
||||
RigidTow.breakExactPair = breakExactPair
|
||||
@@ -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
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
if not TowBarMod then TowBarMod = {} end
|
||||
if not TowBarMod.Hook then TowBarMod.Hook = {} end
|
||||
|
||||
require("TowBar/VehicleCompatibility")
|
||||
|
||||
local DefaultTowBarTowMass = 200
|
||||
local AutoReattachCooldownHours = 1 / 7200 -- 0.5 seconds
|
||||
TowBarMod.Hook.lastAutoReattachAtByVehicle = TowBarMod.Hook.lastAutoReattachAtByVehicle or {}
|
||||
local AutoReattachPlayerCooldownHours = 1 / 14400 -- 0.25 seconds
|
||||
TowBarMod.Hook.lastAutoReattachAtByPlayer = TowBarMod.Hook.lastAutoReattachAtByPlayer or {}
|
||||
local FreeRollTickInterval = 15
|
||||
local freeRollTickCounter = 0
|
||||
|
||||
@@ -44,7 +42,7 @@ local function applyFreeRollingTowState(vehicle)
|
||||
modData.towBarOriginalBrakingForce = vehicle:getBrakingForce()
|
||||
end
|
||||
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrakeOn", "isParkingBrakeOn")
|
||||
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrake", "isParkingBrake")
|
||||
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrake", "getParkingBrake")
|
||||
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalHandbrake", "isHandbrake")
|
||||
|
||||
local configuredTowMass = TowBarMod.Config and tonumber(TowBarMod.Config.towedVehicleRollingMass)
|
||||
@@ -61,10 +59,13 @@ local function applyFreeRollingTowState(vehicle)
|
||||
tryVehicleCall(vehicle, "setHandbrake", false)
|
||||
end
|
||||
|
||||
vehicle:constraintChanged()
|
||||
vehicle:updateTotalMass()
|
||||
-- Match the working wrecker path. Recalculating total mass here would
|
||||
-- immediately replace the temporary towing mass in multiplayer; creating
|
||||
-- the rigid constraint below notifies Bullet of the changed vehicle state.
|
||||
end
|
||||
|
||||
TowBarMod.Hook.applyFreeRollingTowState = applyFreeRollingTowState
|
||||
|
||||
local function restoreFreeRollingTowState(vehicle, modData)
|
||||
if not vehicle or not modData then return end
|
||||
|
||||
@@ -88,25 +89,6 @@ local function restoreFreeRollingTowState(vehicle, modData)
|
||||
vehicle:updateTotalMass()
|
||||
end
|
||||
|
||||
local function isTowBarTowPair(towingVehicle, towedVehicle)
|
||||
if not towingVehicle or not towedVehicle then return false end
|
||||
|
||||
local towingModData = towingVehicle:getModData()
|
||||
local towedModData = towedVehicle:getModData()
|
||||
if not towingModData or not towedModData then return false end
|
||||
|
||||
if towingModData["isTowingByTowBar"] and towedModData["isTowingByTowBar"] and towedModData["towed"] then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Rejoin fallback: original towbar state on the towed vehicle is enough to reapply rigid spacing.
|
||||
if towedModData.towBarOriginalScriptName ~= nil then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function getTowBarItem(playerObj)
|
||||
if not playerObj then return nil end
|
||||
local inventory = playerObj:getInventory()
|
||||
@@ -116,15 +98,8 @@ end
|
||||
|
||||
local function sendTowAttachCommand(playerObj, args)
|
||||
if not playerObj or not args then return end
|
||||
|
||||
-- MP-safe/server-authoritative attach path (Landtrain style).
|
||||
if isClient() and isMultiplayer() then
|
||||
sendClientCommand(playerObj, "towbar", "attachTowBar", args)
|
||||
return
|
||||
end
|
||||
|
||||
-- Keep vanilla attach path for SP/local behavior.
|
||||
sendClientCommand(playerObj, "vehicle", "attachTrailer", args)
|
||||
-- SP and MP now share the same authoritative attach lifecycle.
|
||||
sendClientCommand(playerObj, "towbar", "attachTowBar", args)
|
||||
end
|
||||
|
||||
local TowbarVariantSize = 24
|
||||
@@ -138,6 +113,8 @@ local TowbarVisualScale = 2.5
|
||||
local TowbarModelLength = 0.9714089036
|
||||
local TowbarScaledModelLength = TowbarModelLength * TowbarVisualScale
|
||||
local TowbarModelHalfLength = TowbarScaledModelLength / 2
|
||||
local VanillaScaleMin = 1.5
|
||||
local VanillaScaleMax = 2.0
|
||||
|
||||
local function getTowbarFrontEdgeZ(script)
|
||||
if not script then return nil end
|
||||
@@ -178,16 +155,87 @@ local function getTowbarModelSlot(script)
|
||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||
end
|
||||
|
||||
local function getVehicleModelScale(script)
|
||||
if not script then return nil end
|
||||
|
||||
local ok, result = pcall(function()
|
||||
return script:getModelScale()
|
||||
end)
|
||||
if ok and type(result) == "number" then return result end
|
||||
|
||||
ok, result = pcall(function()
|
||||
local model = script:getModel()
|
||||
return model and model:getScale() or nil
|
||||
end)
|
||||
if ok and type(result) == "number" then return result end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
local function isVanillaScale(script)
|
||||
local modelScale = getVehicleModelScale(script)
|
||||
if modelScale == nil then return true end
|
||||
|
||||
local configuredMin = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMin)
|
||||
local configuredMax = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMax)
|
||||
return modelScale >= (configuredMin or VanillaScaleMin)
|
||||
and modelScale <= (configuredMax or VanillaScaleMax)
|
||||
end
|
||||
|
||||
local function getTowbarIndexVanilla(script)
|
||||
if not script then return nil end
|
||||
local ok, shape = pcall(function() return script:getPhysicsChassisShape() end)
|
||||
if not ok or not shape then return nil end
|
||||
local zOk, shapeZ = pcall(function() return shape:z() end)
|
||||
if not zOk or type(shapeZ) ~= "number" then return nil end
|
||||
|
||||
local z = shapeZ / 2 - 0.1
|
||||
local index = math.floor((z * 2 / 3 - 1) * 10)
|
||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||
end
|
||||
|
||||
local function getTowbarIndexSmallScale(script)
|
||||
if not script then return nil end
|
||||
|
||||
local maxAbsTowZ = nil
|
||||
local trailer = script:getAttachmentById("trailer")
|
||||
if trailer then maxAbsTowZ = math.abs(trailer:getOffset():z()) end
|
||||
local trailerFront = script:getAttachmentById("trailerfront")
|
||||
if trailerFront then
|
||||
local frontAbsZ = math.abs(trailerFront:getOffset():z())
|
||||
if not maxAbsTowZ or frontAbsZ > maxAbsTowZ then maxAbsTowZ = frontAbsZ end
|
||||
end
|
||||
|
||||
if maxAbsTowZ == nil then return nil end
|
||||
local index = math.floor((maxAbsTowZ + 0.1 - 1.0) * 10)
|
||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||
end
|
||||
|
||||
local function getLegacyTowbarModelSlot(script)
|
||||
local useNormalPart = isVanillaScale(script)
|
||||
local index = getTowbarIndexVanilla(script)
|
||||
if not useNormalPart then
|
||||
index = getTowbarIndexSmallScale(script) or index
|
||||
if index == nil then
|
||||
local offset = TowBarMod.Config and tonumber(TowBarMod.Config.smallScaleTowbarIndexOffset) or 2
|
||||
index = math.max(0, math.min(TowbarMaxIndex, offset))
|
||||
end
|
||||
end
|
||||
return index, useNormalPart
|
||||
end
|
||||
|
||||
local function setTowBarModelVisible(vehicle, isVisible)
|
||||
if not vehicle then return end
|
||||
|
||||
local normalPart = vehicle:getPartById("towbar")
|
||||
local largePart = vehicle:getPartById("towbarLarge")
|
||||
if normalPart == nil and largePart == nil then return end
|
||||
local ki5Part = vehicle:getPartById("towbarKI5")
|
||||
if normalPart == nil and largePart == nil and ki5Part == nil then return end
|
||||
|
||||
for j = 0, TowbarVariantSize - 1 do
|
||||
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
|
||||
if largePart then largePart:setModelVisible("towbar" .. j, false) end
|
||||
if ki5Part then ki5Part:setModelVisible("towbar" .. j, false) end
|
||||
end
|
||||
|
||||
if not isVisible then
|
||||
@@ -201,8 +249,16 @@ local function setTowBarModelVisible(vehicle, isVisible)
|
||||
return
|
||||
end
|
||||
|
||||
local index = getTowbarModelSlot(script)
|
||||
local part = normalPart
|
||||
local isKi5 = TowBarMod.Compatibility.isKi5Vehicle(vehicle)
|
||||
local index, useNormalPart
|
||||
if isKi5 then
|
||||
index = getTowbarModelSlot(script)
|
||||
else
|
||||
index, useNormalPart = getLegacyTowbarModelSlot(script)
|
||||
end
|
||||
|
||||
local part = isKi5 and ki5Part or (useNormalPart and normalPart or largePart)
|
||||
if part == nil and not isKi5 then part = normalPart or largePart end
|
||||
if part and index ~= nil then
|
||||
part:setModelVisible("towbar" .. index, true)
|
||||
end
|
||||
@@ -259,23 +315,6 @@ local function resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedMo
|
||||
return attachmentA, attachmentB
|
||||
end
|
||||
|
||||
local function hasTowBarTowState(modData)
|
||||
if not modData then
|
||||
return false
|
||||
end
|
||||
|
||||
if modData["isTowingByTowBar"] and modData["towed"] then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Rejoin fallback: legacy saves may only have the original-script marker.
|
||||
if modData.towBarOriginalScriptName ~= nil then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function isActiveTowBarTowedVehicle(vehicle, modData)
|
||||
if not vehicle or not modData then
|
||||
return false
|
||||
@@ -293,142 +332,22 @@ local function isActiveTowBarTowedVehicle(vehicle, modData)
|
||||
return false
|
||||
end
|
||||
|
||||
local function reattachTowBarPair(playerObj, towingVehicle, towedVehicle, requireDriver)
|
||||
if not playerObj or not towingVehicle or not towedVehicle then
|
||||
return false
|
||||
end
|
||||
if requireDriver and not towingVehicle:isDriver(playerObj) then
|
||||
return false
|
||||
end
|
||||
|
||||
local towingModData = towingVehicle:getModData()
|
||||
local towedModData = towedVehicle:getModData()
|
||||
if not towingModData or not towedModData then
|
||||
return false
|
||||
end
|
||||
if requireDriver then
|
||||
if not isTowBarTowPair(towingVehicle, towedVehicle) then
|
||||
return false
|
||||
end
|
||||
else
|
||||
if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
|
||||
if not attachmentA or not attachmentB then
|
||||
return false
|
||||
end
|
||||
|
||||
local towingScript = towingVehicle:getScript()
|
||||
local towedScript = towedVehicle:getScript()
|
||||
if not towingScript or not towedScript then
|
||||
return false
|
||||
end
|
||||
if not towingScript:getAttachmentById(attachmentA) or not towedScript:getAttachmentById(attachmentB) then
|
||||
return false
|
||||
end
|
||||
|
||||
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
|
||||
|
||||
towedModData.towBarOriginalScriptName = towedModData.towBarOriginalScriptName or towedVehicle:getScriptName()
|
||||
applyFreeRollingTowState(towedVehicle)
|
||||
|
||||
towingModData["isTowingByTowBar"] = true
|
||||
towingModData["towed"] = false
|
||||
towingModData["towBarTowedVehicleId"] = towedVehicle:getId()
|
||||
towingModData["towBarTowingVehicleId"] = nil
|
||||
towingModData["towBarExpectedAttachment"] = attachmentA
|
||||
towedModData["isTowingByTowBar"] = true
|
||||
towedModData["towed"] = true
|
||||
towedModData["towBarTowedVehicleId"] = nil
|
||||
towedModData["towBarTowingVehicleId"] = towingVehicle:getId()
|
||||
towedModData["towBarExpectedAttachment"] = attachmentB
|
||||
towingVehicle:transmitModData()
|
||||
towedVehicle:transmitModData()
|
||||
|
||||
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, "notTowingA_Trailer")
|
||||
|
||||
local args = {
|
||||
vehicleA = towingVehicle:getId(),
|
||||
vehicleB = towedVehicle:getId(),
|
||||
attachmentA = attachmentA,
|
||||
attachmentB = attachmentB
|
||||
}
|
||||
sendTowAttachCommand(playerObj, args)
|
||||
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
|
||||
return true
|
||||
end
|
||||
|
||||
local function reattachTowBarPairAfterCleanDetach(playerObj, towingVehicle, towedVehicle, requireDriver)
|
||||
if not playerObj or not towingVehicle or not towedVehicle then
|
||||
return false
|
||||
end
|
||||
if requireDriver and not towingVehicle:isDriver(playerObj) then
|
||||
return false
|
||||
end
|
||||
|
||||
local detachArgs = {
|
||||
towingVehicle = towingVehicle:getId(),
|
||||
vehicle = towedVehicle:getId()
|
||||
}
|
||||
sendClientCommand(playerObj, "towbar", "detachTowBar", detachArgs)
|
||||
|
||||
-- World load/spawn can restore constraints in a bad state. Reattach one
|
||||
-- short tick later so the detach is fully applied first.
|
||||
ISTimedActionQueue.add(TowBarScheduleAction:new(
|
||||
playerObj,
|
||||
1,
|
||||
reattachTowBarPair,
|
||||
towingVehicle,
|
||||
towedVehicle,
|
||||
requireDriver
|
||||
))
|
||||
return true
|
||||
end
|
||||
|
||||
local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft)
|
||||
if not vehicle then return end
|
||||
|
||||
local modData = vehicle:getModData()
|
||||
if not hasTowBarTowState(modData) then
|
||||
return
|
||||
end
|
||||
|
||||
local retries = tonumber(retriesLeft) or 0
|
||||
local localPlayer = playerObj or getPlayer()
|
||||
local towingVehicle = vehicle:getVehicleTowedBy()
|
||||
|
||||
if towingVehicle then
|
||||
-- Apply rigid spacing as soon as the tow link exists to avoid a visible
|
||||
-- bumper-to-bumper snap. Server/SP reconciliation owns recovery and
|
||||
-- never refunds or consumes an item while loading a save.
|
||||
TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
|
||||
return
|
||||
end
|
||||
|
||||
if localPlayer and retries > 0 then
|
||||
-- During world load, tow links can become available a few ticks later.
|
||||
ISTimedActionQueue.add(TowBarScheduleAction:new(localPlayer, 10, recoverTowBarVehicleAfterLoad, vehicle, retries - 1))
|
||||
return
|
||||
end
|
||||
|
||||
-- Leave reciprocal saved state untouched. The authoritative audit will
|
||||
-- reconnect it when both vehicle chunks are loaded.
|
||||
end
|
||||
|
||||
function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, retriesLeft)
|
||||
function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, knownTowingVehicle)
|
||||
if not towedVehicle then return end
|
||||
|
||||
local towedModData = towedVehicle:getModData()
|
||||
if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then return end
|
||||
|
||||
if towedModData and towedModData.towBarOriginalScriptName then
|
||||
local towingVehicle = knownTowingVehicle or towedVehicle:getVehicleTowedBy()
|
||||
if not towingVehicle then return end
|
||||
|
||||
-- The rigid primitive passes the authoritative towing vehicle because MP
|
||||
-- reciprocal getters can lag behind accepted local constraint submission.
|
||||
if towedModData and towedModData.towBarOriginalScriptName
|
||||
and towedVehicle:getScriptName() ~= towedModData.towBarOriginalScriptName then
|
||||
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName)
|
||||
end
|
||||
|
||||
local towingVehicle = towedVehicle:getVehicleTowedBy()
|
||||
if towingVehicle then
|
||||
local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
|
||||
if attachmentA and attachmentB then
|
||||
@@ -443,7 +362,6 @@ function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, retriesLef
|
||||
towingVehicle:transmitModData()
|
||||
towedVehicle:transmitModData()
|
||||
end
|
||||
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -459,35 +377,9 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
|
||||
if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end
|
||||
|
||||
local towBarItem = getTowBarItem(playerObj)
|
||||
if towBarItem ~= nil and not (isClient() and isMultiplayer()) then
|
||||
sendClientCommand(playerObj, "towbar", "consumeTowBar", { itemId = towBarItem:getID() })
|
||||
end
|
||||
if towBarItem == nil then return end
|
||||
playerObj:setPrimaryHandItem(nil)
|
||||
|
||||
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
|
||||
|
||||
local towingModData = towingVehicle:getModData()
|
||||
local towedModData = towedVehicle:getModData()
|
||||
|
||||
towedModData.towBarOriginalScriptName = towedVehicle:getScriptName()
|
||||
applyFreeRollingTowState(towedVehicle)
|
||||
|
||||
towingModData["isTowingByTowBar"] = true
|
||||
towingModData["towed"] = false
|
||||
towingModData["towBarTowedVehicleId"] = towedVehicle:getId()
|
||||
towingModData["towBarTowingVehicleId"] = nil
|
||||
towingModData["towBarExpectedAttachment"] = attachmentA
|
||||
towedModData["isTowingByTowBar"] = true
|
||||
towedModData["towed"] = true
|
||||
towedModData["towBarTowedVehicleId"] = nil
|
||||
towedModData["towBarTowingVehicleId"] = towingVehicle:getId()
|
||||
towedModData["towBarExpectedAttachment"] = attachmentB
|
||||
towingVehicle:transmitModData()
|
||||
towedVehicle:transmitModData()
|
||||
|
||||
-- Match the known-good rigid tow path: fake trailer + vanilla attach command.
|
||||
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, "notTowingA_Trailer")
|
||||
|
||||
local args = {
|
||||
vehicleA = towingVehicle:getId(),
|
||||
vehicleB = towedVehicle:getId(),
|
||||
@@ -496,7 +388,6 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
|
||||
itemId = towBarItem and towBarItem:getID() or nil
|
||||
}
|
||||
sendTowAttachCommand(playerObj, args)
|
||||
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
|
||||
end
|
||||
|
||||
function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
|
||||
@@ -538,7 +429,6 @@ function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
|
||||
towingVehicle:transmitModData()
|
||||
towedVehicle:transmitModData()
|
||||
|
||||
TowBarMod.Hook.lastAutoReattachAtByVehicle[towingVehicle:getId()] = nil
|
||||
setTowBarModelVisible(towedVehicle, false)
|
||||
end
|
||||
|
||||
@@ -547,46 +437,6 @@ function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehic
|
||||
|
||||
local args = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() }
|
||||
sendClientCommand(playerObj, "towbar", "detachTowBar", args)
|
||||
|
||||
TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
|
||||
end
|
||||
|
||||
function TowBarMod.Hook.reattachTowBarFromDriverSeat(playerObj, towingVehicle)
|
||||
if not playerObj or not towingVehicle then return end
|
||||
|
||||
local towedVehicle = towingVehicle:getVehicleTowing()
|
||||
if not towedVehicle then return end
|
||||
|
||||
reattachTowBarPair(playerObj, towingVehicle, towedVehicle, true)
|
||||
end
|
||||
|
||||
local function tryAutoReattachFromCharacter(character)
|
||||
if not character or not instanceof(character, "IsoPlayer") or not character:isLocalPlayer() then return end
|
||||
|
||||
local playerObj = character
|
||||
local nowHours = getGameTime() and getGameTime():getWorldAgeHours() or 0
|
||||
local playerNum = playerObj:getPlayerNum()
|
||||
local lastPlayerHours = TowBarMod.Hook.lastAutoReattachAtByPlayer[playerNum]
|
||||
if lastPlayerHours and (nowHours - lastPlayerHours) < AutoReattachPlayerCooldownHours then
|
||||
return
|
||||
end
|
||||
|
||||
local towingVehicle = playerObj:getVehicle()
|
||||
if not towingVehicle then return end
|
||||
if not towingVehicle:isDriver(playerObj) then return end
|
||||
local towedVehicle = towingVehicle:getVehicleTowing()
|
||||
if not towedVehicle then return end
|
||||
if not isTowBarTowPair(towingVehicle, towedVehicle) then return end
|
||||
|
||||
local vehicleId = towingVehicle:getId()
|
||||
local lastHours = TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId]
|
||||
if lastHours and (nowHours - lastHours) < AutoReattachCooldownHours then
|
||||
return
|
||||
end
|
||||
|
||||
TowBarMod.Hook.lastAutoReattachAtByPlayer[playerNum] = nowHours
|
||||
TowBarMod.Hook.lastAutoReattachAtByVehicle[vehicleId] = nowHours
|
||||
TowBarMod.Hook.reattachTowBarFromDriverSeat(playerObj, towingVehicle)
|
||||
end
|
||||
|
||||
local function forEachCollectionItem(collection, callback)
|
||||
@@ -634,14 +484,6 @@ local function keepTowBarVehiclesFreeRolling()
|
||||
end)
|
||||
end
|
||||
|
||||
function TowBarMod.Hook.OnEnterVehicle(character)
|
||||
tryAutoReattachFromCharacter(character)
|
||||
end
|
||||
|
||||
function TowBarMod.Hook.OnSwitchVehicleSeat(character)
|
||||
tryAutoReattachFromCharacter(character)
|
||||
end
|
||||
|
||||
function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehicle)
|
||||
if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
|
||||
|
||||
@@ -725,20 +567,11 @@ function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle)
|
||||
end
|
||||
|
||||
function TowBarMod.Hook.OnSpawnVehicle(vehicle)
|
||||
recoverTowBarVehicleAfterLoad(nil, vehicle, 6)
|
||||
-- Server persistence snapshots own attach recovery after vehicle streaming.
|
||||
end
|
||||
|
||||
function TowBarMod.Hook.OnGameStart()
|
||||
local cell = getCell()
|
||||
if not cell then return end
|
||||
|
||||
local vehicles = cell:getVehicles()
|
||||
if not vehicles then return end
|
||||
|
||||
local playerObj = getPlayer()
|
||||
forEachCollectionItem(vehicles, function(vehicle)
|
||||
recoverTowBarVehicleAfterLoad(playerObj, vehicle, 6)
|
||||
end)
|
||||
-- Server persistence broadcasts the same attach snapshot used by a fresh pair.
|
||||
end
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
@@ -749,8 +582,9 @@ function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
|
||||
if not vehicle then return end
|
||||
local normalPart = vehicle:getPartById("towbar")
|
||||
local largePart = vehicle:getPartById("towbarLarge")
|
||||
if normalPart == nil and largePart == nil then
|
||||
print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
|
||||
local ki5Part = vehicle:getPartById("towbarKI5")
|
||||
if normalPart == nil and largePart == nil and ki5Part == nil then
|
||||
print("[TowBar DEV] No towbar model part found on vehicle " .. tostring(vehicle:getScriptName()))
|
||||
return
|
||||
end
|
||||
local script = vehicle:getScript()
|
||||
@@ -765,10 +599,11 @@ function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
|
||||
print("[TowBar DEV] chassisShape.z = " .. tostring(chassisZ) .. ", half = " .. tostring(halfZ))
|
||||
print("[TowBar DEV] frontEdgeZ = " .. tostring(script and getTowbarFrontEdgeZ(script) or nil) .. ", part = " .. selectedPart)
|
||||
print("[TowBar DEV] Formula picks index = " .. tostring(index) .. " (towbar" .. tostring(index) .. " at Z offset " .. tostring(1.0 + index * 0.1) .. ")")
|
||||
print("[TowBar DEV] Showing towbar0..towbar23 on both parts")
|
||||
print("[TowBar DEV] Showing towbar0..towbar23 on all parts")
|
||||
for j = 0, TowbarVariantSize - 1 do
|
||||
if normalPart then normalPart:setModelVisible("towbar" .. j, true) end
|
||||
if largePart then largePart:setModelVisible("towbar" .. j, true) end
|
||||
if ki5Part then ki5Part:setModelVisible("towbar" .. j, true) end
|
||||
end
|
||||
vehicle:doDamageOverlay()
|
||||
end
|
||||
@@ -777,14 +612,16 @@ function TowBarMod.Hook.devHideAllTowbarModels(playerObj, vehicle)
|
||||
if not vehicle then return end
|
||||
local normalPart = vehicle:getPartById("towbar")
|
||||
local largePart = vehicle:getPartById("towbarLarge")
|
||||
if normalPart == nil and largePart == nil then
|
||||
print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
|
||||
local ki5Part = vehicle:getPartById("towbarKI5")
|
||||
if normalPart == nil and largePart == nil and ki5Part == nil then
|
||||
print("[TowBar DEV] No towbar model part found on vehicle " .. tostring(vehicle:getScriptName()))
|
||||
return
|
||||
end
|
||||
print("[TowBar DEV] Hiding ALL towbar models on " .. tostring(vehicle:getScriptName()))
|
||||
for j = 0, TowbarVariantSize - 1 do
|
||||
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
|
||||
if largePart then largePart:setModelVisible("towbar" .. j, false) end
|
||||
if ki5Part then ki5Part:setModelVisible("towbar" .. j, false) end
|
||||
end
|
||||
vehicle:doDamageOverlay()
|
||||
end
|
||||
@@ -793,24 +630,32 @@ function TowBarMod.Hook.devShowSingleTowbar(playerObj, vehicle, index)
|
||||
if not vehicle then return end
|
||||
local normalPart = vehicle:getPartById("towbar")
|
||||
local largePart = vehicle:getPartById("towbarLarge")
|
||||
if normalPart == nil and largePart == nil then
|
||||
print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
|
||||
local ki5Part = vehicle:getPartById("towbarKI5")
|
||||
if normalPart == nil and largePart == nil and ki5Part == nil then
|
||||
print("[TowBar DEV] No towbar model part found on vehicle " .. tostring(vehicle:getScriptName()))
|
||||
return
|
||||
end
|
||||
|
||||
local localIndex = math.max(0, math.min(TowbarMaxIndex, index % TowbarVariantSize))
|
||||
local useLargePart = index >= TowbarVariantSize
|
||||
local selectedPartId = "towbar"
|
||||
if index >= TowbarVariantSize * 2 then
|
||||
selectedPartId = "towbarKI5"
|
||||
elseif index >= TowbarVariantSize then
|
||||
selectedPartId = "towbarLarge"
|
||||
end
|
||||
|
||||
for j = 0, TowbarVariantSize - 1 do
|
||||
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
|
||||
if largePart then largePart:setModelVisible("towbar" .. j, false) end
|
||||
if ki5Part then ki5Part:setModelVisible("towbar" .. j, false) end
|
||||
end
|
||||
|
||||
local part = useLargePart and largePart or normalPart
|
||||
local part = selectedPartId == "towbarKI5" and ki5Part
|
||||
or (selectedPartId == "towbarLarge" and largePart or normalPart)
|
||||
if part == nil then
|
||||
part = normalPart or largePart
|
||||
part = normalPart or largePart or ki5Part
|
||||
end
|
||||
print("[TowBar DEV] Showing only towbar" .. tostring(localIndex) .. " on part " .. tostring(useLargePart and "towbarLarge" or "towbar") .. " (Z offset " .. tostring(1.0 + localIndex * 0.1) .. ") on " .. tostring(vehicle:getScriptName()))
|
||||
print("[TowBar DEV] Showing only towbar" .. tostring(localIndex) .. " on part " .. selectedPartId .. " (Z offset " .. tostring(1.0 + localIndex * 0.1) .. ") on " .. tostring(vehicle:getScriptName()))
|
||||
if part then
|
||||
part:setModelVisible("towbar" .. localIndex, true)
|
||||
end
|
||||
@@ -821,6 +666,4 @@ Events.OnSpawnVehicleEnd.Add(TowBarMod.Hook.OnSpawnVehicle)
|
||||
if Events.OnGameStart then
|
||||
Events.OnGameStart.Add(TowBarMod.Hook.OnGameStart)
|
||||
end
|
||||
Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle)
|
||||
Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat)
|
||||
Events.OnTick.Add(keepTowBarVehiclesFreeRolling)
|
||||
|
||||
@@ -4,6 +4,26 @@ TowBarMod.WreckerSync = TowBarMod.WreckerSync or {}
|
||||
|
||||
local Sync = TowBarMod.WreckerSync
|
||||
Sync.appliedLevels = Sync.appliedLevels or {}
|
||||
Sync.desiredPairs = Sync.desiredPairs or {}
|
||||
|
||||
local function argsPairKey(args)
|
||||
if type(args) ~= "table" then return nil end
|
||||
local wreckerId = tonumber(args.wrecker)
|
||||
local targetId = tonumber(args.target)
|
||||
if not wreckerId or not targetId then return nil end
|
||||
return tostring(wreckerId) .. ":" .. tostring(targetId)
|
||||
end
|
||||
|
||||
local function copyAttachArgs(args)
|
||||
return {
|
||||
wrecker = tonumber(args.wrecker),
|
||||
target = tonumber(args.target),
|
||||
wreckerSqlId = tonumber(args.wreckerSqlId),
|
||||
targetSqlId = tonumber(args.targetSqlId),
|
||||
targetAttachment = args.targetAttachment,
|
||||
heightLevel = tonumber(args.heightLevel) or 0
|
||||
}
|
||||
end
|
||||
|
||||
local function pairKeyContainsVehicle(key, vehicleId)
|
||||
local id = tostring(vehicleId)
|
||||
@@ -123,8 +143,10 @@ local function setScriptSafely(vehicle, scriptName)
|
||||
return true
|
||||
end
|
||||
|
||||
local function applyAttachSync(args)
|
||||
local function applyAttachSync(args, forceReattach)
|
||||
if not args then return end
|
||||
local desiredKey = argsPairKey(args)
|
||||
if desiredKey then Sync.desiredPairs[desiredKey] = copyAttachArgs(args) end
|
||||
local wrecker = getVehicleById(args.wrecker)
|
||||
local target = getVehicleById(args.target)
|
||||
if not wrecker or not target then return end
|
||||
@@ -138,7 +160,7 @@ local function applyAttachSync(args)
|
||||
local key = pairKey(wrecker, target)
|
||||
local canonicalLevel = tonumber(args.heightLevel) or 0
|
||||
local appliedLevel = Sync.appliedLevels[key]
|
||||
if isPairLinked(wrecker, target) and appliedLevel == canonicalLevel then
|
||||
if not forceReattach and isPairLinked(wrecker, target) and appliedLevel == canonicalLevel then
|
||||
Sync.appliedLevels[key] = canonicalLevel
|
||||
applyFreeRollingState(target)
|
||||
return
|
||||
@@ -167,6 +189,8 @@ end
|
||||
|
||||
local function applyDetachSync(args)
|
||||
if not args then return end
|
||||
local desiredKey = argsPairKey(args)
|
||||
if desiredKey then Sync.desiredPairs[desiredKey] = nil end
|
||||
local wrecker = getVehicleById(args.wrecker)
|
||||
local target = getVehicleById(args.target)
|
||||
if not target then return end
|
||||
@@ -203,6 +227,30 @@ end
|
||||
Sync.applyAttachSync = applyAttachSync
|
||||
Sync.applyDetachSync = applyDetachSync
|
||||
|
||||
local function forceReattachForDriver(character)
|
||||
if not character or type(character.getVehicle) ~= "function" then return end
|
||||
local wrecker = character:getVehicle()
|
||||
if not wrecker or not wrecker:isDriver(character) then return end
|
||||
|
||||
local wreckerMd = wrecker:getModData()
|
||||
if not wreckerMd or wreckerMd.wreckerTowActive ~= true then return end
|
||||
local targetId = tonumber(wreckerMd.wreckerTowedVehicleId)
|
||||
local target = targetId and getVehicleById(targetId) or nil
|
||||
if not target then return end
|
||||
|
||||
local targetMd = target:getModData()
|
||||
if not targetMd or tonumber(targetMd.wreckerTowingVehicleId) ~= wrecker:getId() then return end
|
||||
|
||||
applyAttachSync({
|
||||
wrecker = wrecker:getId(),
|
||||
target = target:getId(),
|
||||
wreckerSqlId = wrecker:getSqlId(),
|
||||
targetSqlId = wreckerMd.wreckerTowedVehicleSqlId,
|
||||
targetAttachment = wreckerMd.wreckerTargetAttachment,
|
||||
heightLevel = tonumber(wreckerMd.wreckerHeightLevel) or 0
|
||||
}, true)
|
||||
end
|
||||
|
||||
Events.OnServerCommand.Add(function(module, command, args)
|
||||
if module ~= "towbar" then return end
|
||||
if command == "wreckerAttachSync" then
|
||||
@@ -214,3 +262,9 @@ end)
|
||||
if Events.OnSpawnVehicleEnd then
|
||||
Events.OnSpawnVehicleEnd.Add(clearAppliedLevelForVehicle)
|
||||
end
|
||||
if Events.OnEnterVehicle then
|
||||
Events.OnEnterVehicle.Add(forceReattachForDriver)
|
||||
end
|
||||
if Events.OnSwitchVehicleSeat then
|
||||
Events.OnSwitchVehicleSeat.Add(forceReattachForDriver)
|
||||
end
|
||||
|
||||
@@ -2,6 +2,8 @@ BTtow = {}
|
||||
BTtow.Create = {}
|
||||
BTtow.Init = {}
|
||||
|
||||
require("TowBar/VehicleCompatibility")
|
||||
|
||||
local TowbarVariantSize = 24
|
||||
local TowbarMaxIndex = TowbarVariantSize - 1
|
||||
local TowbarFirstZ = 1.0
|
||||
@@ -13,6 +15,8 @@ local TowbarVisualScale = 2.5
|
||||
local TowbarModelLength = 0.9714089036
|
||||
local TowbarScaledModelLength = TowbarModelLength * TowbarVisualScale
|
||||
local TowbarModelHalfLength = TowbarScaledModelLength / 2
|
||||
local VanillaScaleMin = 1.5
|
||||
local VanillaScaleMax = 2.0
|
||||
|
||||
local function getTowbarFrontEdgeZ(script)
|
||||
if not script then return nil end
|
||||
@@ -53,6 +57,59 @@ local function getTowbarModelSlot(script)
|
||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||
end
|
||||
|
||||
local function getVehicleModelScale(script)
|
||||
if not script then return nil end
|
||||
local ok, result = pcall(function() return script:getModelScale() end)
|
||||
if ok and type(result) == "number" then return result end
|
||||
ok, result = pcall(function()
|
||||
local model = script:getModel()
|
||||
return model and model:getScale() or nil
|
||||
end)
|
||||
return ok and type(result) == "number" and result or nil
|
||||
end
|
||||
|
||||
local function isVanillaScale(script)
|
||||
local modelScale = getVehicleModelScale(script)
|
||||
if modelScale == nil then return true end
|
||||
local configuredMin = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMin)
|
||||
local configuredMax = TowBarMod.Config and tonumber(TowBarMod.Config.vanillaTowbarModelScaleMax)
|
||||
return modelScale >= (configuredMin or VanillaScaleMin)
|
||||
and modelScale <= (configuredMax or VanillaScaleMax)
|
||||
end
|
||||
|
||||
local function getTowbarIndexVanilla(script)
|
||||
if not script then return nil end
|
||||
local ok, shape = pcall(function() return script:getPhysicsChassisShape() end)
|
||||
if not ok or not shape then return nil end
|
||||
local zOk, shapeZ = pcall(function() return shape:z() end)
|
||||
if not zOk or type(shapeZ) ~= "number" then return nil end
|
||||
local z = shapeZ / 2 - 0.1
|
||||
local index = math.floor((z * 2 / 3 - 1) * 10)
|
||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||
end
|
||||
|
||||
local function getTowbarIndexSmallScale(script)
|
||||
if not script then return nil end
|
||||
local maxAbsTowZ = nil
|
||||
local trailer = script:getAttachmentById("trailer")
|
||||
if trailer then maxAbsTowZ = math.abs(trailer:getOffset():z()) end
|
||||
local trailerFront = script:getAttachmentById("trailerfront")
|
||||
if trailerFront then
|
||||
local frontAbsZ = math.abs(trailerFront:getOffset():z())
|
||||
if not maxAbsTowZ or frontAbsZ > maxAbsTowZ then maxAbsTowZ = frontAbsZ end
|
||||
end
|
||||
if maxAbsTowZ == nil then return nil end
|
||||
local index = math.floor((maxAbsTowZ + 0.1 - 1.0) * 10)
|
||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||
end
|
||||
|
||||
local function getLegacyTowbarModelSlot(script)
|
||||
local useNormalPart = isVanillaScale(script)
|
||||
local index = getTowbarIndexVanilla(script)
|
||||
if not useNormalPart then index = getTowbarIndexSmallScale(script) or index end
|
||||
return index, useNormalPart
|
||||
end
|
||||
|
||||
function BTtow.Create.towbar(vehicle, part)
|
||||
if part == nil then return end
|
||||
for j=0, TowbarVariantSize - 1 do
|
||||
@@ -72,8 +129,17 @@ function BTtow.Init.towbar(vehicle, part)
|
||||
if modData and modData["isTowingByTowBar"] and modData["towed"] then
|
||||
local script = vehicle:getScript()
|
||||
if script then
|
||||
local index = getTowbarModelSlot(script)
|
||||
local shouldShowOnThisPart = part:getId() == "towbar"
|
||||
local isKi5 = TowBarMod.Compatibility.isKi5Vehicle(vehicle)
|
||||
local index, useNormalPart
|
||||
if isKi5 then
|
||||
index = getTowbarModelSlot(script)
|
||||
else
|
||||
index, useNormalPart = getLegacyTowbarModelSlot(script)
|
||||
end
|
||||
local partId = part:getId()
|
||||
local shouldShowOnThisPart = (isKi5 and partId == "towbarKI5")
|
||||
or (not isKi5 and useNormalPart and partId == "towbar")
|
||||
or (not isKi5 and not useNormalPart and partId == "towbarLarge")
|
||||
if shouldShowOnThisPart and index ~= nil then
|
||||
part:setModelVisible("towbar" .. index, true)
|
||||
end
|
||||
|
||||
@@ -4,10 +4,8 @@ require("TowBar/Persistence")
|
||||
local TowingCommands = {}
|
||||
local Commands = {}
|
||||
local TowBarItemType = "TowBar.TowBar"
|
||||
local SyncDelayTicks = 2
|
||||
local SnapshotIntervalTicks = 120
|
||||
local BrokenPairAuditIntervalTicks = 5
|
||||
local pendingSync = {}
|
||||
local snapshotTickCounter = 0
|
||||
local brokenPairAuditTickCounter = 0
|
||||
local confirmedTowPairs = {}
|
||||
@@ -24,57 +22,6 @@ local noise = function(msg)
|
||||
end
|
||||
end
|
||||
|
||||
local function queueSync(kind, player, args, reservedTowBar)
|
||||
if not args then return end
|
||||
table.insert(pendingSync, {
|
||||
kind = kind,
|
||||
ticks = SyncDelayTicks,
|
||||
attempts = 0,
|
||||
reservedTowBar = reservedTowBar == true,
|
||||
player = player,
|
||||
args = args
|
||||
})
|
||||
end
|
||||
|
||||
local function cancelPendingAttach(vehicleA, vehicleB)
|
||||
if not vehicleA or not vehicleB then return end
|
||||
|
||||
local vehicleAId = vehicleA:getId()
|
||||
local vehicleBId = vehicleB:getId()
|
||||
local remaining = {}
|
||||
for i = 1, #pendingSync do
|
||||
local item = pendingSync[i]
|
||||
local itemArgs = item.args or {}
|
||||
local isThisAttach = item.kind == "attach"
|
||||
and itemArgs.vehicleA == vehicleAId
|
||||
and itemArgs.vehicleB == vehicleBId
|
||||
if isThisAttach then
|
||||
-- The reserved item is about to become the single ground drop.
|
||||
item.reservedTowBar = false
|
||||
else
|
||||
table.insert(remaining, item)
|
||||
end
|
||||
end
|
||||
pendingSync = remaining
|
||||
end
|
||||
|
||||
local function hasPendingAttach(vehicleA, vehicleB)
|
||||
if not vehicleA or not vehicleB then return false end
|
||||
|
||||
local vehicleAId = vehicleA:getId()
|
||||
local vehicleBId = vehicleB:getId()
|
||||
for i = 1, #pendingSync do
|
||||
local item = pendingSync[i]
|
||||
local itemArgs = item.args or {}
|
||||
if item.kind == "attach"
|
||||
and itemArgs.vehicleA == vehicleAId
|
||||
and itemArgs.vehicleB == vehicleBId then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function resolveAttachmentA(args, vehicleA)
|
||||
if args and args.attachmentA then return args.attachmentA end
|
||||
if vehicleA and vehicleA:getTowAttachmentSelf() then return vehicleA:getTowAttachmentSelf() end
|
||||
@@ -369,10 +316,16 @@ local function broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
|
||||
end
|
||||
|
||||
local function broadcastDetach(vehicleAId, vehicleBId)
|
||||
sendServerCommand("towbar", "forceDetachSync", {
|
||||
local args = {
|
||||
vehicleA = vehicleAId,
|
||||
vehicleB = vehicleBId
|
||||
})
|
||||
}
|
||||
if isServer() then
|
||||
sendServerCommand("towbar", "forceDetachSync", args)
|
||||
elseif not isClient() and TowBarMod and TowBarMod.Sync
|
||||
and TowBarMod.Sync.applyDetachSync then
|
||||
TowBarMod.Sync.applyDetachSync(args)
|
||||
end
|
||||
end
|
||||
|
||||
local function broadcastSpontaneousDetach(vehicleA, vehicleB)
|
||||
@@ -458,7 +411,6 @@ local function finalizeBrokenTowBarPair(towingVehicle, towedVehicle, reason)
|
||||
return false
|
||||
end
|
||||
|
||||
cancelPendingAttach(towingVehicle, towedVehicle)
|
||||
forgetTowBarPair(towingVehicle, towedVehicle)
|
||||
breakTowBarConstraint(towingVehicle, towedVehicle)
|
||||
clearExpectedTowBarPair(towingVehicle, towedVehicle)
|
||||
@@ -567,54 +519,6 @@ local function reconcileBrokenTowBarPairsServer()
|
||||
end)
|
||||
end
|
||||
|
||||
local function processAttachSync(item)
|
||||
local args = item.args or {}
|
||||
local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil
|
||||
local vehicleB = args.vehicleB and getVehicleById(args.vehicleB) or nil
|
||||
if not vehicleA or not vehicleB then
|
||||
noise("attach sync skipped missing vehicles A=" .. tostring(args.vehicleA) .. " B=" .. tostring(args.vehicleB))
|
||||
return "failed"
|
||||
end
|
||||
|
||||
local attachmentA = resolveAttachmentA(args, vehicleA)
|
||||
local attachmentB = resolveAttachmentB(args, vehicleB)
|
||||
if not isLinked(vehicleA, vehicleB) then
|
||||
if isTowBarPairConfirmed(vehicleA, vehicleB) then return "broken" end
|
||||
if item.attempts >= 3 then return "failed" end
|
||||
vehicleA:addPointConstraint(item.player, vehicleB, attachmentA, attachmentB)
|
||||
return "retry"
|
||||
end
|
||||
|
||||
markExpectedTowBarPair(vehicleA, vehicleB, attachmentA, attachmentB)
|
||||
markTowBarPairConfirmed(vehicleA, vehicleB)
|
||||
broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
|
||||
return "complete"
|
||||
end
|
||||
|
||||
local function failAttachSync(item)
|
||||
local args = item.args or {}
|
||||
local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil
|
||||
local vehicleB = args.vehicleB and getVehicleById(args.vehicleB) or nil
|
||||
|
||||
if isLinked(vehicleA, vehicleB) then
|
||||
vehicleA:breakConstraint(true, false)
|
||||
end
|
||||
clearExpectedTowBarPair(vehicleA, vehicleB)
|
||||
broadcastDetach(args.vehicleA, args.vehicleB)
|
||||
|
||||
if item.reservedTowBar then
|
||||
giveTowBar(item.player, true)
|
||||
item.reservedTowBar = false
|
||||
end
|
||||
end
|
||||
|
||||
local function processDetachSync(item)
|
||||
local args = item.args or {}
|
||||
local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle
|
||||
local vehicleBId = args.vehicleB or args.vehicle
|
||||
broadcastDetach(vehicleAId, vehicleBId)
|
||||
end
|
||||
|
||||
local function snapshotActiveTowbarLinksServer()
|
||||
local cell = getCell()
|
||||
if not cell then return end
|
||||
@@ -643,7 +547,7 @@ local function snapshotActiveTowbarLinksServer()
|
||||
end)
|
||||
end
|
||||
|
||||
local function processPendingSync()
|
||||
local function processTowBarServerTick()
|
||||
reconcileBrokenTowBarPairsServer()
|
||||
|
||||
snapshotTickCounter = snapshotTickCounter + 1
|
||||
@@ -651,37 +555,6 @@ local function processPendingSync()
|
||||
snapshotTickCounter = 0
|
||||
snapshotActiveTowbarLinksServer()
|
||||
end
|
||||
|
||||
if #pendingSync == 0 then return end
|
||||
|
||||
local remaining = {}
|
||||
for i = 1, #pendingSync do
|
||||
local item = pendingSync[i]
|
||||
item.ticks = item.ticks - 1
|
||||
if item.ticks <= 0 then
|
||||
if item.kind == "attach" then
|
||||
local status = processAttachSync(item)
|
||||
if status == "retry" and item.attempts < 3 then
|
||||
item.attempts = item.attempts + 1
|
||||
item.ticks = SyncDelayTicks
|
||||
table.insert(remaining, item)
|
||||
elseif status == "broken" then
|
||||
finalizeBrokenTowBarPair(
|
||||
item.args.vehicleA and getVehicleById(item.args.vehicleA) or nil,
|
||||
item.args.vehicleB and getVehicleById(item.args.vehicleB) or nil,
|
||||
"attach-confirmation-break"
|
||||
)
|
||||
elseif status ~= "complete" then
|
||||
failAttachSync(item)
|
||||
end
|
||||
elseif item.kind == "detach" then
|
||||
processDetachSync(item)
|
||||
end
|
||||
else
|
||||
table.insert(remaining, item)
|
||||
end
|
||||
end
|
||||
pendingSync = remaining
|
||||
end
|
||||
|
||||
function Commands.attachTowBar(player, args)
|
||||
@@ -707,9 +580,7 @@ function Commands.attachTowBar(player, args)
|
||||
return
|
||||
end
|
||||
if isExpectedTowBarPair(vehicleA, vehicleB)
|
||||
or isExpectedTowBarPair(vehicleB, vehicleA)
|
||||
or hasPendingAttach(vehicleA, vehicleB)
|
||||
or hasPendingAttach(vehicleB, vehicleA) then
|
||||
or isExpectedTowBarPair(vehicleB, vehicleA) then
|
||||
noise("rejected duplicate pending towbar attach")
|
||||
return
|
||||
end
|
||||
@@ -724,12 +595,14 @@ function Commands.attachTowBar(player, args)
|
||||
return
|
||||
end
|
||||
|
||||
-- Constraint creation may complete on a later server tick in multiplayer.
|
||||
-- Record and queue the reserved item first so an immediate Build 42.20
|
||||
-- spontaneous break can resolve the pair and consume the reservation once.
|
||||
-- Match the working wrecker path: persist the accepted logical pair, ask
|
||||
-- the server for its native relation, then immediately tell clients to
|
||||
-- replace that relation with the local rigid fake-trailer constraint.
|
||||
markExpectedTowBarPair(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
|
||||
queueSync("attach", player, args, true)
|
||||
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
|
||||
if isServer() then
|
||||
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
|
||||
end
|
||||
broadcastAttach(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
|
||||
if isLinked(vehicleA, vehicleB) then
|
||||
markTowBarPairConfirmed(vehicleA, vehicleB)
|
||||
end
|
||||
@@ -749,10 +622,6 @@ function Commands.detachTowBar(player, args)
|
||||
noise("rejected unauthorized towbar detach")
|
||||
return
|
||||
end
|
||||
if not isLinked(towingVehicle, towedVehicle) then
|
||||
noise("rejected mismatched or stale towbar detach")
|
||||
return
|
||||
end
|
||||
if not isExpectedTowBarPair(towingVehicle, towedVehicle)
|
||||
and not isLegacyTowBarPair(towingVehicle, towedVehicle) then
|
||||
noise("rejected detach for a non-towbar or stale linked pair")
|
||||
@@ -764,7 +633,7 @@ function Commands.detachTowBar(player, args)
|
||||
breakTowBarConstraint(towingVehicle, towedVehicle)
|
||||
forgetTowBarPair(towingVehicle, towedVehicle)
|
||||
clearExpectedTowBarPair(towingVehicle, towedVehicle)
|
||||
queueSync("detach", player, args)
|
||||
broadcastDetach(towingVehicle:getId(), towedVehicle:getId())
|
||||
if shouldRefund then
|
||||
giveTowBar(player, true)
|
||||
end
|
||||
@@ -817,4 +686,4 @@ TowingCommands.OnClientCommand = function(module, command, player, args)
|
||||
end
|
||||
|
||||
Events.OnClientCommand.Add(TowingCommands.OnClientCommand)
|
||||
Events.OnTick.Add(processPendingSync)
|
||||
Events.OnTick.Add(processTowBarServerTick)
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
if not TowBarMod then TowBarMod = {} end
|
||||
|
||||
TowBarMod.Compatibility = TowBarMod.Compatibility or {}
|
||||
local Compatibility = TowBarMod.Compatibility
|
||||
|
||||
-- Exact Mod ID -> exact VehicleScript full-name records for KI5's vehicle
|
||||
-- catalogue. This deliberately avoids model-scale and attachment heuristics:
|
||||
-- unrelated vehicle mods can use the same scale and attachment names.
|
||||
local Ki5VehiclesByModId = {
|
||||
["04vwTouran"] = "Base.04vwTouran",
|
||||
["49powerWagon"] = "Base.49powerWagon|Base.49powerWagonMP|Base.49powerWagonPA|Base.49powerWagonPD",
|
||||
["59meteor"] = "Base.59ambulance|Base.59meteor|Base.ECTO1|Base.ECTO1Burnt",
|
||||
["63beetle"] = "Base.63beetle|Base.63beetleBuggy|Base.63beetleHP",
|
||||
["63Type2Van"] = "Base.63Type2Van|Base.63Type2VanApocalypse|Base.63Type2VanHippie|Base.63Type2VanMilitary",
|
||||
["65banshee"] = "Base.65banshee400|Base.65bansheeSprint|Base.65bansheeXP",
|
||||
["66pontiacLeMans"] = "Base.66pontiacGTO|Base.66pontiacGTOconv|Base.66pontiacLeMans|Base.66pontiacLeMansConv",
|
||||
["67commando"] = "Base.67commando|Base.67commandoBurnt|Base.67commandoPolice|Base.67commandoT50",
|
||||
["67gt500"] = "Base.67gt500|Base.67gt500e",
|
||||
["68firebird"] = "Base.68firebird350|Base.68firebird400|Base.68firebirdRamAir|Base.68firebirdRamAirCustom",
|
||||
["69camaro"] = "Base.69camaroRS|Base.69camaroSS",
|
||||
["69charger"] = "Base.69charger440|Base.69charger500|Base.69chargerDaytona|Base.69chargerDemon|Base.69chargerRT",
|
||||
["69fordMustang"] = "Base.69fordMustangBoss302|Base.69fordMustangBoss429|Base.69fordMustangEV6|Base.69fordMustangMach1|Base.69fordMustangTBC|Base.69fordMustangUBC",
|
||||
["69mini"] = "Base.69mini|Base.69miniIJ|Base.69miniMrB|Base.69miniPS|Base.69miniUnionJack",
|
||||
["70barracuda"] = "Base.70barracuda|Base.70barracudaAAR|Base.70cuda",
|
||||
["70chevelle"] = "Base.70chevelleCoupe|Base.70chevelleCoupeSS|Base.70chevelleCoupeSSL6|Base.70chevelleSedan|Base.70chevelleWagon|Base.70chevelleWagonSS|Base.70elCamino|Base.70elCaminoSS",
|
||||
["70dodge"] = "Base.70dodgeBG|Base.70dodgeOP|Base.70dodgePD|Base.70dodgeRT|Base.70dodgeTA",
|
||||
["70fordEscort"] = "Base.70fordEscortCoupe|Base.70fordEscortRS|Base.70fordEscortSedan|Base.70fordEscortWagon",
|
||||
["70roadRunner"] = "Base.70roadRunner",
|
||||
["73fordFalcon"] = "Base.73fordFalconPS|Base.73fordFalconPSlhd|Base.73fordFalconXBGT|Base.73fordFalconXBGTlhd",
|
||||
["73nissanGTR"] = "Base.73nissanGTR|Base.73nissanGTRlhd",
|
||||
["75grandPrix"] = "Base.75grandPrixHurst|Base.75grandPrixLJ|Base.75grandPrixSJ",
|
||||
["76chevyKseries"] = "Base.76chevyBlazer|Base.76chevyC30CCwrecker|Base.76chevyC30SCwrecker|Base.76chevyK10|Base.76chevyK10fd|Base.76chevyK10spirit|Base.76chevyK20|Base.76chevyK20BigRed|Base.76chevyK20fd|Base.76chevyK20utility|Base.76chevyK30CC|Base.76chevyK30CCdually|Base.76chevyK30CCduallyS|Base.76chevyK30CCfd|Base.76chevyK30CCutility|Base.76chevyK30CCwrecker|Base.76chevyK30SCdually|Base.76chevyK30SCwrecker|Base.76chevySuburban|Base.76chevySuburban2",
|
||||
["76chryslerNewYorker"] = "Base.76chryslerNewYorker|Base.76chryslerNewYorkerTPB",
|
||||
["77firebird"] = "Base.77firebird|Base.77firebirdES|Base.77firebirdFR|Base.77firebirdTA",
|
||||
["78amgeneralM35A2"] = "Base.78amgeneralM35A2|Base.78amgeneralM35A2Burnt|Base.78amgeneralM49A2C|Base.78amgeneralM50A3|Base.78amgeneralM62",
|
||||
["78amgeneralM49A2C"] = "Base.78amgeneralM49A2C",
|
||||
["78amgeneralM50A3"] = "Base.78amgeneralM50A3",
|
||||
["78amgeneralM62"] = "Base.78amgeneralM62",
|
||||
["78lamboCountach"] = "Base.78lamboCountachLP400|Base.78lamboCountachLP400S|Base.78lamboCountachLP400Scb",
|
||||
["79camaro"] = "Base.79camaro|Base.79camaroGhost|Base.79camaroRS|Base.79camaroZ28",
|
||||
["80manKat1"] = "Base.80manKat1",
|
||||
["81deloreanDMC12"] = "Base.81deloreanDMC12|Base.81deloreanDMC12BTTF",
|
||||
["81deloreanDMC12BTTF"] = "Base.81deloreanDMC12BTTF",
|
||||
["82firebird"] = "Base.82firebird|Base.82firebirdKARR|Base.82firebirdKITT|Base.82firebirdSE|Base.82firebirdTA",
|
||||
["82firebirdKITT"] = "Base.82firebirdKARR|Base.82firebirdKITT",
|
||||
["82jeepJ10"] = "Base.82jeepJ10|Base.82jeepJ10pd|Base.82jeepJ10ranger|Base.82jeepJ10t",
|
||||
["82jeepJ10t"] = "Base.82jeepJ10t",
|
||||
["82oshkoshM911"] = "Base.82oshkoshM911|Base.82oshkoshM911B|Base.82oshkoshM911Burnt",
|
||||
["82porsche911"] = "Base.82porsche911rwb|Base.82porsche911sc|Base.82porsche911targa|Base.82porsche911turbo",
|
||||
["83amgeneralM923"] = "Base.83amgeneralM923|Base.83amgeneralM923Burnt",
|
||||
["84buickElectra"] = "Base.84buickElectraCoupe|Base.84buickElectraSedan",
|
||||
["84cadillacDeVille"] = "Base.84cadillacDeVilleCoupe|Base.84cadillacDeVilleSedan",
|
||||
["84corvette"] = "Base.84corvetteC4|Base.93corvetteC4",
|
||||
["84jeepXJ"] = "Base.84jeepXJ2|Base.84jeepXJ4|Base.84jeepXJksp|Base.84jeepXJpd|Base.84jeepXJranger",
|
||||
["84merc"] = "Base.84mercLWB2|Base.84mercLWB4|Base.84mercLWB4M|Base.84mercSWB",
|
||||
["84oldsmobile98"] = "Base.84oldsmobile98Coupe|Base.84oldsmobile98Sedan",
|
||||
["85buickLeSabre"] = "Base.85buickLeSabreCoupe|Base.85buickLeSabreSedan|Base.85buickLeSabreWagon|Base.85buickLeSabreWagon2",
|
||||
["85chevyCaprice"] = "Base.85chevyCapriceCoupe|Base.85chevyCapriceSedan|Base.85chevyCapriceWagon|Base.85chevyCapriceWagon2|Base.85chevyImpalaSedanAirport|Base.85chevyImpalaSedanBCS|Base.85chevyImpalaSedanCLPD|Base.85chevyImpalaSedanFD|Base.85chevyImpalaSedanKSP|Base.85chevyImpalaSedanLCPD|Base.85chevyImpalaSedanMCS|Base.85chevyImpalaSedanMPD|Base.85chevyImpalaSedanPD|Base.85chevyImpalaSedanPDu|Base.85chevyImpalaSedanPrison|Base.85chevyImpalaSedanRanger|Base.85chevyImpalaSedanTaxi|Base.85chevyImpalaSedanWPPD",
|
||||
["85chevyStepVan"] = "Base.85chevyStepVan|Base.85chevyStepVanSWAT",
|
||||
["85chevyStepVanexpanded"] = "Base.85chevyStepVanBlacksmith|Base.85chevyStepVanButchers|Base.85chevyStepVanCitrusWave|Base.85chevyStepVanDelirosPlonkies|Base.85chevyStepVanFlorist|Base.85chevyStepVanGenuine|Base.85chevyStepVanHerald|Base.85chevyStepVanJorgensen|Base.85chevyStepVanLibrary|Base.85chevyStepVanLvAirportCatering|Base.85chevyStepVanLvMotorshop|Base.85chevyStepVanMarineBites|Base.85chevyStepVanMasonry|Base.85chevyStepVanMrHuangsLaundry|Base.85chevyStepVanPostal|Base.85chevyStepVanPropane|Base.85chevyStepVanRandys|Base.85chevyStepVanScarletOak|Base.85chevyStepVanSeHospitality|Base.85chevyStepVanSePaintingServices|Base.85chevyStepVanSmartCut|Base.85chevyStepVanSunBallz|Base.85chevyStepVanTheCompleteRepair|Base.85chevyStepVanTimelessGlass|Base.85chevyStepVanUsLogistics|Base.85chevyStepVanZippeeMarket",
|
||||
["85oldsmobileDelta88"] = "Base.85oldsmobileDelta88Coupe|Base.85oldsmobileDelta88Sedan|Base.85oldsmobileDelta88Wagon|Base.85oldsmobileDelta88Wagon2",
|
||||
["85pontiacParisienne"] = "Base.85pontiacParisienneSedan|Base.85pontiacParisienneWagon|Base.85pontiacParisienneWagon2",
|
||||
["86chevyCUCV"] = "Base.86chevyK5blazer|Base.86chevyK5ksp|Base.86chevyK5pd|Base.86chevyM1008|Base.86chevyM1009|Base.86chevyM1009mp|Base.86chevyM1010|Base.86chevyM1028|Base.86chevyM1031",
|
||||
["86fordE150"] = "Base.86fordE150|Base.86fordE150dnd|Base.86fordE150ksp|Base.86fordE150long|Base.86fordE150longW|Base.86fordE150mccoy|Base.86fordE150med|Base.86fordE150mm|Base.86fordE150pd|Base.86fordE150slide|Base.86fordE150slideSpiffo|Base.86fordE150so",
|
||||
["86fordE150dnd"] = "Base.86fordE150dnd",
|
||||
["86fordE150expanded"] = "Base.86fordE150beckmansBuilding|Base.86fordE150blacksmith|Base.86fordE150brewster|Base.86fordE150brushAndClay|Base.86fordE150bugWipers|Base.86fordE150ccconstruction|Base.86fordE150creatureCruiser|Base.86fordE150deerValley|Base.86fordE150fossoil|Base.86fordE150greenes|Base.86fordE150heritageTailors|Base.86fordE150jones|Base.86fordE150kerrHomes|Base.86fordE150knobCreek|Base.86fordE150knoxDistilery|Base.86fordE150knoxTelecom|Base.86fordE150korshunovs|Base.86fordE150kyTransit|Base.86fordE150LBMWradio|Base.86fordE150leatherwork|Base.86fordE150lectromax|Base.86fordE150locksmith|Base.86fordE150LVairportShuttle|Base.86fordE150lvLandscaping|Base.86fordE150massGenfac|Base.86fordE150McCoyWoodworking|Base.86fordE150meltingPointMetal|Base.86fordE150mesmerWagon|Base.86fordE150metalheads|Base.86fordE150michelesWoodshop|Base.86fordE150mobileMechanics|Base.86fordE150mooresMechanics|Base.86fordE150oldMillWaterCompany|Base.86fordE150oVoFarms|Base.86fordE150pennSham|Base.86fordE150perfick|Base.86fordE150plattAutoRepair|Base.86fordE150pluggedInElectrics|Base.86fordE150postal|Base.86fordE150quantumVessel|Base.86fordE150riversideFab|Base.86fordE150rosewoodWorking|Base.86fordE150schwab|Base.86fordE150stoneworksMasonry|Base.86fordE150tasteTheBrew|Base.86fordE150theGardenGods|Base.86fordE150theLadyDelighter|Base.86fordE150treyBaines|Base.86fordE150uncloggers|Base.86fordE150valkyriesSpear|Base.86fordE150voltMojo|Base.86fordE150wpCarpentry|Base.86fordE150zenith",
|
||||
["86fordE150mm"] = "Base.86fordE150mm",
|
||||
["86fordE150pd"] = "Base.86fordE150pd",
|
||||
["86oshkoshP19A"] = "Base.86oshkoshFRTR55|Base.86oshkoshKYFD|Base.86oshkoshP19ABurnt|Base.86oshkoshUSMC",
|
||||
["87buickRegal"] = "Base.87buickRegalGNX|Base.87buickRegalTurboT|Base.87buickRegalTurboTfbi",
|
||||
["87chevySuburban"] = "Base.87chevySuburban|Base.87chevySuburbanCUCV|Base.87chevySuburbanOP",
|
||||
["87fordB700"] = "Base.87fordB700military|Base.87fordB700prison|Base.87fordB700school|Base.87fordF700bank|Base.87fordF700box|Base.87fordF700swat",
|
||||
["87toyotaCorolla"] = "Base.87toyotaCorollaAE92levin|Base.87toyotaCorollaAE92levinLhd|Base.87toyotaCorollaAE92trueno|Base.87toyotaCorollaAE92truenoLhd",
|
||||
["87toyotaMR2"] = "Base.87toyotaMR2|Base.87toyotaMR2c",
|
||||
["88chevyS10"] = "Base.88chevyS10",
|
||||
["88toyotaHilux"] = "Base.88toyotaHiluxSC|Base.88toyotaHiluxXC|Base.88toyotaHiluxXCS",
|
||||
["89defender"] = "Base.89defender110|Base.89defender110utility|Base.89defender130|Base.89defender90|Base.89defender90utility|Base.89defenderWolf",
|
||||
["89dodgeCaravan"] = "Base.89dodgeCaravan|Base.89dodgeCaravanLE|Base.89dodgeCaravanNomad",
|
||||
["89fordBronco"] = "Base.89fordBronco|Base.89fordBroncoPD|Base.89fordBroncoRanger",
|
||||
["89trooper"] = "Base.89trooper|Base.89trooperOP|Base.89trooperRS",
|
||||
["89volvo200"] = "Base.89volvo242turbo|Base.89volvo244sedan|Base.89volvo245wagon",
|
||||
["90bmwE30"] = "Base.90bmwE30cabrio|Base.90bmwE30m3|Base.90bmwE30sedan2|Base.90bmwE30sedan4|Base.90bmwE30touring",
|
||||
["90fordF350ambulance"] = "Base.90fordF350ambulance|Base.90fordF350SWAT",
|
||||
["90pierceArrow"] = "Base.90pierceArrow|Base.90pierceArrowQuint",
|
||||
["91fordLTD"] = "Base.91fordLTD|Base.91fordLTDksp|Base.91fordLTDksp2|Base.91fordLTDpd|Base.91fordLTDranger|Base.91fordLTDtaxi|Base.91fordLTDunmarked|Base.91fordLTDwagon",
|
||||
["91fordRanger"] = "Base.91fordRangerPD|Base.91fordRangerRanger|Base.91fordRangerSC|Base.91fordRangerSClong|Base.91fordRangerXC|Base.91fordRangerXClong",
|
||||
["91geoMetro"] = "Base.91geoMetro",
|
||||
["91lexusLS400"] = "Base.91lexusLS400",
|
||||
["91nissan240sx"] = "Base.91nissan240sx|Base.91nissan240sx2",
|
||||
["91range"] = "Base.91range|Base.91range2",
|
||||
["92amgeneralM998"] = "Base.92amgeneralM998|Base.92amgeneralM998Burnt",
|
||||
["92fordCVPI"] = "Base.92fordCV|Base.92fordCVPI|Base.92fordCVPI2|Base.92fordCVPI2ksp|Base.92fordCVPI2kspst|Base.92fordCVPI2so|Base.92fordCVPI2sup|Base.92fordCVPIfd|Base.92fordCVPIpdu|Base.92fordCVPItaxi|Base.92fordCVPIunmarked",
|
||||
["92jeepYJ"] = "Base.92jeepYJjp|Base.92jeepYJranger|Base.92jeepYJs|Base.92jeepYJse",
|
||||
["92jeepYJJP18"] = "Base.92jeepYJjp",
|
||||
["92nissanGTR"] = "Base.92nissanGTR|Base.92nissanGTRlhd",
|
||||
["93chevySuburban"] = "Base.93chevySilveradoCC|Base.93chevySilveradoCCdually|Base.93chevySilveradoCClong|Base.93chevySilveradoCClongfd|Base.93chevySilveradoK3500flatbed|Base.93chevySilveradoK3500wrecker|Base.93chevySilveradoSC|Base.93chevySilveradoSCdually|Base.93chevySilveradoSClong|Base.93chevySilveradoSClongFossoil|Base.93chevySilveradoXC|Base.93chevySilveradoXCdually|Base.93chevySilveradoXClong|Base.93chevySilveradoXClongMcCoy|Base.93chevySilveradoXClongRanger|Base.93chevySuburban|Base.93chevySuburbanDually|Base.93chevySuburbanfbi|Base.93chevySuburbanfd|Base.93chevySuburbanksp|Base.93chevySuburbanpd|Base.93chevySuburbanpdu",
|
||||
["93chevySuburbanExpanded"] = "Base.93chevySilveradoAirport|Base.93chevySilveradoK3500lvLandscaping|Base.93chevySilveradoK3500mechanic|Base.93chevySilveradoMcCoyWoodworking|Base.93chevySilveradoPennSham|Base.93chevySilveradoPoliceBCS|Base.93chevySilveradoPoliceMCS|Base.93chevySilveradoRiversideFab|Base.93chevySilveradoStoneworksMasonry|Base.93chevySilveradoUncloggers|Base.93chevySilveradoVoltMojo|Base.93chevySilveradoWpCarpentry|Base.93chevySuburbanAirportSec|Base.93chevySuburbanPoliceBCS|Base.93chevySuburbanPoliceCLPD|Base.93chevySuburbanPoliceLCPD|Base.93chevySuburbanPoliceMCS|Base.93chevySuburbanPoliceMPD|Base.93chevySuburbanPoliceWPPD|Base.93chevySuburbanPrison",
|
||||
["93fordElgin"] = "Base.93fordElgin|Base.93fordElginSpec",
|
||||
["93fordF350"] = "Base.93fordF150|Base.93fordF150S|Base.93fordF250|Base.93fordF350|Base.93fordF350dually|Base.93fordF350fd|Base.93fordF350pd|Base.93fordF350so|Base.93fordF350utility|Base.93fordF350utilityDpw|Base.93fordF350utilityFd",
|
||||
["93fordTaurus"] = "Base.93fordTaurus|Base.93fordTaurusSHO|Base.93fordTaurusWagon",
|
||||
["93mustangSSP"] = "Base.93mustangGT|Base.93mustangSSP|Base.93mustangSSPksp|Base.93mustangSSPksp2|Base.93mustangSSPkspCol|Base.93mustangSSPpd|Base.93mustangSSPpd2|Base.93mustangSSPunmarked|Base.93mustangSVTcobraR",
|
||||
["93townCar"] = "Base.93townCar|Base.93townCarLimo",
|
||||
["95impreza"] = "Base.95impreza|Base.95imprezalhd",
|
||||
["96lancerEVO"] = "Base.96lancerEVO|Base.96lancerEVOlhd",
|
||||
["96saturnSseries"] = "Base.96saturnSL2",
|
||||
["97bushmaster"] = "Base.97bushAmbulance|Base.97bushmaster",
|
||||
["98stagea"] = "Base.98stagea260RS|Base.98stagea260RSlhd",
|
||||
["99fordCVPI"] = "Base.99fordCVPI|Base.99fordCVPIunmarked",
|
||||
["cobbM540"] = "Base.cobbM540",
|
||||
["lockMartM577"] = "Base.lockMartM577",
|
||||
}
|
||||
|
||||
local function getScriptFullName(script)
|
||||
if not script then return nil end
|
||||
|
||||
local ok, fullName = pcall(function()
|
||||
return script:getFullName()
|
||||
end)
|
||||
if ok and type(fullName) == "string" then return fullName end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function Compatibility.isKi5Vehicle(vehicle)
|
||||
if not vehicle then return false end
|
||||
|
||||
local ok, script = pcall(function()
|
||||
return vehicle:getScript()
|
||||
end)
|
||||
if not ok or not script then return false end
|
||||
|
||||
local fullName = getScriptFullName(script)
|
||||
if not fullName then return false end
|
||||
|
||||
local activeMods = getActivatedMods and getActivatedMods() or nil
|
||||
if not activeMods or not activeMods.contains then return false end
|
||||
|
||||
for modId, vehicleNames in pairs(Ki5VehiclesByModId) do
|
||||
if activeMods:contains(modId) then
|
||||
for candidate in string.gmatch(vehicleNames, "[^|]+") do
|
||||
if candidate == fullName then return true end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -263,6 +263,36 @@ module Base
|
||||
create = BTtow.Create.towbar,
|
||||
init = BTtow.Init.towbar,
|
||||
}
|
||||
}
|
||||
part towbarKI5
|
||||
{
|
||||
model towbar0 { file = towbarModelKI5, offset = 0 -0.3 1.0, }
|
||||
model towbar1 { file = towbarModelKI5, offset = 0 -0.3 1.1, }
|
||||
model towbar2 { file = towbarModelKI5, offset = 0 -0.3 1.2, }
|
||||
model towbar3 { file = towbarModelKI5, offset = 0 -0.3 1.3, }
|
||||
model towbar4 { file = towbarModelKI5, offset = 0 -0.3 1.4, }
|
||||
model towbar5 { file = towbarModelKI5, offset = 0 -0.3 1.5, }
|
||||
model towbar6 { file = towbarModelKI5, offset = 0 -0.3 1.6, }
|
||||
model towbar7 { file = towbarModelKI5, offset = 0 -0.3 1.7, }
|
||||
model towbar8 { file = towbarModelKI5, offset = 0 -0.3 1.8, }
|
||||
model towbar9 { file = towbarModelKI5, offset = 0 -0.3 1.9, }
|
||||
model towbar10 { file = towbarModelKI5, offset = 0 -0.3 2.0, }
|
||||
model towbar11 { file = towbarModelKI5, offset = 0 -0.3 2.1, }
|
||||
model towbar12 { file = towbarModelKI5, offset = 0 -0.3 2.2, }
|
||||
model towbar13 { file = towbarModelKI5, offset = 0 -0.3 2.3, }
|
||||
model towbar14 { file = towbarModelKI5, offset = 0 -0.3 2.4, }
|
||||
model towbar15 { file = towbarModelKI5, offset = 0 -0.3 2.5, }
|
||||
model towbar16 { file = towbarModelKI5, offset = 0 -0.3 2.6, }
|
||||
model towbar17 { file = towbarModelKI5, offset = 0 -0.3 2.7, }
|
||||
model towbar18 { file = towbarModelKI5, offset = 0 -0.3 2.8, }
|
||||
model towbar19 { file = towbarModelKI5, offset = 0 -0.3 2.9, }
|
||||
model towbar20 { file = towbarModelKI5, offset = 0 -0.3 3.0, }
|
||||
model towbar21 { file = towbarModelKI5, offset = 0 -0.3 3.1, }
|
||||
model towbar22 { file = towbarModelKI5, offset = 0 -0.3 3.2, }
|
||||
model towbar23 { file = towbarModelKI5, offset = 0 -0.3 3.3, }
|
||||
area = Engine,
|
||||
mechanicRequireKey = false,
|
||||
lua { create = BTtow.Create.towbar, init = BTtow.Init.towbar, }
|
||||
}
|
||||
part Battery
|
||||
{
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
module Base
|
||||
{
|
||||
model towbarModel
|
||||
{
|
||||
mesh = vehicles/Towbar,
|
||||
texture = Vehicles/Towbar_Texture,
|
||||
scale = 0.01,
|
||||
}
|
||||
|
||||
model towbarModelKI5
|
||||
{
|
||||
mesh = vehicles/Towbar,
|
||||
texture = Vehicles/Towbar_Texture,
|
||||
@@ -280,5 +287,36 @@ module Base
|
||||
}
|
||||
}
|
||||
|
||||
part towbarKI5
|
||||
{
|
||||
model towbar0 { file = towbarModelKI5, offset = 0 -0.3 1.0, }
|
||||
model towbar1 { file = towbarModelKI5, offset = 0 -0.3 1.1, }
|
||||
model towbar2 { file = towbarModelKI5, offset = 0 -0.3 1.2, }
|
||||
model towbar3 { file = towbarModelKI5, offset = 0 -0.3 1.3, }
|
||||
model towbar4 { file = towbarModelKI5, offset = 0 -0.3 1.4, }
|
||||
model towbar5 { file = towbarModelKI5, offset = 0 -0.3 1.5, }
|
||||
model towbar6 { file = towbarModelKI5, offset = 0 -0.3 1.6, }
|
||||
model towbar7 { file = towbarModelKI5, offset = 0 -0.3 1.7, }
|
||||
model towbar8 { file = towbarModelKI5, offset = 0 -0.3 1.8, }
|
||||
model towbar9 { file = towbarModelKI5, offset = 0 -0.3 1.9, }
|
||||
model towbar10 { file = towbarModelKI5, offset = 0 -0.3 2.0, }
|
||||
model towbar11 { file = towbarModelKI5, offset = 0 -0.3 2.1, }
|
||||
model towbar12 { file = towbarModelKI5, offset = 0 -0.3 2.2, }
|
||||
model towbar13 { file = towbarModelKI5, offset = 0 -0.3 2.3, }
|
||||
model towbar14 { file = towbarModelKI5, offset = 0 -0.3 2.4, }
|
||||
model towbar15 { file = towbarModelKI5, offset = 0 -0.3 2.5, }
|
||||
model towbar16 { file = towbarModelKI5, offset = 0 -0.3 2.6, }
|
||||
model towbar17 { file = towbarModelKI5, offset = 0 -0.3 2.7, }
|
||||
model towbar18 { file = towbarModelKI5, offset = 0 -0.3 2.8, }
|
||||
model towbar19 { file = towbarModelKI5, offset = 0 -0.3 2.9, }
|
||||
model towbar20 { file = towbarModelKI5, offset = 0 -0.3 3.0, }
|
||||
model towbar21 { file = towbarModelKI5, offset = 0 -0.3 3.1, }
|
||||
model towbar22 { file = towbarModelKI5, offset = 0 -0.3 3.2, }
|
||||
model towbar23 { file = towbarModelKI5, offset = 0 -0.3 3.3, }
|
||||
area = Engine,
|
||||
mechanicRequireKey = false,
|
||||
lua { create = BTtow.Create.towbar, init = BTtow.Init.towbar, }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user