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
|
if TowBarMod.Sync._towSyncClientLoaded then return end
|
||||||
TowBarMod.Sync._towSyncClientLoaded = true
|
TowBarMod.Sync._towSyncClientLoaded = true
|
||||||
TowBarMod.Sync.appliedPairs = TowBarMod.Sync.appliedPairs or {}
|
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 function pairKeyContainsVehicle(key, vehicleId)
|
||||||
local id = tostring(vehicleId)
|
local id = tostring(vehicleId)
|
||||||
@@ -15,201 +43,218 @@ end
|
|||||||
local function clearAppliedPairForVehicle(vehicle)
|
local function clearAppliedPairForVehicle(vehicle)
|
||||||
if not vehicle then return end
|
if not vehicle then return end
|
||||||
local vehicleId = vehicle:getId()
|
local vehicleId = vehicle:getId()
|
||||||
for key in pairs(TowBarMod.Sync.appliedPairs) do
|
for key in pairs(Sync.appliedPairs) do
|
||||||
if pairKeyContainsVehicle(key, vehicleId) then
|
if pairKeyContainsVehicle(key, vehicleId) then
|
||||||
TowBarMod.Sync.appliedPairs[key] = nil
|
Sync.appliedPairs[key] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function resolveVehicle(id)
|
local function resolvePair(args)
|
||||||
if not id then return nil end
|
if type(args) ~= "table" then return nil, nil end
|
||||||
return getVehicleById(id)
|
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
|
end
|
||||||
|
|
||||||
local function ensureAttachment(vehicle, attachmentId)
|
local function isPairLinked(vehicleA, vehicleB)
|
||||||
if not vehicle or not attachmentId then return false end
|
return vehicleA and vehicleB
|
||||||
|
and vehicleA:getVehicleTowing() == vehicleB
|
||||||
|
and vehicleB:getVehicleTowedBy() == vehicleA
|
||||||
|
end
|
||||||
|
|
||||||
local script = vehicle:getScript()
|
local function hasConflictingLink(vehicle, expectedOther)
|
||||||
if not script then return false end
|
if not vehicle or not expectedOther then return false end
|
||||||
if script:getAttachmentById(attachmentId) ~= nil 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 wheelCount = script:getWheelCount()
|
local function preparePairState(vehicleA, vehicleB, attachmentA, attachmentB)
|
||||||
local yOffset = -0.5
|
local towingMd = vehicleA:getModData()
|
||||||
if wheelCount > 0 then
|
local towedMd = vehicleB:getModData()
|
||||||
local wheel = script:getWheel(0)
|
if not towingMd or not towedMd then return false end
|
||||||
if wheel and wheel:getOffset() then
|
|
||||||
yOffset = wheel:getOffset():y() + 0.1
|
local currentScript = vehicleB:getScriptName()
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
local chassis = script:getPhysicsChassisShape()
|
towingMd.isTowingByTowBar = true
|
||||||
if not chassis then return false end
|
towingMd.towed = false
|
||||||
|
towingMd.towBarTowedVehicleId = vehicleB:getId()
|
||||||
local attach = ModelAttachment.new(attachmentId)
|
towingMd.towBarTowingVehicleId = nil
|
||||||
if attachmentId == "trailer" then
|
towingMd.towBarExpectedAttachment = attachmentA
|
||||||
attach:getOffset():set(0, yOffset, -chassis:z() / 2 - 0.1)
|
towedMd.isTowingByTowBar = true
|
||||||
attach:setZOffset(-1)
|
towedMd.towed = true
|
||||||
else
|
towedMd.towBarTowedVehicleId = nil
|
||||||
attach:getOffset():set(0, yOffset, chassis:z() / 2 + 0.1)
|
towedMd.towBarTowingVehicleId = vehicleA:getId()
|
||||||
attach:setZOffset(1)
|
towedMd.towBarExpectedAttachment = attachmentB
|
||||||
end
|
vehicleA:transmitModData()
|
||||||
script:addAttachment(attach)
|
vehicleB:transmitModData()
|
||||||
return true
|
return true
|
||||||
end
|
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
|
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 attachmentA = args.attachmentA or "trailer"
|
||||||
local attachmentB = args.attachmentB or "trailerfront"
|
local attachmentB = args.attachmentB or "trailerfront"
|
||||||
if not ensureAttachment(vehicleA, attachmentA) or not ensureAttachment(vehicleB, attachmentB) then
|
if not preparePairState(vehicleA, vehicleB, attachmentA, attachmentB) then return false end
|
||||||
return
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
local key = tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())
|
local key = pairKey(vehicleA, vehicleB)
|
||||||
if TowBarMod.Sync.appliedPairs[key] and not isLinked(vehicleA, vehicleB) then
|
if not forceReattach and isPairLinked(vehicleA, vehicleB) and Sync.appliedPairs[key] then
|
||||||
-- Vehicle streaming can remove the native constraint while the Lua
|
|
||||||
-- module remains loaded. Allow the authoritative recovery sync to
|
|
||||||
-- rebuild local physics for the returning pair.
|
|
||||||
TowBarMod.Sync.appliedPairs[key] = nil
|
|
||||||
end
|
|
||||||
if not TowBarMod.Sync.appliedPairs[key] then
|
|
||||||
-- The server initially creates a normal Build 42 tow relation. It may
|
|
||||||
-- also restore one from the save. Replace that native rope exactly
|
|
||||||
-- once with this mod's rigid fake-trailer constraint.
|
|
||||||
breakTowBarPair(vehicleA, vehicleB)
|
|
||||||
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
|
|
||||||
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicleB, "notTowingA_Trailer")
|
|
||||||
end
|
|
||||||
-- The final true keeps this a local physics rebuild. The server owns
|
|
||||||
-- the persisted logical relation and must not receive a detach/attach race.
|
|
||||||
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB, true)
|
|
||||||
TowBarMod.Sync.appliedPairs[key] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hasConflictingTowLink(vehicle, expectedOther)
|
|
||||||
if not vehicle or not expectedOther then return false end
|
|
||||||
|
|
||||||
local towing = vehicle:getVehicleTowing()
|
|
||||||
local towedBy = vehicle:getVehicleTowedBy()
|
|
||||||
if (towing ~= nil and towing ~= expectedOther)
|
|
||||||
or (towedBy ~= nil and towedBy ~= expectedOther) then
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Also reject a delayed detach while a new tow is being established but
|
-- This is deliberately the same one-shot method as WreckerSyncClient:
|
||||||
-- has not created its physical constraint yet.
|
-- replace any exact native relation with one client-local rigid relation.
|
||||||
local modData = vehicle:getModData()
|
if TowBarMod.RigidTow.attach(vehicleA, vehicleB, attachmentA, attachmentB) ~= true then
|
||||||
if not modData then return false end
|
return false
|
||||||
local expectedOtherId = expectedOther:getId()
|
|
||||||
return (modData["towBarTowedVehicleId"] ~= nil
|
|
||||||
and modData["towBarTowedVehicleId"] ~= expectedOtherId)
|
|
||||||
or (modData["towBarTowingVehicleId"] ~= nil
|
|
||||||
and modData["towBarTowingVehicleId"] ~= expectedOtherId)
|
|
||||||
end
|
|
||||||
|
|
||||||
breakTowBarPair = function(vehicleA, vehicleB)
|
|
||||||
if not vehicleA or not vehicleB then return end
|
|
||||||
|
|
||||||
local vehicleAReferencesB = vehicleA:getVehicleTowing() == vehicleB
|
|
||||||
or vehicleA:getVehicleTowedBy() == vehicleB
|
|
||||||
local vehicleBReferencesA = vehicleB:getVehicleTowing() == vehicleA
|
|
||||||
or vehicleB:getVehicleTowedBy() == vehicleA
|
|
||||||
if vehicleAReferencesB then
|
|
||||||
vehicleA:breakConstraint(true, true)
|
|
||||||
elseif vehicleBReferencesA then
|
|
||||||
vehicleB:breakConstraint(true, true)
|
|
||||||
end
|
end
|
||||||
|
Sync.appliedPairs[key] = true
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local function applyDetachSync(args)
|
local function applyDetachSync(args)
|
||||||
if not args then return end
|
local desiredKey = argsPairKey(args)
|
||||||
local vehicleA = resolveVehicle(args.vehicleA)
|
local wasApplied = desiredKey and Sync.appliedPairs[desiredKey] == true
|
||||||
local vehicleB = resolveVehicle(args.vehicleB)
|
if desiredKey then
|
||||||
if not vehicleA or not vehicleB then return end
|
Sync.desiredPairs[desiredKey] = nil
|
||||||
if hasConflictingTowLink(vehicleA, vehicleB) or hasConflictingTowLink(vehicleB, vehicleA) then
|
Sync.appliedPairs[desiredKey] = nil
|
||||||
return
|
|
||||||
end
|
end
|
||||||
breakTowBarPair(vehicleA, vehicleB)
|
local vehicleA, vehicleB = resolvePair(args)
|
||||||
TowBarMod.Sync.appliedPairs[tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())] = nil
|
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
|
if TowBarMod.Hook and TowBarMod.Hook.cleanupDetachedTowBar then
|
||||||
pcall(TowBarMod.Hook.cleanupDetachedTowBar, vehicleA, vehicleB)
|
TowBarMod.Hook.cleanupDetachedTowBar(vehicleA, vehicleB)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function onServerCommand(module, command, args)
|
Sync.applyAttachSync = applyAttachSync
|
||||||
if module ~= "towbar" then return end
|
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
|
if command == "forceAttachSync" then
|
||||||
applyAttachSync(args)
|
applyAttachSync(args)
|
||||||
elseif command == "forceDetachSync" or command == "spontaneousDetachSync" then
|
elseif command == "forceDetachSync" or command == "spontaneousDetachSync" then
|
||||||
applyDetachSync(args)
|
applyDetachSync(args)
|
||||||
end
|
end
|
||||||
end
|
end)
|
||||||
|
|
||||||
TowBarMod.Sync.applyAttachSync = applyAttachSync
|
|
||||||
TowBarMod.Sync.applyDetachSync = applyDetachSync
|
|
||||||
|
|
||||||
Events.OnServerCommand.Add(onServerCommand)
|
|
||||||
if Events.OnSpawnVehicleEnd then
|
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
|
end
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
if not TowBarMod then TowBarMod = {} end
|
if not TowBarMod then TowBarMod = {} end
|
||||||
if not TowBarMod.Hook then TowBarMod.Hook = {} end
|
if not TowBarMod.Hook then TowBarMod.Hook = {} end
|
||||||
|
|
||||||
|
require("TowBar/VehicleCompatibility")
|
||||||
|
|
||||||
local DefaultTowBarTowMass = 200
|
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 FreeRollTickInterval = 15
|
||||||
local freeRollTickCounter = 0
|
local freeRollTickCounter = 0
|
||||||
|
|
||||||
@@ -44,7 +42,7 @@ local function applyFreeRollingTowState(vehicle)
|
|||||||
modData.towBarOriginalBrakingForce = vehicle:getBrakingForce()
|
modData.towBarOriginalBrakingForce = vehicle:getBrakingForce()
|
||||||
end
|
end
|
||||||
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrakeOn", "isParkingBrakeOn")
|
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrakeOn", "isParkingBrakeOn")
|
||||||
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrake", "isParkingBrake")
|
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalParkingBrake", "getParkingBrake")
|
||||||
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalHandbrake", "isHandbrake")
|
storeOriginalVehicleCall(vehicle, modData, "towBarOriginalHandbrake", "isHandbrake")
|
||||||
|
|
||||||
local configuredTowMass = TowBarMod.Config and tonumber(TowBarMod.Config.towedVehicleRollingMass)
|
local configuredTowMass = TowBarMod.Config and tonumber(TowBarMod.Config.towedVehicleRollingMass)
|
||||||
@@ -61,10 +59,13 @@ local function applyFreeRollingTowState(vehicle)
|
|||||||
tryVehicleCall(vehicle, "setHandbrake", false)
|
tryVehicleCall(vehicle, "setHandbrake", false)
|
||||||
end
|
end
|
||||||
|
|
||||||
vehicle:constraintChanged()
|
-- Match the working wrecker path. Recalculating total mass here would
|
||||||
vehicle:updateTotalMass()
|
-- immediately replace the temporary towing mass in multiplayer; creating
|
||||||
|
-- the rigid constraint below notifies Bullet of the changed vehicle state.
|
||||||
end
|
end
|
||||||
|
|
||||||
|
TowBarMod.Hook.applyFreeRollingTowState = applyFreeRollingTowState
|
||||||
|
|
||||||
local function restoreFreeRollingTowState(vehicle, modData)
|
local function restoreFreeRollingTowState(vehicle, modData)
|
||||||
if not vehicle or not modData then return end
|
if not vehicle or not modData then return end
|
||||||
|
|
||||||
@@ -88,25 +89,6 @@ local function restoreFreeRollingTowState(vehicle, modData)
|
|||||||
vehicle:updateTotalMass()
|
vehicle:updateTotalMass()
|
||||||
end
|
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)
|
local function getTowBarItem(playerObj)
|
||||||
if not playerObj then return nil end
|
if not playerObj then return nil end
|
||||||
local inventory = playerObj:getInventory()
|
local inventory = playerObj:getInventory()
|
||||||
@@ -116,15 +98,8 @@ end
|
|||||||
|
|
||||||
local function sendTowAttachCommand(playerObj, args)
|
local function sendTowAttachCommand(playerObj, args)
|
||||||
if not playerObj or not args then return end
|
if not playerObj or not args then return end
|
||||||
|
-- SP and MP now share the same authoritative attach lifecycle.
|
||||||
-- MP-safe/server-authoritative attach path (Landtrain style).
|
sendClientCommand(playerObj, "towbar", "attachTowBar", args)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local TowbarVariantSize = 24
|
local TowbarVariantSize = 24
|
||||||
@@ -138,6 +113,8 @@ local TowbarVisualScale = 2.5
|
|||||||
local TowbarModelLength = 0.9714089036
|
local TowbarModelLength = 0.9714089036
|
||||||
local TowbarScaledModelLength = TowbarModelLength * TowbarVisualScale
|
local TowbarScaledModelLength = TowbarModelLength * TowbarVisualScale
|
||||||
local TowbarModelHalfLength = TowbarScaledModelLength / 2
|
local TowbarModelHalfLength = TowbarScaledModelLength / 2
|
||||||
|
local VanillaScaleMin = 1.5
|
||||||
|
local VanillaScaleMax = 2.0
|
||||||
|
|
||||||
local function getTowbarFrontEdgeZ(script)
|
local function getTowbarFrontEdgeZ(script)
|
||||||
if not script then return nil end
|
if not script then return nil end
|
||||||
@@ -178,16 +155,87 @@ local function getTowbarModelSlot(script)
|
|||||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||||
end
|
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)
|
local function setTowBarModelVisible(vehicle, isVisible)
|
||||||
if not vehicle then return end
|
if not vehicle then return end
|
||||||
|
|
||||||
local normalPart = vehicle:getPartById("towbar")
|
local normalPart = vehicle:getPartById("towbar")
|
||||||
local largePart = vehicle:getPartById("towbarLarge")
|
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
|
for j = 0, TowbarVariantSize - 1 do
|
||||||
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
|
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
|
||||||
if largePart then largePart:setModelVisible("towbar" .. j, false) end
|
if largePart then largePart:setModelVisible("towbar" .. j, false) end
|
||||||
|
if ki5Part then ki5Part:setModelVisible("towbar" .. j, false) end
|
||||||
end
|
end
|
||||||
|
|
||||||
if not isVisible then
|
if not isVisible then
|
||||||
@@ -201,8 +249,16 @@ local function setTowBarModelVisible(vehicle, isVisible)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local index = getTowbarModelSlot(script)
|
local isKi5 = TowBarMod.Compatibility.isKi5Vehicle(vehicle)
|
||||||
local part = normalPart
|
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
|
if part and index ~= nil then
|
||||||
part:setModelVisible("towbar" .. index, true)
|
part:setModelVisible("towbar" .. index, true)
|
||||||
end
|
end
|
||||||
@@ -259,23 +315,6 @@ local function resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedMo
|
|||||||
return attachmentA, attachmentB
|
return attachmentA, attachmentB
|
||||||
end
|
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)
|
local function isActiveTowBarTowedVehicle(vehicle, modData)
|
||||||
if not vehicle or not modData then
|
if not vehicle or not modData then
|
||||||
return false
|
return false
|
||||||
@@ -293,142 +332,22 @@ local function isActiveTowBarTowedVehicle(vehicle, modData)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
local function reattachTowBarPair(playerObj, towingVehicle, towedVehicle, requireDriver)
|
function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, knownTowingVehicle)
|
||||||
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)
|
|
||||||
if not towedVehicle then return end
|
if not towedVehicle then return end
|
||||||
|
|
||||||
local towedModData = towedVehicle:getModData()
|
local towedModData = towedVehicle:getModData()
|
||||||
if not isActiveTowBarTowedVehicle(towedVehicle, towedModData) then return end
|
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)
|
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName)
|
||||||
end
|
end
|
||||||
|
|
||||||
local towingVehicle = towedVehicle:getVehicleTowedBy()
|
|
||||||
if towingVehicle then
|
if towingVehicle then
|
||||||
local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
|
local attachmentA, attachmentB = resolveTowAttachmentsForPair(towingVehicle, towedVehicle, towedModData)
|
||||||
if attachmentA and attachmentB then
|
if attachmentA and attachmentB then
|
||||||
@@ -443,7 +362,6 @@ function TowBarMod.Hook.setVehiclePostAttach(playerObj, towedVehicle, retriesLef
|
|||||||
towingVehicle:transmitModData()
|
towingVehicle:transmitModData()
|
||||||
towedVehicle:transmitModData()
|
towedVehicle:transmitModData()
|
||||||
end
|
end
|
||||||
TowBarMod.Utils.updateAttachmentsForRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -459,35 +377,9 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
|
|||||||
if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end
|
if #(TowBarMod.Utils.getHookTypeVariants(towingVehicle, towedVehicle, true)) == 0 then return end
|
||||||
|
|
||||||
local towBarItem = getTowBarItem(playerObj)
|
local towBarItem = getTowBarItem(playerObj)
|
||||||
if towBarItem ~= nil and not (isClient() and isMultiplayer()) then
|
if towBarItem == nil then return end
|
||||||
sendClientCommand(playerObj, "towbar", "consumeTowBar", { itemId = towBarItem:getID() })
|
|
||||||
end
|
|
||||||
playerObj:setPrimaryHandItem(nil)
|
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 = {
|
local args = {
|
||||||
vehicleA = towingVehicle:getId(),
|
vehicleA = towingVehicle:getId(),
|
||||||
vehicleB = towedVehicle:getId(),
|
vehicleB = towedVehicle:getId(),
|
||||||
@@ -496,7 +388,6 @@ function TowBarMod.Hook.performAttachTowBar(playerObj, towingVehicle, towedVehic
|
|||||||
itemId = towBarItem and towBarItem:getID() or nil
|
itemId = towBarItem and towBarItem:getID() or nil
|
||||||
}
|
}
|
||||||
sendTowAttachCommand(playerObj, args)
|
sendTowAttachCommand(playerObj, args)
|
||||||
ISTimedActionQueue.add(TowBarScheduleAction:new(playerObj, 10, TowBarMod.Hook.setVehiclePostAttach, towedVehicle))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
|
function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
|
||||||
@@ -538,7 +429,6 @@ function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
|
|||||||
towingVehicle:transmitModData()
|
towingVehicle:transmitModData()
|
||||||
towedVehicle:transmitModData()
|
towedVehicle:transmitModData()
|
||||||
|
|
||||||
TowBarMod.Hook.lastAutoReattachAtByVehicle[towingVehicle:getId()] = nil
|
|
||||||
setTowBarModelVisible(towedVehicle, false)
|
setTowBarModelVisible(towedVehicle, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -547,46 +437,6 @@ function TowBarMod.Hook.performDetachTowBar(playerObj, towingVehicle, towedVehic
|
|||||||
|
|
||||||
local args = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() }
|
local args = { towingVehicle = towingVehicle:getId(), vehicle = towedVehicle:getId() }
|
||||||
sendClientCommand(playerObj, "towbar", "detachTowBar", args)
|
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
|
end
|
||||||
|
|
||||||
local function forEachCollectionItem(collection, callback)
|
local function forEachCollectionItem(collection, callback)
|
||||||
@@ -634,14 +484,6 @@ local function keepTowBarVehiclesFreeRolling()
|
|||||||
end)
|
end)
|
||||||
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)
|
function TowBarMod.Hook.attachByTowBarAction(playerObj, towingVehicle, towedVehicle)
|
||||||
if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
|
if playerObj == nil or towingVehicle == nil or towedVehicle == nil then return end
|
||||||
|
|
||||||
@@ -725,20 +567,11 @@ function TowBarMod.Hook.deattachTowBarAction(playerObj, vehicle)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function TowBarMod.Hook.OnSpawnVehicle(vehicle)
|
function TowBarMod.Hook.OnSpawnVehicle(vehicle)
|
||||||
recoverTowBarVehicleAfterLoad(nil, vehicle, 6)
|
-- Server persistence snapshots own attach recovery after vehicle streaming.
|
||||||
end
|
end
|
||||||
|
|
||||||
function TowBarMod.Hook.OnGameStart()
|
function TowBarMod.Hook.OnGameStart()
|
||||||
local cell = getCell()
|
-- Server persistence broadcasts the same attach snapshot used by a fresh pair.
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---------------------------------------------------------------------------
|
---------------------------------------------------------------------------
|
||||||
@@ -749,8 +582,9 @@ function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
|
|||||||
if not vehicle then return end
|
if not vehicle then return end
|
||||||
local normalPart = vehicle:getPartById("towbar")
|
local normalPart = vehicle:getPartById("towbar")
|
||||||
local largePart = vehicle:getPartById("towbarLarge")
|
local largePart = vehicle:getPartById("towbarLarge")
|
||||||
if normalPart == nil and largePart == nil then
|
local ki5Part = vehicle:getPartById("towbarKI5")
|
||||||
print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
|
if normalPart == nil and largePart == nil and ki5Part == nil then
|
||||||
|
print("[TowBar DEV] No towbar model part found on vehicle " .. tostring(vehicle:getScriptName()))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local script = vehicle:getScript()
|
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] chassisShape.z = " .. tostring(chassisZ) .. ", half = " .. tostring(halfZ))
|
||||||
print("[TowBar DEV] frontEdgeZ = " .. tostring(script and getTowbarFrontEdgeZ(script) or nil) .. ", part = " .. selectedPart)
|
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] 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
|
for j = 0, TowbarVariantSize - 1 do
|
||||||
if normalPart then normalPart:setModelVisible("towbar" .. j, true) end
|
if normalPart then normalPart:setModelVisible("towbar" .. j, true) end
|
||||||
if largePart then largePart:setModelVisible("towbar" .. j, true) end
|
if largePart then largePart:setModelVisible("towbar" .. j, true) end
|
||||||
|
if ki5Part then ki5Part:setModelVisible("towbar" .. j, true) end
|
||||||
end
|
end
|
||||||
vehicle:doDamageOverlay()
|
vehicle:doDamageOverlay()
|
||||||
end
|
end
|
||||||
@@ -777,14 +612,16 @@ function TowBarMod.Hook.devHideAllTowbarModels(playerObj, vehicle)
|
|||||||
if not vehicle then return end
|
if not vehicle then return end
|
||||||
local normalPart = vehicle:getPartById("towbar")
|
local normalPart = vehicle:getPartById("towbar")
|
||||||
local largePart = vehicle:getPartById("towbarLarge")
|
local largePart = vehicle:getPartById("towbarLarge")
|
||||||
if normalPart == nil and largePart == nil then
|
local ki5Part = vehicle:getPartById("towbarKI5")
|
||||||
print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
|
if normalPart == nil and largePart == nil and ki5Part == nil then
|
||||||
|
print("[TowBar DEV] No towbar model part found on vehicle " .. tostring(vehicle:getScriptName()))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
print("[TowBar DEV] Hiding ALL towbar models on " .. tostring(vehicle:getScriptName()))
|
print("[TowBar DEV] Hiding ALL towbar models on " .. tostring(vehicle:getScriptName()))
|
||||||
for j = 0, TowbarVariantSize - 1 do
|
for j = 0, TowbarVariantSize - 1 do
|
||||||
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
|
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
|
||||||
if largePart then largePart:setModelVisible("towbar" .. j, false) end
|
if largePart then largePart:setModelVisible("towbar" .. j, false) end
|
||||||
|
if ki5Part then ki5Part:setModelVisible("towbar" .. j, false) end
|
||||||
end
|
end
|
||||||
vehicle:doDamageOverlay()
|
vehicle:doDamageOverlay()
|
||||||
end
|
end
|
||||||
@@ -793,24 +630,32 @@ function TowBarMod.Hook.devShowSingleTowbar(playerObj, vehicle, index)
|
|||||||
if not vehicle then return end
|
if not vehicle then return end
|
||||||
local normalPart = vehicle:getPartById("towbar")
|
local normalPart = vehicle:getPartById("towbar")
|
||||||
local largePart = vehicle:getPartById("towbarLarge")
|
local largePart = vehicle:getPartById("towbarLarge")
|
||||||
if normalPart == nil and largePart == nil then
|
local ki5Part = vehicle:getPartById("towbarKI5")
|
||||||
print("[TowBar DEV] No 'towbar' or 'towbarLarge' part found on vehicle " .. tostring(vehicle:getScriptName()))
|
if normalPart == nil and largePart == nil and ki5Part == nil then
|
||||||
|
print("[TowBar DEV] No towbar model part found on vehicle " .. tostring(vehicle:getScriptName()))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local localIndex = math.max(0, math.min(TowbarMaxIndex, index % TowbarVariantSize))
|
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
|
for j = 0, TowbarVariantSize - 1 do
|
||||||
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
|
if normalPart then normalPart:setModelVisible("towbar" .. j, false) end
|
||||||
if largePart then largePart:setModelVisible("towbar" .. j, false) end
|
if largePart then largePart:setModelVisible("towbar" .. j, false) end
|
||||||
|
if ki5Part then ki5Part:setModelVisible("towbar" .. j, false) end
|
||||||
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
|
if part == nil then
|
||||||
part = normalPart or largePart
|
part = normalPart or largePart or ki5Part
|
||||||
end
|
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
|
if part then
|
||||||
part:setModelVisible("towbar" .. localIndex, true)
|
part:setModelVisible("towbar" .. localIndex, true)
|
||||||
end
|
end
|
||||||
@@ -821,6 +666,4 @@ Events.OnSpawnVehicleEnd.Add(TowBarMod.Hook.OnSpawnVehicle)
|
|||||||
if Events.OnGameStart then
|
if Events.OnGameStart then
|
||||||
Events.OnGameStart.Add(TowBarMod.Hook.OnGameStart)
|
Events.OnGameStart.Add(TowBarMod.Hook.OnGameStart)
|
||||||
end
|
end
|
||||||
Events.OnEnterVehicle.Add(TowBarMod.Hook.OnEnterVehicle)
|
|
||||||
Events.OnSwitchVehicleSeat.Add(TowBarMod.Hook.OnSwitchVehicleSeat)
|
|
||||||
Events.OnTick.Add(keepTowBarVehiclesFreeRolling)
|
Events.OnTick.Add(keepTowBarVehiclesFreeRolling)
|
||||||
|
|||||||
@@ -4,6 +4,26 @@ TowBarMod.WreckerSync = TowBarMod.WreckerSync or {}
|
|||||||
|
|
||||||
local Sync = TowBarMod.WreckerSync
|
local Sync = TowBarMod.WreckerSync
|
||||||
Sync.appliedLevels = Sync.appliedLevels or {}
|
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 function pairKeyContainsVehicle(key, vehicleId)
|
||||||
local id = tostring(vehicleId)
|
local id = tostring(vehicleId)
|
||||||
@@ -123,8 +143,10 @@ local function setScriptSafely(vehicle, scriptName)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local function applyAttachSync(args)
|
local function applyAttachSync(args, forceReattach)
|
||||||
if not args then return end
|
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 wrecker = getVehicleById(args.wrecker)
|
||||||
local target = getVehicleById(args.target)
|
local target = getVehicleById(args.target)
|
||||||
if not wrecker or not target then return end
|
if not wrecker or not target then return end
|
||||||
@@ -138,7 +160,7 @@ local function applyAttachSync(args)
|
|||||||
local key = pairKey(wrecker, target)
|
local key = pairKey(wrecker, target)
|
||||||
local canonicalLevel = tonumber(args.heightLevel) or 0
|
local canonicalLevel = tonumber(args.heightLevel) or 0
|
||||||
local appliedLevel = Sync.appliedLevels[key]
|
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
|
Sync.appliedLevels[key] = canonicalLevel
|
||||||
applyFreeRollingState(target)
|
applyFreeRollingState(target)
|
||||||
return
|
return
|
||||||
@@ -167,6 +189,8 @@ end
|
|||||||
|
|
||||||
local function applyDetachSync(args)
|
local function applyDetachSync(args)
|
||||||
if not args then return end
|
if not args then return end
|
||||||
|
local desiredKey = argsPairKey(args)
|
||||||
|
if desiredKey then Sync.desiredPairs[desiredKey] = nil end
|
||||||
local wrecker = getVehicleById(args.wrecker)
|
local wrecker = getVehicleById(args.wrecker)
|
||||||
local target = getVehicleById(args.target)
|
local target = getVehicleById(args.target)
|
||||||
if not target then return end
|
if not target then return end
|
||||||
@@ -203,6 +227,30 @@ end
|
|||||||
Sync.applyAttachSync = applyAttachSync
|
Sync.applyAttachSync = applyAttachSync
|
||||||
Sync.applyDetachSync = applyDetachSync
|
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)
|
Events.OnServerCommand.Add(function(module, command, args)
|
||||||
if module ~= "towbar" then return end
|
if module ~= "towbar" then return end
|
||||||
if command == "wreckerAttachSync" then
|
if command == "wreckerAttachSync" then
|
||||||
@@ -214,3 +262,9 @@ end)
|
|||||||
if Events.OnSpawnVehicleEnd then
|
if Events.OnSpawnVehicleEnd then
|
||||||
Events.OnSpawnVehicleEnd.Add(clearAppliedLevelForVehicle)
|
Events.OnSpawnVehicleEnd.Add(clearAppliedLevelForVehicle)
|
||||||
end
|
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.Create = {}
|
||||||
BTtow.Init = {}
|
BTtow.Init = {}
|
||||||
|
|
||||||
|
require("TowBar/VehicleCompatibility")
|
||||||
|
|
||||||
local TowbarVariantSize = 24
|
local TowbarVariantSize = 24
|
||||||
local TowbarMaxIndex = TowbarVariantSize - 1
|
local TowbarMaxIndex = TowbarVariantSize - 1
|
||||||
local TowbarFirstZ = 1.0
|
local TowbarFirstZ = 1.0
|
||||||
@@ -13,6 +15,8 @@ local TowbarVisualScale = 2.5
|
|||||||
local TowbarModelLength = 0.9714089036
|
local TowbarModelLength = 0.9714089036
|
||||||
local TowbarScaledModelLength = TowbarModelLength * TowbarVisualScale
|
local TowbarScaledModelLength = TowbarModelLength * TowbarVisualScale
|
||||||
local TowbarModelHalfLength = TowbarScaledModelLength / 2
|
local TowbarModelHalfLength = TowbarScaledModelLength / 2
|
||||||
|
local VanillaScaleMin = 1.5
|
||||||
|
local VanillaScaleMax = 2.0
|
||||||
|
|
||||||
local function getTowbarFrontEdgeZ(script)
|
local function getTowbarFrontEdgeZ(script)
|
||||||
if not script then return nil end
|
if not script then return nil end
|
||||||
@@ -53,6 +57,59 @@ local function getTowbarModelSlot(script)
|
|||||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||||
end
|
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)
|
function BTtow.Create.towbar(vehicle, part)
|
||||||
if part == nil then return end
|
if part == nil then return end
|
||||||
for j=0, TowbarVariantSize - 1 do
|
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
|
if modData and modData["isTowingByTowBar"] and modData["towed"] then
|
||||||
local script = vehicle:getScript()
|
local script = vehicle:getScript()
|
||||||
if script then
|
if script then
|
||||||
local index = getTowbarModelSlot(script)
|
local isKi5 = TowBarMod.Compatibility.isKi5Vehicle(vehicle)
|
||||||
local shouldShowOnThisPart = part:getId() == "towbar"
|
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
|
if shouldShowOnThisPart and index ~= nil then
|
||||||
part:setModelVisible("towbar" .. index, true)
|
part:setModelVisible("towbar" .. index, true)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ require("TowBar/Persistence")
|
|||||||
local TowingCommands = {}
|
local TowingCommands = {}
|
||||||
local Commands = {}
|
local Commands = {}
|
||||||
local TowBarItemType = "TowBar.TowBar"
|
local TowBarItemType = "TowBar.TowBar"
|
||||||
local SyncDelayTicks = 2
|
|
||||||
local SnapshotIntervalTicks = 120
|
local SnapshotIntervalTicks = 120
|
||||||
local BrokenPairAuditIntervalTicks = 5
|
local BrokenPairAuditIntervalTicks = 5
|
||||||
local pendingSync = {}
|
|
||||||
local snapshotTickCounter = 0
|
local snapshotTickCounter = 0
|
||||||
local brokenPairAuditTickCounter = 0
|
local brokenPairAuditTickCounter = 0
|
||||||
local confirmedTowPairs = {}
|
local confirmedTowPairs = {}
|
||||||
@@ -24,57 +22,6 @@ local noise = function(msg)
|
|||||||
end
|
end
|
||||||
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)
|
local function resolveAttachmentA(args, vehicleA)
|
||||||
if args and args.attachmentA then return args.attachmentA end
|
if args and args.attachmentA then return args.attachmentA end
|
||||||
if vehicleA and vehicleA:getTowAttachmentSelf() then return vehicleA:getTowAttachmentSelf() end
|
if vehicleA and vehicleA:getTowAttachmentSelf() then return vehicleA:getTowAttachmentSelf() end
|
||||||
@@ -369,10 +316,16 @@ local function broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function broadcastDetach(vehicleAId, vehicleBId)
|
local function broadcastDetach(vehicleAId, vehicleBId)
|
||||||
sendServerCommand("towbar", "forceDetachSync", {
|
local args = {
|
||||||
vehicleA = vehicleAId,
|
vehicleA = vehicleAId,
|
||||||
vehicleB = vehicleBId
|
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
|
end
|
||||||
|
|
||||||
local function broadcastSpontaneousDetach(vehicleA, vehicleB)
|
local function broadcastSpontaneousDetach(vehicleA, vehicleB)
|
||||||
@@ -458,7 +411,6 @@ local function finalizeBrokenTowBarPair(towingVehicle, towedVehicle, reason)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
cancelPendingAttach(towingVehicle, towedVehicle)
|
|
||||||
forgetTowBarPair(towingVehicle, towedVehicle)
|
forgetTowBarPair(towingVehicle, towedVehicle)
|
||||||
breakTowBarConstraint(towingVehicle, towedVehicle)
|
breakTowBarConstraint(towingVehicle, towedVehicle)
|
||||||
clearExpectedTowBarPair(towingVehicle, towedVehicle)
|
clearExpectedTowBarPair(towingVehicle, towedVehicle)
|
||||||
@@ -567,54 +519,6 @@ local function reconcileBrokenTowBarPairsServer()
|
|||||||
end)
|
end)
|
||||||
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 function snapshotActiveTowbarLinksServer()
|
||||||
local cell = getCell()
|
local cell = getCell()
|
||||||
if not cell then return end
|
if not cell then return end
|
||||||
@@ -643,7 +547,7 @@ local function snapshotActiveTowbarLinksServer()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function processPendingSync()
|
local function processTowBarServerTick()
|
||||||
reconcileBrokenTowBarPairsServer()
|
reconcileBrokenTowBarPairsServer()
|
||||||
|
|
||||||
snapshotTickCounter = snapshotTickCounter + 1
|
snapshotTickCounter = snapshotTickCounter + 1
|
||||||
@@ -651,37 +555,6 @@ local function processPendingSync()
|
|||||||
snapshotTickCounter = 0
|
snapshotTickCounter = 0
|
||||||
snapshotActiveTowbarLinksServer()
|
snapshotActiveTowbarLinksServer()
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
function Commands.attachTowBar(player, args)
|
function Commands.attachTowBar(player, args)
|
||||||
@@ -707,9 +580,7 @@ function Commands.attachTowBar(player, args)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
if isExpectedTowBarPair(vehicleA, vehicleB)
|
if isExpectedTowBarPair(vehicleA, vehicleB)
|
||||||
or isExpectedTowBarPair(vehicleB, vehicleA)
|
or isExpectedTowBarPair(vehicleB, vehicleA) then
|
||||||
or hasPendingAttach(vehicleA, vehicleB)
|
|
||||||
or hasPendingAttach(vehicleB, vehicleA) then
|
|
||||||
noise("rejected duplicate pending towbar attach")
|
noise("rejected duplicate pending towbar attach")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -724,12 +595,14 @@ function Commands.attachTowBar(player, args)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Constraint creation may complete on a later server tick in multiplayer.
|
-- Match the working wrecker path: persist the accepted logical pair, ask
|
||||||
-- Record and queue the reserved item first so an immediate Build 42.20
|
-- the server for its native relation, then immediately tell clients to
|
||||||
-- spontaneous break can resolve the pair and consume the reservation once.
|
-- replace that relation with the local rigid fake-trailer constraint.
|
||||||
markExpectedTowBarPair(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
|
markExpectedTowBarPair(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
|
||||||
queueSync("attach", player, args, true)
|
if isServer() then
|
||||||
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
|
vehicleA:addPointConstraint(player, vehicleB, args.attachmentA, args.attachmentB)
|
||||||
|
end
|
||||||
|
broadcastAttach(vehicleA, vehicleB, args.attachmentA, args.attachmentB)
|
||||||
if isLinked(vehicleA, vehicleB) then
|
if isLinked(vehicleA, vehicleB) then
|
||||||
markTowBarPairConfirmed(vehicleA, vehicleB)
|
markTowBarPairConfirmed(vehicleA, vehicleB)
|
||||||
end
|
end
|
||||||
@@ -749,10 +622,6 @@ function Commands.detachTowBar(player, args)
|
|||||||
noise("rejected unauthorized towbar detach")
|
noise("rejected unauthorized towbar detach")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if not isLinked(towingVehicle, towedVehicle) then
|
|
||||||
noise("rejected mismatched or stale towbar detach")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not isExpectedTowBarPair(towingVehicle, towedVehicle)
|
if not isExpectedTowBarPair(towingVehicle, towedVehicle)
|
||||||
and not isLegacyTowBarPair(towingVehicle, towedVehicle) then
|
and not isLegacyTowBarPair(towingVehicle, towedVehicle) then
|
||||||
noise("rejected detach for a non-towbar or stale linked pair")
|
noise("rejected detach for a non-towbar or stale linked pair")
|
||||||
@@ -764,7 +633,7 @@ function Commands.detachTowBar(player, args)
|
|||||||
breakTowBarConstraint(towingVehicle, towedVehicle)
|
breakTowBarConstraint(towingVehicle, towedVehicle)
|
||||||
forgetTowBarPair(towingVehicle, towedVehicle)
|
forgetTowBarPair(towingVehicle, towedVehicle)
|
||||||
clearExpectedTowBarPair(towingVehicle, towedVehicle)
|
clearExpectedTowBarPair(towingVehicle, towedVehicle)
|
||||||
queueSync("detach", player, args)
|
broadcastDetach(towingVehicle:getId(), towedVehicle:getId())
|
||||||
if shouldRefund then
|
if shouldRefund then
|
||||||
giveTowBar(player, true)
|
giveTowBar(player, true)
|
||||||
end
|
end
|
||||||
@@ -817,4 +686,4 @@ TowingCommands.OnClientCommand = function(module, command, player, args)
|
|||||||
end
|
end
|
||||||
|
|
||||||
Events.OnClientCommand.Add(TowingCommands.OnClientCommand)
|
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,
|
create = BTtow.Create.towbar,
|
||||||
init = BTtow.Init.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
|
part Battery
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
module Base
|
module Base
|
||||||
{
|
{
|
||||||
model towbarModel
|
model towbarModel
|
||||||
|
{
|
||||||
|
mesh = vehicles/Towbar,
|
||||||
|
texture = Vehicles/Towbar_Texture,
|
||||||
|
scale = 0.01,
|
||||||
|
}
|
||||||
|
|
||||||
|
model towbarModelKI5
|
||||||
{
|
{
|
||||||
mesh = vehicles/Towbar,
|
mesh = vehicles/Towbar,
|
||||||
texture = Vehicles/Towbar_Texture,
|
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, }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
name=Towbars
|
name=Towbars
|
||||||
id=hrsys_towbars_testing
|
id=hrsys_towbars
|
||||||
poster=../common/media/textures/preview.png
|
poster=../common/media/textures/preview.png
|
||||||
description=Tow bars for vehicle-to-vehicle towing.
|
description=Tow bars for vehicle-to-vehicle towing.
|
||||||
author=Riggs0
|
author=Riggs0
|
||||||
category=vehicle
|
category=vehicle
|
||||||
icon=../common/media/textures/tow_bar_icon.png
|
icon=../common/media/textures/tow_bar_icon.png
|
||||||
url=https://hudsonriggs.systems
|
url=https://hudsonriggs.systems
|
||||||
modversion=1.0.12
|
modversion=1.0.22
|
||||||
versionMin=42.20.0
|
versionMin=42.20.0
|
||||||
incompatible=\STowTruck_B42
|
incompatible=\STowTruck_B42
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
name=Towbars
|
name=Towbars
|
||||||
id=hrsys_towbars_testing
|
id=hrsys_towbars
|
||||||
poster=common/media/textures/preview.png
|
poster=common/media/textures/preview.png
|
||||||
description=Tow bars for vehicle-to-vehicle towing.
|
description=Tow bars for vehicle-to-vehicle towing.
|
||||||
author=Riggs0
|
author=Riggs0
|
||||||
category=vehicle
|
category=vehicle
|
||||||
versionMin=42.13.0
|
versionMin=42.13.0
|
||||||
url=https://hudsonriggs.systems
|
url=https://hudsonriggs.systems
|
||||||
modversion=1.0.12
|
modversion=1.0.22
|
||||||
icon=common/media/textures/tow_bar_icon.png
|
icon=common/media/textures/tow_bar_icon.png
|
||||||
incompatible=\STowTruck_B42
|
incompatible=\STowTruck_B42
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
local failures = 0
|
||||||
|
|
||||||
|
local function expect(condition, message)
|
||||||
|
if not condition then
|
||||||
|
failures = failures + 1
|
||||||
|
io.stderr:write("FAIL: " .. message .. "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
package.path = table.concat({
|
||||||
|
"42.20/media/lua/client/?.lua",
|
||||||
|
"42.20/media/lua/shared/?.lua",
|
||||||
|
package.path
|
||||||
|
}, ";")
|
||||||
|
|
||||||
|
isServer = function() return false end
|
||||||
|
isClient = function() return true end
|
||||||
|
|
||||||
|
local serverCommandCallbacks = {}
|
||||||
|
local enterVehicleCallbacks = {}
|
||||||
|
local spawnVehicleCallbacks = {}
|
||||||
|
local tickCallbacks = {}
|
||||||
|
Events = {
|
||||||
|
OnServerCommand = {
|
||||||
|
Add = function(callback)
|
||||||
|
serverCommandCallbacks[#serverCommandCallbacks + 1] = callback
|
||||||
|
end
|
||||||
|
},
|
||||||
|
OnEnterVehicle = {
|
||||||
|
Add = function(callback)
|
||||||
|
enterVehicleCallbacks[#enterVehicleCallbacks + 1] = callback
|
||||||
|
end
|
||||||
|
},
|
||||||
|
OnSwitchVehicleSeat = { Add = function() end },
|
||||||
|
OnSpawnVehicleEnd = {
|
||||||
|
Add = function(callback)
|
||||||
|
spawnVehicleCallbacks[#spawnVehicleCallbacks + 1] = callback
|
||||||
|
end
|
||||||
|
},
|
||||||
|
OnTick = {
|
||||||
|
Add = function(callback)
|
||||||
|
tickCallbacks[#tickCallbacks + 1] = callback
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
local localPlayer = { vehicle = nil }
|
||||||
|
function localPlayer:getVehicle() return self.vehicle end
|
||||||
|
|
||||||
|
local attachmentIds = {
|
||||||
|
trailer = true,
|
||||||
|
trailerfront = true,
|
||||||
|
towbarWreckerHookLow = true,
|
||||||
|
towbarWreckerHookMid = true,
|
||||||
|
towbarWreckerHookHigh = true
|
||||||
|
}
|
||||||
|
|
||||||
|
local function unlink(vehicleA, vehicleB)
|
||||||
|
vehicleA.towing, vehicleA.towedBy = nil, nil
|
||||||
|
vehicleB.towing, vehicleB.towedBy = nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function link(vehicleA, vehicleB)
|
||||||
|
vehicleA.towing, vehicleA.towedBy = vehicleB, nil
|
||||||
|
vehicleB.towing, vehicleB.towedBy = nil, vehicleA
|
||||||
|
end
|
||||||
|
|
||||||
|
local function newVehicle(id, sqlId)
|
||||||
|
local value = {
|
||||||
|
id = id,
|
||||||
|
sqlId = sqlId,
|
||||||
|
modData = {},
|
||||||
|
scriptName = "Base.DriverEntry" .. tostring(id),
|
||||||
|
mass = 1000,
|
||||||
|
brakingForce = 20,
|
||||||
|
towing = nil,
|
||||||
|
towedBy = nil,
|
||||||
|
driver = nil,
|
||||||
|
addCalls = 0,
|
||||||
|
breakCalls = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
local script = {
|
||||||
|
getAttachmentById = function(_, attachmentId)
|
||||||
|
return attachmentIds[attachmentId] and {} or nil
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
function value:getId() return self.id end
|
||||||
|
function value:getSqlId() return self.sqlId end
|
||||||
|
function value:getModData() return self.modData end
|
||||||
|
function value:transmitModData() end
|
||||||
|
function value:getScript() return script end
|
||||||
|
function value:getScriptName() return self.scriptName end
|
||||||
|
function value:setScriptName(scriptName) self.scriptName = scriptName end
|
||||||
|
function value:getMass() return self.mass end
|
||||||
|
function value:setMass(mass) self.mass = mass end
|
||||||
|
function value:getBrakingForce() return self.brakingForce end
|
||||||
|
function value:setBrakingForce(force) self.brakingForce = force end
|
||||||
|
function value:isParkingBrakeOn() return false end
|
||||||
|
function value:setParkingBrakeOn() end
|
||||||
|
function value:getParkingBrake() return false end
|
||||||
|
function value:setParkingBrake() end
|
||||||
|
function value:isHandbrake() return false end
|
||||||
|
function value:setHandbrake() end
|
||||||
|
function value:getVehicleTowing() return self.towing end
|
||||||
|
function value:getVehicleTowedBy() return self.towedBy end
|
||||||
|
function value:isDriver(player) return self.driver == player end
|
||||||
|
function value:attachmentExist(attachmentId)
|
||||||
|
return attachmentIds[attachmentId] == true
|
||||||
|
end
|
||||||
|
function value:addPointConstraint(_, other)
|
||||||
|
self.addCalls = self.addCalls + 1
|
||||||
|
link(self, other)
|
||||||
|
end
|
||||||
|
function value:breakConstraint()
|
||||||
|
self.breakCalls = self.breakCalls + 1
|
||||||
|
local other = self.towing or self.towedBy
|
||||||
|
if other then unlink(self, other) end
|
||||||
|
end
|
||||||
|
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local portableTowing = newVehicle(101, 1001)
|
||||||
|
local portableTowed = newVehicle(102, 1002)
|
||||||
|
local wrecker = newVehicle(201, 2001)
|
||||||
|
local wreckerTarget = newVehicle(202, 2002)
|
||||||
|
local unrelated = newVehicle(301, 3001)
|
||||||
|
local allVehicles = {
|
||||||
|
portableTowing,
|
||||||
|
portableTowed,
|
||||||
|
wrecker,
|
||||||
|
wreckerTarget,
|
||||||
|
unrelated
|
||||||
|
}
|
||||||
|
local vehiclesById = {}
|
||||||
|
for _, vehicle in ipairs(allVehicles) do vehiclesById[vehicle:getId()] = vehicle end
|
||||||
|
|
||||||
|
getPlayer = function() return localPlayer end
|
||||||
|
getVehicleById = function(id) return vehiclesById[tonumber(id)] end
|
||||||
|
getCell = function()
|
||||||
|
return { getVehicles = function() return allVehicles end }
|
||||||
|
end
|
||||||
|
|
||||||
|
TowBarMod = {
|
||||||
|
Utils = { updateAttachmentsForRigidTow = function() end },
|
||||||
|
Hook = {
|
||||||
|
applyFreeRollingTowState = function() end,
|
||||||
|
setVehiclePostAttach = function() end,
|
||||||
|
setVehicleScriptWithTowBarHidden = function(vehicle, scriptName)
|
||||||
|
vehicle:setScriptName(scriptName)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
cleanupDetachedTowBar = function() end
|
||||||
|
},
|
||||||
|
Wrecker = {
|
||||||
|
getWorldVehicles = function() return allVehicles end,
|
||||||
|
getHeightAttachmentId = function(level)
|
||||||
|
return ({
|
||||||
|
[0] = "towbarWreckerHookLow",
|
||||||
|
[1] = "towbarWreckerHookMid",
|
||||||
|
[2] = "towbarWreckerHookHigh"
|
||||||
|
})[tonumber(level) or 0]
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Model a fully persisted portable pair whose transient sync cache was lost
|
||||||
|
-- during reload. The exact native relation is deliberately present: entry
|
||||||
|
-- must replace it with one fresh client-owned rigid constraint.
|
||||||
|
portableTowing.modData = {
|
||||||
|
isTowingByTowBar = true,
|
||||||
|
towed = false,
|
||||||
|
towBarTowedVehicleId = portableTowed:getId(),
|
||||||
|
towBarTowedVehicleSqlId = portableTowed:getSqlId(),
|
||||||
|
towBarExpectedAttachment = "trailer"
|
||||||
|
}
|
||||||
|
portableTowed.modData = {
|
||||||
|
isTowingByTowBar = true,
|
||||||
|
towed = true,
|
||||||
|
towBarTowingVehicleId = portableTowing:getId(),
|
||||||
|
towBarTowingVehicleSqlId = portableTowing:getSqlId(),
|
||||||
|
towBarExpectedAttachment = "trailerfront",
|
||||||
|
towBarOriginalScriptName = portableTowed:getScriptName()
|
||||||
|
}
|
||||||
|
link(portableTowing, portableTowed)
|
||||||
|
|
||||||
|
-- Model the equivalent persisted wrecker pair, also without applied-level
|
||||||
|
-- cache state. Both members carry the reciprocal identity written by the
|
||||||
|
-- authoritative wrecker attach path.
|
||||||
|
wrecker.modData = {
|
||||||
|
wreckerTowActive = true,
|
||||||
|
wreckerTowedVehicleId = wreckerTarget:getId(),
|
||||||
|
wreckerTowedVehicleSqlId = wreckerTarget:getSqlId(),
|
||||||
|
wreckerTargetAttachment = "trailerfront",
|
||||||
|
wreckerHeightLevel = 2
|
||||||
|
}
|
||||||
|
wreckerTarget.modData = {
|
||||||
|
wreckerTowingVehicleId = wrecker:getId(),
|
||||||
|
wreckerTowingVehicleSqlId = wrecker:getSqlId(),
|
||||||
|
wreckerOriginalScriptName = wreckerTarget:getScriptName()
|
||||||
|
}
|
||||||
|
link(wrecker, wreckerTarget)
|
||||||
|
|
||||||
|
dofile("42.20/media/lua/client/TowBar/TowSyncClient.lua")
|
||||||
|
dofile("42.20/media/lua/client/TowBar/WreckerSyncClient.lua")
|
||||||
|
|
||||||
|
expect(next(TowBarMod.Sync.desiredPairs) == nil,
|
||||||
|
"portable regression setup must not depend on transient desired-pair sync")
|
||||||
|
expect(next(TowBarMod.Sync.appliedPairs) == nil,
|
||||||
|
"portable regression setup must begin without an applied-pair cache")
|
||||||
|
expect(next(TowBarMod.WreckerSync.appliedLevels) == nil,
|
||||||
|
"wrecker regression setup must begin without an applied-level cache")
|
||||||
|
|
||||||
|
local function enterAsDriver(vehicle)
|
||||||
|
for _, candidate in ipairs(allVehicles) do candidate.driver = nil end
|
||||||
|
vehicle.driver = localPlayer
|
||||||
|
localPlayer.vehicle = vehicle
|
||||||
|
for i = 1, #enterVehicleCallbacks do
|
||||||
|
enterVehicleCallbacks[i](localPlayer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
enterAsDriver(portableTowing)
|
||||||
|
expect(portableTowing.breakCalls == 1 and portableTowing.addCalls == 1,
|
||||||
|
"portable driver entry must force exactly one rebuild from persisted modData")
|
||||||
|
enterAsDriver(portableTowing)
|
||||||
|
expect(portableTowing.breakCalls == 2 and portableTowing.addCalls == 2,
|
||||||
|
"each later portable driver entry must force one new rebuild, not remain cache-idempotent")
|
||||||
|
|
||||||
|
enterAsDriver(wrecker)
|
||||||
|
expect(wrecker.breakCalls == 1 and wrecker.addCalls == 1,
|
||||||
|
"wrecker driver entry must force exactly one rebuild from persisted modData")
|
||||||
|
enterAsDriver(wrecker)
|
||||||
|
expect(wrecker.breakCalls == 2 and wrecker.addCalls == 2,
|
||||||
|
"each later wrecker driver entry must force one new rebuild, not remain cache-idempotent")
|
||||||
|
|
||||||
|
local portableCounts = portableTowing.breakCalls + portableTowing.addCalls
|
||||||
|
local wreckerCounts = wrecker.breakCalls + wrecker.addCalls
|
||||||
|
enterAsDriver(unrelated)
|
||||||
|
expect(portableTowing.breakCalls + portableTowing.addCalls == portableCounts
|
||||||
|
and wrecker.breakCalls + wrecker.addCalls == wreckerCounts,
|
||||||
|
"entering an unrelated vehicle must not rebuild any persisted tow pair")
|
||||||
|
expect(#tickCallbacks == 0,
|
||||||
|
"driver-entry recovery must remain event-driven and must not install a tick retry loop")
|
||||||
|
|
||||||
|
if failures > 0 then os.exit(1) end
|
||||||
|
print("PASS: driver entry force-reattaches persisted portable and wrecker pairs once")
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
local failures = 0
|
||||||
|
|
||||||
|
local function expect(condition, message)
|
||||||
|
if not condition then
|
||||||
|
failures = failures + 1
|
||||||
|
io.stderr:write("FAIL: " .. message .. "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local activeModIds = {}
|
||||||
|
|
||||||
|
getActivatedMods = function()
|
||||||
|
return {
|
||||||
|
contains = function(_, modId)
|
||||||
|
return activeModIds[modId] == true
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vehicle(fullName, modelScale)
|
||||||
|
return {
|
||||||
|
getScript = function()
|
||||||
|
return {
|
||||||
|
getFullName = function() return fullName end,
|
||||||
|
getModelScale = function() return modelScale end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local classifierPath = "42.20/media/lua/shared/TowBar/VehicleCompatibility.lua"
|
||||||
|
local loaded, loadError = pcall(dofile, classifierPath)
|
||||||
|
expect(loaded, "dual-mode classifier must load from " .. classifierPath .. ": " .. tostring(loadError))
|
||||||
|
|
||||||
|
local Compatibility = TowBarMod and TowBarMod.Compatibility
|
||||||
|
expect(Compatibility ~= nil, "classifier must expose TowBarMod.Compatibility")
|
||||||
|
expect(Compatibility and type(Compatibility.isKi5Vehicle) == "function",
|
||||||
|
"classifier must expose isKi5Vehicle(vehicle)")
|
||||||
|
|
||||||
|
if Compatibility and type(Compatibility.isKi5Vehicle) == "function" then
|
||||||
|
local function isKi5(fullName, modelScale)
|
||||||
|
return Compatibility.isKi5Vehicle(vehicle(fullName, modelScale))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Positive contracts use exact mod IDs and exact script full names verified
|
||||||
|
-- from the locally installed KI5 workshop items.
|
||||||
|
activeModIds = { ["91range"] = true }
|
||||||
|
expect(isKi5("Base.91range", 0.01), "active KI5 Range Rover must use KI5 mode")
|
||||||
|
|
||||||
|
activeModIds = { ["87toyotaCorolla"] = true }
|
||||||
|
expect(isKi5("Base.87toyotaCorollaAE92levin", 1.75),
|
||||||
|
"active KI5 Corolla must use KI5 mode regardless of model scale")
|
||||||
|
|
||||||
|
activeModIds = { ["76chevyKseries"] = true }
|
||||||
|
expect(isKi5("Base.76chevyK30CCwrecker", 1.75),
|
||||||
|
"active KI5 Chevrolet variant must use KI5 mode")
|
||||||
|
|
||||||
|
-- Exact identity is intentional. Scale, name fragments, attachment shape,
|
||||||
|
-- and an active mod ID by itself must never capture another vehicle.
|
||||||
|
activeModIds = {}
|
||||||
|
expect(not isKi5("Base.91range", 0.01),
|
||||||
|
"a KI5-looking script without its exact active mod must use legacy mode")
|
||||||
|
|
||||||
|
activeModIds = { ["91range"] = true }
|
||||||
|
expect(not isKi5("Base.91rangeBurnt", 0.01),
|
||||||
|
"a suffixed KI5 lookalike must use legacy mode")
|
||||||
|
expect(not isKi5("Base.CarNormal", 0.01),
|
||||||
|
"vanilla must not be classified by KI5-like model scale")
|
||||||
|
expect(not isKi5("Base.SmallCar", 1.75),
|
||||||
|
"vanilla must not be classified by legacy vanilla model scale")
|
||||||
|
expect(not isKi5("Base.Chevalier_Rhino_TowTruck", 0.01),
|
||||||
|
"a non-KI5 tow-truck mod must use legacy mode")
|
||||||
|
|
||||||
|
activeModIds = { ["91range-copy"] = true }
|
||||||
|
expect(not isKi5("Base.91range", 0.01),
|
||||||
|
"a prefix or suffix match on mod ID must not enable KI5 mode")
|
||||||
|
|
||||||
|
expect(not Compatibility.isKi5Vehicle(nil), "nil vehicle must safely use legacy mode")
|
||||||
|
expect(not Compatibility.isKi5Vehicle({ getScript = function() return nil end }),
|
||||||
|
"vehicle without a script must safely use legacy mode")
|
||||||
|
end
|
||||||
|
|
||||||
|
if failures > 0 then os.exit(1) end
|
||||||
|
print("PASS: exact KI5 dual-mode classifier")
|
||||||
@@ -156,14 +156,24 @@ local function runPortableHandlerSpec()
|
|||||||
|
|
||||||
fireTicks(callbacks.tick, 5)
|
fireTicks(callbacks.tick, 5)
|
||||||
expect(metrics.addConstraint == 1, "portable restart must request one native restore")
|
expect(metrics.addConstraint == 1, "portable restart must request one native restore")
|
||||||
expect(metrics.attachSync == 1, "portable restart must broadcast one restore")
|
expect(metrics.attachSync == 1,
|
||||||
|
"portable restart must immediately synchronize one wrecker-style rigid restore")
|
||||||
|
|
||||||
|
-- The restore is queued before the native call. Once Build 42 exposes the
|
||||||
|
-- reciprocal relation, the pending confirmation broadcasts exactly once.
|
||||||
|
linkPair(towing, towed)
|
||||||
|
now = 1100
|
||||||
|
fireTicks(callbacks.tick, 1)
|
||||||
|
expect(metrics.addConstraint == 1,
|
||||||
|
"portable confirmation must adopt the first native add without re-adding")
|
||||||
|
expect(metrics.attachSync == 1,
|
||||||
|
"portable native acknowledgement must not introduce a second sync phase")
|
||||||
|
|
||||||
now = 1500
|
now = 1500
|
||||||
fireTicks(callbacks.tick, 5)
|
fireTicks(callbacks.tick, 4)
|
||||||
expect(metrics.addConstraint == 1, "portable pending restore must not duplicate its constraint")
|
expect(metrics.addConstraint == 1, "portable pending restore must not duplicate its constraint")
|
||||||
expect(metrics.attachSync == 1, "portable pending restore must not duplicate its sync")
|
expect(metrics.attachSync == 1, "portable pending restore must not duplicate its sync")
|
||||||
|
|
||||||
linkPair(towing, towed)
|
|
||||||
now = 2000
|
now = 2000
|
||||||
fireTicks(callbacks.tick, 5)
|
fireTicks(callbacks.tick, 5)
|
||||||
expect(metrics.addConstraint == 1, "portable delayed acknowledgement must be adopted without re-adding")
|
expect(metrics.addConstraint == 1, "portable delayed acknowledgement must be adopted without re-adding")
|
||||||
@@ -187,7 +197,23 @@ local function runPortableHandlerSpec()
|
|||||||
now = 17000
|
now = 17000
|
||||||
fireTicks(callbacks.tick, 5)
|
fireTicks(callbacks.tick, 5)
|
||||||
expect(metrics.addConstraint == 2, "a reloaded portable peer must re-enter recovery")
|
expect(metrics.addConstraint == 2, "a reloaded portable peer must re-enter recovery")
|
||||||
expect(metrics.attachSync == 2, "a reloaded portable peer must receive one fresh restore sync")
|
expect(metrics.attachSync == 2,
|
||||||
|
"a reloaded portable peer must receive one fresh wrecker-style restore sync")
|
||||||
|
|
||||||
|
linkPair(towing, towed)
|
||||||
|
now = 17100
|
||||||
|
fireTicks(callbacks.tick, 1)
|
||||||
|
expect(metrics.addConstraint == 2,
|
||||||
|
"reloaded portable confirmation must adopt without another native add")
|
||||||
|
expect(metrics.attachSync == 2,
|
||||||
|
"reloaded portable native acknowledgement must not duplicate its sync")
|
||||||
|
|
||||||
|
now = 17500
|
||||||
|
fireTicks(callbacks.tick, 4)
|
||||||
|
expect(metrics.addConstraint == 2,
|
||||||
|
"confirmed reloaded pair must not duplicate its native add")
|
||||||
|
expect(metrics.attachSync == 2,
|
||||||
|
"confirmed reloaded pair must not duplicate its authoritative sync")
|
||||||
expect(metrics.worldDrops == 0, "portable peer reload must not be mistaken for a break")
|
expect(metrics.worldDrops == 0, "portable peer reload must not be mistaken for a break")
|
||||||
expect(metrics.breakConstraint == 0, "portable peer reload must not break an unrelated constraint")
|
expect(metrics.breakConstraint == 0, "portable peer reload must not break an unrelated constraint")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
local failures = 0
|
||||||
|
|
||||||
|
local function expect(condition, message)
|
||||||
|
if not condition then
|
||||||
|
failures = failures + 1
|
||||||
|
io.stderr:write("FAIL: " .. message .. "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
package.path = table.concat({
|
||||||
|
"42.20/media/lua/client/?.lua",
|
||||||
|
"42.20/media/lua/shared/?.lua",
|
||||||
|
package.path
|
||||||
|
}, ";")
|
||||||
|
|
||||||
|
isServer = function() return false end
|
||||||
|
isClient = function() return true end
|
||||||
|
|
||||||
|
local serverCommandCallbacks = {}
|
||||||
|
local enterVehicleCallbacks = {}
|
||||||
|
local spawnVehicleCallbacks = {}
|
||||||
|
Events = {
|
||||||
|
OnServerCommand = {
|
||||||
|
Add = function(callback)
|
||||||
|
serverCommandCallbacks[#serverCommandCallbacks + 1] = callback
|
||||||
|
end
|
||||||
|
},
|
||||||
|
OnEnterVehicle = {
|
||||||
|
Add = function(callback)
|
||||||
|
enterVehicleCallbacks[#enterVehicleCallbacks + 1] = callback
|
||||||
|
end
|
||||||
|
},
|
||||||
|
OnSpawnVehicleEnd = {
|
||||||
|
Add = function(callback)
|
||||||
|
spawnVehicleCallbacks[#spawnVehicleCallbacks + 1] = callback
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
local localPlayer = { onlineId = 7 }
|
||||||
|
local function attachmentScript()
|
||||||
|
local attachments = { trailer = {}, trailerfront = {} }
|
||||||
|
return {
|
||||||
|
getAttachmentById = function(_, attachmentId)
|
||||||
|
return attachments[attachmentId]
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unlink(a, b)
|
||||||
|
a.towing, a.towedBy = nil, nil
|
||||||
|
b.towing, b.towedBy = nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vehicle(id)
|
||||||
|
local value = {
|
||||||
|
id = id,
|
||||||
|
script = attachmentScript(),
|
||||||
|
scriptName = "Base.DriverOwnership" .. tostring(id),
|
||||||
|
modData = {},
|
||||||
|
driver = nil,
|
||||||
|
towing = nil,
|
||||||
|
towedBy = nil,
|
||||||
|
addCalls = 0,
|
||||||
|
breakCalls = 0,
|
||||||
|
freeRolling = false,
|
||||||
|
freeRollingAtAdd = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function value:getId() return self.id end
|
||||||
|
function value:getScript() return self.script end
|
||||||
|
function value:getScriptName() return self.scriptName end
|
||||||
|
function value:setScriptName(scriptName) self.scriptName = scriptName end
|
||||||
|
function value:getModData() return self.modData end
|
||||||
|
function value:transmitModData() end
|
||||||
|
function value:getMass() return 1000 end
|
||||||
|
function value:getBrakingForce() return 20 end
|
||||||
|
function value:getVehicleTowing() return self.towing end
|
||||||
|
function value:getVehicleTowedBy() return self.towedBy end
|
||||||
|
function value:getDriver() return self.driver end
|
||||||
|
function value:isDriver(player) return self.driver == player end
|
||||||
|
function value:addPointConstraint(player, other, attachmentA, attachmentB, localOnly)
|
||||||
|
self.addCalls = self.addCalls + 1
|
||||||
|
self.freeRollingAtAdd[#self.freeRollingAtAdd + 1] = other.freeRolling
|
||||||
|
expect(player == nil, "client rigid attach must not impersonate a player")
|
||||||
|
expect(attachmentA == "trailer" and attachmentB == "trailerfront",
|
||||||
|
"deferred attach must preserve its authoritative endpoints")
|
||||||
|
expect(localOnly == true, "deferred attach must remain client-local")
|
||||||
|
self.towing = other
|
||||||
|
other.towedBy = self
|
||||||
|
end
|
||||||
|
function value:breakConstraint()
|
||||||
|
self.breakCalls = self.breakCalls + 1
|
||||||
|
local other = self.towing or self.towedBy
|
||||||
|
if other then unlink(self, other) end
|
||||||
|
end
|
||||||
|
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local vehicles = {
|
||||||
|
[101] = vehicle(101),
|
||||||
|
[102] = vehicle(102),
|
||||||
|
[201] = vehicle(201),
|
||||||
|
[202] = vehicle(202)
|
||||||
|
}
|
||||||
|
|
||||||
|
getVehicleById = function(id) return vehicles[tonumber(id)] end
|
||||||
|
getPlayer = function() return localPlayer end
|
||||||
|
|
||||||
|
TowBarMod = {
|
||||||
|
Utils = { updateAttachmentsForRigidTow = function() end },
|
||||||
|
Hook = {
|
||||||
|
setVehicleScriptWithTowBarHidden = function(target, scriptName)
|
||||||
|
target:setScriptName(scriptName)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
setVehiclePostAttach = function(_, target)
|
||||||
|
-- This stands in for applyFreeRollingTowState(). The ownership
|
||||||
|
-- handoff must run it before Bullet receives the local constraint,
|
||||||
|
-- matching the working wrecker order.
|
||||||
|
target.freeRolling = true
|
||||||
|
end,
|
||||||
|
cleanupDetachedTowBar = function() end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dofile("42.20/media/lua/client/TowBar/TowSyncClient.lua")
|
||||||
|
|
||||||
|
local function serverCommand(command, vehicleA, vehicleB)
|
||||||
|
local args = {
|
||||||
|
vehicleA = vehicleA:getId(),
|
||||||
|
vehicleB = vehicleB:getId(),
|
||||||
|
attachmentA = "trailer",
|
||||||
|
attachmentB = "trailerfront"
|
||||||
|
}
|
||||||
|
for i = 1, #serverCommandCallbacks do
|
||||||
|
serverCommandCallbacks[i]("towbar", command, args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function enterVehicle(player)
|
||||||
|
for i = 1, #enterVehicleCallbacks do
|
||||||
|
enterVehicleCallbacks[i](player)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function spawnVehicle(value)
|
||||||
|
for i = 1, #spawnVehicleCallbacks do
|
||||||
|
spawnVehicleCallbacks[i](value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Portable installation finishes while the player is standing beside the two
|
||||||
|
-- cars. At this point the local client does not own either vehicle's physics.
|
||||||
|
local towing, towed = vehicles[101], vehicles[102]
|
||||||
|
-- The server's ordinary towing relation may reach this client before the mod
|
||||||
|
-- command. It is usable as a visual snap, but replacing it before driver
|
||||||
|
-- ownership is precisely the multiplayer race this regression covers.
|
||||||
|
towing.towing = towed
|
||||||
|
towed.towedBy = towing
|
||||||
|
serverCommand("forceAttachSync", towing, towed)
|
||||||
|
|
||||||
|
expect(type(TowBarMod.Sync.desiredPairs) == "table",
|
||||||
|
"authoritative sync must retain desired pairs until this client owns the towing vehicle")
|
||||||
|
expect(towing.addCalls == 0 and towing.breakCalls == 0,
|
||||||
|
"sync received outside the driver seat must not create or replace local physics")
|
||||||
|
|
||||||
|
-- Entering as driver transfers physics ownership to this client. That event is
|
||||||
|
-- the first safe point to replace the server/native relation with rigid towing.
|
||||||
|
towing.driver = localPlayer
|
||||||
|
expect(#enterVehicleCallbacks == 1,
|
||||||
|
"portable sync must register one driver-entry ownership handoff")
|
||||||
|
enterVehicle(localPlayer)
|
||||||
|
|
||||||
|
expect(towing.addCalls == 1,
|
||||||
|
"becoming driver must apply the deferred rigid constraint exactly once")
|
||||||
|
expect(towing.breakCalls == 1,
|
||||||
|
"driver ownership handoff must replace the pre-existing native relation exactly once")
|
||||||
|
expect(towing:getVehicleTowing() == towed and towed:getVehicleTowedBy() == towing,
|
||||||
|
"driver ownership handoff must leave the authoritative pair linked")
|
||||||
|
expect(towing.freeRollingAtAdd[1] == true,
|
||||||
|
"towed free-roll state must be applied before the rigid constraint is created")
|
||||||
|
|
||||||
|
-- The user explicitly chose driver entry as a forced repair point. A later
|
||||||
|
-- entry rebuilds once, while the ordinary server snapshot remains idempotent.
|
||||||
|
enterVehicle(localPlayer)
|
||||||
|
serverCommand("forceAttachSync", towing, towed)
|
||||||
|
expect(towing.addCalls == 2 and towing.breakCalls == 2,
|
||||||
|
"each driver entry must force one rebuild while snapshots do not add another")
|
||||||
|
|
||||||
|
-- A detach can race the later driver entry. It must cancel the desired pair so
|
||||||
|
-- that entering the vehicle cannot resurrect a server-rejected tow.
|
||||||
|
local canceledTowing, canceledTowed = vehicles[201], vehicles[202]
|
||||||
|
serverCommand("forceAttachSync", canceledTowing, canceledTowed)
|
||||||
|
expect(canceledTowing.addCalls == 0 and canceledTowing.breakCalls == 0,
|
||||||
|
"a second outside-seat sync must also remain pending without touching physics")
|
||||||
|
-- The dedicated server may already have replicated its native relation. A
|
||||||
|
-- non-owning client must not break that relation while canceling its handoff.
|
||||||
|
canceledTowing.towing = canceledTowed
|
||||||
|
canceledTowed.towedBy = canceledTowing
|
||||||
|
serverCommand("forceDetachSync", canceledTowing, canceledTowed)
|
||||||
|
expect(TowBarMod.Sync.desiredPairs["201:202"] == nil,
|
||||||
|
"detach must remove the pending desired pair before any later seat event")
|
||||||
|
canceledTowing.driver = localPlayer
|
||||||
|
enterVehicle(localPlayer)
|
||||||
|
expect(canceledTowing.addCalls == 0 and canceledTowing.breakCalls == 0,
|
||||||
|
"detach must cancel pending ownership handoff without a non-owner break")
|
||||||
|
|
||||||
|
-- A restart snapshot can arrive before either vehicle has streamed in. Keep
|
||||||
|
-- that desired state and retry it from vehicle spawn when this player already
|
||||||
|
-- owns the towing vehicle.
|
||||||
|
local streamedTowing, streamedTowed = vehicle(301), vehicle(302)
|
||||||
|
serverCommand("forceAttachSync", streamedTowing, streamedTowed)
|
||||||
|
expect(TowBarMod.Sync.desiredPairs["301:302"] ~= nil,
|
||||||
|
"missing streamed peers must not discard the authoritative attach snapshot")
|
||||||
|
streamedTowing.driver = localPlayer
|
||||||
|
vehicles[301], vehicles[302] = streamedTowing, streamedTowed
|
||||||
|
spawnVehicle(streamedTowed)
|
||||||
|
expect(streamedTowing.addCalls == 1,
|
||||||
|
"vehicle streaming must apply one saved rigid handoff for an existing local driver")
|
||||||
|
|
||||||
|
if failures > 0 then os.exit(1) end
|
||||||
|
print("PASS: portable multiplayer rigid tow waits for driver physics ownership")
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
local failures = 0
|
||||||
|
|
||||||
|
local function expect(condition, message)
|
||||||
|
if not condition then
|
||||||
|
failures = failures + 1
|
||||||
|
io.stderr:write("FAIL: " .. message .. "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
package.path = table.concat({
|
||||||
|
"42.20/media/lua/client/?.lua",
|
||||||
|
"42.20/media/lua/shared/?.lua",
|
||||||
|
package.path
|
||||||
|
}, ";")
|
||||||
|
|
||||||
|
local runtime = "client"
|
||||||
|
isServer = function() return runtime == "server" end
|
||||||
|
isClient = function() return runtime == "client" end
|
||||||
|
getDebug = function() return false end
|
||||||
|
getTimestampMs = function() return 1000 end
|
||||||
|
|
||||||
|
local serverCommandCallbacks = {}
|
||||||
|
local clientPlayer = { onlineId = 17 }
|
||||||
|
Events = {
|
||||||
|
OnServerCommand = {
|
||||||
|
Add = function(callback)
|
||||||
|
serverCommandCallbacks[#serverCommandCallbacks + 1] = callback
|
||||||
|
end
|
||||||
|
},
|
||||||
|
OnSpawnVehicleEnd = { Add = function() end }
|
||||||
|
}
|
||||||
|
|
||||||
|
local function attachmentScript()
|
||||||
|
local attachments = { trailer = {}, trailerfront = {} }
|
||||||
|
return {
|
||||||
|
getAttachmentById = function(_, attachmentId)
|
||||||
|
return attachments[attachmentId]
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vehicle(id, sqlId, acknowledgeConstraint)
|
||||||
|
local value = {
|
||||||
|
id = id,
|
||||||
|
sqlId = sqlId,
|
||||||
|
script = attachmentScript(),
|
||||||
|
scriptName = "Base.MultiplayerRegression" .. tostring(id),
|
||||||
|
modData = {},
|
||||||
|
towing = nil,
|
||||||
|
towedBy = nil,
|
||||||
|
constraintKind = nil,
|
||||||
|
acknowledgeConstraint = acknowledgeConstraint,
|
||||||
|
constraintCalls = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function value:getId() return self.id end
|
||||||
|
function value:getSqlId() return self.sqlId end
|
||||||
|
function value:getScript() return self.script end
|
||||||
|
function value:getScriptName() return self.scriptName end
|
||||||
|
function value:setScriptName(scriptName) self.scriptName = scriptName end
|
||||||
|
function value:getModData() return self.modData end
|
||||||
|
function value:transmitModData() end
|
||||||
|
function value:getMass() return 1000 end
|
||||||
|
function value:getBrakingForce() return 20 end
|
||||||
|
function value:getVehicleTowing() return self.towing end
|
||||||
|
function value:getVehicleTowedBy() return self.towedBy end
|
||||||
|
function value:isDriver(player) return self.driver == player end
|
||||||
|
function value:getTowAttachmentSelf() return nil end
|
||||||
|
function value:attachmentExist(attachmentId)
|
||||||
|
return self.script:getAttachmentById(attachmentId) ~= nil
|
||||||
|
end
|
||||||
|
function value:getX() return 0 end
|
||||||
|
function value:getY() return 0 end
|
||||||
|
function value:addPointConstraint(_, other)
|
||||||
|
self.constraintCalls = self.constraintCalls + 1
|
||||||
|
if self.acknowledgeConstraint then
|
||||||
|
self.towing = other
|
||||||
|
other.towedBy = self
|
||||||
|
self.constraintKind = runtime == "client" and "rigid" or "native"
|
||||||
|
other.constraintKind = self.constraintKind
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function value:breakConstraint()
|
||||||
|
local other = self.towing or self.towedBy
|
||||||
|
self.towing, self.towedBy = nil, nil
|
||||||
|
self.constraintKind = nil
|
||||||
|
if other then
|
||||||
|
other.towing, other.towedBy = nil, nil
|
||||||
|
other.constraintKind = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local clientVehicles = {
|
||||||
|
[101] = vehicle(101, 1001, true),
|
||||||
|
[102] = vehicle(102, 1002, true),
|
||||||
|
[201] = vehicle(201, 2001, false),
|
||||||
|
[202] = vehicle(202, 2002, false)
|
||||||
|
}
|
||||||
|
clientVehicles[101].driver = clientPlayer
|
||||||
|
clientVehicles[201].driver = clientPlayer
|
||||||
|
local serverVehicles = {
|
||||||
|
-- This models the dedicated-server behavior behind the live regression:
|
||||||
|
-- addPointConstraint is accepted, but the server getters do not expose the
|
||||||
|
-- relation before the authoritative client sync must be sent.
|
||||||
|
[101] = vehicle(101, 1001, false),
|
||||||
|
[102] = vehicle(102, 1002, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
getVehicleById = function(id)
|
||||||
|
local vehicles = runtime == "server" and serverVehicles or clientVehicles
|
||||||
|
return vehicles[tonumber(id)]
|
||||||
|
end
|
||||||
|
getPlayer = function() return clientPlayer end
|
||||||
|
|
||||||
|
TowBarMod = {
|
||||||
|
Utils = {
|
||||||
|
updateAttachmentsForRigidTow = function() end
|
||||||
|
},
|
||||||
|
Hook = {
|
||||||
|
applyFreeRollingTowState = function() end,
|
||||||
|
setVehicleScriptWithTowBarHidden = function(target, scriptName)
|
||||||
|
target:setScriptName(scriptName)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
setVehiclePostAttach = function() end,
|
||||||
|
cleanupDetachedTowBar = function() end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Load the real portable client sync and rigid-constraint implementation.
|
||||||
|
dofile("42.20/media/lua/client/TowBar/TowSyncClient.lua")
|
||||||
|
|
||||||
|
local globalData = {}
|
||||||
|
ModData = {
|
||||||
|
getOrCreate = function(name)
|
||||||
|
globalData[name] = globalData[name] or {}
|
||||||
|
return globalData[name]
|
||||||
|
end,
|
||||||
|
transmit = function() end
|
||||||
|
}
|
||||||
|
|
||||||
|
local serverCallbacks = {}
|
||||||
|
Events = {
|
||||||
|
OnClientCommand = { Add = function(callback) serverCallbacks.clientCommand = callback end },
|
||||||
|
OnTick = { Add = function(callback) serverCallbacks.tick = callback end }
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime = "server"
|
||||||
|
|
||||||
|
local item = { id = 7001 }
|
||||||
|
function item:getID() return self.id end
|
||||||
|
function item:getFullType() return "TowBar.TowBar" end
|
||||||
|
|
||||||
|
local inventory = { items = { item } }
|
||||||
|
function inventory:getItemWithID(id)
|
||||||
|
return self.items[1] and self.items[1]:getID() == id and self.items[1] or nil
|
||||||
|
end
|
||||||
|
function inventory:getFirstTypeRecurse(fullType)
|
||||||
|
return self.items[1] and self.items[1]:getFullType() == fullType and self.items[1] or nil
|
||||||
|
end
|
||||||
|
function inventory:contains(candidate) return self.items[1] == candidate end
|
||||||
|
function inventory:Remove(candidate)
|
||||||
|
if self.items[1] == candidate then self.items[1] = nil end
|
||||||
|
end
|
||||||
|
function inventory:AddItem() return nil end
|
||||||
|
|
||||||
|
local player = { vehicle = serverVehicles[101] }
|
||||||
|
function player:getInventory() return inventory end
|
||||||
|
function player:getVehicle() return self.vehicle end
|
||||||
|
function player:getX() return 0 end
|
||||||
|
function player:getY() return 0 end
|
||||||
|
function player:isPrimaryHandItem() return false end
|
||||||
|
function player:isSecondaryHandItem() return false end
|
||||||
|
function player:removeFromHands() end
|
||||||
|
function player:setPrimaryHandItem() end
|
||||||
|
|
||||||
|
sendRemoveItemFromContainer = function() end
|
||||||
|
sendAddItemToContainer = function() end
|
||||||
|
sendEquip = function() end
|
||||||
|
|
||||||
|
local attachSyncCount = 0
|
||||||
|
local attachSyncPhases = {}
|
||||||
|
sendServerCommand = function(module, command, args)
|
||||||
|
if module == "towbar" and command == "forceAttachSync" then
|
||||||
|
attachSyncCount = attachSyncCount + 1
|
||||||
|
attachSyncPhases[#attachSyncPhases + 1] = args.phase
|
||||||
|
end
|
||||||
|
|
||||||
|
local previousRuntime = runtime
|
||||||
|
runtime = "client"
|
||||||
|
for i = 1, #serverCommandCallbacks do
|
||||||
|
serverCommandCallbacks[i](module, command, args)
|
||||||
|
end
|
||||||
|
runtime = previousRuntime
|
||||||
|
end
|
||||||
|
|
||||||
|
getCell = function()
|
||||||
|
return { getVehicles = function() return nil end }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Load the real server command handler after the client event handler has been
|
||||||
|
-- captured. Both halves now communicate through sendServerCommand above.
|
||||||
|
dofile("42.20/media/lua/server/TowingCommands.lua")
|
||||||
|
|
||||||
|
serverCallbacks.clientCommand("towbar", "attachTowBar", player, {
|
||||||
|
vehicleA = 101,
|
||||||
|
vehicleB = 102,
|
||||||
|
attachmentA = "trailer",
|
||||||
|
attachmentB = "trailerfront",
|
||||||
|
itemId = item:getID()
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(serverVehicles[101].modData.isTowingByTowBar == true,
|
||||||
|
"accepted MP attach must publish towbar state for its visual")
|
||||||
|
expect(serverVehicles[101].constraintCalls == 1,
|
||||||
|
"accepted MP attach must still request the server-side logical constraint")
|
||||||
|
expect(attachSyncCount == 1,
|
||||||
|
"accepted MP attach must immediately send one forceAttachSync even when server towing getters lag")
|
||||||
|
expect(attachSyncPhases[1] == nil,
|
||||||
|
"portable MP attach must use the same single unphased sync as the working wrecker")
|
||||||
|
expect(clientVehicles[101]:getVehicleTowing() == clientVehicles[102]
|
||||||
|
and clientVehicles[102]:getVehicleTowedBy() == clientVehicles[101],
|
||||||
|
"the same command that shows the MP towbar visual must create the client rigid constraint")
|
||||||
|
expect(clientVehicles[101].constraintKind == "rigid",
|
||||||
|
"the one-shot sync must create rigid rather than native towing physics")
|
||||||
|
|
||||||
|
-- Like the working wrecker, server acknowledgement is bookkeeping only. It
|
||||||
|
-- must not introduce a second portable-only attach protocol or rebuild.
|
||||||
|
serverVehicles[101].towing = serverVehicles[102]
|
||||||
|
serverVehicles[102].towedBy = serverVehicles[101]
|
||||||
|
serverCallbacks.tick()
|
||||||
|
serverCallbacks.tick()
|
||||||
|
|
||||||
|
expect(attachSyncCount == 1,
|
||||||
|
"server acknowledgement must not send a second portable-only attach phase")
|
||||||
|
expect(clientVehicles[101].constraintCalls == 1,
|
||||||
|
"the wrecker-style attach must submit one local rigid constraint")
|
||||||
|
expect(clientVehicles[101].constraintKind == "rigid",
|
||||||
|
"the single attach sync must leave the client using rigid towing physics")
|
||||||
|
|
||||||
|
sendServerCommand("towbar", "forceAttachSync", {
|
||||||
|
vehicleA = 101,
|
||||||
|
vehicleB = 102,
|
||||||
|
attachmentA = "trailer",
|
||||||
|
attachmentB = "trailerfront"
|
||||||
|
})
|
||||||
|
expect(clientVehicles[101].constraintCalls == 1,
|
||||||
|
"duplicate snapshots must not repeatedly rebuild rigid physics")
|
||||||
|
|
||||||
|
-- The local add itself can also precede the engine's towing-getter update.
|
||||||
|
-- Reconciliation must adopt the now-visible exact pair without duplicating
|
||||||
|
-- the working wrecker-style single attach.
|
||||||
|
runtime = "client"
|
||||||
|
local delayedProvisional = {
|
||||||
|
vehicleA = 201,
|
||||||
|
vehicleB = 202,
|
||||||
|
attachmentA = "trailer",
|
||||||
|
attachmentB = "trailerfront"
|
||||||
|
}
|
||||||
|
for i = 1, #serverCommandCallbacks do
|
||||||
|
serverCommandCallbacks[i]("towbar", "forceAttachSync", delayedProvisional)
|
||||||
|
end
|
||||||
|
expect(clientVehicles[201].constraintCalls == 1
|
||||||
|
and clientVehicles[201]:getVehicleTowing() == nil,
|
||||||
|
"a rigid add may be accepted before its local getters update")
|
||||||
|
|
||||||
|
clientVehicles[201].towing = clientVehicles[202]
|
||||||
|
clientVehicles[202].towedBy = clientVehicles[201]
|
||||||
|
clientVehicles[201].constraintKind = "rigid"
|
||||||
|
clientVehicles[202].constraintKind = "rigid"
|
||||||
|
for i = 1, #serverCommandCallbacks do
|
||||||
|
serverCommandCallbacks[i]("towbar", "forceAttachSync", delayedProvisional)
|
||||||
|
end
|
||||||
|
expect(clientVehicles[201].constraintCalls == 1,
|
||||||
|
"delayed local getter acknowledgement must make the next snapshot idempotent")
|
||||||
|
|
||||||
|
clientVehicles[201].acknowledgeConstraint = true
|
||||||
|
for i = 1, #serverCommandCallbacks do
|
||||||
|
serverCommandCallbacks[i]("towbar", "forceAttachSync", delayedProvisional)
|
||||||
|
end
|
||||||
|
expect(clientVehicles[201].constraintCalls == 1
|
||||||
|
and clientVehicles[201].constraintKind == "rigid",
|
||||||
|
"duplicate sync after delayed local acceptance must remain idempotent")
|
||||||
|
runtime = "server"
|
||||||
|
|
||||||
|
if failures > 0 then os.exit(1) end
|
||||||
|
print("PASS: multiplayer portable towbar immediately synchronizes its rigid client constraint")
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
local failures = 0
|
||||||
|
|
||||||
|
local function expect(condition, message)
|
||||||
|
if not condition then
|
||||||
|
failures = failures + 1
|
||||||
|
io.stderr:write("FAIL: " .. message .. "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
isServer = function() return false end
|
||||||
|
|
||||||
|
local metrics = {
|
||||||
|
offsets = 0,
|
||||||
|
breaks = 0,
|
||||||
|
adds = 0,
|
||||||
|
hides = 0,
|
||||||
|
postAttach = 0,
|
||||||
|
scriptSwaps = 0,
|
||||||
|
freeRolling = 0
|
||||||
|
}
|
||||||
|
local trace = {}
|
||||||
|
local function record(event) trace[#trace + 1] = event end
|
||||||
|
|
||||||
|
local function script()
|
||||||
|
return {
|
||||||
|
getAttachmentById = function(_, id)
|
||||||
|
if id == "trailer" or id == "trailerfront" then return {} end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local acknowledgeAdd = true
|
||||||
|
local function unlink(a, b)
|
||||||
|
a.towing, a.towedBy = nil, nil
|
||||||
|
b.towing, b.towedBy = nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function link(a, b)
|
||||||
|
a.towing, a.towedBy = b, nil
|
||||||
|
b.towing, b.towedBy = nil, a
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vehicle(id)
|
||||||
|
local value = {
|
||||||
|
id = id,
|
||||||
|
towing = nil,
|
||||||
|
towedBy = nil,
|
||||||
|
script = script(),
|
||||||
|
scriptName = "Base.TestVehicle",
|
||||||
|
modData = {}
|
||||||
|
}
|
||||||
|
function value:getId() return self.id end
|
||||||
|
function value:getScript() return self.script end
|
||||||
|
function value:getScriptName() return self.scriptName end
|
||||||
|
function value:getModData() return self.modData end
|
||||||
|
function value:getVehicleTowing() return self.towing end
|
||||||
|
function value:getVehicleTowedBy() return self.towedBy end
|
||||||
|
function value:breakConstraint()
|
||||||
|
metrics.breaks = metrics.breaks + 1
|
||||||
|
record("break")
|
||||||
|
local other = self.towing or self.towedBy
|
||||||
|
if other then unlink(self, other) end
|
||||||
|
end
|
||||||
|
function value:addPointConstraint(player, other, attachmentA, attachmentB, localOnly)
|
||||||
|
metrics.adds = metrics.adds + 1
|
||||||
|
record("add")
|
||||||
|
expect(player == nil, "rigid primitive must not impersonate a player")
|
||||||
|
expect(attachmentA == "trailer" and attachmentB == "trailerfront",
|
||||||
|
"rigid primitive must preserve selected attachment IDs")
|
||||||
|
expect(localOnly == true, "rigid primitive must create the client-local rigid constraint")
|
||||||
|
if acknowledgeAdd then link(self, other) end
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
TowBarMod = {
|
||||||
|
Utils = {
|
||||||
|
updateAttachmentsForRigidTow = function(a, b, attachmentA, attachmentB)
|
||||||
|
metrics.offsets = metrics.offsets + 1
|
||||||
|
expect(a ~= nil and b ~= nil, "rigid offset update must receive both vehicles")
|
||||||
|
expect(attachmentA == "trailer" and attachmentB == "trailerfront",
|
||||||
|
"rigid offset update must receive the selected endpoints")
|
||||||
|
end
|
||||||
|
},
|
||||||
|
Hook = {
|
||||||
|
applyFreeRollingTowState = function(vehicle)
|
||||||
|
metrics.freeRolling = metrics.freeRolling + 1
|
||||||
|
record("free")
|
||||||
|
expect(vehicle ~= nil, "rigid primitive must receive the towed vehicle for free-roll setup")
|
||||||
|
end,
|
||||||
|
setVehicleScriptWithTowBarHidden = function(vehicle, scriptName)
|
||||||
|
metrics.hides = metrics.hides + 1
|
||||||
|
metrics.scriptSwaps = metrics.scriptSwaps + 1
|
||||||
|
vehicle.scriptName = scriptName
|
||||||
|
record("script:" .. tostring(scriptName))
|
||||||
|
expect(vehicle ~= nil,
|
||||||
|
"rigid primitive must receive a vehicle for each hidden script swap")
|
||||||
|
end,
|
||||||
|
setVehiclePostAttach = function(player, vehicle, towingVehicle)
|
||||||
|
metrics.postAttach = metrics.postAttach + 1
|
||||||
|
record("post")
|
||||||
|
expect(player == nil and vehicle ~= nil and towingVehicle ~= nil,
|
||||||
|
"rigid primitive must restore post-attach state after acknowledgement")
|
||||||
|
expect(vehicle:getScriptName() == "Base.TestVehicle",
|
||||||
|
"rigid primitive must restore the real script before post-attach finalization")
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dofile("42.20/media/lua/client/TowBar/RigidTow.lua")
|
||||||
|
|
||||||
|
expect(TowBarMod.RigidTow ~= nil and type(TowBarMod.RigidTow.attach) == "function",
|
||||||
|
"production rigid module must expose attach")
|
||||||
|
|
||||||
|
-- First attach with no physical relation uses the exact rigid path once.
|
||||||
|
local a, b = vehicle(101), vehicle(102)
|
||||||
|
trace = {}
|
||||||
|
local attached = TowBarMod.RigidTow.attach(a, b, "trailer", "trailerfront")
|
||||||
|
expect(attached == true, "acknowledged initial rigid add must succeed")
|
||||||
|
expect(a:getVehicleTowing() == b and b:getVehicleTowedBy() == a,
|
||||||
|
"acknowledged rigid add must leave an exact reciprocal relation")
|
||||||
|
expect(metrics.offsets == 1 and metrics.adds == 1 and metrics.breaks == 0,
|
||||||
|
"initial rigid add must update offsets and add once without a needless break")
|
||||||
|
expect(metrics.scriptSwaps == 2 and metrics.postAttach == 1,
|
||||||
|
"successful rigid add must select the fake script and restore the real script exactly once")
|
||||||
|
expect(table.concat(trace, ",") == "free,script:notTowingA_Trailer,add,script:Base.TestVehicle,post",
|
||||||
|
"initial rigid sequence must be free-roll, fake script, local add, real script, then post-attach")
|
||||||
|
|
||||||
|
-- A restored native relation is replaced inside this primitive. The lifecycle
|
||||||
|
-- controller must not grow its own second copy of this physics sequence.
|
||||||
|
local restoredA, restoredB = vehicle(201), vehicle(202)
|
||||||
|
link(restoredA, restoredB)
|
||||||
|
local beforeOffsets, beforeAdds = metrics.offsets, metrics.adds
|
||||||
|
local beforeBreaks, beforePost = metrics.breaks, metrics.postAttach
|
||||||
|
trace = {}
|
||||||
|
attached = TowBarMod.RigidTow.attach(restoredA, restoredB, "trailer", "trailerfront")
|
||||||
|
expect(attached == true, "restored native relation must be replaced by rigid mechanics")
|
||||||
|
expect(metrics.offsets == beforeOffsets + 1 and metrics.adds == beforeAdds + 1,
|
||||||
|
"native replacement must update offsets and add one rigid constraint")
|
||||||
|
expect(metrics.breaks == beforeBreaks + 1,
|
||||||
|
"native replacement must break the exact old pair once")
|
||||||
|
expect(metrics.postAttach == beforePost + 1,
|
||||||
|
"native replacement must run post-attach only after the new link exists")
|
||||||
|
expect(table.concat(trace, ",") == "break,free,script:notTowingA_Trailer,add,script:Base.TestVehicle,post",
|
||||||
|
"native replacement sequence must break, free-roll, use the fake script, add, restore the real script, then finalize")
|
||||||
|
|
||||||
|
-- A link to a different vehicle is never collateral damage.
|
||||||
|
local conflictA, expectedB, unrelated = vehicle(301), vehicle(302), vehicle(399)
|
||||||
|
link(conflictA, unrelated)
|
||||||
|
local beforeConflictBreaks, beforeConflictAdds = metrics.breaks, metrics.adds
|
||||||
|
attached = TowBarMod.RigidTow.attach(conflictA, expectedB, "trailer", "trailerfront")
|
||||||
|
expect(attached == false, "conflicting physical link must reject rigid attachment")
|
||||||
|
expect(metrics.breaks == beforeConflictBreaks and metrics.adds == beforeConflictAdds,
|
||||||
|
"conflicting physical link must not break or layer another constraint")
|
||||||
|
|
||||||
|
-- Invalid endpoints are rejected before modifying a valid existing pair.
|
||||||
|
local missingA, missingB = vehicle(351), vehicle(352)
|
||||||
|
link(missingA, missingB)
|
||||||
|
local beforeMissingBreaks, beforeMissingAdds = metrics.breaks, metrics.adds
|
||||||
|
attached = TowBarMod.RigidTow.attach(missingA, missingB, "missing", "trailerfront")
|
||||||
|
expect(attached == false, "missing attachment must reject rigid attachment")
|
||||||
|
expect(metrics.breaks == beforeMissingBreaks and metrics.adds == beforeMissingAdds,
|
||||||
|
"missing attachment must not break the existing pair or attempt an add")
|
||||||
|
|
||||||
|
-- In multiplayer Java/Bullet may accept the local constraint before the Lua
|
||||||
|
-- reciprocal getters update. Submission is success; waiting for those getters
|
||||||
|
-- here strands an already-streamed pair with only its visual state applied.
|
||||||
|
local delayedA, delayedB = vehicle(401), vehicle(402)
|
||||||
|
acknowledgeAdd = false
|
||||||
|
local beforeDelayedPost = metrics.postAttach
|
||||||
|
attached = TowBarMod.RigidTow.attach(delayedA, delayedB, "trailer", "trailerfront")
|
||||||
|
expect(attached == true, "submitted rigid add must tolerate delayed multiplayer getters")
|
||||||
|
expect(metrics.postAttach == beforeDelayedPost + 1,
|
||||||
|
"delayed multiplayer getters must not block post-attach restoration")
|
||||||
|
|
||||||
|
if failures > 0 then os.exit(1) end
|
||||||
|
print("PASS: single rigid-tow contract preserves exact-pair mechanics and delayed MP submission")
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
local failures = 0
|
||||||
|
|
||||||
|
local function expect(condition, message)
|
||||||
|
if not condition then
|
||||||
|
failures = failures + 1
|
||||||
|
io.stderr:write("FAIL: " .. message .. "\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
package.path = "42.20/media/lua/shared/?.lua;" .. package.path
|
||||||
|
|
||||||
|
local globalData = {}
|
||||||
|
ModData = {
|
||||||
|
getOrCreate = function(name)
|
||||||
|
globalData[name] = globalData[name] or {}
|
||||||
|
return globalData[name]
|
||||||
|
end,
|
||||||
|
transmit = function() end
|
||||||
|
}
|
||||||
|
|
||||||
|
TowBarMod = {}
|
||||||
|
require("TowBar/Persistence")
|
||||||
|
|
||||||
|
local callbacks = {}
|
||||||
|
Events = {
|
||||||
|
OnClientCommand = { Add = function(callback) callbacks.clientCommand = callback end },
|
||||||
|
OnTick = { Add = function(callback) callbacks.tick = callback end }
|
||||||
|
}
|
||||||
|
|
||||||
|
isClient = function() return false end
|
||||||
|
isServer = function() return true end
|
||||||
|
getDebug = function() return false end
|
||||||
|
getTimestampMs = function() return 1000 end
|
||||||
|
|
||||||
|
local metrics = {
|
||||||
|
removals = 0,
|
||||||
|
refunds = 0,
|
||||||
|
attachSyncs = 0,
|
||||||
|
detachSyncs = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
local nextItemId = 700
|
||||||
|
local function towbarItem()
|
||||||
|
nextItemId = nextItemId + 1
|
||||||
|
local item = { id = nextItemId }
|
||||||
|
function item:getID() return self.id end
|
||||||
|
function item:getFullType() return "TowBar.TowBar" end
|
||||||
|
return item
|
||||||
|
end
|
||||||
|
|
||||||
|
local inventory = { items = { towbarItem() } }
|
||||||
|
function inventory:getItemWithID(id)
|
||||||
|
for i = 1, #self.items do
|
||||||
|
if self.items[i]:getID() == id then return self.items[i] end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function inventory:getFirstTypeRecurse(fullType)
|
||||||
|
for i = 1, #self.items do
|
||||||
|
if self.items[i]:getFullType() == fullType then return self.items[i] end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function inventory:contains(item)
|
||||||
|
for i = 1, #self.items do
|
||||||
|
if self.items[i] == item then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
function inventory:Remove(item)
|
||||||
|
for i = #self.items, 1, -1 do
|
||||||
|
if self.items[i] == item then table.remove(self.items, i) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function inventory:AddItem(fullType)
|
||||||
|
expect(fullType == "TowBar.TowBar", "refund must create the correct item type")
|
||||||
|
local item = towbarItem()
|
||||||
|
table.insert(self.items, item)
|
||||||
|
return item
|
||||||
|
end
|
||||||
|
|
||||||
|
local player = { primary = nil }
|
||||||
|
function player:getInventory() return inventory end
|
||||||
|
function player:getVehicle() return self.vehicle end
|
||||||
|
function player:getX() return 0 end
|
||||||
|
function player:getY() return 0 end
|
||||||
|
function player:isPrimaryHandItem(item) return self.primary == item end
|
||||||
|
function player:isSecondaryHandItem() return false end
|
||||||
|
function player:removeFromHands(item)
|
||||||
|
if self.primary == item then self.primary = nil end
|
||||||
|
end
|
||||||
|
function player:setPrimaryHandItem(item) self.primary = item end
|
||||||
|
|
||||||
|
sendRemoveItemFromContainer = function()
|
||||||
|
metrics.removals = metrics.removals + 1
|
||||||
|
end
|
||||||
|
sendAddItemToContainer = function()
|
||||||
|
metrics.refunds = metrics.refunds + 1
|
||||||
|
end
|
||||||
|
sendEquip = function() end
|
||||||
|
|
||||||
|
local function vehicle(id, sqlId, acknowledgeAttach)
|
||||||
|
local value = {
|
||||||
|
id = id,
|
||||||
|
sqlId = sqlId,
|
||||||
|
modData = {},
|
||||||
|
towing = nil,
|
||||||
|
towedBy = nil,
|
||||||
|
acknowledgeAttach = acknowledgeAttach
|
||||||
|
}
|
||||||
|
function value:getId() return self.id end
|
||||||
|
function value:getSqlId() return self.sqlId end
|
||||||
|
function value:getModData() return self.modData end
|
||||||
|
function value:transmitModData() end
|
||||||
|
function value:getVehicleTowing() return self.towing end
|
||||||
|
function value:getVehicleTowedBy() return self.towedBy end
|
||||||
|
function value:getTowAttachmentSelf() return nil end
|
||||||
|
function value:attachmentExist() return true end
|
||||||
|
function value:getX() return 0 end
|
||||||
|
function value:getY() return 0 end
|
||||||
|
function value:addPointConstraint(_, other)
|
||||||
|
if self.acknowledgeAttach then
|
||||||
|
self.towing = other
|
||||||
|
other.towedBy = self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function value:breakConstraint()
|
||||||
|
local other = self.towing or self.towedBy
|
||||||
|
self.towing, self.towedBy = nil, nil
|
||||||
|
if other then other.towing, other.towedBy = nil, nil end
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local vehicles = {}
|
||||||
|
local loaded = {}
|
||||||
|
getVehicleById = function(id) return vehicles[tonumber(id)] end
|
||||||
|
getCell = function()
|
||||||
|
return {
|
||||||
|
getVehicles = function()
|
||||||
|
return {
|
||||||
|
iterator = function()
|
||||||
|
local index = 0
|
||||||
|
return {
|
||||||
|
hasNext = function() return index < #loaded end,
|
||||||
|
next = function()
|
||||||
|
index = index + 1
|
||||||
|
return loaded[index]
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
sendServerCommand = function(module, command)
|
||||||
|
expect(module == "towbar", "accounting sync must use the towbar module")
|
||||||
|
if command == "forceAttachSync" then
|
||||||
|
metrics.attachSyncs = metrics.attachSyncs + 1
|
||||||
|
elseif command == "forceDetachSync" then
|
||||||
|
metrics.detachSyncs = metrics.detachSyncs + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
dofile("42.20/media/lua/server/TowingCommands.lua")
|
||||||
|
|
||||||
|
local function command(name, args)
|
||||||
|
callbacks.clientCommand("towbar", name, player, args)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ticks(count)
|
||||||
|
for _ = 1, count do callbacks.tick() end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function attachArgs(a, b, item)
|
||||||
|
return {
|
||||||
|
vehicleA = a:getId(),
|
||||||
|
vehicleB = b:getId(),
|
||||||
|
attachmentA = "trailer",
|
||||||
|
attachmentB = "trailerfront",
|
||||||
|
itemId = item and item:getID() or nil
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A successful attach consumes exactly one physical item. Duplicate commands
|
||||||
|
-- and confirmation ticks must never consume another one.
|
||||||
|
local a, b = vehicle(101, 1001, true), vehicle(102, 1002, true)
|
||||||
|
vehicles[101], vehicles[102] = a, b
|
||||||
|
loaded = { a, b }
|
||||||
|
player.vehicle = a
|
||||||
|
local firstItem = inventory.items[1]
|
||||||
|
command("attachTowBar", attachArgs(a, b, firstItem))
|
||||||
|
command("attachTowBar", attachArgs(a, b, firstItem))
|
||||||
|
ticks(8)
|
||||||
|
expect(#inventory.items == 0, "successful attach must consume exactly one towbar")
|
||||||
|
expect(metrics.removals == 1, "duplicate attach must not transmit a second item removal")
|
||||||
|
expect(metrics.attachSyncs == 1,
|
||||||
|
"successful pair must broadcast one immediate wrecker-style attach without consuming twice")
|
||||||
|
|
||||||
|
-- A normal detach refunds exactly that one reserved/installed item. Duplicate
|
||||||
|
-- detaches are stale and cannot duplicate the refund.
|
||||||
|
local detachArgs = { towingVehicle = a:getId(), vehicle = b:getId(), vehicleA = a:getId(), vehicleB = b:getId() }
|
||||||
|
command("detachTowBar", detachArgs)
|
||||||
|
command("detachTowBar", detachArgs)
|
||||||
|
ticks(4)
|
||||||
|
expect(#inventory.items == 1, "successful detach must refund exactly one towbar")
|
||||||
|
expect(metrics.refunds == 1, "duplicate detach must not transmit a second refund")
|
||||||
|
|
||||||
|
-- The refunded object can be used immediately on the same pair. No enter/exit
|
||||||
|
-- event or stale completion marker may prevent the second lifecycle.
|
||||||
|
local secondItem = inventory.items[1]
|
||||||
|
command("attachTowBar", attachArgs(a, b, secondItem))
|
||||||
|
ticks(4)
|
||||||
|
expect(#inventory.items == 0, "direct reattach must consume the one refunded towbar")
|
||||||
|
expect(metrics.removals == 2, "direct reattach must account for one new removal")
|
||||||
|
command("detachTowBar", detachArgs)
|
||||||
|
ticks(4)
|
||||||
|
expect(#inventory.items == 1, "second detach must return one towbar, not lose it")
|
||||||
|
expect(metrics.refunds == 2, "second lifecycle must produce one new refund")
|
||||||
|
|
||||||
|
-- An immediate detach after the one-shot attach must remain detached. There is
|
||||||
|
-- no portable-only queued confirmation allowed to recreate a ghost tow.
|
||||||
|
local fastA, fastB = vehicle(151, 1501, true), vehicle(152, 1502, true)
|
||||||
|
vehicles[151], vehicles[152] = fastA, fastB
|
||||||
|
loaded = { fastA, fastB }
|
||||||
|
player.vehicle = fastA
|
||||||
|
local fastItem = inventory.items[1]
|
||||||
|
local syncsBeforeFastDetach = metrics.attachSyncs
|
||||||
|
local refundsBeforeFastDetach = metrics.refunds
|
||||||
|
command("attachTowBar", attachArgs(fastA, fastB, fastItem))
|
||||||
|
command("detachTowBar", {
|
||||||
|
towingVehicle = fastA:getId(), vehicle = fastB:getId(),
|
||||||
|
vehicleA = fastA:getId(), vehicleB = fastB:getId()
|
||||||
|
})
|
||||||
|
ticks(8)
|
||||||
|
expect(fastA:getVehicleTowing() == nil and fastB:getVehicleTowedBy() == nil,
|
||||||
|
"detach before confirmation must not be undone by a stale queued attach")
|
||||||
|
expect(metrics.attachSyncs == syncsBeforeFastDetach + 1,
|
||||||
|
"immediate detach must not be followed by another attach sync")
|
||||||
|
expect(metrics.refunds == refundsBeforeFastDetach + 1 and #inventory.items == 1,
|
||||||
|
"detach before confirmation must refund exactly one reserved towbar")
|
||||||
|
|
||||||
|
-- Match the wrecker's server-owned persistence model: once a validated attach
|
||||||
|
-- is accepted, the towbar is installed and the saved pair keeps retrying. A
|
||||||
|
-- delayed native getter must not refund the installed item behind the player.
|
||||||
|
local failedA, failedB = vehicle(201, 2001, false), vehicle(202, 2002, false)
|
||||||
|
vehicles[201], vehicles[202] = failedA, failedB
|
||||||
|
loaded = { failedA, failedB }
|
||||||
|
player.vehicle = failedA
|
||||||
|
local failedItem = inventory.items[1]
|
||||||
|
local refundsBeforeFailure = metrics.refunds
|
||||||
|
command("attachTowBar", attachArgs(failedA, failedB, failedItem))
|
||||||
|
ticks(30)
|
||||||
|
expect(#inventory.items == 0, "accepted delayed attach must keep its towbar installed")
|
||||||
|
expect(metrics.refunds == refundsBeforeFailure,
|
||||||
|
"persistence retries must not mint an inventory refund")
|
||||||
|
expect(failedA.modData.isTowingByTowBar == true
|
||||||
|
and failedB.modData.isTowingByTowBar == true,
|
||||||
|
"accepted delayed attach must preserve its recoverable logical pair")
|
||||||
|
|
||||||
|
command("detachTowBar", {
|
||||||
|
towingVehicle = failedA:getId(), vehicle = failedB:getId(),
|
||||||
|
vehicleA = failedA:getId(), vehicleB = failedB:getId()
|
||||||
|
})
|
||||||
|
expect(#inventory.items == 1 and metrics.refunds == refundsBeforeFailure + 1,
|
||||||
|
"wrecker-style detach must refund an expected pair even while server getters lag")
|
||||||
|
expect(failedA.modData.isTowingByTowBar == false
|
||||||
|
and failedB.modData.isTowingByTowBar == false,
|
||||||
|
"logical detach must clear delayed pair metadata")
|
||||||
|
|
||||||
|
if failures > 0 then os.exit(1) end
|
||||||
|
print("PASS: towbar attach, retry, detach, and direct reattach account for exactly one item")
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
local failures = 0
|
|
||||||
|
|
||||||
local function expect(condition, message)
|
|
||||||
if not condition then
|
|
||||||
failures = failures + 1
|
|
||||||
io.stderr:write("FAIL: " .. message .. "\n")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local serverCommand
|
|
||||||
local vehicleSpawned
|
|
||||||
Events = {
|
|
||||||
OnServerCommand = {
|
|
||||||
Add = function(callback) serverCommand = callback end
|
|
||||||
},
|
|
||||||
OnSpawnVehicleEnd = {
|
|
||||||
Add = function(callback) vehicleSpawned = callback end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isServer = function() return false end
|
|
||||||
|
|
||||||
local trace = {}
|
|
||||||
local function record(value) trace[#trace + 1] = value end
|
|
||||||
|
|
||||||
local function script()
|
|
||||||
return {
|
|
||||||
getAttachmentById = function(_, id)
|
|
||||||
if id == "trailer" or id == "trailerfront" then return {} end
|
|
||||||
return nil
|
|
||||||
end,
|
|
||||||
getWheelCount = function() return 0 end,
|
|
||||||
getPhysicsChassisShape = function()
|
|
||||||
return { z = function() return 4 end }
|
|
||||||
end,
|
|
||||||
addAttachment = function() end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
local rigidAdds = 0
|
|
||||||
local breaks = 0
|
|
||||||
local towing
|
|
||||||
local towed
|
|
||||||
|
|
||||||
local function vehicle(id)
|
|
||||||
local value = {
|
|
||||||
id = id,
|
|
||||||
modData = {},
|
|
||||||
script = script(),
|
|
||||||
towing = nil,
|
|
||||||
towedBy = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
function value:getId() return self.id end
|
|
||||||
function value:getScript() return self.script end
|
|
||||||
function value:getScriptName() return "Base.TestVehicle" end
|
|
||||||
function value:getModData() return self.modData end
|
|
||||||
function value:transmitModData() end
|
|
||||||
function value:getMass() return 1200 end
|
|
||||||
function value:getBrakingForce() return 25 end
|
|
||||||
function value:getVehicleTowing() return self.towing end
|
|
||||||
function value:getVehicleTowedBy() return self.towedBy end
|
|
||||||
function value:addPointConstraint(_, other, attachmentA, attachmentB, localOnly)
|
|
||||||
rigidAdds = rigidAdds + 1
|
|
||||||
record("rigid-add")
|
|
||||||
expect(self == towing and other == towed, "rigid rebuild must use the original pair")
|
|
||||||
expect(attachmentA == "trailer" and attachmentB == "trailerfront", "rigid rebuild must preserve endpoints")
|
|
||||||
expect(localOnly == true, "rigid rebuild must be local-only to avoid an attach/detach command race")
|
|
||||||
self.towing = other
|
|
||||||
other.towedBy = self
|
|
||||||
end
|
|
||||||
function value:breakConstraint()
|
|
||||||
breaks = breaks + 1
|
|
||||||
record("break-native")
|
|
||||||
if self == towing or self == towed then
|
|
||||||
towing.towing = nil
|
|
||||||
towing.towedBy = nil
|
|
||||||
towed.towing = nil
|
|
||||||
towed.towedBy = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return value
|
|
||||||
end
|
|
||||||
|
|
||||||
towing = vehicle(101)
|
|
||||||
towed = vehicle(202)
|
|
||||||
local vehicles = { [101] = towing, [202] = towed }
|
|
||||||
getVehicleById = function(id) return vehicles[id] end
|
|
||||||
|
|
||||||
TowBarMod = {
|
|
||||||
Utils = {
|
|
||||||
updateAttachmentsForRigidTow = function()
|
|
||||||
record("rigid-offsets")
|
|
||||||
end
|
|
||||||
},
|
|
||||||
Hook = {
|
|
||||||
cleanupDetachedTowBar = function()
|
|
||||||
record("cleanup")
|
|
||||||
towing.modData = {}
|
|
||||||
towed.modData = {}
|
|
||||||
end,
|
|
||||||
setVehicleScriptWithTowBarHidden = function(_, scriptName)
|
|
||||||
record("script:" .. tostring(scriptName))
|
|
||||||
return true
|
|
||||||
end,
|
|
||||||
setVehiclePostAttach = function()
|
|
||||||
record("post-attach")
|
|
||||||
end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dofile("42.20/media/lua/client/TowBar/TowSyncClient.lua")
|
|
||||||
expect(type(serverCommand) == "function", "client sync must register its server-command handler")
|
|
||||||
|
|
||||||
-- Begin with the old physical pair so spontaneous cleanup exercises the same
|
|
||||||
-- path as a real Build 42 break notification.
|
|
||||||
towing.towing = towed
|
|
||||||
towed.towedBy = towing
|
|
||||||
towing.modData.towBarTowedVehicleId = towed:getId()
|
|
||||||
towed.modData.towBarTowingVehicleId = towing:getId()
|
|
||||||
serverCommand("towbar", "spontaneousDetachSync", {
|
|
||||||
vehicleA = towing:getId(),
|
|
||||||
vehicleB = towed:getId()
|
|
||||||
})
|
|
||||||
expect(breaks > 0, "spontaneous cleanup must remove the old native relation")
|
|
||||||
expect(towing:getVehicleTowing() == nil and towed:getVehicleTowedBy() == nil, "spontaneous cleanup must leave the pair unlinked")
|
|
||||||
|
|
||||||
-- A direct reattach can arrive from the server as a native B42 relationship
|
|
||||||
-- before forceAttachSync reaches the client. That native relationship is the
|
|
||||||
-- rope-like constraint; forceAttachSync must replace it with the rigid path.
|
|
||||||
towing.towing = towed
|
|
||||||
towed.towedBy = towing
|
|
||||||
local breaksBeforeReattach = breaks
|
|
||||||
serverCommand("towbar", "forceAttachSync", {
|
|
||||||
vehicleA = towing:getId(),
|
|
||||||
vehicleB = towed:getId(),
|
|
||||||
attachmentA = "trailer",
|
|
||||||
attachmentB = "trailerfront"
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(breaks == breaksBeforeReattach + 1, "direct reattach must replace the native B42 rope relation")
|
|
||||||
expect(rigidAdds == 1, "direct reattach must create exactly one rigid local constraint")
|
|
||||||
|
|
||||||
local fakeScriptIndex
|
|
||||||
local rigidAddIndex
|
|
||||||
for index, value in ipairs(trace) do
|
|
||||||
if value == "script:notTowingA_Trailer" and not fakeScriptIndex then fakeScriptIndex = index end
|
|
||||||
if value == "rigid-add" and not rigidAddIndex then rigidAddIndex = index end
|
|
||||||
end
|
|
||||||
expect(fakeScriptIndex ~= nil, "rigid reattach must select the fake-trailer script")
|
|
||||||
expect(rigidAddIndex ~= nil and fakeScriptIndex < rigidAddIndex, "fake-trailer selection must precede rigid constraint creation")
|
|
||||||
expect(towing:getVehicleTowing() == towed and towed:getVehicleTowedBy() == towing, "rigid rebuild must finish with a reciprocal tow relation")
|
|
||||||
|
|
||||||
local rigidAddsAfterFirstSync = rigidAdds
|
|
||||||
local breaksAfterFirstSync = breaks
|
|
||||||
serverCommand("towbar", "forceAttachSync", {
|
|
||||||
vehicleA = towing:getId(),
|
|
||||||
vehicleB = towed:getId(),
|
|
||||||
attachmentA = "trailer",
|
|
||||||
attachmentB = "trailerfront"
|
|
||||||
})
|
|
||||||
expect(rigidAdds == rigidAddsAfterFirstSync, "a repeated attach snapshot must not layer a second rigid constraint")
|
|
||||||
expect(breaks == breaksAfterFirstSync, "a repeated attach snapshot must adopt the already-rigid pair")
|
|
||||||
|
|
||||||
-- Streaming can recreate a native relationship with the same runtime IDs
|
|
||||||
-- while the Lua module and its applied cache survive. The spawn notification
|
|
||||||
-- must invalidate that stale cache so the next snapshot replaces the rope.
|
|
||||||
expect(type(vehicleSpawned) == "function", "client sync must observe vehicle streaming")
|
|
||||||
vehicleSpawned(towed)
|
|
||||||
local rigidAddsBeforeStreamRestore = rigidAdds
|
|
||||||
local breaksBeforeStreamRestore = breaks
|
|
||||||
serverCommand("towbar", "forceAttachSync", {
|
|
||||||
vehicleA = towing:getId(),
|
|
||||||
vehicleB = towed:getId(),
|
|
||||||
attachmentA = "trailer",
|
|
||||||
attachmentB = "trailerfront"
|
|
||||||
})
|
|
||||||
expect(rigidAdds == rigidAddsBeforeStreamRestore + 1, "stream-restored native rope must be rebuilt rigid")
|
|
||||||
expect(breaks == breaksBeforeStreamRestore + 1, "stream-restored native rope must be removed exactly once")
|
|
||||||
|
|
||||||
if failures > 0 then os.exit(1) end
|
|
||||||
print("PASS: spontaneous break followed by direct attach rebuilds a rigid towbar")
|
|
||||||
+47
-37
@@ -29,10 +29,10 @@ Assert-True (Test-Path -LiteralPath $releaseRoot) "Missing 42.20 release folder.
|
|||||||
|
|
||||||
$releaseInfo = Get-Content -LiteralPath (Join-Path $releaseRoot "mod.info")
|
$releaseInfo = Get-Content -LiteralPath (Join-Path $releaseRoot "mod.info")
|
||||||
$rootInfo = Get-Content -LiteralPath (Join-Path $repositoryRoot "mod.info")
|
$rootInfo = Get-Content -LiteralPath (Join-Path $repositoryRoot "mod.info")
|
||||||
Assert-True ($releaseInfo -contains "id=hrsys_towbars_testing") "42.20 must retain the current testing mod id."
|
Assert-True ($releaseInfo -contains "id=hrsys_towbars") "42.20 must retain the stable root mod id."
|
||||||
Assert-True ($releaseInfo -contains "versionMin=42.20.0") "42.20 must require Build 42.20."
|
Assert-True ($releaseInfo -contains "versionMin=42.20.0") "42.20 must require Build 42.20."
|
||||||
Assert-True ($releaseInfo -contains "modversion=1.0.12") "42.20 must declare mod version 1.0.12."
|
Assert-True ($releaseInfo -contains "modversion=1.0.22") "42.20 must declare mod version 1.0.22."
|
||||||
Assert-True ($rootInfo -contains "modversion=1.0.12") "Root and release mod versions must match."
|
Assert-True ($rootInfo -contains "modversion=1.0.22") "Root and release mod versions must match."
|
||||||
|
|
||||||
$baselineFiles = Get-RelativeFileNames $baselineRoot
|
$baselineFiles = Get-RelativeFileNames $baselineRoot
|
||||||
$releaseFiles = Get-RelativeFileNames $releaseRoot
|
$releaseFiles = Get-RelativeFileNames $releaseRoot
|
||||||
@@ -40,8 +40,10 @@ $allowedAdditions = @(
|
|||||||
"media/lua/client/TowBar/WreckerSyncClient.lua",
|
"media/lua/client/TowBar/WreckerSyncClient.lua",
|
||||||
"media/lua/client/TowBar/WreckerTimedAction.lua",
|
"media/lua/client/TowBar/WreckerTimedAction.lua",
|
||||||
"media/lua/client/TowBar/WreckerUI.lua",
|
"media/lua/client/TowBar/WreckerUI.lua",
|
||||||
|
"media/lua/client/TowBar/RigidTow.lua",
|
||||||
"media/lua/server/WreckerCommands.lua",
|
"media/lua/server/WreckerCommands.lua",
|
||||||
"media/lua/shared/TowBar/Persistence.lua",
|
"media/lua/shared/TowBar/Persistence.lua",
|
||||||
|
"media/lua/shared/TowBar/VehicleCompatibility.lua",
|
||||||
"media/lua/shared/TowBar/WreckerUtils.lua"
|
"media/lua/shared/TowBar/WreckerUtils.lua"
|
||||||
)
|
)
|
||||||
$expectedReleaseFiles = @($baselineFiles) + $allowedAdditions | Sort-Object
|
$expectedReleaseFiles = @($baselineFiles) + $allowedAdditions | Sort-Object
|
||||||
@@ -59,6 +61,7 @@ foreach ($jsonFile in Get-ChildItem -LiteralPath $releaseRoot -Recurse -Filter "
|
|||||||
$serverCommandsPath = Join-Path $releaseRoot "media/lua/server/TowingCommands.lua"
|
$serverCommandsPath = Join-Path $releaseRoot "media/lua/server/TowingCommands.lua"
|
||||||
$btTowPath = Join-Path $releaseRoot "media/lua/server/BTTow.lua"
|
$btTowPath = Join-Path $releaseRoot "media/lua/server/BTTow.lua"
|
||||||
$clientSyncPath = Join-Path $releaseRoot "media/lua/client/TowBar/TowSyncClient.lua"
|
$clientSyncPath = Join-Path $releaseRoot "media/lua/client/TowBar/TowSyncClient.lua"
|
||||||
|
$rigidTowPath = Join-Path $releaseRoot "media/lua/client/TowBar/RigidTow.lua"
|
||||||
$hookingPath = Join-Path $releaseRoot "media/lua/client/TowBar/TowingHooking.lua"
|
$hookingPath = Join-Path $releaseRoot "media/lua/client/TowBar/TowingHooking.lua"
|
||||||
$towbarTemplatePath = Join-Path $releaseRoot "media/scripts/vehicles/template_towbar.txt"
|
$towbarTemplatePath = Join-Path $releaseRoot "media/scripts/vehicles/template_towbar.txt"
|
||||||
$itemNamePath = Join-Path $releaseRoot "media/lua/shared/Translate/EN/ItemName.json"
|
$itemNamePath = Join-Path $releaseRoot "media/lua/shared/Translate/EN/ItemName.json"
|
||||||
@@ -67,6 +70,7 @@ $legacyItemNamePath = Join-Path $repositoryRoot "common/media/lua/shared/Transla
|
|||||||
$serverCommands = Get-Content -LiteralPath $serverCommandsPath -Raw
|
$serverCommands = Get-Content -LiteralPath $serverCommandsPath -Raw
|
||||||
$btTow = Get-Content -LiteralPath $btTowPath -Raw
|
$btTow = Get-Content -LiteralPath $btTowPath -Raw
|
||||||
$clientSync = Get-Content -LiteralPath $clientSyncPath -Raw
|
$clientSync = Get-Content -LiteralPath $clientSyncPath -Raw
|
||||||
|
$rigidTow = Get-Content -LiteralPath $rigidTowPath -Raw
|
||||||
$hooking = Get-Content -LiteralPath $hookingPath -Raw
|
$hooking = Get-Content -LiteralPath $hookingPath -Raw
|
||||||
$towbarTemplate = Get-Content -LiteralPath $towbarTemplatePath -Raw
|
$towbarTemplate = Get-Content -LiteralPath $towbarTemplatePath -Raw
|
||||||
$itemNames = Get-Content -LiteralPath $itemNamePath -Raw | ConvertFrom-Json
|
$itemNames = Get-Content -LiteralPath $itemNamePath -Raw | ConvertFrom-Json
|
||||||
@@ -79,7 +83,7 @@ $towbarScaledScriptScale = $towbarBaseScriptScale * $towbarVisualScale
|
|||||||
$towbarMeasuredLengthAtBaseScale = 0.9714089036
|
$towbarMeasuredLengthAtBaseScale = 0.9714089036
|
||||||
$towbarScaledHalfLength = ($towbarMeasuredLengthAtBaseScale * $towbarVisualScale) / 2
|
$towbarScaledHalfLength = ($towbarMeasuredLengthAtBaseScale * $towbarVisualScale) / 2
|
||||||
|
|
||||||
Assert-True ($towbarTemplate -match ('(?s)model towbarModel\s*\{{.*?scale = {0},' -f [regex]::Escape($towbarScaledScriptScale.ToString('0.000', [Globalization.CultureInfo]::InvariantCulture)))) "The normal towbar visual must render at 2.5x its measured 0.01 script scale."
|
Assert-True ($towbarTemplate -match ('(?s)model towbarModelKI5\s*\{{.*?scale = {0},' -f [regex]::Escape($towbarScaledScriptScale.ToString('0.000', [Globalization.CultureInfo]::InvariantCulture)))) "The isolated KI5 towbar visual must render at 2.5x its measured 0.01 script scale."
|
||||||
Assert-True ($hooking -match 'local TowbarVisualScale = 2\.5') "Client placement must declare the 2.5x visual scale."
|
Assert-True ($hooking -match 'local TowbarVisualScale = 2\.5') "Client placement must declare the 2.5x visual scale."
|
||||||
Assert-True ($btTow -match 'local TowbarVisualScale = 2\.5') "Part initialization must declare the 2.5x visual scale."
|
Assert-True ($btTow -match 'local TowbarVisualScale = 2\.5') "Part initialization must declare the 2.5x visual scale."
|
||||||
Assert-True ($hooking -match '(?s)local TowbarScaledModelLength = TowbarModelLength \* TowbarVisualScale.*?local TowbarModelHalfLength = TowbarScaledModelLength / 2') "Client placement must compensate offsets using the scaled mesh length."
|
Assert-True ($hooking -match '(?s)local TowbarScaledModelLength = TowbarModelLength \* TowbarVisualScale.*?local TowbarModelHalfLength = TowbarScaledModelLength / 2') "Client placement must compensate offsets using the scaled mesh length."
|
||||||
@@ -102,47 +106,55 @@ Assert-True ($serverCommands -match '(?s)isLegacyTowBarPair.*?hasTowBarState\(ve
|
|||||||
Assert-True ($serverCommands -match '(?s)local function resolveExpectedTowBarPair.*?towingVehicle and towedVehicle.*?isExpectedTowBarPair\(towingVehicle, towedVehicle\)') "Current-link spontaneous resolution must still require reciprocal towbar identity."
|
Assert-True ($serverCommands -match '(?s)local function resolveExpectedTowBarPair.*?towingVehicle and towedVehicle.*?isExpectedTowBarPair\(towingVehicle, towedVehicle\)') "Current-link spontaneous resolution must still require reciprocal towbar identity."
|
||||||
Assert-True ($serverCommands -match '(?s)local function snapshotActiveTowbarLinksServer.*?isExpectedTowBarPair\(towingVehicle, towedVehicle\).*?isLegacyTowBarPair\(towingVehicle, towedVehicle\)') "Server snapshots must only preserve reciprocal or clean legacy towbar pairs."
|
Assert-True ($serverCommands -match '(?s)local function snapshotActiveTowbarLinksServer.*?isExpectedTowBarPair\(towingVehicle, towedVehicle\).*?isLegacyTowBarPair\(towingVehicle, towedVehicle\)') "Server snapshots must only preserve reciprocal or clean legacy towbar pairs."
|
||||||
Assert-True ($serverCommands -match 'spontaneousDetachSync') "Server must broadcast spontaneous towbar cleanup."
|
Assert-True ($serverCommands -match 'spontaneousDetachSync') "Server must broadcast spontaneous towbar cleanup."
|
||||||
Assert-True ($serverCommands -match 'local vehicleBId = args\.vehicleB or args\.vehicle') "Manual detach sync must include the towed vehicle id."
|
Assert-True ($serverCommands -match '(?s)function Commands\.detachTowBar.*?broadcastDetach\(towingVehicle:getId\(\), towedVehicle:getId\(\)\)') "Manual detach sync must immediately include both vehicle ids."
|
||||||
Assert-True ($serverCommands -notmatch 'function Commands\.giveTowBar') "Clients must not be able to invoke the towbar refund helper."
|
Assert-True ($serverCommands -notmatch 'function Commands\.giveTowBar') "Clients must not be able to invoke the towbar refund helper."
|
||||||
Assert-True ($serverCommands -match 'isPlayerAuthorizedForPair') "Server refunds must validate the requesting player."
|
Assert-True ($serverCommands -match 'isPlayerAuthorizedForPair') "Server refunds must validate the requesting player."
|
||||||
Assert-True ($serverCommands -match 'rejected towbar attach without item') "Multiplayer attach must require server-side towbar ownership."
|
Assert-True ($serverCommands -match 'rejected towbar attach without item') "Multiplayer attach must require server-side towbar ownership."
|
||||||
Assert-True ($serverCommands -match '(?s)function Commands\.attachTowBar.*?if hasAnyTowLink\(vehicleA\) or hasAnyTowLink\(vehicleB\) then.*?return.*?if isExpectedTowBarPair\(vehicleA, vehicleB\).*?or isExpectedTowBarPair\(vehicleB, vehicleA\).*?or hasPendingAttach\(vehicleA, vehicleB\).*?or hasPendingAttach\(vehicleB, vehicleA\).*?then.*?return.*?local towBarItem = findTowBarItem') "Existing or reversed pending tow pairs must be rejected before consuming another towbar."
|
Assert-True ($serverCommands -match '(?s)function Commands\.attachTowBar.*?if hasAnyTowLink\(vehicleA\) or hasAnyTowLink\(vehicleB\) then.*?return.*?if isExpectedTowBarPair\(vehicleA, vehicleB\).*?or isExpectedTowBarPair\(vehicleB, vehicleA\).*?then.*?return.*?local towBarItem = findTowBarItem') "Existing or reversed logical tow pairs must be rejected before consuming another towbar."
|
||||||
Assert-True ($serverCommands -match 'queueSync\("attach", player, args, true\)') "Multiplayer attach must reserve the towbar for delayed confirmation."
|
Assert-True ($serverCommands -match '(?s)function Commands\.attachTowBar.*?markExpectedTowBarPair\(vehicleA, vehicleB, args\.attachmentA, args\.attachmentB\).*?vehicleA:addPointConstraint.*?broadcastAttach\(vehicleA, vehicleB, args\.attachmentA, args\.attachmentB\)') "Portable attach must use the working wrecker one-shot server add and immediate sync sequence."
|
||||||
Assert-True ($serverCommands -match '(?s)function Commands\.attachTowBar.*?markExpectedTowBarPair\(vehicleA, vehicleB, args\.attachmentA, args\.attachmentB\).*?queueSync\("attach", player, args, true\).*?vehicleA:addPointConstraint') "Attach must record and queue the pair before constraint creation can break spontaneously."
|
Assert-True ($serverCommands -notmatch 'pendingSync|queueSync|processAttachSync|failAttachSync|phase =') "Portable attach must not retain its failed provisional/confirmed controller."
|
||||||
Assert-True ($serverCommands -match 'local function failAttachSync') "Failed multiplayer attach must have a cleanup/refund path."
|
|
||||||
Assert-True ($serverCommands -match '(?s)local function processAttachSync.*?if isTowBarPairConfirmed\(vehicleA, vehicleB\) then return "broken" end') "A confirmed pair that breaks during attach confirmation must not be reattached."
|
|
||||||
Assert-True ($serverCommands -match '(?s)local function processPendingSync.*?status == "broken".*?finalizeBrokenTowBarPair') "A break during attach confirmation must use the shared ground-drop finalizer."
|
|
||||||
Assert-True ($serverCommands -match 'if item\.reservedTowBar then') "Failed multiplayer attach must refund the reserved towbar exactly once."
|
|
||||||
Assert-True ($serverCommands -match 'Constraint creation may complete on a later server tick') "Multiplayer attach must preserve delayed constraint confirmation."
|
|
||||||
Assert-True ($serverCommands -match 'local function finalizeBrokenTowBarPair') "Broken towbar cleanup must use one idempotent finalizer."
|
Assert-True ($serverCommands -match 'local function finalizeBrokenTowBarPair') "Broken towbar cleanup must use one idempotent finalizer."
|
||||||
Assert-True ($serverCommands -match 'local function reconcileBrokenTowBarPairsServer') "Server and single-player must audit confirmed towbar constraints for silent breaks."
|
Assert-True ($serverCommands -match 'local function reconcileBrokenTowBarPairsServer') "Server and single-player must audit confirmed towbar constraints for silent breaks."
|
||||||
Assert-True ($serverCommands -match '(?s)local function reconcileBrokenTowBarPairsServer.*?Persistence\.advanceRecoveryState.*?RestoreRetryMs, SustainedBreakMs.*?action == "break".*?finalizeBrokenTowBarPair.*?action == "restore".*?restorePersistedTowBarPair') "The physical-link audit must use the shared bounded recovery state machine."
|
Assert-True ($serverCommands -match '(?s)local function reconcileBrokenTowBarPairsServer.*?Persistence\.advanceRecoveryState.*?RestoreRetryMs, SustainedBreakMs.*?action == "break".*?finalizeBrokenTowBarPair.*?action == "restore".*?restorePersistedTowBarPair') "The physical-link audit must use the shared bounded recovery state machine."
|
||||||
Assert-True ($persistence -match '(?s)function Persistence\.advanceRecoveryState.*?state\.unlinkedSince = state\.unlinkedSince or now.*?now - state\.unlinkedSince >= breakMs.*?return state, "break"' -and $persistence -match '(?s)if not state\.pendingUntil or now >= state\.pendingUntil then.*?state\.pendingUntil = now \+ retryMs.*?return state, "restore"') "Persistent-pair recovery must debounce restores and require a sustained break before cleanup."
|
Assert-True ($persistence -match '(?s)function Persistence\.advanceRecoveryState.*?state\.unlinkedSince = state\.unlinkedSince or now.*?now - state\.unlinkedSince >= breakMs.*?return state, "break"' -and $persistence -match '(?s)if not state\.pendingUntil or now >= state\.pendingUntil then.*?state\.pendingUntil = now \+ retryMs.*?return state, "restore"') "Persistent-pair recovery must debounce restores and require a sustained break before cleanup."
|
||||||
Assert-True ($serverCommands -match '(?s)local function restorePersistedTowBarPair.*?towBarExpectedAttachment.*?addPointConstraint.*?broadcastAttach') "A saved towbar pair must rebuild its physical constraint without consuming another item."
|
Assert-True ($serverCommands -match '(?s)local function restorePersistedTowBarPair.*?towBarExpectedAttachment.*?addPointConstraint.*?broadcastAttach') "A saved towbar pair must use the same immediate wrecker-style restore without consuming another item."
|
||||||
Assert-True ($persistence -match '(?s)if not state\.pendingUntil or now >= state\.pendingUntil then.*?return state, "restore"') "An unconfirmed saved pair must schedule a bounded restoration attempt."
|
Assert-True ($persistence -match '(?s)if not state\.pendingUntil or now >= state\.pendingUntil then.*?return state, "restore"') "An unconfirmed saved pair must schedule a bounded restoration attempt."
|
||||||
Assert-True ($clientSync -match 'TowBarMod\.Sync\.applyAttachSync = applyAttachSync') "Single-player must expose the existing attach synchronizer for saved-pair recovery."
|
Assert-True ($clientSync -match 'TowBarMod\.Sync\.appliedPairs') "Client must retain the wrecker-style applied-pair cache."
|
||||||
Assert-True ($clientSync -match '(?s)local key = tostring\(vehicleA:getId\(\)\).*?if TowBarMod\.Sync\.appliedPairs\[key\] and not isLinked\(vehicleA, vehicleB\) then.*?TowBarMod\.Sync\.appliedPairs\[key\] = nil.*?if not TowBarMod\.Sync\.appliedPairs\[key\] then.*?breakTowBarPair\(vehicleA, vehicleB\).*?setVehicleScriptWithTowBarHidden\(vehicleB, "notTowingA_Trailer"\).*?addPointConstraint\(nil, vehicleB, attachmentA, attachmentB, true\)') "Every fresh or recovered towbar pair must replace the native rope with one local rigid constraint."
|
Assert-True ($clientSync -match '(?s)local function applyAttachSync.*?isPairLinked\(vehicleA, vehicleB\) and Sync\.appliedPairs\[key\].*?TowBarMod\.RigidTow\.attach.*?Sync\.appliedPairs\[key\] = true') "Portable client sync must use one idempotent wrecker-style rigid conversion."
|
||||||
Assert-True ($clientSync -notmatch '(?s)if linked then\s*--.*?appliedPairs\[key\] = true') "A newly observed native tow link must not be accepted as rigid without rebuilding it."
|
Assert-True ($clientSync -match 'TowBarMod\.Sync\.desiredPairs = TowBarMod\.Sync\.desiredPairs or \{\}') "Portable MP sync must retain an authoritative pair until its driver owns local physics."
|
||||||
Assert-True ($clientSync -match '(?s)local function clearAppliedPairForVehicle.*?TowBarMod\.Sync\.appliedPairs\[key\] = nil.*?Events\.OnSpawnVehicleEnd\.Add\(clearAppliedPairForVehicle\)') "Vehicle streaming must invalidate cached rigid towbar constraints."
|
Assert-True ($clientSync -match '(?s)local function applyAttachSync\(args, playerObj, forceReattach\).*?if not isLocalDriver\(vehicleA, localPlayer\) then.*?return true.*?not forceReattach and isPairLinked.*?TowBarMod\.RigidTow\.attach') "Portable MP sync must defer ordinary conversion until driver ownership and allow an explicit forced entry rebuild."
|
||||||
|
Assert-True ($clientSync -match '(?s)local function getPersistedPairArgsForDriver.*?isTowingByTowBar.*?towed == true.*?towBarTowingVehicleId') "Portable driver entry must require reciprocal persisted towbar identity."
|
||||||
|
Assert-True ($clientSync -match 'Events\.OnEnterVehicle\.Add\(forceReattachForDriver\)') "Driver entry must force the one-shot portable rigid rebuild."
|
||||||
|
Assert-True ($clientSync -match 'Events\.OnSpawnVehicleEnd\.Add\(onSpawnVehicle\)') "Vehicle streaming must replay a retained authoritative pair for an existing local driver."
|
||||||
|
Assert-True ($clientSync -match '(?s)local function clearAppliedPairForVehicle.*?Sync\.appliedPairs\[key\] = nil') "Vehicle streaming must invalidate the applied cache for the next server snapshot."
|
||||||
|
Assert-True ($clientSync -match '(?s)local function onSpawnVehicle\(vehicle\).*?clearAppliedPairForVehicle\(vehicle\).*?retryDesiredPairsForDriver\(getPlayer\(\)\).*?Events\.OnSpawnVehicleEnd\.Add\(onSpawnVehicle\)') "Vehicle streaming must replay a retained authoritative pair for an existing local driver."
|
||||||
|
Assert-True ($clientSync -notmatch 'Lifecycle|pending|provisional|confirmed|Events\.OnTick\.Add') "Portable client sync must not retain timing or phase controllers."
|
||||||
|
Assert-True ($rigidTow -match 'function RigidTow\.attach') "Rigid physics must be preserved behind one public primitive."
|
||||||
|
Assert-True ($rigidTow -match '(?s)function RigidTow\.attach.*?updateAttachmentsForRigidTow.*?breakExactPair.*?applyFreeRollingTowState\(towedVehicle\).*?setVehicleScriptWithTowBarHidden\(towedVehicle, "notTowingA_Trailer"\).*?addPointConstraint\(nil, towedVehicle, attachmentA, attachmentB, true\).*?setVehicleScriptWithTowBarHidden\(towedVehicle, originalScript\).*?setVehiclePostAttach') "The rigid primitive must preserve the wrecker sequence: free-roll, fake script, rigid add, immediate original-script restore, then finalization."
|
||||||
|
Assert-True ([regex]::Matches($hooking, 'updateAttachmentsForRigidTow\(').Count -eq 0) "Post-attach presentation must not mutate rigid attachment geometry a second time."
|
||||||
|
Assert-True ($hooking -match 'storeOriginalVehicleCall\(vehicle, modData, "towBarOriginalParkingBrake", "getParkingBrake"\)') "Portable towing must use the same parking-brake getter as the working wrecker path."
|
||||||
|
$applyFreeStart = $hooking.IndexOf('local function applyFreeRollingTowState(vehicle)')
|
||||||
|
$applyFreeEnd = $hooking.IndexOf('local function restoreFreeRollingTowState', $applyFreeStart)
|
||||||
|
$applyFreeBody = if ($applyFreeStart -ge 0 -and $applyFreeEnd -gt $applyFreeStart) { $hooking.Substring($applyFreeStart, $applyFreeEnd - $applyFreeStart) } else { '' }
|
||||||
|
Assert-True ($applyFreeBody -notmatch 'updateTotalMass\(|constraintChanged\(') "Portable free-roll must not recalculate away its towing mass before or during rigid movement."
|
||||||
|
Assert-True ($rigidTow -notmatch 'if not isExactPairLinked\(towingVehicle, towedVehicle\) then return false end') "Rigid submission must not fail solely because MP reciprocal getters update later."
|
||||||
|
Assert-True ($rigidTow -notmatch 'sendClientCommand|sendServerCommand|TowBarItem|Persistence') "Rigid physics must remain separate from commands, item accounting, and persistence."
|
||||||
Assert-True ($serverCommands -match '(?s)local function broadcastAttach.*?if isServer\(\).*?sendServerCommand.*?elseif not isClient\(\).*?TowBarMod\.Sync\.applyAttachSync') "Saved towbar recovery must synchronize both dedicated-server and single-player sessions."
|
Assert-True ($serverCommands -match '(?s)local function broadcastAttach.*?if isServer\(\).*?sendServerCommand.*?elseif not isClient\(\).*?TowBarMod\.Sync\.applyAttachSync') "Saved towbar recovery must synchronize both dedicated-server and single-player sessions."
|
||||||
Assert-True ($serverCommands -match 'towBarTowedVehicleSqlId' -and $serverCommands -match 'towBarTowingVehicleSqlId' -and $serverCommands -match 'getSqlId') "Towbar persistence must use stable save IDs in addition to live network IDs."
|
Assert-True ($serverCommands -match 'towBarTowedVehicleSqlId' -and $serverCommands -match 'towBarTowingVehicleSqlId' -and $serverCommands -match 'getSqlId') "Towbar persistence must use stable save IDs in addition to live network IDs."
|
||||||
Assert-True ($serverCommands -match 'Persistence\.savePair\("towbar"' -and $serverCommands -match 'Persistence\.forEachPair\("towbar"') "Towbar persistence must use the server-owned saved-pair registry."
|
Assert-True ($serverCommands -match 'Persistence\.savePair\("towbar"' -and $serverCommands -match 'Persistence\.forEachPair\("towbar"') "Towbar persistence must use the server-owned saved-pair registry."
|
||||||
Assert-True ($hooking -match '(?s)local function recoverTowBarVehicleAfterLoad.*?authoritative audit will.*?reconnect') "Client load recovery must defer missing constraints to the authoritative saved-pair audit."
|
Assert-True ($hooking -notmatch 'Events\.OnEnterVehicle\.Add|Events\.OnSwitchVehicleSeat\.Add') "Entering or switching seats must never repair or restart towing."
|
||||||
$loadRecoveryStart = $hooking.IndexOf('local function recoverTowBarVehicleAfterLoad')
|
Assert-True ($hooking -notmatch 'reattachTowBarPair|recoverTowBarVehicleAfterLoad|tryAutoReattachFromCharacter|reattachTowBarFromDriverSeat|lastAutoReattach') "The rewritten attach lifecycle must not retain the old seat-event repair helpers."
|
||||||
$loadRecoveryEnd = $hooking.IndexOf('function TowBarMod.Hook.setVehiclePostAttach', $loadRecoveryStart)
|
Assert-True ($hooking -match '(?s)function TowBarMod\.Hook\.performAttachTowBar.*?sendTowAttachCommand\(playerObj, args\).*?end') "The radial attach action must only submit the authoritative request."
|
||||||
if ($loadRecoveryStart -ge 0 -and $loadRecoveryEnd -gt $loadRecoveryStart) {
|
Assert-True ($hooking -notmatch 'sendClientCommand\(playerObj, "towbar", "consumeTowBar"') "The client must not consume the towbar before server attach validation."
|
||||||
$loadRecovery = $hooking.Substring($loadRecoveryStart, $loadRecoveryEnd - $loadRecoveryStart)
|
Assert-True ($serverCommands -match '(?s)local function processTowBarServerTick.*?reconcileBrokenTowBarPairsServer\(\)') "The physical-link audit must run from the server/SP tick handler."
|
||||||
Assert-True ($loadRecovery -notmatch 'detachTowBar|attachTowBar|reattachTowBarPairAfterCleanDetach') "Loading a saved towbar pair must not refund or consume the item."
|
Assert-True ($serverCommands -match '(?s)local function finalizeBrokenTowBarPair.*?dropTowBarOnGround.*?forgetTowBarPair.*?breakTowBarConstraint.*?clearExpectedTowBarPair.*?broadcastSpontaneousDetach') "Broken-pair finalization must drop, disconnect, clear state, and synchronize cleanup."
|
||||||
}
|
|
||||||
Assert-True ($serverCommands -match '(?s)local function processPendingSync.*?reconcileBrokenTowBarPairsServer\(\)') "The physical-link audit must run from the server/SP tick handler."
|
|
||||||
Assert-True ($serverCommands -match '(?s)local function finalizeBrokenTowBarPair.*?dropTowBarOnGround.*?cancelPendingAttach.*?breakTowBarConstraint.*?clearExpectedTowBarPair.*?broadcastSpontaneousDetach') "Broken-pair finalization must drop, disconnect, clear state, and synchronize cleanup."
|
|
||||||
Assert-True ($serverCommands -match '(?s)local function finalizeBrokenTowBarPair.*?if isServer\(\) then.*?elseif not isClient\(\).*?TowBarMod\.Hook\.cleanupDetachedTowBar') "Single-player breaks must directly restore the vehicle without relying on a server command."
|
Assert-True ($serverCommands -match '(?s)local function finalizeBrokenTowBarPair.*?if isServer\(\) then.*?elseif not isClient\(\).*?TowBarMod\.Hook\.cleanupDetachedTowBar') "Single-player breaks must directly restore the vehicle without relying on a server command."
|
||||||
Assert-True ($clientSync -match 'spontaneousDetachSync') "Client sync must receive spontaneous towbar cleanup."
|
Assert-True ($clientSync -match 'spontaneousDetachSync') "Client sync must receive spontaneous towbar cleanup."
|
||||||
Assert-True ($clientSync -match 'local function hasConflictingTowLink') "Client detach sync must detect links to unrelated vehicles."
|
Assert-True ($clientSync -match 'local function hasConflictingLink') "Client detach sync must detect links to unrelated vehicles."
|
||||||
Assert-True ($clientSync -match 'local breakTowBarPair' -and $clientSync -match 'breakTowBarPair = function') "Client detach sync must break only the requested pair."
|
Assert-True ($clientSync -match 'TowBarMod\.RigidTow\.breakExactPair\(vehicleA, vehicleB\)') "Client detach sync must break only the requested pair."
|
||||||
Assert-True ($clientSync -notmatch 'local function safeBreak') "Client detach sync must not use an unrestricted per-vehicle constraint break."
|
Assert-True ($clientSync -notmatch 'local function safeBreak') "Client detach sync must not use an unrestricted per-vehicle constraint break."
|
||||||
Assert-True ($clientSync -match '(?s)local function applyDetachSync.*?hasConflictingTowLink\(vehicleA, vehicleB\).*?return.*?breakTowBarPair\(vehicleA, vehicleB\).*?cleanupDetachedTowBar') "Client detach sync must reject conflicting links before cleanup."
|
|
||||||
Assert-True ($hooking -match 'function TowBarMod\.Hook\.cleanupDetachedTowBar') "Client must expose idempotent towbar cleanup."
|
Assert-True ($hooking -match 'function TowBarMod\.Hook\.cleanupDetachedTowBar') "Client must expose idempotent towbar cleanup."
|
||||||
|
Assert-True ($hooking -match '(?s)function TowBarMod\.Hook\.setVehiclePostAttach\(playerObj, towedVehicle, knownTowingVehicle\).*?local towingVehicle = knownTowingVehicle or towedVehicle:getVehicleTowedBy\(\).*?if not towingVehicle then return end.*?setVehicleScriptWithTowBarHidden\(towedVehicle, towedModData\.towBarOriginalScriptName\)') "Post-attach must accept the authoritative towing vehicle while MP reciprocal getters lag."
|
||||||
Assert-True ($hooking -match '(?s)if modData\.towBarOriginalParkingBrakeOn ~= nil then.*?setParkingBrakeOn.*?end.*?if modData\.towBarOriginalParkingBrake ~= nil then.*?setParkingBrake.*?end.*?if modData\.towBarOriginalHandbrake ~= nil then.*?setHandbrake.*?end') "Free-rolling state may only change brake controls whose original values were captured."
|
Assert-True ($hooking -match '(?s)if modData\.towBarOriginalParkingBrakeOn ~= nil then.*?setParkingBrakeOn.*?end.*?if modData\.towBarOriginalParkingBrake ~= nil then.*?setParkingBrake.*?end.*?if modData\.towBarOriginalHandbrake ~= nil then.*?setHandbrake.*?end') "Free-rolling state may only change brake controls whose original values were captured."
|
||||||
Assert-True ($hooking -notmatch 'tryVehicleCall\(vehicle, "setBrake"') "Free-rolling state must not change an unrestorable brake control."
|
Assert-True ($hooking -notmatch 'tryVehicleCall\(vehicle, "setBrake"') "Free-rolling state must not change an unrestorable brake control."
|
||||||
Assert-True ($hooking -notmatch 'tryVehicleCall\(vehicle, "setBraking"') "Free-rolling state must not change an unrestorable braking control."
|
Assert-True ($hooking -notmatch 'tryVehicleCall\(vehicle, "setBraking"') "Free-rolling state must not change an unrestorable braking control."
|
||||||
@@ -161,13 +173,11 @@ Assert-True ($hooking -match '(?s)local centerOk, center = pcall.*?if not center
|
|||||||
Assert-True ($btTow -match '(?s)local centerOk, center = pcall.*?if not centerOk or not center then return nil end.*?if not zOk or type\(centerZ\) ~= "number" then return nil end') "Part initialization must not show a towbar when center-of-mass geometry is invalid."
|
Assert-True ($btTow -match '(?s)local centerOk, center = pcall.*?if not centerOk or not center then return nil end.*?if not zOk or type\(centerZ\) ~= "number" then return nil end') "Part initialization must not show a towbar when center-of-mass geometry is invalid."
|
||||||
Assert-True ($hooking -match '(?s)local function getTowbarModelSlot\(script\).*?local frontEdgeZ = getTowbarFrontEdgeZ\(script\).*?local modelCenterZ = frontEdgeZ \+ TowbarModelHalfLength.*?math\.floor\(\(\(modelCenterZ - TowbarFirstZ\) / TowbarSlotStep\) \+ 0\.5\).*?math\.max\(0, math\.min\(TowbarMaxIndex, index\)\)') "Client must place the towbar's inner end at the hitbox edge."
|
Assert-True ($hooking -match '(?s)local function getTowbarModelSlot\(script\).*?local frontEdgeZ = getTowbarFrontEdgeZ\(script\).*?local modelCenterZ = frontEdgeZ \+ TowbarModelHalfLength.*?math\.floor\(\(\(modelCenterZ - TowbarFirstZ\) / TowbarSlotStep\) \+ 0\.5\).*?math\.max\(0, math\.min\(TowbarMaxIndex, index\)\)') "Client must place the towbar's inner end at the hitbox edge."
|
||||||
Assert-True ($btTow -match '(?s)local function getTowbarModelSlot\(script\).*?local frontEdgeZ = getTowbarFrontEdgeZ\(script\).*?local modelCenterZ = frontEdgeZ \+ TowbarModelHalfLength.*?math\.floor\(\(\(modelCenterZ - TowbarFirstZ\) / TowbarSlotStep\) \+ 0\.5\).*?math\.max\(0, math\.min\(TowbarMaxIndex, index\)\)') "Part initialization must place the towbar's inner end at the hitbox edge."
|
Assert-True ($btTow -match '(?s)local function getTowbarModelSlot\(script\).*?local frontEdgeZ = getTowbarFrontEdgeZ\(script\).*?local modelCenterZ = frontEdgeZ \+ TowbarModelHalfLength.*?math\.floor\(\(\(modelCenterZ - TowbarFirstZ\) / TowbarSlotStep\) \+ 0\.5\).*?math\.max\(0, math\.min\(TowbarMaxIndex, index\)\)') "Part initialization must place the towbar's inner end at the hitbox edge."
|
||||||
Assert-True ($hooking -notmatch 'isVanillaScale|modelScale') "Dynamic hitbox placement must not apply a second model-scale correction."
|
Assert-True ($hooking -match 'isKi5 and ki5Part') "Client rendering must isolate KI5 visuals from legacy parts."
|
||||||
Assert-True ($btTow -notmatch 'isVanillaScale|modelScale') "Part initialization must not apply a second model-scale correction."
|
Assert-True ($btTow -match 'isKi5 and partId == "towbarKI5"') "Part initialization must isolate KI5 visuals from legacy parts."
|
||||||
Assert-True ($hooking -match '(?s)local function setTowBarModelVisible.*?local part = normalPart.*?part:setModelVisible\("towbar" \.\. index, true\)') "Automatic rendering must use the normal towbar part."
|
|
||||||
Assert-True ($btTow -match '(?s)function BTtow\.Init\.towbar.*?local shouldShowOnThisPart = part:getId\(\) == "towbar"') "Automatic part initialization must only show the normal towbar part."
|
|
||||||
Assert-True ($hooking -notmatch 'local part = isVanilla and normalPart or largePart') "KI5 automatic rendering must not select the large towbar part."
|
Assert-True ($hooking -notmatch 'local part = isVanilla and normalPart or largePart') "KI5 automatic rendering must not select the large towbar part."
|
||||||
$normalTowbarPart = $towbarTemplate.Substring($towbarTemplate.IndexOf('part towbar'), $towbarTemplate.IndexOf('part towbarLarge') - $towbarTemplate.IndexOf('part towbar'))
|
$normalTowbarPart = $towbarTemplate.Substring($towbarTemplate.IndexOf('part towbar'), $towbarTemplate.IndexOf('part towbarLarge') - $towbarTemplate.IndexOf('part towbar'))
|
||||||
Assert-True ($towbarTemplate -match '(?s)model towbarModel\s*\{.*?scale = 0\.025,') "The rendered towbar mesh must be exactly 2.5 times its original 0.01 scale."
|
Assert-True ($towbarTemplate -match '(?s)model towbarModelKI5\s*\{.*?scale = 0\.025,') "The KI5 towbar mesh must be exactly 2.5 times its original 0.01 scale."
|
||||||
Assert-True ($normalTowbarPart -match '(?s)model towbar0\s*\{\s*file = towbarModel,\s*offset = 0 -0\.3 1\.0,') "Normal towbar0 must retain the radial-menu Z=1.0 geometry."
|
Assert-True ($normalTowbarPart -match '(?s)model towbar0\s*\{\s*file = towbarModel,\s*offset = 0 -0\.3 1\.0,') "Normal towbar0 must retain the radial-menu Z=1.0 geometry."
|
||||||
$normalTowbarModels = [regex]::Matches($normalTowbarPart, '(?s)model towbar(\d+)\s*\{\s*file = towbarModel,\s*offset = ([\d.-]+) ([\d.-]+) ([\d.-]+),')
|
$normalTowbarModels = [regex]::Matches($normalTowbarPart, '(?s)model towbar(\d+)\s*\{\s*file = towbarModel,\s*offset = ([\d.-]+) ([\d.-]+) ([\d.-]+),')
|
||||||
Assert-True ($normalTowbarModels.Count -eq 24) "Normal towbar part must provide all 24 dynamic Z slots."
|
Assert-True ($normalTowbarModels.Count -eq 24) "Normal towbar part must provide all 24 dynamic Z slots."
|
||||||
@@ -209,7 +219,7 @@ Assert-True ($hooking -match '(?s)setVehicleScriptWithTowBarHidden.*?setTowBarMo
|
|||||||
Assert-True ($btTow -match '(?s)function BTtow\.Init\.towbar.*?setModelVisible\("towbar" \.\. j, false\).*?towBarModelSwapInProgress.*?return') "Part initialization must not show a towbar during a script swap."
|
Assert-True ($btTow -match '(?s)function BTtow\.Init\.towbar.*?setModelVisible\("towbar" \.\. j, false\).*?towBarModelSwapInProgress.*?return') "Part initialization must not show a towbar during a script swap."
|
||||||
Assert-True ($hooking -notmatch 'towedVehicle:setScriptName\(') "All hook script swaps must use the anti-flicker helper."
|
Assert-True ($hooking -notmatch 'towedVehicle:setScriptName\(') "All hook script swaps must use the anti-flicker helper."
|
||||||
Assert-True ($clientSync -notmatch 'vehicleB:setScriptName\(') "Client reconciliation must not bypass the anti-flicker helper."
|
Assert-True ($clientSync -notmatch 'vehicleB:setScriptName\(') "Client reconciliation must not bypass the anti-flicker helper."
|
||||||
Assert-True ($clientSync -match 'TowBarMod\.Hook\.setVehicleScriptWithTowBarHidden\(vehicleB, "notTowingA_Trailer"\)') "Client reconciliation must hide towbar models while changing scripts."
|
Assert-True ($rigidTow -match 'TowBarMod\.Hook\.setVehicleScriptWithTowBarHidden\(towedVehicle, "notTowingA_Trailer"\)') "The rigid primitive must hide towbar models while changing scripts."
|
||||||
|
|
||||||
$spontaneousStart = $serverCommands.IndexOf('if module == "vehicle" and command == "detachTrailerSpontaneous" then')
|
$spontaneousStart = $serverCommands.IndexOf('if module == "vehicle" and command == "detachTrailerSpontaneous" then')
|
||||||
$spontaneousEnd = $serverCommands.IndexOf('if module == "towbar" and Commands[command] then', $spontaneousStart)
|
$spontaneousEnd = $serverCommands.IndexOf('if module == "towbar" and Commands[command] then', $spontaneousStart)
|
||||||
@@ -241,7 +251,7 @@ $manualDetachEnd = $serverCommands.IndexOf('function Commands.consumeTowBar', $m
|
|||||||
Assert-True ($manualDetachStart -ge 0 -and $manualDetachEnd -gt $manualDetachStart) "Could not isolate the manual detach handler."
|
Assert-True ($manualDetachStart -ge 0 -and $manualDetachEnd -gt $manualDetachStart) "Could not isolate the manual detach handler."
|
||||||
if ($manualDetachStart -ge 0 -and $manualDetachEnd -gt $manualDetachStart) {
|
if ($manualDetachStart -ge 0 -and $manualDetachEnd -gt $manualDetachStart) {
|
||||||
$manualDetachBranch = $serverCommands.Substring($manualDetachStart, $manualDetachEnd - $manualDetachStart)
|
$manualDetachBranch = $serverCommands.Substring($manualDetachStart, $manualDetachEnd - $manualDetachStart)
|
||||||
Assert-True ($manualDetachBranch -match 'if not isLinked\(towingVehicle, towedVehicle\) then') "Manual detach must reject mismatched or stale vehicle pairs."
|
Assert-True ($manualDetachBranch -notmatch 'if not isLinked\(towingVehicle, towedVehicle\) then') "Manual detach must follow wrecker behavior and allow a reciprocal saved pair while MP getters lag."
|
||||||
Assert-True ($manualDetachBranch -match '(?s)not isExpectedTowBarPair\(towingVehicle, towedVehicle\).*?not isLegacyTowBarPair\(towingVehicle, towedVehicle\)') "Manual detach must reject non-towbar or stale linked pairs."
|
Assert-True ($manualDetachBranch -match '(?s)not isExpectedTowBarPair\(towingVehicle, towedVehicle\).*?not isLegacyTowBarPair\(towingVehicle, towedVehicle\)') "Manual detach must reject non-towbar or stale linked pairs."
|
||||||
Assert-True ($manualDetachBranch -match 'breakTowBarConstraint\(towingVehicle, towedVehicle\)') "Manual detach must only break the validated tow pair."
|
Assert-True ($manualDetachBranch -match 'breakTowBarConstraint\(towingVehicle, towedVehicle\)') "Manual detach must only break the validated tow pair."
|
||||||
Assert-True ($manualDetachBranch -match 'giveTowBar\(player, true\)') "Manual detach must continue returning the towbar to inventory."
|
Assert-True ($manualDetachBranch -match 'giveTowBar\(player, true\)') "Manual detach must continue returning the towbar to inventory."
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$repositoryRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$releaseRoot = Join-Path $repositoryRoot "42.20"
|
||||||
|
$failures = [Collections.Generic.List[string]]::new()
|
||||||
|
|
||||||
|
function Assert-True {
|
||||||
|
param([bool]$Condition, [string]$Message)
|
||||||
|
if (-not $Condition) { $script:failures.Add($Message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Read-Source {
|
||||||
|
param([string]$Path)
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
$script:failures.Add("Missing dual-mode source: $Path")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return Get-Content -LiteralPath $Path -Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
$compatibilityPath = Join-Path $releaseRoot "media/lua/shared/TowBar/VehicleCompatibility.lua"
|
||||||
|
$hooking = Read-Source (Join-Path $releaseRoot "media/lua/client/TowBar/TowingHooking.lua")
|
||||||
|
$btTow = Read-Source (Join-Path $releaseRoot "media/lua/server/BTTow.lua")
|
||||||
|
$template = Read-Source (Join-Path $releaseRoot "media/scripts/vehicles/template_towbar.txt")
|
||||||
|
$compatibility = Read-Source $compatibilityPath
|
||||||
|
|
||||||
|
# The boundary must be an exact mod-ID + exact VehicleScript full-name map.
|
||||||
|
# Model scale remains part of the legacy renderer only; it cannot decide KI5 ownership.
|
||||||
|
Assert-True ($compatibility -match 'TowBarMod\.Compatibility') "Classifier must expose TowBarMod.Compatibility."
|
||||||
|
Assert-True ($compatibility -match 'function Compatibility\.isKi5Vehicle\(vehicle\)') "Classifier must expose isKi5Vehicle(vehicle)."
|
||||||
|
foreach ($identity in @(
|
||||||
|
'91range', 'Base.91range',
|
||||||
|
'87toyotaCorolla', 'Base.87toyotaCorollaAE92levin',
|
||||||
|
'76chevyKseries', 'Base.76chevyK30CCwrecker'
|
||||||
|
)) {
|
||||||
|
Assert-True ($compatibility.Contains($identity)) "Classifier is missing verified exact KI5 identity: $identity"
|
||||||
|
}
|
||||||
|
Assert-True ($compatibility -match '(?s)getActivatedMods\(\).*?contains') "KI5 mode must require an exact active mod ID."
|
||||||
|
Assert-True ($compatibility -match 'getFullName\(\)') "KI5 mode must match the complete VehicleScript name."
|
||||||
|
Assert-True ($compatibility -notmatch 'getModelScale|modelScale|attachmentExist|string\.find|string\.match') "KI5 ownership must not be inferred from scale, attachments, or substring patterns."
|
||||||
|
|
||||||
|
# The visual carrier is classified independently. KI5 keeps current B42.20
|
||||||
|
# dynamic hitbox placement; every other vehicle uses the exact B42.13 selector.
|
||||||
|
foreach ($source in @($hooking, $btTow)) {
|
||||||
|
Assert-True ($source -match 'Compatibility\.isKi5Vehicle\(vehicle\)') "Each visual path must classify the vehicle carrying the towbar."
|
||||||
|
Assert-True ($source -match 'getTowbarFrontEdgeZ') "KI5 visual path must retain hitbox-derived placement."
|
||||||
|
Assert-True ($source -match 'TowbarVisualScale\s*=\s*2\.5') "KI5 visual path must retain 2.5x scale compensation."
|
||||||
|
Assert-True ($source -match 'getTowbarIndexVanilla') "Legacy visual path must restore the B42.13 vanilla slot formula."
|
||||||
|
Assert-True ($source -match 'getTowbarIndexSmallScale') "Legacy non-KI5 path must restore B42.13 small-scale slot selection."
|
||||||
|
Assert-True ($source -match 'VanillaScaleMin\s*=\s*1\.5' -and $source -match 'VanillaScaleMax\s*=\s*2\.0') "Legacy visual classification must preserve the B42.13 scale window."
|
||||||
|
}
|
||||||
|
|
||||||
|
# One bank cannot represent both scales. Restore the original `towbar` bank for
|
||||||
|
# legacy vehicles and isolate today's enlarged KI5 mesh in its own part.
|
||||||
|
Assert-True ($template -match '(?s)model towbarModel\s*\{.*?scale\s*=\s*0\.01,') "Vanilla and normal-scale non-KI5 vehicles must restore the B42.13 0.01 model."
|
||||||
|
Assert-True ($template -match '(?s)model towbarModelKI5\s*\{.*?scale\s*=\s*0\.025,') "KI5 towbar bank must remain exactly 2.5x its original 0.01 scale."
|
||||||
|
Assert-True ($template -match '(?s)model towbarModelLarge\s*\{.*?scale\s*=\s*0\.02022,') "Small-scale non-KI5 vehicles must retain the B42.13 large model scale."
|
||||||
|
Assert-True ($template -match 'part towbarKI5') "Template must provide an isolated KI5 part without resizing the legacy towbar part."
|
||||||
|
|
||||||
|
function Get-Ki5DynamicSlot {
|
||||||
|
param([double]$ShapeZ, [double]$CenterZ)
|
||||||
|
$meshHalfLength = (0.9714089036 * 2.5) / 2
|
||||||
|
$center = $CenterZ + ($ShapeZ / 2) + $meshHalfLength
|
||||||
|
return [Math]::Max(0, [Math]::Min(23, [int][Math]::Floor((($center - 1.0) / 0.1) + 0.5)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-LegacyVanillaSlot {
|
||||||
|
param([double]$ShapeZ)
|
||||||
|
$z = ($ShapeZ / 2) - 0.1
|
||||||
|
return [Math]::Max(0, [Math]::Min(23, [int][Math]::Floor((($z * 2 / 3) - 1) * 10)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-LegacySmallScaleSlot {
|
||||||
|
param([double]$MaxAbsAttachmentZ)
|
||||||
|
return [Math]::Max(0, [Math]::Min(23, [int][Math]::Floor(($MaxAbsAttachmentZ + 0.1 - 1.0) * 10)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-True ((Get-Ki5DynamicSlot 4.3111 -0.0444) -eq 23) "KI5 Corolla must retain B42.20 dynamic slot 23."
|
||||||
|
Assert-True ((Get-Ki5DynamicSlot 4.4444 -0.2556) -eq 22) "KI5 Range Rover must retain B42.20 dynamic slot 22."
|
||||||
|
Assert-True ((Get-LegacyVanillaSlot 2.6044) -eq 0) "B42 Ranger must return to the B42.13 legacy-normal slot 0, not dynamic slot 15."
|
||||||
|
Assert-True ((Get-LegacySmallScaleSlot 2.3222) -eq 14) "Non-KI5 small-scale geometry must retain the B42.13 attachment-derived slot."
|
||||||
|
|
||||||
|
if ($failures.Count -gt 0) {
|
||||||
|
$failures | ForEach-Object { Write-Host "FAIL: $_" -ForegroundColor Red }
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "PASS: Build 42.20 exact-KI5 dual-mode contracts."
|
||||||
@@ -98,10 +98,13 @@ Assert-True ($server -match '(?s)onClientCommand.*?getTimestampMs.*?CommandCoold
|
|||||||
Assert-True ($sync -match 'notTowingA_Trailer') "Client sync must use the existing fake-trailer path to select vanilla rigid constraints."
|
Assert-True ($sync -match 'notTowingA_Trailer') "Client sync must use the existing fake-trailer path to select vanilla rigid constraints."
|
||||||
Assert-True ($sync -match '(?s)applyAttachSync.*?breakWreckerPair.*?setScriptSafely\(target, "notTowingA_Trailer"\).*?addPointConstraint.*?setScriptSafely\(target, originalScript\)') "Client sync must rebuild the constraint while the target uses the fake trailer script, then restore it."
|
Assert-True ($sync -match '(?s)applyAttachSync.*?breakWreckerPair.*?setScriptSafely\(target, "notTowingA_Trailer"\).*?addPointConstraint.*?setScriptSafely\(target, originalScript\)') "Client sync must rebuild the constraint while the target uses the fake trailer script, then restore it."
|
||||||
Assert-True ($sync -match 'addPointConstraint\(nil, target, hookAttachment, targetAttachment, true\)') "Wrecker local physics rebuilds must suppress server detach/attach traffic."
|
Assert-True ($sync -match 'addPointConstraint\(nil, target, hookAttachment, targetAttachment, true\)') "Wrecker local physics rebuilds must suppress server detach/attach traffic."
|
||||||
Assert-True ($sync -match '(?s)local appliedLevel = Sync\.appliedLevels\[key\].*?if isPairLinked\(wrecker, target\).*?and appliedLevel == canonicalLevel then.*?return') "Only a locally applied wrecker constraint at the requested height may be reused."
|
Assert-True ($sync -match '(?s)local appliedLevel = Sync\.appliedLevels\[key\].*?if not forceReattach and isPairLinked\(wrecker, target\).*?and appliedLevel == canonicalLevel then.*?return') "Only an ordinary sync may reuse a locally applied wrecker constraint at the requested height."
|
||||||
|
Assert-True ($sync -match '(?s)local function forceReattachForDriver.*?wreckerTowActive.*?wreckerTowedVehicleId.*?wreckerTowingVehicleId.*?applyAttachSync\(\{.*?\}, true\)') "Wrecker driver entry must force a rebuild only for reciprocal persisted wrecker state."
|
||||||
|
Assert-True ($sync -match 'Events\.OnEnterVehicle\.Add\(forceReattachForDriver\)') "Entering the wrecker as driver must force one rigid reattach."
|
||||||
|
Assert-True ($sync -notmatch 'Events\.OnTick\.Add') "Wrecker entry repair must remain event-driven without a retry loop."
|
||||||
Assert-True ($sync -match '(?s)local appliedLevel = Sync\.appliedLevels\[key\].*?appliedLevel == canonicalLevel.*?return\s+end.*?breakWreckerPair\(wrecker, target\).*?addPointConstraint') "A native-restored wrecker rope without a locally applied height must be rebuilt through the rigid local constraint path."
|
Assert-True ($sync -match '(?s)local appliedLevel = Sync\.appliedLevels\[key\].*?appliedLevel == canonicalLevel.*?return\s+end.*?breakWreckerPair\(wrecker, target\).*?addPointConstraint') "A native-restored wrecker rope without a locally applied height must be rebuilt through the rigid local constraint path."
|
||||||
Assert-True ($sync -match '(?s)local function clearAppliedLevelForVehicle.*?Sync\.appliedLevels\[key\] = nil.*?Events\.OnSpawnVehicleEnd\.Add\(clearAppliedLevelForVehicle\)') "Vehicle streaming must invalidate cached local wrecker constraints."
|
Assert-True ($sync -match '(?s)local function clearAppliedLevelForVehicle.*?Sync\.appliedLevels\[key\] = nil.*?Events\.OnSpawnVehicleEnd\.Add\(clearAppliedLevelForVehicle\)') "Vehicle streaming must invalidate cached local wrecker constraints."
|
||||||
Assert-True ($sync -match '(?s)if isPairLinked\(wrecker, target\).*?appliedLevel.*?canonicalLevel.*?breakWreckerPair\(wrecker, target\).*?addPointConstraint') "A requested wrecker height change must rebuild the local constraint at the new hook attachment."
|
Assert-True ($sync -match '(?s)if not forceReattach and isPairLinked\(wrecker, target\).*?appliedLevel.*?canonicalLevel.*?breakWreckerPair\(wrecker, target\).*?addPointConstraint') "A requested height or forced driver entry must rebuild the local wrecker constraint."
|
||||||
Assert-True ($sync -match '(?s)local function applyFreeRollingState.*?wreckerOriginalMass.*?wreckerOriginalBrakingForce.*?setMass\(200\).*?setBrakingForce\(0\)') "Attached vehicles must be made free-rolling like trailers."
|
Assert-True ($sync -match '(?s)local function applyFreeRollingState.*?wreckerOriginalMass.*?wreckerOriginalBrakingForce.*?setMass\(200\).*?setBrakingForce\(0\)') "Attached vehicles must be made free-rolling like trailers."
|
||||||
Assert-True ($sync -match '(?s)local function restoreFreeRollingState.*?setMass.*?setBrakingForce.*?wreckerOriginalMass = nil') "Detach must restore the target vehicle's pre-tow mass and braking state."
|
Assert-True ($sync -match '(?s)local function restoreFreeRollingState.*?setMass.*?setBrakingForce.*?wreckerOriginalMass = nil') "Detach must restore the target vehicle's pre-tow mass and braking state."
|
||||||
Assert-True ($sync -match '(?s)local function applyDetachSync.*?if not target then return end.*?if wrecker then breakWreckerPair.*?restoreFreeRollingState\(target\)') "Detach sync must restore a reloaded target even when the former wrecker is unavailable."
|
Assert-True ($sync -match '(?s)local function applyDetachSync.*?if not target then return end.*?if wrecker then breakWreckerPair.*?restoreFreeRollingState\(target\)') "Detach sync must restore a reloaded target even when the former wrecker is unavailable."
|
||||||
|
|||||||
Reference in New Issue
Block a user