diff --git a/FS25_ADSmartPickup/adFields.lua b/FS25_ADSmartPickup/adFields.lua new file mode 100644 index 0000000..dff1bc1 --- /dev/null +++ b/FS25_ADSmartPickup/adFields.lua @@ -0,0 +1,223 @@ +-- 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 diff --git a/tests/test_adFields.lua b/tests/test_adFields.lua new file mode 100644 index 0000000..0338292 --- /dev/null +++ b/tests/test_adFields.lua @@ -0,0 +1,33 @@ +-- Kør: luajit tests/test_adFields.lua (fra repo-roden) +dofile("FS25_ADSmartPickup/adFields.lua") +local F = ADFields +local failures = 0 +local function check(name, actual, expected) + if actual == expected then print("OK " .. name) else + failures = failures + 1 + print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) + end +end + +-- F1/F2: markørnavne +check("F1 F109", F.parseMarkerName("F109"), 109) +check("F1 småt", F.parseMarkerName("f7"), 7) +check("F1 mellemrum", F.parseMarkerName(" F 12 "), 12) +check("F2 andet navn", F.parseMarkerName("Field 109"), nil) +check("F2 tekst efter", F.parseMarkerName("F109 wait"), nil) +check("F2 nil", F.parseMarkerName(nil), nil) + +-- F3: slåafgrøde = MOWER giver et skår som TEDDER gør til DRY…; halm udelukket +local names = {[10] = "GRASS_WINDROW", [11] = "DRYGRASS_WINDROW", [20] = "STRAW", [30] = "CLOVER_WINDROW", [31] = "DRYCLOVER_WINDROW", [40] = "BEANSTRAW"} +local mower = {[1] = {fillTypeIndex = 10}, [2] = {fillTypeIndex = 20}, [3] = {fillTypeIndex = 30}, [4] = {fillTypeIndex = 40}} +local tedder = {[10] = {targetFillTypeIndex = 11}, [30] = {targetFillTypeIndex = 31}, [40] = {targetFillTypeIndex = 20}} +local mowable = F.buildMowable(mower, tedder, function(index) return names[index] end) +check("F3 græs", mowable[1] ~= nil, true) +check("F3 græs skår", mowable[1].windrow, 10) +check("F3 græs hø", mowable[1].hay, 11) +check("F3 rug (halm) ikke", mowable[2], nil) +check("F3 kløver", mowable[3].hay, 31) +check("F3 bønnehalm -> halm ikke", mowable[4], nil) + +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)