perf(marker): markscanning som arbejdskø — højst ét prøvepunkt pr. frame (v1.27.2.0)
In-game 26/9: hakket kom sjældnere da scanningen gik fra 1 s til 2 s -> scanningen af én mark i ét frame var årsagen (opslag gennemgår alle ~40 afgrødetyper pr. punkt). Coroutines findes ikke i FS25 (Giants fjernede dem, jf. Courseplays HybridAStar), derfor en eksplicit kø. Tidsmåling bruger netGetTime. Test (test_adFieldsScan) beviser: færdig, korrekt resultat, <= 3 opslag pr. frame.
This commit is contained in:
@@ -8,7 +8,6 @@ ADFieldJobs = {}
|
|||||||
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
||||||
ADFieldJobs.TICK_MS = 1000
|
ADFieldJobs.TICK_MS = 1000
|
||||||
-- én mark scannes hvert andet sekund; passive marker (manuelle, uden mål) vurderes i ti portioner
|
-- én mark scannes hvert andet sekund; passive marker (manuelle, uden mål) vurderes i ti portioner
|
||||||
ADFieldJobs.SCAN_EVERY_TICKS = 2
|
|
||||||
ADFieldJobs.PASSIVE_SLICES = 10
|
ADFieldJobs.PASSIVE_SLICES = 10
|
||||||
-- tidsmåling: tager et tick mere end dette, skrives det i loggen (højst én gang pr. minut)
|
-- tidsmåling: tager et tick mere end dette, skrives det i loggen (højst én gang pr. minut)
|
||||||
ADFieldJobs.SLOW_TICK_MS = 3
|
ADFieldJobs.SLOW_TICK_MS = 3
|
||||||
@@ -269,9 +268,12 @@ local function stepField(adEnv, info, env)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Realtid i ms: netGetTime (bruges overalt i FS), ellers getTimeSec.
|
||||||
local function clockMs()
|
local function clockMs()
|
||||||
local ok, seconds = pcall(getTimeSec)
|
local ok, ms = pcall(function() return netGetTime() end)
|
||||||
return ok and type(seconds) == "number" and seconds * 1000 or nil
|
if ok and type(ms) == "number" then return ms end
|
||||||
|
local okSec, seconds = pcall(function() return getTimeSec() end)
|
||||||
|
return okSec and type(seconds) == "number" and seconds * 1000 or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Et langsomt tick (> SLOW_TICK_MS) skrives i loggen med fordeling, højst én gang pr. minut.
|
-- Et langsomt tick (> SLOW_TICK_MS) skrives i loggen med fordeling, højst én gang pr. minut.
|
||||||
@@ -286,15 +288,17 @@ end
|
|||||||
function ADFieldJobs.tick(adEnv, dt)
|
function ADFieldJobs.tick(adEnv, dt)
|
||||||
ADFieldJobs.now = ADFieldJobs.now + dt
|
ADFieldJobs.now = ADFieldJobs.now + dt
|
||||||
ADFieldJobs.timer = ADFieldJobs.timer + dt
|
ADFieldJobs.timer = ADFieldJobs.timer + dt
|
||||||
|
if ADFieldJobs.loaded then
|
||||||
|
-- markscanningen kører et lille stykke HVERT frame (coroutine), aldrig en hel mark i ét frame
|
||||||
|
local okScan, errScan = pcall(ADFields.scanStep, adEnv, g_currentMission:getFarmId(), ADFieldJobs.now)
|
||||||
|
if not okScan then Logging.warning(ADFieldJobs.LOG_PREFIX .. "markscanning: %s", tostring(errScan)) end
|
||||||
|
end
|
||||||
if ADFieldJobs.timer < ADFieldJobs.TICK_MS then return end
|
if ADFieldJobs.timer < ADFieldJobs.TICK_MS then return end
|
||||||
ADFieldJobs.timer = 0
|
ADFieldJobs.timer = 0
|
||||||
ADFieldJobs.tickCount = ADFieldJobs.tickCount + 1
|
ADFieldJobs.tickCount = ADFieldJobs.tickCount + 1
|
||||||
if not ADFieldJobs.loaded then ADFieldJobs.load() end
|
if not ADFieldJobs.loaded then ADFieldJobs.load() end
|
||||||
local t0 = clockMs()
|
local t0 = clockMs()
|
||||||
if ADFieldJobs.tickCount % ADFieldJobs.SCAN_EVERY_TICKS == 0 then
|
local t1 = t0
|
||||||
ADFields.scanNext(adEnv, g_currentMission:getFarmId(), ADFieldJobs.now)
|
|
||||||
end
|
|
||||||
local t1 = clockMs()
|
|
||||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||||
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
||||||
if not ok then
|
if not ok then
|
||||||
|
|||||||
+193
-134
@@ -127,15 +127,6 @@ local function readWeed(x, z)
|
|||||||
return value
|
return value
|
||||||
end
|
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 function missionSettings()
|
||||||
local info = g_currentMission ~= nil and g_currentMission.missionInfo or {}
|
local info = g_currentMission ~= nil and g_currentMission.missionInfo or {}
|
||||||
local function flag(value) return value == nil or value == true end
|
local function flag(value) return value == nil or value == true end
|
||||||
@@ -146,22 +137,6 @@ local function missionSettings()
|
|||||||
}
|
}
|
||||||
end
|
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 function fieldOwner(field)
|
||||||
local farmland = field.farmland or (field.getFarmland ~= nil and field:getFarmland()) or nil
|
local farmland = field.farmland or (field.getFarmland ~= nil and field:getFarmland()) or nil
|
||||||
if farmland == nil then return nil end
|
if farmland == nil then return nil end
|
||||||
@@ -176,75 +151,6 @@ local function isOnField(x, z)
|
|||||||
return FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z) and true or false
|
return FSDensityMapUtil.getFieldDataAtWorldPosition(x, y, z) and true or false
|
||||||
end
|
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 function findMarker(adEnv, fieldId, x, z)
|
||||||
local best, bestDistance = nil, math.huge
|
local best, bestDistance = nil, math.huge
|
||||||
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
|
||||||
@@ -262,87 +168,240 @@ local function findMarker(adEnv, fieldId, x, z)
|
|||||||
return nil, best ~= nil and bestDistance or nil
|
return nil, best ~= nil and bestDistance or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function scanField(adEnv, field, fieldId)
|
-- ---- scanning som arbejdskø ----
|
||||||
local points, verts = samplePoints(field)
|
-- En marks scanning er en kø af små opgaver (ét prøvepunkt pr. opgave), og scanStep kører én opgave pr.
|
||||||
local info = {id = fieldId, field = field, points = points, verts = verts, mowable = false, readyRatio = 0}
|
-- frame. In-game 26/9 gav en hel mark i ét frame et hak (flere opslag gennemgår alle kortets ~40
|
||||||
info.x, info.z = field.posX, field.posZ
|
-- afgrødetyper pr. punkt). Coroutines findes ikke i FS25 (Giants fjernede dem), derfor en kø.
|
||||||
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
|
-- (eksplicit hale: # er udefineret på en tabel med huller, og udførte opgaver nulstilles)
|
||||||
local mowable = info.fruitIndex ~= nil and ADFields.getMowable()[info.fruitIndex] or nil
|
local function push(scan, task)
|
||||||
if info.fruitIndex ~= nil then
|
scan.tail = (scan.tail or 0) + 1
|
||||||
local fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
scan.queue[scan.tail] = task
|
||||||
local fillType = fruit ~= nil and fruit.fillType or nil
|
end
|
||||||
info.cropTitle = fillType ~= nil and fillType.title or (fruit ~= nil and fruit.name) or "?"
|
|
||||||
|
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
|
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
|
if mowable ~= nil then
|
||||||
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
|
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
|
||||||
end
|
end
|
||||||
local fruit = nil
|
local fruit = nil
|
||||||
if info.fruitIndex ~= nil then
|
if info.fruitIndex ~= nil then
|
||||||
info.readyRatio, info.anyPixels = readyRatio(info.fruitIndex, points)
|
info.readyRatio = scan.anySum > 0 and scan.readySum / scan.anySum or 0
|
||||||
|
info.anyPixels = scan.anySum
|
||||||
fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
fruit = g_fruitTypeManager:getFruitTypeByIndex(info.fruitIndex)
|
||||||
info.fruitName = fruit ~= nil and fruit.name or nil
|
info.fruitName = fruit ~= nil and fruit.name or nil
|
||||||
info.harvestFillType = fruit ~= nil and fruit.fillType ~= nil and fruit.fillType.index 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
|
end
|
||||||
info.kind = ADFields.classify(info.fruitIndex, info.mowable, info.readyRatio)
|
info.kind = ADFields.classify(info.fruitIndex, info.mowable, info.readyRatio)
|
||||||
info.phase = fruit ~= nil and ADFields.growthPhase(growthStateAt(points), fruit) or nil
|
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)
|
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)
|
-- 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
|
if info.kind == "harvest" and (info.phase == "cut" or info.phase == "withered" or info.phase == "none") then
|
||||||
info.kind = "bare"
|
info.kind = "bare"
|
||||||
end
|
end
|
||||||
readSoil(info)
|
local settings = scan.settings or missionSettings()
|
||||||
local previous = ADFields.cache[fieldId]
|
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
|
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
|
info.markerId, info.markerName, info.markerDistance, info.markerCheckedAt = previous.markerId, previous.markerName, previous.markerDistance, previous.markerCheckedAt
|
||||||
elseif info.x ~= nil then
|
elseif info.x ~= nil then
|
||||||
local marker, distance = findMarker(adEnv, fieldId, info.x, info.z)
|
local marker, distance = findMarker(adEnv, info.id, info.x, info.z)
|
||||||
info.markerId = marker ~= nil and marker.id or nil
|
info.markerId = marker ~= nil and marker.id or nil
|
||||||
info.markerName = marker ~= nil and marker.name or nil
|
info.markerName = marker ~= nil and marker.name or nil
|
||||||
info.markerDistance = distance
|
info.markerDistance = distance
|
||||||
info.markerCheckedAt = ADFields.now
|
info.markerCheckedAt = ADFields.now
|
||||||
end
|
end
|
||||||
return info
|
ADFields.cache[info.id] = info
|
||||||
|
ADFields.listCache = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ownedFields(farmId)
|
-- Marklisten (ejede marker) i portioner af 25 marker pr. frame.
|
||||||
local result = {}
|
local function newOwnedScan(farmId)
|
||||||
local all = g_fieldManager ~= nil and (g_fieldManager:getFields() or g_fieldManager.fields) or {}
|
local all = {}
|
||||||
for _, field in pairs(all) do
|
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
|
local id = field.getId ~= nil and field:getId() or field.fieldId
|
||||||
if id ~= nil and fieldOwner(field) == farmId then
|
if id ~= nil and fieldOwner(field) == farmId then
|
||||||
table.insert(result, {id = id, field = field})
|
table.insert(scan.result, {id = id, field = field})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(result, function(a, b) return a.id < b.id end)
|
end)
|
||||||
return result
|
end
|
||||||
|
return scan
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Scanner næste mark (round-robin) og returnerer alle kendte. Kaldes hvert sekund af job-løkken.
|
local function finishOwnedScan(scan)
|
||||||
function ADFields.scanNext(adEnv, farmId, now)
|
table.sort(scan.result, function(a, b) return a.id < b.id end)
|
||||||
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 = {}
|
local ids = {}
|
||||||
for _, entry in ipairs(owned) do ids[entry.id] = true end
|
for _, entry in ipairs(scan.result) do ids[entry.id] = true end
|
||||||
for id in pairs(ADFields.cache) do
|
for id in pairs(ADFields.cache) do
|
||||||
if not ids[id] then ADFields.cache[id] = nil end
|
if not ids[id] then ADFields.cache[id] = nil end
|
||||||
end
|
end
|
||||||
ADFields.order = owned
|
ADFields.order = scan.result
|
||||||
ADFields.listCache = nil
|
ADFields.listCache = nil
|
||||||
end
|
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
|
local owned = ADFields.order
|
||||||
if #owned == 0 then return end
|
if #owned == 0 then return end
|
||||||
|
ADFields.lastScanStart = ADFields.now
|
||||||
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
|
ADFields.scanIndex = ADFields.scanIndex % #owned + 1
|
||||||
local entry = owned[ADFields.scanIndex]
|
scan = newScan(adEnv, 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
|
||||||
|
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
|
end
|
||||||
|
|
||||||
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
|
-- Samme liste indtil næste scanning (list() kaldes hvert sekund; ingen ny tabel hver gang).
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
<modDesc descVersion="107">
|
<modDesc descVersion="107">
|
||||||
<author>masterdraco</author>
|
<author>masterdraco</author>
|
||||||
<version>1.27.1.0</version>
|
<version>1.27.2.0</version>
|
||||||
<title>
|
<title>
|
||||||
<en>AD Smart Pickup</en>
|
<en>AD Smart Pickup</en>
|
||||||
</title>
|
</title>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# AD Smart Pickup — brugermanual
|
# AD Smart Pickup — brugermanual
|
||||||
|
|
||||||
Version 1.27.1 · Farming Simulator 25 · tillæg til AutoDrive
|
Version 1.27.2 · Farming Simulator 25 · tillæg til AutoDrive
|
||||||
|
|
||||||
*English version: [USER_MANUAL.md](USER_MANUAL.md)*
|
*English version: [USER_MANUAL.md](USER_MANUAL.md)*
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Testplan — Markarbejde (fanen Marker), version 1.27.1.0
|
# Testplan — Markarbejde (fanen Marker), version 1.27.2.0
|
||||||
|
|
||||||
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den.
|
Stor test-session. Kør testene i rækkefølge: de første bygger opsætningen, de senere bygger på den.
|
||||||
Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
|
Hver test har **Gør**, **Forvent** og **Log** (linjer i `log.txt`, der beviser at det virkede). Loglinjer
|
||||||
@@ -11,7 +11,7 @@ samme rækkefølge.
|
|||||||
|
|
||||||
## 0. Forberedelse
|
## 0. Forberedelse
|
||||||
|
|
||||||
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.27.1.0) FS25_ADSmartPickup`.
|
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.27.2.0) FS25_ADSmartPickup`.
|
||||||
- [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …"
|
- [ ] **0.2 Fanen Marker åbner.** ESC → Smart Pickup → fanen **Marker**. Forvent: "Marker indlæses …"
|
||||||
i få sekunder, derefter én række pr. ejet mark (også korn-, roe- og bare marker, ikke kun græs).
|
i få sekunder, derefter én række pr. ejet mark (også korn-, roe- og bare marker, ikke kun græs).
|
||||||
Afgrøde-kolonnen viser afgrøden eller "bar jord".
|
Afgrøde-kolonnen viser afgrøden eller "bar jord".
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# AD Smart Pickup — user manual
|
# AD Smart Pickup — user manual
|
||||||
|
|
||||||
Version 1.27.1 · Farming Simulator 25 · add-on for AutoDrive
|
Version 1.27.2 · Farming Simulator 25 · add-on for AutoDrive
|
||||||
|
|
||||||
*Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)*
|
*Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)*
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
-- Kør: luajit tests/test_adFieldsScan.lua (fra repo-roden)
|
||||||
|
-- Markscanningen som arbejdskø: bliver færdig, giver rigtigt resultat, og intet frame laver mere end ét
|
||||||
|
-- prøvepunkts arbejde (in-game 26/9: en hel mark i ét frame gav hak).
|
||||||
|
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
|
||||||
|
Logging = {warning = function(fmt, ...) print("WARN " .. string.format(fmt, ...)) end, info = function() end}
|
||||||
|
|
||||||
|
-- falsk verden: én mark (F7) med hvede der er klar, polygon med 4 hjørner
|
||||||
|
local GRASS, WHEAT = 1, 3
|
||||||
|
local nodes = {[101] = {0, 0}, [102] = {100, 0}, [103] = {100, 100}, [104] = {0, 100}}
|
||||||
|
function getWorldTranslation(node) local p = nodes[node]; return p[1], 0, p[2] end
|
||||||
|
function getTerrainHeightAtWorldPos() return 0 end
|
||||||
|
local calls = 0
|
||||||
|
FSDensityMapUtil = {
|
||||||
|
getFieldDataAtWorldPosition = function() calls = calls + 1; return 1, 6 end, -- på mark, vækststadie 6
|
||||||
|
getFruitTypeIndexAtWorldPos = function() calls = calls + 1; return WHEAT end,
|
||||||
|
getFruitArea = function(_, _, _, _, _, _, _, any) calls = calls + 1; return 0, any and 10 or 10 end,
|
||||||
|
}
|
||||||
|
FieldDensityMap = {SPRAY_LEVEL = 4, LIME_LEVEL = 5, PLOW_LEVEL = 6}
|
||||||
|
DensityCoordType = {POINT_VECTOR_VECTOR = 1}
|
||||||
|
DensityMapModifier = {new = function() return {setParallelogramWorldCoords = function() end, executeGet = function() calls = calls + 1; return 1, 1 end} end}
|
||||||
|
g_fruitTypeManager = {
|
||||||
|
getFruitTypeByIndex = function(_, index) return {name = index == WHEAT and "WHEAT" or "GRASS", fillType = {index = 50, title = "Hvede"}, minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9} end,
|
||||||
|
getConverterDataByName = function() return {} end,
|
||||||
|
}
|
||||||
|
g_fillTypeManager = {getConverterDataByName = function() return {} end, getFillTypeNameByIndex = function() return "X" end}
|
||||||
|
g_farmlandManager = {getFarmlandOwner = function() return 1 end}
|
||||||
|
local field = {getId = function() return 7 end, farmland = {id = 70}, polygonPoints = {101, 102, 103, 104}, posX = 50, posZ = 50}
|
||||||
|
g_fieldManager = {getFields = function() return {field} end}
|
||||||
|
g_currentMission = {terrainRootNode = 1, missionInfo = {}, fieldGroundSystem = {
|
||||||
|
getDensityMapData = function() return 1, 0, 2 end, getMaxValue = function() return 2 end}}
|
||||||
|
local adEnv = {ADGraphManager = {
|
||||||
|
getMapMarkers = function() return {{id = 900, name = "F7"}, {id = 901, name = "Silo"}} end,
|
||||||
|
getWayPointById = function(_, id) return {x = 40, z = 40} end,
|
||||||
|
}}
|
||||||
|
|
||||||
|
dofile("FS25_ADSmartPickup/adFields.lua")
|
||||||
|
|
||||||
|
local maxPerFrame, frames = 0, 0
|
||||||
|
local now = 0
|
||||||
|
for _ = 1, 200 do
|
||||||
|
calls = 0
|
||||||
|
ADFields.scanStep(adEnv, 1, now)
|
||||||
|
if calls > maxPerFrame then maxPerFrame = calls end
|
||||||
|
frames = frames + 1
|
||||||
|
now = now + 16
|
||||||
|
if ADFields.getInfo(7) ~= nil then break end
|
||||||
|
end
|
||||||
|
-- markliste først, så marken (efter SCAN_INTERVAL) — kør videre til marken er scannet
|
||||||
|
for _ = 1, 400 do
|
||||||
|
if ADFields.getInfo(7) ~= nil then break end
|
||||||
|
calls = 0
|
||||||
|
ADFields.scanStep(adEnv, 1, now)
|
||||||
|
if calls > maxPerFrame then maxPerFrame = calls end
|
||||||
|
now = now + 16
|
||||||
|
end
|
||||||
|
local info = ADFields.getInfo(7)
|
||||||
|
check("SC1 marken er scannet", info ~= nil, true)
|
||||||
|
check("SC2 afgrøde", info and info.fruitName, "WHEAT")
|
||||||
|
check("SC3 art = høst", info and info.kind, "harvest")
|
||||||
|
check("SC4 klar-andel", info and info.readyRatio, 1)
|
||||||
|
check("SC5 fase klar", info and info.phase, "ready")
|
||||||
|
check("SC6 markør F7", info and info.markerId, 900)
|
||||||
|
check("SC7 prøvepunkter", info and #info.points, 5)
|
||||||
|
check("SC8 gødning læst", info and info.fert, 1)
|
||||||
|
check("SC9 højst ét punkts arbejde pr. frame (<= 4 opslag)", maxPerFrame <= 4, true)
|
||||||
|
print(" (flest opslag i ét frame: " .. maxPerFrame .. ")")
|
||||||
|
check("SC10 marklisten", #ADFields.list(), 1)
|
||||||
|
|
||||||
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
|
os.exit(failures == 0 and 0 or 1)
|
||||||
Reference in New Issue
Block a user