Files
fs25-adsmartpickup/FS25_ADSmartPickup/adLoadSwap.lua
T
masterdraco 6fd917caca perf: hak-audit — spredte timere, fælles cache (markør->station, salgssteder, identify, Wait-punkter, lagre, positioner), bygningsliste 10 s, profiler, simulering (v1.28.0.0)
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.
2026-09-26 11:24:05 +02:00

202 lines
11 KiB
Lua

-- AD Load Swap
-- En traktor der venter ved et Wait-punkt med en vare målet er fuldt af, mens målet
-- MANGLER en anden vare i traktorens liste, låser forsyningen (alle venter med halm,
-- ingen henter TMR). Her afleveres resten i en silo der tager varen, så vognen er tom
-- og næste tur henter det målet mangler.
ADLoadSwap = {}
ADLoadSwap.LOG_PREFIX = "ADSmartPickup: "
-- Opslaget går over alle markører; ventende traktorer tjekker derfor kun så tit.
ADLoadSwap.CHECK_INTERVAL_MS = 5000
-- Byt også når et fuldt læs ikke passer, hvis den manglende vare mangler mindst så stor en andel af sit lager.
ADLoadSwap.MIN_MISSING_FRACTION = 0.25
local warnedNoSilo = setmetatable({}, {__mode = "k"})
-- Er en anden vare i listen så tom i målet, at et FULDT læs passer, og findes den i en silo?
function ADLoadSwap.findMissingIngredient(adEnv, vehicle, restFillType, homeWayPointId)
local ingredients = ADSmartPickup.getIngredientsFor(adEnv, vehicle)
if ingredients == nil or #ingredients < 2 then
return nil
end
local choice = ADSmartPickup.findSupplyPickup(adEnv, vehicle, ingredients, homeWayPointId)
if choice == nil or choice.fillType == nil or choice.fillType == restFillType then
return nil
end
-- Et fuldt læs der passer er det bedste; men er vognen større end staldens lager, passer det aldrig
-- (Fastrac med stor vogn ved en kostald) — så er det nok at varen mangler en tydelig del.
local need = choice.need
local missesEnough = need.capacity ~= nil and need.capacity > 0 and need.freeSpace ~= nil
and need.freeSpace >= need.capacity * ADLoadSwap.MIN_MISSING_FRACTION
if need.fits ~= true and not missesEnough then
return nil
end
return choice
end
-- Kun rene lager-siloer: stalde og fabrikker har egne regler, og målet selv er fuldt.
local function isSilo(station, homeStation)
local placeable = station.owningPlaceable
return placeable ~= nil and placeable.spec_silo ~= nil and placeable.getHusbandryFillLevel == nil
and placeable.spec_productionPoint == nil and (homeStation == nil or placeable ~= homeStation.owningPlaceable)
end
local function usesAsInput(placeable, fillType)
local productionPoint = placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
for _, production in ipairs(productionPoint ~= nil and productionPoint.productions or {}) do
for _, input in ipairs(production.inputs or {}) do
if input.type == fillType then return true end
end
end
return false
end
-- Lagermål: rene siloer; med allowFactories også gårdens EGNE fabrikker der bruger varen som input
-- (fx et biogasanlæg med gyllesilo). Stalde, fremmede anlæg og hjemmet (målet selv) aldrig.
function ADLoadSwap.isStoreTarget(station, fillType, homeStation, allowFactories)
if isSilo(station, homeStation) then
return true
end
local placeable = station.owningPlaceable
if not allowFactories or placeable == nil or placeable.spec_productionPoint == nil or placeable.getHusbandryFillLevel ~= nil then
return false
end
if homeStation ~= nil and placeable == homeStation.owningPlaceable then
return false
end
local owned = ADPrices ~= nil and ADPrices.isOwnedByPlayer ~= nil and ADPrices.isOwnedByPlayer(placeable)
return owned and usesAsInput(placeable, fillType)
end
local function acceptsRest(station, fillType, rest, farmId)
if station.getIsFillTypeSupported ~= nil and not station:getIsFillTypeSupported(fillType) then
return false
end
local free = station.getFreeCapacity ~= nil and station:getFreeCapacity(fillType, farmId) or 0
return free >= rest
end
-- Nærmeste silo med plads til resten og vejnet til/fra; siloer der allerede har varen foretrækkes.
-- Nærmeste silo med plads til resten og vejnet fra traktoren; siloer der allerede har varen
-- foretrækkes. Returnerer marker — eller nil + en forklaring (til loggen).
-- excludedPlaceables (valgfri): {[placeable] = true} der aldrig må vælges (fx udkørslens kilder).
-- Rækkefølge for aflæsnings-siloer: markører et forløb HENTER fra (avoidMarkers = {[id] = true}) sidst —
-- ellers læsser en lagertur af dér hvor en salgstur skal læsse (samme tank-række); dernæst siloer der
-- allerede har varen, så nærmeste. Returnerer en ny sorteret liste.
function ADLoadSwap.rankDumpCandidates(candidates, avoidMarkers)
local ranked = {}
for _, candidate in ipairs(candidates or {}) do table.insert(ranked, candidate) end
local function avoided(candidate) return avoidMarkers ~= nil and avoidMarkers[candidate.marker.id] == true end
table.sort(ranked, function(a, b)
if avoided(a) ~= avoided(b) then return not avoided(a) end
if a.hasFillType ~= b.hasFillType then return a.hasFillType end
if a.distance ~= b.distance then return a.distance < b.distance end
return a.marker.id < b.marker.id
end)
return ranked
end
function ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable, excludedPlaceables, avoidMarkers, allowFactories)
if vehicle.rootNode == nil then
return nil, "køretøjet har ingen position"
end
local farmId = ADSmartPickup.getFarmId(vehicle)
local homeStation = homeWayPointId ~= nil and ADSmartPickup.getUnloadStationAtWayPointId(adEnv, homeWayPointId, farmId) or nil
local x, _, z = getWorldTranslation(vehicle.rootNode)
local candidates = {}
local silos, withSpace = 0, 0
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId)
local isExcluded = excludedPlaceables ~= nil and excludedPlaceables[station ~= nil and station.owningPlaceable or false] == true
if station ~= nil and ADLoadSwap.isStoreTarget(station, fillType, homeStation, allowFactories) and not isExcluded then
silos = silos + 1
if acceptsRest(station, fillType, rest, farmId) then
withSpace = withSpace + 1
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
if wayPoint ~= nil then
local dx, dz = wayPoint.x - x, wayPoint.z - z
table.insert(candidates, {
marker = marker,
hasFillType = ADSmartPickup.getOwnStorageLevel(station, {fillType}) > 0,
distance = dx * dx + dz * dz,
})
end
end
end
end
candidates = ADLoadSwap.rankDumpCandidates(candidates, avoidMarkers)
for _, candidate in ipairs(candidates) do
if isReachable(candidate.marker.id) then
return candidate.marker
end
end
if silos == 0 then
return nil, allowFactories and "ingen silo eller egen fabrik der bruger varen, med aflæsningsmarkør (sæt en AD-markør ved aflæsningstriggeren)" or "ingen silo med aflæsningsmarkør fundet (siloen skal have en AD-markør ved aflæsningstriggeren)"
elseif withSpace == 0 then
return nil, string.format("%d silo(er) fundet, men ingen tager varen med plads til %.0f l", silos, rest)
end
return nil, string.format("%d silo(er) tager varen, men AutoDrive finder ingen rute fra traktoren til dem", withSpace)
end
-- Tvunget lagermål (flowets "Lager til"): kun de valgte aflæsningsmarkører, i rækkefølge. Første der
-- tager varen, har plads til læsset og kan nås, vinder. Returnerer marker — eller nil + forklaring.
function ADLoadSwap.findForcedDumpMarker(adEnv, vehicle, fillType, liters, markerIds, isReachable)
local farmId = ADSmartPickup.getFarmId(vehicle)
local reasons = {}
for _, wayPointId in ipairs(markerIds or {}) do
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(wayPointId)
local station = marker ~= nil and ADSmartPickup.getUnloadStationAtWayPointId(adEnv, wayPointId, farmId) or nil
local name = marker ~= nil and tostring(marker.name) or tostring(wayPointId)
if station == nil then
table.insert(reasons, name .. ": ingen aflæsning ved markøren")
elseif station.getIsFillTypeSupported ~= nil and not station:getIsFillTypeSupported(fillType) then
table.insert(reasons, name .. ": tager ikke varen")
elseif not acceptsRest(station, fillType, liters, farmId) then
table.insert(reasons, name .. ": fuld")
elseif not isReachable(wayPointId) then
table.insert(reasons, name .. ": ingen vej")
else
return marker
end
end
return nil, "valgt lager: " .. (#reasons > 0 and table.concat(reasons, "; ") or "ingen markør")
end
-- Kaldes fra Wait-logikken hvert tick; redirect(task, id) og release(vehicle) er dens egne.
-- true = traktoren er sendt til en silo (Wait-punktet er frigivet).
function ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable)
-- tilfældig start (0-2 s): ventende vogne tjekker ikke alle i samme frame
wait.swapTimerMs = (wait.swapTimerMs or math.random(0, ADLoadSwap.CHECK_INTERVAL_MS)) + (dt or 0)
if wait.swapTimerMs < ADLoadSwap.CHECK_INTERVAL_MS then
return false
end
wait.swapTimerMs = 0
local vehicle = task.vehicle
-- Tager målet slet ikke varen (forkert last), skal den af uanset hvad der ellers mangler.
local missing = ADLoadSwap.findMissingIngredient(adEnv, vehicle, wait.fillType, wait.homeDestinationID)
if missing == nil and not wait.isRejected then
return false
end
local marker, why = ADLoadSwap.findDumpMarker(adEnv, vehicle, wait.fillType, wait.rest, wait.homeDestinationID, isReachable)
local restName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(wait.fillType) or tostring(wait.fillType)
local missingName = missing ~= nil and (g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(missing.fillType) or tostring(missing.fillType)) or "-"
if marker == nil then
if not warnedNoSilo[vehicle] then
warnedNoSilo[vehicle] = true
Logging.warning(ADLoadSwap.LOG_PREFIX .. "'%s' venter med %.0f l %s (%s) — %s; den bliver ved Wait-punktet", tostring(ADRuns.vehicleLabel(vehicle)), wait.rest, tostring(restName), missing ~= nil and ("målet mangler " .. tostring(missingName)) or "målet tager ikke varen", tostring(why))
end
return false
end
warnedNoSilo[vehicle] = nil
release(vehicle)
redirect(task, marker.id)
local why = missing ~= nil and ("målet mangler " .. tostring(missingName)) or "målet tager ikke varen"
Logging.info(ADLoadSwap.LOG_PREFIX .. "'%s' bytter læs: %.0f l %s tilbage i %s (markør %s), %s", tostring(ADRuns.vehicleLabel(vehicle)), wait.rest, tostring(restName), tostring(marker.name), tostring(marker.markerIndex or marker.id), why)
return true
end
function ADLoadSwap.clearWarning(vehicle)
warnedNoSilo[vehicle] = nil
end