-- 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 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 -- 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 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 local function findMarker(adEnv, fieldId, x, z) 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 = 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 = {}, 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 = 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(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 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.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 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.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). 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] if info ~= nil 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