Loading works
This commit is contained in:
@@ -12,6 +12,13 @@ local MAX_DIST_SQ = 3.25
|
|||||||
local MAX_DZ = 0.9
|
local MAX_DZ = 0.9
|
||||||
local tmpA = Vector3f.new()
|
local tmpA = Vector3f.new()
|
||||||
local tmpB = Vector3f.new()
|
local tmpB = Vector3f.new()
|
||||||
|
local PERSIST_FRONT_SQL_KEY = "landtrainFrontTowSqlId"
|
||||||
|
local PERSIST_FRONT_ID_KEY = "landtrainFrontTowId"
|
||||||
|
local PERSIST_ATTACHMENT_A_KEY = "landtrainFrontTowAttachmentA"
|
||||||
|
local PERSIST_ATTACHMENT_B_KEY = "landtrainFrontTowAttachmentB"
|
||||||
|
local PERSIST_CHAIN_HEAD_SQL_KEY = "landtrainChainHeadSql"
|
||||||
|
local PERSIST_CHAIN_ORDER_KEY = "landtrainChainOrder"
|
||||||
|
local PERSIST_CHAIN_TOKEN_KEY = "landtrainChainToken"
|
||||||
|
|
||||||
local function log(msg)
|
local function log(msg)
|
||||||
print("[Landtrain] " .. tostring(msg))
|
print("[Landtrain] " .. tostring(msg))
|
||||||
@@ -177,12 +184,258 @@ local function refreshAround(vehicle)
|
|||||||
refreshTowBarState(vehicle:getVehicleTowing())
|
refreshTowBarState(vehicle:getVehicleTowing())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function clearPersistedFrontLinkData(rearVehicle)
|
||||||
|
if not rearVehicle then return end
|
||||||
|
local md = rearVehicle:getModData()
|
||||||
|
if not md then return end
|
||||||
|
|
||||||
|
local changed = false
|
||||||
|
if md[PERSIST_FRONT_SQL_KEY] ~= nil then
|
||||||
|
md[PERSIST_FRONT_SQL_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_FRONT_ID_KEY] ~= nil then
|
||||||
|
md[PERSIST_FRONT_ID_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_ATTACHMENT_A_KEY] ~= nil then
|
||||||
|
md[PERSIST_ATTACHMENT_A_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_ATTACHMENT_B_KEY] ~= nil then
|
||||||
|
md[PERSIST_ATTACHMENT_B_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
rearVehicle:transmitModData()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function storePersistedFrontLinkData(frontVehicle, rearVehicle, attachmentA, attachmentB)
|
||||||
|
if not frontVehicle or not rearVehicle then return end
|
||||||
|
local md = rearVehicle:getModData()
|
||||||
|
if not md then return end
|
||||||
|
|
||||||
|
local frontSql = frontVehicle.getSqlId and tonumber(frontVehicle:getSqlId()) or nil
|
||||||
|
local frontId = frontVehicle:getId()
|
||||||
|
local resolvedAttachmentA = attachmentA or "trailer"
|
||||||
|
local resolvedAttachmentB = attachmentB or "trailerfront"
|
||||||
|
local changed = false
|
||||||
|
|
||||||
|
if frontSql and frontSql > 0 then
|
||||||
|
if tonumber(md[PERSIST_FRONT_SQL_KEY]) ~= frontSql then
|
||||||
|
md[PERSIST_FRONT_SQL_KEY] = frontSql
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if tonumber(md[PERSIST_FRONT_ID_KEY]) ~= frontId then
|
||||||
|
md[PERSIST_FRONT_ID_KEY] = frontId
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_ATTACHMENT_A_KEY] ~= resolvedAttachmentA then
|
||||||
|
md[PERSIST_ATTACHMENT_A_KEY] = resolvedAttachmentA
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_ATTACHMENT_B_KEY] ~= resolvedAttachmentB then
|
||||||
|
md[PERSIST_ATTACHMENT_B_KEY] = resolvedAttachmentB
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
rearVehicle:transmitModData()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function storePersistedChainOrderData(vehicle, headSql, orderIndex, chainToken)
|
||||||
|
if not vehicle then return end
|
||||||
|
local md = vehicle:getModData()
|
||||||
|
if not md then return end
|
||||||
|
|
||||||
|
local resolvedHeadSql = headSql and tonumber(headSql) or nil
|
||||||
|
local resolvedOrder = orderIndex and tonumber(orderIndex) or nil
|
||||||
|
local resolvedToken = chainToken and tostring(chainToken) or nil
|
||||||
|
if resolvedToken == "" then resolvedToken = nil end
|
||||||
|
local changed = false
|
||||||
|
|
||||||
|
if resolvedHeadSql and resolvedHeadSql > 0 then
|
||||||
|
if tonumber(md[PERSIST_CHAIN_HEAD_SQL_KEY]) ~= resolvedHeadSql then
|
||||||
|
md[PERSIST_CHAIN_HEAD_SQL_KEY] = resolvedHeadSql
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
elseif md[PERSIST_CHAIN_HEAD_SQL_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_HEAD_SQL_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if resolvedOrder and resolvedOrder > 0 then
|
||||||
|
if tonumber(md[PERSIST_CHAIN_ORDER_KEY]) ~= resolvedOrder then
|
||||||
|
md[PERSIST_CHAIN_ORDER_KEY] = resolvedOrder
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
elseif md[PERSIST_CHAIN_ORDER_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_ORDER_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if resolvedToken then
|
||||||
|
if tostring(md[PERSIST_CHAIN_TOKEN_KEY]) ~= resolvedToken then
|
||||||
|
md[PERSIST_CHAIN_TOKEN_KEY] = resolvedToken
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
elseif md[PERSIST_CHAIN_TOKEN_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_TOKEN_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
vehicle:transmitModData()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clearPersistedChainOrderData(vehicle)
|
||||||
|
if not vehicle then return end
|
||||||
|
local md = vehicle:getModData()
|
||||||
|
if not md then return end
|
||||||
|
|
||||||
|
local changed = false
|
||||||
|
if md[PERSIST_CHAIN_HEAD_SQL_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_HEAD_SQL_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_CHAIN_ORDER_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_ORDER_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_CHAIN_TOKEN_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_TOKEN_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
vehicle:transmitModData()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getLoadedVehicles()
|
||||||
|
local results = {}
|
||||||
|
local cell = getCell()
|
||||||
|
if not cell then return results end
|
||||||
|
local list = cell:getVehicles()
|
||||||
|
if not list then return results end
|
||||||
|
|
||||||
|
for i = 0, list:size() - 1 do
|
||||||
|
local vehicle = list:get(i)
|
||||||
|
if vehicle then
|
||||||
|
table.insert(results, vehicle)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return results
|
||||||
|
end
|
||||||
|
|
||||||
|
local function indexVehiclesBySql(vehicles)
|
||||||
|
local bySql = {}
|
||||||
|
for _, vehicle in ipairs(vehicles) do
|
||||||
|
local sqlId = vehicle and vehicle.getSqlId and tonumber(vehicle:getSqlId()) or nil
|
||||||
|
if sqlId and sqlId > 0 then
|
||||||
|
bySql[sqlId] = vehicle
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return bySql
|
||||||
|
end
|
||||||
|
|
||||||
|
local function hasValidTowLink(vehicle)
|
||||||
|
if not vehicle then return false end
|
||||||
|
local rear = vehicle:getVehicleTowing()
|
||||||
|
if rear and rear:getVehicleTowedBy() == vehicle then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local front = vehicle:getVehicleTowedBy()
|
||||||
|
if front and front:getVehicleTowing() == vehicle then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function snapshotChainFromHead(headVehicle, visitedIds)
|
||||||
|
if not headVehicle then return end
|
||||||
|
|
||||||
|
local headSql = headVehicle.getSqlId and tonumber(headVehicle:getSqlId()) or nil
|
||||||
|
if not (headSql and headSql > 0) then
|
||||||
|
headSql = nil
|
||||||
|
end
|
||||||
|
local headMd = headVehicle:getModData()
|
||||||
|
local chainToken = headMd and tostring(headMd[PERSIST_CHAIN_TOKEN_KEY] or "") or ""
|
||||||
|
if chainToken == "" then
|
||||||
|
if headSql then
|
||||||
|
chainToken = "sql:" .. tostring(headSql)
|
||||||
|
else
|
||||||
|
chainToken = "veh:" .. tostring(headVehicle:getId())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if headVehicle:getVehicleTowedBy() == nil then
|
||||||
|
clearPersistedFrontLinkData(headVehicle)
|
||||||
|
end
|
||||||
|
|
||||||
|
local cursor = headVehicle
|
||||||
|
local order = 1
|
||||||
|
local localVisited = {}
|
||||||
|
while cursor do
|
||||||
|
local cursorId = cursor:getId()
|
||||||
|
if visitedIds[cursorId] or localVisited[cursorId] then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
visitedIds[cursorId] = true
|
||||||
|
localVisited[cursorId] = true
|
||||||
|
storePersistedChainOrderData(cursor, headSql, order, chainToken)
|
||||||
|
|
||||||
|
local rearVehicle = cursor:getVehicleTowing()
|
||||||
|
if rearVehicle and rearVehicle:getVehicleTowedBy() == cursor then
|
||||||
|
local attachmentA, attachmentB = resolvePair(cursor, rearVehicle)
|
||||||
|
storePersistedFrontLinkData(cursor, rearVehicle, attachmentA, attachmentB)
|
||||||
|
end
|
||||||
|
|
||||||
|
cursor = rearVehicle
|
||||||
|
order = order + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function snapshotActiveTowLinks()
|
||||||
|
local vehicles = getLoadedVehicles()
|
||||||
|
if #vehicles == 0 then return end
|
||||||
|
|
||||||
|
local visitedIds = {}
|
||||||
|
|
||||||
|
for _, vehicle in ipairs(vehicles) do
|
||||||
|
local vehicleId = vehicle and vehicle:getId() or nil
|
||||||
|
if vehicleId and not visitedIds[vehicleId] and vehicle:getVehicleTowedBy() == nil then
|
||||||
|
local rearVehicle = vehicle:getVehicleTowing()
|
||||||
|
if rearVehicle and rearVehicle:getVehicleTowedBy() == vehicle then
|
||||||
|
snapshotChainFromHead(vehicle, visitedIds)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, vehicle in ipairs(vehicles) do
|
||||||
|
local vehicleId = vehicle and vehicle:getId() or nil
|
||||||
|
if vehicleId and not visitedIds[vehicleId] and hasValidTowLink(vehicle) then
|
||||||
|
snapshotChainFromHead(vehicle, visitedIds)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function restoreDefaultsIfLeadLost(vehicle)
|
local function restoreDefaultsIfLeadLost(vehicle)
|
||||||
if not vehicle then return end
|
if not vehicle then return end
|
||||||
if vehicle:getVehicleTowedBy() ~= nil then return end
|
if vehicle:getVehicleTowedBy() ~= nil then return end
|
||||||
|
|
||||||
clearChangedOffset(vehicle)
|
clearChangedOffset(vehicle)
|
||||||
restoreTowBarDefaults(vehicle)
|
restoreTowBarDefaults(vehicle)
|
||||||
|
clearPersistedFrontLinkData(vehicle)
|
||||||
|
if vehicle:getVehicleTowing() == nil then
|
||||||
|
clearPersistedChainOrderData(vehicle)
|
||||||
|
end
|
||||||
|
|
||||||
local md = vehicle:getModData()
|
local md = vehicle:getModData()
|
||||||
if md then
|
if md then
|
||||||
@@ -234,24 +487,42 @@ local function markTowBarPair(towingVehicle, towedVehicle)
|
|||||||
local towedMd = towedVehicle and towedVehicle:getModData() or nil
|
local towedMd = towedVehicle and towedVehicle:getModData() or nil
|
||||||
|
|
||||||
if towingMd then
|
if towingMd then
|
||||||
|
local towingChanged = false
|
||||||
|
if towingMd["isTowingByTowBar"] ~= true then
|
||||||
towingMd["isTowingByTowBar"] = true
|
towingMd["isTowingByTowBar"] = true
|
||||||
|
towingChanged = true
|
||||||
|
end
|
||||||
|
if towingChanged then
|
||||||
towingVehicle:transmitModData()
|
towingVehicle:transmitModData()
|
||||||
end
|
end
|
||||||
|
end
|
||||||
if towedMd then
|
if towedMd then
|
||||||
|
local towedChanged = false
|
||||||
local currentScript = towedVehicle and towedVehicle:getScriptName() or nil
|
local currentScript = towedVehicle and towedVehicle:getScriptName() or nil
|
||||||
if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then
|
if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then
|
||||||
towedMd.towBarOriginalScriptName = currentScript
|
towedMd.towBarOriginalScriptName = currentScript
|
||||||
|
towedChanged = true
|
||||||
end
|
end
|
||||||
if towedMd.towBarOriginalMass == nil and towedVehicle then
|
if towedMd.towBarOriginalMass == nil and towedVehicle then
|
||||||
towedMd.towBarOriginalMass = towedVehicle:getMass()
|
towedMd.towBarOriginalMass = towedVehicle:getMass()
|
||||||
|
towedChanged = true
|
||||||
end
|
end
|
||||||
if towedMd.towBarOriginalBrakingForce == nil and towedVehicle then
|
if towedMd.towBarOriginalBrakingForce == nil and towedVehicle then
|
||||||
towedMd.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
|
towedMd.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
|
||||||
|
towedChanged = true
|
||||||
end
|
end
|
||||||
|
if towedMd["isTowingByTowBar"] ~= true then
|
||||||
towedMd["isTowingByTowBar"] = true
|
towedMd["isTowingByTowBar"] = true
|
||||||
|
towedChanged = true
|
||||||
|
end
|
||||||
|
if towedMd["towed"] ~= true then
|
||||||
towedMd["towed"] = true
|
towedMd["towed"] = true
|
||||||
|
towedChanged = true
|
||||||
|
end
|
||||||
|
if towedChanged then
|
||||||
towedVehicle:transmitModData()
|
towedVehicle:transmitModData()
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ensurePairAttached(playerObj, towingVehicle, towedVehicle, preferredA, preferredB, alwaysSend)
|
local function ensurePairAttached(playerObj, towingVehicle, towedVehicle, preferredA, preferredB, alwaysSend)
|
||||||
@@ -278,6 +549,7 @@ local function ensurePairAttached(playerObj, towingVehicle, towedVehicle, prefer
|
|||||||
applyRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
|
applyRigidTow(towingVehicle, towedVehicle, attachmentA, attachmentB)
|
||||||
towedVehicle:setScriptName("notTowingA_Trailer")
|
towedVehicle:setScriptName("notTowingA_Trailer")
|
||||||
markTowBarPair(towingVehicle, towedVehicle)
|
markTowBarPair(towingVehicle, towedVehicle)
|
||||||
|
storePersistedFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB)
|
||||||
|
|
||||||
local linked = isLinkedPair(towingVehicle, towedVehicle)
|
local linked = isLinkedPair(towingVehicle, towedVehicle)
|
||||||
log("ensurePairAttached pair=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle))
|
log("ensurePairAttached pair=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle))
|
||||||
@@ -297,6 +569,7 @@ local function ensurePairAttached(playerObj, towingVehicle, towedVehicle, prefer
|
|||||||
setTowBarModelVisible(towedVehicle, true)
|
setTowBarModelVisible(towedVehicle, true)
|
||||||
refreshAround(towingVehicle)
|
refreshAround(towingVehicle)
|
||||||
refreshAround(towedVehicle)
|
refreshAround(towedVehicle)
|
||||||
|
snapshotActiveTowLinks()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -564,6 +837,7 @@ LT.performDetachWrapper = function(playerObj, towingVehicle, towedVehicle)
|
|||||||
queueAction(playerObj, 12, function(_, v) restoreDefaultsIfLeadLost(v); refreshAround(v) end, resolvedTowed)
|
queueAction(playerObj, 12, function(_, v) restoreDefaultsIfLeadLost(v); refreshAround(v) end, resolvedTowed)
|
||||||
|
|
||||||
setTowBarModelVisible(resolvedTowed, false)
|
setTowBarModelVisible(resolvedTowed, false)
|
||||||
|
snapshotActiveTowLinks()
|
||||||
end
|
end
|
||||||
|
|
||||||
LT.showRadialWrapper = function(playerObj)
|
LT.showRadialWrapper = function(playerObj)
|
||||||
@@ -611,6 +885,319 @@ LT.onDetachTrailerWrapper = function(playerObj, vehicle, ...)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function collectPersistedLinks(vehicles)
|
||||||
|
local links = {}
|
||||||
|
|
||||||
|
for _, rearVehicle in ipairs(vehicles) do
|
||||||
|
local md = rearVehicle and rearVehicle:getModData() or nil
|
||||||
|
local frontSql = md and tonumber(md[PERSIST_FRONT_SQL_KEY]) or nil
|
||||||
|
local headSql = md and tonumber(md[PERSIST_CHAIN_HEAD_SQL_KEY]) or nil
|
||||||
|
local rearOrder = md and tonumber(md[PERSIST_CHAIN_ORDER_KEY]) or nil
|
||||||
|
local chainToken = md and tostring(md[PERSIST_CHAIN_TOKEN_KEY] or "") or ""
|
||||||
|
if not (headSql and headSql > 0) then headSql = nil end
|
||||||
|
if not (rearOrder and rearOrder >= 2) then rearOrder = nil end
|
||||||
|
if chainToken == "" then chainToken = nil end
|
||||||
|
if (frontSql and frontSql > 0) or ((headSql or chainToken) and rearOrder) then
|
||||||
|
local rearSql = rearVehicle.getSqlId and tonumber(rearVehicle:getSqlId()) or nil
|
||||||
|
local link = {
|
||||||
|
rear = rearVehicle,
|
||||||
|
rearSql = rearSql,
|
||||||
|
frontSql = (frontSql and frontSql > 0) and frontSql or nil,
|
||||||
|
headSql = headSql,
|
||||||
|
rearOrder = rearOrder,
|
||||||
|
chainToken = chainToken,
|
||||||
|
attachmentA = md[PERSIST_ATTACHMENT_A_KEY] or "trailer",
|
||||||
|
attachmentB = md[PERSIST_ATTACHMENT_B_KEY] or "trailerfront"
|
||||||
|
}
|
||||||
|
table.insert(links, link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return links
|
||||||
|
end
|
||||||
|
|
||||||
|
local function computePersistedDepth(link, byRearSql, visitedSql)
|
||||||
|
if not link or not link.frontSql then return 0 end
|
||||||
|
visitedSql = visitedSql or {}
|
||||||
|
if visitedSql[link.frontSql] then return 0 end
|
||||||
|
|
||||||
|
local frontLink = byRearSql[link.frontSql]
|
||||||
|
if not frontLink then return 0 end
|
||||||
|
|
||||||
|
visitedSql[link.frontSql] = true
|
||||||
|
return 1 + computePersistedDepth(frontLink, byRearSql, visitedSql)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function filterAmbiguousPersistedLinks(links)
|
||||||
|
if #links <= 1 then return links end
|
||||||
|
|
||||||
|
local frontCounts = {}
|
||||||
|
for _, link in ipairs(links) do
|
||||||
|
if link.frontSql then
|
||||||
|
frontCounts[link.frontSql] = (frontCounts[link.frontSql] or 0) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local filtered = {}
|
||||||
|
local dropped = 0
|
||||||
|
for _, link in ipairs(links) do
|
||||||
|
if link.frontSql and frontCounts[link.frontSql] and frontCounts[link.frontSql] > 1 then
|
||||||
|
dropped = dropped + 1
|
||||||
|
else
|
||||||
|
table.insert(filtered, link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if dropped > 0 then
|
||||||
|
log("load rebuild skipped ambiguous links dropped=" .. tostring(dropped))
|
||||||
|
end
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
end
|
||||||
|
|
||||||
|
local function linkOrderKey(link)
|
||||||
|
if not link then return nil end
|
||||||
|
if link.headSql and link.headSql > 0 then
|
||||||
|
return "sql:" .. tostring(link.headSql)
|
||||||
|
end
|
||||||
|
if link.chainToken and tostring(link.chainToken) ~= "" then
|
||||||
|
return "tok:" .. tostring(link.chainToken)
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function filterConflictingOrderedLinks(links)
|
||||||
|
if #links <= 1 then return links end
|
||||||
|
|
||||||
|
local orderSlots = {}
|
||||||
|
for _, link in ipairs(links) do
|
||||||
|
local orderKey = linkOrderKey(link)
|
||||||
|
if orderKey and link.rearOrder and link.rearOrder >= 2 then
|
||||||
|
local key = orderKey .. ":" .. tostring(link.rearOrder)
|
||||||
|
orderSlots[key] = (orderSlots[key] or 0) + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local filtered = {}
|
||||||
|
local dropped = 0
|
||||||
|
for _, link in ipairs(links) do
|
||||||
|
local orderKey = linkOrderKey(link)
|
||||||
|
if orderKey and link.rearOrder and link.rearOrder >= 2 then
|
||||||
|
local key = orderKey .. ":" .. tostring(link.rearOrder)
|
||||||
|
if (orderSlots[key] or 0) > 1 then
|
||||||
|
dropped = dropped + 1
|
||||||
|
else
|
||||||
|
table.insert(filtered, link)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(filtered, link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if dropped > 0 then
|
||||||
|
log("load rebuild skipped conflicting ordered links dropped=" .. tostring(dropped))
|
||||||
|
end
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
end
|
||||||
|
|
||||||
|
local function indexPersistedLinksByRearSql(links)
|
||||||
|
local byRearSql = {}
|
||||||
|
for _, link in ipairs(links) do
|
||||||
|
if link.rearSql and link.rearSql > 0 then
|
||||||
|
byRearSql[link.rearSql] = link
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return byRearSql
|
||||||
|
end
|
||||||
|
|
||||||
|
local function indexVehiclesByHeadAndOrder(vehicles)
|
||||||
|
local byHead = {}
|
||||||
|
for _, vehicle in ipairs(vehicles) do
|
||||||
|
local md = vehicle and vehicle:getModData() or nil
|
||||||
|
local headSql = md and tonumber(md[PERSIST_CHAIN_HEAD_SQL_KEY]) or nil
|
||||||
|
local order = md and tonumber(md[PERSIST_CHAIN_ORDER_KEY]) or nil
|
||||||
|
local chainToken = md and tostring(md[PERSIST_CHAIN_TOKEN_KEY] or "") or ""
|
||||||
|
if chainToken == "" then chainToken = nil end
|
||||||
|
local key = nil
|
||||||
|
if headSql and headSql > 0 then
|
||||||
|
key = "sql:" .. tostring(headSql)
|
||||||
|
elseif chainToken then
|
||||||
|
key = "tok:" .. tostring(chainToken)
|
||||||
|
end
|
||||||
|
if key and order and order > 0 then
|
||||||
|
byHead[key] = byHead[key] or {}
|
||||||
|
if byHead[key][order] == nil then
|
||||||
|
byHead[key][order] = vehicle
|
||||||
|
else
|
||||||
|
byHead[key][order] = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return byHead
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortPersistedLinks(links, byRearSql)
|
||||||
|
table.sort(links, function(a, b)
|
||||||
|
local orderKeyA = linkOrderKey(a)
|
||||||
|
local orderKeyB = linkOrderKey(b)
|
||||||
|
if orderKeyA and orderKeyB then
|
||||||
|
if orderKeyA ~= orderKeyB then
|
||||||
|
return orderKeyA < orderKeyB
|
||||||
|
end
|
||||||
|
if a.rearOrder and b.rearOrder and a.rearOrder ~= b.rearOrder then
|
||||||
|
return a.rearOrder < b.rearOrder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local depthA = computePersistedDepth(a, byRearSql, {})
|
||||||
|
local depthB = computePersistedDepth(b, byRearSql, {})
|
||||||
|
if depthA == depthB then
|
||||||
|
return vehicleId(a.rear) < vehicleId(b.rear)
|
||||||
|
end
|
||||||
|
return depthA < depthB
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isTowbarPairStateComplete(frontVehicle, rearVehicle)
|
||||||
|
if not isLinkedPair(frontVehicle, rearVehicle) then return false end
|
||||||
|
return rearVehicle:getScriptName() == "notTowingA_Trailer"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function restoreViaAttachWrapper(playerObj, frontVehicle, rearVehicle, attachmentA, attachmentB)
|
||||||
|
if not playerObj or not frontVehicle or not rearVehicle then return false end
|
||||||
|
if not LT.performAttachWrapper then return false end
|
||||||
|
if not TowBarMod or not TowBarMod.Hook or TowBarMod.Hook._ltOriginalPerformAttach == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local ok, err = pcall(LT.performAttachWrapper, playerObj, frontVehicle, rearVehicle, attachmentA, attachmentB)
|
||||||
|
if not ok then
|
||||||
|
log("load rebuild wrapper attach failed pair=" .. tostring(vehicleId(frontVehicle)) .. "->" .. tostring(vehicleId(rearVehicle))
|
||||||
|
.. " err=" .. tostring(err))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function restorePersistedLink(playerObj, link, bySql, byHeadOrder)
|
||||||
|
if not playerObj or not link then return false end
|
||||||
|
|
||||||
|
local rearVehicle = link.rear
|
||||||
|
local frontVehicle = nil
|
||||||
|
if link.frontSql and bySql then
|
||||||
|
frontVehicle = bySql[link.frontSql]
|
||||||
|
end
|
||||||
|
local orderKey = linkOrderKey(link)
|
||||||
|
if not frontVehicle and orderKey and link.rearOrder and link.rearOrder >= 2 and byHeadOrder then
|
||||||
|
local byOrder = byHeadOrder[orderKey]
|
||||||
|
if byOrder then
|
||||||
|
local candidate = byOrder[link.rearOrder - 1]
|
||||||
|
if candidate and candidate ~= false then
|
||||||
|
frontVehicle = candidate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not rearVehicle or not frontVehicle then return false end
|
||||||
|
if rearVehicle == frontVehicle or rearVehicle:getId() == frontVehicle:getId() then return false end
|
||||||
|
|
||||||
|
if isTowbarPairStateComplete(frontVehicle, rearVehicle) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if rearVehicle:getVehicleTowedBy() ~= nil and rearVehicle:getVehicleTowedBy() ~= frontVehicle then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if frontVehicle:getVehicleTowing() ~= nil and frontVehicle:getVehicleTowing() ~= rearVehicle then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if orderKey and link.rearOrder and link.rearOrder >= 2 then
|
||||||
|
local frontMd = frontVehicle and frontVehicle:getModData() or nil
|
||||||
|
local frontHeadSql = frontMd and tonumber(frontMd[PERSIST_CHAIN_HEAD_SQL_KEY]) or nil
|
||||||
|
local frontOrder = frontMd and tonumber(frontMd[PERSIST_CHAIN_ORDER_KEY]) or nil
|
||||||
|
local frontChainToken = frontMd and tostring(frontMd[PERSIST_CHAIN_TOKEN_KEY] or "") or ""
|
||||||
|
if frontChainToken == "" then frontChainToken = nil end
|
||||||
|
local frontOrderKey = nil
|
||||||
|
if frontHeadSql and frontHeadSql > 0 then
|
||||||
|
frontOrderKey = "sql:" .. tostring(frontHeadSql)
|
||||||
|
elseif frontChainToken then
|
||||||
|
frontOrderKey = "tok:" .. tostring(frontChainToken)
|
||||||
|
end
|
||||||
|
if frontOrderKey ~= orderKey or frontOrder ~= (link.rearOrder - 1) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if wouldCreateTowLoop(frontVehicle, rearVehicle) then return false end
|
||||||
|
|
||||||
|
if not isLinkedPair(frontVehicle, rearVehicle) then
|
||||||
|
if not canTowByLandtrain(frontVehicle, rearVehicle, link.attachmentA, link.attachmentB, true) then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if restoreViaAttachWrapper(playerObj, frontVehicle, rearVehicle, link.attachmentA, link.attachmentB) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ensurePairAttached(playerObj, frontVehicle, rearVehicle, link.attachmentA, link.attachmentB, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function runPersistedLoadRebuildPass()
|
||||||
|
if not TowBarMod or not TowBarMod.Hook or TowBarMod.Hook._ltOriginalPerformAttach == nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local playerObj = getPlayer() or getSpecificPlayer(0)
|
||||||
|
if not playerObj then return end
|
||||||
|
|
||||||
|
local vehicles = getLoadedVehicles()
|
||||||
|
if #vehicles == 0 then return end
|
||||||
|
|
||||||
|
local bySql = indexVehiclesBySql(vehicles)
|
||||||
|
local byHeadOrder = indexVehiclesByHeadAndOrder(vehicles)
|
||||||
|
local links = collectPersistedLinks(vehicles)
|
||||||
|
links = filterAmbiguousPersistedLinks(links)
|
||||||
|
links = filterConflictingOrderedLinks(links)
|
||||||
|
if #links == 0 then return end
|
||||||
|
local byRearSql = indexPersistedLinksByRearSql(links)
|
||||||
|
sortPersistedLinks(links, byRearSql)
|
||||||
|
|
||||||
|
local attempted = 0
|
||||||
|
local restored = 0
|
||||||
|
for _, link in ipairs(links) do
|
||||||
|
attempted = attempted + 1
|
||||||
|
if restorePersistedLink(playerObj, link, bySql, byHeadOrder) then
|
||||||
|
restored = restored + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if restored > 0 then
|
||||||
|
log("load rebuild pass restored=" .. tostring(restored) .. "/" .. tostring(attempted))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local loadRebuildWarmupTicks = 0
|
||||||
|
local loadRebuildIntervalTicks = 0
|
||||||
|
|
||||||
|
local function loadRebuildTick()
|
||||||
|
if loadRebuildWarmupTicks > 0 then
|
||||||
|
loadRebuildWarmupTicks = loadRebuildWarmupTicks - 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
loadRebuildIntervalTicks = loadRebuildIntervalTicks + 1
|
||||||
|
if loadRebuildIntervalTicks < 180 then return end
|
||||||
|
loadRebuildIntervalTicks = 0
|
||||||
|
|
||||||
|
runPersistedLoadRebuildPass()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function startLoadRebuildWatch()
|
||||||
|
loadRebuildWarmupTicks = 120
|
||||||
|
loadRebuildIntervalTicks = 0
|
||||||
|
Events.OnTick.Remove(loadRebuildTick)
|
||||||
|
Events.OnTick.Add(loadRebuildTick)
|
||||||
|
end
|
||||||
|
|
||||||
local function ensureTrailerAttachments()
|
local function ensureTrailerAttachments()
|
||||||
local sm = getScriptManager()
|
local sm = getScriptManager()
|
||||||
if sm == nil then return end
|
if sm == nil then return end
|
||||||
@@ -708,6 +1295,7 @@ local function startWatchdog()
|
|||||||
Events.OnTick.Remove(watchdogTick)
|
Events.OnTick.Remove(watchdogTick)
|
||||||
Events.OnTick.Add(watchdogTick)
|
Events.OnTick.Add(watchdogTick)
|
||||||
installPatch()
|
installPatch()
|
||||||
|
startLoadRebuildWatch()
|
||||||
end
|
end
|
||||||
|
|
||||||
Events.OnGameBoot.Add(ensureTrailerAttachments)
|
Events.OnGameBoot.Add(ensureTrailerAttachments)
|
||||||
|
|||||||
@@ -6,12 +6,193 @@ if TowBarMod.Landtrain._towSyncServerLoaded then return end
|
|||||||
TowBarMod.Landtrain._towSyncServerLoaded = true
|
TowBarMod.Landtrain._towSyncServerLoaded = true
|
||||||
|
|
||||||
local SYNC_DELAY_TICKS = 2
|
local SYNC_DELAY_TICKS = 2
|
||||||
|
local SNAPSHOT_INTERVAL_TICKS = 120
|
||||||
local pending = {}
|
local pending = {}
|
||||||
|
local snapshotTickCounter = 0
|
||||||
|
local PERSIST_FRONT_SQL_KEY = "landtrainFrontTowSqlId"
|
||||||
|
local PERSIST_FRONT_ID_KEY = "landtrainFrontTowId"
|
||||||
|
local PERSIST_ATTACHMENT_A_KEY = "landtrainFrontTowAttachmentA"
|
||||||
|
local PERSIST_ATTACHMENT_B_KEY = "landtrainFrontTowAttachmentB"
|
||||||
|
local PERSIST_CHAIN_HEAD_SQL_KEY = "landtrainChainHeadSql"
|
||||||
|
local PERSIST_CHAIN_ORDER_KEY = "landtrainChainOrder"
|
||||||
|
local PERSIST_CHAIN_TOKEN_KEY = "landtrainChainToken"
|
||||||
|
|
||||||
local function log(msg)
|
local function log(msg)
|
||||||
print("[Landtrain][TowSyncServer] " .. tostring(msg))
|
print("[Landtrain][TowSyncServer] " .. tostring(msg))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function storePersistedFrontLinkData(frontVehicle, rearVehicle, attachmentA, attachmentB)
|
||||||
|
if not frontVehicle or not rearVehicle then return end
|
||||||
|
local md = rearVehicle:getModData()
|
||||||
|
if not md then return end
|
||||||
|
|
||||||
|
local frontSql = frontVehicle.getSqlId and tonumber(frontVehicle:getSqlId()) or nil
|
||||||
|
local frontId = frontVehicle:getId()
|
||||||
|
local resolvedAttachmentA = attachmentA or "trailer"
|
||||||
|
local resolvedAttachmentB = attachmentB or "trailerfront"
|
||||||
|
local changed = false
|
||||||
|
|
||||||
|
if frontSql and frontSql > 0 then
|
||||||
|
if tonumber(md[PERSIST_FRONT_SQL_KEY]) ~= frontSql then
|
||||||
|
md[PERSIST_FRONT_SQL_KEY] = frontSql
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if tonumber(md[PERSIST_FRONT_ID_KEY]) ~= frontId then
|
||||||
|
md[PERSIST_FRONT_ID_KEY] = frontId
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_ATTACHMENT_A_KEY] ~= resolvedAttachmentA then
|
||||||
|
md[PERSIST_ATTACHMENT_A_KEY] = resolvedAttachmentA
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_ATTACHMENT_B_KEY] ~= resolvedAttachmentB then
|
||||||
|
md[PERSIST_ATTACHMENT_B_KEY] = resolvedAttachmentB
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
rearVehicle:transmitModData()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function storePersistedChainOrderData(vehicle, headSql, orderIndex, chainToken)
|
||||||
|
if not vehicle then return end
|
||||||
|
local md = vehicle:getModData()
|
||||||
|
if not md then return end
|
||||||
|
|
||||||
|
local resolvedHeadSql = headSql and tonumber(headSql) or nil
|
||||||
|
local resolvedOrder = orderIndex and tonumber(orderIndex) or nil
|
||||||
|
local resolvedToken = chainToken and tostring(chainToken) or nil
|
||||||
|
if resolvedToken == "" then resolvedToken = nil end
|
||||||
|
local changed = false
|
||||||
|
|
||||||
|
if resolvedHeadSql and resolvedHeadSql > 0 then
|
||||||
|
if tonumber(md[PERSIST_CHAIN_HEAD_SQL_KEY]) ~= resolvedHeadSql then
|
||||||
|
md[PERSIST_CHAIN_HEAD_SQL_KEY] = resolvedHeadSql
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
elseif md[PERSIST_CHAIN_HEAD_SQL_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_HEAD_SQL_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if resolvedOrder and resolvedOrder > 0 then
|
||||||
|
if tonumber(md[PERSIST_CHAIN_ORDER_KEY]) ~= resolvedOrder then
|
||||||
|
md[PERSIST_CHAIN_ORDER_KEY] = resolvedOrder
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
elseif md[PERSIST_CHAIN_ORDER_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_ORDER_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if resolvedToken then
|
||||||
|
if tostring(md[PERSIST_CHAIN_TOKEN_KEY]) ~= resolvedToken then
|
||||||
|
md[PERSIST_CHAIN_TOKEN_KEY] = resolvedToken
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
elseif md[PERSIST_CHAIN_TOKEN_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_TOKEN_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
vehicle:transmitModData()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clearPersistedChainOrderData(vehicle)
|
||||||
|
if not vehicle then return end
|
||||||
|
local md = vehicle:getModData()
|
||||||
|
if not md then return end
|
||||||
|
|
||||||
|
local changed = false
|
||||||
|
if md[PERSIST_CHAIN_HEAD_SQL_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_HEAD_SQL_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_CHAIN_ORDER_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_ORDER_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_CHAIN_TOKEN_KEY] ~= nil then
|
||||||
|
md[PERSIST_CHAIN_TOKEN_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
vehicle:transmitModData()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clearPersistedFrontLinkData(rearVehicle)
|
||||||
|
if not rearVehicle then return end
|
||||||
|
local md = rearVehicle:getModData()
|
||||||
|
if not md then return end
|
||||||
|
|
||||||
|
local changed = false
|
||||||
|
if md[PERSIST_FRONT_SQL_KEY] ~= nil then
|
||||||
|
md[PERSIST_FRONT_SQL_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_FRONT_ID_KEY] ~= nil then
|
||||||
|
md[PERSIST_FRONT_ID_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_ATTACHMENT_A_KEY] ~= nil then
|
||||||
|
md[PERSIST_ATTACHMENT_A_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
if md[PERSIST_ATTACHMENT_B_KEY] ~= nil then
|
||||||
|
md[PERSIST_ATTACHMENT_B_KEY] = nil
|
||||||
|
changed = true
|
||||||
|
end
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
rearVehicle:transmitModData()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function resolveDetachPair(vehicleA, vehicleB)
|
||||||
|
local front = vehicleA
|
||||||
|
local rear = vehicleB
|
||||||
|
|
||||||
|
if front and rear then
|
||||||
|
if front:getVehicleTowing() == rear and rear:getVehicleTowedBy() == front then
|
||||||
|
return front, rear
|
||||||
|
end
|
||||||
|
if rear:getVehicleTowing() == front and front:getVehicleTowedBy() == rear then
|
||||||
|
return rear, front
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if front and not rear then
|
||||||
|
local directRear = front:getVehicleTowing()
|
||||||
|
if directRear and directRear:getVehicleTowedBy() == front then
|
||||||
|
return front, directRear
|
||||||
|
end
|
||||||
|
local directFront = front:getVehicleTowedBy()
|
||||||
|
if directFront and directFront:getVehicleTowing() == front then
|
||||||
|
return directFront, front
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if rear and not front then
|
||||||
|
local directFront = rear:getVehicleTowedBy()
|
||||||
|
if directFront and directFront:getVehicleTowing() == rear then
|
||||||
|
return directFront, rear
|
||||||
|
end
|
||||||
|
local directRear = rear:getVehicleTowing()
|
||||||
|
if directRear and directRear:getVehicleTowedBy() == rear then
|
||||||
|
return rear, directRear
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
end
|
||||||
|
|
||||||
local function queueSync(kind, player, args)
|
local function queueSync(kind, player, args)
|
||||||
if not args then return end
|
if not args then return end
|
||||||
table.insert(pending, {
|
table.insert(pending, {
|
||||||
@@ -33,6 +214,19 @@ local function isLinked(vehicleA, vehicleB)
|
|||||||
return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
|
return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function hasValidTowLink(vehicle)
|
||||||
|
if not vehicle then return false end
|
||||||
|
local rear = vehicle:getVehicleTowing()
|
||||||
|
if rear and rear:getVehicleTowedBy() == vehicle then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local front = vehicle:getVehicleTowedBy()
|
||||||
|
if front and front:getVehicleTowing() == vehicle then
|
||||||
|
return true
|
||||||
|
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
|
||||||
@@ -45,6 +239,99 @@ local function resolveAttachmentB(args, vehicleB)
|
|||||||
return "trailerfront"
|
return "trailerfront"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function resolveSnapshotAttachmentPair(vehicleA, vehicleB)
|
||||||
|
local selfA = vehicleA and vehicleA:getTowAttachmentSelf() or nil
|
||||||
|
local selfB = vehicleB and vehicleB:getTowAttachmentSelf() or nil
|
||||||
|
local candidates = {
|
||||||
|
{ selfA, selfB },
|
||||||
|
{ "trailer", "trailerfront" },
|
||||||
|
{ "trailerfront", "trailer" }
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pair in ipairs(candidates) do
|
||||||
|
local attachmentA = pair[1]
|
||||||
|
local attachmentB = pair[2]
|
||||||
|
if attachmentA and attachmentB and attachmentA ~= attachmentB
|
||||||
|
and vehicleHasAttachment(vehicleA, attachmentA) and vehicleHasAttachment(vehicleB, attachmentB) then
|
||||||
|
return attachmentA, attachmentB
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return "trailer", "trailerfront"
|
||||||
|
end
|
||||||
|
|
||||||
|
local function snapshotChainFromHead(headVehicle, visitedIds)
|
||||||
|
if not headVehicle then return end
|
||||||
|
|
||||||
|
local headSql = headVehicle.getSqlId and tonumber(headVehicle:getSqlId()) or nil
|
||||||
|
if not (headSql and headSql > 0) then
|
||||||
|
headSql = nil
|
||||||
|
end
|
||||||
|
local headMd = headVehicle:getModData()
|
||||||
|
local chainToken = headMd and tostring(headMd[PERSIST_CHAIN_TOKEN_KEY] or "") or ""
|
||||||
|
if chainToken == "" then
|
||||||
|
if headSql then
|
||||||
|
chainToken = "sql:" .. tostring(headSql)
|
||||||
|
else
|
||||||
|
chainToken = "veh:" .. tostring(headVehicle:getId())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if headVehicle:getVehicleTowedBy() == nil then
|
||||||
|
clearPersistedFrontLinkData(headVehicle)
|
||||||
|
end
|
||||||
|
|
||||||
|
local cursor = headVehicle
|
||||||
|
local order = 1
|
||||||
|
local localVisited = {}
|
||||||
|
while cursor do
|
||||||
|
local cursorId = cursor:getId()
|
||||||
|
if visitedIds[cursorId] or localVisited[cursorId] then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
visitedIds[cursorId] = true
|
||||||
|
localVisited[cursorId] = true
|
||||||
|
storePersistedChainOrderData(cursor, headSql, order, chainToken)
|
||||||
|
|
||||||
|
local rearVehicle = cursor:getVehicleTowing()
|
||||||
|
if rearVehicle and rearVehicle:getVehicleTowedBy() == cursor then
|
||||||
|
local attachmentA, attachmentB = resolveSnapshotAttachmentPair(cursor, rearVehicle)
|
||||||
|
storePersistedFrontLinkData(cursor, rearVehicle, attachmentA, attachmentB)
|
||||||
|
end
|
||||||
|
|
||||||
|
cursor = rearVehicle
|
||||||
|
order = order + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function snapshotActiveTowLinksServer()
|
||||||
|
local cell = getCell()
|
||||||
|
if not cell then return end
|
||||||
|
local list = cell:getVehicles()
|
||||||
|
if not list then return end
|
||||||
|
|
||||||
|
local visitedIds = {}
|
||||||
|
|
||||||
|
for i = 0, list:size() - 1 do
|
||||||
|
local vehicle = list:get(i)
|
||||||
|
local vehicleId = vehicle and vehicle:getId() or nil
|
||||||
|
if vehicleId and not visitedIds[vehicleId] and vehicle:getVehicleTowedBy() == nil then
|
||||||
|
local rearVehicle = vehicle:getVehicleTowing()
|
||||||
|
if rearVehicle and rearVehicle:getVehicleTowedBy() == vehicle then
|
||||||
|
snapshotChainFromHead(vehicle, visitedIds)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 0, list:size() - 1 do
|
||||||
|
local vehicle = list:get(i)
|
||||||
|
local vehicleId = vehicle and vehicle:getId() or nil
|
||||||
|
if vehicleId and not visitedIds[vehicleId] and hasValidTowLink(vehicle) then
|
||||||
|
snapshotChainFromHead(vehicle, visitedIds)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function processAttach(item)
|
local function processAttach(item)
|
||||||
local args = item.args or {}
|
local args = item.args or {}
|
||||||
local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil
|
local vehicleA = args.vehicleA and getVehicleById(args.vehicleA) or nil
|
||||||
@@ -74,6 +361,9 @@ local function processAttach(item)
|
|||||||
log("attach sync already linked A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()))
|
log("attach sync already linked A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId()))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
storePersistedFrontLinkData(vehicleA, vehicleB, attachmentA, attachmentB)
|
||||||
|
snapshotActiveTowLinksServer()
|
||||||
|
|
||||||
log("attach sync broadcast A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())
|
log("attach sync broadcast A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())
|
||||||
.. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB))
|
.. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB))
|
||||||
sendServerCommand("landtrain", "forceAttachSync", {
|
sendServerCommand("landtrain", "forceAttachSync", {
|
||||||
@@ -88,14 +378,31 @@ local function processDetach(item)
|
|||||||
local args = item.args or {}
|
local args = item.args or {}
|
||||||
local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle
|
local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle
|
||||||
local vehicleBId = args.vehicleB
|
local vehicleBId = args.vehicleB
|
||||||
|
local vehicleA = vehicleAId and getVehicleById(vehicleAId) or nil
|
||||||
|
local vehicleB = vehicleBId and getVehicleById(vehicleBId) or nil
|
||||||
|
local _, resolvedRear = resolveDetachPair(vehicleA, vehicleB)
|
||||||
|
if resolvedRear then
|
||||||
|
clearPersistedFrontLinkData(resolvedRear)
|
||||||
|
clearPersistedChainOrderData(resolvedRear)
|
||||||
|
elseif vehicleB then
|
||||||
|
clearPersistedFrontLinkData(vehicleB)
|
||||||
|
clearPersistedChainOrderData(vehicleB)
|
||||||
|
end
|
||||||
|
|
||||||
sendServerCommand("landtrain", "forceDetachSync", {
|
sendServerCommand("landtrain", "forceDetachSync", {
|
||||||
vehicleA = vehicleAId,
|
vehicleA = vehicleAId,
|
||||||
vehicleB = vehicleBId
|
vehicleB = vehicleBId
|
||||||
})
|
})
|
||||||
|
snapshotActiveTowLinksServer()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function processPending()
|
local function processPending()
|
||||||
|
snapshotTickCounter = snapshotTickCounter + 1
|
||||||
|
if snapshotTickCounter >= SNAPSHOT_INTERVAL_TICKS then
|
||||||
|
snapshotTickCounter = 0
|
||||||
|
snapshotActiveTowLinksServer()
|
||||||
|
end
|
||||||
|
|
||||||
if #pending == 0 then return end
|
if #pending == 0 then return end
|
||||||
|
|
||||||
local remaining = {}
|
local remaining = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user