Loading works
This commit is contained in:
@@ -12,6 +12,13 @@ local MAX_DIST_SQ = 3.25
|
||||
local MAX_DZ = 0.9
|
||||
local tmpA = 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)
|
||||
print("[Landtrain] " .. tostring(msg))
|
||||
@@ -177,12 +184,258 @@ local function refreshAround(vehicle)
|
||||
refreshTowBarState(vehicle:getVehicleTowing())
|
||||
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)
|
||||
if not vehicle then return end
|
||||
if vehicle:getVehicleTowedBy() ~= nil then return end
|
||||
|
||||
clearChangedOffset(vehicle)
|
||||
restoreTowBarDefaults(vehicle)
|
||||
clearPersistedFrontLinkData(vehicle)
|
||||
if vehicle:getVehicleTowing() == nil then
|
||||
clearPersistedChainOrderData(vehicle)
|
||||
end
|
||||
|
||||
local md = vehicle:getModData()
|
||||
if md then
|
||||
@@ -234,24 +487,42 @@ local function markTowBarPair(towingVehicle, towedVehicle)
|
||||
local towedMd = towedVehicle and towedVehicle:getModData() or nil
|
||||
|
||||
if towingMd then
|
||||
local towingChanged = false
|
||||
if towingMd["isTowingByTowBar"] ~= true then
|
||||
towingMd["isTowingByTowBar"] = true
|
||||
towingChanged = true
|
||||
end
|
||||
if towingChanged then
|
||||
towingVehicle:transmitModData()
|
||||
end
|
||||
end
|
||||
if towedMd then
|
||||
local towedChanged = false
|
||||
local currentScript = towedVehicle and towedVehicle:getScriptName() or nil
|
||||
if towedMd.towBarOriginalScriptName == nil and currentScript ~= "notTowingA_Trailer" then
|
||||
towedMd.towBarOriginalScriptName = currentScript
|
||||
towedChanged = true
|
||||
end
|
||||
if towedMd.towBarOriginalMass == nil and towedVehicle then
|
||||
towedMd.towBarOriginalMass = towedVehicle:getMass()
|
||||
towedChanged = true
|
||||
end
|
||||
if towedMd.towBarOriginalBrakingForce == nil and towedVehicle then
|
||||
towedMd.towBarOriginalBrakingForce = towedVehicle:getBrakingForce()
|
||||
towedChanged = true
|
||||
end
|
||||
if towedMd["isTowingByTowBar"] ~= true then
|
||||
towedMd["isTowingByTowBar"] = true
|
||||
towedChanged = true
|
||||
end
|
||||
if towedMd["towed"] ~= true then
|
||||
towedMd["towed"] = true
|
||||
towedChanged = true
|
||||
end
|
||||
if towedChanged then
|
||||
towedVehicle:transmitModData()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
towedVehicle:setScriptName("notTowingA_Trailer")
|
||||
markTowBarPair(towingVehicle, towedVehicle)
|
||||
storePersistedFrontLinkData(towingVehicle, towedVehicle, attachmentA, attachmentB)
|
||||
|
||||
local linked = isLinkedPair(towingVehicle, towedVehicle)
|
||||
log("ensurePairAttached pair=" .. tostring(vehicleId(towingVehicle)) .. "->" .. tostring(vehicleId(towedVehicle))
|
||||
@@ -297,6 +569,7 @@ local function ensurePairAttached(playerObj, towingVehicle, towedVehicle, prefer
|
||||
setTowBarModelVisible(towedVehicle, true)
|
||||
refreshAround(towingVehicle)
|
||||
refreshAround(towedVehicle)
|
||||
snapshotActiveTowLinks()
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -564,6 +837,7 @@ LT.performDetachWrapper = function(playerObj, towingVehicle, towedVehicle)
|
||||
queueAction(playerObj, 12, function(_, v) restoreDefaultsIfLeadLost(v); refreshAround(v) end, resolvedTowed)
|
||||
|
||||
setTowBarModelVisible(resolvedTowed, false)
|
||||
snapshotActiveTowLinks()
|
||||
end
|
||||
|
||||
LT.showRadialWrapper = function(playerObj)
|
||||
@@ -611,6 +885,319 @@ LT.onDetachTrailerWrapper = function(playerObj, vehicle, ...)
|
||||
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 sm = getScriptManager()
|
||||
if sm == nil then return end
|
||||
@@ -708,6 +1295,7 @@ local function startWatchdog()
|
||||
Events.OnTick.Remove(watchdogTick)
|
||||
Events.OnTick.Add(watchdogTick)
|
||||
installPatch()
|
||||
startLoadRebuildWatch()
|
||||
end
|
||||
|
||||
Events.OnGameBoot.Add(ensureTrailerAttachments)
|
||||
|
||||
@@ -6,12 +6,193 @@ if TowBarMod.Landtrain._towSyncServerLoaded then return end
|
||||
TowBarMod.Landtrain._towSyncServerLoaded = true
|
||||
|
||||
local SYNC_DELAY_TICKS = 2
|
||||
local SNAPSHOT_INTERVAL_TICKS = 120
|
||||
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)
|
||||
print("[Landtrain][TowSyncServer] " .. tostring(msg))
|
||||
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)
|
||||
if not args then return end
|
||||
table.insert(pending, {
|
||||
@@ -33,6 +214,19 @@ local function isLinked(vehicleA, vehicleB)
|
||||
return vehicleA:getVehicleTowing() == vehicleB and vehicleB:getVehicleTowedBy() == vehicleA
|
||||
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)
|
||||
if args and args.attachmentA then return args.attachmentA end
|
||||
if vehicleA and vehicleA:getTowAttachmentSelf() then return vehicleA:getTowAttachmentSelf() end
|
||||
@@ -45,6 +239,99 @@ local function resolveAttachmentB(args, vehicleB)
|
||||
return "trailerfront"
|
||||
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 args = item.args or {}
|
||||
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()))
|
||||
end
|
||||
|
||||
storePersistedFrontLinkData(vehicleA, vehicleB, attachmentA, attachmentB)
|
||||
snapshotActiveTowLinksServer()
|
||||
|
||||
log("attach sync broadcast A=" .. tostring(vehicleA:getId()) .. " B=" .. tostring(vehicleB:getId())
|
||||
.. " attachmentA=" .. tostring(attachmentA) .. " attachmentB=" .. tostring(attachmentB))
|
||||
sendServerCommand("landtrain", "forceAttachSync", {
|
||||
@@ -88,14 +378,31 @@ local function processDetach(item)
|
||||
local args = item.args or {}
|
||||
local vehicleAId = args.towingVehicle or args.vehicleA or args.vehicle
|
||||
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", {
|
||||
vehicleA = vehicleAId,
|
||||
vehicleB = vehicleBId
|
||||
})
|
||||
snapshotActiveTowLinksServer()
|
||||
end
|
||||
|
||||
local function processPending()
|
||||
snapshotTickCounter = snapshotTickCounter + 1
|
||||
if snapshotTickCounter >= SNAPSHOT_INTERVAL_TICKS then
|
||||
snapshotTickCounter = 0
|
||||
snapshotActiveTowLinksServer()
|
||||
end
|
||||
|
||||
if #pending == 0 then return end
|
||||
|
||||
local remaining = {}
|
||||
|
||||
Reference in New Issue
Block a user