Loading works

This commit is contained in:
2026-02-13 11:17:31 -05:00
parent f38f28c40a
commit 19ad4b5755
2 changed files with 900 additions and 5 deletions

View File

@@ -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,23 +487,41 @@ local function markTowBarPair(towingVehicle, towedVehicle)
local towedMd = towedVehicle and towedVehicle:getModData() or nil
if towingMd then
towingMd["isTowingByTowBar"] = true
towingVehicle:transmitModData()
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
towedMd["isTowingByTowBar"] = true
towedMd["towed"] = true
towedVehicle:transmitModData()
end
end
@@ -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)

View File

@@ -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 = {}