perf(marker): markscanning som arbejdskø — højst ét prøvepunkt pr. frame (v1.27.2.0)

In-game 26/9: hakket kom sjældnere da scanningen gik fra 1 s til 2 s -> scanningen af én mark i ét frame
var årsagen (opslag gennemgår alle ~40 afgrødetyper pr. punkt). Coroutines findes ikke i FS25 (Giants
fjernede dem, jf. Courseplays HybridAStar), derfor en eksplicit kø. Tidsmåling bruger netGetTime.
Test (test_adFieldsScan) beviser: færdig, korrekt resultat, <= 3 opslag pr. frame.
This commit is contained in:
2026-09-26 10:38:51 +02:00
parent c34a472b1c
commit 05832603e4
7 changed files with 300 additions and 161 deletions
+208 -149
View File
@@ -127,15 +127,6 @@ local function readWeed(x, z)
return value
end
local function average(points, reader, channel)
local sum, count = 0, 0
for _, p in ipairs(points) do
local value = reader(channel, p.x, p.z)
if value ~= nil then sum, count = sum + value, count + 1 end
end
return count > 0 and sum / count or nil
end
local function missionSettings()
local info = g_currentMission ~= nil and g_currentMission.missionInfo or {}
local function flag(value) return value == nil or value == true end
@@ -146,22 +137,6 @@ local function missionSettings()
}
end
-- Jord og pleje: gødning (og max), kalk, pløjning, ukrudt — gennemsnit over prøvepunkterne.
local function readSoil(info)
if type(FieldDensityMap) ~= "table" or #info.points == 0 then return end
local settings = missionSettings()
info.fert = average(info.points, readDensity, FieldDensityMap.SPRAY_LEVEL)
local okMax, maxValue = pcall(function() return g_currentMission.fieldGroundSystem:getMaxValue(FieldDensityMap.SPRAY_LEVEL) end)
info.fertMax = okMax and type(maxValue) == "number" and maxValue or 2
info.lime = average(info.points, readDensity, FieldDensityMap.LIME_LEVEL)
info.plowLevel = average(info.points, readDensity, FieldDensityMap.PLOW_LEVEL)
if settings.weedsEnabled then
info.weed = average(info.points, function(_, x, z) return readWeed(x, z) end, nil)
end
info.needs = ADFields.soilNeeds(info, settings)
if not settings.limeRequired then info.lime = nil end
end
local function fieldOwner(field)
local farmland = field.farmland or (field.getFarmland ~= nil and field:getFarmland()) or nil
if farmland == nil then return nil end
@@ -176,75 +151,6 @@ local function isOnField(x, z)
return FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z) and true or false
end
-- Centroid + halvvejs mod op til 8 hjørner; kun punkter der stadig er mark (bygninger fjerner areal).
local function samplePoints(field)
local verts = {}
for _, node in ipairs(field.polygonPoints or {}) do
if type(node) == "number" then
local x, _, z = getWorldTranslation(node)
table.insert(verts, {x = x, z = z})
end
end
local cx, cz = field.posX, field.posZ
if (cx == nil or cz == nil) and #verts > 0 then
cx, cz = 0, 0
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
cx, cz = cx / #verts, cz / #verts
end
if cx == nil then return {}, verts end
local candidates = {{x = cx, z = cz}}
local step = math.max(1, math.floor(#verts / 8))
for index = 1, #verts, step do
table.insert(candidates, {x = (cx + verts[index].x) / 2, z = (cz + verts[index].z) / 2})
end
local points = {}
for _, p in ipairs(candidates) do
local ok, onField = pcall(isOnField, p.x, p.z)
if ok and onField then table.insert(points, p) end
end
return points, verts
end
local function fruitAt(points)
local votes, best, bestVotes = {}, nil, 0
for _, p in ipairs(points) do
local ok, index = pcall(FSDensityMapUtil.getFruitTypeIndexAtWorldPos, p.x, p.z)
if ok and type(index) == "number" and index > 0 then
votes[index] = (votes[index] or 0) + 1
if votes[index] > bestVotes then best, bestVotes = index, votes[index] end
end
end
return best
end
-- Andel af prøvefladen i høstvinduet (samme kald som Arbejdsseddel: any = true,true; klar = false,false).
local function readyRatio(fruitIndex, points)
local anySum, readySum = 0, 0
for _, p in ipairs(points) do
local okA, _, anyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, p.x - 2, p.z - 2, p.x + 2, p.z, p.x, p.z + 2, true, true)
if okA and anyPixels then anySum = anySum + anyPixels end
local okR, _, readyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, p.x - 2, p.z - 2, p.x + 2, p.z, p.x, p.z + 2, false, false)
if okR and readyPixels then readySum = readySum + readyPixels end
end
return anySum > 0 and readySum / anySum or 0, anySum
end
-- Vækststadiet på prøvepunkterne (flertal): getFieldDataAtWorldPosition's anden returværdi.
local function growthStateAt(points)
local votes, best, bestVotes = {}, nil, 0
for _, p in ipairs(points) do
local ok, _, state = pcall(function()
local y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, p.x, 1, p.z)
return FSDensityMapUtil.getFieldDataAtWorldPosition(p.x, y, p.z)
end)
if ok and type(state) == "number" then
votes[state] = (votes[state] or 0) + 1
if votes[state] > bestVotes then best, bestVotes = state, votes[state] end
end
end
return best
end
local function findMarker(adEnv, fieldId, x, z)
local best, bestDistance = nil, math.huge
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
@@ -262,87 +168,240 @@ local function findMarker(adEnv, fieldId, x, z)
return nil, best ~= nil and bestDistance or nil
end
local function scanField(adEnv, field, fieldId)
local points, verts = samplePoints(field)
local info = {id = fieldId, field = field, points = points, verts = verts, mowable = false, readyRatio = 0}
info.x, info.z = field.posX, field.posZ
if (info.x == nil or info.z == nil) and points[1] ~= nil then info.x, info.z = points[1].x, points[1].z end
info.fruitIndex = #points > 0 and fruitAt(points) or nil
local mowable = info.fruitIndex ~= nil and ADFields.getMowable()[info.fruitIndex] or nil
if info.fruitIndex ~= nil then
local fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
local fillType = fruit ~= nil and fruit.fillType or nil
info.cropTitle = fillType ~= nil and fillType.title or (fruit ~= nil and fruit.name) or "?"
-- ---- scanning som arbejdskø ----
-- En marks scanning er en kø af små opgaver (ét prøvepunkt pr. opgave), og scanStep kører én opgave pr.
-- frame. In-game 26/9 gav en hel mark i ét frame et hak (flere opslag gennemgår alle kortets ~40
-- afgrødetyper pr. punkt). Coroutines findes ikke i FS25 (Giants fjernede dem), derfor en kø.
-- (eksplicit hale: # er udefineret på en tabel med huller, og udførte opgaver nulstilles)
local function push(scan, task)
scan.tail = (scan.tail or 0) + 1
scan.queue[scan.tail] = task
end
local function vote(votes, key)
votes[key] = (votes[key] or 0) + 1
end
local function winner(votes)
local best, bestVotes = nil, 0
for key, count in pairs(votes) do
if count > bestVotes then best, bestVotes = key, count end
end
return best
end
local function newScan(adEnv, entry)
local field, fieldId = entry.field, entry.id
local scan = {id = fieldId, queue = {}, head = 1, tail = 0, points = {}, fruitVotes = {}, growthVotes = {},
anySum = 0, readySum = 0, soil = {fert = {0, 0}, lime = {0, 0}, plowLevel = {0, 0}, weed = {0, 0}}}
local info = {id = fieldId, field = field, points = scan.points, verts = {}, mowable = false, readyRatio = 0}
scan.info = info
-- 1) polygon og kandidatpunkter (centroid + halvvejs mod op til 8 hjørner)
push(scan, function()
local verts = {}
for _, node in ipairs(field.polygonPoints or {}) do
if type(node) == "number" then
local x, _, z = getWorldTranslation(node)
table.insert(verts, {x = x, z = z})
end
end
info.verts = verts
local cx, cz = field.posX, field.posZ
if (cx == nil or cz == nil) and #verts > 0 then
cx, cz = 0, 0
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
cx, cz = cx / #verts, cz / #verts
end
info.x, info.z = cx, cz
if cx == nil then return end
local candidates = {{x = cx, z = cz}}
local step = math.max(1, math.floor(#verts / 8))
for index = 1, #verts, step do
table.insert(candidates, {x = (cx + verts[index].x) / 2, z = (cz + verts[index].z) / 2})
end
-- 2) én opgave pr. kandidat: er punktet stadig mark? (bygninger fjerner areal)
for _, candidate in ipairs(candidates) do
push(scan, function()
local ok, onField = pcall(isOnField, candidate.x, candidate.z)
if ok and onField then table.insert(scan.points, candidate) end
end)
end
-- 3) afgrøde pr. punkt (efter alle kandidater)
push(scan, function()
for _, point in ipairs(scan.points) do
push(scan, function()
local ok, index = pcall(FSDensityMapUtil.getFruitTypeIndexAtWorldPos, point.x, point.z)
if ok and type(index) == "number" and index > 0 then vote(scan.fruitVotes, index) end
end)
end
push(scan, function() ADFields.scanPerFruit(scan) end)
end)
end)
return scan
end
-- Når afgrøden kendes: klar-andel, vækststadie og jord pr. punkt (én opgave pr. punkt).
function ADFields.scanPerFruit(scan)
local info = scan.info
info.fruitIndex = winner(scan.fruitVotes)
local fruitIndex = info.fruitIndex
local settings = missionSettings()
scan.settings = settings
for _, point in ipairs(scan.points) do
push(scan, function()
local x, z = point.x, point.z
if fruitIndex ~= nil then
local okA, _, anyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, x - 2, z - 2, x + 2, z, x, z + 2, true, true)
if okA and anyPixels then scan.anySum = scan.anySum + anyPixels end
local okR, _, readyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, x - 2, z - 2, x + 2, z, x, z + 2, false, false)
if okR and readyPixels then scan.readySum = scan.readySum + readyPixels end
local okG, _, state = pcall(function()
local y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 1, z)
return FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z)
end)
if okG and type(state) == "number" then vote(scan.growthVotes, state) end
end
end)
push(scan, function()
if type(FieldDensityMap) ~= "table" then return end
local x, z = point.x, point.z
local readings = {
fert = readDensity(FieldDensityMap.SPRAY_LEVEL, x, z),
lime = readDensity(FieldDensityMap.LIME_LEVEL, x, z),
plowLevel = readDensity(FieldDensityMap.PLOW_LEVEL, x, z),
weed = settings.weedsEnabled and readWeed(x, z) or nil,
}
for key, value in pairs(readings) do
if value ~= nil then
scan.soil[key][1] = scan.soil[key][1] + value
scan.soil[key][2] = scan.soil[key][2] + 1
end
end
end)
end
end
-- Samler resultatet når køen er tom.
local function finishScan(adEnv, scan)
local info = scan.info
local mowable = info.fruitIndex ~= nil and ADFields.getMowable()[info.fruitIndex] or nil
if mowable ~= nil then
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
end
local fruit = nil
if info.fruitIndex ~= nil then
info.readyRatio, info.anyPixels = readyRatio(info.fruitIndex, points)
info.readyRatio = scan.anySum > 0 and scan.readySum / scan.anySum or 0
info.anyPixels = scan.anySum
fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
info.fruitName = fruit ~= nil and fruit.name or nil
info.harvestFillType = fruit ~= nil and fruit.fillType ~= nil and fruit.fillType.index or nil
local fillType = fruit ~= nil and fruit.fillType or nil
info.cropTitle = fillType ~= nil and fillType.title or (fruit ~= nil and fruit.name) or "?"
end
info.kind = ADFields.classify(info.fruitIndex, info.mowable, info.readyRatio)
info.phase = fruit ~= nil and ADFields.growthPhase(growthStateAt(points), fruit) or nil
info.phase = fruit ~= nil and ADFields.growthPhase(winner(scan.growthVotes), fruit) or nil
info.growing = info.fruitIndex ~= nil and ADFields.isGrowing(info.phase, info.anyPixels, info.readyRatio)
-- høstet stub eller visnet afgrøde er i praksis en bar mark (klargøring/såning)
if info.kind == "harvest" and (info.phase == "cut" or info.phase == "withered" or info.phase == "none") then
info.kind = "bare"
end
readSoil(info)
local previous = ADFields.cache[fieldId]
local settings = scan.settings or missionSettings()
for key, sum in pairs(scan.soil) do
info[key] = sum[2] > 0 and sum[1] / sum[2] or nil
end
local okMax, maxValue = pcall(function() return g_currentMission.fieldGroundSystem:getMaxValue(FieldDensityMap.SPRAY_LEVEL) end)
info.fertMax = okMax and type(maxValue) == "number" and maxValue or 2
info.needs = ADFields.soilNeeds(info, settings)
if not settings.limeRequired then info.lime = nil end
local previous = ADFields.cache[info.id]
if previous ~= nil and previous.markerCheckedAt ~= nil and ADFields.now - previous.markerCheckedAt < ADFields.MARKER_REFRESH_MS then
info.markerId, info.markerName, info.markerDistance, info.markerCheckedAt = previous.markerId, previous.markerName, previous.markerDistance, previous.markerCheckedAt
elseif info.x ~= nil then
local marker, distance = findMarker(adEnv, fieldId, info.x, info.z)
local marker, distance = findMarker(adEnv, info.id, info.x, info.z)
info.markerId = marker ~= nil and marker.id or nil
info.markerName = marker ~= nil and marker.name or nil
info.markerDistance = distance
info.markerCheckedAt = ADFields.now
end
return info
end
local function ownedFields(farmId)
local result = {}
local all = g_fieldManager ~= nil and (g_fieldManager:getFields() or g_fieldManager.fields) or {}
for _, field in pairs(all) do
local id = field.getId ~= nil and field:getId() or field.fieldId
if id ~= nil and fieldOwner(field) == farmId then
table.insert(result, {id = id, field = field})
end
end
table.sort(result, function(a, b) return a.id < b.id end)
return result
end
-- Scanner næste mark (round-robin) og returnerer alle kendte. Kaldes hvert sekund af job-løkken.
function ADFields.scanNext(adEnv, farmId, now)
ADFields.now = now or ADFields.now
if ADFields.ownedAt == nil or ADFields.now - ADFields.ownedAt >= ADFields.OWNED_REFRESH_MS then
ADFields.ownedAt = ADFields.now
local owned = ownedFields(farmId)
local ids = {}
for _, entry in ipairs(owned) do ids[entry.id] = true end
for id in pairs(ADFields.cache) do
if not ids[id] then ADFields.cache[id] = nil end
end
ADFields.order = owned
ADFields.listCache = nil
end
local owned = ADFields.order
if #owned == 0 then return end
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
local entry = owned[ADFields.scanIndex]
local ok, info = pcall(scanField, adEnv, entry.field, entry.id)
ADFields.cache[info.id] = info
ADFields.listCache = nil
if ok then
ADFields.cache[entry.id] = info
else
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(entry.id), tostring(info))
end
-- Marklisten (ejede marker) i portioner af 25 marker pr. frame.
local function newOwnedScan(farmId)
local all = {}
for _, field in pairs(g_fieldManager ~= nil and (g_fieldManager:getFields() or g_fieldManager.fields) or {}) do
table.insert(all, field)
end
local scan = {owned = true, queue = {}, head = 1, tail = 0, result = {}}
for start = 1, #all, 25 do
push(scan, function()
for index = start, math.min(start + 24, #all) do
local field = all[index]
local id = field.getId ~= nil and field:getId() or field.fieldId
if id ~= nil and fieldOwner(field) == farmId then
table.insert(scan.result, {id = id, field = field})
end
end
end)
end
return scan
end
local function finishOwnedScan(scan)
table.sort(scan.result, function(a, b) return a.id < b.id end)
local ids = {}
for _, entry in ipairs(scan.result) do ids[entry.id] = true end
for id in pairs(ADFields.cache) do
if not ids[id] then ADFields.cache[id] = nil end
end
ADFields.order = scan.result
ADFields.listCache = nil
end
-- Kaldes HVERT frame: kører én opgave af den aktuelle scanning. En ny mark påbegyndes højst hvert
-- SCAN_INTERVAL_MS; marklisten opdateres hvert OWNED_REFRESH_MS.
ADFields.SCAN_INTERVAL_MS = 2000
ADFields.lastScanStart = nil
function ADFields.scanStep(adEnv, farmId, now)
ADFields.now = now or ADFields.now
local scan = ADFields.activeScan
if scan == nil then
if ADFields.ownedAt == nil or ADFields.now - ADFields.ownedAt >= ADFields.OWNED_REFRESH_MS then
ADFields.ownedAt = ADFields.now
scan = newOwnedScan(farmId)
else
if ADFields.lastScanStart ~= nil and ADFields.now - ADFields.lastScanStart < ADFields.SCAN_INTERVAL_MS then return end
local owned = ADFields.order
if #owned == 0 then return end
ADFields.lastScanStart = ADFields.now
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
scan = newScan(adEnv, owned[ADFields.scanIndex])
end
ADFields.activeScan = scan
end
local task = scan.queue[scan.head]
if task ~= nil then
scan.queue[scan.head] = nil
scan.head = scan.head + 1
local ok, err = pcall(task)
if not ok then
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(scan.id or "-"), tostring(err))
ADFields.activeScan = nil
end
return
end
-- køen er tom: saml resultatet
ADFields.activeScan = nil
local ok, err
if scan.owned then
ok, err = pcall(finishOwnedScan, scan)
else
ok, err = pcall(finishScan, adEnv, scan)
end
if not ok then Logging.warning("ADSmartPickup: F%s scanning kunne ikke afsluttes: %s", tostring(scan.id or "-"), tostring(err)) end
end
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).