42.20 and Towtrucks
This commit is contained in:
@@ -6,5 +6,3 @@ TowBarMod.Config.rigidTowbarDistance = 1.0
|
||||
TowBarMod.Config.towAttachmentExteriorPadding = 0.25
|
||||
TowBarMod.Config.towedVehicleRollingMass = 75
|
||||
TowBarMod.Config.devMode = false
|
||||
TowBarMod.Config.vanillaTowbarModelScaleMin = 1.5
|
||||
TowBarMod.Config.vanillaTowbarModelScaleMax = 2.0
|
||||
|
||||
@@ -4,6 +4,23 @@ if not TowBarMod then TowBarMod = {} end
|
||||
TowBarMod.Sync = TowBarMod.Sync or {}
|
||||
if TowBarMod.Sync._towSyncClientLoaded then return end
|
||||
TowBarMod.Sync._towSyncClientLoaded = true
|
||||
TowBarMod.Sync.appliedPairs = TowBarMod.Sync.appliedPairs or {}
|
||||
|
||||
local function pairKeyContainsVehicle(key, vehicleId)
|
||||
local id = tostring(vehicleId)
|
||||
return string.sub(key, 1, #id + 1) == id .. ":"
|
||||
or string.sub(key, -(#id + 1)) == ":" .. id
|
||||
end
|
||||
|
||||
local function clearAppliedPairForVehicle(vehicle)
|
||||
if not vehicle then return end
|
||||
local vehicleId = vehicle:getId()
|
||||
for key in pairs(TowBarMod.Sync.appliedPairs) do
|
||||
if pairKeyContainsVehicle(key, vehicleId) then
|
||||
TowBarMod.Sync.appliedPairs[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function resolveVehicle(id)
|
||||
if not id then return nil end
|
||||
@@ -89,6 +106,8 @@ local function reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
|
||||
end
|
||||
end
|
||||
|
||||
local breakTowBarPair
|
||||
|
||||
local function applyAttachSync(args)
|
||||
if not args then return end
|
||||
|
||||
@@ -102,8 +121,25 @@ local function applyAttachSync(args)
|
||||
return
|
||||
end
|
||||
|
||||
if not isLinked(vehicleA, vehicleB) then
|
||||
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB)
|
||||
local key = tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())
|
||||
if TowBarMod.Sync.appliedPairs[key] and not isLinked(vehicleA, vehicleB) then
|
||||
-- Vehicle streaming can remove the native constraint while the Lua
|
||||
-- module remains loaded. Allow the authoritative recovery sync to
|
||||
-- rebuild local physics for the returning pair.
|
||||
TowBarMod.Sync.appliedPairs[key] = nil
|
||||
end
|
||||
if not TowBarMod.Sync.appliedPairs[key] then
|
||||
-- The server initially creates a normal Build 42 tow relation. It may
|
||||
-- also restore one from the save. Replace that native rope exactly
|
||||
-- once with this mod's rigid fake-trailer constraint.
|
||||
breakTowBarPair(vehicleA, vehicleB)
|
||||
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
|
||||
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicleB, "notTowingA_Trailer")
|
||||
end
|
||||
-- The final true keeps this a local physics rebuild. The server owns
|
||||
-- the persisted logical relation and must not receive a detach/attach race.
|
||||
vehicleA:addPointConstraint(nil, vehicleB, attachmentA, attachmentB, true)
|
||||
TowBarMod.Sync.appliedPairs[key] = true
|
||||
end
|
||||
|
||||
reconcilePairState(vehicleA, vehicleB, attachmentA, attachmentB)
|
||||
@@ -130,18 +166,17 @@ local function hasConflictingTowLink(vehicle, expectedOther)
|
||||
and modData["towBarTowingVehicleId"] ~= expectedOtherId)
|
||||
end
|
||||
|
||||
local function breakTowBarPair(vehicleA, vehicleB)
|
||||
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 vehicleBReferencesA then
|
||||
vehicleB:breakConstraint(true, true)
|
||||
end
|
||||
if vehicleAReferencesB then
|
||||
vehicleA:breakConstraint(true, true)
|
||||
elseif vehicleBReferencesA then
|
||||
vehicleB:breakConstraint(true, true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -154,6 +189,7 @@ local function applyDetachSync(args)
|
||||
return
|
||||
end
|
||||
breakTowBarPair(vehicleA, vehicleB)
|
||||
TowBarMod.Sync.appliedPairs[tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())] = nil
|
||||
|
||||
if TowBarMod.Hook and TowBarMod.Hook.cleanupDetachedTowBar then
|
||||
pcall(TowBarMod.Hook.cleanupDetachedTowBar, vehicleA, vehicleB)
|
||||
@@ -170,4 +206,10 @@ local function onServerCommand(module, command, args)
|
||||
end
|
||||
end
|
||||
|
||||
TowBarMod.Sync.applyAttachSync = applyAttachSync
|
||||
TowBarMod.Sync.applyDetachSync = applyDetachSync
|
||||
|
||||
Events.OnServerCommand.Add(onServerCommand)
|
||||
if Events.OnSpawnVehicleEnd then
|
||||
Events.OnSpawnVehicleEnd.Add(clearAppliedPairForVehicle)
|
||||
end
|
||||
|
||||
@@ -128,61 +128,54 @@ local function sendTowAttachCommand(playerObj, args)
|
||||
end
|
||||
|
||||
local TowbarVariantSize = 24
|
||||
local TowbarNormalStart = 0
|
||||
local TowbarLargeStart = 24
|
||||
local TowbarMaxIndex = TowbarVariantSize - 1
|
||||
local VanillaScaleMin = 1.5
|
||||
local VanillaScaleMax = 2.0
|
||||
local TowbarFirstZ = 1.0
|
||||
local TowbarSlotStep = 0.1
|
||||
-- Measured from Towbar.fbx at its original 0.01 script scale. The visual is
|
||||
-- enlarged uniformly, so its center must move outward by the scaled half-length
|
||||
-- to keep the inner end on the same vehicle-hitbox contact point.
|
||||
local TowbarVisualScale = 2.5
|
||||
local TowbarModelLength = 0.9714089036
|
||||
local TowbarScaledModelLength = TowbarModelLength * TowbarVisualScale
|
||||
local TowbarModelHalfLength = TowbarScaledModelLength / 2
|
||||
|
||||
local function getVehicleModelScale(script)
|
||||
local function getTowbarFrontEdgeZ(script)
|
||||
if not script then return nil end
|
||||
|
||||
local ok, result = pcall(function()
|
||||
return script:getModelScale()
|
||||
local ok, shape = pcall(function()
|
||||
return script:getPhysicsChassisShape()
|
||||
end)
|
||||
if ok and type(result) == "number" then
|
||||
return result
|
||||
end
|
||||
if not ok or not shape then return nil end
|
||||
|
||||
ok, result = pcall(function()
|
||||
local model = script:getModel()
|
||||
if model then
|
||||
return model:getScale()
|
||||
end
|
||||
local shapeOk, shapeZ = pcall(function()
|
||||
return shape:z()
|
||||
end)
|
||||
if not shapeOk or type(shapeZ) ~= "number" or shapeZ <= 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local centerOk, center = pcall(function()
|
||||
return script:getCenterOfMassOffset()
|
||||
end)
|
||||
if ok and type(result) == "number" then
|
||||
return result
|
||||
end
|
||||
if not centerOk or not center then return nil end
|
||||
|
||||
return nil
|
||||
end
|
||||
local zOk, centerZ = pcall(function()
|
||||
return center:z()
|
||||
end)
|
||||
if not zOk or type(centerZ) ~= "number" then 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)
|
||||
local minScale = configuredMin or VanillaScaleMin
|
||||
local maxScale = configuredMax or VanillaScaleMax
|
||||
return modelScale >= minScale and modelScale <= maxScale
|
||||
end
|
||||
|
||||
local function getTowbarIndexVanilla(script)
|
||||
local z = script:getPhysicsChassisShape():z() / 2 - 0.1
|
||||
local index = math.floor((z * 2 / 3 - 1) * 10)
|
||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||
return centerZ + shapeZ / 2
|
||||
end
|
||||
|
||||
local function getTowbarModelSlot(script)
|
||||
if not isVanillaScale(script) then
|
||||
-- KI5/small-scale vehicles use the normal towbar0 mesh at Z=1.
|
||||
return 0
|
||||
end
|
||||
return getTowbarIndexVanilla(script)
|
||||
local frontEdgeZ = getTowbarFrontEdgeZ(script)
|
||||
if frontEdgeZ == nil then return nil end
|
||||
|
||||
-- Model offsets position the mesh origin. Move its center outward so the
|
||||
-- towbar's inner end, rather than its center, meets the vehicle hitbox.
|
||||
local modelCenterZ = frontEdgeZ + TowbarModelHalfLength
|
||||
local index = math.floor(((modelCenterZ - TowbarFirstZ) / TowbarSlotStep) + 0.5)
|
||||
return math.max(0, math.min(TowbarMaxIndex, index))
|
||||
end
|
||||
|
||||
local function setTowBarModelVisible(vehicle, isVisible)
|
||||
@@ -210,7 +203,7 @@ local function setTowBarModelVisible(vehicle, isVisible)
|
||||
|
||||
local index = getTowbarModelSlot(script)
|
||||
local part = normalPart
|
||||
if part then
|
||||
if part and index ~= nil then
|
||||
part:setModelVisible("towbar" .. index, true)
|
||||
end
|
||||
|
||||
@@ -409,14 +402,10 @@ local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft)
|
||||
|
||||
if towingVehicle then
|
||||
-- Apply rigid spacing as soon as the tow link exists to avoid a visible
|
||||
-- bumper-to-bumper snap while waiting for reattach recovery.
|
||||
-- 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)
|
||||
end
|
||||
|
||||
if localPlayer and towingVehicle then
|
||||
if reattachTowBarPairAfterCleanDetach(localPlayer, towingVehicle, vehicle, false) then
|
||||
return
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if localPlayer and retries > 0 then
|
||||
@@ -425,9 +414,8 @@ local function recoverTowBarVehicleAfterLoad(playerObj, vehicle, retriesLeft)
|
||||
return
|
||||
end
|
||||
|
||||
-- Fallback: keep original post-attach restoration behavior.
|
||||
setTowBarModelVisible(vehicle, true)
|
||||
TowBarMod.Hook.setVehiclePostAttach(nil, vehicle)
|
||||
-- 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)
|
||||
@@ -514,8 +502,6 @@ end
|
||||
function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
|
||||
if towingVehicle == nil or towedVehicle == nil then return end
|
||||
|
||||
TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
|
||||
|
||||
local towingModData = towingVehicle:getModData()
|
||||
local towedModData = towedVehicle:getModData()
|
||||
if not towingModData or not towedModData then return end
|
||||
@@ -523,17 +509,25 @@ function TowBarMod.Hook.cleanupDetachedTowBar(towingVehicle, towedVehicle)
|
||||
if towedModData.towBarOriginalScriptName then
|
||||
TowBarMod.Hook.setVehicleScriptWithTowBarHidden(towedVehicle, towedModData.towBarOriginalScriptName)
|
||||
end
|
||||
-- A very fast break can happen while the temporary fake-trailer script is
|
||||
-- still active. Restore the real script first so the saved offsets are
|
||||
-- written back to the actual vehicle attachments, not the temporary script.
|
||||
TowBarMod.Utils.updateAttachmentsOnDefaultValues(towingVehicle, towedVehicle)
|
||||
restoreFreeRollingTowState(towedVehicle, towedModData)
|
||||
|
||||
towingModData["isTowingByTowBar"] = false
|
||||
towingModData["towed"] = false
|
||||
towingModData["towBarTowedVehicleId"] = nil
|
||||
towingModData["towBarTowedVehicleSqlId"] = nil
|
||||
towingModData["towBarTowingVehicleId"] = nil
|
||||
towingModData["towBarTowingVehicleSqlId"] = nil
|
||||
towingModData["towBarExpectedAttachment"] = nil
|
||||
towedModData["isTowingByTowBar"] = false
|
||||
towedModData["towed"] = false
|
||||
towedModData["towBarTowedVehicleId"] = nil
|
||||
towedModData["towBarTowedVehicleSqlId"] = nil
|
||||
towedModData["towBarTowingVehicleId"] = nil
|
||||
towedModData["towBarTowingVehicleSqlId"] = nil
|
||||
towedModData["towBarExpectedAttachment"] = nil
|
||||
towedModData.towBarOriginalScriptName = nil
|
||||
towedModData.towBarOriginalMass = nil
|
||||
@@ -762,7 +756,6 @@ function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
|
||||
local script = vehicle:getScript()
|
||||
local chassisZ = script and script:getPhysicsChassisShape():z() or 0
|
||||
local halfZ = chassisZ / 2
|
||||
local modelScale = script and getVehicleModelScale(script) or nil
|
||||
local index = 0
|
||||
if script then
|
||||
index = getTowbarModelSlot(script)
|
||||
@@ -770,7 +763,7 @@ function TowBarMod.Hook.devShowAllTowbarModels(playerObj, vehicle)
|
||||
local selectedPart = "towbar"
|
||||
print("[TowBar DEV] Vehicle: " .. tostring(vehicle:getScriptName()))
|
||||
print("[TowBar DEV] chassisShape.z = " .. tostring(chassisZ) .. ", half = " .. tostring(halfZ))
|
||||
print("[TowBar DEV] modelScale = " .. tostring(modelScale) .. ", 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] Showing towbar0..towbar23 on both parts")
|
||||
for j = 0, TowbarVariantSize - 1 do
|
||||
|
||||
@@ -215,7 +215,13 @@ end
|
||||
function ISVehicleMenu.showRadialMenu(playerObj)
|
||||
TowBarMod.UI.defaultShowRadialMenu(playerObj)
|
||||
|
||||
if playerObj:getVehicle() then return end
|
||||
local currentVehicle = playerObj:getVehicle()
|
||||
if currentVehicle then
|
||||
if TowBarMod.WreckerUI then
|
||||
TowBarMod.WreckerUI.addOptionsToMenu(playerObj, currentVehicle)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local vehicle = ISVehicleMenu.getVehicleToInteractWith(playerObj)
|
||||
if vehicle == nil then return end
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
if isServer() then return end
|
||||
if not TowBarMod then TowBarMod = {} end
|
||||
TowBarMod.WreckerSync = TowBarMod.WreckerSync or {}
|
||||
|
||||
local Sync = TowBarMod.WreckerSync
|
||||
Sync.appliedLevels = Sync.appliedLevels or {}
|
||||
|
||||
local function pairKeyContainsVehicle(key, vehicleId)
|
||||
local id = tostring(vehicleId)
|
||||
return string.sub(key, 1, #id + 1) == id .. ":"
|
||||
or string.sub(key, -(#id + 1)) == ":" .. id
|
||||
end
|
||||
|
||||
local function clearAppliedLevelForVehicle(vehicle)
|
||||
if not vehicle then return end
|
||||
local vehicleId = vehicle:getId()
|
||||
for key in pairs(Sync.appliedLevels) do
|
||||
if pairKeyContainsVehicle(key, vehicleId) then
|
||||
Sync.appliedLevels[key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function pairKey(wrecker, target)
|
||||
return tostring(wrecker:getId()) .. ":" .. tostring(target:getId())
|
||||
end
|
||||
|
||||
local function callVehicle(vehicle, methodName, value)
|
||||
local method = vehicle and vehicle[methodName] or nil
|
||||
if not method then return false, nil end
|
||||
return pcall(function()
|
||||
if value ~= nil then return method(vehicle, value) end
|
||||
return method(vehicle)
|
||||
end)
|
||||
end
|
||||
|
||||
local function applyFreeRollingState(vehicle)
|
||||
local md = vehicle and vehicle:getModData() or nil
|
||||
if not md then return end
|
||||
local capturedOriginal = false
|
||||
if md.wreckerOriginalMass == nil then
|
||||
md.wreckerOriginalMass = vehicle:getMass()
|
||||
capturedOriginal = true
|
||||
end
|
||||
if md.wreckerOriginalBrakingForce == nil then
|
||||
md.wreckerOriginalBrakingForce = vehicle:getBrakingForce()
|
||||
capturedOriginal = true
|
||||
end
|
||||
if md.wreckerOriginalParkingBrakeOn == nil then
|
||||
local ok, value = callVehicle(vehicle, "isParkingBrakeOn")
|
||||
if ok then
|
||||
md.wreckerOriginalParkingBrakeOn = value
|
||||
capturedOriginal = true
|
||||
end
|
||||
end
|
||||
if md.wreckerOriginalParkingBrake == nil then
|
||||
local ok, value = callVehicle(vehicle, "getParkingBrake")
|
||||
if ok then
|
||||
md.wreckerOriginalParkingBrake = value
|
||||
capturedOriginal = true
|
||||
end
|
||||
end
|
||||
if md.wreckerOriginalHandbrake == nil then
|
||||
local ok, value = callVehicle(vehicle, "isHandbrake")
|
||||
if ok then
|
||||
md.wreckerOriginalHandbrake = value
|
||||
capturedOriginal = true
|
||||
end
|
||||
end
|
||||
vehicle:setMass(200)
|
||||
vehicle:setBrakingForce(0)
|
||||
if md.wreckerOriginalParkingBrakeOn ~= nil then callVehicle(vehicle, "setParkingBrakeOn", false) end
|
||||
if md.wreckerOriginalParkingBrake ~= nil then callVehicle(vehicle, "setParkingBrake", false) end
|
||||
if md.wreckerOriginalHandbrake ~= nil then callVehicle(vehicle, "setHandbrake", false) end
|
||||
if capturedOriginal then vehicle:transmitModData() end
|
||||
end
|
||||
|
||||
local function restoreFreeRollingState(vehicle)
|
||||
local md = vehicle and vehicle:getModData() or nil
|
||||
if not md then return end
|
||||
if md.wreckerOriginalMass ~= nil then vehicle:setMass(md.wreckerOriginalMass) end
|
||||
if md.wreckerOriginalBrakingForce ~= nil then vehicle:setBrakingForce(md.wreckerOriginalBrakingForce) end
|
||||
if md.wreckerOriginalParkingBrakeOn ~= nil then
|
||||
callVehicle(vehicle, "setParkingBrakeOn", md.wreckerOriginalParkingBrakeOn)
|
||||
end
|
||||
if md.wreckerOriginalParkingBrake ~= nil then
|
||||
callVehicle(vehicle, "setParkingBrake", md.wreckerOriginalParkingBrake)
|
||||
end
|
||||
if md.wreckerOriginalHandbrake ~= nil then
|
||||
callVehicle(vehicle, "setHandbrake", md.wreckerOriginalHandbrake)
|
||||
end
|
||||
md.wreckerOriginalMass = nil
|
||||
md.wreckerOriginalBrakingForce = nil
|
||||
md.wreckerOriginalParkingBrakeOn = nil
|
||||
md.wreckerOriginalParkingBrake = nil
|
||||
md.wreckerOriginalHandbrake = nil
|
||||
vehicle:transmitModData()
|
||||
end
|
||||
|
||||
local function isPairLinked(wrecker, target)
|
||||
return wrecker and target
|
||||
and wrecker:getVehicleTowing() == target
|
||||
and target:getVehicleTowedBy() == wrecker
|
||||
end
|
||||
|
||||
local function hasConflictingLink(vehicle, other)
|
||||
return vehicle and ((vehicle:getVehicleTowing() and vehicle:getVehicleTowing() ~= other)
|
||||
or (vehicle:getVehicleTowedBy() and vehicle:getVehicleTowedBy() ~= other))
|
||||
end
|
||||
|
||||
local function breakWreckerPair(wrecker, target)
|
||||
if not wrecker or not target then return end
|
||||
if isPairLinked(wrecker, target) then
|
||||
wrecker:breakConstraint(true, true)
|
||||
end
|
||||
end
|
||||
|
||||
local function setScriptSafely(vehicle, scriptName)
|
||||
if TowBarMod.Hook and TowBarMod.Hook.setVehicleScriptWithTowBarHidden then
|
||||
return TowBarMod.Hook.setVehicleScriptWithTowBarHidden(vehicle, scriptName)
|
||||
end
|
||||
vehicle:setScriptName(scriptName)
|
||||
return true
|
||||
end
|
||||
|
||||
local function applyAttachSync(args)
|
||||
if not args then return end
|
||||
local wrecker = getVehicleById(args.wrecker)
|
||||
local target = getVehicleById(args.target)
|
||||
if not wrecker or not target then return end
|
||||
if hasConflictingLink(wrecker, target) or hasConflictingLink(target, wrecker) then return end
|
||||
|
||||
local hookAttachment = TowBarMod.Wrecker.getHeightAttachmentId(args.heightLevel)
|
||||
local targetAttachment = args.targetAttachment
|
||||
if not hookAttachment or (targetAttachment ~= "trailer" and targetAttachment ~= "trailerfront") then return end
|
||||
if not wrecker:attachmentExist(hookAttachment) or not target:attachmentExist(targetAttachment) then return end
|
||||
|
||||
local key = pairKey(wrecker, target)
|
||||
local canonicalLevel = tonumber(args.heightLevel) or 0
|
||||
local appliedLevel = Sync.appliedLevels[key]
|
||||
if isPairLinked(wrecker, target) and appliedLevel == canonicalLevel then
|
||||
Sync.appliedLevels[key] = canonicalLevel
|
||||
applyFreeRollingState(target)
|
||||
return
|
||||
end
|
||||
|
||||
breakWreckerPair(wrecker, target)
|
||||
local targetMd = target:getModData()
|
||||
local originalScript = targetMd.wreckerOriginalScriptName or target:getScriptName()
|
||||
targetMd.wreckerOriginalScriptName = originalScript
|
||||
applyFreeRollingState(target)
|
||||
|
||||
setScriptSafely(target, "notTowingA_Trailer")
|
||||
wrecker:addPointConstraint(nil, target, hookAttachment, targetAttachment, true)
|
||||
setScriptSafely(target, originalScript)
|
||||
|
||||
local wreckerMd = wrecker:getModData()
|
||||
wreckerMd.wreckerTowActive = true
|
||||
wreckerMd.wreckerTowedVehicleId = target:getId()
|
||||
wreckerMd.wreckerTowedVehicleSqlId = tonumber(args.targetSqlId)
|
||||
wreckerMd.wreckerTargetAttachment = targetAttachment
|
||||
wreckerMd.wreckerHeightLevel = canonicalLevel
|
||||
targetMd.wreckerTowingVehicleId = wrecker:getId()
|
||||
targetMd.wreckerTowingVehicleSqlId = tonumber(args.wreckerSqlId)
|
||||
Sync.appliedLevels[key] = canonicalLevel
|
||||
end
|
||||
|
||||
local function applyDetachSync(args)
|
||||
if not args then return end
|
||||
local wrecker = getVehicleById(args.wrecker)
|
||||
local target = getVehicleById(args.target)
|
||||
if not target then return end
|
||||
local targetMd = target:getModData()
|
||||
local expectedWreckerId = tonumber(targetMd.wreckerTowingVehicleId)
|
||||
local expectedWreckerSqlId = tonumber(targetMd.wreckerTowingVehicleSqlId)
|
||||
local commandWreckerSqlId = tonumber(args.wreckerSqlId)
|
||||
if expectedWreckerSqlId and commandWreckerSqlId then
|
||||
if expectedWreckerSqlId ~= commandWreckerSqlId then return end
|
||||
elseif expectedWreckerId and expectedWreckerId ~= tonumber(args.wrecker) then
|
||||
return
|
||||
end
|
||||
if wrecker and (hasConflictingLink(wrecker, target) or hasConflictingLink(target, wrecker)) then return end
|
||||
if wrecker then breakWreckerPair(wrecker, target) end
|
||||
if targetMd.wreckerOriginalScriptName and target:getScriptName() ~= targetMd.wreckerOriginalScriptName then
|
||||
setScriptSafely(target, targetMd.wreckerOriginalScriptName)
|
||||
end
|
||||
restoreFreeRollingState(target)
|
||||
if wrecker then Sync.appliedLevels[pairKey(wrecker, target)] = nil end
|
||||
|
||||
if wrecker then
|
||||
local wreckerMd = wrecker:getModData()
|
||||
wreckerMd.wreckerTowActive = nil
|
||||
wreckerMd.wreckerTowedVehicleId = nil
|
||||
wreckerMd.wreckerTowedVehicleSqlId = nil
|
||||
wreckerMd.wreckerTargetAttachment = nil
|
||||
wreckerMd.wreckerHeightLevel = nil
|
||||
end
|
||||
targetMd.wreckerTowingVehicleId = nil
|
||||
targetMd.wreckerTowingVehicleSqlId = nil
|
||||
targetMd.wreckerOriginalScriptName = nil
|
||||
end
|
||||
|
||||
Sync.applyAttachSync = applyAttachSync
|
||||
Sync.applyDetachSync = applyDetachSync
|
||||
|
||||
Events.OnServerCommand.Add(function(module, command, args)
|
||||
if module ~= "towbar" then return end
|
||||
if command == "wreckerAttachSync" then
|
||||
applyAttachSync(args)
|
||||
elseif command == "wreckerDetachSync" then
|
||||
applyDetachSync(args)
|
||||
end
|
||||
end)
|
||||
if Events.OnSpawnVehicleEnd then
|
||||
Events.OnSpawnVehicleEnd.Add(clearAppliedLevelForVehicle)
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
require("TimedActions/ISBaseTimedAction")
|
||||
|
||||
WreckerTimedAction = ISBaseTimedAction:derive("WreckerTimedAction")
|
||||
|
||||
function WreckerTimedAction:isValid()
|
||||
return self.isValidFunc ~= nil
|
||||
and self.isValidFunc(self.character, self.arg1, self.arg2) == true
|
||||
end
|
||||
|
||||
function WreckerTimedAction:start()
|
||||
end
|
||||
|
||||
function WreckerTimedAction:perform()
|
||||
if self:isValid() and self.performFunc then
|
||||
self.performFunc(self.character, self.arg1, self.arg2)
|
||||
end
|
||||
ISBaseTimedAction.perform(self)
|
||||
end
|
||||
|
||||
function WreckerTimedAction:stop()
|
||||
ISBaseTimedAction.stop(self)
|
||||
end
|
||||
|
||||
function WreckerTimedAction:new(character, time, isValidFunc, performFunc, arg1, arg2)
|
||||
local action = ISBaseTimedAction.new(self, character)
|
||||
action.maxTime = time
|
||||
action.stopOnWalk = false
|
||||
action.stopOnRun = false
|
||||
action.useProgressBar = true
|
||||
action.isValidFunc = isValidFunc
|
||||
action.performFunc = performFunc
|
||||
action.arg1 = arg1
|
||||
action.arg2 = arg2
|
||||
return action
|
||||
end
|
||||
@@ -0,0 +1,109 @@
|
||||
if isServer() then return end
|
||||
require("TowBar/WreckerTimedAction")
|
||||
if not TowBarMod then TowBarMod = {} end
|
||||
TowBarMod.WreckerUI = TowBarMod.WreckerUI or {}
|
||||
|
||||
local UI = TowBarMod.WreckerUI
|
||||
local Wrecker = TowBarMod.Wrecker
|
||||
local AttachDuration = 300
|
||||
local DetachDuration = 200
|
||||
|
||||
local function sendAttachCommand(playerObj, wrecker, target)
|
||||
if not playerObj or not wrecker or not target then return end
|
||||
sendClientCommand(playerObj, "towbar", "attachWrecker", {
|
||||
wrecker = wrecker:getId(),
|
||||
target = target:getId()
|
||||
})
|
||||
end
|
||||
|
||||
local function sendDetachCommand(playerObj, wrecker, target)
|
||||
if not playerObj or not wrecker or not target then return end
|
||||
sendClientCommand(playerObj, "towbar", "detachWrecker", {
|
||||
wrecker = wrecker:getId(),
|
||||
target = target:getId()
|
||||
})
|
||||
end
|
||||
|
||||
local function isAttachStillValid(playerObj, wrecker, target)
|
||||
if not playerObj or not wrecker or not target then return false end
|
||||
if not wrecker:isDriver(playerObj) or not Wrecker.isSupportedWrecker(wrecker) then return false end
|
||||
if wrecker:getVehicleTowing() or wrecker:getVehicleTowedBy()
|
||||
or target:getVehicleTowing() or target:getVehicleTowedBy() then return false end
|
||||
local nearest = Wrecker.resolveNearestTarget(wrecker, Wrecker.getWorldVehicles())
|
||||
return nearest ~= nil and nearest.vehicle == target
|
||||
end
|
||||
|
||||
local function isDetachStillValid(playerObj, wrecker, target)
|
||||
if not playerObj or not wrecker or not target or not wrecker:isDriver(playerObj) then return false end
|
||||
local md = wrecker:getModData()
|
||||
return wrecker:getVehicleTowing() == target
|
||||
or (md and tonumber(md.wreckerTowedVehicleId) == target:getId())
|
||||
end
|
||||
|
||||
local function requestAttach(playerObj, wrecker, target)
|
||||
if not isAttachStillValid(playerObj, wrecker, target) then return end
|
||||
ISTimedActionQueue.add(WreckerTimedAction:new(
|
||||
playerObj, AttachDuration, isAttachStillValid, sendAttachCommand, wrecker, target
|
||||
))
|
||||
end
|
||||
|
||||
local function requestDetach(playerObj, wrecker, target)
|
||||
if not isDetachStillValid(playerObj, wrecker, target) then return end
|
||||
ISTimedActionQueue.add(WreckerTimedAction:new(
|
||||
playerObj, DetachDuration, isDetachStillValid, sendDetachCommand, wrecker, target
|
||||
))
|
||||
end
|
||||
|
||||
local function requestHeight(playerObj, wrecker, direction)
|
||||
if not playerObj or not wrecker then return end
|
||||
sendClientCommand(playerObj, "towbar", "adjustWreckerHeight", {
|
||||
wrecker = wrecker:getId(),
|
||||
direction = direction
|
||||
})
|
||||
end
|
||||
|
||||
function UI.addOptionsToMenu(playerObj, wrecker)
|
||||
if not playerObj or not wrecker or not Wrecker.isSupportedWrecker(wrecker) then return false end
|
||||
if not wrecker:isDriver(playerObj) then return false end
|
||||
|
||||
local menu = getPlayerRadialMenu(playerObj:getPlayerNum())
|
||||
if not menu then return false end
|
||||
|
||||
local md = wrecker:getModData()
|
||||
local targetId = md and tonumber(md.wreckerTowedVehicleId) or nil
|
||||
local target = targetId and getVehicleById(targetId) or wrecker:getVehicleTowing()
|
||||
if target and (target == wrecker:getVehicleTowing() or targetId == target:getId()) then
|
||||
TowBarMod.UI.removeDefaultDetachOption(playerObj)
|
||||
local level = tonumber(md.wreckerHeightLevel) or 0
|
||||
if level < Wrecker.MaxHeightLevel then
|
||||
menu:addSlice(
|
||||
getText("UI_Text_Towing_heightUp"),
|
||||
getTexture("media/textures/arrow_up.png"),
|
||||
requestHeight, playerObj, wrecker, 1
|
||||
)
|
||||
end
|
||||
if level > Wrecker.MinHeightLevel then
|
||||
menu:addSlice(
|
||||
getText("UI_Text_Towing_heightDown"),
|
||||
getTexture("media/textures/arrow_down.png"),
|
||||
requestHeight, playerObj, wrecker, -1
|
||||
)
|
||||
end
|
||||
menu:addSlice(
|
||||
getText("UI_Text_Towing_detachHook", ISVehicleMenu.getVehicleDisplayName(target)),
|
||||
getTexture("media/textures/untow_car_icon.png"),
|
||||
requestDetach, playerObj, wrecker, target
|
||||
)
|
||||
return true
|
||||
end
|
||||
|
||||
if wrecker:getVehicleTowing() or wrecker:getVehicleTowedBy() then return false end
|
||||
local nearest = Wrecker.resolveNearestTarget(wrecker, Wrecker.getWorldVehicles())
|
||||
if not nearest then return false end
|
||||
menu:addSlice(
|
||||
getText("UI_Text_Towing_attachHook", ISVehicleMenu.getVehicleDisplayName(nearest.vehicle)),
|
||||
getTexture("media/textures/tow_car_icon.png"),
|
||||
requestAttach, playerObj, wrecker, nearest.vehicle
|
||||
)
|
||||
return true
|
||||
end
|
||||
Reference in New Issue
Block a user