42.20 and Towtrucks

This commit is contained in:
2026-08-13 18:59:54 -04:00
parent a6ba4877d0
commit e403323017
33 changed files with 2333 additions and 142 deletions
+168 -17
View File
@@ -1,4 +1,5 @@
if isClient() then return end
require("TowBar/Persistence")
local TowingCommands = {}
local Commands = {}
@@ -10,6 +11,9 @@ local pendingSync = {}
local snapshotTickCounter = 0
local brokenPairAuditTickCounter = 0
local confirmedTowPairs = {}
local towPairRuntime = {}
local RestoreRetryMs = 2000
local SustainedBreakMs = 5000
local giveTowBar
TowingCommands.wantNoise = getDebug() or false
@@ -88,6 +92,22 @@ local function isLinked(vehicleA, vehicleB)
return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
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 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 hasAnyTowLink(vehicle)
if not vehicle then return false end
return vehicle:getVehicleTowing() ~= nil or vehicle:getVehicleTowedBy() ~= nil
@@ -95,12 +115,17 @@ end
local function getTowBarPairKey(vehicleA, vehicleB)
if not vehicleA or not vehicleB then return nil end
return tostring(vehicleA:getId()) .. ":" .. tostring(vehicleB:getId())
local vehicleAId = getPersistentVehicleId(vehicleA) or vehicleA:getId()
local vehicleBId = getPersistentVehicleId(vehicleB) or vehicleB:getId()
return tostring(vehicleAId) .. ":" .. tostring(vehicleBId)
end
local function markTowBarPairConfirmed(vehicleA, vehicleB)
local key = getTowBarPairKey(vehicleA, vehicleB)
if key then confirmedTowPairs[key] = true end
if key then
confirmedTowPairs[key] = true
towPairRuntime[key] = { confirmed = true }
end
end
local function isTowBarPairConfirmed(vehicleA, vehicleB)
@@ -110,7 +135,10 @@ end
local function forgetTowBarPair(vehicleA, vehicleB)
local key = getTowBarPairKey(vehicleA, vehicleB)
if key then confirmedTowPairs[key] = nil end
if key then
confirmedTowPairs[key] = nil
towPairRuntime[key] = nil
end
end
local function hasTowBarState(vehicle)
@@ -130,25 +158,35 @@ local function markExpectedTowBarPair(vehicleA, vehicleB, attachmentA, attachmen
towingModData["isTowingByTowBar"] = true
towingModData["towed"] = false
towingModData["towBarTowedVehicleId"] = vehicleB:getId()
towingModData["towBarTowedVehicleSqlId"] = getPersistentVehicleId(vehicleB)
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarTowingVehicleSqlId"] = nil
towingModData["towBarExpectedAttachment"] = attachmentA
towedModData["isTowingByTowBar"] = true
towedModData["towed"] = true
towedModData["towBarTowedVehicleId"] = nil
towedModData["towBarTowedVehicleSqlId"] = nil
towedModData["towBarTowingVehicleId"] = vehicleA:getId()
towedModData["towBarTowingVehicleSqlId"] = getPersistentVehicleId(vehicleA)
towedModData["towBarExpectedAttachment"] = attachmentB
vehicleA:transmitModData()
vehicleB:transmitModData()
TowBarMod.Persistence.savePair("towbar", vehicleA, vehicleB, attachmentA, attachmentB)
end
local function clearExpectedTowBarPair(vehicleA, vehicleB)
if vehicleA and vehicleB then
TowBarMod.Persistence.removePair("towbar", vehicleA, vehicleB)
end
if vehicleA then
local towingModData = vehicleA:getModData()
if towingModData then
towingModData["isTowingByTowBar"] = false
towingModData["towed"] = false
towingModData["towBarTowedVehicleId"] = nil
towingModData["towBarTowedVehicleSqlId"] = nil
towingModData["towBarTowingVehicleId"] = nil
towingModData["towBarTowingVehicleSqlId"] = nil
towingModData["towBarExpectedAttachment"] = nil
vehicleA:transmitModData()
end
@@ -160,7 +198,9 @@ local function clearExpectedTowBarPair(vehicleA, vehicleB)
towedModData["isTowingByTowBar"] = false
towedModData["towed"] = false
towedModData["towBarTowedVehicleId"] = nil
towedModData["towBarTowedVehicleSqlId"] = nil
towedModData["towBarTowingVehicleId"] = nil
towedModData["towBarTowingVehicleSqlId"] = nil
towedModData["towBarExpectedAttachment"] = nil
vehicleB:transmitModData()
end
@@ -174,8 +214,11 @@ local function isExpectedTowBarPair(vehicleA, vehicleB)
local towedModData = vehicleB:getModData()
if not towingModData or not towedModData then return false end
return towingModData["towBarTowedVehicleId"] == vehicleB:getId()
and towedModData["towBarTowingVehicleId"] == vehicleA:getId()
return matchesSavedVehicle(
towingModData["towBarTowedVehicleId"], towingModData["towBarTowedVehicleSqlId"], vehicleB
) and matchesSavedVehicle(
towedModData["towBarTowingVehicleId"], towedModData["towBarTowingVehicleSqlId"], vehicleA
)
end
local function isLegacyTowBarPair(vehicleA, vehicleB)
@@ -295,14 +338,34 @@ local function forEachCollectionItem(collection, callback)
end
end
local function findLoadedVehicle(runtimeId, sqlId)
local stableId = tonumber(sqlId)
if stableId and stableId >= 0 then
local cell = getCell()
local vehicles = cell and cell:getVehicles() or nil
local match
forEachCollectionItem(vehicles, function(vehicle)
if not match and getPersistentVehicleId(vehicle) == stableId then match = vehicle end
end)
return match
end
return runtimeId and getVehicleById(tonumber(runtimeId)) or nil
end
local function broadcastAttach(vehicleA, vehicleB, attachmentA, attachmentB)
if not vehicleA or not vehicleB then return end
sendServerCommand("towbar", "forceAttachSync", {
local args = {
vehicleA = vehicleA:getId(),
vehicleB = vehicleB:getId(),
attachmentA = attachmentA,
attachmentB = attachmentB
})
}
if isServer() then
sendServerCommand("towbar", "forceAttachSync", args)
elseif not isClient() and TowBarMod and TowBarMod.Sync
and TowBarMod.Sync.applyAttachSync then
TowBarMod.Sync.applyAttachSync(args)
end
end
local function broadcastDetach(vehicleAId, vehicleBId)
@@ -320,6 +383,33 @@ local function broadcastSpontaneousDetach(vehicleA, vehicleB)
})
end
local function restorePersistedTowBarPair(towingVehicle, towedVehicle)
if not isExpectedTowBarPair(towingVehicle, towedVehicle)
or hasAnyTowLink(towingVehicle) or hasAnyTowLink(towedVehicle) then
return false
end
local towingModData = towingVehicle:getModData()
local towedModData = towedVehicle:getModData()
local attachmentA = towingModData["towBarExpectedAttachment"] or "trailer"
local attachmentB = towedModData["towBarExpectedAttachment"] or "trailerfront"
if not towingVehicle:attachmentExist(attachmentA)
or not towedVehicle:attachmentExist(attachmentB) then
return false
end
markExpectedTowBarPair(towingVehicle, towedVehicle, attachmentA, attachmentB)
if isServer() then
towingVehicle:addPointConstraint(nil, towedVehicle, attachmentA, attachmentB)
end
broadcastAttach(towingVehicle, towedVehicle, attachmentA, attachmentB)
if isLinked(towingVehicle, towedVehicle) then
markTowBarPairConfirmed(towingVehicle, towedVehicle)
return true
end
return false
end
local function breakTowBarConstraint(towingVehicle, towedVehicle)
if not towingVehicle or not towedVehicle then return end
@@ -400,19 +490,80 @@ local function reconcileBrokenTowBarPairsServer()
local vehicles = cell:getVehicles()
if not vehicles then return end
local loadedBySqlId = {}
forEachCollectionItem(vehicles, function(vehicle)
local sqlId = getPersistentVehicleId(vehicle)
if sqlId then loadedBySqlId[sqlId] = vehicle end
end)
local processed = {}
local function reconcilePair(towingVehicle, towedVehicle, attachmentA, attachmentB)
if not towingVehicle or not towedVehicle then return end
local key = getTowBarPairKey(towingVehicle, towedVehicle)
if not key or processed[key] then return end
processed[key] = true
if not isExpectedTowBarPair(towingVehicle, towedVehicle) then
markExpectedTowBarPair(
towingVehicle, towedVehicle,
attachmentA or "trailer", attachmentB or "trailerfront"
)
end
if not isExpectedTowBarPair(towingVehicle, towedVehicle) then return end
local now = getTimestampMs()
local state = towPairRuntime[key] or {}
local linked = isLinked(towingVehicle, towedVehicle)
local occupied = hasAnyTowLink(towingVehicle) or hasAnyTowLink(towedVehicle)
local action
state, action = TowBarMod.Persistence.advanceRecoveryState(
state, now, linked, occupied, RestoreRetryMs, SustainedBreakMs
)
towPairRuntime[key] = state
if action == "adopt" then
local towingMd = towingVehicle:getModData()
local towedMd = towedVehicle:getModData()
local savedAttachmentA = towingMd["towBarExpectedAttachment"] or attachmentA or "trailer"
local savedAttachmentB = towedMd["towBarExpectedAttachment"] or attachmentB or "trailerfront"
if tonumber(towingMd["towBarTowedVehicleId"]) ~= towedVehicle:getId()
or tonumber(towedMd["towBarTowingVehicleId"]) ~= towingVehicle:getId() then
markExpectedTowBarPair(
towingVehicle, towedVehicle,
savedAttachmentA, savedAttachmentB
)
end
TowBarMod.Persistence.savePair(
"towbar", towingVehicle, towedVehicle, savedAttachmentA, savedAttachmentB
)
confirmedTowPairs[key] = true
elseif action == "break" then
finalizeBrokenTowBarPair(towingVehicle, towedVehicle, "sustained-physical-link-audit")
elseif action == "restore" then
restorePersistedTowBarPair(towingVehicle, towedVehicle)
end
end
TowBarMod.Persistence.forEachPair("towbar", function(record)
local towingVehicle = loadedBySqlId[tonumber(record.towingSqlId)]
local towedVehicle = loadedBySqlId[tonumber(record.towedSqlId)]
local key = tostring(record.towingSqlId) .. ":" .. tostring(record.towedSqlId)
towPairRuntime[key] = TowBarMod.Persistence.notePeerAvailability(
towPairRuntime[key], towingVehicle ~= nil and towedVehicle ~= nil
)
reconcilePair(
towingVehicle,
towedVehicle,
record.attachmentA,
record.attachmentB
)
end)
forEachCollectionItem(vehicles, function(towingVehicle)
local towingModData = towingVehicle and towingVehicle:getModData() or nil
local towedVehicleId = towingModData and towingModData["towBarTowedVehicleId"] or nil
local towedVehicle = towedVehicleId and getVehicleById(towedVehicleId) or nil
if towingVehicle and towedVehicle and isExpectedTowBarPair(towingVehicle, towedVehicle) then
if isLinked(towingVehicle, towedVehicle) then
markTowBarPairConfirmed(towingVehicle, towedVehicle)
elseif isTowBarPairConfirmed(towingVehicle, towedVehicle)
and not hasAnyTowLink(towingVehicle)
and not hasAnyTowLink(towedVehicle) then
finalizeBrokenTowBarPair(towingVehicle, towedVehicle, "physical-link-audit")
end
end
local towedVehicleSqlId = towingModData and towingModData["towBarTowedVehicleSqlId"] or nil
local towedVehicle = findLoadedVehicle(towedVehicleId, towedVehicleSqlId)
if towingVehicle and towedVehicle then reconcilePair(towingVehicle, towedVehicle) end
end)
end