Files

224 lines
9.2 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
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
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
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
-- 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
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
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 "?"
end
if mowable ~= nil then
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
info.readyRatio = readyRatio(info.fruitIndex, points)
end
if info.x ~= nil then
local marker, distance = findMarker(adEnv, fieldId, 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
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)
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
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)
if ok then
ADFields.cache[entry.id] = info
else
Logging.warning("ADSmartPickup: F%s kunne ikke scannes: %s", tostring(entry.id), tostring(info))
end
end
function ADFields.list()
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
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