6fd917caca
Audit (2 reviewere) + simulering bench/bench_hak.lua: værste frame 124,7 ms -> 0,9 ms. - Holdende vognes genplanlægning (5 s) spredes tilfældigt over hele intervallet (før: alle i samme frame). - ADCache: korttids-cache m. spredt udløb (+0-50 %), kun aktiv i spillet; stationsopslag 30 s. - Bygningsliste 10 s (før 1 s: bygninger x markører hvert sekund). Markarbejde-tick forskudt 0,5 s fra menuen. - ADProfiler: logger top-6 langsomste indgange pr. minut ved > 2 ms.
270 lines
11 KiB
Lua
270 lines
11 KiB
Lua
-- AD Wait Pool
|
|
-- Fælles pulje af Wait-punkter (markører i mapper med ordet "Wait" i navnet) med
|
|
-- reservationer, nærmeste-ledige-opslag, rutetjek og omdirigering af en AutoDrive-opgave.
|
|
-- Bruges af adUnloadWait (rest-læs ved fuldt mål) og adOutbound (hold uden tur).
|
|
|
|
ADWaitPool = {}
|
|
|
|
-- ADCache-opslag der også virker uden adCache (tests)
|
|
local function cacheGet(bucket, key, ttlMs, fn)
|
|
if ADCacheGet == nil then return fn() end
|
|
return ADCacheGet(bucket, key, ttlMs, fn)
|
|
end
|
|
|
|
|
|
ADWaitPool.LOG_PREFIX = "ADSmartPickup: "
|
|
-- sættes af den der installerer opgave-hooks (task.STATE_DRIVING); 2 er AD's værdi
|
|
ADWaitPool.STATE_DRIVING = 2
|
|
|
|
-- markør-id -> køretøj der har reserveret Wait-punktet
|
|
ADWaitPool.claims = setmetatable({}, {__mode = "v"})
|
|
|
|
-- poolGroup: run'ets Wait-mappe; tom/nil = alle Wait-mapper.
|
|
function ADWaitPool.getCandidates(adEnv, poolGroup)
|
|
return cacheGet("waitCandidates", tostring(poolGroup or ""), 10000, function() return ADWaitPool.findCandidates(adEnv, poolGroup) end)
|
|
end
|
|
|
|
function ADWaitPool.findCandidates(adEnv, poolGroup)
|
|
local candidates = {}
|
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
|
local isInPool = poolGroup == nil or poolGroup == "" or marker.group == poolGroup
|
|
if isInPool and ADSupplyPlanner.isWaitMarker(marker) then
|
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
|
|
if wayPoint ~= nil then
|
|
table.insert(candidates, {marker = marker, x = wayPoint.x, z = wayPoint.z})
|
|
end
|
|
end
|
|
end
|
|
return candidates
|
|
end
|
|
|
|
function ADWaitPool.claim(markerId, vehicle)
|
|
ADWaitPool.claims[markerId] = vehicle
|
|
end
|
|
|
|
function ADWaitPool.getOwner(markerId)
|
|
return ADWaitPool.claims[markerId]
|
|
end
|
|
|
|
function ADWaitPool.releaseVehicle(vehicle)
|
|
for markerId, owner in pairs(ADWaitPool.claims) do
|
|
if owner == vehicle then
|
|
ADWaitPool.claims[markerId] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
local function getVehiclePosition(vehicle)
|
|
if vehicle.rootNode == nil then
|
|
return nil
|
|
end
|
|
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
|
return x, z
|
|
end
|
|
|
|
-- Nærmeste ledige (eller eget reserverede) Wait-punkt fra vognens position; isReachable(candidate)
|
|
-- valgfri. nil uden position eller uden brugbart punkt. Reserverer IKKE selv.
|
|
ADWaitPool.OCCUPIED_RADIUS = 6
|
|
|
|
-- Positioner for ANDRE motoriserede køretøjer (ikke vognen selv eller dens redskaber) — til at se om et
|
|
-- Wait-punkt fysisk er optaget, også uden reservation (fx lige efter en omplanlægning).
|
|
-- Alle motoriserede køretøjers positioner, delt mellem opslag i 0,5 s (ikke én runde pr. vogn pr. opslag).
|
|
local function allVehiclePositions()
|
|
return cacheGet("vehiclePositions", "all", 500, function()
|
|
local list = {}
|
|
local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil
|
|
for _, other in pairs(system ~= nil and system.vehicles or {}) do
|
|
if other.spec_motorized ~= nil and other.rootNode ~= nil then
|
|
local ok, x, _, z = pcall(getWorldTranslation, other.rootNode)
|
|
if ok and x ~= nil then table.insert(list, {vehicle = other, x = x, z = z}) end
|
|
end
|
|
end
|
|
return list
|
|
end)
|
|
end
|
|
|
|
local function otherVehiclePositions(vehicle)
|
|
local positions = {}
|
|
local own = {}
|
|
if vehicle.getChildVehicles ~= nil then
|
|
local ok, children = pcall(vehicle.getChildVehicles, vehicle)
|
|
for _, child in pairs(ok and children or {}) do own[child] = true end
|
|
end
|
|
own[vehicle] = true
|
|
for _, entry in ipairs(allVehiclePositions()) do
|
|
if not own[entry.vehicle] then table.insert(positions, entry) end
|
|
end
|
|
return positions
|
|
end
|
|
|
|
function ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable)
|
|
local x, z = getVehiclePosition(vehicle)
|
|
if x == nil then
|
|
return nil
|
|
end
|
|
local candidates = ADWaitPool.getCandidates(adEnv, poolGroup)
|
|
local occupied = ADSupplyPlanner.markOccupiedWaitPoints(candidates, otherVehiclePositions(vehicle), ADWaitPool.OCCUPIED_RADIUS)
|
|
return ADSupplyPlanner.pickNearestFreeWaitPoint(candidates, ADWaitPool.claims, x, z, vehicle, isReachable, occupied)
|
|
end
|
|
|
|
-- Ender ruten i målet? (AutoDrive returnerer tom/afkortet liste når der ingen vej er.)
|
|
local function pathReaches(path, wayPointId)
|
|
return path ~= nil and #path > 0 and path[#path].id == wayPointId
|
|
end
|
|
|
|
-- Sendes AutoDrive mod et punkt uden rute, viser den "cannot reach" og slukker sig selv.
|
|
-- getPathTo/pathFromTo er rene opslag uden bivirkninger.
|
|
-- Reserve: bredde-først gennem vejnettets udgange fra traktorens nærmeste waypoint. AD's getPathTo kan
|
|
-- sige nej trods en vej (set ved et Wait-punkt: ruten fandtes fra alle omkringliggende waypoints).
|
|
ADWaitPool.GRAPH_SEARCH_LIMIT = 50000
|
|
local loggedFallback = {}
|
|
|
|
local function graphReaches(adEnv, vehicle, targetId)
|
|
local graph = adEnv.ADGraphManager
|
|
if vehicle.getClosestWayPoint == nil or graph.getWayPointById == nil then
|
|
return false
|
|
end
|
|
local ok, startId = pcall(vehicle.getClosestWayPoint, vehicle)
|
|
if not ok or startId == nil then
|
|
return false
|
|
end
|
|
local seen, queue, head = {[startId] = true}, {startId}, 1
|
|
while queue[head] ~= nil and head <= ADWaitPool.GRAPH_SEARCH_LIMIT do
|
|
local id = queue[head]
|
|
head = head + 1
|
|
if id == targetId then
|
|
return true
|
|
end
|
|
local wayPoint = graph:getWayPointById(id)
|
|
for _, nextId in ipairs(wayPoint ~= nil and wayPoint.out or {}) do
|
|
if not seen[nextId] then
|
|
seen[nextId] = true
|
|
table.insert(queue, nextId)
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Rutesvar gemmes ROUTE_CACHE_MS pr. mål og 10 m-område: holdende vogne spørger hvert 5. s for hver
|
|
-- kandidat, og AD's stifinding (+ reserve-søgningen) gennem hele vejnettet gav et hak hvert 5. s
|
|
-- (in-game 26/9). Vejnettet ændrer sig ikke så hurtigt.
|
|
ADWaitPool.ROUTE_CACHE_MS = 30000
|
|
ADWaitPool.ROUTE_CELL_M = 10
|
|
ADWaitPool.SLOW_ROUTE_MS = 3
|
|
local routeCache = {}
|
|
local routeCacheSize = 0
|
|
ADWaitPool.ROUTE_CACHE_MAX = 2000
|
|
|
|
local function remember(key, result, at)
|
|
if routeCache[key] == nil then
|
|
routeCacheSize = routeCacheSize + 1
|
|
if routeCacheSize > ADWaitPool.ROUTE_CACHE_MAX then
|
|
routeCache, routeCacheSize = {}, 1
|
|
end
|
|
end
|
|
routeCache[key] = {result = result, at = at}
|
|
end
|
|
|
|
local function clockMs()
|
|
local ok, ms = pcall(function() return netGetTime() end)
|
|
if ok and type(ms) == "number" then return ms end
|
|
return nil
|
|
end
|
|
|
|
local function routeKey(vehicle, wayPointId)
|
|
if vehicle.rootNode == nil or getWorldTranslation == nil then return nil end
|
|
local ok, x, _, z = pcall(getWorldTranslation, vehicle.rootNode)
|
|
if not ok or x == nil then return nil end
|
|
local cell = ADWaitPool.ROUTE_CELL_M
|
|
return string.format("%s:%d:%d", tostring(wayPointId), math.floor(x / cell), math.floor(z / cell))
|
|
end
|
|
|
|
local function computeRoute(adEnv, vehicle, wayPointId)
|
|
local graph = adEnv.ADGraphManager
|
|
if pathReaches(graph:getPathTo(vehicle, wayPointId), wayPointId) then
|
|
return true
|
|
end
|
|
if graphReaches(adEnv, vehicle, wayPointId) then
|
|
if not loggedFallback[wayPointId] and Logging ~= nil and Logging.info ~= nil then
|
|
loggedFallback[wayPointId] = true
|
|
Logging.info("ADSmartPickup: AutoDrives rutesøgning fandt ingen vej til markør %s, men vejnettet har en — godtages", tostring(wayPointId))
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function ADWaitPool.hasRouteTo(adEnv, vehicle, wayPointId)
|
|
local graph = adEnv.ADGraphManager
|
|
if graph.getPathTo == nil then
|
|
return true
|
|
end
|
|
local now = clockMs()
|
|
local key = now ~= nil and routeKey(vehicle, wayPointId) or nil
|
|
local cached = key ~= nil and routeCache[key] or nil
|
|
if cached ~= nil and now - cached.at < ADWaitPool.ROUTE_CACHE_MS then
|
|
return cached.result
|
|
end
|
|
local result = computeRoute(adEnv, vehicle, wayPointId)
|
|
if key ~= nil then
|
|
local spent = (clockMs() or now) - now
|
|
remember(key, result, now)
|
|
if spent > ADWaitPool.SLOW_ROUTE_MS and Logging ~= nil and Logging.info ~= nil
|
|
and (ADWaitPool.lastSlowLog == nil or now - ADWaitPool.lastSlowLog > 60000) then
|
|
ADWaitPool.lastSlowLog = now
|
|
Logging.info("ADSmartPickup: rutetjek til markør %s tog %.1f ms (%s) — gemmes nu i %d s", tostring(wayPointId), spent, result and "vej" or "ingen vej", ADWaitPool.ROUTE_CACHE_MS / 1000)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
function ADWaitPool.hasRouteBothWays(adEnv, vehicle, toWayPointId, homeWayPointId)
|
|
local graph = adEnv.ADGraphManager
|
|
if graph.getPathTo == nil or graph.pathFromTo == nil then
|
|
return true
|
|
end
|
|
local now = clockMs()
|
|
local key = now ~= nil and routeKey(vehicle, tostring(toWayPointId) .. ">" .. tostring(homeWayPointId)) or nil
|
|
local cached = key ~= nil and routeCache[key] or nil
|
|
if cached ~= nil and now - cached.at < ADWaitPool.ROUTE_CACHE_MS then
|
|
return cached.result
|
|
end
|
|
local result = pathReaches(graph:getPathTo(vehicle, toWayPointId), toWayPointId)
|
|
and pathReaches(graph:pathFromTo(toWayPointId, homeWayPointId), homeWayPointId)
|
|
if key ~= nil then remember(key, result, now) end
|
|
return result
|
|
end
|
|
|
|
-- Står AD midt i en (blokeret) af-/pålæsning, stoppes den pænt først — ellers kører
|
|
-- traktoren væk med tippet vogn. Kun opgavens destination ændres.
|
|
function ADWaitPool.redirect(task, destinationID)
|
|
local trailerModule = task.vehicle.ad.trailerModule
|
|
if trailerModule ~= nil and trailerModule.isActiveAtTrigger ~= nil and trailerModule:isActiveAtTrigger() then
|
|
if trailerModule.stopUnloading ~= nil then trailerModule:stopUnloading() end
|
|
if trailerModule.stopLoading ~= nil then trailerModule:stopLoading() end
|
|
end
|
|
task.destinationID = destinationID
|
|
task.state = ADWaitPool.STATE_DRIVING
|
|
task.vehicle.ad.drivePathModule:setPathTo(destinationID)
|
|
end
|
|
|
|
-- Hvem holder hvor (GUI'ets Wait-puljer-visning). Sorteret på mappe, dernæst markør-id.
|
|
function ADWaitPool.getClaims(adEnv)
|
|
local result = {}
|
|
local graph = adEnv ~= nil and adEnv.ADGraphManager or (FS25_AutoDrive ~= nil and FS25_AutoDrive.ADGraphManager or nil)
|
|
for markerId, vehicle in pairs(ADWaitPool.claims) do
|
|
local marker = graph ~= nil and graph.getMapMarkerByWayPointId ~= nil and graph:getMapMarkerByWayPointId(markerId) or nil
|
|
if marker ~= nil then
|
|
table.insert(result, {marker = marker, vehicle = vehicle})
|
|
end
|
|
end
|
|
table.sort(result, function(a, b)
|
|
if a.marker.group ~= b.marker.group then
|
|
return tostring(a.marker.group) < tostring(b.marker.group)
|
|
end
|
|
return a.marker.id < b.marker.id
|
|
end)
|
|
return result
|
|
end
|