c34a472b1c
In-game 26/9: lille hak ca. hvert sekund. Markarbejdets tick kopierede alle markers data hvert sekund (affald -> GC-pause), hentede alle kortets marker og alle AD-markører.
383 lines
17 KiB
Lua
383 lines
17 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 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 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
|
|
return {
|
|
limeRequired = flag(info.limeRequired),
|
|
plowingRequired = flag(info.plowingRequiredEnabled),
|
|
weedsEnabled = flag(info.weedsEnabled),
|
|
}
|
|
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
|
|
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, 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
|
|
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
|
|
end
|
|
local fruit = nil
|
|
if info.fruitIndex ~= nil then
|
|
info.readyRatio, info.anyPixels = readyRatio(info.fruitIndex, points)
|
|
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
|
|
end
|
|
info.kind = ADFields.classify(info.fruitIndex, info.mowable, info.readyRatio)
|
|
info.phase = fruit ~= nil and ADFields.growthPhase(growthStateAt(points), 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]
|
|
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)
|
|
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.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
|
|
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
|