42.20 and Towtrucks
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
if not TowBarMod then TowBarMod = {} end
|
||||
TowBarMod.Persistence = TowBarMod.Persistence or {}
|
||||
|
||||
local Persistence = TowBarMod.Persistence
|
||||
local RegistryName = "TowBar.PersistentPairsV2"
|
||||
|
||||
local function getRegistry()
|
||||
if not ModData or not ModData.getOrCreate then return nil end
|
||||
local registry = ModData.getOrCreate(RegistryName)
|
||||
registry.pairs = registry.pairs or {}
|
||||
return registry
|
||||
end
|
||||
|
||||
local function getSqlId(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 makeKey(kind, towingSqlId, towedSqlId)
|
||||
if type(kind) ~= "string" or type(towingSqlId) ~= "number" or type(towedSqlId) ~= "number" then
|
||||
return nil
|
||||
end
|
||||
return kind .. ":" .. tostring(towingSqlId) .. ":" .. tostring(towedSqlId)
|
||||
end
|
||||
|
||||
local function transmit()
|
||||
if isServer and isServer() and ModData.transmit then
|
||||
ModData.transmit(RegistryName)
|
||||
end
|
||||
end
|
||||
|
||||
function Persistence.savePair(kind, towingVehicle, towedVehicle, attachmentA, attachmentB, heightLevel)
|
||||
local towingSqlId = getSqlId(towingVehicle)
|
||||
local towedSqlId = getSqlId(towedVehicle)
|
||||
local key = makeKey(kind, towingSqlId, towedSqlId)
|
||||
local registry = getRegistry()
|
||||
if not key or not registry then return false end
|
||||
local previous = registry.pairs[key]
|
||||
if previous and previous.attachmentA == attachmentA and previous.attachmentB == attachmentB
|
||||
and previous.heightLevel == heightLevel then
|
||||
return true
|
||||
end
|
||||
registry.pairs[key] = {
|
||||
kind = kind,
|
||||
towingSqlId = towingSqlId,
|
||||
towedSqlId = towedSqlId,
|
||||
attachmentA = attachmentA,
|
||||
attachmentB = attachmentB,
|
||||
heightLevel = heightLevel
|
||||
}
|
||||
transmit()
|
||||
return true
|
||||
end
|
||||
|
||||
function Persistence.getPair(kind, towingVehicle, towedVehicle)
|
||||
local key = makeKey(kind, getSqlId(towingVehicle), getSqlId(towedVehicle))
|
||||
local registry = getRegistry()
|
||||
return key and registry and registry.pairs[key] or nil
|
||||
end
|
||||
|
||||
function Persistence.removePair(kind, towingVehicle, towedVehicle)
|
||||
local key = makeKey(kind, getSqlId(towingVehicle), getSqlId(towedVehicle))
|
||||
local registry = getRegistry()
|
||||
if not key or not registry or registry.pairs[key] == nil then return false end
|
||||
registry.pairs[key] = nil
|
||||
transmit()
|
||||
return true
|
||||
end
|
||||
|
||||
function Persistence.forEachPair(kind, callback)
|
||||
local registry = getRegistry()
|
||||
if not registry or not callback then return end
|
||||
for _, record in pairs(registry.pairs) do
|
||||
if type(record) == "table" and record.kind == kind then callback(record) end
|
||||
end
|
||||
end
|
||||
|
||||
function Persistence.notePeerAvailability(state, available)
|
||||
state = state or {}
|
||||
if not available then
|
||||
state.peerMissing = true
|
||||
return state
|
||||
end
|
||||
if state.peerMissing then
|
||||
-- A missing vehicle means its cell was not loaded. When both members
|
||||
-- return, treat the absent native constraint as load recovery rather
|
||||
-- than a runtime break of a previously confirmed pair.
|
||||
state.peerMissing = nil
|
||||
state.confirmed = false
|
||||
state.pendingUntil = nil
|
||||
state.unlinkedSince = nil
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
function Persistence.advanceRecoveryState(state, now, linked, occupied, retryMs, breakMs)
|
||||
state = state or {}
|
||||
if linked then
|
||||
state.confirmed = true
|
||||
state.pendingUntil = nil
|
||||
state.unlinkedSince = nil
|
||||
return state, "adopt"
|
||||
end
|
||||
if occupied then
|
||||
state.unlinkedSince = nil
|
||||
return state, "wait"
|
||||
end
|
||||
if state.confirmed then
|
||||
state.unlinkedSince = state.unlinkedSince or now
|
||||
if now - state.unlinkedSince >= breakMs then return state, "break" end
|
||||
return state, "wait"
|
||||
end
|
||||
if not state.pendingUntil or now >= state.pendingUntil then
|
||||
state.pendingUntil = now + retryMs
|
||||
return state, "restore"
|
||||
end
|
||||
return state, "wait"
|
||||
end
|
||||
|
||||
Persistence.getSqlId = getSqlId
|
||||
@@ -0,0 +1,163 @@
|
||||
if not TowBarMod then TowBarMod = {} end
|
||||
TowBarMod.Wrecker = TowBarMod.Wrecker or {}
|
||||
|
||||
local Wrecker = TowBarMod.Wrecker
|
||||
|
||||
Wrecker.MaxAttachDistance = 0.6
|
||||
Wrecker.HeightStep = 0.30
|
||||
Wrecker.MinHeightLevel = 0
|
||||
Wrecker.MaxHeightLevel = 2
|
||||
Wrecker.ScanRadius = 8
|
||||
|
||||
local SupportedWreckers = {
|
||||
-- KI5 '76 Chevrolet K Series (Workshop 3161951724)
|
||||
["Base.76chevyC30CCwrecker"] = true,
|
||||
["Base.76chevyC30SCwrecker"] = true,
|
||||
["Base.76chevyK30CCwrecker"] = true,
|
||||
["Base.76chevyK30SCwrecker"] = true,
|
||||
|
||||
-- KI5 '93 Chevrolet Suburban / Silverado (Workshop 3152529790).
|
||||
-- The mechanic variant inherits the K3500 wrecker template.
|
||||
["Base.93chevySilveradoK3500wrecker"] = true,
|
||||
["Base.93chevySilveradoK3500mechanic"] = true,
|
||||
|
||||
-- KI5 '78 AM General M35 Series Trucks (Workshop 2799152995).
|
||||
["Base.78amgeneralM62"] = true
|
||||
}
|
||||
|
||||
local HeightAttachmentIds = {
|
||||
[0] = "towbarWreckerHookLow",
|
||||
[1] = "towbarWreckerHookMid",
|
||||
[2] = "towbarWreckerHookHigh"
|
||||
}
|
||||
|
||||
local function getScriptFullName(script)
|
||||
if not script then return nil end
|
||||
local ok, fullName = pcall(function() return script:getFullName() end)
|
||||
if ok and fullName then return tostring(fullName) end
|
||||
local name = script:getName()
|
||||
return name and ("Base." .. tostring(name)) or nil
|
||||
end
|
||||
|
||||
function Wrecker.isSupportedWrecker(vehicle)
|
||||
if not vehicle then return false end
|
||||
local script = vehicle:getScript()
|
||||
local fullName = getScriptFullName(script)
|
||||
return SupportedWreckers[fullName] == true
|
||||
and vehicle:attachmentExist("hook")
|
||||
end
|
||||
|
||||
function Wrecker.getHeightAttachmentId(level)
|
||||
return HeightAttachmentIds[Wrecker.normalizeHeightLevel(level)]
|
||||
end
|
||||
|
||||
function Wrecker.normalizeHeightLevel(level)
|
||||
local numeric = tonumber(level) or Wrecker.MinHeightLevel
|
||||
numeric = math.floor(numeric + 0.5)
|
||||
return math.max(Wrecker.MinHeightLevel, math.min(Wrecker.MaxHeightLevel, numeric))
|
||||
end
|
||||
|
||||
function Wrecker.nextHeightLevel(currentLevel, direction)
|
||||
local current = Wrecker.normalizeHeightLevel(currentLevel)
|
||||
local delta = tonumber(direction)
|
||||
if delta ~= -1 and delta ~= 1 then return nil end
|
||||
return math.max(Wrecker.MinHeightLevel, math.min(Wrecker.MaxHeightLevel, current + delta))
|
||||
end
|
||||
|
||||
local function distanceSquared(pointA, pointB)
|
||||
if not pointA or not pointB then return nil end
|
||||
local dx = pointA:x() - pointB:x()
|
||||
local dy = pointA:y() - pointB:y()
|
||||
return dx * dx + dy * dy
|
||||
end
|
||||
|
||||
local function forEachVehicle(collection, callback)
|
||||
if not collection then return end
|
||||
if type(collection) == "table" then
|
||||
for _, vehicle in ipairs(collection) do callback(vehicle) end
|
||||
return
|
||||
end
|
||||
|
||||
local ok, iterator = pcall(function() return collection:iterator() end)
|
||||
if ok and iterator then
|
||||
while iterator:hasNext() do callback(iterator:next()) end
|
||||
return
|
||||
end
|
||||
|
||||
local sizeOk, size = pcall(function() return collection:size() end)
|
||||
if not sizeOk then return end
|
||||
for index = 0, size - 1 do callback(collection:get(index)) end
|
||||
end
|
||||
|
||||
local function isTargetEligible(wrecker, vehicle)
|
||||
if not vehicle or vehicle == wrecker then return false end
|
||||
if vehicle:getVehicleTowing() or vehicle:getVehicleTowedBy() then return false end
|
||||
if wrecker:getVehicleTowing() or wrecker:getVehicleTowedBy() then return false end
|
||||
local script = vehicle:getScript()
|
||||
if not script then return false end
|
||||
local name = string.lower(tostring(script:getName() or ""))
|
||||
return not string.find(name, "trailer", 1, true)
|
||||
end
|
||||
|
||||
function Wrecker.resolveNearestTarget(wrecker, candidates)
|
||||
if not Wrecker.isSupportedWrecker(wrecker) then return nil end
|
||||
local hookPoint = wrecker:getAttachmentWorldPos("hook", Vector3f.new())
|
||||
if not hookPoint then return nil end
|
||||
|
||||
local best
|
||||
local maxDistanceSquared = Wrecker.MaxAttachDistance * Wrecker.MaxAttachDistance
|
||||
forEachVehicle(candidates, function(vehicle)
|
||||
if not isTargetEligible(wrecker, vehicle) then return end
|
||||
for _, attachmentId in ipairs({ "trailerfront", "trailer" }) do
|
||||
if vehicle:attachmentExist(attachmentId) then
|
||||
local targetPoint = vehicle:getAttachmentWorldPos(attachmentId, Vector3f.new())
|
||||
local score = distanceSquared(hookPoint, targetPoint)
|
||||
if score and score <= maxDistanceSquared then
|
||||
local vehicleId = vehicle:getId()
|
||||
if not best or score < best.distanceSquared
|
||||
or (score == best.distanceSquared and vehicleId < best.vehicle:getId()) then
|
||||
best = {
|
||||
vehicle = vehicle,
|
||||
attachment = attachmentId,
|
||||
distanceSquared = score
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
return best
|
||||
end
|
||||
|
||||
function Wrecker.getWorldVehicles()
|
||||
local cell = getCell()
|
||||
return cell and cell:getVehicles() or nil
|
||||
end
|
||||
|
||||
local function registerHookAttachments()
|
||||
local manager = getScriptManager()
|
||||
if not manager then return end
|
||||
for fullName in pairs(SupportedWreckers) do
|
||||
local script = manager:getVehicle(fullName)
|
||||
local baseAttachment = script and script:getAttachmentById("hook") or nil
|
||||
local baseOffset = baseAttachment and baseAttachment:getOffset() or nil
|
||||
if baseOffset then
|
||||
for level = Wrecker.MinHeightLevel, Wrecker.MaxHeightLevel do
|
||||
local attachmentId = HeightAttachmentIds[level]
|
||||
if script:getAttachmentById(attachmentId) == nil then
|
||||
local attachment = ModelAttachment.new(attachmentId)
|
||||
attachment:getOffset():set(
|
||||
baseOffset:x(),
|
||||
baseOffset:y() + level * Wrecker.HeightStep,
|
||||
baseOffset:z()
|
||||
)
|
||||
attachment:setUpdateConstraint(false)
|
||||
script:addAttachment(attachment)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Wrecker.registerHookAttachments = registerHookAttachments
|
||||
Events.OnGameBoot.Add(registerHookAttachments)
|
||||
Reference in New Issue
Block a user