42.20 and Towtrucks
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
if isClient() then return end
|
||||
require("TowBar/Persistence")
|
||||
if not TowBarMod or not TowBarMod.Wrecker then return end
|
||||
|
||||
local Wrecker = TowBarMod.Wrecker
|
||||
local Commands = {}
|
||||
local AuditInterval = 30
|
||||
local SnapshotInterval = 120
|
||||
local CommandCooldownMs = 150
|
||||
local auditTicks = 0
|
||||
local snapshotTicks = 0
|
||||
local commandTimes = {}
|
||||
local confirmedWreckerPairs = {}
|
||||
local wreckerPairRuntime = {}
|
||||
local RestoreRetryMs = 2000
|
||||
local SustainedBreakMs = 5000
|
||||
|
||||
local function isInteger(value)
|
||||
return type(value) == "number" and value == math.floor(value)
|
||||
end
|
||||
|
||||
local function resolveVehicleId(value)
|
||||
if not isInteger(value) or value < 0 then return nil end
|
||||
return getVehicleById(value)
|
||||
end
|
||||
|
||||
local function isLinked(wrecker, target)
|
||||
return wrecker and target
|
||||
and wrecker:getVehicleTowing() == target
|
||||
and target:getVehicleTowedBy() == wrecker
|
||||
end
|
||||
|
||||
local function getPersistentVehicleId(vehicle)
|
||||
if not vehicle or not vehicle.getSqlId then return nil end
|
||||
local ok, sqlId = pcall(function() return vehicle:getSqlId() end)
|
||||
if ok and type(sqlId) == "number" and sqlId >= 0 then return sqlId end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function findLoadedVehicle(runtimeId, sqlId)
|
||||
local stableId = tonumber(sqlId)
|
||||
if stableId and stableId >= 0 then
|
||||
local vehicles = Wrecker.getWorldVehicles()
|
||||
local iterator = vehicles and vehicles:iterator() or nil
|
||||
while iterator and iterator:hasNext() do
|
||||
local vehicle = iterator:next()
|
||||
if getPersistentVehicleId(vehicle) == stableId then return vehicle end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
return runtimeId and getVehicleById(tonumber(runtimeId)) or nil
|
||||
end
|
||||
|
||||
local function matchesSavedVehicle(runtimeId, sqlId, vehicle)
|
||||
if not vehicle then return false end
|
||||
local stableId = tonumber(sqlId)
|
||||
if stableId and stableId >= 0 then
|
||||
return getPersistentVehicleId(vehicle) == stableId
|
||||
end
|
||||
return tonumber(runtimeId) == vehicle:getId()
|
||||
end
|
||||
|
||||
local function pairKey(wrecker, target)
|
||||
local wreckerId = getPersistentVehicleId(wrecker) or wrecker:getId()
|
||||
local targetId = getPersistentVehicleId(target) or target:getId()
|
||||
return tostring(wreckerId) .. ":" .. tostring(targetId)
|
||||
end
|
||||
|
||||
|
||||
local function markWreckerPairConfirmed(wrecker, target)
|
||||
local key = pairKey(wrecker, target)
|
||||
confirmedWreckerPairs[key] = true
|
||||
wreckerPairRuntime[key] = { confirmed = true }
|
||||
end
|
||||
|
||||
local function isWreckerPairConfirmed(wrecker, target)
|
||||
return confirmedWreckerPairs[pairKey(wrecker, target)] == true
|
||||
end
|
||||
|
||||
local function forgetWreckerPair(wrecker, target)
|
||||
local key = pairKey(wrecker, target)
|
||||
confirmedWreckerPairs[key] = nil
|
||||
wreckerPairRuntime[key] = nil
|
||||
end
|
||||
|
||||
local function isExpectedPair(wrecker, target)
|
||||
if not wrecker or not target then return false end
|
||||
local wreckerMd = wrecker:getModData()
|
||||
local targetMd = target:getModData()
|
||||
return matchesSavedVehicle(
|
||||
wreckerMd.wreckerTowedVehicleId, wreckerMd.wreckerTowedVehicleSqlId, target
|
||||
) and matchesSavedVehicle(
|
||||
targetMd.wreckerTowingVehicleId, targetMd.wreckerTowingVehicleSqlId, wrecker
|
||||
)
|
||||
end
|
||||
|
||||
local function setPairState(wrecker, target, targetAttachment, heightLevel)
|
||||
local wreckerMd = wrecker:getModData()
|
||||
local targetMd = target:getModData()
|
||||
wreckerMd.wreckerTowActive = true
|
||||
wreckerMd.wreckerTowedVehicleId = target:getId()
|
||||
wreckerMd.wreckerTowedVehicleSqlId = getPersistentVehicleId(target)
|
||||
wreckerMd.wreckerTowingVehicleId = nil
|
||||
wreckerMd.wreckerTowingVehicleSqlId = nil
|
||||
wreckerMd.wreckerTargetAttachment = targetAttachment
|
||||
wreckerMd.wreckerHeightLevel = Wrecker.normalizeHeightLevel(heightLevel)
|
||||
targetMd.wreckerTowingVehicleId = wrecker:getId()
|
||||
targetMd.wreckerTowingVehicleSqlId = getPersistentVehicleId(wrecker)
|
||||
targetMd.wreckerTowedVehicleId = nil
|
||||
targetMd.wreckerTowedVehicleSqlId = nil
|
||||
wrecker:transmitModData()
|
||||
target:transmitModData()
|
||||
TowBarMod.Persistence.savePair(
|
||||
"wrecker", wrecker, target,
|
||||
Wrecker.getHeightAttachmentId(wreckerMd.wreckerHeightLevel),
|
||||
targetAttachment,
|
||||
wreckerMd.wreckerHeightLevel
|
||||
)
|
||||
end
|
||||
|
||||
local function clearPairState(wrecker, target)
|
||||
if not wrecker or not target then return end
|
||||
local wreckerMd = wrecker:getModData()
|
||||
local targetMd = target:getModData()
|
||||
TowBarMod.Persistence.removePair("wrecker", wrecker, target)
|
||||
wreckerMd.wreckerTowActive = nil
|
||||
wreckerMd.wreckerTowedVehicleId = nil
|
||||
wreckerMd.wreckerTowedVehicleSqlId = nil
|
||||
wreckerMd.wreckerTowingVehicleId = nil
|
||||
wreckerMd.wreckerTowingVehicleSqlId = nil
|
||||
wreckerMd.wreckerTargetAttachment = nil
|
||||
wreckerMd.wreckerHeightLevel = nil
|
||||
targetMd.wreckerTowingVehicleId = nil
|
||||
targetMd.wreckerTowingVehicleSqlId = nil
|
||||
targetMd.wreckerTowedVehicleId = nil
|
||||
targetMd.wreckerTowedVehicleSqlId = nil
|
||||
wrecker:transmitModData()
|
||||
target:transmitModData()
|
||||
end
|
||||
|
||||
local function broadcastWreckerSync(command, args)
|
||||
if isServer() then
|
||||
sendServerCommand("towbar", command, args)
|
||||
elseif TowBarMod.WreckerSync then
|
||||
if command == "wreckerAttachSync" then
|
||||
TowBarMod.WreckerSync.applyAttachSync(args)
|
||||
else
|
||||
TowBarMod.WreckerSync.applyDetachSync(args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function syncPairState(wrecker, target, hookAttachment, targetAttachment, heightLevel, player)
|
||||
if isServer() then
|
||||
-- Dedicated servers own the logical relation; clients replace their
|
||||
-- local rope with the rigid height-indexed constraint on sync.
|
||||
wrecker:addPointConstraint(player, target, hookAttachment, targetAttachment)
|
||||
end
|
||||
broadcastWreckerSync("wreckerAttachSync", {
|
||||
wrecker = wrecker:getId(), target = target:getId(),
|
||||
wreckerSqlId = getPersistentVehicleId(wrecker),
|
||||
targetSqlId = getPersistentVehicleId(target),
|
||||
targetAttachment = targetAttachment, heightLevel = heightLevel
|
||||
})
|
||||
if isLinked(wrecker, target) then markWreckerPairConfirmed(wrecker, target) end
|
||||
end
|
||||
|
||||
local function breakPair(wrecker, target)
|
||||
if isLinked(wrecker, target) then
|
||||
wrecker:breakConstraint(true, false)
|
||||
end
|
||||
end
|
||||
|
||||
local function detachPair(wrecker, target)
|
||||
if not isExpectedPair(wrecker, target) then return false end
|
||||
breakPair(wrecker, target)
|
||||
forgetWreckerPair(wrecker, target)
|
||||
clearPairState(wrecker, target)
|
||||
broadcastWreckerSync("wreckerDetachSync", {
|
||||
wrecker = wrecker:getId(), target = target:getId(),
|
||||
wreckerSqlId = getPersistentVehicleId(wrecker)
|
||||
})
|
||||
return true
|
||||
end
|
||||
|
||||
function Commands.attachWrecker(player, args)
|
||||
if type(args) ~= "table" then return end
|
||||
local wrecker = resolveVehicleId(args.wrecker)
|
||||
local target = resolveVehicleId(args.target)
|
||||
if not wrecker or not target or wrecker == target then return end
|
||||
if not wrecker:isDriver(player) or not Wrecker.isSupportedWrecker(wrecker) then return end
|
||||
if wrecker:getVehicleTowing() or wrecker:getVehicleTowedBy()
|
||||
or target:getVehicleTowing() or target:getVehicleTowedBy() then return end
|
||||
|
||||
local nearest = Wrecker.resolveNearestTarget(wrecker, Wrecker.getWorldVehicles())
|
||||
if not nearest or nearest.vehicle ~= target then return end
|
||||
local targetAttachment = nearest.attachment
|
||||
local heightLevel = 0
|
||||
local hookAttachment = Wrecker.getHeightAttachmentId(heightLevel)
|
||||
if not hookAttachment or not wrecker:attachmentExist(hookAttachment) then return end
|
||||
|
||||
setPairState(wrecker, target, targetAttachment, heightLevel)
|
||||
syncPairState(wrecker, target, hookAttachment, targetAttachment, heightLevel, player)
|
||||
end
|
||||
|
||||
function Commands.detachWrecker(player, args)
|
||||
if type(args) ~= "table" then return end
|
||||
local wrecker = resolveVehicleId(args.wrecker)
|
||||
local target = resolveVehicleId(args.target)
|
||||
if not wrecker or not target or not wrecker:isDriver(player) then return end
|
||||
detachPair(wrecker, target)
|
||||
end
|
||||
|
||||
function Commands.adjustWreckerHeight(player, args)
|
||||
if type(args) ~= "table" then return end
|
||||
local wrecker = resolveVehicleId(args.wrecker)
|
||||
local direction = args.direction
|
||||
if not wrecker or not wrecker:isDriver(player) or not Wrecker.isSupportedWrecker(wrecker) then return end
|
||||
if direction ~= -1 and direction ~= 1 then return end
|
||||
|
||||
local md = wrecker:getModData()
|
||||
local target = findLoadedVehicle(md.wreckerTowedVehicleId, md.wreckerTowedVehicleSqlId)
|
||||
if not target or not isExpectedPair(wrecker, target) or not isLinked(wrecker, target) then return end
|
||||
local currentLevel = tonumber(md.wreckerHeightLevel) or 0
|
||||
local nextLevel = Wrecker.nextHeightLevel(currentLevel, direction)
|
||||
if not nextLevel or nextLevel == currentLevel then return end
|
||||
local hookAttachment = Wrecker.getHeightAttachmentId(nextLevel)
|
||||
local targetAttachment = md.wreckerTargetAttachment
|
||||
if not hookAttachment or not wrecker:attachmentExist(hookAttachment)
|
||||
or (targetAttachment ~= "trailer" and targetAttachment ~= "trailerfront") then return end
|
||||
|
||||
breakPair(wrecker, target)
|
||||
setPairState(wrecker, target, targetAttachment, nextLevel)
|
||||
syncPairState(wrecker, target, hookAttachment, targetAttachment, nextLevel, player)
|
||||
end
|
||||
|
||||
local function onClientCommand(module, command, player, args)
|
||||
if module == "towbar" and Commands[command] then
|
||||
if not player then return end
|
||||
local playerTimes = commandTimes[player]
|
||||
if not playerTimes then
|
||||
playerTimes = {}
|
||||
commandTimes[player] = playerTimes
|
||||
end
|
||||
local now = getTimestampMs()
|
||||
local previous = playerTimes[command]
|
||||
if previous and now - previous < CommandCooldownMs then return end
|
||||
playerTimes[command] = now
|
||||
Commands[command](player, args)
|
||||
return
|
||||
end
|
||||
if module == "vehicle" and command == "detachTrailerSpontaneous" then
|
||||
local vehicle = args and resolveVehicleId(args.vehicle) or nil
|
||||
if not vehicle then return end
|
||||
local md = vehicle:getModData()
|
||||
local wrecker = (md.wreckerTowedVehicleId or md.wreckerTowedVehicleSqlId) and vehicle or nil
|
||||
local target = wrecker and findLoadedVehicle(
|
||||
md.wreckerTowedVehicleId, md.wreckerTowedVehicleSqlId
|
||||
) or nil
|
||||
if not wrecker and (md.wreckerTowingVehicleId or md.wreckerTowingVehicleSqlId) then
|
||||
wrecker = findLoadedVehicle(md.wreckerTowingVehicleId, md.wreckerTowingVehicleSqlId)
|
||||
target = vehicle
|
||||
end
|
||||
if wrecker and target then detachPair(wrecker, target) end
|
||||
end
|
||||
end
|
||||
|
||||
local function auditWreckerPairs()
|
||||
auditTicks = auditTicks + 1
|
||||
if auditTicks < AuditInterval then return end
|
||||
auditTicks = 0
|
||||
snapshotTicks = snapshotTicks + AuditInterval
|
||||
local shouldSnapshot = snapshotTicks >= SnapshotInterval
|
||||
if shouldSnapshot then snapshotTicks = 0 end
|
||||
local vehicles = Wrecker.getWorldVehicles()
|
||||
if not vehicles then return end
|
||||
|
||||
local loadedBySqlId = {}
|
||||
local scan = vehicles:iterator()
|
||||
while scan:hasNext() do
|
||||
local vehicle = scan:next()
|
||||
local sqlId = getPersistentVehicleId(vehicle)
|
||||
if sqlId then loadedBySqlId[sqlId] = vehicle end
|
||||
end
|
||||
|
||||
local processed = {}
|
||||
local function reconcilePair(wrecker, target, targetAttachment, heightLevel)
|
||||
if not wrecker or not target or not Wrecker.isSupportedWrecker(wrecker) then return end
|
||||
local key = pairKey(wrecker, target)
|
||||
if processed[key] then return end
|
||||
processed[key] = true
|
||||
|
||||
local canonicalLevel = Wrecker.normalizeHeightLevel(heightLevel)
|
||||
local canonicalTargetAttachment = targetAttachment
|
||||
if canonicalTargetAttachment ~= "trailer" and canonicalTargetAttachment ~= "trailerfront" then return end
|
||||
local hookAttachment = Wrecker.getHeightAttachmentId(canonicalLevel)
|
||||
if not hookAttachment or not wrecker:attachmentExist(hookAttachment)
|
||||
or not target:attachmentExist(canonicalTargetAttachment) then return end
|
||||
|
||||
if not isExpectedPair(wrecker, target) then
|
||||
setPairState(wrecker, target, canonicalTargetAttachment, canonicalLevel)
|
||||
end
|
||||
if not isExpectedPair(wrecker, target) then return end
|
||||
|
||||
local now = getTimestampMs()
|
||||
local state = wreckerPairRuntime[key] or {}
|
||||
local linked = isLinked(wrecker, target)
|
||||
local occupied = wrecker:getVehicleTowing() or wrecker:getVehicleTowedBy()
|
||||
or target:getVehicleTowing() or target:getVehicleTowedBy()
|
||||
local action
|
||||
state, action = TowBarMod.Persistence.advanceRecoveryState(
|
||||
state, now, linked, occupied ~= nil, RestoreRetryMs, SustainedBreakMs
|
||||
)
|
||||
wreckerPairRuntime[key] = state
|
||||
if action == "adopt" then
|
||||
local wreckerMd = wrecker:getModData()
|
||||
local targetMd = target:getModData()
|
||||
if tonumber(wreckerMd.wreckerTowedVehicleId) ~= target:getId()
|
||||
or tonumber(targetMd.wreckerTowingVehicleId) ~= wrecker:getId()
|
||||
or tonumber(wreckerMd.wreckerTowedVehicleSqlId) ~= getPersistentVehicleId(target)
|
||||
or tonumber(targetMd.wreckerTowingVehicleSqlId) ~= getPersistentVehicleId(wrecker) then
|
||||
setPairState(wrecker, target, canonicalTargetAttachment, canonicalLevel)
|
||||
end
|
||||
TowBarMod.Persistence.savePair(
|
||||
"wrecker", wrecker, target, hookAttachment, canonicalTargetAttachment, canonicalLevel
|
||||
)
|
||||
confirmedWreckerPairs[key] = true
|
||||
if shouldSnapshot then
|
||||
broadcastWreckerSync("wreckerAttachSync", {
|
||||
wrecker = wrecker:getId(), target = target:getId(),
|
||||
wreckerSqlId = getPersistentVehicleId(wrecker),
|
||||
targetSqlId = getPersistentVehicleId(target),
|
||||
targetAttachment = canonicalTargetAttachment,
|
||||
heightLevel = canonicalLevel
|
||||
})
|
||||
end
|
||||
elseif action == "break" then
|
||||
detachPair(wrecker, target)
|
||||
elseif action == "restore" then
|
||||
setPairState(wrecker, target, canonicalTargetAttachment, canonicalLevel)
|
||||
syncPairState(wrecker, target, hookAttachment, canonicalTargetAttachment, canonicalLevel, nil)
|
||||
end
|
||||
end
|
||||
|
||||
TowBarMod.Persistence.forEachPair("wrecker", function(record)
|
||||
local wrecker = loadedBySqlId[tonumber(record.towingSqlId)]
|
||||
local target = loadedBySqlId[tonumber(record.towedSqlId)]
|
||||
local key = tostring(record.towingSqlId) .. ":" .. tostring(record.towedSqlId)
|
||||
wreckerPairRuntime[key] = TowBarMod.Persistence.notePeerAvailability(
|
||||
wreckerPairRuntime[key], wrecker ~= nil and target ~= nil
|
||||
)
|
||||
reconcilePair(
|
||||
wrecker,
|
||||
target,
|
||||
record.attachmentB,
|
||||
record.heightLevel
|
||||
)
|
||||
end)
|
||||
|
||||
local iterator = vehicles:iterator()
|
||||
while iterator:hasNext() do
|
||||
local vehicle = iterator:next()
|
||||
local vehicleMd = vehicle:getModData()
|
||||
local targetId = tonumber(vehicleMd.wreckerTowedVehicleId)
|
||||
local targetSqlId = tonumber(vehicleMd.wreckerTowedVehicleSqlId)
|
||||
if targetId or targetSqlId then
|
||||
local wrecker = vehicle
|
||||
local target = findLoadedVehicle(targetId, targetSqlId)
|
||||
if target then
|
||||
reconcilePair(
|
||||
wrecker, target,
|
||||
vehicleMd.wreckerTargetAttachment,
|
||||
vehicleMd.wreckerHeightLevel
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local towingVehicleId = tonumber(vehicleMd.wreckerTowingVehicleId)
|
||||
local towingVehicleSqlId = tonumber(vehicleMd.wreckerTowingVehicleSqlId)
|
||||
if towingVehicleId or towingVehicleSqlId then
|
||||
local towingVehicle = findLoadedVehicle(towingVehicleId, towingVehicleSqlId)
|
||||
if towingVehicle and not isExpectedPair(towingVehicle, vehicle) then
|
||||
vehicleMd.wreckerTowingVehicleId = nil
|
||||
vehicleMd.wreckerTowingVehicleSqlId = nil
|
||||
vehicle:transmitModData()
|
||||
broadcastWreckerSync("wreckerDetachSync", {
|
||||
wrecker = towingVehicleId,
|
||||
wreckerSqlId = towingVehicleSqlId,
|
||||
target = vehicle:getId()
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Events.OnClientCommand.Add(onClientCommand)
|
||||
Events.OnTick.Add(auditWreckerPairs)
|
||||
Reference in New Issue
Block a user