Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 944e61d1a8 | |||
| b1a65929a7 | |||
| ae1362d164 | |||
| 65986b1793 | |||
| 969db0a699 | |||
| 7b0a9e452b | |||
| 02e355e700 | |||
| 793352c4c4 | |||
| 9be60d4b1c | |||
| 9cab4b937d | |||
| 03c05b7608 | |||
| 7f83cc4d28 | |||
| cf7a2a51e5 | |||
| ba88eb5f99 | |||
| 4c88118e56 | |||
| 048f5e6547 | |||
| 52250dc35e | |||
| 6f387c051f | |||
| b1f1aaa148 | |||
| 716045a36b | |||
| bade84129b | |||
| 61a32720ea |
@@ -5,7 +5,47 @@
|
||||
|
||||
ADCpBridge = {}
|
||||
|
||||
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon"}
|
||||
ADCpBridge.SPECS = {"spec_mower", "spec_tedder", "spec_windrower", "spec_forageWagon", "spec_combine",
|
||||
"spec_sowingMachine", "spec_cultivator", "spec_plow", "spec_roller", "spec_weeder", "spec_mulcher"}
|
||||
|
||||
-- Sprøjtens/sprederens fyld -> rolle (tom sprøjte har ingen rolle; den kan ikke sendes ud).
|
||||
ADCpBridge.SPRAY_ROLES = {
|
||||
FERTILIZER = "fertilizer", LIQUIDFERTILIZER = "fertilizer", MANURE = "fertilizer",
|
||||
LIQUIDMANURE = "fertilizer", DIGESTATE = "fertilizer", LIME = "limer", HERBICIDE = "weeder",
|
||||
}
|
||||
function ADCpBridge.sprayRoleForFillType(name)
|
||||
return ADCpBridge.SPRAY_ROLES[name]
|
||||
end
|
||||
|
||||
-- Afgrøder et skærebord/optager kan høste (spec_cutter). Finsnittere (fruitTypeConverters) tager ingen:
|
||||
-- de laver hakkelse, ikke korn/roer.
|
||||
function ADCpBridge.harvestFruitSet(cutterSpec)
|
||||
local fruits = {}
|
||||
if cutterSpec == nil then return fruits end
|
||||
if type(cutterSpec.fruitTypeConverters) == "table" and next(cutterSpec.fruitTypeConverters) ~= nil then
|
||||
return fruits
|
||||
end
|
||||
-- (ikke ipairs over {a, b}: den stopper ved første nil)
|
||||
for _, key in ipairs({"fruitTypes", "fruitTypeIndices"}) do
|
||||
local source = cutterSpec[key]
|
||||
if type(source) == "table" then
|
||||
for key, value in pairs(source) do
|
||||
if type(value) == "number" then fruits[value] = true
|
||||
elseif value == true and type(key) == "number" then fruits[key] = true end
|
||||
end
|
||||
end
|
||||
end
|
||||
return fruits
|
||||
end
|
||||
|
||||
-- Må afgrøden sås i perioden? (growthDataSeasonal.periods[p].plantingAllowed, bevist i AI-Bonden).
|
||||
-- Ukendte data = tilladt (kort uden sæsondata).
|
||||
function ADCpBridge.isPlantableIn(fruit, period)
|
||||
local periods = fruit ~= nil and fruit.growthDataSeasonal ~= nil and fruit.growthDataSeasonal.periods or nil
|
||||
local data = periods ~= nil and periods[period] or nil
|
||||
if type(data) ~= "table" then return true end
|
||||
return data.plantingAllowed == true
|
||||
end
|
||||
|
||||
function ADCpBridge.isAvailable()
|
||||
return AIJobType ~= nil and AIJobType.FIELDWORK_CP ~= nil
|
||||
@@ -22,20 +62,171 @@ local function collect(vehicle, list)
|
||||
return list
|
||||
end
|
||||
|
||||
-- Sprøjtens/sprederens rolle ud fra det fyld den har på (tom -> nil).
|
||||
local function sprayRole(object)
|
||||
if object.spec_sprayer == nil or object.getSprayerFillUnitIndex == nil then return nil end
|
||||
local ok, role = pcall(function()
|
||||
local unit = object:getSprayerFillUnitIndex()
|
||||
if (object:getFillUnitFillLevel(unit) or 0) <= 0 then return nil end
|
||||
local name = g_fillTypeManager:getFillTypeNameByIndex(object:getFillUnitFillType(unit))
|
||||
return ADCpBridge.sprayRoleForFillType(name)
|
||||
end)
|
||||
return ok and role or nil
|
||||
end
|
||||
|
||||
function ADCpBridge.getSpecs(vehicle)
|
||||
local specs = {}
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
for _, name in ipairs(ADCpBridge.SPECS) do
|
||||
if object[name] ~= nil then specs[name] = true end
|
||||
end
|
||||
specs.sprayRole = specs.sprayRole or sprayRole(object)
|
||||
end
|
||||
if vehicle.getCanStartCpCombineUnloader ~= nil then
|
||||
local ok, can = pcall(vehicle.getCanStartCpCombineUnloader, vehicle)
|
||||
specs.canUnload = ok and can == true or nil
|
||||
end
|
||||
return specs
|
||||
end
|
||||
|
||||
-- Afgrøder høsteren kan tage (alle skærebord/optagere i kæden). nil = ikke en høster.
|
||||
function ADCpBridge.getHarvestFruits(vehicle)
|
||||
if vehicle.spec_combine == nil then return nil end
|
||||
local fruits = {}
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
for fruit in pairs(ADCpBridge.harvestFruitSet(object.spec_cutter)) do fruits[fruit] = true end
|
||||
end
|
||||
return fruits
|
||||
end
|
||||
|
||||
-- Mekanisk strigle (kun småt ukrudt) frem for ukrudtssprøjte.
|
||||
function ADCpBridge.isMechanicalWeeder(vehicle)
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_weeder ~= nil then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function seederOf(vehicle)
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sowingMachine ~= nil then return object end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Har såmaskinen frø på? (fyldenheder med SEEDS)
|
||||
function ADCpBridge.hasSeeds(vehicle)
|
||||
local seeder = seederOf(vehicle)
|
||||
if seeder == nil then return false end
|
||||
local ok, has = pcall(function()
|
||||
local seedsIndex = g_fillTypeManager:getFillTypeIndexByName("SEEDS")
|
||||
for index, unit in ipairs(seeder.spec_fillUnit ~= nil and seeder.spec_fillUnit.fillUnits or {}) do
|
||||
if unit.fillType == seedsIndex and (unit.fillLevel or 0) > 0 then return true end
|
||||
if unit.supportedFillTypes ~= nil and unit.supportedFillTypes[seedsIndex] and seeder:getFillUnitFillLevel(index) > 0 then return true end
|
||||
end
|
||||
return false
|
||||
end)
|
||||
return ok and has == true
|
||||
end
|
||||
|
||||
-- Afgrøder såmaskinen kan så (spec_sowingMachine.seeds).
|
||||
function ADCpBridge.getSeedFruits(vehicle)
|
||||
local seeder = seederOf(vehicle)
|
||||
local fruits = {}
|
||||
local seeds = seeder ~= nil and seeder.spec_sowingMachine.seeds or nil
|
||||
for _, fruit in pairs(seeds or {}) do fruits[fruit] = true end
|
||||
return fruits
|
||||
end
|
||||
|
||||
-- Sæt såmaskinens afgrøde (setSeedIndex(slot), bevist i AI-Bonden).
|
||||
function ADCpBridge.setSeedFruit(vehicle, fruitIndex)
|
||||
local seeder = seederOf(vehicle)
|
||||
if seeder == nil then return false, "ingen såmaskine" end
|
||||
for slot, fruit in pairs(seeder.spec_sowingMachine.seeds or {}) do
|
||||
if fruit == fruitIndex then
|
||||
if seeder.setSeedIndex ~= nil then
|
||||
local ok, err = pcall(seeder.setSeedIndex, seeder, slot)
|
||||
return ok, not ok and tostring(err) or nil
|
||||
end
|
||||
end
|
||||
end
|
||||
return false, "såmaskinen kan ikke så afgrøden"
|
||||
end
|
||||
|
||||
function ADCpBridge.isPlantableNow(fruitIndex)
|
||||
local fruit = g_fruitTypeManager:getFruitTypeByIndex(fruitIndex)
|
||||
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
|
||||
return ADCpBridge.isPlantableIn(fruit, environment ~= nil and environment.currentPeriod or 0)
|
||||
end
|
||||
|
||||
-- Hvilken vare sættet bruger og kan få fyldt på af AutoDrive: frø (såmaskine) eller sprøjtens/sprederens
|
||||
-- nuværende fyld (gødning, kalk, sprøjtemiddel). nil = intet at fylde på (strigle, plov ...).
|
||||
function ADCpBridge.getRefillFillType(vehicle)
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sowingMachine ~= nil and g_fillTypeManager ~= nil then
|
||||
return g_fillTypeManager:getFillTypeIndexByName("SEEDS")
|
||||
end
|
||||
end
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
|
||||
local ok, fillType = pcall(function() return object:getFillUnitFillType(object:getSprayerFillUnitIndex()) end)
|
||||
if ok and fillType ~= nil and fillType ~= 0 then return fillType end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hvor meget af påfyldningsvaren sættet har tilbage (liter, alle fyldenheder med varen).
|
||||
function ADCpBridge.getRefillLevel(vehicle, fillType)
|
||||
local total = 0
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
pcall(function()
|
||||
for index, unit in ipairs(object.spec_fillUnit ~= nil and object.spec_fillUnit.fillUnits or {}) do
|
||||
if unit.fillType == fillType then total = total + (object:getFillUnitFillLevel(index) or 0) end
|
||||
end
|
||||
end)
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
-- Stop køretøjets AI-job (Courseplay/hjælper).
|
||||
function ADCpBridge.stop(vehicle)
|
||||
pcall(function()
|
||||
if vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() and vehicle.stopCurrentAIJob ~= nil then
|
||||
vehicle:stopCurrentAIJob(AIMessageSuccessStoppedByUser.new())
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Variabel bredde (Unrealistic Pack m.fl.) først, som Courseplays WorkWidthUtil: AI-markørerne står på
|
||||
-- grundbredden (læssevogn 2,4 m selv om den er sat til 40,8 m).
|
||||
local function variableWidth(object)
|
||||
if object.getVariableWorkWidth == nil then return nil end
|
||||
local okLeft, left, _, leftValid = pcall(object.getVariableWorkWidth, object, true)
|
||||
local okRight, right, _, rightValid = pcall(object.getVariableWorkWidth, object)
|
||||
if okLeft and okRight and leftValid and rightValid and type(left) == "number" and type(right) == "number" then
|
||||
return math.abs(left) + math.abs(right)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Courseplays egen bredde-indstilling (den i HUD'en) er den marken faktisk køres med.
|
||||
local function courseplayWidth(vehicle)
|
||||
if vehicle.getCourseGeneratorSettings == nil then return nil end
|
||||
local ok, width = pcall(function() return vehicle:getCourseGeneratorSettings().workWidth:getValue() end)
|
||||
if ok and type(width) == "number" and width > 0 then return width end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ADCpBridge.getWorkWidth(vehicle)
|
||||
local cpWidth = courseplayWidth(vehicle)
|
||||
if cpWidth ~= nil then return cpWidth end
|
||||
local best = 0
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
if object.getAIMarkers ~= nil then
|
||||
local variable = variableWidth(object)
|
||||
if variable ~= nil then
|
||||
best = math.max(best, variable)
|
||||
elseif object.getAIMarkers ~= nil then
|
||||
pcall(function() if object.updateAIMarkerWidth ~= nil then object:updateAIMarkerWidth() end end)
|
||||
local ok, _, _, _, _, width = pcall(object.getAIMarkers, object)
|
||||
if ok and type(width) == "number" and width > best then best = width end
|
||||
@@ -49,7 +240,97 @@ function ADCpBridge.isWorking(vehicle)
|
||||
return vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() or false
|
||||
end
|
||||
|
||||
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone)
|
||||
-- Courseplays værktøjsforskydning (venstre positiv) på køretøjet; den der flytter kursuslinjen.
|
||||
function ADCpBridge.getToolOffset(vehicle)
|
||||
if vehicle == nil or vehicle.getCpSettings == nil then return 0 end
|
||||
local ok, value = pcall(function() return vehicle:getCpSettings().toolOffsetX:getValue() end)
|
||||
return ok and type(value) == "number" and value or 0
|
||||
end
|
||||
|
||||
function ADCpBridge.setToolOffset(vehicle, value)
|
||||
if vehicle == nil or vehicle.getCpSettings == nil then return false end
|
||||
return (pcall(function() vehicle:getCpSettings().toolOffsetX:setFloatValue(value) end))
|
||||
end
|
||||
|
||||
-- Riverens XML: workArea-nr (1-baseret) -> dropWindrowWorkAreaIndex. Bruges når spillet ikke har feltet
|
||||
-- på workArea-tabellen. Cachet pr. fil.
|
||||
ADCpBridge.dropIndexCache = {}
|
||||
function ADCpBridge.readDropIndicesFromXml(fileName)
|
||||
if ADCpBridge.dropIndexCache[fileName] ~= nil then return ADCpBridge.dropIndexCache[fileName] end
|
||||
local indices = {}
|
||||
local ok = pcall(function()
|
||||
local xmlFile = XMLFile.load("adspuRake", fileName)
|
||||
if xmlFile == nil then return end
|
||||
local position = 0
|
||||
xmlFile:iterate("vehicle.workAreas.workArea", function(_, key)
|
||||
position = position + 1
|
||||
local dropIndex = xmlFile:getInt(key .. ".windrower#dropWindrowWorkAreaIndex")
|
||||
if dropIndex ~= nil then indices[position] = dropIndex end
|
||||
end)
|
||||
xmlFile:delete()
|
||||
end)
|
||||
if not ok then indices = {} end
|
||||
ADCpBridge.dropIndexCache[fileName] = indices
|
||||
return indices
|
||||
end
|
||||
|
||||
-- Hvor langt til venstre (positiv) for rivens kursuslinje skåret lægges: midten af nedlægningsområdet i
|
||||
-- traktorens ramme minus rivens egen CP-forskydning. Midterrive ≈ 0, siderive fx 3 m. nil = ingen river.
|
||||
function ADCpBridge.getWindrowOffset(vehicle)
|
||||
local root = vehicle.rootNode
|
||||
local sum, count = 0, 0
|
||||
for _, object in ipairs(collect(vehicle)) do
|
||||
local areas = object.spec_windrower ~= nil and object.spec_workArea ~= nil and object.spec_workArea.workAreas or nil
|
||||
if areas ~= nil then
|
||||
local fromXml = nil
|
||||
for position, workArea in ipairs(areas) do
|
||||
local dropIndex = workArea.dropWindrowWorkAreaIndex
|
||||
if dropIndex == nil and object.configFileName ~= nil then
|
||||
fromXml = fromXml or ADCpBridge.readDropIndicesFromXml(object.configFileName)
|
||||
dropIndex = fromXml[position]
|
||||
end
|
||||
local drop = dropIndex ~= nil and areas[dropIndex] or nil
|
||||
if drop ~= nil and drop.width ~= nil and drop.height ~= nil then
|
||||
local widthX = localToLocal(drop.width, root, 0, 0, 0)
|
||||
local heightX = localToLocal(drop.height, root, 0, 0, 0)
|
||||
sum, count = sum + (widthX + heightX) / 2, count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if count == 0 then return nil end
|
||||
return sum / count - ADCpBridge.getToolOffset(vehicle)
|
||||
end
|
||||
|
||||
-- Køretøjets nuværende Courseplay-kursus (slåmaskinens kursus genbruges af de næste trin).
|
||||
function ADCpBridge.getCourse(vehicle)
|
||||
if vehicle == nil or vehicle.getFieldWorkCourse == nil then return nil end
|
||||
local ok, course = pcall(vehicle.getFieldWorkCourse, vehicle)
|
||||
return ok and course or nil
|
||||
end
|
||||
|
||||
-- Multi-tool: sæt antal sæt på kursus-generatoren før kurset laves (1 = normalt kursus).
|
||||
local function setMultiTools(vehicle, n)
|
||||
pcall(function() vehicle:getCourseGeneratorSettings().multiTools:setValue(n or 1) end)
|
||||
end
|
||||
|
||||
-- Sæt køretøjets bane på et multi-tool-kursus (samme som HUD'ens "Baneforskydning").
|
||||
local function setLane(vehicle, params, lane)
|
||||
if lane == nil then return end
|
||||
pcall(function()
|
||||
local course = vehicle:getFieldWorkCourse()
|
||||
if course ~= nil and course.getMultiTools ~= nil and course:getMultiTools() > 1 then course:setPosition(lane) end
|
||||
end)
|
||||
if params ~= nil and params.laneOffset ~= nil then pcall(function() params.laneOffset:setValue(lane) end) end
|
||||
end
|
||||
|
||||
-- Start Courseplay-markarbejde. opts (valgfri):
|
||||
-- course = færdigt kursus der kopieres (slåmaskinens/rivens, eller trinnets fælles multi-tool-kursus)
|
||||
-- multiTools = antal sæt når kurset laves her (multi-tool-leder)
|
||||
-- lane = køretøjets bane på et multi-tool-kursus (laneOffset -2..2)
|
||||
-- onDone(ok, reason) kaldes én gang; reason "copied" ved succes med kopieret kursus.
|
||||
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
|
||||
opts = opts or {}
|
||||
if not ADCpBridge.isAvailable() then
|
||||
return nil, "Courseplay (FIELDWORK_CP) findes ikke"
|
||||
end
|
||||
@@ -68,16 +349,29 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone)
|
||||
handle.finished = true
|
||||
if not handle.cancelled then onDone(ok, reason) end
|
||||
end
|
||||
local function startWithCourse()
|
||||
setLane(vehicle, params, opts.lane)
|
||||
job:setValues()
|
||||
local ok, err = job:validate(farmId)
|
||||
if not ok then return finish(false, "Courseplay: " .. tostring(err)) end
|
||||
g_currentMission.aiSystem:startJob(job, farmId)
|
||||
finish(true, handle.copiedCourse and "copied" or nil)
|
||||
end
|
||||
local function generateAndStart()
|
||||
if handle.cancelled then return end
|
||||
job:onClickGenerateFieldWorkCourse(function(course)
|
||||
if opts.course ~= nil and vehicle.cpCopyCourse ~= nil then
|
||||
local copied, copyError = pcall(vehicle.cpCopyCourse, vehicle, opts.course)
|
||||
if copied then
|
||||
handle.copiedCourse = true
|
||||
return startWithCourse()
|
||||
end
|
||||
Logging.warning("ADSmartPickup: kunne ikke kopiere kursus: %s — laver nyt", tostring(copyError))
|
||||
end
|
||||
setMultiTools(vehicle, opts.multiTools or 1)
|
||||
job:onClickGenerateFieldWorkCourse(function(generated)
|
||||
if handle.cancelled then return end
|
||||
if not course then return finish(false, "Courseplay kunne ikke lave kursus") end
|
||||
job:setValues()
|
||||
local ok, err = job:validate(farmId)
|
||||
if not ok then return finish(false, "Courseplay: " .. tostring(err)) end
|
||||
g_currentMission.aiSystem:startJob(job, farmId)
|
||||
finish(true)
|
||||
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end
|
||||
startWithCourse()
|
||||
end)
|
||||
end
|
||||
handle.onBoundary = function(_, success, message)
|
||||
@@ -96,3 +390,75 @@ function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone)
|
||||
end
|
||||
return handle
|
||||
end
|
||||
|
||||
-- Aflæsningsvogn under høst: Courseplays combine-unloader (bevist i AI-Bonden). Vognen venter ved
|
||||
-- (waitX, waitZ) — markens F-markør — følger høsteren på marken og kører fuld til station. station =
|
||||
-- den station Smart Pickups silovalg fandt; findes den ikke i Courseplays liste, bruges nærmeste.
|
||||
function ADCpBridge.startUnloader(vehicle, fieldX, fieldZ, waitX, waitZ, station, onDone)
|
||||
if AIJobType == nil or AIJobType.COMBINE_UNLOADER_CP == nil then
|
||||
return nil, "Courseplay (COMBINE_UNLOADER_CP) findes ikke"
|
||||
end
|
||||
local farmId = vehicle:getOwnerFarmId()
|
||||
local job = g_currentMission.aiJobTypeManager:createJob(AIJobType.COMBINE_UNLOADER_CP)
|
||||
if job == nil then return nil, "kunne ikke oprette aflæsningsjob" end
|
||||
pcall(function() job:applyCurrentState(vehicle, g_currentMission, farmId, false, false) end)
|
||||
local params = job.cpJobParameters
|
||||
if params ~= nil then
|
||||
if params.fieldPosition ~= nil then pcall(function() params.fieldPosition:setPosition(fieldX, fieldZ) end) end
|
||||
if params.startPosition ~= nil then pcall(function() params.startPosition:setPosition(waitX, waitZ) end) end
|
||||
if params.useGiantsUnload ~= nil then pcall(function() params.useGiantsUnload:setValue(true) end) end
|
||||
end
|
||||
pcall(function() job:setValues() end)
|
||||
local chosen = nil
|
||||
local ok, stations = pcall(function() return job:getUnloadingStations() end)
|
||||
if ok and type(stations) == "table" then
|
||||
local best, bestDistance = nil, math.huge
|
||||
for _, candidate in ipairs(stations) do
|
||||
if station ~= nil and (candidate == station or candidate.owningPlaceable == station.owningPlaceable) then
|
||||
best, bestDistance = candidate, -1
|
||||
elseif bestDistance >= 0 then
|
||||
local okPos, sx, sz = pcall(function()
|
||||
local x, _, z = getWorldTranslation(candidate.owningPlaceable.rootNode)
|
||||
return x, z
|
||||
end)
|
||||
local distance = okPos and ((sx - fieldX) ^ 2 + (sz - fieldZ) ^ 2) or math.huge
|
||||
if distance < bestDistance then best, bestDistance = candidate, distance end
|
||||
end
|
||||
end
|
||||
chosen = best
|
||||
end
|
||||
if chosen == nil or params == nil or params.unloadingStation == nil then
|
||||
return nil, "ingen aflæsningsstation Courseplay kan bruge"
|
||||
end
|
||||
pcall(function() params.unloadingStation:setValue(NetworkUtil.getObjectId(chosen)) end)
|
||||
pcall(function() job:setValues() end)
|
||||
local handle = {cancelled = false, fired = false, finished = false, stationName = chosen.getName ~= nil and chosen:getName() or nil}
|
||||
local function finish(success, reason)
|
||||
if handle.finished then return end
|
||||
handle.finished = true
|
||||
if not handle.cancelled then onDone(success, reason) end
|
||||
end
|
||||
local function startJob()
|
||||
if handle.cancelled then return end
|
||||
local valid, err = job:validate(farmId)
|
||||
if not valid then return finish(false, "Courseplay: " .. tostring(err)) end
|
||||
g_currentMission.aiSystem:startJob(job, farmId)
|
||||
finish(true)
|
||||
end
|
||||
handle.onBoundary = function(_, success, message)
|
||||
if handle.fired then return end
|
||||
handle.fired = true
|
||||
if not success then return finish(false, "Courseplay fandt ikke markgrænsen: " .. tostring(message)) end
|
||||
startJob()
|
||||
end
|
||||
if job.registerFieldBoundaryDetectionCallback ~= nil then
|
||||
job:registerFieldBoundaryDetectionCallback(handle, handle.onBoundary)
|
||||
end
|
||||
local isValid, isRunning, message = job:detectFieldBoundary()
|
||||
if isValid and not isRunning then
|
||||
startJob()
|
||||
elseif not isRunning then
|
||||
return nil, "Courseplay: " .. tostring(message)
|
||||
end
|
||||
return handle
|
||||
end
|
||||
|
||||
@@ -1,48 +1,92 @@
|
||||
-- AD Field Fleet
|
||||
-- Markflåden som ren logik: hvilken rolle et sæt har ud fra redskabernes specs, og hvilket ledigt sæt
|
||||
-- der sendes til en mark. Kun sæt med flueben bruges; et sæt spilleren sidder i, røres aldrig.
|
||||
-- Markflåden som ren logik: hvilken rolle et sæt har ud fra redskabernes specs, og hvilke ledige sæt der
|
||||
-- sendes til en mark. Kun sæt med flueben bruges; et sæt spilleren sidder i, røres aldrig.
|
||||
|
||||
ADFieldFleet = {}
|
||||
|
||||
-- Rækkefølge = prioritet når et køretøj har flere (læssevogn med egen pickup m.m.)
|
||||
-- Rækkefølge = prioritet når et køretøj har flere (såmaskine med gødning er såmaskine; traktor med plov er
|
||||
-- plov, ikke aflæsningsvogn). sprayRole kommer fra sprøjtens/sprederens fyld (gødning/kalk/ukrudt).
|
||||
ADFieldFleet.ROLE_SPECS = {
|
||||
{role = "harvester", spec = "spec_combine"},
|
||||
{role = "collector", spec = "spec_forageWagon"},
|
||||
{role = "rake", spec = "spec_windrower"},
|
||||
{role = "tedder", spec = "spec_tedder"},
|
||||
{role = "mower", spec = "spec_mower"},
|
||||
{role = "seeder", spec = "spec_sowingMachine"},
|
||||
{role = "roller", spec = "spec_roller"},
|
||||
{role = "weeder", spec = "spec_weeder"},
|
||||
{role = "sprayRole"},
|
||||
{role = "mulcher", spec = "spec_mulcher"},
|
||||
{role = "cultivator", spec = "spec_cultivator"},
|
||||
{role = "plow", spec = "spec_plow"},
|
||||
{role = "unloader", spec = "canUnload"},
|
||||
}
|
||||
|
||||
function ADFieldFleet.roleFromSpecs(specs)
|
||||
for _, entry in ipairs(ADFieldFleet.ROLE_SPECS) do
|
||||
if specs[entry.spec] then return entry.role end
|
||||
if entry.role == "sprayRole" then
|
||||
if specs.sprayRole ~= nil then return specs.sprayRole end
|
||||
elseif specs[entry.spec] then
|
||||
return entry.role
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Nærmeste brugbare sæt til rollen. Grund når intet passer: noRig (ingen med flueben og rollen),
|
||||
-- allBusy (alle optaget/i brug af spilleren), tooWide/tooNarrow (bredden, med tal fra den første).
|
||||
function ADFieldFleet.pick(candidates, role, x, z, mowWidth)
|
||||
local best, bestDistance = nil, math.huge
|
||||
local sawRole, widthReason, widthArgs = false, nil, nil
|
||||
-- Kandidater med rollen, sorteret nærmest først, delt i brugbare og afviste (med første grund).
|
||||
local function sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
local usable, sawRole, sawAccepted = {}, false, false
|
||||
local widthReason, widthArgs = nil, nil
|
||||
for _, candidate in ipairs(candidates) do
|
||||
if candidate.role == role and candidate.enabled then
|
||||
sawRole = true
|
||||
if not candidate.busy and not candidate.controlled then
|
||||
local ok, reason, args = ADFieldModel.checkWidth(role, candidate.width or 0, mowWidth)
|
||||
if ok then
|
||||
local dx, dz = candidate.x - x, candidate.z - z
|
||||
local distance = dx * dx + dz * dz
|
||||
if distance < bestDistance then best, bestDistance = candidate, distance end
|
||||
elseif widthReason == nil then
|
||||
widthReason, widthArgs = reason, args
|
||||
if accept == nil or accept(candidate) then
|
||||
sawAccepted = true
|
||||
if not candidate.busy and not candidate.controlled then
|
||||
local ok, reason, args = ADFieldModel.checkWidth(role, candidate.width or 0, requiredWidth)
|
||||
if ok then
|
||||
local dx, dz = candidate.x - x, candidate.z - z
|
||||
table.insert(usable, {candidate = candidate, distance = dx * dx + dz * dz})
|
||||
elseif widthReason == nil then
|
||||
widthReason, widthArgs = reason, args
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if best ~= nil then return best end
|
||||
if not sawRole then return nil, "noRig" end
|
||||
if widthReason ~= nil then return nil, widthReason, widthArgs end
|
||||
return nil, "allBusy"
|
||||
table.sort(usable, function(a, b) return a.distance < b.distance end)
|
||||
local reason, args = nil, nil
|
||||
if not sawRole then
|
||||
reason = "noRig"
|
||||
elseif not sawAccepted then
|
||||
reason = "cannotDo"
|
||||
elseif widthReason ~= nil then
|
||||
reason, args = widthReason, widthArgs
|
||||
else
|
||||
reason = "allBusy"
|
||||
end
|
||||
return usable, reason, args
|
||||
end
|
||||
|
||||
-- Nærmeste brugbare sæt. Grund når intet passer: noRig (ingen med flueben og rollen), cannotDo (ingen kan
|
||||
-- tage opgaven, fx afgrøden), allBusy, tooWide/tooNarrow (bredden, med tal fra den første).
|
||||
-- accept (valgfri): candidate -> bool, fx "høsteren kan tage hvede".
|
||||
function ADFieldFleet.pick(candidates, role, x, z, requiredWidth, accept)
|
||||
local usable, reason, args = sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
if usable[1] ~= nil then return usable[1].candidate end
|
||||
return nil, reason, args
|
||||
end
|
||||
|
||||
-- n sæt på én gang (multi-tool kræver alle baner): de n nærmeste, ellers ingen. notEnough har {ledige, n}.
|
||||
function ADFieldFleet.pickMany(candidates, role, n, x, z, requiredWidth, accept)
|
||||
local usable, reason, args = sortCandidates(candidates, role, x, z, requiredWidth, accept)
|
||||
if #usable >= n then
|
||||
local picked = {}
|
||||
for index = 1, n do picked[index] = usable[index].candidate end
|
||||
return picked
|
||||
end
|
||||
if #usable > 0 then return nil, "notEnough", {#usable, n} end
|
||||
return nil, reason, args
|
||||
end
|
||||
|
||||
function ADFieldFleet.toggle(enabledIds, id)
|
||||
|
||||
+140
-293
@@ -1,51 +1,96 @@
|
||||
-- AD Field Jobs
|
||||
-- Markarbejdets job-løkke (hvert sekund): opdater klar-andel, start marker, send sæt ud, følg dem
|
||||
-- gennem AutoDrive-kørsel -> Courseplay-arbejde -> hjem, og gå videre til næste trin. decide() er ren
|
||||
-- og testet; resten oversætter til AutoDrive/Courseplay og er pcall'et pr. mark og pr. sæt.
|
||||
-- Markarbejdets job-løkke (hvert sekund): opdater markerne, start kæder og pleje, beslut hvilke sæt der
|
||||
-- skal ud, og gå videre til næste trin. decide() er ren og testet; udsendelse og opfølgning af sættene
|
||||
-- (AutoDrive/Courseplay) ligger i adFieldWork. Alt spil-nært er pcall'et pr. mark og pr. sæt.
|
||||
|
||||
ADFieldJobs = {}
|
||||
|
||||
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
||||
ADFieldJobs.TICK_MS = 1000
|
||||
ADFieldJobs.ARRIVE_DISTANCE = 30
|
||||
ADFieldJobs.IDLE_CONFIRM_MS = 5000
|
||||
ADFieldJobs.START_TIMEOUT_MS = 60000
|
||||
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige slå-ture går marken videre.
|
||||
ADFieldJobs.MAX_MOW_PASSES = 2
|
||||
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige ture pr. sæt går marken videre.
|
||||
ADFieldJobs.MAX_PASSES = 2
|
||||
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}}
|
||||
-- mark-id -> {mower = kursus, rake = kursus, <trin> = fælles multi-tool-kursus}: kopier (kun i hukommelsen;
|
||||
-- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på)
|
||||
ADFieldJobs.courses = {}
|
||||
ADFieldJobs.finished = {}
|
||||
ADFieldJobs.timer = 0
|
||||
ADFieldJobs.now = 0
|
||||
ADFieldJobs.loaded = false
|
||||
|
||||
local ACTIVE_PHASES = {driving = true, starting = true, working = true}
|
||||
ADFieldJobs.ACTIVE_PHASES = {driving = true, starting = true, waitingCourse = true, working = true}
|
||||
|
||||
-- ---- ren beslutning ----
|
||||
|
||||
function ADFieldJobs.decide(field, info, assignments, now)
|
||||
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar eller under 50 %.
|
||||
local function cropGone(stage, info)
|
||||
if info == nil then return false end
|
||||
if stage == "harvesting" and info.kind == "bare" then return true end
|
||||
return (info.readyRatio or 0) < ADFieldModel.CUT_RATIO
|
||||
end
|
||||
|
||||
-- Beslutning for én mark. assignments = sæt på marken ({phase, stage, helper}); kun sæt på det aktuelle
|
||||
-- trin tæller (en aflæsningsvogn på vej hjem fra høsten forstyrrer ikke kultiveringen).
|
||||
-- env = {raining = bool}: vejrfølsomme trin (høst/slå/vend/riv/hø) sendes ikke ud i regn.
|
||||
-- Svar: {action = "start"|"care"|"advance"|"dispatch"|"wait"|"none", role, count, helper, stage, care, reason}.
|
||||
function ADFieldJobs.decide(field, info, assignments, now, env)
|
||||
local hasMarker = info ~= nil and info.markerId ~= nil
|
||||
local raining = env ~= nil and env.raining == true
|
||||
local kind = field.kind or (info ~= nil and info.kind) or "mowable"
|
||||
if field.stage == "ready" then
|
||||
if not ADFieldModel.shouldStart(field) then return {action = "none"} end
|
||||
if info == nil or info.markerId == nil then return {action = "wait"} end
|
||||
if not hasMarker then return {action = "wait"} end
|
||||
local first = ADFieldModel.workStages(field.plan, info.kind, info.needs)[1]
|
||||
if raining and first ~= nil and ADFieldModel.isWeatherSensitive(first, field.plan, info.kind) then
|
||||
return {action = "wait", reason = "rain"}
|
||||
end
|
||||
return {action = "start"}
|
||||
end
|
||||
if not ADFieldModel.isWorking(field) then return {action = "none"} end
|
||||
local active, finished = 0, 0
|
||||
if not ADFieldModel.isWorking(field) then
|
||||
local care = ADFieldModel.careNeed(field, info)
|
||||
if care == nil then return {action = "none"} end
|
||||
if field.plan.mode ~= "auto" and not field.startRequested then return {action = "none", care = care} end
|
||||
if not hasMarker then return {action = "wait", care = care} end
|
||||
return {action = "care", stage = care}
|
||||
end
|
||||
local active, finished, helpers = 0, 0, 0
|
||||
for _, assignment in ipairs(assignments) do
|
||||
if ACTIVE_PHASES[assignment.phase] then active = active + 1 end
|
||||
if assignment.phase == "finished" or assignment.phase == "returning" then finished = finished + 1 end
|
||||
if assignment.stage == nil or assignment.stage == field.stage then
|
||||
local isActive = ADFieldJobs.ACTIVE_PHASES[assignment.phase]
|
||||
if assignment.helper then
|
||||
if isActive then helpers = helpers + 1 end
|
||||
else
|
||||
if isActive then active = active + 1 end
|
||||
if assignment.phase == "finished" then finished = finished + 1 end
|
||||
end
|
||||
end
|
||||
end
|
||||
if active == 0 and finished > 0 then
|
||||
local stillStanding = field.stage == "mowing" and info ~= nil and (info.readyRatio or 0) >= ADFieldModel.CUT_RATIO
|
||||
if not stillStanding or finished >= ADFieldJobs.MAX_MOW_PASSES then return {action = "advance"} end
|
||||
local isCropStage = ADFieldModel.CROP_STAGES[field.stage] == true
|
||||
if active == 0 then
|
||||
-- afgrøden er væk (sluppet undervejs, genindlæst, eller færdig): videre
|
||||
if isCropStage and cropGone(field.stage, info) then return {action = "advance"} end
|
||||
if finished > 0 then
|
||||
local maxPasses = ADFieldJobs.MAX_PASSES * ADFieldModel.maxRigs(field.stage, field.plan)
|
||||
if not isCropStage or finished >= maxPasses then return {action = "advance"} end
|
||||
end
|
||||
if not hasMarker then return {action = "wait"} end
|
||||
if (field.retryAt or 0) > now then return {action = "none"} end
|
||||
if raining and ADFieldModel.isWeatherSensitive(field.stage, field.plan, kind) then
|
||||
return {action = "wait", reason = "rain"}
|
||||
end
|
||||
return {action = "dispatch", role = ADFieldModel.roleFor(field.stage), count = ADFieldModel.maxRigs(field.stage, field.plan), helper = false}
|
||||
end
|
||||
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
||||
local helperRole = ADFieldModel.helperFor(field.stage)
|
||||
if helperRole ~= nil and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then
|
||||
return {action = "dispatch", role = helperRole, count = 1, helper = true}
|
||||
end
|
||||
if info == nil or info.markerId == nil then return {action = "wait"} end
|
||||
local role = ADFieldModel.wantsRig(field, active, now)
|
||||
if role ~= nil then return {action = "dispatch", role = role} end
|
||||
return {action = "none"}
|
||||
end
|
||||
|
||||
-- ---- tilstand ----
|
||||
|
||||
local function log(fieldId, fmt, ...)
|
||||
function ADFieldJobs.log(fieldId, fmt, ...)
|
||||
Logging.info(ADFieldJobs.LOG_PREFIX .. "F%s " .. fmt, tostring(fieldId), ...)
|
||||
end
|
||||
|
||||
@@ -53,10 +98,15 @@ function ADFieldJobs.getField(id)
|
||||
return ADFieldJobs.state.fields[id] or ADFieldModel.newField(id)
|
||||
end
|
||||
|
||||
local function setField(field)
|
||||
function ADFieldJobs.setField(field)
|
||||
ADFieldJobs.state.fields[field.id] = field
|
||||
end
|
||||
|
||||
-- Ændr felter på marken (kopi via modellen).
|
||||
function ADFieldJobs.note(fieldId, changes)
|
||||
ADFieldJobs.setField(ADFieldModel.with(ADFieldJobs.getField(fieldId), changes))
|
||||
end
|
||||
|
||||
function ADFieldJobs.assignmentsFor(fieldId)
|
||||
local list = {}
|
||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||
@@ -81,20 +131,16 @@ function ADFieldJobs.load()
|
||||
end
|
||||
|
||||
function ADFieldJobs.setPlan(id, changes)
|
||||
setField(ADFieldModel.with(ADFieldJobs.getField(id), {plan = changes}))
|
||||
ADFieldJobs.note(id, {plan = changes})
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
function ADFieldJobs.requestStart(id)
|
||||
setField(ADFieldModel.with(ADFieldJobs.getField(id), {startRequested = true}))
|
||||
end
|
||||
|
||||
local function note(fieldId, changes)
|
||||
setField(ADFieldModel.with(ADFieldJobs.getField(fieldId), changes))
|
||||
ADFieldJobs.note(id, {startRequested = true})
|
||||
end
|
||||
|
||||
function ADFieldJobs.forceReady(id)
|
||||
setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||
end
|
||||
|
||||
function ADFieldJobs.toggleFleet(vehicleId)
|
||||
@@ -102,286 +148,89 @@ function ADFieldJobs.toggleFleet(vehicleId)
|
||||
ADFieldJobs.save()
|
||||
end
|
||||
|
||||
-- ---- AutoDrive ----
|
||||
-- GUI'et henter flåde og gæringssiloer herfra (ligger i adFieldWork).
|
||||
function ADFieldJobs.getRigs(adEnv) return ADFieldWork.getRigs(adEnv) end
|
||||
function ADFieldJobs.getFermenters(...) return ADFieldWork.getFermenters(...) end
|
||||
|
||||
local function stateModule(vehicle)
|
||||
return vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
end
|
||||
|
||||
local function isAdActive(vehicle)
|
||||
local module = stateModule(vehicle)
|
||||
return module ~= nil and module:isActive()
|
||||
end
|
||||
|
||||
local function stopAd(vehicle)
|
||||
if isAdActive(vehicle) then vehicle:stopAutoDrive() end
|
||||
end
|
||||
|
||||
local function startAd(adEnv, vehicle, mode, firstId, secondId, withCp)
|
||||
local module = stateModule(vehicle)
|
||||
stopAd(vehicle)
|
||||
module:setMode(mode)
|
||||
module:setFirstMarkerByWayPointId(firstId)
|
||||
if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end
|
||||
module:setStartHelper(withCp == true)
|
||||
if withCp then
|
||||
module.usedHelper = adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
||||
if vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished ~= nil then
|
||||
vehicle.ad.settings.enableParkAtJobFinished.current = 2
|
||||
vehicle.ad.settings.enableParkAtJobFinished.new = 2
|
||||
end
|
||||
end
|
||||
local farmId = vehicle:getOwnerFarmId()
|
||||
if module.setActualFarmId ~= nil then module:setActualFarmId(farmId) end
|
||||
module:raiseDirtyFlag()
|
||||
local current = module:getCurrentMode()
|
||||
if current ~= nil and current.start ~= nil then
|
||||
current:start(adEnv.AutoDrive.USER_PLAYER or 1)
|
||||
else
|
||||
vehicle:startAutoDrive()
|
||||
end
|
||||
end
|
||||
|
||||
local function parkMarkerId(adEnv, vehicle)
|
||||
local module = stateModule(vehicle)
|
||||
local index = module ~= nil and module.getParkDestinationAtJobFinished ~= nil and module:getParkDestinationAtJobFinished() or nil
|
||||
if index == nil or index < 1 then return nil end
|
||||
local marker = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
return marker ~= nil and marker.id or nil
|
||||
end
|
||||
|
||||
local function restLoad(adEnv, vehicle)
|
||||
local ok, level = pcall(function()
|
||||
return (adEnv.AutoDrive.getAllFillLevels(adEnv.AutoDrive.getAllUnits(vehicle)))
|
||||
end)
|
||||
return ok and tonumber(level) or 0
|
||||
end
|
||||
|
||||
local function vehiclePosition(vehicle)
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
return x, z
|
||||
end
|
||||
|
||||
local function isControlledByPlayer(vehicle)
|
||||
return (vehicle.getIsControlled ~= nil and vehicle:getIsControlled()) or false
|
||||
end
|
||||
|
||||
-- ---- flåde ----
|
||||
|
||||
function ADFieldJobs.getRigs(adEnv)
|
||||
local rigs = {}
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local role = ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle))
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
if role ~= nil and id ~= nil then
|
||||
local x, z = vehiclePosition(vehicle)
|
||||
table.insert(rigs, {
|
||||
id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle),
|
||||
enabled = ADFieldJobs.state.fleet[id] == true,
|
||||
busy = ADFieldJobs.state.assignments[id] ~= nil or ADRunsController.getRunForVehicle(vehicle) ~= nil,
|
||||
controlled = isControlledByPlayer(vehicle),
|
||||
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
||||
})
|
||||
end
|
||||
end
|
||||
return rigs
|
||||
end
|
||||
|
||||
-- ---- silo ----
|
||||
|
||||
local function chooseStore(adEnv, field, info, vehicle)
|
||||
local fillType = field.plan.product == "hay" and info.hay or info.windrow
|
||||
local isReachable = function(markerId) return ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId) end
|
||||
if #field.plan.storeBuildingIds > 0 then
|
||||
local markerIds = {}
|
||||
for _, buildingId in ipairs(field.plan.storeBuildingIds) do
|
||||
local building = ADBuildings.find(adEnv, vehicle:getOwnerFarmId(), buildingId)
|
||||
for _, marker in ipairs(building ~= nil and ADBuildings.getMarkers(building, "unload") or {}) do
|
||||
table.insert(markerIds, marker.id)
|
||||
end
|
||||
end
|
||||
return ADLoadSwap.findForcedDumpMarker(adEnv, vehicle, fillType, 1, markerIds, isReachable)
|
||||
end
|
||||
return ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, 1, info.markerId, isReachable, nil, nil, false)
|
||||
end
|
||||
|
||||
-- ---- udsendelse og opfølgning ----
|
||||
|
||||
-- Færdige sæt for en mark glemmes, når marken går videre eller stoppes. Listen er begrænset: en mark
|
||||
-- der sendes ud igen uden at gå videre (hjørner), går videre efter MAX_MOW_PASSES færdige ture.
|
||||
local function forgetFinished(fieldId)
|
||||
function ADFieldJobs.forgetFinished(fieldId)
|
||||
local remaining = {}
|
||||
for _, done in ipairs(ADFieldJobs.finished or {}) do
|
||||
for _, done in ipairs(ADFieldJobs.finished) do
|
||||
if done.fieldId ~= fieldId then table.insert(remaining, done) end
|
||||
end
|
||||
ADFieldJobs.finished = remaining
|
||||
end
|
||||
|
||||
local function release(assignment, phase)
|
||||
assignment.phase = phase or "released"
|
||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
||||
end
|
||||
|
||||
local function dispatch(adEnv, field, info, role)
|
||||
local candidates = ADFieldJobs.getRigs(adEnv)
|
||||
local rig, reason, args = ADFieldFleet.pick(candidates, role, info.x, info.z, field.mowWidth)
|
||||
if rig == nil then
|
||||
note(field.id, {reason = {key = "spu_field_reason_" .. reason, role = role, args = args}})
|
||||
return
|
||||
end
|
||||
local assignment = {vehicleId = rig.id, vehicle = rig.vehicle, fieldId = field.id, role = role, phase = "driving", since = ADFieldJobs.now, width = rig.width}
|
||||
ADFieldJobs.state.assignments[rig.id] = assignment
|
||||
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil, false)
|
||||
log(field.id, "%s '%s' sendt til %s (%.1f m)", role, rig.label, tostring(info.markerName), rig.width)
|
||||
note(field.id, {reason = false})
|
||||
end
|
||||
|
||||
local function beginWork(adEnv, field, info, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
if assignment.role == "collector" then
|
||||
local store, why = chooseStore(adEnv, field, info, vehicle)
|
||||
if store == nil then
|
||||
release(assignment)
|
||||
note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(why)}})
|
||||
log(field.id, "intet lager: %s", tostring(why))
|
||||
return
|
||||
end
|
||||
assignment.storeId = store.id
|
||||
-- AD overtager ved "fuld": Hent og aflever F -> silo, hjælper = Courseplay
|
||||
local module = stateModule(vehicle)
|
||||
module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
|
||||
module:setFirstMarkerByWayPointId(info.markerId)
|
||||
module:setSecondMarkerByWayPointId(store.id)
|
||||
module:setStartHelper(true)
|
||||
module.usedHelper = adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
||||
if vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished ~= nil then
|
||||
vehicle.ad.settings.enableParkAtJobFinished.current = 2
|
||||
vehicle.ad.settings.enableParkAtJobFinished.new = 2
|
||||
end
|
||||
module:raiseDirtyFlag()
|
||||
log(field.id, "læssevogn '%s' aflæsser i %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
|
||||
end
|
||||
local point = info.points[1] or {x = info.x, z = info.z}
|
||||
assignment.phase = "starting"
|
||||
assignment.since = ADFieldJobs.now
|
||||
local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason)
|
||||
if ok then
|
||||
assignment.phase = "working"
|
||||
assignment.since = ADFieldJobs.now
|
||||
if assignment.role == "mower" then
|
||||
note(field.id, {mowWidth = assignment.width})
|
||||
end
|
||||
log(field.id, "Courseplay startet (%s)", assignment.role)
|
||||
else
|
||||
release(assignment)
|
||||
note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(reason)}})
|
||||
log(field.id, "Courseplay fejlede: %s", tostring(reason))
|
||||
end
|
||||
end)
|
||||
if handle == nil then
|
||||
release(assignment)
|
||||
log(field.id, "Courseplay kunne ikke startes: %s", tostring(err))
|
||||
note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(err)}})
|
||||
return
|
||||
end
|
||||
assignment.handle = handle
|
||||
end
|
||||
|
||||
local function follow(adEnv, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
if isControlledByPlayer(vehicle) then
|
||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||
log(assignment.fieldId, "'%s' frigivet — spilleren kører det", ADRuns.vehicleLabel(vehicle))
|
||||
return release(assignment)
|
||||
end
|
||||
local field = ADFieldJobs.getField(assignment.fieldId)
|
||||
local info = ADFields.getInfo(assignment.fieldId)
|
||||
if assignment.phase == "driving" then
|
||||
if not isAdActive(vehicle) and ADFieldJobs.now - assignment.since > 2000 then
|
||||
local x, z = vehiclePosition(vehicle)
|
||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
||||
local distance = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) or math.huge
|
||||
if distance <= ADFieldJobs.ARRIVE_DISTANCE then
|
||||
beginWork(adEnv, field, info, assignment)
|
||||
else
|
||||
log(assignment.fieldId, "'%s' nåede ikke %s (%.0f m væk)", ADRuns.vehicleLabel(vehicle), tostring(info.markerName), distance)
|
||||
release(assignment)
|
||||
note(assignment.fieldId, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS,
|
||||
reason = {text = string.format("%s nåede ikke %s", ADRuns.vehicleLabel(vehicle), tostring(info.markerName))}})
|
||||
end
|
||||
end
|
||||
elseif assignment.phase == "starting" then
|
||||
if ADFieldJobs.now - assignment.since > ADFieldJobs.START_TIMEOUT_MS then
|
||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||
release(assignment)
|
||||
note(assignment.fieldId, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = "Courseplay svarede ikke (60 s)"}})
|
||||
log(assignment.fieldId, "Courseplay svarede ikke inden 60 s")
|
||||
end
|
||||
elseif assignment.phase == "working" then
|
||||
local busy = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle)
|
||||
if busy then
|
||||
assignment.idleSince = nil
|
||||
else
|
||||
assignment.idleSince = assignment.idleSince or ADFieldJobs.now
|
||||
local rest = assignment.role == "collector" and assignment.storeId ~= nil and not assignment.deliveredRest and restLoad(adEnv, vehicle) or 0
|
||||
if rest > 0 and ADFieldJobs.now - assignment.idleSince >= ADFieldJobs.IDLE_CONFIRM_MS then
|
||||
-- CP færdig med læs på: AD tømmer kun selv med parkering sat, så vi sender resten til silo
|
||||
assignment.deliveredRest = true
|
||||
assignment.idleSince = nil
|
||||
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, assignment.storeId, nil, false)
|
||||
log(assignment.fieldId, "'%s' kører rest-læs (%.0f l) til silo", ADRuns.vehicleLabel(vehicle), rest)
|
||||
elseif ADFieldJobs.now - assignment.idleSince >= ADFieldJobs.IDLE_CONFIRM_MS then
|
||||
log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
|
||||
local park = assignment.role ~= "collector" and parkMarkerId(adEnv, vehicle) or nil
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil, false) end
|
||||
assignment.phase = "finished"
|
||||
ADFieldJobs.finished = ADFieldJobs.finished or {}
|
||||
table.insert(ADFieldJobs.finished, assignment)
|
||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldJobs.stopField(adEnv, id)
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(id)) do
|
||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||
pcall(stopAd, assignment.vehicle)
|
||||
pcall(function() if assignment.vehicle.stopCurrentAIJob ~= nil and ADCpBridge.isWorking(assignment.vehicle) then assignment.vehicle:stopCurrentAIJob(AIMessageSuccessStoppedByUser.new()) end end)
|
||||
release(assignment)
|
||||
end
|
||||
forgetFinished(id)
|
||||
setField(ADFieldModel.stop(ADFieldJobs.getField(id)))
|
||||
ADFieldJobs.save()
|
||||
log(id, "stoppet fra menuen")
|
||||
end
|
||||
|
||||
local function finishedFor(fieldId)
|
||||
local list = {}
|
||||
for _, assignment in ipairs(ADFieldJobs.finished or {}) do
|
||||
for _, assignment in ipairs(ADFieldJobs.finished) do
|
||||
if assignment.fieldId == fieldId then table.insert(list, assignment) end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
function ADFieldJobs.stopField(adEnv, id)
|
||||
ADFieldWork.stopAll(adEnv, id)
|
||||
ADFieldJobs.forgetFinished(id)
|
||||
ADFieldJobs.setField(ADFieldModel.stop(ADFieldJobs.getField(id)))
|
||||
ADFieldJobs.save()
|
||||
ADFieldJobs.log(id, "stoppet fra menuen")
|
||||
end
|
||||
|
||||
-- Besked i spillet (højre side). critical = rød. Samme tekst vises kun én gang i træk.
|
||||
function ADFieldJobs.notify(text, critical)
|
||||
if ADFieldJobs.lastNotice == text then return end
|
||||
ADFieldJobs.lastNotice = text
|
||||
pcall(function()
|
||||
local kind = critical and FSBaseMission.INGAME_NOTIFICATION_CRITICAL or FSBaseMission.INGAME_NOTIFICATION_OK
|
||||
g_currentMission:addIngameNotification(kind, "Smart Pickup: " .. text)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Vejret lige nu (regn), læst en gang pr. tick.
|
||||
function ADFieldJobs.environment()
|
||||
local ok, raining = pcall(function()
|
||||
local weather = g_currentMission.environment.weather
|
||||
return weather ~= nil and weather.getIsRaining ~= nil and weather:getIsRaining() or false
|
||||
end)
|
||||
return {raining = ok and raining == true}
|
||||
end
|
||||
|
||||
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
|
||||
ADFieldJobs.lastDecision = {}
|
||||
|
||||
local function stepField(adEnv, info)
|
||||
local field = ADFieldModel.onReadiness(ADFieldJobs.getField(info.id), info.readyRatio or 0)
|
||||
setField(field)
|
||||
ADFieldJobs.setField(field)
|
||||
local assignments = ADFieldJobs.assignmentsFor(info.id)
|
||||
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now)
|
||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now, ADFieldJobs.environment())
|
||||
ADFieldJobs.lastDecision[info.id] = decision
|
||||
if decision.action == "start" then
|
||||
setField(ADFieldModel.start(field))
|
||||
log(info.id, "starter (%s)", field.plan.product)
|
||||
local started = ADFieldModel.start(field, info.kind, info.needs)
|
||||
started.lastFruit = info.kind == "harvest" and info.fruitName or field.lastFruit
|
||||
ADFieldJobs.setField(started)
|
||||
ADFieldJobs.log(info.id, "starter (%s: %s)", tostring(info.kind), started.stage)
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "care" then
|
||||
ADFieldJobs.setField(ADFieldModel.startCare(field, decision.stage))
|
||||
ADFieldJobs.log(info.id, "pleje: %s", decision.stage)
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "advance" then
|
||||
local nextField = ADFieldModel.advance(field)
|
||||
setField(nextField)
|
||||
forgetFinished(info.id)
|
||||
log(info.id, "%s -> %s", field.stage, nextField.stage)
|
||||
local nextField = ADFieldModel.advance(field, info.needs)
|
||||
ADFieldJobs.setField(nextField)
|
||||
ADFieldJobs.forgetFinished(info.id)
|
||||
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
||||
ADFieldJobs.log(info.id, "%s -> %s", field.stage, nextField.stage)
|
||||
if nextField.stage == "done" or (ADFieldModel.isCare(field.stage) and nextField.stage == "idle") then
|
||||
ADFieldJobs.notify(string.format("F%d %s", info.id, g_i18n ~= nil and g_i18n:getText("spu_field_notice_done") or "done"), false)
|
||||
end
|
||||
ADFieldJobs.save()
|
||||
elseif decision.action == "wait" and decision.reason == "rain" then
|
||||
if field.reason == nil or field.reason.key ~= "spu_field_reason_rain" then
|
||||
ADFieldJobs.note(info.id, {reason = {key = "spu_field_reason_rain"}})
|
||||
end
|
||||
elseif decision.action == "dispatch" then
|
||||
dispatch(adEnv, field, info, decision.role)
|
||||
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -394,16 +243,14 @@ function ADFieldJobs.tick(adEnv, dt)
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
ADFields.scanNext(adEnv, farmId)
|
||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||
local ok, err = pcall(follow, adEnv, assignment)
|
||||
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
||||
if not ok then
|
||||
Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s opfølgning fejlede: %s", tostring(assignment.fieldId), tostring(err))
|
||||
release(assignment)
|
||||
ADFieldWork.release(assignment)
|
||||
end
|
||||
end
|
||||
for _, info in ipairs(ADFields.list()) do
|
||||
if info.mowable or ADFieldModel.isWorking(ADFieldJobs.getField(info.id)) then
|
||||
local ok, err = pcall(stepField, adEnv, info)
|
||||
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
||||
end
|
||||
local ok, err = pcall(stepField, adEnv, info)
|
||||
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
-- AD Field Model
|
||||
-- Markarbejdets regler som ren data (ingen spil-API): en marks plan og trin, hvornår den er klar,
|
||||
-- hvilket trin der følger, hvor mange sæt et trin må have, og bredde-reglerne. Alle funktioner
|
||||
-- returnerer nye tabeller; job-løkken (adFieldJobs) og GUI'et kalder kun herind.
|
||||
-- hvilket trin der følger, hvor mange sæt et trin må have, pleje-behov og bredde-reglerne. Alle
|
||||
-- funktioner returnerer nye tabeller; job-løkken (adFieldJobs/adFieldWork) og GUI'et kalder kun herind.
|
||||
--
|
||||
-- Markens art (kind): "mowable" (græs/kløver/lucerne), "harvest" (alt andet der høstes), "bare" (ingen
|
||||
-- afgrøde: stub, pløjet, harvet). Kæder:
|
||||
-- slåafgrøde: mowing -> [tedding] -> [raking] -> collecting
|
||||
-- høst: harvesting -> klargøring
|
||||
-- klargøring: [mulching] -> [liming] -> [plowing|cultivating] -> [sowing] -> [rolling]
|
||||
-- Pleje under vækst (enkelt trin, derefter tilbage til idle): fertilizing, weeding, liming.
|
||||
|
||||
ADFieldModel = {}
|
||||
|
||||
@@ -10,10 +17,35 @@ ADFieldModel.CUT_RATIO = 0.5
|
||||
ADFieldModel.CP_MAX_WIDTH = 50
|
||||
ADFieldModel.WIDTH_TOLERANCE = 0.05
|
||||
ADFieldModel.RETRY_MS = 300000
|
||||
ADFieldModel.MAX_COLLECTORS = 2
|
||||
ADFieldModel.CP_MAX_TOOL_OFFSET = 10
|
||||
ADFieldModel.MAX_RIGS = 5
|
||||
ADFieldModel.MAX_UNLOADERS = 3
|
||||
ADFieldModel.WEED_MIN = 2 -- weedState 1 = spredt, 2+ = tæt (AI-Bonde/Arbejdsseddel)
|
||||
ADFieldModel.WEED_MECHANICAL_MAX = 4 -- større ukrudt kræver sprøjte, strigle kan ikke
|
||||
ADFieldModel.MAX_CARE_RUNS = 2 -- samme pleje højst to gange pr. vækstcyklus (undgår løkker)
|
||||
|
||||
ADFieldModel.ROLES = {mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector"}
|
||||
local KNOWN_STAGES = {idle = true, ready = true, mowing = true, tedding = true, raking = true, collecting = true, done = true}
|
||||
ADFieldModel.ROLES = {
|
||||
mowing = "mower", tedding = "tedder", raking = "rake", collecting = "collector",
|
||||
harvesting = "harvester", mulching = "mulcher", liming = "limer", plowing = "plow",
|
||||
cultivating = "cultivator", sowing = "seeder", rolling = "roller", weeding = "weeder",
|
||||
fertilizing = "fertilizer",
|
||||
}
|
||||
ADFieldModel.HELPERS = {harvesting = "unloader"}
|
||||
ADFieldModel.CARE_STAGES = {fertilizing = true, weeding = true, careLiming = true}
|
||||
-- trin med afgrøde der skal være væk bagefter (ellers sendes maskinen ud igen)
|
||||
ADFieldModel.CROP_STAGES = {mowing = true, harvesting = true}
|
||||
|
||||
local KNOWN_STAGES = {idle = true, ready = true, done = true}
|
||||
for stage in pairs(ADFieldModel.ROLES) do KNOWN_STAGES[stage] = true end
|
||||
KNOWN_STAGES.careLiming = true
|
||||
|
||||
local DEFAULT_PLAN = {
|
||||
mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1,
|
||||
mulch = false, lime = true, soil = "auto", sow = false, sowFruit = "", roll = false,
|
||||
fertilize = true, weed = true,
|
||||
}
|
||||
local SOIL_VALUES = {auto = true, cultivate = true, plow = true, none = true}
|
||||
local PRODUCTS = {auto = true, silage = true, hay = true, harvest = true}
|
||||
|
||||
local function copyList(list)
|
||||
local copy = {}
|
||||
@@ -21,52 +53,133 @@ local function copyList(list)
|
||||
return copy
|
||||
end
|
||||
|
||||
local function copyPlan(plan)
|
||||
local copy = {}
|
||||
for key, value in pairs(DEFAULT_PLAN) do copy[key] = value end
|
||||
for key, value in pairs(plan or {}) do copy[key] = value end
|
||||
copy.storeBuildingIds = copyList(plan ~= nil and plan.storeBuildingIds or nil)
|
||||
return copy
|
||||
end
|
||||
|
||||
local function copyField(field)
|
||||
local copy = {}
|
||||
for key, value in pairs(field) do copy[key] = value end
|
||||
copy.plan = {
|
||||
mode = field.plan.mode, product = field.plan.product, rake = field.plan.rake,
|
||||
storeBuildingIds = copyList(field.plan.storeBuildingIds),
|
||||
}
|
||||
copy.plan = copyPlan(field.plan)
|
||||
local care = {}
|
||||
for key, value in pairs(field.careRuns or {}) do care[key] = value end
|
||||
copy.careRuns = care
|
||||
return copy
|
||||
end
|
||||
|
||||
local function clamp(value, low, high)
|
||||
return math.max(low, math.min(high, math.floor(tonumber(value) or low)))
|
||||
end
|
||||
|
||||
function ADFieldModel.newField(id)
|
||||
return {
|
||||
id = id, stage = "idle", mowWidth = nil, retryAt = 0, reason = nil, startRequested = false,
|
||||
plan = {mode = "manual", product = "silage", rake = false, storeBuildingIds = {}},
|
||||
careRuns = {}, plan = copyPlan(nil),
|
||||
}
|
||||
end
|
||||
|
||||
function ADFieldModel.workStages(plan)
|
||||
if plan.product == "hay" then
|
||||
if plan.rake then
|
||||
return {"mowing", "tedding", "raking", "collecting"}
|
||||
end
|
||||
return {"mowing", "tedding", "collecting"}
|
||||
-- Plan "auto": slåafgrøder -> ensilage, alt andet -> høst.
|
||||
function ADFieldModel.product(plan, kind)
|
||||
if plan.product ~= nil and plan.product ~= "auto" then return plan.product end
|
||||
return kind == "mowable" and "silage" or "harvest"
|
||||
end
|
||||
|
||||
-- Klargøringskæden efter høst (eller på en bar mark). needs = {lime = bool, plow = bool} fra marken.
|
||||
function ADFieldModel.prepStages(plan, needs)
|
||||
needs = needs or {}
|
||||
local stages = {}
|
||||
if plan.mulch then table.insert(stages, "mulching") end
|
||||
if plan.lime and needs.lime then table.insert(stages, "liming") end
|
||||
local soil = plan.soil or "auto"
|
||||
if soil == "plow" or (soil == "auto" and needs.plow) then
|
||||
table.insert(stages, "plowing")
|
||||
elseif soil == "cultivate" or soil == "auto" then
|
||||
table.insert(stages, "cultivating")
|
||||
end
|
||||
return {"mowing", "collecting"}
|
||||
if plan.sow then
|
||||
table.insert(stages, "sowing")
|
||||
if plan.roll then table.insert(stages, "rolling") end
|
||||
end
|
||||
return stages
|
||||
end
|
||||
|
||||
-- Hele kæden for markens art. kind nil = slåafgrøde (bagudkompatibelt med etape 1).
|
||||
function ADFieldModel.workStages(plan, kind, needs)
|
||||
kind = kind or "mowable"
|
||||
if kind == "bare" then
|
||||
return ADFieldModel.prepStages(plan, needs)
|
||||
end
|
||||
local product = ADFieldModel.product(plan, kind)
|
||||
if product == "harvest" then
|
||||
local stages = {"harvesting"}
|
||||
for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end
|
||||
return stages
|
||||
end
|
||||
local stages = {"mowing"}
|
||||
if product == "hay" then table.insert(stages, "tedding") end
|
||||
if plan.rake then table.insert(stages, "raking") end
|
||||
table.insert(stages, "collecting")
|
||||
return stages
|
||||
end
|
||||
|
||||
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner.
|
||||
function ADFieldModel.requiredWidth(field, role)
|
||||
if role == "collector" and not field.plan.rake then
|
||||
return field.mowWidth
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hvilket tidligere kursus sættet skal køre: læssevognen følger skårene; de andre laver deres eget.
|
||||
function ADFieldModel.courseSource(field, role)
|
||||
if role ~= "collector" then return nil end
|
||||
return field.plan.rake and "rake" or "mower"
|
||||
end
|
||||
|
||||
function ADFieldModel.roleFor(stage)
|
||||
if stage == "careLiming" then return "limer" end
|
||||
return ADFieldModel.ROLES[stage]
|
||||
end
|
||||
|
||||
function ADFieldModel.helperFor(stage)
|
||||
return ADFieldModel.HELPERS[stage]
|
||||
end
|
||||
|
||||
function ADFieldModel.isWorking(field)
|
||||
return ADFieldModel.ROLES[field.stage] ~= nil
|
||||
return ADFieldModel.roleFor(field.stage) ~= nil
|
||||
end
|
||||
|
||||
function ADFieldModel.maxRigs(stage)
|
||||
return stage == "collecting" and ADFieldModel.MAX_COLLECTORS or 1
|
||||
function ADFieldModel.isCare(stage)
|
||||
return ADFieldModel.CARE_STAGES[stage] == true
|
||||
end
|
||||
|
||||
-- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis marken blev slået uden os.
|
||||
-- Arbejdende marker styres af job-løkken, ikke af vækst.
|
||||
-- Antal sæt pr. trin (multi-tool 1–5) og aflæsningsvogne under høst (1–3).
|
||||
function ADFieldModel.maxRigs(stage, plan)
|
||||
return clamp(plan ~= nil and plan.rigs or 1, 1, ADFieldModel.MAX_RIGS)
|
||||
end
|
||||
|
||||
function ADFieldModel.maxHelpers(stage, plan)
|
||||
if ADFieldModel.helperFor(stage) == nil then return 0 end
|
||||
return clamp(plan ~= nil and plan.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS)
|
||||
end
|
||||
|
||||
-- Courseplays multi-tool-baner (laneOffset) for n sæt: 2 -> -1,1 · 3 -> -1,0,1 · 4 -> -2,-1,1,2 · 5 -> -2..2
|
||||
function ADFieldModel.lanePositions(n)
|
||||
local lanes = {[1] = {0}, [2] = {-1, 1}, [3] = {-1, 0, 1}, [4] = {-2, -1, 1, 2}, [5] = {-2, -1, 0, 1, 2}}
|
||||
return copyList(lanes[clamp(n, 1, 5)])
|
||||
end
|
||||
|
||||
-- Venter/færdig -> klar ved fuld vækst; klar -> venter hvis afgrøden blev taget uden os.
|
||||
function ADFieldModel.onReadiness(field, ratio)
|
||||
local copy = copyField(field)
|
||||
if (field.stage == "idle" or field.stage == "done") and ratio >= ADFieldModel.READY_RATIO then
|
||||
copy.stage = "ready"
|
||||
elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO then
|
||||
elseif field.stage == "ready" and ratio < ADFieldModel.CUT_RATIO and not field.startRequested then
|
||||
-- (en Start nu-forespørgsel holder marken klar: bare marker har klar-andel 0)
|
||||
copy.stage = "idle"
|
||||
copy.startRequested = false
|
||||
end
|
||||
@@ -85,58 +198,136 @@ function ADFieldModel.forceReady(field)
|
||||
return field
|
||||
end
|
||||
|
||||
function ADFieldModel.start(field)
|
||||
local copy = copyField(field)
|
||||
copy.stage = ADFieldModel.workStages(field.plan)[1]
|
||||
local function freshCycle(copy)
|
||||
copy.startRequested = false
|
||||
copy.retryAt = 0
|
||||
copy.reason = nil
|
||||
copy.mowWidth = nil
|
||||
copy.mowVehicleId = nil
|
||||
copy.rakeVehicleId = nil
|
||||
copy.windrowOffset = nil
|
||||
copy.careRuns = {}
|
||||
end
|
||||
|
||||
-- Første trin i kæden. En tom kæde (bar mark uden jordbehandling eller såning) -> færdig.
|
||||
function ADFieldModel.start(field, kind, needs)
|
||||
local copy = copyField(field)
|
||||
local stages = ADFieldModel.workStages(field.plan, kind, needs)
|
||||
copy.stage = stages[1] or "done"
|
||||
copy.kind = kind or "mowable"
|
||||
freshCycle(copy)
|
||||
return copy
|
||||
end
|
||||
|
||||
function ADFieldModel.advance(field)
|
||||
-- Næste trin i kæden (behov genberegnes, så fx kalk springes over hvis jorden allerede er kalket).
|
||||
-- Pleje-trin går tilbage til idle.
|
||||
function ADFieldModel.advance(field, needs)
|
||||
local copy = copyField(field)
|
||||
local stages = ADFieldModel.workStages(field.plan)
|
||||
copy.stage = "done"
|
||||
for index, stage in ipairs(stages) do
|
||||
if stage == field.stage and stages[index + 1] ~= nil then
|
||||
copy.stage = stages[index + 1]
|
||||
end
|
||||
end
|
||||
copy.retryAt = 0
|
||||
copy.reason = nil
|
||||
if ADFieldModel.isCare(field.stage) then
|
||||
copy.stage = "idle"
|
||||
return copy
|
||||
end
|
||||
local stages = ADFieldModel.workStages(field.plan, field.kind, needs)
|
||||
copy.stage = "done"
|
||||
local passed = false
|
||||
local order = {"mowing", "tedding", "raking", "collecting", "harvesting", "mulching", "liming", "plowing", "cultivating", "sowing", "rolling"}
|
||||
local rank = {}
|
||||
for index, stage in ipairs(order) do rank[stage] = index end
|
||||
for _, stage in ipairs(stages) do
|
||||
if not passed and (rank[stage] or 0) > (rank[field.stage] or 0) then
|
||||
copy.stage = stage
|
||||
passed = true
|
||||
end
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Stop fra menuen: arbejdet afbrydes; marken står klar igen (manuel start kræves).
|
||||
-- Stop fra menuen: arbejdet afbrydes; marken står klar (hovedkæde) eller venter (pleje/klargøring).
|
||||
function ADFieldModel.stop(field)
|
||||
local copy = copyField(field)
|
||||
if ADFieldModel.isWorking(field) then
|
||||
if ADFieldModel.CROP_STAGES[field.stage] or field.stage == "tedding" or field.stage == "raking" or field.stage == "collecting" then
|
||||
copy.stage = "ready"
|
||||
elseif ADFieldModel.isWorking(field) then
|
||||
copy.stage = "idle"
|
||||
end
|
||||
copy.startRequested = false
|
||||
copy.reason = nil
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst slåbredden.
|
||||
function ADFieldModel.checkWidth(role, width, mowWidth)
|
||||
-- Vejrfølsomme trin: afgrøde og skår skal være tørre (høst, slåning, vending, rivning, hø-opsamling).
|
||||
-- Jordbehandling, såning, pleje og ensilage-opsamling kan køre i regn.
|
||||
local WEATHER_STAGES = {harvesting = true, mowing = true, tedding = true, raking = true}
|
||||
function ADFieldModel.isWeatherSensitive(stage, plan, kind)
|
||||
if WEATHER_STAGES[stage] then return true end
|
||||
return stage == "collecting" and ADFieldModel.product(plan, kind or "mowable") == "hay"
|
||||
end
|
||||
|
||||
-- Pleje under vækst: hvilket pleje-trin marken trænger til nu (eller nil). info fra adFields:
|
||||
-- {kind, growing, fert, fertMax, lime, weed}. Højst MAX_CARE_RUNS pr. pleje pr. cyklus.
|
||||
function ADFieldModel.careNeed(field, info)
|
||||
if not (field.stage == "idle" or field.stage == "done") or info == nil or not info.growing then
|
||||
return nil
|
||||
end
|
||||
local runs = field.careRuns or {}
|
||||
local function allowed(stage) return (runs[stage] or 0) < ADFieldModel.MAX_CARE_RUNS end
|
||||
if field.plan.weed and info.kind == "harvest" and (info.weed or 0) >= ADFieldModel.WEED_MIN and allowed("weeding") then
|
||||
return "weeding"
|
||||
end
|
||||
if field.plan.fertilize and info.fert ~= nil and info.fertMax ~= nil and info.fert < info.fertMax and allowed("fertilizing") then
|
||||
return "fertilizing"
|
||||
end
|
||||
if field.plan.lime and info.lime ~= nil and info.lime < 1 and allowed("careLiming") then
|
||||
return "careLiming"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Start et pleje-trin (tæller kørslen, så samme pleje ikke gentages i det uendelige).
|
||||
function ADFieldModel.startCare(field, stage)
|
||||
local copy = copyField(field)
|
||||
copy.careRuns[stage] = (copy.careRuns[stage] or 0) + 1
|
||||
copy.stage = stage
|
||||
copy.startRequested = false
|
||||
copy.retryAt = 0
|
||||
copy.reason = nil
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Et sæt må ikke over Courseplays grænse; efter-trin skal dække mindst den krævede bredde.
|
||||
function ADFieldModel.checkWidth(role, width, requiredWidth)
|
||||
if width > ADFieldModel.CP_MAX_WIDTH + ADFieldModel.WIDTH_TOLERANCE then
|
||||
return false, "tooWide", {width, ADFieldModel.CP_MAX_WIDTH}
|
||||
end
|
||||
if role ~= "mower" and mowWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < mowWidth then
|
||||
return false, "tooNarrow", {width, mowWidth}
|
||||
if role ~= "mower" and requiredWidth ~= nil and width + ADFieldModel.WIDTH_TOLERANCE < requiredWidth then
|
||||
return false, "tooNarrow", {width, requiredWidth}
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ADFieldModel.wantsRig(field, assignedCount, now)
|
||||
-- Hvilken rolle trinet mangler: {role, count, helper}. Hovedrollen sendes samlet (multi-tool kræver alle
|
||||
-- baner), hjælpere (aflæsningsvogne) en ad gangen. counts = {primary = aktive, helpers = aktive}.
|
||||
function ADFieldModel.wantsRig(field, counts, now)
|
||||
if type(counts) == "number" then counts = {primary = counts, helpers = 0} end
|
||||
local role = ADFieldModel.roleFor(field.stage)
|
||||
if role == nil or (field.retryAt or 0) > now or assignedCount >= ADFieldModel.maxRigs(field.stage) then
|
||||
return nil
|
||||
if role == nil or (field.retryAt or 0) > now then return nil end
|
||||
local wanted = ADFieldModel.maxRigs(field.stage, field.plan)
|
||||
if (counts.primary or 0) < wanted then
|
||||
return {role = role, count = wanted - (counts.primary or 0), helper = false}
|
||||
end
|
||||
return role
|
||||
local helper = ADFieldModel.helperFor(field.stage)
|
||||
if helper ~= nil and (counts.helpers or 0) < ADFieldModel.maxHelpers(field.stage, field.plan) then
|
||||
return {role = helper, count = 1, helper = true}
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Hvilken afgrøde der skal sås: planens valg, ellers den afgrøde der blev høstet.
|
||||
function ADFieldModel.sowFruit(field)
|
||||
if field.plan.sowFruit ~= nil and field.plan.sowFruit ~= "" then return field.plan.sowFruit end
|
||||
return field.lastFruit
|
||||
end
|
||||
|
||||
function ADFieldModel.with(field, changes)
|
||||
@@ -144,6 +335,8 @@ function ADFieldModel.with(field, changes)
|
||||
for key, value in pairs(changes) do
|
||||
if key == "plan" then
|
||||
for planKey, planValue in pairs(value) do copy.plan[planKey] = planValue end
|
||||
elseif value == false and (key == "reason") then
|
||||
copy[key] = nil
|
||||
else
|
||||
copy[key] = value
|
||||
end
|
||||
@@ -151,21 +344,83 @@ function ADFieldModel.with(field, changes)
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Læssevognens Courseplay-forskydning når den kører rivens kursus (se etape 1-spec).
|
||||
function ADFieldModel.collectorOffset(ownOffset, windrowOffset)
|
||||
local offset = (ownOffset or 0) - (windrowOffset or 0)
|
||||
return math.max(-ADFieldModel.CP_MAX_TOOL_OFFSET, math.min(ADFieldModel.CP_MAX_TOOL_OFFSET, offset))
|
||||
end
|
||||
|
||||
-- Gæringssilo: en produktion der har markens skår som input og ensilage som output.
|
||||
function ADFieldModel.isFermenter(productions, windrowFillType, silageFillType)
|
||||
if silageFillType == nil then return false end
|
||||
for _, production in ipairs(productions or {}) do
|
||||
local takesWindrow, makesSilage = false, false
|
||||
for _, input in ipairs(production.inputs or {}) do
|
||||
if input.type == windrowFillType then takesWindrow = true end
|
||||
end
|
||||
for _, output in ipairs(production.outputs or {}) do
|
||||
if output.type == silageFillType then makesSilage = true end
|
||||
end
|
||||
if takesWindrow and makesSilage then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function flag(value) return value and 1 or 0 end
|
||||
|
||||
function ADFieldModel.toRecord(field)
|
||||
local plan = field.plan
|
||||
local care = {}
|
||||
for stage, runs in pairs(field.careRuns or {}) do table.insert(care, stage .. "=" .. tostring(runs)) end
|
||||
table.sort(care)
|
||||
return {
|
||||
id = field.id, stage = field.stage, mode = field.plan.mode, product = field.plan.product,
|
||||
rake = field.plan.rake and 1 or 0, store = table.concat(field.plan.storeBuildingIds, " "),
|
||||
id = field.id, stage = field.stage, mode = plan.mode, product = plan.product,
|
||||
rake = flag(plan.rake), store = table.concat(plan.storeBuildingIds, " "),
|
||||
rigs = plan.rigs, unloaders = plan.unloaders, mulch = flag(plan.mulch), lime = flag(plan.lime),
|
||||
soil = plan.soil, sow = flag(plan.sow), sowFruit = plan.sowFruit or "", roll = flag(plan.roll),
|
||||
fertilize = flag(plan.fertilize), weed = flag(plan.weed),
|
||||
kind = field.kind or "", lastFruit = field.lastFruit or "", care = table.concat(care, " "),
|
||||
mowWidth = field.mowWidth or 0,
|
||||
mowVehicle = field.mowVehicleId or "",
|
||||
rakeVehicle = field.rakeVehicleId or "",
|
||||
windrowOffset = field.windrowOffset ~= nil and string.format("%.2f", field.windrowOffset) or "",
|
||||
}
|
||||
end
|
||||
|
||||
local function readFlag(value, default)
|
||||
if value == nil then return default end
|
||||
return value == 1 or value == true
|
||||
end
|
||||
|
||||
local function readText(value)
|
||||
return value ~= nil and value ~= "" and value or nil
|
||||
end
|
||||
|
||||
function ADFieldModel.fromRecord(record)
|
||||
local field = ADFieldModel.newField(record.id)
|
||||
local plan = field.plan
|
||||
field.stage = KNOWN_STAGES[record.stage] and record.stage or "idle"
|
||||
field.plan.mode = record.mode == "auto" and "auto" or "manual"
|
||||
field.plan.product = record.product == "hay" and "hay" or "silage"
|
||||
field.plan.rake = record.rake == 1 or record.rake == true
|
||||
for id in string.gmatch(record.store or "", "%S+") do table.insert(field.plan.storeBuildingIds, id) end
|
||||
plan.mode = record.mode == "auto" and "auto" or "manual"
|
||||
-- etape 1 gemte "silage"/"hay"; ukendt -> auto
|
||||
plan.product = PRODUCTS[record.product] and record.product or "auto"
|
||||
plan.rake = readFlag(record.rake, false)
|
||||
for id in string.gmatch(record.store or "", "%S+") do table.insert(plan.storeBuildingIds, id) end
|
||||
plan.rigs = clamp(record.rigs or 1, 1, ADFieldModel.MAX_RIGS)
|
||||
plan.unloaders = clamp(record.unloaders or 1, 1, ADFieldModel.MAX_UNLOADERS)
|
||||
plan.mulch = readFlag(record.mulch, false)
|
||||
plan.lime = readFlag(record.lime, true)
|
||||
plan.soil = SOIL_VALUES[record.soil] and record.soil or "auto"
|
||||
plan.sow = readFlag(record.sow, false)
|
||||
plan.sowFruit = record.sowFruit or ""
|
||||
plan.roll = readFlag(record.roll, false)
|
||||
plan.fertilize = readFlag(record.fertilize, true)
|
||||
plan.weed = readFlag(record.weed, true)
|
||||
field.kind = readText(record.kind)
|
||||
field.lastFruit = readText(record.lastFruit)
|
||||
for stage, runs in string.gmatch(record.care or "", "(%w+)=(%d+)") do field.careRuns[stage] = tonumber(runs) end
|
||||
field.mowWidth = (record.mowWidth or 0) > 0 and record.mowWidth or nil
|
||||
field.mowVehicleId = readText(record.mowVehicle)
|
||||
field.rakeVehicleId = readText(record.rakeVehicle)
|
||||
field.windrowOffset = tonumber(record.windrowOffset)
|
||||
return field
|
||||
end
|
||||
|
||||
@@ -1,17 +1,41 @@
|
||||
-- AD Field Storage
|
||||
-- Markernes trin/plan og markflåden gemmes i savegame-mappen ved siden af runs.
|
||||
-- Markernes trin/plan og markflåden gemmes i savegame-mappen ved siden af runs. Hvert felt modellen gemmer
|
||||
-- (ADFieldModel.toRecord) står i FIELD_TYPES med sin XML-type — en test sikrer at intet felt glemmes.
|
||||
|
||||
ADFieldStorage = {}
|
||||
|
||||
ADFieldStorage.FILE_NAME = "ADSmartPickup_fields.xml"
|
||||
ADFieldStorage.ROOT = "ADSmartPickupFields"
|
||||
|
||||
ADFieldStorage.FIELD_TYPES = {
|
||||
id = "Int", stage = "String", mode = "String", product = "String", rake = "Int", store = "String",
|
||||
rigs = "Int", unloaders = "Int", mulch = "Int", lime = "Int", soil = "String", sow = "Int",
|
||||
sowFruit = "String", roll = "Int", fertilize = "Int", weed = "Int",
|
||||
kind = "String", lastFruit = "String", care = "String",
|
||||
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", windrowOffset = "String",
|
||||
}
|
||||
|
||||
function ADFieldStorage.getPath()
|
||||
local runsPath = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
|
||||
if runsPath == nil then return nil end
|
||||
return (string.gsub(runsPath, ADRunsStorage.FILE_NAME .. "$", ADFieldStorage.FILE_NAME))
|
||||
end
|
||||
|
||||
function ADFieldStorage.writeRecord(xmlFile, key, record)
|
||||
for name, kind in pairs(ADFieldStorage.FIELD_TYPES) do
|
||||
local value = record[name]
|
||||
if value ~= nil then xmlFile["set" .. kind](xmlFile, key .. "#" .. name, value) end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldStorage.readRecord(xmlFile, key)
|
||||
local record = {}
|
||||
for name, kind in pairs(ADFieldStorage.FIELD_TYPES) do
|
||||
record[name] = xmlFile["get" .. kind](xmlFile, key .. "#" .. name)
|
||||
end
|
||||
return record
|
||||
end
|
||||
|
||||
function ADFieldStorage.save(state)
|
||||
local path = ADFieldStorage.getPath()
|
||||
if path == nil then return false end
|
||||
@@ -19,15 +43,7 @@ function ADFieldStorage.save(state)
|
||||
if xmlFile == nil then return false end
|
||||
local index = 0
|
||||
for _, field in pairs(state.fields) do
|
||||
local record = ADFieldModel.toRecord(field)
|
||||
local key = string.format("%s.field(%d)", ADFieldStorage.ROOT, index)
|
||||
xmlFile:setInt(key .. "#id", record.id)
|
||||
xmlFile:setString(key .. "#stage", record.stage)
|
||||
xmlFile:setString(key .. "#mode", record.mode)
|
||||
xmlFile:setString(key .. "#product", record.product)
|
||||
xmlFile:setInt(key .. "#rake", record.rake)
|
||||
xmlFile:setString(key .. "#store", record.store)
|
||||
xmlFile:setFloat(key .. "#mowWidth", record.mowWidth)
|
||||
ADFieldStorage.writeRecord(xmlFile, string.format("%s.field(%d)", ADFieldStorage.ROOT, index), ADFieldModel.toRecord(field))
|
||||
index = index + 1
|
||||
end
|
||||
local vehicleIndex = 0
|
||||
@@ -47,12 +63,7 @@ function ADFieldStorage.load()
|
||||
local xmlFile = XMLFile.load("ADSmartPickupFields", path)
|
||||
if xmlFile == nil then return result end
|
||||
xmlFile:iterate(ADFieldStorage.ROOT .. ".field", function(_, key)
|
||||
local field = ADFieldModel.fromRecord({
|
||||
id = xmlFile:getInt(key .. "#id"), stage = xmlFile:getString(key .. "#stage"),
|
||||
mode = xmlFile:getString(key .. "#mode"), product = xmlFile:getString(key .. "#product"),
|
||||
rake = xmlFile:getInt(key .. "#rake"), store = xmlFile:getString(key .. "#store"),
|
||||
mowWidth = xmlFile:getFloat(key .. "#mowWidth"),
|
||||
})
|
||||
local field = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xmlFile, key))
|
||||
if field.id ~= nil then result.fields[field.id] = field end
|
||||
end)
|
||||
xmlFile:iterate(ADFieldStorage.ROOT .. ".fleet.vehicle", function(_, key)
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
-- AD Field View
|
||||
-- Rene regler for fanen Marker: hvilke marker vises (slåafgrøder som standard), rækkefølge og
|
||||
-- statusord. GUI'et oversætter nøglerne.
|
||||
-- Rene regler for fanen Marker: rækkefølge, statusord, planens tekst og jordtilstand. GUI'et oversætter
|
||||
-- nøglerne.
|
||||
|
||||
ADFieldView = {}
|
||||
|
||||
function ADFieldView.rows(infos, fieldsById, showAll)
|
||||
-- Alle gårdens marker (slåafgrøder, høstafgrøder og bare), sorteret efter marknummer.
|
||||
function ADFieldView.rows(infos, fieldsById)
|
||||
local rows = {}
|
||||
for _, info in ipairs(infos or {}) do
|
||||
if showAll or info.mowable then
|
||||
table.insert(rows, {info = info, field = fieldsById[info.id] or ADFieldModel.newField(info.id)})
|
||||
end
|
||||
table.insert(rows, {info = info, field = fieldsById[info.id] or ADFieldModel.newField(info.id)})
|
||||
end
|
||||
table.sort(rows, function(a, b) return a.info.id < b.info.id end)
|
||||
return rows
|
||||
end
|
||||
|
||||
function ADFieldView.hiddenCount(infos, showAll)
|
||||
if showAll then return 0 end
|
||||
local hidden = 0
|
||||
for _, info in ipairs(infos or {}) do
|
||||
if not info.mowable then hidden = hidden + 1 end
|
||||
end
|
||||
return hidden
|
||||
end
|
||||
|
||||
function ADFieldView.statusKey(field, info)
|
||||
if info.markerId == nil then
|
||||
return "spu_field_noMarker"
|
||||
end
|
||||
return "spu_field_stage_" .. tostring(field.stage)
|
||||
end
|
||||
|
||||
-- Planens tekstnøgle ud fra markens art: ensilage/hø for slåafgrøder, høst, eller klargøring (bar mark).
|
||||
function ADFieldView.planKey(plan, kind)
|
||||
if kind == "bare" then return "spu_field_plan_prep" end
|
||||
return "spu_field_plan_" .. ADFieldModel.product(plan, kind)
|
||||
end
|
||||
|
||||
-- Hvad jorden trænger til, som korte nøgler i fast rækkefølge: kalk, pløj, gødning, ukrudt.
|
||||
function ADFieldView.soilParts(info)
|
||||
local parts = {}
|
||||
local needs = info.needs or {}
|
||||
if needs.lime then table.insert(parts, "lime") end
|
||||
if needs.plow then table.insert(parts, "plow") end
|
||||
if info.fert ~= nil and info.fertMax ~= nil and info.fert < info.fertMax then table.insert(parts, "fert") end
|
||||
if (info.weed or 0) >= ADFieldModel.WEED_MIN then table.insert(parts, "weed") end
|
||||
return parts
|
||||
end
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
-- AD Field Work
|
||||
-- Udsendelse og opfølgning af markflådens sæt: AutoDrive kører sættet til markens F-markør, derfra starter
|
||||
-- Courseplay arbejdet (eget kursus, kopieret kursus eller multi-tool-bane), og når det er færdigt kører
|
||||
-- sættet hjem. Læssevogne tømmes af AutoDrive (Hent og aflever), aflæsningsvogne under høst er Courseplays
|
||||
-- combine-unloader. Beslutningen om HVAD der sendes ud ligger i ADFieldJobs.decide.
|
||||
|
||||
ADFieldWork = {}
|
||||
|
||||
ADFieldWork.ARRIVE_DISTANCE = 30
|
||||
ADFieldWork.IDLE_CONFIRM_MS = 5000
|
||||
ADFieldWork.START_TIMEOUT_MS = 60000
|
||||
ADFieldWork.COURSE_WAIT_MS = 180000
|
||||
ADFieldWork.PLAYER_DRIVING_KMH = 3
|
||||
-- riven skal være udfoldet og i gang, før nedlægningsområdet måles
|
||||
ADFieldWork.WINDROW_MEASURE_MS = 10000
|
||||
-- mangler der sæt, prøves igen efter så lang tid (ikke hvert sekund over hele flåden)
|
||||
ADFieldWork.NO_RIG_RETRY_MS = 30000
|
||||
ADFieldWork.REFILL_ROLES = {seeder = true, fertilizer = true, limer = true, weeder = true}
|
||||
|
||||
local function log(...) ADFieldJobs.log(...) end
|
||||
local function note(...) ADFieldJobs.note(...) end
|
||||
local function now() return ADFieldJobs.now end
|
||||
|
||||
-- ---- AutoDrive ----
|
||||
|
||||
local function stateModule(vehicle)
|
||||
return vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||
end
|
||||
|
||||
local function isAdActive(vehicle)
|
||||
local module = stateModule(vehicle)
|
||||
return module ~= nil and module:isActive()
|
||||
end
|
||||
|
||||
local function stopAd(vehicle)
|
||||
if isAdActive(vehicle) then vehicle:stopAutoDrive() end
|
||||
end
|
||||
|
||||
-- Parkerede køretøjer SOVER i FS25 (motoren kalder ikke onUpdate), så AutoDrives køreopgave står stille,
|
||||
-- til nogen vækker dem (in-game 25/9). Samme greb som flows (applyAndStart) + raiseActive fra AI-Bonden.
|
||||
local function wake(vehicle, kick)
|
||||
if vehicle.raiseActive ~= nil then vehicle:raiseActive() end
|
||||
if kick and vehicle.isServer and SpecializationUtil ~= nil and SpecializationUtil.raiseEvent ~= nil then
|
||||
SpecializationUtil.raiseEvent(vehicle, "onUpdate", 16, false, false, false)
|
||||
end
|
||||
end
|
||||
|
||||
local function enableParkAtJobFinished(vehicle)
|
||||
local setting = vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished or nil
|
||||
if setting ~= nil then
|
||||
setting.current = 2
|
||||
setting.new = 2
|
||||
end
|
||||
end
|
||||
|
||||
local function helperCp(adEnv)
|
||||
return adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
||||
end
|
||||
|
||||
local function startAd(adEnv, vehicle, mode, firstId, secondId)
|
||||
local module = stateModule(vehicle)
|
||||
stopAd(vehicle)
|
||||
module:setMode(mode)
|
||||
module:setFirstMarkerByWayPointId(firstId)
|
||||
if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end
|
||||
module:setStartHelper(false)
|
||||
if module.setActualFarmId ~= nil then module:setActualFarmId(vehicle:getOwnerFarmId()) end
|
||||
module:raiseDirtyFlag()
|
||||
local current = module:getCurrentMode()
|
||||
if current ~= nil and current.start ~= nil then
|
||||
current:start(adEnv.AutoDrive.USER_PLAYER or 1)
|
||||
else
|
||||
vehicle:startAutoDrive()
|
||||
end
|
||||
wake(vehicle, true)
|
||||
end
|
||||
|
||||
-- AD-parkering: sidder på redskabet og kopieres af AD til traktorens stateModule (markør-INDEKS).
|
||||
local function parkMarkerId(adEnv, vehicle)
|
||||
local module = stateModule(vehicle)
|
||||
local index = module ~= nil and module.getParkDestinationAtJobFinished ~= nil and module:getParkDestinationAtJobFinished() or nil
|
||||
if index == nil or index < 1 then return nil end
|
||||
local marker = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||
return marker ~= nil and marker.id or nil
|
||||
end
|
||||
|
||||
local function restLoad(adEnv, vehicle)
|
||||
local ok, level = pcall(function()
|
||||
return (adEnv.AutoDrive.getAllFillLevels(adEnv.AutoDrive.getAllUnits(vehicle)))
|
||||
end)
|
||||
return ok and tonumber(level) or 0
|
||||
end
|
||||
|
||||
local function vehiclePosition(vehicle)
|
||||
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||
return x, z
|
||||
end
|
||||
|
||||
local function isControlledByPlayer(vehicle)
|
||||
return (vehicle.getIsControlled ~= nil and vehicle:getIsControlled()) or false
|
||||
end
|
||||
|
||||
-- ---- flåde ----
|
||||
|
||||
function ADFieldWork.getRigs(adEnv)
|
||||
local rigs = {}
|
||||
local state = ADFieldJobs.state
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
local specs = ADCpBridge.getSpecs(vehicle)
|
||||
local role = ADFieldFleet.roleFromSpecs(specs)
|
||||
local id = ADRunsController.getVehicleId(vehicle)
|
||||
if role ~= nil and id ~= nil then
|
||||
local x, z = vehiclePosition(vehicle)
|
||||
local rig = {
|
||||
id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle),
|
||||
enabled = state.fleet[id] == true,
|
||||
-- optaget: vores egen opgave, et Smart Pickup-flow, eller AutoDrive/Courseplay kører den allerede
|
||||
-- (fx spillerens egen parkeringstur) — den må ikke kapres midt i en anden kørsel
|
||||
busy = state.assignments[id] ~= nil or ADRunsController.getRunForVehicle(vehicle) ~= nil
|
||||
or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle),
|
||||
controlled = isControlledByPlayer(vehicle),
|
||||
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
||||
}
|
||||
if role == "harvester" then rig.fruits = ADCpBridge.getHarvestFruits(vehicle) or {} end
|
||||
if role == "seeder" then
|
||||
rig.seedFruits = ADCpBridge.getSeedFruits(vehicle)
|
||||
rig.hasSeeds = ADCpBridge.hasSeeds(vehicle)
|
||||
end
|
||||
if role == "weeder" then rig.mechanical = ADCpBridge.isMechanicalWeeder(vehicle) end
|
||||
table.insert(rigs, rig)
|
||||
end
|
||||
end
|
||||
return rigs
|
||||
end
|
||||
|
||||
-- ---- lager ----
|
||||
|
||||
-- Gårdens gæringssiloer for markens skår, nærmest marken først (produktioner skår -> SILAGE).
|
||||
function ADFieldWork.getFermenters(adEnv, farmId, windrowFillType, x, z)
|
||||
local silage = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeIndexByName("SILAGE") or nil
|
||||
local result = {}
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
local spec = building.kind == "factory" and building.placeable ~= nil and building.placeable.spec_productionPoint or nil
|
||||
local point = spec ~= nil and spec.productionPoint or nil
|
||||
if point ~= nil and ADFieldModel.isFermenter(point.productions, windrowFillType, silage) then
|
||||
local bx, _, bz = getWorldTranslation(building.placeable.rootNode)
|
||||
table.insert(result, {building = building, distance = (bx - (x or bx)) ^ 2 + (bz - (z or bz)) ^ 2})
|
||||
end
|
||||
end
|
||||
table.sort(result, function(a, b) return a.distance < b.distance end)
|
||||
local buildings = {}
|
||||
for _, entry in ipairs(result) do table.insert(buildings, entry.building) end
|
||||
return buildings
|
||||
end
|
||||
|
||||
local function unloadMarkerIds(buildings)
|
||||
local markerIds = {}
|
||||
for _, building in ipairs(buildings) do
|
||||
for _, marker in ipairs(ADBuildings.getMarkers(building, "unload") or {}) do
|
||||
table.insert(markerIds, marker.id)
|
||||
end
|
||||
end
|
||||
return markerIds
|
||||
end
|
||||
|
||||
-- Hvilken vare markens trin fører til lager: skår (ensilage), tørskår (hø) eller høstet afgrøde.
|
||||
function ADFieldWork.storeFillType(field, info)
|
||||
if field.stage == "harvesting" then return info.harvestFillType end
|
||||
local product = ADFieldModel.product(field.plan, field.kind or info.kind)
|
||||
return product == "hay" and info.hay or info.windrow
|
||||
end
|
||||
|
||||
-- Tvunget lager -> de valgte; ensilage -> gæringssiloer før almindelige siloer (in-game 25/9: græs endte
|
||||
-- i en Farma-silo); hø og høst -> kun siloer (gæringssiloer laver også hø om til ensilage).
|
||||
function ADFieldWork.chooseStore(adEnv, field, info, vehicle, fillType)
|
||||
local isReachable = function(markerId) return ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId) end
|
||||
local farmId = vehicle:getOwnerFarmId()
|
||||
if #field.plan.storeBuildingIds > 0 then
|
||||
local buildings = {}
|
||||
for _, buildingId in ipairs(field.plan.storeBuildingIds) do
|
||||
local building = ADBuildings.find(adEnv, farmId, buildingId)
|
||||
if building ~= nil then table.insert(buildings, building) end
|
||||
end
|
||||
return ADLoadSwap.findForcedDumpMarker(adEnv, vehicle, fillType, 1, unloadMarkerIds(buildings), isReachable)
|
||||
end
|
||||
local product = ADFieldModel.product(field.plan, field.kind or info.kind)
|
||||
if field.stage == "collecting" and product == "silage" then
|
||||
local fermenters = ADFieldWork.getFermenters(adEnv, farmId, fillType, info.x, info.z)
|
||||
if #fermenters > 0 then
|
||||
local store, why = ADLoadSwap.findForcedDumpMarker(adEnv, vehicle, fillType, 1, unloadMarkerIds(fermenters), isReachable)
|
||||
if store ~= nil then return store end
|
||||
log(field.id, "ingen gæringssilo kan tage græsset (%s) — bruger almindelig silo", tostring(why))
|
||||
end
|
||||
end
|
||||
return ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, 1, info.markerId, isReachable, nil, nil, false)
|
||||
end
|
||||
|
||||
-- ---- kurser ----
|
||||
|
||||
local function coursesOf(fieldId)
|
||||
ADFieldJobs.courses[fieldId] = ADFieldJobs.courses[fieldId] or {}
|
||||
return ADFieldJobs.courses[fieldId]
|
||||
end
|
||||
|
||||
-- Kurset et trin skal genbruge ("mower"/"rake"): gemt kopi, ellers maskinens nuværende kursus.
|
||||
local function sourceCourse(adEnv, field, source)
|
||||
local saved = coursesOf(field.id)[source]
|
||||
if saved ~= nil then return saved end
|
||||
local vehicleId = source == "rake" and field.rakeVehicleId or field.mowVehicleId
|
||||
if vehicleId == nil then return nil end
|
||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||
if ADRunsController.getVehicleId(vehicle) == vehicleId then return ADCpBridge.getCourse(vehicle) end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function copyCourse(course)
|
||||
if course ~= nil and course.copy ~= nil then
|
||||
local ok, copy = pcall(course.copy, course)
|
||||
if ok and copy ~= nil then return copy end
|
||||
end
|
||||
return course
|
||||
end
|
||||
|
||||
local function rememberCourse(field, key, vehicle)
|
||||
coursesOf(field.id)[key] = copyCourse(ADCpBridge.getCourse(vehicle))
|
||||
end
|
||||
|
||||
-- ---- udsendelse ----
|
||||
|
||||
local function restoreToolOffset(assignment)
|
||||
if assignment.restoreToolOffset ~= nil then
|
||||
ADCpBridge.setToolOffset(assignment.vehicle, assignment.restoreToolOffset)
|
||||
assignment.restoreToolOffset = nil
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldWork.release(assignment, phase)
|
||||
restoreToolOffset(assignment)
|
||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||
assignment.phase = phase or "released"
|
||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
||||
end
|
||||
|
||||
local function fail(fieldId, text)
|
||||
note(fieldId, {retryAt = now() + ADFieldModel.RETRY_MS, reason = {text = tostring(text)}})
|
||||
log(fieldId, "%s", tostring(text))
|
||||
ADFieldJobs.notify(string.format("F%s: %s", tostring(fieldId), tostring(text)), true)
|
||||
end
|
||||
|
||||
-- Hvad et sæt skal kunne ud over rollen (afgrøde, frø, ukrudtets størrelse).
|
||||
local function acceptFor(field, info, role)
|
||||
if role == "harvester" then
|
||||
local fruit = info.fruitIndex
|
||||
return function(candidate) return fruit ~= nil and candidate.fruits ~= nil and candidate.fruits[fruit] == true end
|
||||
elseif role == "seeder" then
|
||||
local fruitIndex = ADFieldWork.sowFruitIndex(field)
|
||||
return function(candidate) return candidate.hasSeeds and fruitIndex ~= nil and candidate.seedFruits[fruitIndex] == true end
|
||||
elseif role == "weeder" then
|
||||
local large = (info.weed or 0) > ADFieldModel.WEED_MECHANICAL_MAX
|
||||
return function(candidate) return not (large and candidate.mechanical) end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ADFieldWork.sowFruitIndex(field)
|
||||
local name = ADFieldModel.sowFruit(field)
|
||||
if name == nil or g_fruitTypeManager == nil then return nil end
|
||||
local fruit = g_fruitTypeManager:getFruitTypeByName(name)
|
||||
return fruit ~= nil and fruit.index or nil
|
||||
end
|
||||
|
||||
-- Kan trinnet overhovedet begynde? (såning: afgrøde valgt og i såperioden)
|
||||
local function preflight(field, role)
|
||||
if role == "seeder" then
|
||||
local fruitIndex = ADFieldWork.sowFruitIndex(field)
|
||||
if fruitIndex == nil then return false, {key = "spu_field_reason_noSowFruit"} end
|
||||
if not ADCpBridge.isPlantableNow(fruitIndex) then
|
||||
return false, {key = "spu_field_reason_notPlantable", name = ADFieldModel.sowFruit(field)}
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||
local role = decision.role
|
||||
local ok, reason = preflight(field, role)
|
||||
if not ok then
|
||||
note(field.id, {retryAt = now() + ADFieldModel.RETRY_MS, reason = reason})
|
||||
return
|
||||
end
|
||||
local candidates = ADFieldWork.getRigs(adEnv)
|
||||
local required = ADFieldModel.requiredWidth(field, role)
|
||||
local accept = acceptFor(field, info, role)
|
||||
local rigs, why, args
|
||||
if (decision.count or 1) > 1 then
|
||||
rigs, why, args = ADFieldFleet.pickMany(candidates, role, decision.count, info.x, info.z, required, accept)
|
||||
else
|
||||
local rig
|
||||
rig, why, args = ADFieldFleet.pick(candidates, role, info.x, info.z, required, accept)
|
||||
rigs = rig ~= nil and {rig} or nil
|
||||
end
|
||||
if rigs == nil then
|
||||
note(field.id, {reason = {key = "spu_field_reason_" .. tostring(why), role = role, args = args}, retryAt = now() + ADFieldWork.NO_RIG_RETRY_MS})
|
||||
return
|
||||
end
|
||||
local count = #rigs
|
||||
local lanes = ADFieldModel.lanePositions(count)
|
||||
for index, rig in ipairs(rigs) do
|
||||
local assignment = {
|
||||
vehicleId = rig.id, vehicle = rig.vehicle, fieldId = field.id, role = role, stage = field.stage,
|
||||
helper = decision.helper == true, phase = "driving", since = now(), width = rig.width,
|
||||
groupSize = decision.helper and 1 or count, lane = count > 1 and lanes[index] or nil, leader = index == 1,
|
||||
fillType = decision.helper and info.harvestFillType or nil,
|
||||
}
|
||||
ADFieldJobs.state.assignments[rig.id] = assignment
|
||||
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
||||
log(field.id, "%s '%s' sendt til %s (%.1f m%s)", role, rig.label, tostring(info.markerName), rig.width or 0,
|
||||
assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "")
|
||||
end
|
||||
note(field.id, {reason = false})
|
||||
end
|
||||
|
||||
-- Læssevogn: AutoDrive overtager ved "fuld" (Hent og aflever F -> lager, hjælper = Courseplay).
|
||||
local function prepareCollector(adEnv, field, info, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
local store, why = ADFieldWork.chooseStore(adEnv, field, info, vehicle, ADFieldWork.storeFillType(field, info))
|
||||
if store == nil then
|
||||
ADFieldWork.release(assignment)
|
||||
fail(field.id, "intet lager: " .. tostring(why))
|
||||
return false
|
||||
end
|
||||
assignment.storeId = store.id
|
||||
local module = stateModule(vehicle)
|
||||
module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
|
||||
module:setFirstMarkerByWayPointId(info.markerId)
|
||||
module:setSecondMarkerByWayPointId(store.id)
|
||||
module:setStartHelper(true)
|
||||
module.usedHelper = helperCp(adEnv)
|
||||
enableParkAtJobFinished(vehicle)
|
||||
module:raiseDirtyFlag()
|
||||
log(field.id, "læssevogn '%s' aflæsser i %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
|
||||
return true
|
||||
end
|
||||
|
||||
-- Nærmeste påfyldning af varen: en læssestation gården har adgang til, med varen på lager, en AutoDrive-
|
||||
-- markør ved triggeren og vej fra traktoren. (AutoDrives eget "automatiske påfyldningsmål" er slået fra i
|
||||
-- AutoDrive — AutoDrive.automaticPickupTarget = false — så vi finder stedet selv, som silovalget.)
|
||||
function ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z)
|
||||
local farmId = vehicle:getOwnerFarmId()
|
||||
local best, bestDistance = nil, math.huge
|
||||
local ok = pcall(function()
|
||||
for _, station in pairs(g_currentMission.storageSystem:getLoadingStations()) do
|
||||
local supported = station.getAISupportedFillTypes ~= nil and station:getAISupportedFillTypes() or {}
|
||||
if g_currentMission.accessHandler:canFarmAccess(farmId, station) and supported[fillType]
|
||||
and (station:getFillLevel(fillType, farmId) or 0) > 0 then
|
||||
local wayPointId = adEnv.ADTriggerManager:getMarkerAtStation(station, vehicle, 10)
|
||||
local wayPoint = wayPointId ~= nil and wayPointId > 0 and adEnv.ADGraphManager:getWayPointById(wayPointId) or nil
|
||||
if wayPoint ~= nil then
|
||||
local distance = (wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2
|
||||
if distance < bestDistance and ADUnloadWait.hasRouteTo(adEnv, vehicle, wayPointId) then
|
||||
best, bestDistance = wayPointId, distance
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
return ok and best or nil
|
||||
end
|
||||
|
||||
-- Påfyldning undervejs (såmaskine/sprøjte/spreder): AutoDrive LOAD med hjælper = Courseplay. Når CP melder
|
||||
-- tom (onCpEmpty), kører AD til påfyldningen (2. markør), fylder op, kører tilbage til F-markøren (1. markør)
|
||||
-- og genstarter CP. Findes ingen påfyldning, stopper sættet når det er tomt ("tom for …", se finish).
|
||||
local function prepareRefill(adEnv, field, info, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
local fillType = ADCpBridge.getRefillFillType(vehicle)
|
||||
if fillType == nil then return end
|
||||
assignment.refillFillType = fillType
|
||||
local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
|
||||
local refillId = ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, info.x, info.z)
|
||||
if refillId == nil then
|
||||
log(field.id, "ingen påfyldning af %s med AutoDrive-markør fundet — '%s' stopper når den er tom", tostring(name), ADRuns.vehicleLabel(vehicle))
|
||||
return
|
||||
end
|
||||
local module = stateModule(vehicle)
|
||||
local ok = pcall(function()
|
||||
module:setMode(adEnv.AutoDrive.MODE_LOAD)
|
||||
module:setFirstMarkerByWayPointId(info.markerId)
|
||||
module:setSecondMarkerByWayPointId(refillId)
|
||||
module:setFillType(fillType)
|
||||
module:setStartHelper(true)
|
||||
module.usedHelper = helperCp(adEnv)
|
||||
enableParkAtJobFinished(vehicle)
|
||||
module:raiseDirtyFlag()
|
||||
end)
|
||||
if ok then
|
||||
assignment.adHandlesEnd = true
|
||||
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(refillId)
|
||||
log(field.id, "'%s' fyldes op med %s ved %s af AutoDrive når den er tom", ADRuns.vehicleLabel(vehicle), tostring(name), tostring(marker ~= nil and marker.name or refillId))
|
||||
end
|
||||
end
|
||||
|
||||
-- Aflæsningsvogn under høst: Courseplays combine-unloader med stationen fra silovalget.
|
||||
local function beginUnloader(adEnv, field, info, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
local fillType = assignment.fillType or info.harvestFillType
|
||||
local store, why = ADFieldWork.chooseStore(adEnv, field, info, vehicle, fillType)
|
||||
if store == nil then
|
||||
ADFieldWork.release(assignment)
|
||||
fail(field.id, "intet lager til høsten: " .. tostring(why))
|
||||
return
|
||||
end
|
||||
assignment.storeId = store.id
|
||||
local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, store.id, vehicle:getOwnerFarmId())
|
||||
local markerPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
||||
local point = info.points[1] or {x = info.x, z = info.z}
|
||||
assignment.phase = "starting"
|
||||
assignment.since = now()
|
||||
local handle, err = ADCpBridge.startUnloader(vehicle, point.x, point.z, markerPoint ~= nil and markerPoint.x or point.x,
|
||||
markerPoint ~= nil and markerPoint.z or point.z, station, function(ok, reason)
|
||||
if ok then
|
||||
assignment.phase = "working"
|
||||
assignment.since = now()
|
||||
log(field.id, "aflæsningsvogn '%s' følger høsteren -> %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
|
||||
else
|
||||
ADFieldWork.release(assignment)
|
||||
fail(field.id, "aflæsningsvogn: " .. tostring(reason))
|
||||
end
|
||||
end)
|
||||
if handle == nil then
|
||||
ADFieldWork.release(assignment)
|
||||
fail(field.id, "aflæsningsvogn: " .. tostring(err))
|
||||
return
|
||||
end
|
||||
assignment.handle = handle
|
||||
end
|
||||
|
||||
-- Multi-tool: følgere venter på lederens kursus for trinnet og kopierer det med deres bane.
|
||||
local function courseOptions(adEnv, field, assignment)
|
||||
local source = ADFieldModel.courseSource(field, assignment.role)
|
||||
if source ~= nil then
|
||||
local course = sourceCourse(adEnv, field, source)
|
||||
if course == nil then
|
||||
log(field.id, "intet %s-kursus kendt — Courseplay laver et nyt kursus til %s", source, assignment.role)
|
||||
end
|
||||
return {course = course, lane = assignment.lane}, source
|
||||
end
|
||||
if (assignment.groupSize or 1) > 1 then
|
||||
if assignment.leader then
|
||||
return {multiTools = assignment.groupSize, lane = assignment.lane}, nil
|
||||
end
|
||||
local shared = coursesOf(field.id)[field.stage]
|
||||
if shared == nil then return nil, nil end
|
||||
return {course = shared, lane = assignment.lane}, "multi"
|
||||
end
|
||||
return {}, nil
|
||||
end
|
||||
|
||||
local function beginWork(adEnv, field, info, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
if assignment.role == "unloader" then return beginUnloader(adEnv, field, info, assignment) end
|
||||
local opts, source = courseOptions(adEnv, field, assignment)
|
||||
if opts == nil then
|
||||
-- følger uden lederens kursus endnu
|
||||
if assignment.phase ~= "waitingCourse" then
|
||||
assignment.phase = "waitingCourse"
|
||||
assignment.since = now()
|
||||
end
|
||||
return
|
||||
end
|
||||
if assignment.role == "collector" then
|
||||
if not prepareCollector(adEnv, field, info, assignment) then return end
|
||||
assignment.adHandlesEnd = true
|
||||
elseif assignment.role == "seeder" or assignment.role == "fertilizer" or assignment.role == "limer" or assignment.role == "weeder" then
|
||||
prepareRefill(adEnv, field, info, assignment)
|
||||
end
|
||||
if assignment.role == "seeder" then
|
||||
local okSeed, seedError = ADCpBridge.setSeedFruit(vehicle, ADFieldWork.sowFruitIndex(field))
|
||||
if not okSeed then
|
||||
ADFieldWork.release(assignment)
|
||||
fail(field.id, "såmaskine: " .. tostring(seedError))
|
||||
return
|
||||
end
|
||||
end
|
||||
if source == "rake" and field.windrowOffset ~= nil and math.abs(field.windrowOffset) > 0.05 then
|
||||
local own = ADCpBridge.getToolOffset(vehicle)
|
||||
local target = ADFieldModel.collectorOffset(own, field.windrowOffset)
|
||||
if ADCpBridge.setToolOffset(vehicle, target) then
|
||||
assignment.restoreToolOffset = own
|
||||
log(field.id, "skår ligger %.1f m %s for rivens linje -> læssevognens forskydning %.1f m (var %.1f m)",
|
||||
math.abs(field.windrowOffset), field.windrowOffset > 0 and "til venstre" or "til højre", target, own)
|
||||
end
|
||||
end
|
||||
local point = info.points[1] or {x = info.x, z = info.z}
|
||||
assignment.phase = "starting"
|
||||
assignment.since = now()
|
||||
local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason)
|
||||
if ok then
|
||||
assignment.phase = "working"
|
||||
assignment.since = now()
|
||||
if assignment.role == "mower" then
|
||||
note(field.id, {mowWidth = assignment.width, mowVehicleId = assignment.vehicleId})
|
||||
rememberCourse(field, "mower", vehicle)
|
||||
elseif assignment.role == "rake" then
|
||||
note(field.id, {rakeVehicleId = assignment.vehicleId})
|
||||
rememberCourse(field, "rake", vehicle)
|
||||
end
|
||||
if assignment.leader and (assignment.groupSize or 1) > 1 then
|
||||
rememberCourse(field, field.stage, vehicle)
|
||||
end
|
||||
log(field.id, "Courseplay startet (%s%s%s)", assignment.role,
|
||||
reason == "copied" and (", " .. tostring(source) .. "-kursus kopieret") or "",
|
||||
assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "")
|
||||
else
|
||||
ADFieldWork.release(assignment)
|
||||
fail(field.id, "Courseplay fejlede: " .. tostring(reason))
|
||||
end
|
||||
end, opts)
|
||||
if handle == nil then
|
||||
ADFieldWork.release(assignment)
|
||||
fail(field.id, "Courseplay kunne ikke startes: " .. tostring(err))
|
||||
return
|
||||
end
|
||||
assignment.handle = handle
|
||||
end
|
||||
|
||||
-- Sættet er færdigt: rest til lager (læsse-/aflæsningsvogn), ellers parkering.
|
||||
local function finishAssignment(adEnv, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
-- tom såmaskine/sprøjte der ikke blev fyldt op: arbejdet er IKKE færdigt — marken må ikke gå videre
|
||||
if assignment.refillFillType ~= nil and ADCpBridge.getRefillLevel(vehicle, assignment.refillFillType) <= 0 then
|
||||
local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(assignment.refillFillType) or "?"
|
||||
ADFieldWork.release(assignment)
|
||||
fail(assignment.fieldId, string.format("'%s' er tom for %s — fyld op, så fortsætter marken", ADRuns.vehicleLabel(vehicle), tostring(name)))
|
||||
return
|
||||
end
|
||||
log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
|
||||
-- læssevogn/påfyldte sæt parkeres af AutoDrive selv (onCpFinished med parkering sat)
|
||||
local park = not assignment.adHandlesEnd and parkMarkerId(adEnv, vehicle) or nil
|
||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||
restoreToolOffset(assignment)
|
||||
assignment.phase = "finished"
|
||||
table.insert(ADFieldJobs.finished, assignment)
|
||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
||||
end
|
||||
|
||||
local function followWorking(adEnv, field, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
if assignment.role == "rake" and field.windrowOffset == nil and now() - assignment.since >= ADFieldWork.WINDROW_MEASURE_MS
|
||||
and ADCpBridge.isWorking(vehicle) then
|
||||
local offset = ADCpBridge.getWindrowOffset(vehicle)
|
||||
note(assignment.fieldId, {windrowOffset = offset or 0})
|
||||
log(assignment.fieldId, offset ~= nil and "river lægger skåret %.1f m fra sin kursuslinje (venstre +)" or "river: nedlægningsområde ikke fundet — regner med midten (%.1f m)", offset or 0)
|
||||
end
|
||||
if ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) then
|
||||
assignment.idleSince = nil
|
||||
return
|
||||
end
|
||||
assignment.idleSince = assignment.idleSince or now()
|
||||
if now() - assignment.idleSince < ADFieldWork.IDLE_CONFIRM_MS then return end
|
||||
local rest = assignment.storeId ~= nil and not assignment.deliveredRest and restLoad(adEnv, vehicle) or 0
|
||||
if rest > 0 then
|
||||
-- CP færdig/stoppet med læs på: AD tømmer kun selv med parkering sat, så vi sender resten til lager
|
||||
assignment.deliveredRest = true
|
||||
assignment.idleSince = nil
|
||||
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, assignment.storeId, nil)
|
||||
log(assignment.fieldId, "'%s' kører rest-læs (%.0f l) til lager", ADRuns.vehicleLabel(vehicle), rest)
|
||||
return
|
||||
end
|
||||
finishAssignment(adEnv, assignment)
|
||||
end
|
||||
|
||||
function ADFieldWork.follow(adEnv, assignment)
|
||||
local vehicle = assignment.vehicle
|
||||
-- Kun når spilleren selv KØRER (bevæger sig uden Courseplay/AutoDrive) slippes sættet — at sidde i
|
||||
-- førersædet og kigge må ikke slippe det (in-game 25/9).
|
||||
local steeredByAi = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) or assignment.phase == "starting"
|
||||
local speed = vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() or 0
|
||||
if isControlledByPlayer(vehicle) and not steeredByAi and speed > ADFieldWork.PLAYER_DRIVING_KMH then
|
||||
log(assignment.fieldId, "'%s' frigivet — spilleren kører det", ADRuns.vehicleLabel(vehicle))
|
||||
return ADFieldWork.release(assignment)
|
||||
end
|
||||
local field = ADFieldJobs.getField(assignment.fieldId)
|
||||
local info = ADFields.getInfo(assignment.fieldId)
|
||||
wake(vehicle)
|
||||
if assignment.phase == "driving" then
|
||||
if not isAdActive(vehicle) and now() - assignment.since > 2000 then
|
||||
local x, z = vehiclePosition(vehicle)
|
||||
local wayPoint = info ~= nil and adEnv.ADGraphManager:getWayPointById(info.markerId) or nil
|
||||
local distance = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) or math.huge
|
||||
if distance <= ADFieldWork.ARRIVE_DISTANCE then
|
||||
beginWork(adEnv, field, info, assignment)
|
||||
else
|
||||
ADFieldWork.release(assignment)
|
||||
fail(assignment.fieldId, string.format("%s nåede ikke %s (%.0f m væk)", ADRuns.vehicleLabel(vehicle), tostring(info ~= nil and info.markerName or "?"), distance))
|
||||
end
|
||||
end
|
||||
elseif assignment.phase == "waitingCourse" then
|
||||
if now() - assignment.since > ADFieldWork.COURSE_WAIT_MS then
|
||||
ADFieldWork.release(assignment)
|
||||
fail(assignment.fieldId, "multi-tool: lederens kursus kom ikke (3 min)")
|
||||
elseif coursesOf(field.id)[field.stage] ~= nil then
|
||||
beginWork(adEnv, field, info, assignment)
|
||||
end
|
||||
elseif assignment.phase == "starting" then
|
||||
if now() - assignment.since > ADFieldWork.START_TIMEOUT_MS then
|
||||
ADFieldWork.release(assignment)
|
||||
fail(assignment.fieldId, "Courseplay svarede ikke (60 s)")
|
||||
end
|
||||
elseif assignment.phase == "working" then
|
||||
followWorking(adEnv, field, assignment)
|
||||
end
|
||||
end
|
||||
|
||||
-- Høsten er færdig: aflæsningsvognene stoppes; har de rest, kører den til lager, ellers parkering.
|
||||
function ADFieldWork.windDownHelpers(adEnv, fieldId, stage)
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
||||
if assignment.helper and assignment.stage == stage then
|
||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||
ADCpBridge.stop(assignment.vehicle)
|
||||
assignment.phase = "working"
|
||||
assignment.idleSince = nil
|
||||
log(fieldId, "aflæsningsvogn '%s' stoppes — høsten er færdig", ADRuns.vehicleLabel(assignment.vehicle))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ADFieldWork.stopAll(adEnv, fieldId)
|
||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
||||
pcall(stopAd, assignment.vehicle)
|
||||
ADCpBridge.stop(assignment.vehicle)
|
||||
ADFieldWork.release(assignment)
|
||||
end
|
||||
end
|
||||
@@ -33,6 +33,42 @@ function ADFields.buildMowable(mowerConverter, tedderConverter, fillTypeName)
|
||||
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()
|
||||
@@ -47,6 +83,80 @@ function ADFields.getMowable()
|
||||
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
|
||||
@@ -111,7 +221,23 @@ local function readyRatio(fruitIndex, points)
|
||||
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
|
||||
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)
|
||||
@@ -145,8 +271,22 @@ local function scanField(adEnv, field, fieldId)
|
||||
end
|
||||
if mowable ~= nil then
|
||||
info.mowable, info.windrow, info.hay = true, mowable.windrow, mowable.hay
|
||||
info.readyRatio = readyRatio(info.fruitIndex, points)
|
||||
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)
|
||||
if info.x ~= nil then
|
||||
local marker, distance = findMarker(adEnv, fieldId, info.x, info.z)
|
||||
info.markerId = marker ~= nil and marker.id or nil
|
||||
|
||||
@@ -1,44 +1,77 @@
|
||||
-- Smart Pickup — fanen Marker
|
||||
-- Tabel over gårdens slåafgrøde-marker (Mark · Afgrøde · Plan · Auto/Man. · Status · Maskiner); klik på
|
||||
-- en række viser markens detaljer under tabellen (plan, kørsel, riv, tving lager, marksiloer). Nederst
|
||||
-- markflåden: foreslåede sæt med flueben. Al logik ligger i ADFieldJobs/ADFieldModel/ADFieldView.
|
||||
-- Tabel over alle gårdens marker (Mark · Afgrøde · Plan · Kørsel · Status · Maskiner); klik på en række
|
||||
-- viser markens detaljer under tabellen: plan efter markens art (slåafgrøde, høst, bar), maskiner pr. trin,
|
||||
-- aflæsningsvogne, efter høst, såning, pleje, tving lager, jordtilstand. Nederst markflåden med flueben.
|
||||
-- Al logik ligger i ADFieldJobs/ADFieldWork/ADFieldModel/ADFieldView.
|
||||
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
|
||||
|
||||
SmartPickupFrame.FIELD_ROW_KINDS = {
|
||||
showAllFields = true, fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = true,
|
||||
fieldProduct = true, fieldMode = true, fieldRake = true, fieldStore = true, fleetVehicle = true,
|
||||
fieldRigs = true, fieldUnloaders = true, fieldMulch = true, fieldLime = true, fieldSoil = true,
|
||||
fieldSow = true, fieldSowFruit = true, fieldRoll = true, fieldFertilize = true, fieldWeed = true,
|
||||
}
|
||||
SmartPickupFrame.ROLE_KEYS = {mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake", collector = "spu_field_role_collector"}
|
||||
SmartPickupFrame.ROLE_KEYS = {
|
||||
mower = "spu_field_role_mower", tedder = "spu_field_role_tedder", rake = "spu_field_role_rake",
|
||||
collector = "spu_field_role_collector", harvester = "spu_field_role_harvester", unloader = "spu_field_role_unloader",
|
||||
mulcher = "spu_field_role_mulcher", limer = "spu_field_role_limer", plow = "spu_field_role_plow",
|
||||
cultivator = "spu_field_role_cultivator", seeder = "spu_field_role_seeder", roller = "spu_field_role_roller",
|
||||
weeder = "spu_field_role_weeder", fertilizer = "spu_field_role_fertilizer",
|
||||
}
|
||||
SmartPickupFrame.SOIL_VALUES = {"auto", "cultivate", "plow", "none"}
|
||||
-- simple Ja/Nej-valg på planen: rækketype -> plan-felt
|
||||
SmartPickupFrame.PLAN_TOGGLES = {
|
||||
fieldRake = "rake", fieldMulch = "mulch", fieldLime = "lime", fieldSow = "sow", fieldRoll = "roll",
|
||||
fieldFertilize = "fertilize", fieldWeed = "weed",
|
||||
}
|
||||
-- valg med flere muligheder: rækketype -> plan-felt
|
||||
SmartPickupFrame.PLAN_CHOICES = {fieldRigs = "rigs", fieldUnloaders = "unloaders", fieldSoil = "soil", fieldSowFruit = "sowFruit"}
|
||||
|
||||
-- ---- tekster ----
|
||||
|
||||
function SmartPickupFrame:roleText(role)
|
||||
return self.i18n:getText(SmartPickupFrame.ROLE_KEYS[role] or "spu_field_role_mower")
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldReasonText(reason)
|
||||
if type(reason) ~= "table" then return nil end
|
||||
if reason.text ~= nil then return reason.text end
|
||||
local role = reason.role ~= nil and self.i18n:getText(SmartPickupFrame.ROLE_KEYS[reason.role] or "spu_field_role_mower") or ""
|
||||
local first = reason.name or (reason.role ~= nil and self:roleText(reason.role)) or ""
|
||||
local template = self.i18n:getText(reason.key)
|
||||
local args = reason.args or {}
|
||||
local ok, text = pcall(string.format, template, role, args[1] or 0, args[2] or 0)
|
||||
local ok, text = pcall(string.format, template, first, args[1] or 0, args[2] or 0)
|
||||
return ok and text or template
|
||||
end
|
||||
|
||||
function SmartPickupFrame:soilText(info)
|
||||
local texts = {}
|
||||
for _, part in ipairs(ADFieldView.soilParts(info)) do
|
||||
table.insert(texts, self.i18n:getText("spu_field_need_" .. part))
|
||||
end
|
||||
return table.concat(texts, ", ")
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldStatusText(field, info)
|
||||
local text = self.i18n:getText(ADFieldView.statusKey(field, info))
|
||||
if info.markerId == nil then
|
||||
return string.format(text, info.id)
|
||||
end
|
||||
if (field.stage == "idle" or field.stage == "ready") and info.mowable then
|
||||
if (field.stage == "idle" or field.stage == "ready") and info.kind ~= "bare" and info.readyRatio ~= nil then
|
||||
text = string.format("%s %d %%", text, math.floor((info.readyRatio or 0) * 100 + 0.5))
|
||||
end
|
||||
local decision = ADFieldJobs.lastDecision[info.id]
|
||||
if decision ~= nil and decision.care ~= nil and not ADFieldModel.isWorking(field) then
|
||||
text = text .. " — " .. string.format(self.i18n:getText("spu_field_careNeed"), self.i18n:getText("spu_field_stage_" .. decision.care))
|
||||
end
|
||||
local reason = self:fieldReasonText(field.reason)
|
||||
if reason ~= nil then text = text .. ": " .. reason end
|
||||
return text
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fieldPlanText(field)
|
||||
local text = self.i18n:getText(field.plan.product == "hay" and "spu_field_plan_hay" or "spu_field_plan_silage")
|
||||
if field.plan.product == "hay" and field.plan.rake then
|
||||
text = text .. " + " .. self.i18n:getText("spu_field_plan_rake")
|
||||
end
|
||||
function SmartPickupFrame:fieldPlanText(field, info)
|
||||
local text = self.i18n:getText(ADFieldView.planKey(field.plan, info.kind))
|
||||
if field.plan.rake and info.kind == "mowable" then text = text .. " + " .. self.i18n:getText("spu_field_plan_rake") end
|
||||
if field.plan.sow and info.kind ~= "mowable" then text = text .. " + " .. self.i18n:getText("spu_field_plan_sow") end
|
||||
if (field.plan.rigs or 1) > 1 then text = string.format("%s (x%d)", text, field.plan.rigs) end
|
||||
return text
|
||||
end
|
||||
|
||||
@@ -49,14 +82,52 @@ function SmartPickupFrame:fieldCells(row)
|
||||
table.insert(names, ADRuns.vehicleLabel(assignment.vehicle))
|
||||
end
|
||||
local name = string.format("%s%sF%d", info.id == self.selectedFieldId and "> " or "", info.markerId == nil and "! " or "", info.id)
|
||||
local crop = info.kind == "bare" and self.i18n:getText("spu_field_bare") or (info.cropTitle or "-")
|
||||
return {
|
||||
name, info.cropTitle or "-", info.mowable and self:fieldPlanText(field) or "-",
|
||||
name, crop, self:fieldPlanText(field, info),
|
||||
self.i18n:getText(field.plan.mode == "auto" and "spu_field_mode_auto" or "spu_field_mode_manual"),
|
||||
info.mowable and self:fieldStatusText(field, info) or self.i18n:getText("spu_field_notMowable"),
|
||||
self:fieldStatusText(field, info),
|
||||
ADFlowView.joinCounted(names, ", ", "-"),
|
||||
}
|
||||
end
|
||||
|
||||
-- ---- rækker ----
|
||||
|
||||
-- Valg med flere muligheder (klon af productPrefab): values[i] er værdien for texts[i].
|
||||
function SmartPickupFrame:addChoiceRow(kind, title, texts, values, current)
|
||||
local row = self:addRow(self.productPrefab, {kind = kind, values = values})
|
||||
local option = row:getDescendantByName("option")
|
||||
option:setTexts(texts)
|
||||
local index = 1
|
||||
for position, value in ipairs(values) do
|
||||
if value == current then index = position end
|
||||
end
|
||||
option:setState(index, false)
|
||||
row:getDescendantByName("title"):setText(title)
|
||||
return option
|
||||
end
|
||||
|
||||
local function numberTexts(from, to)
|
||||
local texts, values = {}, {}
|
||||
for value = from, to do
|
||||
table.insert(texts, tostring(value))
|
||||
table.insert(values, value)
|
||||
end
|
||||
return texts, values
|
||||
end
|
||||
|
||||
-- Afgrøder der kan sås (første valg: samme som den høstede).
|
||||
function SmartPickupFrame:sowFruitChoices()
|
||||
local texts, values = {self.i18n:getText("spu_field_sowSame")}, {""}
|
||||
for _, fruit in ipairs(g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}) do
|
||||
if fruit.allowsSeeding ~= false and fruit.fillType ~= nil and fruit.name ~= nil then
|
||||
table.insert(texts, tostring(fruit.fillType.title or fruit.name))
|
||||
table.insert(values, fruit.name)
|
||||
end
|
||||
end
|
||||
return texts, values
|
||||
end
|
||||
|
||||
-- ---- opbygning ----
|
||||
|
||||
function SmartPickupFrame:fillFieldRows(adEnv)
|
||||
@@ -67,12 +138,8 @@ function SmartPickupFrame:fillFieldRows(adEnv)
|
||||
self:addTextRow(i18n:getText("spu_field_scanning"))
|
||||
else
|
||||
self:addTextRow(i18n:getText("spu_field_hint"))
|
||||
local hidden = ADFieldView.hiddenCount(infos, self.showAllFields == true)
|
||||
if hidden > 0 or self.showAllFields then
|
||||
self:addToggleRow("showAllFields", string.format(i18n:getText("spu_field_showAll"), hidden), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, self.showAllFields == true)
|
||||
end
|
||||
self:addCellRow(self.fieldPrefab, {i18n:getText("spu_col_field"), i18n:getText("spu_col_crop"), i18n:getText("spu_col_plan"), i18n:getText("spu_col_mode"), i18n:getText("spu_col_status"), i18n:getText("spu_col_tractors")})
|
||||
for _, row in ipairs(ADFieldView.rows(infos, ADFieldJobs.state.fields, self.showAllFields == true)) do
|
||||
for _, row in ipairs(ADFieldView.rows(infos, ADFieldJobs.state.fields)) do
|
||||
local cells = self:addCellRow(self.fieldPrefab, self:fieldCells(row))
|
||||
local fieldId = row.info.id
|
||||
if cells[1] ~= nil and cells[1].parent ~= nil then
|
||||
@@ -94,17 +161,42 @@ end
|
||||
function SmartPickupFrame:fillFieldDetail(adEnv, info)
|
||||
local i18n = self.i18n
|
||||
local field = ADFieldJobs.getField(info.id)
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, tostring(info.cropTitle or "-")))
|
||||
if not info.mowable then
|
||||
self:addTextRow(" " .. i18n:getText("spu_field_notMowable"))
|
||||
return
|
||||
local plan = field.plan
|
||||
local yesNo = {i18n:getText("ui_no"), i18n:getText("ui_yes")}
|
||||
local crop = info.kind == "bare" and i18n:getText("spu_field_bare") or tostring(info.cropTitle or "-")
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_section"), info.id, crop))
|
||||
self:addToggleRow("fieldMode", i18n:getText("spu_field_mode"), {i18n:getText("spu_field_mode_manual"), i18n:getText("spu_field_mode_auto")}, plan.mode == "auto")
|
||||
local rigTexts, rigValues = numberTexts(1, ADFieldModel.MAX_RIGS)
|
||||
self:addChoiceRow("fieldRigs", i18n:getText("spu_field_rigs"), rigTexts, rigValues, plan.rigs or 1)
|
||||
if info.kind == "mowable" then
|
||||
local isHay = ADFieldModel.product(plan, "mowable") == "hay"
|
||||
self:addToggleRow("fieldProduct", i18n:getText("spu_field_plan"), {i18n:getText("spu_field_plan_silage"), i18n:getText("spu_field_plan_hay")}, isHay)
|
||||
self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), yesNo, plan.rake == true)
|
||||
else
|
||||
if info.kind == "harvest" then
|
||||
local unloaderTexts, unloaderValues = numberTexts(1, ADFieldModel.MAX_UNLOADERS)
|
||||
self:addChoiceRow("fieldUnloaders", i18n:getText("spu_field_unloaders"), unloaderTexts, unloaderValues, plan.unloaders or 1)
|
||||
end
|
||||
self:addTextRow(i18n:getText(info.kind == "harvest" and "spu_field_section_after" or "spu_field_section_prep"))
|
||||
self:addToggleRow("fieldMulch", i18n:getText("spu_field_mulch"), yesNo, plan.mulch == true)
|
||||
local soilTexts = {}
|
||||
for _, value in ipairs(SmartPickupFrame.SOIL_VALUES) do table.insert(soilTexts, i18n:getText("spu_field_soil_" .. value)) end
|
||||
self:addChoiceRow("fieldSoil", i18n:getText("spu_field_soil"), soilTexts, SmartPickupFrame.SOIL_VALUES, plan.soil or "auto")
|
||||
self:addToggleRow("fieldSow", i18n:getText("spu_field_sow"), yesNo, plan.sow == true)
|
||||
if plan.sow then
|
||||
local fruitTexts, fruitValues = self:sowFruitChoices()
|
||||
self:addChoiceRow("fieldSowFruit", i18n:getText("spu_field_sowFruit"), fruitTexts, fruitValues, plan.sowFruit or "")
|
||||
self:addToggleRow("fieldRoll", i18n:getText("spu_field_roll"), yesNo, plan.roll == true)
|
||||
end
|
||||
end
|
||||
local isHay = field.plan.product == "hay"
|
||||
self:addToggleRow("fieldProduct", i18n:getText("spu_field_plan"), {i18n:getText("spu_field_plan_silage"), i18n:getText("spu_field_plan_hay")}, isHay)
|
||||
self:addToggleRow("fieldMode", i18n:getText("spu_field_mode"), {i18n:getText("spu_field_mode_manual"), i18n:getText("spu_field_mode_auto")}, field.plan.mode == "auto")
|
||||
if isHay then
|
||||
self:addToggleRow("fieldRake", i18n:getText("spu_field_rake"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, field.plan.rake == true)
|
||||
self:addTextRow(i18n:getText("spu_field_section_care"))
|
||||
self:addToggleRow("fieldFertilize", i18n:getText("spu_field_fertilize"), yesNo, plan.fertilize ~= false)
|
||||
self:addToggleRow("fieldLime", i18n:getText("spu_field_lime"), yesNo, plan.lime ~= false)
|
||||
if info.kind ~= "mowable" then
|
||||
self:addToggleRow("fieldWeed", i18n:getText("spu_field_weed"), yesNo, plan.weed ~= false)
|
||||
end
|
||||
local soil = self:soilText(info)
|
||||
self:addTextRow(" " .. string.format(i18n:getText("spu_field_soilState"), soil ~= "" and soil or i18n:getText("spu_field_soilOk")))
|
||||
if info.markerId ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_marker"), tostring(info.markerName), info.markerDistance or 0))
|
||||
else
|
||||
@@ -113,25 +205,42 @@ function SmartPickupFrame:fillFieldDetail(adEnv, info)
|
||||
if field.mowWidth ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_mowWidth"), field.mowWidth))
|
||||
end
|
||||
if field.plan.rake and field.windrowOffset ~= nil then
|
||||
self:addTextRow(string.format(i18n:getText("spu_field_windrowOffset"), math.abs(field.windrowOffset),
|
||||
i18n:getText(math.abs(field.windrowOffset) <= 0.05 and "spu_field_side_center" or (field.windrowOffset > 0 and "spu_field_side_left" or "spu_field_side_right"))))
|
||||
end
|
||||
self:fillFieldStoreRows(adEnv, info, field)
|
||||
end
|
||||
|
||||
-- Tving lager: siloer der tager markens vare (skår ved ensilage, tørskår ved hø); valgte først.
|
||||
-- Tving lager: siloer der tager markens vare (skår/tørskår/høst); ved ensilage også gæringssiloer.
|
||||
function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field)
|
||||
local i18n = self.i18n
|
||||
local product = ADFieldModel.product(field.plan, info.kind)
|
||||
local fillType
|
||||
if info.kind == "mowable" then
|
||||
fillType = product == "hay" and info.hay or info.windrow
|
||||
else
|
||||
fillType = info.harvestFillType
|
||||
end
|
||||
if fillType == nil then return end
|
||||
self:addTextRow(i18n:getText("spu_field_section_store"))
|
||||
local fillType = field.plan.product == "hay" and info.hay or info.windrow
|
||||
local chosen = {}
|
||||
for _, id in ipairs(field.plan.storeBuildingIds) do chosen[id] = true end
|
||||
local farmId = g_currentMission:getFarmId()
|
||||
local fieldSilos = {}
|
||||
for _, building in ipairs(ADFields.fieldSilos(adEnv, farmId, info)) do fieldSilos[building.id] = building end
|
||||
local fermenterIds = {}
|
||||
if info.kind == "mowable" and product == "silage" then
|
||||
for _, building in ipairs(ADFieldJobs.getFermenters(adEnv, farmId, fillType, info.x, info.z)) do fermenterIds[building.id] = true end
|
||||
end
|
||||
local count = 0
|
||||
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||
if building.kind == "silo" and (chosen[building.id] or ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})) then
|
||||
local isSilo = building.kind == "silo" and ADRunsController.canStoreAt(adEnv, building, {{fillType = fillType, isPallet = false}})
|
||||
if chosen[building.id] or isSilo or fermenterIds[building.id] then
|
||||
count = count + 1
|
||||
local row = self:addRow(self.ingredientPrefab, {kind = "fieldStore", buildingId = building.id})
|
||||
local suffix = fieldSilos[building.id] ~= nil and (" (" .. i18n:getText("spu_field_fieldSilo") .. ")") or ""
|
||||
if fermenterIds[building.id] then suffix = suffix .. " (" .. i18n:getText("spu_field_fermenter") .. ")" end
|
||||
row:getDescendantByName("title"):setText(" " .. tostring(building.name) .. suffix)
|
||||
row:getDescendantByName("option"):setState(chosen[building.id] and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
@@ -139,7 +248,7 @@ function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field)
|
||||
if count == 0 then
|
||||
self:addTextRow(" " .. i18n:getText("spu_field_noStore"))
|
||||
elseif #field.plan.storeBuildingIds == 0 then
|
||||
self:addTextRow(" " .. i18n:getText("spu_field_storeAuto"))
|
||||
self:addTextRow(" " .. i18n:getText((info.kind == "mowable" and product == "silage") and "spu_field_storeAutoSilage" or "spu_field_storeAuto"))
|
||||
end
|
||||
-- marksiloer skal tømmes hjem af et udkørselsflow
|
||||
for id, building in pairs(fieldSilos) do
|
||||
@@ -155,6 +264,25 @@ function SmartPickupFrame:fillFieldStoreRows(adEnv, info, field)
|
||||
end
|
||||
end
|
||||
|
||||
-- Ekstra oplysning pr. sæt i markflåden (afgrøder, frø, strigle/sprøjte, bredde over 50 m, mark).
|
||||
function SmartPickupFrame:rigDetails(rig)
|
||||
local i18n = self.i18n
|
||||
local parts = {}
|
||||
if rig.role == "harvester" then
|
||||
local count = 0
|
||||
for _ in pairs(rig.fruits or {}) do count = count + 1 end
|
||||
table.insert(parts, string.format(i18n:getText("spu_field_rig_crops"), count))
|
||||
elseif rig.role == "seeder" then
|
||||
table.insert(parts, i18n:getText(rig.hasSeeds and "spu_field_rig_seeds" or "spu_field_rig_noSeeds"))
|
||||
elseif rig.role == "weeder" then
|
||||
table.insert(parts, i18n:getText(rig.mechanical and "spu_field_rig_mechanical" or "spu_field_rig_herbicide"))
|
||||
end
|
||||
if (rig.width or 0) > ADFieldModel.CP_MAX_WIDTH then table.insert(parts, i18n:getText("spu_field_tooWideShort")) end
|
||||
local assignment = ADFieldJobs.state.assignments[rig.id]
|
||||
if assignment ~= nil then table.insert(parts, string.format("F%d", assignment.fieldId)) end
|
||||
return #parts > 0 and (" — " .. table.concat(parts, " — ")) or ""
|
||||
end
|
||||
|
||||
function SmartPickupFrame:fillFleetRows(adEnv)
|
||||
local i18n = self.i18n
|
||||
self:addTextRow(i18n:getText("spu_field_section_fleet"))
|
||||
@@ -165,14 +293,7 @@ function SmartPickupFrame:fillFleetRows(adEnv)
|
||||
end)
|
||||
for _, rig in ipairs(rigs) do
|
||||
local row = self:addRow(self.vehiclePrefab, {kind = "fleetVehicle", vehicleId = rig.id})
|
||||
local title = string.format(" %s — %s, %.1f m", tostring(rig.label), i18n:getText(SmartPickupFrame.ROLE_KEYS[rig.role]), rig.width or 0)
|
||||
if (rig.width or 0) > ADFieldModel.CP_MAX_WIDTH then
|
||||
title = title .. " — " .. i18n:getText("spu_field_tooWideShort")
|
||||
end
|
||||
local assignment = ADFieldJobs.state.assignments[rig.id]
|
||||
if assignment ~= nil then
|
||||
title = title .. string.format(" — F%d", assignment.fieldId)
|
||||
end
|
||||
local title = string.format(" %s — %s, %.1f m%s", tostring(rig.label), self:roleText(rig.role), rig.width or 0, self:rigDetails(rig))
|
||||
row:getDescendantByName("title"):setText(title)
|
||||
row:getDescendantByName("option"):setState(rig.enabled and SmartPickupFrame.STATE_ON or SmartPickupFrame.STATE_OFF, false)
|
||||
end
|
||||
@@ -185,13 +306,13 @@ end
|
||||
|
||||
function SmartPickupFrame:refreshFieldRows(adEnv)
|
||||
local rowsById = {}
|
||||
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields, true)) do rowsById[row.info.id] = row end
|
||||
for _, row in ipairs(ADFieldView.rows(ADFields.list(), ADFieldJobs.state.fields)) do rowsById[row.info.id] = row end
|
||||
local working, total = 0, 0
|
||||
for _, entry in ipairs(self.fieldRows or {}) do
|
||||
local row = rowsById[entry.id]
|
||||
if row ~= nil then
|
||||
SmartPickupFrame.setCells(entry.cells, self:fieldCells(row))
|
||||
if row.info.mowable then total = total + 1 end
|
||||
total = total + 1
|
||||
if ADFieldModel.isWorking(row.field) then working = working + 1 end
|
||||
end
|
||||
end
|
||||
@@ -200,8 +321,7 @@ function SmartPickupFrame:refreshFieldRows(adEnv)
|
||||
local info = self.selectedFieldId ~= nil and ADFields.getInfo(self.selectedFieldId) or nil
|
||||
local isWorking = selected ~= nil and ADFieldModel.isWorking(selected)
|
||||
self.fieldStartButtonInfo.text = self.i18n:getText(isWorking and "spu_field_button_stop" or "spu_field_button_start")
|
||||
self.fieldStartButtonInfo.disabled = selected == nil or info == nil or not info.mowable
|
||||
or (not isWorking and (info.markerId == nil or (info.readyRatio or 0) < ADFieldModel.CUT_RATIO))
|
||||
self.fieldStartButtonInfo.disabled = selected == nil or info == nil or (not isWorking and info.markerId == nil)
|
||||
end
|
||||
|
||||
-- ---- klik ----
|
||||
@@ -210,18 +330,19 @@ function SmartPickupFrame:onClickFieldRow(state, data)
|
||||
self:safe(function(frame)
|
||||
local isOn = state == SmartPickupFrame.STATE_ON
|
||||
local id = frame.selectedFieldId
|
||||
if data.kind == "showAllFields" then
|
||||
frame.showAllFields = isOn
|
||||
elseif data.kind == "fleetVehicle" then
|
||||
if data.kind == "fleetVehicle" then
|
||||
ADFieldJobs.toggleFleet(data.vehicleId)
|
||||
elseif id == nil then
|
||||
return
|
||||
elseif SmartPickupFrame.PLAN_CHOICES[data.kind] ~= nil then
|
||||
local value = data.values ~= nil and data.values[state] or nil
|
||||
if value ~= nil then ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_CHOICES[data.kind]] = value}) end
|
||||
elseif data.kind == "fieldProduct" then
|
||||
ADFieldJobs.setPlan(id, {product = isOn and "hay" or "silage"})
|
||||
elseif data.kind == "fieldMode" then
|
||||
ADFieldJobs.setPlan(id, {mode = isOn and "auto" or "manual"})
|
||||
elseif data.kind == "fieldRake" then
|
||||
ADFieldJobs.setPlan(id, {rake = isOn})
|
||||
elseif SmartPickupFrame.PLAN_TOGGLES[data.kind] ~= nil then
|
||||
ADFieldJobs.setPlan(id, {[SmartPickupFrame.PLAN_TOGGLES[data.kind]] = isOn})
|
||||
elseif data.kind == "fieldStore" then
|
||||
local ids = {}
|
||||
for _, buildingId in ipairs(ADFieldJobs.getField(id).plan.storeBuildingIds) do
|
||||
@@ -234,6 +355,7 @@ function SmartPickupFrame:onClickFieldRow(state, data)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Start nu: pleje hvis marken trænger (og vokser), ellers hele kæden (høst/slå, eller klargøring på bar mark).
|
||||
function SmartPickupFrame:onClickFieldStartStop()
|
||||
self:safe(function(frame)
|
||||
local id = frame.selectedFieldId
|
||||
@@ -242,7 +364,8 @@ function SmartPickupFrame:onClickFieldStartStop()
|
||||
if ADFieldModel.isWorking(field) then
|
||||
ADFieldJobs.stopField(frame.getAdEnv(), id)
|
||||
else
|
||||
if field.stage ~= "ready" then
|
||||
local decision = ADFieldJobs.lastDecision[id]
|
||||
if decision == nil or decision.care == nil then
|
||||
ADFieldJobs.forceReady(id)
|
||||
end
|
||||
ADFieldJobs.requestStart(id)
|
||||
|
||||
@@ -182,6 +182,9 @@ function SmartPickupFrame:onClickProduct(state, element)
|
||||
if clicked ~= nil and clicked.kind == "storageBuilding" then
|
||||
return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
|
||||
end
|
||||
if clicked ~= nil and SmartPickupFrame.FIELD_ROW_KINDS ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[clicked.kind] then
|
||||
return self:onClickFieldRow(state, clicked)
|
||||
end
|
||||
self:safe(function(frame)
|
||||
local run, data = frame:getRun(), frame.rowData[element]
|
||||
if run == nil or data == nil or data.kind ~= "product" then return end
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<text name="spu_field_reason_noRig" text="ingen %s med flueben i markflåden"/>
|
||||
<text name="spu_field_reason_allBusy" text="venter på ledig %s"/>
|
||||
<text name="spu_field_reason_tooWide" text="%s for bred: %.1f m > %.0f m (Courseplay)"/>
|
||||
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m < slåbredde %.1f m"/>
|
||||
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m < slåbredde %.1f m — sæt Riv til eller brug en bredere"/>
|
||||
<text name="spu_field_role_mower" text="slåmaskine"/>
|
||||
<text name="spu_field_role_tedder" text="vender"/>
|
||||
<text name="spu_field_role_rake" text="river"/>
|
||||
@@ -74,7 +74,7 @@
|
||||
<text name="spu_field_section" text="— Mark F%d: %s —"/>
|
||||
<text name="spu_field_plan" text="Plan"/>
|
||||
<text name="spu_field_mode" text="Kørsel"/>
|
||||
<text name="spu_field_rake" text="Riv høet sammen før opsamling"/>
|
||||
<text name="spu_field_rake" text="Riv skårene sammen før opsamling (gør smalle læssevogne brugbare)"/>
|
||||
<text name="spu_field_marker" text=" Markør %s, %.0f m fra marken"/>
|
||||
<text name="spu_field_mowWidth" text=" Slået med %.1f m — opsamling skal være mindst lige så bred"/>
|
||||
<text name="spu_field_section_store" text="— Tving lager (ellers nærmeste silo med plads) —"/>
|
||||
@@ -87,6 +87,72 @@
|
||||
<text name="spu_field_noRigs" text="ingen køretøjer med slåmaskine, vender, river eller læssevogn"/>
|
||||
<text name="spu_field_status" text="Marker i gang: %d af %d"/>
|
||||
<text name="spu_field_startRequested" text="F%d startes, når en maskine er ledig"/>
|
||||
<text name="spu_field_windrowOffset" text=" Riven lægger skåret %.1f m %s — læssevognen forskydes tilsvarende"/>
|
||||
<text name="spu_field_side_center" text="i midten"/>
|
||||
<text name="spu_field_side_left" text="til venstre"/>
|
||||
<text name="spu_field_side_right" text="til højre"/>
|
||||
<text name="spu_field_fermenter" text="gæringssilo"/>
|
||||
<text name="spu_field_storeAutoSilage" text="intet valgt: nærmeste gæringssilo med plads, ellers nærmeste silo"/>
|
||||
<text name="spu_field_bare" text="bar jord"/>
|
||||
<text name="spu_field_careNeed" text="trænger til %s"/>
|
||||
<text name="spu_field_fertilize" text="Gød op til max (når afgrøden vokser)"/>
|
||||
<text name="spu_field_lime" text="Kalk når jorden trænger"/>
|
||||
<text name="spu_field_mulch" text="Stubbehakning efter høst"/>
|
||||
<text name="spu_field_need_fert" text="gødning"/>
|
||||
<text name="spu_field_need_lime" text="kalk"/>
|
||||
<text name="spu_field_need_plow" text="pløjning"/>
|
||||
<text name="spu_field_need_weed" text="ukrudt"/>
|
||||
<text name="spu_field_plan_harvest" text="Høst"/>
|
||||
<text name="spu_field_plan_prep" text="Klargøring"/>
|
||||
<text name="spu_field_plan_sow" text="så"/>
|
||||
<text name="spu_field_reason_cannotDo" text="ingen %s i markflåden kan tage opgaven (afgrøde/frø/ukrudt)"/>
|
||||
<text name="spu_field_reason_noSowFruit" text="vælg en afgrøde at så"/>
|
||||
<text name="spu_field_reason_notEnough" text="multi-tool: %s — kun %d af %d ledige"/>
|
||||
<text name="spu_field_reason_notPlantable" text="%s kan ikke sås i denne periode"/>
|
||||
<text name="spu_field_rig_crops" text="%d afgrøder"/>
|
||||
<text name="spu_field_rig_herbicide" text="sprøjte"/>
|
||||
<text name="spu_field_rig_mechanical" text="strigle (småt ukrudt)"/>
|
||||
<text name="spu_field_rig_noSeeds" text="!! tom for frø"/>
|
||||
<text name="spu_field_rig_seeds" text="frø ok"/>
|
||||
<text name="spu_field_rigs" text="Maskiner pr. trin (Courseplay multi-tool)"/>
|
||||
<text name="spu_field_role_cultivator" text="kultivator"/>
|
||||
<text name="spu_field_role_fertilizer" text="gødningsspreder"/>
|
||||
<text name="spu_field_role_harvester" text="høster"/>
|
||||
<text name="spu_field_role_limer" text="kalkspreder"/>
|
||||
<text name="spu_field_role_mulcher" text="stubhakker"/>
|
||||
<text name="spu_field_role_plow" text="plov"/>
|
||||
<text name="spu_field_role_roller" text="tromle"/>
|
||||
<text name="spu_field_role_seeder" text="såmaskine"/>
|
||||
<text name="spu_field_role_unloader" text="aflæsningsvogn"/>
|
||||
<text name="spu_field_role_weeder" text="ukrudtsmaskine"/>
|
||||
<text name="spu_field_roll" text="Tromle efter såning"/>
|
||||
<text name="spu_field_section_after" text="— Efter høst —"/>
|
||||
<text name="spu_field_section_care" text="— Pleje mens afgrøden vokser —"/>
|
||||
<text name="spu_field_section_prep" text="— Klargøring —"/>
|
||||
<text name="spu_field_soil" text="Jordbehandling"/>
|
||||
<text name="spu_field_soilOk" text="intet"/>
|
||||
<text name="spu_field_soilState" text="Jorden trænger til: %s"/>
|
||||
<text name="spu_field_soil_auto" text="Auto (pløj når nødvendigt)"/>
|
||||
<text name="spu_field_soil_cultivate" text="Kultivér"/>
|
||||
<text name="spu_field_soil_none" text="Ingen"/>
|
||||
<text name="spu_field_soil_plow" text="Pløj"/>
|
||||
<text name="spu_field_sow" text="Så bagefter"/>
|
||||
<text name="spu_field_sowFruit" text="Afgrøde at så"/>
|
||||
<text name="spu_field_sowSame" text="Samme som høstet"/>
|
||||
<text name="spu_field_stage_careLiming" text="Kalkes"/>
|
||||
<text name="spu_field_stage_cultivating" text="Kultiveres"/>
|
||||
<text name="spu_field_stage_fertilizing" text="Gødes"/>
|
||||
<text name="spu_field_stage_harvesting" text="Høstes"/>
|
||||
<text name="spu_field_stage_liming" text="Kalkes"/>
|
||||
<text name="spu_field_stage_mulching" text="Stub hakkes"/>
|
||||
<text name="spu_field_stage_plowing" text="Pløjes"/>
|
||||
<text name="spu_field_stage_rolling" text="Tromles"/>
|
||||
<text name="spu_field_stage_sowing" text="Sås"/>
|
||||
<text name="spu_field_stage_weeding" text="Ukrudt bekæmpes"/>
|
||||
<text name="spu_field_unloaders" text="Aflæsningsvogne under høst"/>
|
||||
<text name="spu_field_weed" text="Bekæmp ukrudt"/>
|
||||
<text name="spu_field_notice_done" text="er færdig"/>
|
||||
<text name="spu_field_reason_rain" text="venter på tørvejr"/>
|
||||
<text name="spu_button_start" text="Start run"/>
|
||||
<text name="spu_button_stop" text="Stop run"/>
|
||||
<text name="spu_button_new" text="Nyt run"/>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<text name="spu_field_reason_noRig" text="no %s ticked in the field fleet"/>
|
||||
<text name="spu_field_reason_allBusy" text="waiting for a free %s"/>
|
||||
<text name="spu_field_reason_tooWide" text="%s too wide: %.1f m > %.0f m (Courseplay)"/>
|
||||
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m < mowing width %.1f m"/>
|
||||
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m < mowing width %.1f m — turn on Rake or use a wider one"/>
|
||||
<text name="spu_field_role_mower" text="mower"/>
|
||||
<text name="spu_field_role_tedder" text="tedder"/>
|
||||
<text name="spu_field_role_rake" text="rake"/>
|
||||
@@ -74,7 +74,7 @@
|
||||
<text name="spu_field_section" text="— Field F%d: %s —"/>
|
||||
<text name="spu_field_plan" text="Plan"/>
|
||||
<text name="spu_field_mode" text="Mode"/>
|
||||
<text name="spu_field_rake" text="Rake the hay before collecting"/>
|
||||
<text name="spu_field_rake" text="Rake the swaths before collecting (lets narrow loading wagons collect)"/>
|
||||
<text name="spu_field_marker" text=" Marker %s, %.0f m from the field"/>
|
||||
<text name="spu_field_mowWidth" text=" Mowed at %.1f m — collecting must be at least as wide"/>
|
||||
<text name="spu_field_section_store" text="— Force storage (else nearest silo with room) —"/>
|
||||
@@ -87,6 +87,72 @@
|
||||
<text name="spu_field_noRigs" text="no vehicles with mower, tedder, rake or loading wagon"/>
|
||||
<text name="spu_field_status" text="Fields in progress: %d of %d"/>
|
||||
<text name="spu_field_startRequested" text="F%d starts when a machine is free"/>
|
||||
<text name="spu_field_windrowOffset" text=" The rake drops the swath %.1f m %s — the loading wagon is offset to match"/>
|
||||
<text name="spu_field_side_center" text="in the middle"/>
|
||||
<text name="spu_field_side_left" text="to the left"/>
|
||||
<text name="spu_field_side_right" text="to the right"/>
|
||||
<text name="spu_field_fermenter" text="fermenting silo"/>
|
||||
<text name="spu_field_storeAutoSilage" text="none chosen: nearest fermenting silo with room, else nearest silo"/>
|
||||
<text name="spu_field_bare" text="bare soil"/>
|
||||
<text name="spu_field_careNeed" text="needs %s"/>
|
||||
<text name="spu_field_fertilize" text="Fertilize to max (while the crop grows)"/>
|
||||
<text name="spu_field_lime" text="Lime when the soil needs it"/>
|
||||
<text name="spu_field_mulch" text="Mulch stubble after harvest"/>
|
||||
<text name="spu_field_need_fert" text="fertilizer"/>
|
||||
<text name="spu_field_need_lime" text="lime"/>
|
||||
<text name="spu_field_need_plow" text="plowing"/>
|
||||
<text name="spu_field_need_weed" text="weeds"/>
|
||||
<text name="spu_field_plan_harvest" text="Harvest"/>
|
||||
<text name="spu_field_plan_prep" text="Preparation"/>
|
||||
<text name="spu_field_plan_sow" text="sow"/>
|
||||
<text name="spu_field_reason_cannotDo" text="no %s in the field fleet can do the job (crop/seeds/weeds)"/>
|
||||
<text name="spu_field_reason_noSowFruit" text="choose a crop to sow"/>
|
||||
<text name="spu_field_reason_notEnough" text="multi-tool: %s — only %d of %d free"/>
|
||||
<text name="spu_field_reason_notPlantable" text="%s cannot be sown in this period"/>
|
||||
<text name="spu_field_rig_crops" text="%d crops"/>
|
||||
<text name="spu_field_rig_herbicide" text="sprayer"/>
|
||||
<text name="spu_field_rig_mechanical" text="weeder (small weeds)"/>
|
||||
<text name="spu_field_rig_noSeeds" text="!! out of seeds"/>
|
||||
<text name="spu_field_rig_seeds" text="seeds ok"/>
|
||||
<text name="spu_field_rigs" text="Machines per step (Courseplay multi-tool)"/>
|
||||
<text name="spu_field_role_cultivator" text="cultivator"/>
|
||||
<text name="spu_field_role_fertilizer" text="fertilizer spreader"/>
|
||||
<text name="spu_field_role_harvester" text="harvester"/>
|
||||
<text name="spu_field_role_limer" text="lime spreader"/>
|
||||
<text name="spu_field_role_mulcher" text="mulcher"/>
|
||||
<text name="spu_field_role_plow" text="plow"/>
|
||||
<text name="spu_field_role_roller" text="roller"/>
|
||||
<text name="spu_field_role_seeder" text="seeder"/>
|
||||
<text name="spu_field_role_unloader" text="unloading trailer"/>
|
||||
<text name="spu_field_role_weeder" text="weeding machine"/>
|
||||
<text name="spu_field_roll" text="Roll after sowing"/>
|
||||
<text name="spu_field_section_after" text="— After harvest —"/>
|
||||
<text name="spu_field_section_care" text="— Care while the crop grows —"/>
|
||||
<text name="spu_field_section_prep" text="— Preparation —"/>
|
||||
<text name="spu_field_soil" text="Soil work"/>
|
||||
<text name="spu_field_soilOk" text="nothing"/>
|
||||
<text name="spu_field_soilState" text="The soil needs: %s"/>
|
||||
<text name="spu_field_soil_auto" text="Auto (plow when needed)"/>
|
||||
<text name="spu_field_soil_cultivate" text="Cultivate"/>
|
||||
<text name="spu_field_soil_none" text="None"/>
|
||||
<text name="spu_field_soil_plow" text="Plow"/>
|
||||
<text name="spu_field_sow" text="Sow afterwards"/>
|
||||
<text name="spu_field_sowFruit" text="Crop to sow"/>
|
||||
<text name="spu_field_sowSame" text="Same as harvested"/>
|
||||
<text name="spu_field_stage_careLiming" text="Liming"/>
|
||||
<text name="spu_field_stage_cultivating" text="Cultivating"/>
|
||||
<text name="spu_field_stage_fertilizing" text="Fertilizing"/>
|
||||
<text name="spu_field_stage_harvesting" text="Harvesting"/>
|
||||
<text name="spu_field_stage_liming" text="Liming"/>
|
||||
<text name="spu_field_stage_mulching" text="Mulching"/>
|
||||
<text name="spu_field_stage_plowing" text="Plowing"/>
|
||||
<text name="spu_field_stage_rolling" text="Rolling"/>
|
||||
<text name="spu_field_stage_sowing" text="Sowing"/>
|
||||
<text name="spu_field_stage_weeding" text="Weeding"/>
|
||||
<text name="spu_field_unloaders" text="Unloading trailers during harvest"/>
|
||||
<text name="spu_field_weed" text="Control weeds"/>
|
||||
<text name="spu_field_notice_done" text="is done"/>
|
||||
<text name="spu_field_reason_rain" text="waiting for dry weather"/>
|
||||
<text name="spu_button_start" text="Start run"/>
|
||||
<text name="spu_button_stop" text="Stop run"/>
|
||||
<text name="spu_button_new" text="New run"/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<modDesc descVersion="107">
|
||||
<author>masterdraco</author>
|
||||
<version>1.21.0.0</version>
|
||||
<version>1.26.0.0</version>
|
||||
<title>
|
||||
<en>AD Smart Pickup</en>
|
||||
</title>
|
||||
@@ -32,6 +32,7 @@
|
||||
<sourceFile filename="adFields.lua"/>
|
||||
<sourceFile filename="adCpBridge.lua"/>
|
||||
<sourceFile filename="adFieldStorage.lua"/>
|
||||
<sourceFile filename="adFieldWork.lua"/>
|
||||
<sourceFile filename="adFieldJobs.lua"/>
|
||||
<sourceFile filename="adSmartPickup.lua"/>
|
||||
</extraSourceFiles>
|
||||
|
||||
+66
-50
@@ -1,6 +1,6 @@
|
||||
# AD Smart Pickup — brugermanual
|
||||
|
||||
Version 1.21.0 · Farming Simulator 25 · tillæg til AutoDrive
|
||||
Version 1.26.0 · Farming Simulator 25 · tillæg til AutoDrive
|
||||
|
||||
*English version: [USER_MANUAL.md](USER_MANUAL.md)*
|
||||
|
||||
@@ -28,7 +28,7 @@ Moden kan to ting:
|
||||
7. [Traktorer og vogne](#7-traktorer-og-vogne)
|
||||
8. [Start, stop og gemning](#8-start-stop-og-gemning)
|
||||
9. [Lagerfanerne: beholdning og statistik](#9-lagerfanerne-beholdning-og-statistik)
|
||||
10. [Markarbejde: græs, kløver og lucerne](#10-markarbejde-græs-kløver-og-lucerne)
|
||||
10. [Markarbejde: alle marker](#10-markarbejde-alle-marker)
|
||||
11. [Opskrifter](#11-opskrifter)
|
||||
12. [Fejlfinding](#12-fejlfinding)
|
||||
13. [Ordliste](#13-ordliste)
|
||||
@@ -127,7 +127,7 @@ Tryk **ESC** og vælg fanen **Smart Pickup**. Fanen har syv underfaner:
|
||||
|---|---|
|
||||
| **Overblik** | Tabel over alle flows og Wait-puljerne |
|
||||
| **Flows** | Redigering af det valgte flow |
|
||||
| **Marker** | Markarbejde på græs-, kløver- og lucernemarker (kapitel 10) |
|
||||
| **Marker** | Markarbejde på alle marker: slå, høst, klargøring, såning, pleje (kapitel 10) |
|
||||
| **Fabrikker** | Beholdning og statistik for gårdens fabrikker |
|
||||
| **Siloer** | Beholdning og statistik for gårdens siloer |
|
||||
| **Stalde** | Beholdning og statistik for gårdens stalde |
|
||||
@@ -523,77 +523,93 @@ Varer, der ikke bruges på gården, har en streg under **Rækker til**. Varer ud
|
||||
|
||||
---
|
||||
|
||||
## 10. Markarbejde: græs, kløver og lucerne
|
||||
## 10. Markarbejde: alle marker
|
||||
|
||||
Fanen **Marker** sender selv maskinerne ud til gårdens slåafgrøder (græs, kløver og lucerne).
|
||||
AutoDrive kører på vejen, Courseplay arbejder på marken, og Smart Pickup bestemmer hvilken mark,
|
||||
hvilket trin og hvilken maskine. Kræver **Courseplay** ud over AutoDrive.
|
||||
Fanen **Marker** kører gårdens marker: slåafgrøder (græs, kløver, lucerne), høstafgrøder (korn, raps,
|
||||
majs, roer, kartofler …), klargøring og såning efter høst, og pleje mens afgrøden vokser. AutoDrive kører
|
||||
på vejen, Courseplay arbejder på marken, og Smart Pickup bestemmer hvilken mark, hvilket trin og hvilke
|
||||
maskiner. Kræver **Courseplay** ud over AutoDrive.
|
||||
|
||||
| Plan | Trin |
|
||||
| Markens art | Kæde |
|
||||
|---|---|
|
||||
| **Ensilage** | Slås → Samles op → silo |
|
||||
| **Hø** | Slås → Vendes → (Rives) → Samles op → silo |
|
||||
| **Slåafgrøde, Ensilage** | Slås → (Rives) → Samles op → gæringssilo/silo |
|
||||
| **Slåafgrøde, Hø** | Slås → Vendes → (Rives) → Samles op → silo |
|
||||
| **Høstafgrøde** | Høstes (+ aflæsningsvogne) → Efter høst |
|
||||
| **Efter høst / bar mark** | (Stub hakkes) → (Kalkes) → Pløjes eller Kultiveres → (Sås) → (Tromles) |
|
||||
| **Pleje mens afgrøden vokser** | Gødes · Ukrudt bekæmpes · Kalkes — når jorden trænger |
|
||||
|
||||
### Opsætning
|
||||
|
||||
1. **En markør pr. mark.** Lav en AutoDrive-markør ved markkanten med navnet `F` + marknummeret,
|
||||
fx `F109` for mark 109. Maskinerne kører dertil ad vejen, før Courseplay tager over. Markøren
|
||||
må højst ligge 400 m fra marken. Mangler den, står marken med `!` og sendes ikke ud.
|
||||
2. **Faste sæt.** Redskabet skal sidde på traktoren (eller være selvkørende). Moden kobler aldrig
|
||||
redskaber på.
|
||||
3. **Markflåde.** Nederst på fanen står alle køretøjer med slåmaskine, vender, river eller læssevogn
|
||||
med rolle og aktuel arbejdsbredde. Sæt flueben ved dem, moden må sende ud. Køretøjer i et flow,
|
||||
og køretøjer du selv sidder i, bliver aldrig brugt.
|
||||
4. **Parkering.** Sæt en AutoDrive-parkeringsplads på hvert sæt. Når et sæt er færdigt på marken,
|
||||
kører det dertil. Uden parkering bliver det stående ved marken.
|
||||
1. **En markør pr. mark.** Lav en AutoDrive-markør ved markkanten med navnet `F` + marknummeret, fx
|
||||
`F109`. Maskinerne kører dertil ad vejen, før Courseplay tager over. Højst 400 m fra marken. Mangler
|
||||
den, står marken med `!` og sendes ikke ud.
|
||||
2. **Faste sæt.** Redskabet skal sidde på traktoren (eller være selvkørende). Moden kobler aldrig på.
|
||||
3. **Markflåde.** Nederst på fanen står alle køretøjer med et markredskab, med rolle og bredde. Sæt
|
||||
flueben ved dem, moden må sende ud. Køretøjer i et flow, køretøjer AutoDrive/Courseplay allerede kører,
|
||||
og køretøjer du selv kører, bruges aldrig. En sprøjte/spreder har først en rolle, når den har fyld på
|
||||
(gødning → gødningsspreder, kalk → kalkspreder, sprøjtemiddel → ukrudtsmaskine).
|
||||
4. **Parkering.** Sæt en AutoDrive-parkering på hvert sæt (på redskabet). Færdige sæt kører dertil.
|
||||
|
||||
### Bredde
|
||||
|
||||
- Courseplay kan højst køre **50 m** bredt. Et sæt over 50 m sendes ikke ud (`!! over 50 m` i
|
||||
markflåden).
|
||||
- Moden husker, hvor bredt marken blev **slået**. Vender, river og læssevogn skal være **mindst lige
|
||||
så brede**, så hver bane dækker det, slåmaskinen lagde. Med Unrealistic Pack: sæt slåmaskinen og
|
||||
læssevognene til samme bredde, fx 40,8 m.
|
||||
- Courseplay kan højst køre **50 m** bredt. Moden bruger Courseplays egen bredde (den i HUD'en).
|
||||
- **Vender, river, jordbehandling, såning, pleje og høst** kører marken i deres egen bredde.
|
||||
- **Læssevognen** skal ramme skårene og kører et kopieret kursus:
|
||||
- **uden Riv:** slåmaskinens kursus, og vognen skal være mindst lige så bred som slåbredden.
|
||||
- **med Riv:** rivens kursus, og vognens bredde er ligegyldig. Moden måler, hvor riven lægger skåret
|
||||
(midt i banen eller ude til siden), og forskyder læssevognen tilsvarende. Målingen står under marken.
|
||||
- Har du kun smalle standardvogne, så slå **Riv** til.
|
||||
|
||||
### En mark
|
||||
|
||||
Klik på en mark i tabellen for at vælge den. Under tabellen står:
|
||||
Klik på en mark i tabellen. Under tabellen står:
|
||||
|
||||
| Valg | Betydning |
|
||||
|---|---|
|
||||
| **Plan** | Ensilage eller Hø |
|
||||
| **Kørsel** | *Manuel*: marken venter på **Start nu**. *Auto*: marken starter selv, når den er klar |
|
||||
| **Riv** | Kun ved hø: riv høet sammen før opsamling. Standard fra, fordi brede læssevogne samler spredt hø |
|
||||
| **Tving lager** | Vælg siloer. Uden valg bruges nærmeste silo med plads, der tager varen |
|
||||
| **Kørsel** | *Manuel*: marken venter på **Start nu**. *Auto*: den starter selv, når den er klar eller trænger til pleje |
|
||||
| **Maskiner pr. trin** | 1–5 sæt samtidig (Courseplays multi-tool). Et trin starter først når alle sæt er ledige |
|
||||
| **Plan** (slåafgrøde) | Ensilage eller Hø |
|
||||
| **Riv** (slåafgrøde) | Riv skårene sammen før opsamling |
|
||||
| **Aflæsningsvogne** (høst) | 1–3 vogne der følger høsteren og kører til lager |
|
||||
| **Stubbehakning** | Hak stubben efter høst |
|
||||
| **Jordbehandling** | Auto (pløj når jorden trænger, ellers kultivér), Kultivér, Pløj eller Ingen |
|
||||
| **Så bagefter** + **Afgrøde at så** | Så efter jordbehandlingen. Standard: samme afgrøde som blev høstet |
|
||||
| **Tromle** | Tromle efter såning |
|
||||
| **Gød / Kalk / Bekæmp ukrudt** | Pleje mens afgrøden vokser, når jorden trænger (højst to gange pr. pleje pr. vækst) |
|
||||
| **Tving lager** | Vælg lagre. Uden valg: ensilage → nærmeste gæringssilo, ellers silo; hø og høst → nærmeste silo |
|
||||
|
||||
En mark er **klar**, når mindst 90 % af den står i høstvinduet. Status viser andelen, fx `Venter 72 %`.
|
||||
**Start nu** virker fra 50 %. **Stop mark** stopper maskinerne på marken, og marken står klar igen.
|
||||
Under valgene står **Jorden trænger til** (kalk, pløjning, gødning, ukrudt), markøren og evt. slåbredde og
|
||||
skår-forskydning.
|
||||
|
||||
**Marksilo:** en silo højst 150 m fra marken er et mellemlager med korte ture. Moden viser
|
||||
`! Marksilo … tømmes ikke hjem`, hvis intet *Ud fra bygning*-flow har den som kilde. Lav et sådant
|
||||
flow for at få ensilagen hjem på gården.
|
||||
En mark er **klar**, når mindst 90 % står i høstvinduet. **Start nu** starter hele kæden (eller pleje, hvis
|
||||
marken vokser og trænger). På en bar mark starter den klargøring/såning. **Stop mark** stopper maskinerne.
|
||||
|
||||
### Vejr, påfyldning og beskeder
|
||||
|
||||
- **Regn:** høst, slåning, vending, rivning og hø-opsamling sendes ikke ud, mens det regner ("venter på
|
||||
tørvejr"). Maskiner, der allerede kører, fortsætter. Jordbehandling, såning og pleje kører i regn.
|
||||
- **Påfyldning:** når en såmaskine eller sprøjte/spreder bliver tom, kører AutoDrive den selv til nærmeste
|
||||
sted, der har varen, fylder op og kører tilbage, og Courseplay fortsætter.
|
||||
- **Såperiode:** en afgrøde, der ikke kan sås i den aktuelle periode, venter med grund.
|
||||
- **Beskeder:** grøn besked når en mark er færdig, rød ved fejl.
|
||||
- **Marksilo:** en silo højst 150 m fra marken er et mellemlager. `!` hvis intet *Ud fra bygning*-flow
|
||||
tømmer den hjem.
|
||||
|
||||
### Status og grunde
|
||||
|
||||
| Status | Betyder |
|
||||
|---|---|
|
||||
| **Venter / Klar** | Marken vokser / er klar (Manuel venter på Start nu) |
|
||||
| **Slås, Vendes, Rives, Samles op** | Trinnet er i gang |
|
||||
| **Færdig** | Alle trin er færdige. Marken bliver *Klar* igen, når den er vokset |
|
||||
| `venter på ledig slåmaskine` | Alle sæt med rollen er optaget |
|
||||
| `læssevogn for smal: 20 m < slåbredde 40,8 m` | Sæt læssevognens bredde op |
|
||||
| **Venter / Klar** | Vokser / klar (Manuel venter på Start nu). Med "— trænger til …" når pleje er nødvendig |
|
||||
| **Slås, Vendes, Rives, Samles op, Høstes, Kultiveres, Pløjes, Sås, Tromles, Gødes …** | Trinnet er i gang |
|
||||
| **Færdig** | Kæden er færdig. Marken bliver *Klar* igen, når den er vokset |
|
||||
| `venter på ledig …` | Alle sæt med rollen er optaget |
|
||||
| `multi-tool: … kun 2 af 3 ledige` | For få ledige sæt til "Maskiner pr. trin" |
|
||||
| `ingen … kan tage opgaven` | Fx høster med forkert skærebord eller såmaskine der ikke kan så afgrøden |
|
||||
| `venter på tørvejr` | Regn |
|
||||
| `Courseplay kunne ikke lave kursus` | Nyt forsøg efter 5 minutter |
|
||||
|
||||
Loglinjerne i `log.txt` begynder med `ADSmartPickup: F109 …`.
|
||||
|
||||
### Første test
|
||||
|
||||
1. Sæt én græsmark til **Manuel** og **Ensilage**, med `F`-markør og én slåmaskine og én
|
||||
læssevogn med flueben.
|
||||
2. Tryk **Start nu**. Slåmaskinen kører til markøren, Courseplay slår marken, og sættet kører hjem.
|
||||
3. Læssevognen kører ud. Når den er fuld, tømmer AutoDrive den i siloen og kører tilbage, og
|
||||
Courseplay fortsætter. Marken står **Færdig**.
|
||||
4. Prøv derefter **Hø** og til sidst **Auto**.
|
||||
Loglinjerne i `log.txt` begynder med `ADSmartPickup: F109 …`. En samlet testliste står i
|
||||
`docs/TESTPLAN-markarbejde.md`.
|
||||
|
||||
## 11. Opskrifter
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Testplan — Markarbejde (fanen Marker), version 1.26.0.0
|
||||
|
||||
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
|
||||
begynder altid med `ADSmartPickup: F<marknr>`.
|
||||
|
||||
Kryds af undervejs. Skriv resultatet (OK / FEJL + hvad der skete) ud for hver test, så retter vi i
|
||||
samme rækkefølge.
|
||||
|
||||
---
|
||||
|
||||
## 0. Forberedelse
|
||||
|
||||
- [ ] **0.1 Version.** Log: `Available mod: … (Version: 1.26.0.0) FS25_ADSmartPickup`.
|
||||
- [ ] **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).
|
||||
Afgrøde-kolonnen viser afgrøden eller "bar jord".
|
||||
- [ ] **0.3 F-markører.** Marker uden `F<nr>`-markør står med `!` foran marknummeret og "! mangler
|
||||
markør F…". Lav markører til de marker, der skal testes.
|
||||
- [ ] **0.4 Markflåden.** Nederst: alle maskiner med slåmaskine, vender, river, læssevogn, høster,
|
||||
aflæsningsvogn, kultivator, plov, såmaskine, tromle, strigle/sprøjte, gødnings-/kalkspreder,
|
||||
stubhakker — med rolle og bredde. Tjek:
|
||||
- [ ] Høstere viser "N afgrøder".
|
||||
- [ ] Såmaskiner viser "frø ok" eller "!! tom for frø".
|
||||
- [ ] Strigle viser "strigle (småt ukrudt)", ukrudtssprøjte viser "sprøjte".
|
||||
- [ ] En **tom** sprøjte/spreder vises ikke (den har ingen rolle før den har fyld på).
|
||||
- [ ] Maskiner i et Smart Pickup-flow kan godt ses, men sendes aldrig ud.
|
||||
- [ ] **0.5 Bredder.** BiG M 450 viser **40,8 m** (Courseplays egen bredde), læssevognene 40,8 m.
|
||||
|
||||
## 1. Etape 1 genbekræftet (græs → ensilage)
|
||||
|
||||
Rest fra 25/9: slåning og opsamlingsstart virkede; afslutningen blev aldrig set.
|
||||
|
||||
- [ ] **1.1 Afslutning af opsamling.** Mark 22 (eller en anden lille græsmark) på Manuel + Ensilage,
|
||||
BiG M + én Boss Alpin med flueben → **Start nu**.
|
||||
- Forvent: slåmaskine → F22 → Courseplay slår → hjem. Læssevogn → F22 → Courseplay samler op med
|
||||
**slåmaskinens kursus** → AutoDrive tømmer ved fuld → tilbage → færdig → rest til lager → parkering.
|
||||
Marken står **Færdig**, og der kommer en grøn besked "F22 er færdig".
|
||||
- Log: `mower … sendt til F22`, `Courseplay startet (mower)`, `mowing -> collecting`,
|
||||
`læssevogn … aflæsser i <gæringssilo>`, `Courseplay startet (collector, mower-kursus kopieret)`,
|
||||
`collector … færdig`, `collecting -> done`.
|
||||
- [ ] **1.2 Gæringssilo.** Ensilage-læs går til nærmeste **gæringssilo** (FermentingSilo/GrassSilageSilo),
|
||||
ikke en Farma-silo. Log: `aflæsser i <gæringssilo>` eller `ingen gæringssilo kan tage græsset (…)`.
|
||||
- [ ] **1.3 Sidde i maskinen.** Sæt dig i slåmaskinen mens Courseplay kører, og stig ud igen.
|
||||
Forvent: den slippes IKKE (ingen `frigivet`-linje). Kør selv > 3 km/t uden CP → `frigivet — spilleren kører det`.
|
||||
- [ ] **1.4 Sovende køretøj.** Parkerede maskiner kører af sted med det samme ved udsendelse, uden at
|
||||
du skal sætte dig ind.
|
||||
|
||||
## 2. Hø og river
|
||||
|
||||
- [ ] **2.1 Hø uden river.** Plan = **Hø**, Riv = Nej. Slå → **Vendes** (venderen i sin egen bredde) →
|
||||
Samles op (læssevogn på slåkurset, ≥ 40,8 m) → hø-silo (kun almindelig silo, aldrig gæringssilo).
|
||||
Log: `mowing -> tedding`, `Courseplay startet (tedder)`, `tedding -> collecting`.
|
||||
- [ ] **2.2 Smal læssevogn uden river.** En læssevogn under 40,8 m → status "læssevogn for smal:
|
||||
… — sæt Riv til eller brug en bredere".
|
||||
- [ ] **2.3 Riv (midterrive).** Riv = Ja. Log: `river lægger skåret 0.x m fra sin kursuslinje`,
|
||||
derefter `Courseplay startet (collector, rake-kursus kopieret)`. Læssevognen rammer skårene.
|
||||
- [ ] **2.4 Sideriver.** Med en river der lægger skåret til siden: log viser fx `3.2 m`, og
|
||||
`skår ligger 3.2 m til … -> læssevognens forskydning …`. **Tjek at vognen kører OVER skåret.**
|
||||
Kører den ved siden af (forkert side), så skriv det ned — så vender jeg fortegnet.
|
||||
- [ ] **2.5 Riv til ensilage.** Plan = Ensilage + Riv = Ja: Slå → Rives → Samles op.
|
||||
|
||||
## 3. Høst (korn, raps, majs …)
|
||||
|
||||
- [ ] **3.1 Høst.** En moden kornmark, Manuel, høster med passende skærebord + én aflæsningsvogn
|
||||
(traktor + tipvogn/overlæssevogn) med flueben → **Start nu**.
|
||||
- Forvent: høster → F-markør → Courseplay høster. Aflæsningsvognen sendes ud når høsteren
|
||||
arbejder, følger den (Courseplays combine-unloader) og kører fulde læs til siloen.
|
||||
- Log: `harvester … sendt`, `Courseplay startet (harvester)`, `unloader … sendt`,
|
||||
`aflæsningsvogn … følger høsteren -> <silo>`.
|
||||
- [ ] **3.2 Afslutning.** Høsten færdig → `harvesting -> <næste trin eller done>`,
|
||||
`aflæsningsvogn … stoppes — høsten er færdig`, evt. `kører rest-læs … til lager`, parkering.
|
||||
- [ ] **3.3 Forkert skærebord.** Høster der ikke kan tage afgrøden → status "ingen høster i
|
||||
markflåden kan tage opgaven (afgrøde/frø/ukrudt)". Ingen udsendelse.
|
||||
- [ ] **3.4 Flere aflæsningsvogne.** "Aflæsningsvogne under høst" = 2 → to vogne sendes (én ad gangen).
|
||||
- [ ] **3.5 Regn.** Når det regner: høst/slå/vend/riv sendes ikke ud, status "venter på tørvejr".
|
||||
Kørende maskiner fortsætter.
|
||||
|
||||
## 4. Roer
|
||||
|
||||
- [ ] **4.1 Roehøst.** Roemark (sukkerroer), roeoptager (Ropa) + aflæsningsvogn → som 3.1, men med
|
||||
roer. Log viser `harvester 'Ropa …'`. Roerne går til nærmeste silo/lager der tager SUGARBEET
|
||||
(brug **Tving lager** hvis de skal et bestemt sted hen, fx roeknuseren).
|
||||
|
||||
## 5. Efter høst: klargøring og såning
|
||||
|
||||
Sæt på en kornmark før høst: Efter høst → Stubbehakning Nej, Jordbehandling **Auto**, **Så bagefter** Ja,
|
||||
Afgrøde "Samme som høstet", Tromle Ja. Kalk Ja.
|
||||
|
||||
- [ ] **5.1 Kæde.** Efter høsten fortsætter marken selv: (kalk hvis jorden trænger) → pløj eller
|
||||
kultivér → så → tromle → **Færdig**. Log: `harvesting -> cultivating`, `cultivating -> sowing` …
|
||||
- [ ] **5.2 Auto-jordbehandling.** Står "Jorden trænger til: pløjning" under marken → plov sendes.
|
||||
Ellers kultivator.
|
||||
- [ ] **5.3 Såning.** Såmaskinen sættes til afgrøden (`setSeedIndex`), og den rigtige afgrøde spirer.
|
||||
- [ ] **5.4 Ude af såperioden.** Afgrøde der ikke kan sås nu → status "<afgrøde> kan ikke sås i denne
|
||||
periode", ingen udsendelse.
|
||||
- [ ] **5.5 Tom for frø / påfyldning.** Såmaskine med lidt frø. Moden leder efter en læssestation med frø
|
||||
på lager og en AutoDrive-markør ved triggeren (fx en silo/big-bag-station med SEEDS).
|
||||
- Findes den: log `… fyldes op med SEEDS ved <markør> af AutoDrive når den er tom`. Når den er tom
|
||||
midt på marken, kører AutoDrive derhen, fylder op, kører tilbage, og Courseplay fortsætter.
|
||||
- Findes den ikke: log `ingen påfyldning af SEEDS med AutoDrive-markør fundet …`. Når den er tom:
|
||||
rød besked `'…' er tom for SEEDS — fyld op, så fortsætter marken`, og marken går IKKE videre.
|
||||
- [ ] **5.6 Bar mark alene.** Vælg en bar mark (stub/pløjet) → **Start nu** → kun klargøring/såning.
|
||||
|
||||
## 6. Pleje mens afgrøden vokser
|
||||
|
||||
- [ ] **6.1 Behov vises.** En voksende mark viser "Jorden trænger til: gødning, ukrudt …" og status
|
||||
"— trænger til Gødes". Manuel: intet sker før **Start nu**.
|
||||
- [ ] **6.2 Gødning.** Start nu (eller Auto) → gødningsspreder/-sprøjte med fyld sendes → `pleje:
|
||||
fertilizing` → `fertilizing -> idle`. Højst 2 gange pr. vækstcyklus.
|
||||
- [ ] **6.3 Ukrudt.** Høstafgrøde med tæt ukrudt → strigle (småt ukrudt) eller ukrudtssprøjte
|
||||
(stort ukrudt, > 4). Græs ukrudtsbehandles ikke.
|
||||
- [ ] **6.4 Kalk.** Jord der trænger til kalk → kalkspreder.
|
||||
- [ ] **6.5 Påfyldning.** Sprøjte/spreder der løber tør → AutoDrive fylder op og kører tilbage.
|
||||
|
||||
## 7. Multi-maskine (Courseplay multi-tool)
|
||||
|
||||
- [ ] **7.1 To slåmaskiner.** "Maskiner pr. trin" = 2 på en græsmark med to BiG M med flueben.
|
||||
Forvent: begge sendes samtidig, den ene laver kurset (`bane -1`), den anden kopierer det (`bane 1`)
|
||||
og de kører i konvoj. Log: `… sendt … bane -1`, `… bane 1`, `Courseplay startet (mower, bane -1)`,
|
||||
`Courseplay startet (mower, multi-kursus kopieret, bane 1)`.
|
||||
- [ ] **7.2 For få maskiner.** 3 pr. trin men kun 2 ledige → status "multi-tool: slåmaskine — kun 2 af 3 ledige".
|
||||
- [ ] **7.3 Læssevogne på multi-kursus.** Efter 7.1 sendes 2 læssevogne, én pr. bane.
|
||||
|
||||
## 8. Robusthed
|
||||
|
||||
- [ ] **8.1 Genindlæsning midt i et trin.** Gem og genstart under fx opsamling → marken fortsætter på
|
||||
samme trin, sæt sendes ud igen, slåkurset hentes fra slåmaskinen.
|
||||
- [ ] **8.2 Stop mark.** **Stop mark** midt i høst → alle maskiner på marken stopper; høst står **Klar**
|
||||
igen, pleje/klargøring står **Venter**.
|
||||
- [ ] **8.3 Ingen vej.** Mark hvis F-markør ikke er forbundet → status "… nåede ikke F…" / "intet lager:
|
||||
… AutoDrive finder ingen rute …" og nyt forsøg efter 5 min.
|
||||
- [ ] **8.4 Beskeder.** Grøn besked når en mark/pleje er færdig; rød besked ved fejl (hver kun én gang i træk).
|
||||
- [ ] **8.5 Ydelse.** Ingen mærkbar lag med fanen åben og mange marker (én mark scannes pr. sekund).
|
||||
|
||||
---
|
||||
|
||||
## Kendte usikkerheder (første gang i spillet)
|
||||
|
||||
| Område | Hvad der er gættet | Tegn på fejl |
|
||||
|---|---|---|
|
||||
| Vækstfase | `getFieldDataAtWorldPosition` anden returværdi = vækststadie | Høstet stub vises som voksende, eller moden mark som bar |
|
||||
| Høsterens afgrøder | `spec_cutter.fruitTypes`/`fruitTypeIndices` | Høster vises med "0 afgrøder" |
|
||||
| Combine-unloader | stationsvalg ud fra silovalget | `ingen aflæsningsstation Courseplay kan bruge` |
|
||||
| Multi-tool | `multiTools` + `laneOffset` + `Course:setPosition` | Begge maskiner kører samme bane |
|
||||
| Sideriver | fortegn på forskydningen | Læssevognen kører ved siden af skåret |
|
||||
| Påfyldning | AutoDrive LOAD til den læssestation moden fandt (AutoDrives eget automatiske mål er slået fra i AutoDrive) | Sættet står ved påfyldningen og læsser ikke, eller kører ikke tilbage |
|
||||
| Ukrudt | `weedSystem`-værdi ≥ 2 = tæt | Ukrudtsbehandling uden ukrudt, eller aldrig |
|
||||
+66
-51
@@ -1,6 +1,6 @@
|
||||
# AD Smart Pickup — user manual
|
||||
|
||||
Version 1.21.0 · Farming Simulator 25 · add-on for AutoDrive
|
||||
Version 1.26.0 · Farming Simulator 25 · add-on for AutoDrive
|
||||
|
||||
*Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)*
|
||||
|
||||
@@ -28,7 +28,7 @@ The mod does two things:
|
||||
7. [Tractors and trailers](#7-tractors-and-trailers)
|
||||
8. [Start, stop and saving](#8-start-stop-and-saving)
|
||||
9. [The storage tabs: inventory and statistics](#9-the-storage-tabs-inventory-and-statistics)
|
||||
10. [Field work: grass, clover and alfalfa](#10-field-work-grass-clover-and-alfalfa)
|
||||
10. [Field work: all fields](#10-field-work-all-fields)
|
||||
11. [Recipes](#11-recipes)
|
||||
12. [Troubleshooting](#12-troubleshooting)
|
||||
13. [Glossary](#13-glossary)
|
||||
@@ -129,7 +129,7 @@ Press **ESC** and open the **Smart Pickup** tab. It has seven sub-tabs:
|
||||
|---|---|
|
||||
| **Overview** | A table of all flows and the Wait pools |
|
||||
| **Flows** | Editing of the selected flow |
|
||||
| **Fields** | Field work on grass, clover and alfalfa fields (chapter 10) |
|
||||
| **Fields** | Field work on all fields: mowing, harvest, preparation, sowing, care (chapter 10) |
|
||||
| **Factories** | Inventory and statistics for the farm's factories |
|
||||
| **Silos** | Inventory and statistics for the farm's silos |
|
||||
| **Barns** | Inventory and statistics for the farm's barns |
|
||||
@@ -529,79 +529,94 @@ selling point*.
|
||||
|
||||
---
|
||||
|
||||
## 10. Field work: grass, clover and alfalfa
|
||||
## 10. Field work: all fields
|
||||
|
||||
The **Fields** tab sends machines out to the farm's mowable crops (grass, clover and alfalfa) by
|
||||
itself. AutoDrive drives on the road, Courseplay works the field, and Smart Pickup decides which field,
|
||||
which step and which machine. Requires **Courseplay** in addition to AutoDrive.
|
||||
The **Fields** tab runs the farm's fields: mowable crops (grass, clover, alfalfa), harvest crops (grain,
|
||||
canola, maize, beets, potatoes …), preparation and sowing after harvest, and care while the crop grows.
|
||||
AutoDrive drives on the road, Courseplay works the field, and Smart Pickup decides which field, which step
|
||||
and which machines. Requires **Courseplay** in addition to AutoDrive.
|
||||
|
||||
| Plan | Steps |
|
||||
| Field type | Chain |
|
||||
|---|---|
|
||||
| **Silage** | Mow → Collect → silo |
|
||||
| **Hay** | Mow → Ted → (Rake) → Collect → silo |
|
||||
| **Mowable, Silage** | Mow → (Rake) → Collect → fermenting silo/silo |
|
||||
| **Mowable, Hay** | Mow → Ted → (Rake) → Collect → silo |
|
||||
| **Harvest crop** | Harvest (+ unloading trailers) → After harvest |
|
||||
| **After harvest / bare field** | (Mulch) → (Lime) → Plow or Cultivate → (Sow) → (Roll) |
|
||||
| **Care while the crop grows** | Fertilize · Control weeds · Lime — when the soil needs it |
|
||||
|
||||
### Setup
|
||||
|
||||
1. **One marker per field.** Create an AutoDrive marker at the field edge named `F` + the field number,
|
||||
such as `F109` for field 109. Machines drive there on the road before Courseplay takes over. The
|
||||
marker must be within 400 m of the field. Without it, the field shows `!` and nothing is sent.
|
||||
2. **Fixed rigs.** The implement must be attached to the tractor (or be self-propelled). The mod never
|
||||
attaches implements.
|
||||
3. **Field fleet.** At the bottom of the tab are all vehicles with a mower, tedder, rake or loading
|
||||
wagon, with role and current working width. Tick the ones the mod may send out. Vehicles in a flow,
|
||||
and vehicles you are driving, are never used.
|
||||
4. **Parking.** Set an AutoDrive parking spot on each rig. When a rig is done on the field, it drives
|
||||
there. Without parking it stays at the field.
|
||||
such as `F109`. Machines drive there on the road before Courseplay takes over. At most 400 m from the
|
||||
field. Without it, the field shows `!` and nothing is sent.
|
||||
2. **Fixed rigs.** The implement must be attached (or self-propelled). The mod never attaches.
|
||||
3. **Field fleet.** At the bottom are all vehicles with a field implement, with role and width. Tick the
|
||||
ones the mod may send out. Vehicles in a flow, vehicles AutoDrive/Courseplay are already driving, and
|
||||
vehicles you drive are never used. A sprayer/spreader only has a role once it holds product
|
||||
(fertilizer → fertilizer spreader, lime → lime spreader, herbicide → weeding machine).
|
||||
4. **Parking.** Set an AutoDrive parking spot on each rig (on the implement). Finished rigs drive there.
|
||||
|
||||
### Width
|
||||
|
||||
- Courseplay can work at most **50 m** wide. A rig over 50 m is not sent out (`!! over 50 m` in the
|
||||
fleet).
|
||||
- The mod remembers how wide the field was **mowed**. Tedder, rake and loading wagon must be **at least
|
||||
as wide**, so every lane covers what the mower laid down. With Unrealistic Pack: set the mower and the
|
||||
loading wagons to the same width, such as 40.8 m.
|
||||
- Courseplay works at most **50 m** wide. The mod uses Courseplay's own width (the one in its HUD).
|
||||
- **Tedder, rake, soil work, sowing, care and harvest** work the field at their own width.
|
||||
- **The loading wagon** must hit the swaths and drives a copied course:
|
||||
- **without Rake:** the mower's course, and the wagon must be at least as wide as the mowing width.
|
||||
- **with Rake:** the rake's course, and the wagon's width does not matter. The mod measures where the
|
||||
rake drops the swath (centre or to the side) and offsets the wagon to match. Shown under the field.
|
||||
- If you only have narrow standard wagons, turn **Rake** on.
|
||||
|
||||
### A field
|
||||
|
||||
Click a field in the table to select it. Below the table:
|
||||
Click a field in the table. Below the table:
|
||||
|
||||
| Option | Meaning |
|
||||
|---|---|
|
||||
| **Plan** | Silage or Hay |
|
||||
| **Mode** | *Manual*: the field waits for **Start now**. *Auto*: the field starts by itself when ready |
|
||||
| **Rake** | Hay only: rake before collecting. Off by default, because wide loading wagons collect spread hay |
|
||||
| **Force storage** | Pick silos. Without a choice the nearest silo with room that takes the goods is used |
|
||||
| **Mode** | *Manual*: the field waits for **Start now**. *Auto*: it starts by itself when ready or when it needs care |
|
||||
| **Machines per step** | 1–5 rigs at once (Courseplay multi-tool). A step starts only when all rigs are free |
|
||||
| **Plan** (mowable) | Silage or Hay |
|
||||
| **Rake** (mowable) | Rake the swaths before collecting |
|
||||
| **Unloading trailers** (harvest) | 1–3 trailers that follow the harvester and drive to storage |
|
||||
| **Mulch stubble** | Mulch the stubble after harvest |
|
||||
| **Soil work** | Auto (plow when the soil needs it, else cultivate), Cultivate, Plow or None |
|
||||
| **Sow afterwards** + **Crop to sow** | Sow after soil work. Default: the crop that was harvested |
|
||||
| **Roll** | Roll after sowing |
|
||||
| **Fertilize / Lime / Control weeds** | Care while the crop grows, when the soil needs it (at most twice per care per season) |
|
||||
| **Force storage** | Pick stores. Without a choice: silage → nearest fermenting silo, else silo; hay and harvest → nearest silo |
|
||||
|
||||
A field is **ready** when at least 90 % of it is in the harvest window. The status shows the share,
|
||||
such as `Waiting 72 %`. **Start now** works from 50 %. **Stop field** stops the machines on the field,
|
||||
and the field is ready again.
|
||||
Below the options: **The soil needs** (lime, plowing, fertilizer, weeds), the marker, and mowing width and
|
||||
swath offset when relevant.
|
||||
|
||||
**Field silo:** a silo within 150 m of the field is an intermediate store with short trips. The mod
|
||||
shows `! Field silo … is not emptied home` if no *Out of building* flow has it as a source. Create such
|
||||
a flow to bring the silage home.
|
||||
A field is **ready** when at least 90 % is in the harvest window. **Start now** starts the whole chain (or
|
||||
care, when the field grows and needs it). On a bare field it starts preparation/sowing. **Stop field** stops
|
||||
the machines.
|
||||
|
||||
### Weather, refilling and notifications
|
||||
|
||||
- **Rain:** harvesting, mowing, tedding, raking and hay collecting are not sent out while it rains
|
||||
("waiting for dry weather"). Machines already running continue. Soil work, sowing and care run in rain.
|
||||
- **Refilling:** when a seeder or sprayer/spreader runs empty, AutoDrive drives it to the nearest place
|
||||
with the product, refills, drives back, and Courseplay continues.
|
||||
- **Sowing period:** a crop that cannot be sown in the current period waits with a reason.
|
||||
- **Notifications:** green when a field is done, red on errors.
|
||||
- **Field silo:** a silo within 150 m of the field is an intermediate store. `!` if no *Out of building*
|
||||
flow empties it home.
|
||||
|
||||
### Status and reasons
|
||||
|
||||
| Status | Meaning |
|
||||
|---|---|
|
||||
| **Waiting / Ready** | The field is growing / ready (Manual waits for Start now) |
|
||||
| **Mowing, Tedding, Raking, Collecting** | The step is running |
|
||||
| **Done** | All steps are done. The field becomes *Ready* again once it has grown |
|
||||
| `waiting for a free mower` | All rigs with the role are busy |
|
||||
| `loading wagon too narrow: 20 m < mowing width 40.8 m` | Increase the loading wagon's width |
|
||||
| **Waiting / Ready** | Growing / ready (Manual waits for Start now). With "— needs …" when care is due |
|
||||
| **Mowing, Tedding, Raking, Collecting, Harvesting, Cultivating, Plowing, Sowing, Rolling, Fertilizing …** | The step is running |
|
||||
| **Done** | The chain is done. The field becomes *Ready* again once it has grown |
|
||||
| `waiting for a free …` | All rigs with the role are busy |
|
||||
| `multi-tool: … only 2 of 3 free` | Too few free rigs for "Machines per step" |
|
||||
| `no … can do the job` | Such as a harvester with the wrong header, or a seeder that cannot sow the crop |
|
||||
| `waiting for dry weather` | Rain |
|
||||
| `Courseplay could not create a course` | Retried after 5 minutes |
|
||||
|
||||
Log lines in `log.txt` start with `ADSmartPickup: F109 …`.
|
||||
|
||||
### First test
|
||||
|
||||
1. Set one grass field to **Manual** and **Silage**, with an `F` marker and one mower and one loading
|
||||
wagon ticked.
|
||||
2. Press **Start now**. The mower drives to the marker, Courseplay mows the field, and the rig drives
|
||||
home.
|
||||
3. The loading wagon goes out. When it is full, AutoDrive empties it in the silo and drives back, and
|
||||
Courseplay continues. The field shows **Done**.
|
||||
4. Then try **Hay**, and finally **Auto**.
|
||||
Log lines in `log.txt` start with `ADSmartPickup: F109 …`. A full test list is in
|
||||
`docs/TESTPLAN-markarbejde.md`.
|
||||
|
||||
## 11. Recipes
|
||||
|
||||
|
||||
@@ -38,10 +38,24 @@ Ikke med i etape 1: mejetærskerafgrøder (etape 2), roer (etape 3), jordbehandl
|
||||
læssevognene i Unrealistic Pack kan køre 40 m bredt og samler spredt hø op.
|
||||
6. **Bredde.** Maskiner kører med deres *aktuelle* bredde (Unrealistic Packs `variableWorkWidth`,
|
||||
`aiKeepCurrentWidth`). Courseplay kan højst 50 m → et sæt over 50 m sendes ikke ud (advarsel).
|
||||
Marken husker **slåbredden**; opsamling/vending/rivning kræver bredde ≥ slåbredden, så hver bane
|
||||
dækker det, slåmaskinen lagde. Så behøver banerne ikke flugte med skårene.
|
||||
7. **Silovalg** genbruger Smart Pickups motor (`ADLoadSwap`): nærmeste silo med plads, der tager varen,
|
||||
med vej fra `F`-markøren. *Tving lager* pr. mark (som "Lager til" i flows). Fabrikker er ikke mål.
|
||||
Marken husker **slåbredden**; opsamling/vending/rivning kræver bredde ≥ slåbredden.
|
||||
**Revideret efter in-game test 25/9:** et nyt Courseplay-kursus til læssevognen lå forskudt i forhold til
|
||||
skårene (anden start/retning/forager). Derfor genbruger vender, river og læssevogn **slåmaskinens
|
||||
kursus** (`vehicle:cpCopyCourse`). Kurset gemmes pr. mark; efter genindlæsning hentes det fra
|
||||
slåmaskinen (id gemt som `mowVehicle`), som Courseplay selv gemmer kurset på. Uden kendt slåkursus
|
||||
laves et nyt som reserve (logges).
|
||||
**Revideret igen 25/9 (smalle standardmaskiner):** vender og river kører hele marken i egen bredde med
|
||||
eget kursus (intet breddekrav). Læssevognen følger skårene: uden riv slåmaskinens kursus og bredde ≥
|
||||
slåbredde; med riv rivens kursus (skåret ligger midt i banen) og intet breddekrav. Riv kan vælges til
|
||||
både hø og ensilage.
|
||||
**Sideriver (25/9):** mens riven arbejder, måles nedlægningsområdets midte (runtime-felt
|
||||
`dropWindrowWorkAreaIndex`, ellers riverens XML) i traktorens ramme minus rivens CP-forskydning =
|
||||
skårets sideforskydning (gemt pr. mark). Læssevognen på rivens kursus får `toolOffsetX = egen −
|
||||
skårets` (±10 m) og får sin egen værdi tilbage bagefter.
|
||||
7. **Silovalg** genbruger Smart Pickups motor (`ADLoadSwap`). *Tving lager* pr. mark (som "Lager til" i
|
||||
flows). **Revideret 25/9:** ensilage går først til egne **gæringssiloer** (produktion skår -> SILAGE,
|
||||
fx FermentingSiloPack/GrassSilageSilo), nærmest marken, ellers nærmeste silo med plads; hø kun til
|
||||
almindelige siloer (gæringssiloer laver også hø om til ensilage).
|
||||
8. **Marksilo.** En silo, hvis afstand til markens polygon er ≤ 150 m, er et mellemlager. Den må gerne
|
||||
vælges (korte ture). Oversigten viser "!" ved marken, hvis intet Smart Pickup-udkørselsflow har
|
||||
siloen som kilde — hjemkørslen klares af de eksisterende flows, ikke af markdelen.
|
||||
@@ -128,3 +142,10 @@ og parkerer.
|
||||
overgange, bredde-regler, forslag, sortering/filtre, gem/indlæs.
|
||||
- In-game testprotokol (manualen + denne spec): én mark Manuel → Start nu → slå → saml op → silo, derefter
|
||||
Hø, derefter Auto. Loglinjer: `ADSmartPickup: F109 …`.
|
||||
|
||||
## Senere udvidelser (noter)
|
||||
|
||||
- **Flere maskiner om samme mark, maks 5** (Courseplays multi-tool-grænse, 2026-09-25): slå/vende/rive med
|
||||
op til 5 sæt på én mark samtidig via Courseplays konvoj (multi-tool), og op til 5 læssevogne. Kræver at
|
||||
`ADFieldModel.maxRigs` bliver en indstilling pr. mark (1–5) og at sættene får samme CP-kursus med
|
||||
hver sin position i konvojen.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Markarbejde, etape 2–3 + multi-maskine: høst, roer, jordbehandling, såning
|
||||
|
||||
Dato: 2026-09-26 (nat) · Bygger på `2026-09-25-markarbejde-etape1-design.md` · Branch `feature/markarbejde-2`
|
||||
|
||||
Brugeren gav fuldt mandat ("byg resten, ALLE PUNKTER, du træffer selv gode valg") og tester i spillet dagen
|
||||
efter. Beslutningerne herunder er truffet uden dialog og skal læses igennem.
|
||||
|
||||
## Mål
|
||||
|
||||
Alle gårdens marker kan køres af Smart Pickup: slåafgrøder (etape 1) og nu **høstafgrøder** (korn, raps,
|
||||
majs, roer, kartofler … alt der høstes med mejetærsker/optager), inkl. **aflæsningsvogne**, **jordbehandling**
|
||||
efter høst og **såning**. Hvert trin kan køres med **1–5 maskiner** på samme mark (Courseplays multi-tool).
|
||||
|
||||
## Beslutninger
|
||||
|
||||
1. **Høst er én pipeline for alle høstafgrøder.** Roer og korn adskiller sig kun ved hvilken høster der
|
||||
kan tage afgrøden. Høsteren matches på afgrøden: skærebordets/optagerens frugttyper (`spec_cutter`)
|
||||
skal omfatte markens afgrøde. Finsnittere (cutter med `fruitTypeConverters`) bruges ikke til høst.
|
||||
2. **Aflæsning under høst = Courseplays combine-unloader** (`COMBINE_UNLOADER_CP`, bevist i AI-Bonden):
|
||||
vognen følger høsteren, tømmer den og kører til en **station**. Stationen vælges af Smart Pickups
|
||||
silovalg (nærmeste silo med plads der tager afgrøden, eller *Tving lager*); findes den ikke i
|
||||
Courseplays stationsliste, bruges nærmeste station. Antal vogne pr. mark: 1–3 (plan).
|
||||
3. **Høsteren sendes hjem** (AD-parkering) når Courseplay er færdig og tanken er tømt. Aflæsningsvogne
|
||||
stoppes når høsten er færdig; har de rest, kører AutoDrive den til lageret (samme reserve som
|
||||
læssevognen), ellers parkering.
|
||||
4. **Efter høst** (valg pr. mark): *Ingenting*, *Kultivér* eller *Pløj*, og derefter evt. **Så**.
|
||||
Såmaskinen sættes til planens afgrøde (standard: samme afgrøde som blev høstet). Kan afgrøden ikke
|
||||
sås i den nuværende periode, venter marken med grund. Tom for frø -> sættet springes over med grund
|
||||
(moden fylder ikke frø på; det gør brugeren/AutoDrive-refill).
|
||||
5. **Multi-maskine (1–5 pr. trin, plan pr. mark).** Trin med eget kursus (slå, vend, riv, høst,
|
||||
kultivér, pløj, så) laver ét Courseplay-kursus med `multiTools = n` på første sæt; de øvrige kopierer det,
|
||||
og hver får sin bane (`laneOffset`: 2 -> −1,1 · 3 -> −1,0,1 · 4 -> −2,−1,1,2 · 5 -> −2…2). Et multi-trin
|
||||
startes kun når **alle n** sæt er ledige (ellers kører én bane ikke). Læssevognen på et multi-kursus
|
||||
kræver også n vogne, én pr. bane.
|
||||
6. **Rolle-rækkefølge** når et køretøj har flere redskaber: høster > læssevogn > river > vender >
|
||||
slåmaskine > såmaskine > kultivator > plov > aflæsningsvogn (traktor+vogn som Courseplay kan bruge som
|
||||
combine-unloader).
|
||||
7. **Afgrødetype pr. mark:** slåafgrøde (etape 1) eller høstafgrøde (alt andet med en høstbar frugt). Plan
|
||||
*Høst* er standard for høstafgrøder. Marker uden afgrøde (pløjet/stub) kan få *Start nu* for
|
||||
jordbehandling/såning alene.
|
||||
8. **Stadig ikke med:** gødning/kalk/sprøjtning, baller, halm efter mejetærsker, auto-påfyldning af frø.
|
||||
|
||||
## Opbygning (ændringer)
|
||||
|
||||
| Fil | Ændring |
|
||||
|---|---|
|
||||
| `adFieldModel.lua` | Planer `harvest`, jordbehandling, såning, `rigs`/`unloaders`; trin `harvesting/cultivating/plowing/sowing`; hjælperoller pr. trin; `lanePositions(n)` |
|
||||
| `adFieldFleet.lua` | Nye roller; `pick` tager afgrøde (høster skal kunne tage den); `pickMany` til multi-trin |
|
||||
| `adCpBridge.lua` | `startUnloader`, `canHarvest`, frø-valg (`setSeedFruit`, `isPlantable`, `hasSeeds`), multi-tool kursus + lane |
|
||||
| `adFields.lua` | Afgrødetype (slå/høst), sidst høstede afgrøde |
|
||||
| `adFieldWork.lua` (ny) | Udsendelse og opfølgning af sæt (AD/CP) — flyttet ud af `adFieldJobs.lua` der blev for stor |
|
||||
| `gui/SmartPickupFieldTab.lua` | Plan-rækker pr. afgrødetype, maskiner pr. trin, aflæsningsvogne, efter høst, så + afgrøde |
|
||||
@@ -0,0 +1,90 @@
|
||||
-- Kør: luajit tests/test_adCpBridge.lua (fra repo-roden)
|
||||
-- Kun getWorkWidth: variabel bredde (Unrealistic Pack) vinder over AI-markørernes grundbredde.
|
||||
dofile("FS25_ADSmartPickup/adCpBridge.lua")
|
||||
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
|
||||
|
||||
local function withMarkers(width)
|
||||
return {getAIMarkers = function() return 1, 2, 3, 4, width end}
|
||||
end
|
||||
|
||||
-- W1: læssevogn med variabel bredde 20,4 + 20,4 = 40,8 m, AI-markører siger 2,4 m
|
||||
local wagon = withMarkers(2.4)
|
||||
wagon.getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end
|
||||
local tractor = {getAttachedImplements = function() return {{object = wagon}} end}
|
||||
check("W1 variabel bredde", ADCpBridge.getWorkWidth(tractor), 40.8)
|
||||
-- W2: ugyldig variabel bredde -> AI-markører
|
||||
local plain = withMarkers(9.5)
|
||||
plain.getVariableWorkWidth = function() return 0, nil, false end
|
||||
check("W2 AI-markører", ADCpBridge.getWorkWidth(plain), 9.5)
|
||||
-- W3: selvkørende slåmaskine uden redskaber, kun variabel bredde
|
||||
local mower = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -20.4, nil, true end}
|
||||
check("W3 selvkørende", ADCpBridge.getWorkWidth(mower), 40.8)
|
||||
|
||||
-- W4: Courseplays egen bredde (HUD'en) vinder — det er den marken køres med
|
||||
local cpVehicle = {getVariableWorkWidth = function(_, isLeft) return isLeft and 20.4 or -0.4, nil, true end,
|
||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 40.8 end}} end}
|
||||
check("W4 Courseplay-bredde", ADCpBridge.getWorkWidth(cpVehicle), 40.8)
|
||||
-- W5: Courseplay-indstilling 0/ugyldig -> egen beregning
|
||||
local zeroCp = {getVariableWorkWidth = function(_, isLeft) return isLeft and 5 or -5, nil, true end,
|
||||
getCourseGeneratorSettings = function() return {workWidth = {getValue = function() return 0 end}} end}
|
||||
check("W5 CP 0 -> egen beregning", ADCpBridge.getWorkWidth(zeroCp), 10)
|
||||
|
||||
-- WO: skårets sideforskydning på en river, målt fra nedlægningsområdet (venstre positiv), minus rivens
|
||||
-- egen CP-forskydning. Noder er tal; localToLocal-mock giver nodens x i traktorens ramme.
|
||||
local nodeX = {[11] = 2.0, [12] = 4.0, [21] = -1.0, [22] = 1.0}
|
||||
function localToLocal(node) return nodeX[node] or 0, 0, 0 end
|
||||
local function rake(dropIndex, runtime)
|
||||
local areas = {
|
||||
{start = 1, width = 2, height = 3, dropWindrowWorkAreaIndex = runtime and dropIndex or nil},
|
||||
{start = 10, width = 11, height = 12},
|
||||
{start = 20, width = 21, height = 22},
|
||||
}
|
||||
return {spec_windrower = {}, spec_workArea = {workAreas = areas}, configFileName = "rake.xml"}
|
||||
end
|
||||
local sideRake = rake(2, true)
|
||||
local tractorWithRake = {rootNode = 100, getAttachedImplements = function() return {{object = sideRake}} end,
|
||||
getCpSettings = function() return {toolOffsetX = {getValue = function() return 0.5 end}} end}
|
||||
check("WO1 sideriver (runtime-felt)", ADCpBridge.getWindrowOffset(tractorWithRake), 2.5)
|
||||
local centerRake = rake(3, true)
|
||||
local tractorCenter = {rootNode = 100, getAttachedImplements = function() return {{object = centerRake}} end}
|
||||
check("WO2 midterrive", ADCpBridge.getWindrowOffset(tractorCenter), 0)
|
||||
-- WO3: runtime-felt mangler -> riverens XML (workArea(0) windrower#dropWindrowWorkAreaIndex = 2)
|
||||
ADCpBridge.readDropIndicesFromXml = function(fileName) return fileName == "rake.xml" and {[1] = 2} or {} end
|
||||
local xmlRake = rake(2, false)
|
||||
local tractorXml = {rootNode = 100, getAttachedImplements = function() return {{object = xmlRake}} end}
|
||||
check("WO3 fra XML", ADCpBridge.getWindrowOffset(tractorXml), 3)
|
||||
check("WO4 ingen river -> nil", ADCpBridge.getWindrowOffset({rootNode = 100}), nil)
|
||||
|
||||
-- SR: sprøjte/spreder -> rolle ud fra fyldet
|
||||
check("SR gødning", ADCpBridge.sprayRoleForFillType("FERTILIZER"), "fertilizer")
|
||||
check("SR flydende", ADCpBridge.sprayRoleForFillType("LIQUIDFERTILIZER"), "fertilizer")
|
||||
check("SR gylle", ADCpBridge.sprayRoleForFillType("LIQUIDMANURE"), "fertilizer")
|
||||
check("SR møg", ADCpBridge.sprayRoleForFillType("MANURE"), "fertilizer")
|
||||
check("SR digestat", ADCpBridge.sprayRoleForFillType("DIGESTATE"), "fertilizer")
|
||||
check("SR kalk", ADCpBridge.sprayRoleForFillType("LIME"), "limer")
|
||||
check("SR ukrudt", ADCpBridge.sprayRoleForFillType("HERBICIDE"), "weeder")
|
||||
check("SR vand/ukendt", ADCpBridge.sprayRoleForFillType("WATER"), nil)
|
||||
|
||||
-- HF: høsterens afgrøder fra skærebordet (liste eller opslag); finsnitter tager ingen
|
||||
local list = ADCpBridge.harvestFruitSet({fruitTypes = {3, 5}})
|
||||
check("HF liste", list[3] and list[5], true)
|
||||
local lookup = ADCpBridge.harvestFruitSet({fruitTypes = {[3] = true}})
|
||||
check("HF opslag", lookup[3], true)
|
||||
local forage = ADCpBridge.harvestFruitSet({fruitTypes = {3}, fruitTypeConverters = {[3] = {}}})
|
||||
check("HF finsnitter tager intet", next(forage), nil)
|
||||
check("HF fruitTypeIndices", ADCpBridge.harvestFruitSet({fruitTypeIndices = {7}})[7], true)
|
||||
|
||||
-- PL: plantbar i perioden
|
||||
local fruitPlant = {growthDataSeasonal = {periods = {[3] = {plantingAllowed = true}, [4] = {plantingAllowed = false}}}}
|
||||
check("PL tilladt", ADCpBridge.isPlantableIn(fruitPlant, 3), true)
|
||||
check("PL ikke nu", ADCpBridge.isPlantableIn(fruitPlant, 4), false)
|
||||
check("PL ukendt data = tilladt", ADCpBridge.isPlantableIn({}, 4), true)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
@@ -15,7 +15,7 @@ check("FL1 slåmaskine", F.roleFromSpecs({spec_mower = true}), "mower")
|
||||
check("FL1 vender", F.roleFromSpecs({spec_tedder = true}), "tedder")
|
||||
check("FL1 river", F.roleFromSpecs({spec_windrower = true}), "rake")
|
||||
check("FL1 læssevogn", F.roleFromSpecs({spec_forageWagon = true, spec_mower = true}), "collector")
|
||||
check("FL1 intet", F.roleFromSpecs({spec_plow = true}), nil)
|
||||
check("FL1 intet", F.roleFromSpecs({spec_trailer = true}), nil)
|
||||
|
||||
local function rig(id, role, x, extra)
|
||||
local r = {id = id, role = role, enabled = true, busy = false, controlled = false, width = 40.8, x = x, z = 0, label = id}
|
||||
@@ -49,5 +49,38 @@ local toggled = F.toggle(enabled, "b")
|
||||
check("FL7 b til", toggled.b, true); check("FL7 original urørt", enabled.b, nil)
|
||||
check("FL7 a fra", F.toggle(toggled, "a").a, nil)
|
||||
|
||||
-- FL8: nye roller i prioriteret rækkefølge
|
||||
check("FL8 høster", F.roleFromSpecs({spec_combine = true, spec_cutter = true}), "harvester")
|
||||
check("FL8 såmaskine før kultivator (combi)", F.roleFromSpecs({spec_sowingMachine = true, spec_cultivator = true, sprayRole = "fertilizer"}), "seeder")
|
||||
check("FL8 kultivator", F.roleFromSpecs({spec_cultivator = true}), "cultivator")
|
||||
check("FL8 plov", F.roleFromSpecs({spec_plow = true}), "plow")
|
||||
check("FL8 tromle", F.roleFromSpecs({spec_roller = true}), "roller")
|
||||
check("FL8 strigle", F.roleFromSpecs({spec_weeder = true}), "weeder")
|
||||
check("FL8 stubhakker", F.roleFromSpecs({spec_mulcher = true}), "mulcher")
|
||||
check("FL8 gødningssprøjte", F.roleFromSpecs({sprayRole = "fertilizer"}), "fertilizer")
|
||||
check("FL8 kalkspreder", F.roleFromSpecs({sprayRole = "limer"}), "limer")
|
||||
check("FL8 ukrudtssprøjte", F.roleFromSpecs({sprayRole = "weeder"}), "weeder")
|
||||
check("FL8 aflæsningsvogn", F.roleFromSpecs({canUnload = true}), "unloader")
|
||||
check("FL8 traktor med plov er ikke aflæsningsvogn", F.roleFromSpecs({spec_plow = true, canUnload = true}), "plow")
|
||||
|
||||
-- FL9: accept-filter (fx høster der kan tage afgrøden, strigle kun til småt ukrudt)
|
||||
local combineWheat = rig("jd", "harvester", 10, {fruits = {[3] = true}})
|
||||
local combineBeet = rig("ropa", "harvester", 1, {fruits = {[9] = true}})
|
||||
local forWheat = F.pick({combineBeet, combineWheat}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
|
||||
check("FL9 høster der kan tage hvede", forWheat.id, "jd")
|
||||
local _, whyFruit = F.pick({combineBeet}, "harvester", 0, 0, nil, function(c) return c.fruits[3] == true end)
|
||||
check("FL9 ingen kan tage afgrøden", whyFruit, "cannotDo")
|
||||
|
||||
-- FL10: flere sæt til multi-tool — alle eller ingen, nærmeste først
|
||||
local m1, m2, m3 = rig("m1", "mower", 30), rig("m2", "mower", 10), rig("m3", "mower", 20, {busy = true})
|
||||
local two = F.pickMany({m1, m2, m3}, "mower", 2, 0, 0)
|
||||
check("FL10 to valgt", #two, 2)
|
||||
check("FL10 nærmeste først", two[1].id, "m2")
|
||||
local none, whyMany, args = F.pickMany({m1, m2, m3}, "mower", 3, 0, 0)
|
||||
check("FL10 for få -> ingen", none, nil)
|
||||
check("FL10 grund", whyMany, "notEnough")
|
||||
check("FL10 ledige/krævet", args[1] .. "/" .. args[2], "2/3")
|
||||
check("FL10 ingen med rollen", select(2, F.pickMany({m1}, "tedder", 1, 0, 0)), "noRig")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -38,5 +38,56 @@ local standing = {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}
|
||||
check("J6 én tur -> slå igen", J.decide(mowing, standing, {{phase = "finished"}}, 0).action, "dispatch")
|
||||
check("J6 to ture -> videre", J.decide(mowing, standing, {{phase = "finished"}, {phase = "finished"}}, 0).action, "advance")
|
||||
|
||||
-- J7: slået mark uden sæt (fx sluppet undervejs) -> videre til opsamling, ikke en ny slå-tur
|
||||
check("J7 slået uden sæt -> videre", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {}, 0).action, "advance")
|
||||
check("J7 stående uden sæt -> slå", J.decide(mowing, info, {}, 0).action, "dispatch")
|
||||
check("J7 slået men sæt i gang -> vent", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {{phase = "working"}}, 0).action, "none")
|
||||
|
||||
-- J8: høst — høster først (hovedrolle), aflæsningsvogne kun mens høsteren arbejder
|
||||
local harvestInfo = {id = 50, markerId = 7, kind = "harvest", readyRatio = 1, fruitIndex = 3}
|
||||
local harvesting = M.start(M.onReadiness(M.newField(50), 1), "harvest", {})
|
||||
local d = J.decide(harvesting, harvestInfo, {}, 0)
|
||||
check("J8 send høster", d.role, "harvester"); check("J8 ikke hjælper", d.helper, false)
|
||||
local withHarvester = {{phase = "working", stage = "harvesting", helper = false}}
|
||||
local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0)
|
||||
check("J8 så aflæsningsvogn", dHelper.role, "unloader"); check("J8 hjælper", dHelper.helper, true)
|
||||
check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none")
|
||||
-- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører
|
||||
local afterInfo = {id = 50, markerId = 7, kind = "bare", readyRatio = 0}
|
||||
check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance")
|
||||
-- sæt fra et tidligere trin (vogn på vej hjem) tæller ikke på det nye trin
|
||||
local cultivating = M.advance(harvesting, {})
|
||||
check("J8 gammel vogn tæller ikke", J.decide(cultivating, afterInfo, {{phase = "working", stage = "harvesting", helper = true}}, 0).role, "cultivator")
|
||||
|
||||
-- J9: multi-tool — hele holdet sendes samlet; mens nogen kører, sendes ingen nye hovedsæt
|
||||
local multiMow = M.with(mowing, {plan = {rigs = 3}})
|
||||
local dMulti = J.decide(multiMow, info, {}, 0)
|
||||
check("J9 tre slåmaskiner", dMulti.count, 3)
|
||||
check("J9 én kører stadig -> ingen nye", J.decide(multiMow, info, {{phase = "working"}, {phase = "finished"}}, 0).action, "none")
|
||||
|
||||
-- J10: pleje under vækst — Auto starter, Manuel viser behovet
|
||||
local growingInfo = {id = 60, markerId = 8, kind = "harvest", growing = true, fert = 0, fertMax = 2, lime = 2, weed = 0, readyRatio = 0.2}
|
||||
local autoField = M.with(M.newField(60), {plan = {mode = "auto"}})
|
||||
local care = J.decide(autoField, growingInfo, {}, 0)
|
||||
check("J10 auto -> pleje", care.action, "care"); check("J10 gødning", care.stage, "fertilizing")
|
||||
local manualCare = J.decide(M.newField(60), growingInfo, {}, 0)
|
||||
check("J10 manuel -> intet, men behov vist", manualCare.action, "none"); check("J10 behov", manualCare.care, "fertilizing")
|
||||
check("J10 Start nu -> pleje", J.decide(M.with(M.newField(60), {startRequested = true}), growingInfo, {}, 0).action, "care")
|
||||
check("J10 uden markør -> vent", J.decide(autoField, {id = 60, kind = "harvest", growing = true, fert = 0, fertMax = 2}, {}, 0).action, "wait")
|
||||
|
||||
-- J11: regn — vejrfølsomme trin sendes ikke ud (kørende sæt fortsætter); klargøring/pleje kører videre
|
||||
local rain = {raining = true}
|
||||
local dRain = J.decide(mowing, info, {}, 0, rain)
|
||||
check("J11 slåning venter i regn", dRain.action, "wait"); check("J11 grund", dRain.reason, "rain")
|
||||
check("J11 kørende slåmaskine fortsætter", J.decide(mowing, info, {{phase = "working"}}, 0, rain).action, "none")
|
||||
check("J11 høst venter", J.decide(harvesting, harvestInfo, {}, 0, rain).reason, "rain")
|
||||
check("J11 kultivering kører i regn", J.decide(cultivating, afterInfo, {}, 0, rain).role, "cultivator")
|
||||
check("J11 start af høst venter", J.decide(M.with(M.onReadiness(M.newField(51), 1), {startRequested = true, kind = "harvest"}), harvestInfo, {}, 0, rain).reason, "rain")
|
||||
local silageCollect = M.advance(mowing)
|
||||
check("J11 ensilage-opsamling kører i regn", J.decide(silageCollect, info, {}, 0, rain).action, "dispatch")
|
||||
local hayCollect = M.with(silageCollect, {plan = {product = "hay"}})
|
||||
check("J11 hø-opsamling venter", J.decide(hayCollect, info, {}, 0, rain).reason, "rain")
|
||||
check("J11 tørvejr", J.decide(mowing, info, {}, 0, {raining = false}).action, "dispatch")
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
+141
-6
@@ -13,7 +13,9 @@ end
|
||||
local f = M.newField(109)
|
||||
check("M1 stage", f.stage, "idle")
|
||||
check("M1 mode", f.plan.mode, "manual")
|
||||
check("M1 product", f.plan.product, "silage")
|
||||
check("M1 product auto", f.plan.product, "auto")
|
||||
check("M1 auto = ensilage på slåafgrøde", M.product(f.plan, "mowable"), "silage")
|
||||
check("M1 auto = høst ellers", M.product(f.plan, "harvest"), "harvest")
|
||||
check("M1 rake", f.plan.rake, false)
|
||||
|
||||
-- M2: klar ved >= 90 %
|
||||
@@ -28,6 +30,7 @@ check("M3 lidt under 90 % forbliver klar", M.onReadiness(ready, 0.8).stage, "rea
|
||||
|
||||
-- M4: trin pr. plan
|
||||
check("M4 ensilage", table.concat(M.workStages({product = "silage"}), ","), "mowing,collecting")
|
||||
check("M4 ensilage m. riv", table.concat(M.workStages({product = "silage", rake = true}), ","), "mowing,raking,collecting")
|
||||
check("M4 hø", table.concat(M.workStages({product = "hay", rake = false}), ","), "mowing,tedding,collecting")
|
||||
check("M4 hø m. riv", table.concat(M.workStages({product = "hay", rake = true}), ","), "mowing,tedding,raking,collecting")
|
||||
|
||||
@@ -41,6 +44,8 @@ check("M5 Start nu", M.shouldStart(requested), true)
|
||||
local started = M.start(ready)
|
||||
check("M5 start -> slås", started.stage, "mowing")
|
||||
check("M5 start rydder forespørgsel", started.startRequested, false)
|
||||
local oldMow = M.with(ready, {mowVehicleId = "old"})
|
||||
check("M5 start glemmer gammel slåmaskine", M.start(oldMow).mowVehicleId, nil)
|
||||
|
||||
-- M6: videre gennem trinene; færdig mark bliver klar igen når den er vokset
|
||||
local hay = M.start(M.onReadiness(M.newField(7), 1)); hay.plan.product = "hay"
|
||||
@@ -57,7 +62,7 @@ check("M6 stop -> klar", M.stop(started).stage, "ready")
|
||||
check("M7 rolle slå", M.roleFor("mowing"), "mower")
|
||||
check("M7 rolle saml", M.roleFor("collecting"), "collector")
|
||||
check("M7 rolle klar = ingen", M.roleFor("ready"), nil)
|
||||
check("M7 maks læssevogne", M.maxRigs("collecting"), 2)
|
||||
check("M7 maks læssevogne = planens sæt", M.maxRigs("collecting", {rigs = 2}), 2)
|
||||
check("M7 maks slå", M.maxRigs("mowing"), 1)
|
||||
local okWide, whyWide = M.checkWidth("mower", 60, nil)
|
||||
check("M7 over 50 m", okWide, false); check("M7 grund", whyWide, "tooWide")
|
||||
@@ -68,15 +73,15 @@ check("M7 lige bred ok (tolerance)", (M.checkWidth("collector", 40.76, 40.8)), t
|
||||
check("M7 ukendt slåbredde ok", (M.checkWidth("tedder", 10, nil)), true)
|
||||
|
||||
-- wantsRig: kun arbejdende trin, under maks, efter pause
|
||||
check("M7 wantsRig slå", M.wantsRig(started, 0, 0), "mower")
|
||||
check("M7 wantsRig slå", M.wantsRig(started, 0, 0).role, "mower")
|
||||
check("M7 wantsRig fuld", M.wantsRig(started, 1, 0), nil)
|
||||
local paused = M.start(M.onReadiness(M.newField(8), 1)); paused.retryAt = 1000
|
||||
check("M7 wantsRig i pause", M.wantsRig(paused, 0, 500), nil)
|
||||
check("M7 wantsRig efter pause", M.wantsRig(paused, 0, 1000), "mower")
|
||||
check("M7 wantsRig efter pause", M.wantsRig(paused, 0, 1000).role, "mower")
|
||||
check("M7 wantsRig klar = nil", M.wantsRig(ready, 0, 0), nil)
|
||||
|
||||
-- M8: gem/indlæs bevarer trin, plan og slåbredde
|
||||
local saved = M.advance(M.start(M.onReadiness(M.newField(109), 1))); saved.mowWidth = 40.8
|
||||
local saved = M.advance(M.start(M.onReadiness(M.newField(109), 1))); saved.mowWidth = 40.8; saved.mowVehicleId = "vehicleBigM"
|
||||
saved.plan = {mode = "auto", product = "hay", rake = true, storeBuildingIds = {"b12", "b7"}}
|
||||
local back = M.fromRecord(M.toRecord(saved))
|
||||
check("M8 id", back.id, 109)
|
||||
@@ -86,6 +91,8 @@ check("M8 product", back.plan.product, "hay")
|
||||
check("M8 rake", back.plan.rake, true)
|
||||
check("M8 lager", table.concat(back.plan.storeBuildingIds, " "), "b12 b7")
|
||||
check("M8 slåbredde", back.mowWidth, 40.8)
|
||||
check("M8 slåmaskine", back.mowVehicleId, "vehicleBigM")
|
||||
check("M8 ingen slåmaskine", M.fromRecord({id = 4, stage = "idle"}).mowVehicleId, nil)
|
||||
check("M8 ukendt trin -> venter", M.fromRecord({id = 3, stage = "bogus"}).stage, "idle")
|
||||
|
||||
-- M9: with() kopierer og fletter planen
|
||||
@@ -93,11 +100,139 @@ local base = M.start(M.onReadiness(M.newField(1), 1))
|
||||
local noted = M.with(base, {reason = {text = "x"}, retryAt = 5, plan = {product = "hay"}})
|
||||
check("M9 grund", noted.reason.text, "x"); check("M9 pause", noted.retryAt, 5)
|
||||
check("M9 plan flettet", noted.plan.product, "hay"); check("M9 mode bevaret", noted.plan.mode, "manual")
|
||||
check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", base.plan.product, "silage")
|
||||
check("M9 trin bevaret", noted.stage, "mowing"); check("M9 original urørt", base.plan.product, "auto")
|
||||
|
||||
-- M10: Start nu på en mark der ikke er helt klar -> klar; arbejdende mark røres ikke
|
||||
check("M10 venter -> klar", M.forceReady(M.newField(2)).stage, "ready")
|
||||
check("M10 arbejdende uændret", M.forceReady(base).stage, "mowing")
|
||||
|
||||
-- M11: krævet bredde og kursus-kilde pr. rolle (smalle standardmaskiner, 25/9)
|
||||
local cut = M.with(M.newField(3), {mowWidth = 40.8})
|
||||
check("M11 vender kræver ingen bredde", M.requiredWidth(cut, "tedder"), nil)
|
||||
check("M11 river kræver ingen bredde", M.requiredWidth(cut, "rake"), nil)
|
||||
check("M11 læssevogn uden riv = slåbredde", M.requiredWidth(cut, "collector"), 40.8)
|
||||
local raked = M.with(cut, {plan = {rake = true}})
|
||||
check("M11 læssevogn efter riv = ingen krav", M.requiredWidth(raked, "collector"), nil)
|
||||
check("M11 vender eget kursus", M.courseSource(cut, "tedder"), nil)
|
||||
check("M11 river eget kursus", M.courseSource(raked, "rake"), nil)
|
||||
check("M11 læssevogn uden riv -> slåkursus", M.courseSource(cut, "collector"), "mower")
|
||||
check("M11 læssevogn efter riv -> rivekursus", M.courseSource(raked, "collector"), "rake")
|
||||
check("M11 slåmaskine eget kursus", M.courseSource(cut, "mower"), nil)
|
||||
-- gem/indlæs rivemaskine
|
||||
local withRake = M.with(raked, {rakeVehicleId = "vehicleRake"})
|
||||
check("M11 rivemaskine gemt", M.fromRecord(M.toRecord(withRake)).rakeVehicleId, "vehicleRake")
|
||||
check("M11 start glemmer rivemaskine", M.start(M.with(M.onReadiness(M.newField(9), 1), {rakeVehicleId = "x"})).rakeVehicleId, nil)
|
||||
|
||||
-- M12: sideriver — læssevognens CP-forskydning = egen forskydning − skårets forskydning (venstre positiv),
|
||||
-- holdt inden for Courseplays ±10 m; skårets forskydning gemmes pr. mark
|
||||
check("M12 midterrive", M.collectorOffset(0, 0), 0)
|
||||
check("M12 skår 3 m til venstre", M.collectorOffset(0, 3), -3)
|
||||
check("M12 vogn med egen forskydning", M.collectorOffset(0.5, -2), 2.5)
|
||||
check("M12 holdes inden for 10 m", M.collectorOffset(0, 14), -10)
|
||||
check("M12 ukendt skår -> egen forskydning", M.collectorOffset(0.4, nil), 0.4)
|
||||
local side = M.with(M.newField(11), {windrowOffset = 3.2})
|
||||
check("M12 skår gemt", M.fromRecord(M.toRecord(side)).windrowOffset, 3.2)
|
||||
check("M12 intet skår gemt", M.fromRecord(M.toRecord(M.newField(12))).windrowOffset, nil)
|
||||
check("M12 start glemmer skår", M.start(M.with(M.onReadiness(M.newField(13), 1), {windrowOffset = 1})).windrowOffset, nil)
|
||||
|
||||
-- M13: gæringssilo = produktion der laver markens skår om til ensilage (FermentingSiloPack m.fl.)
|
||||
local GRASS_W, DRY_W, SILAGE, CHAFF, BREAD = 10, 11, 20, 30, 40
|
||||
local fermenter = {{inputs = {{type = CHAFF}}, outputs = {{type = SILAGE}}}, {inputs = {{type = GRASS_W}}, outputs = {{type = SILAGE}}}}
|
||||
check("M13 græs -> ensilage", M.isFermenter(fermenter, GRASS_W, SILAGE), true)
|
||||
check("M13 hø ikke i denne", M.isFermenter(fermenter, DRY_W, SILAGE), false)
|
||||
check("M13 bageri ikke", M.isFermenter({{inputs = {{type = GRASS_W}}, outputs = {{type = BREAD}}}}, GRASS_W, SILAGE), false)
|
||||
check("M13 ingen produktioner", M.isFermenter(nil, GRASS_W, SILAGE), false)
|
||||
check("M13 ukendt ensilage-type", M.isFermenter(fermenter, GRASS_W, nil), false)
|
||||
|
||||
-- M14: høstkæde + klargøring efter behov
|
||||
local plan = {mode = "manual", product = "auto", rake = false, rigs = 1, unloaders = 1, mulch = false, lime = true,
|
||||
soil = "auto", sow = true, sowFruit = "", roll = true, fertilize = true, weed = true, storeBuildingIds = {}}
|
||||
check("M14 høst + kultivér + så + tromle", table.concat(M.workStages(plan, "harvest", {}), ","), "harvesting,cultivating,sowing,rolling")
|
||||
check("M14 kalk + pløj når jorden trænger", table.concat(M.workStages(plan, "harvest", {lime = true, plow = true}), ","), "harvesting,liming,plowing,sowing,rolling")
|
||||
local noSoil = {soil = "none", sow = false, lime = false, mulch = true}
|
||||
check("M14 kun stubbehakning", table.concat(M.workStages(noSoil, "harvest", {}), ","), "harvesting,mulching")
|
||||
check("M14 bar mark = kun klargøring", table.concat(M.workStages(plan, "bare", {plow = true}), ","), "plowing,sowing,rolling")
|
||||
check("M14 tvunget pløj", table.concat(M.prepStages({soil = "plow"}, {}), ","), "plowing")
|
||||
local harvestField = M.start(M.with(M.newField(20), {plan = {sow = true, soil = "cultivate"}}), "harvest", {})
|
||||
check("M14 start = høst", harvestField.stage, "harvesting")
|
||||
check("M14 husker art", harvestField.kind, "harvest")
|
||||
local afterHarvest = M.advance(harvestField, {})
|
||||
check("M14 høst -> kultivér", afterHarvest.stage, "cultivating")
|
||||
check("M14 -> så", M.advance(afterHarvest, {}).stage, "sowing")
|
||||
check("M14 så -> færdig", M.advance(M.advance(afterHarvest, {}), {}).stage, "done")
|
||||
check("M14 kalk springes over når jorden er kalket", M.advance(M.with(harvestField, {plan = {lime = true}}), {lime = false}).stage, "cultivating")
|
||||
check("M14 tom kæde -> færdig", M.start(M.with(M.newField(21), {plan = {soil = "none", sow = false, lime = false}}), "bare", {}).stage, "done")
|
||||
|
||||
-- M15: hjælpere under høst og multi-tool-baner
|
||||
check("M15 aflæsningsvogn efter høsteren", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).role, "unloader")
|
||||
check("M15 hjælper-flag", M.wantsRig(harvestField, {primary = 1, helpers = 0}, 0).helper, true)
|
||||
check("M15 alle vogne på plads -> intet", M.wantsRig(harvestField, {primary = 1, helpers = 1}, 0), nil)
|
||||
local multi = M.with(harvestField, {plan = {rigs = 3, unloaders = 2}})
|
||||
check("M15 multi: 3 høstere samlet", M.wantsRig(multi, {primary = 0, helpers = 0}, 0).count, 3)
|
||||
check("M15 multi: 2 vogne", M.maxHelpers("harvesting", multi.plan), 2)
|
||||
check("M15 ingen hjælper ved slåning", M.maxHelpers("mowing", multi.plan), 0)
|
||||
check("M15 baner 2", table.concat(M.lanePositions(2), ","), "-1,1")
|
||||
check("M15 baner 3", table.concat(M.lanePositions(3), ","), "-1,0,1")
|
||||
check("M15 baner 4", table.concat(M.lanePositions(4), ","), "-2,-1,1,2")
|
||||
check("M15 baner 5", table.concat(M.lanePositions(5), ","), "-2,-1,0,1,2")
|
||||
check("M15 over 5 -> 5", #M.lanePositions(9), 5)
|
||||
check("M15 maxRigs holdes 1-5", M.maxRigs("mowing", {rigs = 7}), 5)
|
||||
|
||||
-- M16: pleje under vækst
|
||||
local growing = {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 0}
|
||||
local idleField = M.newField(30)
|
||||
check("M16 gødning", M.careNeed(idleField, growing), "fertilizing")
|
||||
check("M16 ukrudt før gødning", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 1, fertMax = 2, lime = 2, weed = 3}), "weeding")
|
||||
check("M16 græs ukrudtsbehandles ikke", M.careNeed(idleField, {kind = "mowable", growing = true, fert = 2, fertMax = 2, lime = 2, weed = 3}), nil)
|
||||
check("M16 kalk", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 0, weed = 0}), "careLiming")
|
||||
check("M16 intet behov", M.careNeed(idleField, {kind = "harvest", growing = true, fert = 2, fertMax = 2, lime = 1, weed = 1}), nil)
|
||||
check("M16 ikke voksende", M.careNeed(idleField, {kind = "harvest", growing = false, fert = 0, fertMax = 2}), nil)
|
||||
check("M16 gødning slået fra", M.careNeed(M.with(idleField, {plan = {fertilize = false}}), growing), nil)
|
||||
check("M16 arbejdende mark", M.careNeed(harvestField, growing), nil)
|
||||
local cared = M.startCare(idleField, "fertilizing")
|
||||
check("M16 pleje-trin", cared.stage, "fertilizing")
|
||||
check("M16 rolle", M.roleFor(cared.stage), "fertilizer")
|
||||
check("M16 kalk-pleje rolle", M.roleFor("careLiming"), "limer")
|
||||
check("M16 pleje -> idle", M.advance(cared, {}).stage, "idle")
|
||||
local twice = M.startCare(M.advance(cared, {}), "fertilizing")
|
||||
check("M16 højst 2 gange", M.careNeed(M.advance(twice, {}), growing), nil)
|
||||
check("M16 ny cyklus nulstiller", M.careNeed(M.advance(M.start(M.advance(twice, {}), "harvest", {}), {}), growing) ~= "x", true)
|
||||
check("M16 start nulstiller pleje-tæller", next(M.start(twice, "harvest", {}).careRuns), nil)
|
||||
check("M16 stop af pleje -> idle", M.stop(cared).stage, "idle")
|
||||
check("M16 stop af høst -> klar", M.stop(harvestField).stage, "ready")
|
||||
|
||||
-- M17: såafgrøde og gem/indlæs af den udvidede plan
|
||||
check("M17 så samme afgrøde", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT"})), "WHEAT")
|
||||
check("M17 planens afgrøde vinder", M.sowFruit(M.with(M.newField(40), {lastFruit = "WHEAT", plan = {sowFruit = "CANOLA"}})), "CANOLA")
|
||||
local full = M.with(M.startCare(M.newField(41), "weeding"), {kind = "harvest", lastFruit = "BARLEY",
|
||||
plan = {rigs = 3, unloaders = 2, mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "WHEAT", roll = true, fertilize = false, weed = false, product = "harvest"}})
|
||||
local round = M.fromRecord(M.toRecord(full))
|
||||
check("M17 trin", round.stage, "weeding")
|
||||
check("M17 sæt", round.plan.rigs, 3)
|
||||
check("M17 vogne", round.plan.unloaders, 2)
|
||||
check("M17 stub", round.plan.mulch, true)
|
||||
check("M17 kalk fra", round.plan.lime, false)
|
||||
check("M17 jord", round.plan.soil, "plow")
|
||||
check("M17 så", round.plan.sow, true)
|
||||
check("M17 såafgrøde", round.plan.sowFruit, "WHEAT")
|
||||
check("M17 tromle", round.plan.roll, true)
|
||||
check("M17 gødning fra", round.plan.fertilize, false)
|
||||
check("M17 ukrudt fra", round.plan.weed, false)
|
||||
check("M17 art", round.kind, "harvest")
|
||||
check("M17 sidste afgrøde", round.lastFruit, "BARLEY")
|
||||
check("M17 pleje-tæller", round.careRuns.weeding, 1)
|
||||
check("M17 produkt", round.plan.product, "harvest")
|
||||
local legacy = M.fromRecord({id = 5, stage = "collecting", mode = "auto", product = "hay", rake = 1})
|
||||
check("M17 etape 1-post: hø", legacy.plan.product, "hay")
|
||||
check("M17 etape 1-post: standard kalk til", legacy.plan.lime, true)
|
||||
check("M17 etape 1-post: 1 sæt", legacy.plan.rigs, 1)
|
||||
|
||||
-- M18: Start nu på en bar mark (klar-andel 0) må ikke nulstilles af vækst-reglen før den starter
|
||||
local bare = M.with(M.forceReady(M.newField(70)), {startRequested = true})
|
||||
check("M18 bar mark forbliver klar", M.onReadiness(bare, 0).stage, "ready")
|
||||
check("M18 uden forespørgsel -> venter", M.onReadiness(M.forceReady(M.newField(71)), 0).stage, "idle")
|
||||
check("M18 vejrfølsom: høst", M.isWeatherSensitive("harvesting", {}, "harvest"), true)
|
||||
check("M18 vejrfølsom: kultivering ikke", M.isWeatherSensitive("cultivating", {}, "harvest"), false)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Kør: luajit tests/test_adFieldStorage.lua (fra repo-roden)
|
||||
-- Lagringen skal dække HVERT felt modellen gemmer (FIELD_TYPES), og en mark skal overleve en tur gennem XML.
|
||||
dofile("FS25_ADSmartPickup/adFieldModel.lua")
|
||||
dofile("FS25_ADSmartPickup/adFieldStorage.lua")
|
||||
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
|
||||
|
||||
-- falsk XMLFile: gemmer værdier pr. nøgle
|
||||
local function fakeXml()
|
||||
local values = {}
|
||||
local xml = {}
|
||||
for _, kind in ipairs({"Int", "String", "Float"}) do
|
||||
xml["set" .. kind] = function(_, key, value) values[key] = value end
|
||||
xml["get" .. kind] = function(_, key) return values[key] end
|
||||
end
|
||||
return xml
|
||||
end
|
||||
|
||||
local field = ADFieldModel.with(ADFieldModel.startCare(ADFieldModel.newField(77), "weeding"), {
|
||||
kind = "harvest", lastFruit = "WHEAT", mowWidth = 40.8, mowVehicleId = "v1", rakeVehicleId = "v2", windrowOffset = -2.5,
|
||||
plan = {mode = "auto", product = "harvest", rake = true, storeBuildingIds = {"b1"}, rigs = 3, unloaders = 2,
|
||||
mulch = true, lime = false, soil = "plow", sow = true, sowFruit = "CANOLA", roll = true, fertilize = false, weed = false},
|
||||
})
|
||||
local record = ADFieldModel.toRecord(field)
|
||||
for key in pairs(record) do
|
||||
check("ST1 felt med i lagringen: " .. key, ADFieldStorage.FIELD_TYPES[key] ~= nil, true)
|
||||
end
|
||||
local xml = fakeXml()
|
||||
ADFieldStorage.writeRecord(xml, "k", record)
|
||||
local back = ADFieldModel.fromRecord(ADFieldStorage.readRecord(xml, "k"))
|
||||
local again = ADFieldModel.toRecord(back)
|
||||
for key, value in pairs(record) do
|
||||
check("ST2 rundtur: " .. key, tostring(again[key]), tostring(value))
|
||||
end
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
+16
-10
@@ -17,20 +17,26 @@ local infos = {
|
||||
}
|
||||
local fields = {[109] = ADFieldModel.newField(109), [12] = ADFieldModel.newField(12)}
|
||||
|
||||
-- V1: kun slåafgrøder, sorteret efter nummer
|
||||
local rows = V.rows(infos, fields, false)
|
||||
check("V1 to rækker", #rows, 2)
|
||||
-- V1: alle marker (etape 2: også høst og bare), sorteret efter nummer
|
||||
local rows = V.rows(infos, fields)
|
||||
check("V1 tre rækker", #rows, 3)
|
||||
check("V1 sorteret", rows[1].info.id, 12)
|
||||
check("V1 model med", rows[2].field.id, 109)
|
||||
-- V2: vis alle tager resten med; mangler model -> ny standardmark
|
||||
local all = V.rows(infos, fields, true)
|
||||
check("V2 tre rækker", #all, 3)
|
||||
check("V2 standardmodel", all[2].field.stage, "idle")
|
||||
check("V2 skjulte", V.hiddenCount(infos, false), 1)
|
||||
check("V2 vis alle skjuler intet", V.hiddenCount(infos, true), 0)
|
||||
check("V1 model med", rows[3].field.id, 109)
|
||||
-- V2: mangler model -> ny standardmark
|
||||
check("V2 standardmodel", rows[2].field.stage, "idle")
|
||||
-- V3: status — manglende markør vinder over trin
|
||||
check("V3 mangler markør", V.statusKey(fields[12], infos[2]), "spu_field_noMarker")
|
||||
check("V3 trin", V.statusKey(fields[109], infos[1]), "spu_field_stage_idle")
|
||||
|
||||
-- V4: planens tekstnøgler pr. markart
|
||||
check("V4 ensilage", V.planKey({product = "auto"}, "mowable"), "spu_field_plan_silage")
|
||||
check("V4 hø", V.planKey({product = "hay"}, "mowable"), "spu_field_plan_hay")
|
||||
check("V4 høst", V.planKey({product = "auto"}, "harvest"), "spu_field_plan_harvest")
|
||||
check("V4 bar mark", V.planKey({product = "auto"}, "bare"), "spu_field_plan_prep")
|
||||
-- V5: jordtilstand som korte tekstdele
|
||||
local parts = V.soilParts({lime = 0.2, fert = 1, fertMax = 2, weed = 3, needs = {lime = true, plow = true}})
|
||||
check("V5 dele", table.concat(parts, ","), "lime,plow,fert,weed")
|
||||
check("V5 intet", #V.soilParts({lime = 2, fert = 2, fertMax = 2, weed = 0, needs = {}}), 0)
|
||||
|
||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||
os.exit(failures == 0 and 0 or 1)
|
||||
|
||||
@@ -29,5 +29,37 @@ 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)
|
||||
|
||||
-- F4: markens art og om den vokser
|
||||
local kind, growing = F.classify(3, false, 0.4)
|
||||
check("F4 høstafgrøde", kind, "harvest"); check("F4 vokser", growing, true)
|
||||
check("F4 klar høst vokser ikke", select(2, F.classify(3, false, 0.95)), false)
|
||||
check("F4 slåafgrøde", (F.classify(1, true, 0.2)), "mowable")
|
||||
check("F4 ingen afgrøde = bar", (F.classify(nil, false, 0)), "bare")
|
||||
check("F4 bar vokser ikke", select(2, F.classify(nil, false, 0)), false)
|
||||
|
||||
-- F5: jordens behov ud fra gennemsnit og spillets indstillinger
|
||||
local needs = F.soilNeeds({lime = 0.2, plowLevel = 0}, {limeRequired = true, plowingRequired = true})
|
||||
check("F5 kalk", needs.lime, true); check("F5 pløj", needs.plow, true)
|
||||
local off = F.soilNeeds({lime = 0, plowLevel = 0}, {limeRequired = false, plowingRequired = false})
|
||||
check("F5 kalk slået fra i spillet", off.lime, false); check("F5 pløjning slået fra", off.plow, false)
|
||||
check("F5 kalket jord", F.soilNeeds({lime = 2, plowLevel = 1}, {limeRequired = true, plowingRequired = true}).lime, false)
|
||||
check("F5 ukendt niveau = intet behov", F.soilNeeds({}, {limeRequired = true, plowingRequired = true}).plow, false)
|
||||
|
||||
-- F6: vækststadie -> fase ud fra afgrødens egne grænser
|
||||
local wheat = {minHarvestingGrowthState = 6, maxHarvestingGrowthState = 7, cutState = 9, witheredState = 8}
|
||||
check("F6 intet", F.growthPhase(0, wheat), "none")
|
||||
check("F6 spirer/vokser", F.growthPhase(3, wheat), "growing")
|
||||
check("F6 klar", F.growthPhase(6, wheat), "ready")
|
||||
check("F6 visnet", F.growthPhase(8, wheat), "withered")
|
||||
check("F6 høstet stub", F.growthPhase(9, wheat), "cut")
|
||||
check("F6 ukendt stadie", F.growthPhase(nil, wheat), nil)
|
||||
check("F6 FS25-navne (minHarvestGrowthState)", F.growthPhase(6, {minHarvestGrowthState = 5, maxHarvestGrowthState = 6, cutState = 8}), "ready")
|
||||
-- F7: vokser = fase growing; ukendt fase -> voksende pixels og ikke klar
|
||||
check("F7 fase vinder", F.isGrowing("cut", 100, 0.1), false)
|
||||
check("F7 fase vokser", F.isGrowing("growing", 0, 0), true)
|
||||
check("F7 reserve: voksende pixels", F.isGrowing(nil, 50, 0.2), true)
|
||||
check("F7 reserve: ingen pixels (stub)", F.isGrowing(nil, 0, 0), false)
|
||||
check("F7 reserve: klar", F.isGrowing(nil, 50, 0.95), false)
|
||||
|
||||
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