Files
fs25-adsmartpickup/FS25_ADSmartPickup/adFields.lua
T

659 lines
31 KiB
Lua

-- AD Fields
-- Gårdens marker set fra spillet: ejede marker, prøvepunkter i marken, slåafgrøde og klar-andel
-- (density maps, samme metode som Arbejdsseddel — field.fieldState er en frossen NPC-plan og bruges
-- ikke), markens F-markør (AD-markør "F109") og siloer ved marken. Én mark scannes pr. kald.
ADFields = {}
ADFields.FIELD_SILO_DISTANCE = 150
ADFields.MARKER_MAX_DISTANCE = 400
ADFields.cache = {}
ADFields.order = {}
ADFields.scanIndex = 0
ADFields.mowable = nil
-- markliste og F-markører ændrer sig sjældent: slås op igen efter så lang tid (ikke hvert scan)
ADFields.OWNED_REFRESH_MS = 30000
ADFields.MARKER_REFRESH_MS = 30000
ADFields.ownedAt = nil
ADFields.now = 0
function ADFields.parseMarkerName(name)
if type(name) ~= "string" then return nil end
local digits = string.match(name, "^%s*[Ff]%s*(%d+)%s*$")
return digits ~= nil and tonumber(digits) or nil
end
-- MOWER: frugt -> skår; TEDDER: skår -> DRY…-skår. Kun kæder der ender i DRY… er slåafgrøder.
function ADFields.buildMowable(mowerConverter, tedderConverter, fillTypeName)
local result = {}
for fruitIndex, data in pairs(mowerConverter or {}) do
local windrow = data.fillTypeIndex or data.targetFillTypeIndex
local tedded = windrow ~= nil and tedderConverter ~= nil and tedderConverter[windrow] or nil
local hay = tedded ~= nil and (tedded.targetFillTypeIndex or tedded.fillTypeIndex) or nil
local hayName = hay ~= nil and fillTypeName(hay) or nil
if hay ~= nil and hay ~= windrow and type(hayName) == "string" and string.sub(hayName, 1, 3) == "DRY" then
result[fruitIndex] = {windrow = windrow, hay = hay}
end
end
return result
end
-- Markens art: slåafgrøde, høstafgrøde eller bar (ingen afgrøde). Vokser = afgrøde der ikke er klar endnu.
function ADFields.classify(fruitIndex, isMowable, readyRatio)
if fruitIndex == nil then return "bare", false end
local kind = isMowable and "mowable" or "harvest"
return kind, (readyRatio or 0) < 0.9
end
-- Vækststadie -> fase ud fra afgrødens grænser (FS25-felter hedder minHarvestingGrowthState eller
-- minHarvestGrowthState afhængigt af version).
function ADFields.growthPhase(state, fruit)
if state == nil or fruit == nil then return nil end
if state == 0 then return "none" end
if ADFields.isPreparable(state, fruit) then return "ready" end
if fruit.cutState ~= nil and fruit.cutState ~= 0 and state == fruit.cutState then return "cut" end
if fruit.witheredState ~= nil and state == fruit.witheredState then return "withered" end
local minReady = fruit.minHarvestingGrowthState or fruit.minHarvestGrowthState
local maxReady = fruit.maxHarvestingGrowthState or fruit.maxHarvestGrowthState or minReady
if minReady ~= nil and state >= minReady and state <= (maxReady or minReady) then return "ready" end
if minReady ~= nil and state < minReady then return "growing" end
return "cut"
end
-- Fuldt udvokset stadie for en slåafgrøde: græs er "høstbart" allerede i mellemhøjde (halv ydelse,
-- Dakota-græs: greenMiddle 3 < harvestReady 4 < harvestReady2 5), så slåning venter til stadiet efter
-- det første høstbare, når afgrøden har flere. nil = ukendte grænser (ingen ekstra krav).
function ADFields.primeState(fruit)
if fruit == nil then return nil end
local minReady = fruit.minHarvestingGrowthState or fruit.minHarvestGrowthState
if minReady == nil then return nil end
local maxReady = fruit.maxHarvestingGrowthState or fruit.maxHarvestGrowthState or minReady
if maxReady > minReady then return minReady + 1 end
return minReady
end
-- Kan stadiet "forberedes" (toppes)? Dakota-kartofler: "harvestReady" er kun forberedelig — først det
-- toppede stadie er høstklart, men skærebord med topper (Ropa rrXL9) tager begge i ét drag.
function ADFields.isPreparable(state, fruit)
if fruit == nil or type(state) ~= "number" or state <= 0 then return false end
local minPrep, maxPrep = fruit.minPreparingGrowthState, fruit.maxPreparingGrowthState
if type(minPrep) ~= "number" or minPrep <= 0 then return false end
return state >= minPrep and state <= (type(maxPrep) == "number" and maxPrep >= minPrep and maxPrep or minPrep)
end
-- Tæller punktets klar-pixels? Kun når stadiet (4. værdi fra getFruitArea) er fuldt udvokset.
function ADFields.isPrime(growthState, prime)
if prime == nil or type(growthState) ~= "number" or growthState <= 0 then return true end
return growthState >= prime
end
-- Vokser marken? Fasen vinder; ukendt fase -> der er voksende afgrøde (getFruitArea) og den er ikke klar.
function ADFields.isGrowing(phase, anyPixels, readyRatio)
if phase ~= nil then return phase == "growing" end
return (anyPixels or 0) > 0 and (readyRatio or 0) < 0.9
end
-- Jordens behov (kalk/pløjning) ud fra gennemsnitsniveauer og spillets indstillinger (kalk/pløjning kan
-- være slået fra i savegamet). Ukendt niveau = intet behov.
function ADFields.soilNeeds(levels, settings)
return {
lime = settings.limeRequired and levels.lime ~= nil and levels.lime < 1 or false,
plow = settings.plowingRequired and levels.plowLevel ~= nil and levels.plowLevel < 1 or false,
}
end
-- Vin og oliven høstes med egen høster — aldrig slåafgrøder, selv om et mod har sat dem på MOWER-listen
-- (in-game 29/9: F22 hvide druer fik "Silage + så" og en skårlægger). Samme liste som ADCpBridge.VINE_FRUITS.
ADFields.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true}
-- fruitName + indgangen fra getMowable() (nil = ikke på listen) -> slåbar?
function ADFields.isMowableFruit(fruitName, mowableEntry)
if mowableEntry == nil then return false end
return not ADFields.VINE_FRUITS[string.upper(tostring(fruitName or ""))]
end
function ADFields.getMowable()
if ADFields.mowable ~= nil then return ADFields.mowable end
local ok, result = pcall(function()
local mower = g_fruitTypeManager:getConverterDataByName("MOWER")
local tedder = g_fillTypeManager:getConverterDataByName("TEDDER")
return ADFields.buildMowable(mower, tedder, function(index) return g_fillTypeManager:getFillTypeNameByIndex(index) end)
end)
ADFields.mowable = ok and result or {}
if not ok then
Logging.warning("ADSmartPickup: slåafgrøder kunne ikke læses: %s", tostring(result))
end
return ADFields.mowable
end
-- ---- density-maps (samme læsning som AI-Bonden/Arbejdsseddel, bevist i spillet) ----
ADFields.densityCache = {}
local function readDensity(channel, x, z)
if channel == nil or g_currentMission == nil or g_currentMission.fieldGroundSystem == nil then return nil end
local cached = ADFields.densityCache[channel]
if cached == nil then
local ok, mapId, first, num = pcall(function() return g_currentMission.fieldGroundSystem:getDensityMapData(channel) end)
if not ok or mapId == nil then ADFields.densityCache[channel] = false; return nil end
cached = {modifier = DensityMapModifier.new(mapId, first, num, g_currentMission.terrainRootNode)}
ADFields.densityCache[channel] = cached
elseif cached == false then
return nil
end
local value
pcall(function()
cached.modifier:setParallelogramWorldCoords(x - 0.5, z - 0.5, 1, 0, 0, 1, DensityCoordType.POINT_VECTOR_VECTOR)
local sum, area = cached.modifier:executeGet()
if area ~= nil and area > 0 then value = sum / area end
end)
return value
end
local function readWeed(x, z)
local weedSystem = g_currentMission ~= nil and g_currentMission.weedSystem or nil
if weedSystem == nil or weedSystem.densityMap == nil then return nil end
if ADFields.weedModifier == nil then
local ok, modifier = pcall(DensityMapModifier.new, weedSystem.densityMap, weedSystem.firstChannel or 0, weedSystem.numChannels or 4, g_currentMission.terrainRootNode)
ADFields.weedModifier = ok and modifier or false
end
if ADFields.weedModifier == false then return nil end
local value
pcall(function()
ADFields.weedModifier:setParallelogramWorldCoords(x - 0.5, z - 0.5, 1, 0, 0, 1, DensityCoordType.POINT_VECTOR_VECTOR)
local sum, area = ADFields.weedModifier:executeGet()
if area ~= nil and area > 0 then value = sum / area end
end)
return value
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
return {
limeRequired = flag(info.limeRequired),
plowingRequired = flag(info.plowingRequiredEnabled),
weedsEnabled = flag(info.weedsEnabled),
}
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
if farmland.id ~= nil and g_farmlandManager ~= nil and g_farmlandManager.getFarmlandOwner ~= nil then
return g_farmlandManager:getFarmlandOwner(farmland.id)
end
return farmland.farmId
end
local function isOnField(x, z)
local y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 1, z)
return FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z) and true or false
end
ADFields.SAMPLE_DIRECTIONS = 16
-- field.fieldState (FS25, bekræftet i AI-Bonde via aifProbe): {fruitIndex|nil, growthState} eller nil
-- når spillet ikke har tilstanden (så bruges punkt-afstemningen).
function ADFields.readFieldState(field)
local state = field ~= nil and field.fieldState or nil
if type(state) ~= "table" or type(state.fruitTypeIndex) ~= "number" then return nil end
local fruitIndex = state.fruitTypeIndex > 0 and state.fruitTypeIndex or nil
local groundNone = type(FieldGroundType) == "table" and FieldGroundType.NONE or 0
return {fruitIndex = fruitIndex, growthState = fruitIndex ~= nil and state.growthState or nil,
noFieldGround = type(state.groundType) == "number" and state.groundType == groundNone}
end
-- Jordtyper der tæller som klargjort til såning (stubbearbejdet, kultiveret, såbed, pløjet).
ADFields.PREPARED_GROUNDS = {"STUBBLE_TILLAGE", "CULTIVATED", "SEEDBED", "PLOWED", "ROLLED_SEEDBED"}
-- value = jordtypens tæthedsværdi i et punkt; values = {[typenavn] = værdi}. -> klargjort, pløjet (nil: ukendt)
function ADFields.classifyGround(value, values)
if value == nil or values == nil then return nil, nil end
-- ingen markjord (bygget over / uden for marken): tæller ikke med i andelen
if values.NONE ~= nil and values.NONE == value then return nil, nil end
local prepared = false
for _, name in ipairs(ADFields.PREPARED_GROUNDS) do
if values[name] ~= nil and values[name] == value then prepared = true end
end
return prepared, values.PLOWED ~= nil and values.PLOWED == value
end
-- Spillets tæthedsværdier for jordtyperne (FieldGroundType -> værdi i GROUND_TYPE-kanalen).
function ADFields.groundValues()
if ADFields.groundValueCache ~= nil then return ADFields.groundValueCache or nil end
local system = g_currentMission ~= nil and g_currentMission.fieldGroundSystem or nil
if type(FieldGroundType) ~= "table" or system == nil then return nil end
local values = {}
for _, name in ipairs({"NONE", "STUBBLE_TILLAGE", "CULTIVATED", "SEEDBED", "PLOWED", "ROLLED_SEEDBED"}) do
local groundType = FieldGroundType[name]
if groundType ~= nil then
local ok, value = pcall(function() return system:getFieldGroundValue(groundType) end)
values[name] = ok and type(value) == "number" and value or groundType
end
end
ADFields.groundValueCache = values
return values
end
-- Kan marken dyrkes? Nej hvis spillet siger "ingen markjord" (groundType NONE: bygget over — gårdsplads,
-- stald, biogasanlæg), eller hvis intet prøvepunkt ligger på markjord. En markensilo i et hjørne ændrer
-- intet: resten af marken er stadig markjord.
function ADFields.isCultivable(state, onFieldPoints)
if state ~= nil and state.noFieldGround then return false end
return (onFieldPoints or 0) > 0
end
-- Markens omrids som {{x, z}, ...}. FS25: field:getDensityMapPolygon():getVerticesList() (flad liste
-- x1, z1, x2, z2 ... — som Courseplay); ældre: polygonPoints (noder). Tom liste hvis intet findes.
function ADFields.readPolygon(field)
local verts = {}
local okList, list = pcall(function()
return field.getDensityMapPolygon ~= nil and field:getDensityMapPolygon():getVerticesList() or nil
end)
if okList and type(list) == "table" and #list >= 6 then
for index = 1, #list - 1, 2 do
table.insert(verts, {x = list[index], z = list[index + 1]})
end
return verts
end
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
return verts
end
-- Markens form-fingeraftryk: antal hjørner, areal (shoelace, nærmeste 10 m²) og tyngdepunkt (nærmeste meter).
-- Samme mark giver samme streng trods små float-forskelle; en ændret mark (hjørne/kant) giver en ny.
-- Bruges til at genbruge Courseplay-kurser (ADCourseCache). nil uden en flade (under 3 hjørner).
ADFields.SHAPE_AREA_STEP = 10
local function roundTo(value, step)
return math.floor(value / step + 0.5) * step
end
function ADFields.shapeSignature(verts)
if type(verts) ~= "table" or #verts < 3 then return nil end
local n = #verts
local twiceArea, sx, sz = 0, 0, 0
for index = 1, n do
local a, b = verts[index], verts[index % n + 1]
local cross = a.x * b.z - b.x * a.z
twiceArea = twiceArea + cross
sx, sz = sx + (a.x + b.x) * cross, sz + (a.z + b.z) * cross
end
local cx, cz
if math.abs(twiceArea) > 1e-6 then
cx, cz = sx / (3 * twiceArea), sz / (3 * twiceArea)
else
cx, cz = ADFields.polygonCenter(verts)
end
return string.format("n=%d;a=%d;c=%d,%d", n, roundTo(math.abs(twiceArea) / 2, ADFields.SHAPE_AREA_STEP),
roundTo(cx, 1), roundTo(cz, 1))
end
-- Hjørnernes gennemsnit (nil uden hjørner).
function ADFields.polygonCenter(verts)
if verts == nil or #verts == 0 then return nil end
local cx, cz = 0, 0
for _, v in ipairs(verts) do cx, cz = cx + v.x, cz + v.z end
return cx / #verts, cz / #verts
end
-- Afstand fra (x, z) til omridsets nærmeste kant; nil uden omrids.
function ADFields.distanceToPolygon(verts, x, z)
if verts == nil or #verts == 0 then return nil end
local best = math.huge
for index = 1, #verts do
local a, b = verts[index], verts[index % #verts + 1]
local dx, dz = b.x - a.x, b.z - a.z
local len2 = dx * dx + dz * dz
local t = len2 > 0 and math.max(0, math.min(1, ((x - a.x) * dx + (z - a.z) * dz) / len2)) or 0
local d = math.sqrt((a.x + dx * t - x) ^ 2 + (a.z + dz * t - z) ^ 2)
if d < best then best = d end
end
return best
end
-- Markøren "F<nr>" nærmest marken: afstand til omridset (markører står ved kanten; midten kan
-- ligge 900 m væk på store kort), ellers til midten.
local function findMarker(adEnv, fieldId, x, z, verts)
local best, bestDistance = nil, math.huge
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
if ADFields.parseMarkerName(marker.name) == fieldId then
local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
if wayPoint ~= nil then
local distance = ADFields.distanceToPolygon(verts, wayPoint.x, wayPoint.z)
or math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2)
if distance < bestDistance then best, bestDistance = marker, distance end
end
end
end
if best ~= nil and bestDistance <= ADFields.MARKER_MAX_DISTANCE then
return best, bestDistance
end
return nil, best ~= nil and bestDistance or nil
end
-- ---- 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 = {},
anySum = 0, readySum = 0, immatureSum = 0, totalSum = 0, soil = {fert = {0, 0}, lime = {0, 0}, plowLevel = {0, 0}, weed = {0, 0}, prepared = {0, 0}, plowed = {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 = ADFields.readPolygon(field)
info.verts = verts
local cx, cz = ADFields.polygonCenter(verts)
if cx == nil then cx, cz = field.posX, field.posZ end
info.x, info.z = cx, cz
if cx == nil then return end
-- midten, markskiltet og halvvejs mod op til SAMPLE_DIRECTIONS hjørner: store marker (Dakota
-- 16x: 1,7 km) har striber uden afgrøde, så ét punkt kan ramme forkert
local candidates = {{x = cx, z = cz}}
if field.posX ~= nil and field.posZ ~= nil and (field.posX - cx) ^ 2 + (field.posZ - cz) ^ 2 > 1 then
table.insert(candidates, {x = field.posX, z = field.posZ})
end
local step = math.max(1, math.floor(#verts / ADFields.SAMPLE_DIRECTIONS))
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
-- spillets egen marktilstand (som fields.xml) er sandheden; punkt-afstemningen er reserve.
-- (getFieldDataAtWorldPosition giver isOnField, densityBits, groundType — IKKE vækststadie)
local state = ADFields.readFieldState(info.field)
info.growthState = state ~= nil and state.growthState or nil
info.cultivable = ADFields.isCultivable(state, #scan.points)
if state ~= nil then
info.fruitIndex = state.fruitIndex
else
info.fruitIndex = winner(scan.fruitVotes)
end
local fruitIndex = info.fruitIndex
local settings = missionSettings()
scan.settings = settings
-- slåafgrøder: genvækst i mellemhøjde tæller ikke som klar (in-game 27/9: halvt slået F90 blev
-- klippet igen i mellemhøjde)
local prime = nil
local fruitDesc = fruitIndex ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruitIndex) or nil
if fruitIndex ~= nil and ADFields.getMowable()[fruitIndex] ~= nil then
prime = ADFields.primeState(fruitDesc)
end
for _, point in ipairs(scan.points) do
push(scan, function()
local x, z = point.x, point.z
if fruitIndex ~= nil then
-- returnerer fruitValue, numPixels (afgrøde i høsthøjde), totalNumPixels (hele arealet), growthState
local okA, _, anyPixels, totalPixels, pointState = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, x - 2, z - 2, x + 2, z, x, z + 2, true, true)
if okA and type(totalPixels) == "number" then scan.totalSum = scan.totalSum + totalPixels end
if okA and anyPixels then
scan.anySum = scan.anySum + anyPixels
if not ADFields.isPrime(pointState, prime) then scan.immatureSum = scan.immatureSum + anyPixels end
end
local okR, _, readyPixels = pcall(FSDensityMapUtil.getFruitArea, fruitIndex, x - 2, z - 2, x + 2, z, x, z + 2, false, false)
-- utoppede kartofler tæller som klar (in-game 27/9 F16: klar-andel 0 -> sprang høsten over)
if okA and anyPixels and ADFields.isPreparable(pointState, fruitDesc) then
okR, readyPixels = true, math.max(readyPixels or 0, anyPixels)
end
if okR and readyPixels and ADFields.isPrime(okA and pointState or nil, prime) then
scan.readySum = scan.readySum + readyPixels
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,
}
-- klargjort/pløjet jord i punktet (kun på markjord): klargøringstrin der allerede er gjort går videre
local prepared, plowed = ADFields.classifyGround(readDensity(FieldDensityMap.GROUND_TYPE, x, z), ADFields.groundValues())
if prepared ~= nil then
readings.prepared = prepared and 1 or 0
readings.plowed = plowed and 1 or 0
end
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
local fruitDesc = info.fruitIndex ~= nil and g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex) or nil
if mowable ~= nil and not ADFields.isMowableFruit(fruitDesc ~= nil and fruitDesc.name or nil, mowable) then mowable = nil end
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 = scan.anySum > 0 and scan.readySum / scan.anySum or 0
info.immatureRatio = scan.anySum > 0 and scan.immatureSum / scan.anySum or 0
info.anyPixels = scan.anySum
-- andel af hele arealet der stadig står i høsthøjde (fremdrift på fanen Måned)
info.standingRatio = scan.totalSum > 0 and scan.readySum / scan.totalSum or nil
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(info.growthState, 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"
info.stubbleOf = info.phase == "cut" and info.cropTitle or nil
end
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, info.id, info.x, info.z, info.verts)
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
-- markens form: ændret mark (anden signatur end kursus-registret) -> gemte kurser til marken glemmes
info.shapeSig = ADFields.shapeSignature(info.verts)
if ADCourseCache ~= nil and ADCourseCache.checkField ~= nil then pcall(ADCourseCache.checkField, info.id, info.shapeSig) end
ADFields.cache[info.id] = info
ADFields.listCache = nil
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.MAX_TASKS_PER_FRAME = 8
ADFields.FRAME_BUDGET_MS = 0.5
function ADFields.clock()
if netGetTime == nil then return nil end
local ok, ms = pcall(netGetTime)
return ok and type(ms) == "number" and ms or nil
end
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
-- flere små opgaver pr. frame inden for et tidsbudget (efter genstart skal alle marker scannes;
-- én pr. frame gjorde listen fuld først efter flere minutter). Uden ur (tests): én pr. frame.
local started = ADFields.clock()
for _ = 1, ADFields.MAX_TASKS_PER_FRAME do
task = scan.queue[scan.head]
if task == nil then return end
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
return
end
local current = started ~= nil and ADFields.clock() or nil
if current == nil or current - started >= ADFields.FRAME_BUDGET_MS then return end
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).
function ADFields.list()
if ADFields.listCache ~= nil then return ADFields.listCache end
local infos = {}
for _, entry in ipairs(ADFields.order) do
local info = ADFields.cache[entry.id]
-- bygget-over marker (stald, biogas, gårdsplads) kan ikke dyrkes: ikke med i oversigt/arbejde
if info ~= nil and info.cultivable ~= false then table.insert(infos, info) end
end
ADFields.listCache = infos
return infos
end
function ADFields.getInfo(id)
return ADFields.cache[id]
end
-- Siloer tæt på markens polygon (eller centrum): mellemlagre, som et udkørselsflow skal tømme hjem.
function ADFields.fieldSilos(adEnv, farmId, info)
local result = {}
if ADBuildings == nil or info == nil or info.x == nil then return result end
local anchors = #info.verts > 0 and info.verts or {{x = info.x, z = info.z}}
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
local node = building.kind == "silo" and building.placeable ~= nil and building.placeable.rootNode or nil
if node ~= nil then
local bx, _, bz = getWorldTranslation(node)
for _, anchor in ipairs(anchors) do
if (anchor.x - bx) ^ 2 + (anchor.z - bz) ^ 2 <= ADFields.FIELD_SILO_DISTANCE ^ 2 then
table.insert(result, building)
break
end
end
end
end
return result
end