26 Commits

Author SHA1 Message Date
masterdraco 7a073ae047 refactor(marker): review-fund — død kodegren i klik-routing, eksplicit kursus-kilde-opslag 2026-09-26 01:32:55 +02:00
masterdraco 322f7b1c30 docs(marker): vælg og glem (mål-afgrøde), halm, alle ledige maskiner — manual + testplan; version 1.27.0.0 2026-09-26 01:27:19 +02:00
masterdraco 293d66156f feat(marker): halm efter høst — hakkes, samles op til silo eller presses til baller + ballesamler (CP BALE_FINDER); græsvalg også ved græs-mål 2026-09-26 01:26:35 +02:00
masterdraco af71548b0b feat(marker): mål-afgrøde pr. mark ('vælg mark + afgrøde, glem den') + brug alle ledige maskiner (standard) 2026-09-26 01:24:28 +02:00
masterdraco 944e61d1a8 fix(marker): påfyldning finder selv læssestation m. varen og AD-markør (AD's automaticPickupTarget er slået fra); tom sæt = ikke færdig; 30 s backoff når sæt mangler
Review-fund 26/9 (CRITICAL): setAutomaticPickupTarget er no-op i AutoDrive (AutoDrive.automaticPickupTarget = false),
så LOAD-mode ville køre til F-markøren og hænge. (MEDIUM): ingen backoff ved manglende sæt.
2026-09-26 00:13:01 +02:00
masterdraco b1a65929a7 docs(marker): kapitel 10 for alle marker (da/en), testplan til test-session; version 1.26.0.0 2026-09-26 00:06:18 +02:00
masterdraco ae1362d164 feat(marker): vejr (vent på tørvejr), påfyldning via AutoDrive LOAD+CP, beskeder i spillet; Start nu virker på bare marker 2026-09-26 00:04:37 +02:00
masterdraco 65986b1793 feat(marker): fanen Marker for alle markarter — plan, maskiner pr. trin, aflæsningsvogne, efter høst, såning, pleje, jordtilstand, nye roller i markflåden 2026-09-26 00:01:56 +02:00
masterdraco 969db0a699 feat(marker): generisk lagring af alle plan-felter (test sikrer at intet felt glemmes) 2026-09-25 23:58:47 +02:00
masterdraco 7b0a9e452b feat(marker): job-løkke delt i adFieldJobs (beslutning) + adFieldWork (udsendelse) — høst m. aflæsningsvogne, klargøring, pleje, multi-tool 2026-09-25 23:57:59 +02:00
masterdraco 02e355e700 feat(marker): Courseplay-bro — sprøjteroller, høsterens afgrøder, frø/plantbarhed, multi-tool-baner, combine-unloader 2026-09-25 23:54:45 +02:00
masterdraco 793352c4c4 feat(marker): marker — art (slå/høst/bar), vækstfase, jordniveauer (gødning/kalk/pløj/ukrudt) og spillets indstillinger 2026-09-25 23:53:20 +02:00
masterdraco 9be60d4b1c feat(marker): markflåde — alle markroller, accept-filter (afgrøde/ukrudt), pickMany til multi-tool 2026-09-25 23:51:32 +02:00
masterdraco 9cab4b937d feat(marker): model for høst, klargøring, pleje og multi-tool (1-5 sæt, aflæsningsvogne) 2026-09-25 23:51:12 +02:00
masterdraco 03c05b7608 docs: spec etape 2-3 + multi-maskine (høst, roer, jordbehandling, såning) 2026-09-25 23:47:46 +02:00
masterdraco 7f83cc4d28 fix(marker): ensilage går til gæringssilo (produktion skår -> SILAGE) før almindelig silo; hø aldrig i gæringssilo (v1.25.0.0)
In-game 25/9: læssevognen aflæssede græs i en Farma-silo i stedet for en ensilage-silo.
2026-09-25 23:43:51 +02:00
masterdraco cf7a2a51e5 feat(marker): sideriver — mål hvor riven lægger skåret, forskyd læssevognen tilsvarende (v1.24.0.0)
Brugerkrav 25/9: alle river-typer skal virke. Nedlægningsområdet (runtime-felt eller riverens XML) måles
i traktorens ramme mens riven arbejder; læssevognen på rivens kursus får CP-toolOffsetX = egen -
skårets forskydning og får sin værdi tilbage når den slippes.
2026-09-25 23:35:42 +02:00
masterdraco ba88eb5f99 feat(marker): smalle standardmaskiner — vender/river i egen bredde, læssevogn følger rivens kursus; Riv også til ensilage (v1.23.0.0)
Brugerspørgsmål 25/9: få standardredskaber har 40,8 m. Vender og river kører hele marken i deres egen
bredde; efter riv ligger skåret midt i banen, så læssevognen kopierer rivens kursus uden breddekrav.
Uden riv kræver læssevognen stadig slåbredden (nu med hint 'sæt Riv til').
2026-09-25 23:31:46 +02:00
masterdraco 4c88118e56 feat(marker): vender/river/læssevogn genbruger slåmaskinens Courseplay-kursus (v1.22.0.0)
In-game 25/9: læssevognens nye CP-kursus lå forskudt i forhold til skårene -> ikke samlet korrekt.
Slåkurset gemmes pr. mark (kopi) og kopieres til næste trin med vehicle:cpCopyCourse; efter
genindlæsning hentes det fra slåmaskinen (mowVehicle gemt i ADSmartPickup_fields.xml).
2026-09-25 23:25:55 +02:00
masterdraco 048f5e6547 fix(marker): væk sovende køretøjer ved AD-start og under opfølgning (v1.21.6.0)
In-game 25/9: Fastrac'en stod stille ved AutoDrive-start til spilleren satte sig ind (parkerede
køretøjer kalder ikke onUpdate). Samme greb som flows' applyAndStart + raiseActive fra AI-Bonden.
2026-09-25 23:22:04 +02:00
masterdraco 52250dc35e fix(marker): slip kun et sæt når spilleren faktisk kører det (> 3 km/t uden AD/CP) (v1.21.5.0)
In-game 25/9: læssevognen blev sluppet ved ankomsten til F22, fordi spilleren sad stille i førersædet.
2026-09-25 23:11:50 +02:00
masterdraco 6f387c051f fix(marker): tag aldrig et sæt som AutoDrive/Courseplay allerede kører (v1.21.4.0)
In-game 25/9: spillerens parkeringstur kunne blive kapret af en ny udsendelse.
2026-09-25 23:03:06 +02:00
masterdraco b1f1aaa148 fix(marker): slip kun sæt spilleren selv kører; slået mark uden sæt går videre til opsamling (v1.21.3.0)
In-game 25/9: spilleren satte sig i BiG M'en for at se CP-HUD'en -> sættet blev sluppet, og da CP var
færdig, blev det aldrig sendt hjem; marken ville have sendt slåmaskinen ud igen til en slået mark.
2026-09-25 22:59:31 +02:00
masterdraco 716045a36b fix(marker): arbejdsbredde = Courseplays egen workWidth-indstilling når den findes (v1.21.2.0)
In-game 25/9: variabel bredde gav 20,8 m for BiG M 450, Courseplay-HUD'en viser 40,8 m.
2026-09-25 22:57:48 +02:00
masterdraco bade84129b fix(marker): arbejdsbredde læser variabel bredde (Unrealistic Pack) før AI-markører, som Courseplay (v1.21.1.0)
In-game 25/9: BiG M 450 læst som 10 m og læssevogn som 2,4 m, selv om begge står på 40,8 m.
2026-09-25 22:45:01 +02:00
masterdraco 61a32720ea docs(marker): note — flere maskiner pr. mark, maks 5 (Courseplay multi-tool) 2026-09-25 22:14:22 +02:00
25 changed files with 3226 additions and 586 deletions
+432 -10
View File
@@ -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_baler",
"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,227 @@ 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
if vehicle.getCanStartCpBaleFinder ~= nil then
local ok, can = pcall(vehicle.getCanStartCpBaleFinder, vehicle)
specs.canBaleCollect = 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
-- Halmskår bag mejetærskeren (Courseplays køretøjsindstilling strawSwath: 1 = fra/hakkes, 2 = til).
function ADCpBridge.setStrawSwath(vehicle, on)
return (pcall(function() vehicle:getCpSettings().strawSwath:setValue(on and 2 or 1) end))
end
-- Ballesamler: Courseplays BALE_FINDER_CP (ballevogn eller Universal Autoload) samler markens baller.
function ADCpBridge.startBaleFinder(vehicle, fieldX, fieldZ, onDone)
if AIJobType == nil or AIJobType.BALE_FINDER_CP == nil then
return nil, "Courseplay (BALE_FINDER_CP) findes ikke"
end
local farmId = vehicle:getOwnerFarmId()
local job = g_currentMission.aiJobTypeManager:createJob(AIJobType.BALE_FINDER_CP)
if job == nil then return nil, "kunne ikke oprette ballesamler-job" end
pcall(function() job:applyCurrentState(vehicle, g_currentMission, farmId, false, false) end)
local params = job.cpJobParameters
if params ~= nil and params.fieldPosition ~= nil then pcall(function() params.fieldPosition:setPosition(fieldX, fieldZ) end) end
if params ~= nil and params.startPosition ~= nil then
local vx, _, vz = getWorldTranslation(vehicle.rootNode)
pcall(function() params.startPosition:setPosition(vx, vz) end)
end
pcall(function() job:setValues() end)
local handle = {cancelled = false, fired = false, finished = false}
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
-- 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 +296,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 +405,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 +446,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
+73 -21
View File
@@ -1,48 +1,100 @@
-- 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 = "baler", spec = "spec_baler"},
{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 = "baleCollector", spec = "canBaleCollect"},
{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
-- Må sættet sendes ud? "all" (standard): alle ledige maskiner; "ticked": kun med flueben.
function ADFieldFleet.isEnabled(mode, enabledIds, id)
if mode == "ticked" then return (enabledIds or {})[id] == true end
return true
end
function ADFieldFleet.toggle(enabledIds, id)
+169 -294
View File
@@ -1,51 +1,105 @@
-- 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
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}}
-- 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
-- fleetMode: "all" = alle ledige maskiner (standard, "vælg og glem"), "ticked" = kun med flueben
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}, fleetMode = "all"}
-- 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
-- mål-afgrøde på en bar mark: klargør og så så snart målet kan sås
if info ~= nil and info.kind == "bare" and field.plan.targetFruit ~= nil and field.plan.targetFruit ~= "" then
local plantable = env ~= nil and env.plantable ~= nil and env.plantable(field.plan.targetFruit) or false
if ADFieldModel.shouldStartBare(field, "bare", plantable) then
if not hasMarker then return {action = "wait"} end
return {action = "start"}
end
end
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 +107,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
@@ -74,6 +133,7 @@ function ADFieldJobs.load()
local ok, loaded = pcall(ADFieldStorage.load)
if ok then
ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet
ADFieldJobs.state.fleetMode = loaded.fleetMode or "all"
else
Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded))
end
@@ -81,20 +141,27 @@ 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.setTarget(id, fruitName)
ADFieldJobs.setField(ADFieldModel.setTarget(ADFieldJobs.getField(id), fruitName))
ADFieldJobs.log(id, "mål-afgrøde: %s", fruitName ~= "" and fruitName or "ingen")
ADFieldJobs.save()
end
function ADFieldJobs.setFleetMode(mode)
ADFieldJobs.state.fleetMode = mode
ADFieldJobs.save()
end
function ADFieldJobs.toggleFleet(vehicleId)
@@ -102,286 +169,96 @@ 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,
-- kan afgrøden (navn) sås i den aktuelle periode?
plantable = function(name)
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByName(name) or nil
return fruit ~= nil and ADCpBridge.isPlantableNow(fruit.index)
end,
}
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, info.fruitName)
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 +271,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
+363 -50
View File
@@ -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,38 @@ 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", strawCollecting = "collector", baling = "baler", baleCollecting = "baleCollector",
}
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, targetFruit = "", prepAfterMow = false, straw = "chop",
}
local STRAW_VALUES = {chop = true, collect = true, bale = true}
-- halm-trin efter høst (mejetærskeren lægger skår når halmen skal bruges)
local STRAW_STAGES = {collect = {"strawCollecting"}, bale = {"baling", "baleCollecting"}}
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 +56,179 @@ 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(STRAW_STAGES[plan.straw or "chop"] or {}) do table.insert(stages, stage) end
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")
-- mål-afgrøde på en slåmark med en anden afgrøde: efter sidste slæt klargøres og sås målet
if plan.prepAfterMow then
for _, stage in ipairs(ADFieldModel.prepStages(plan, needs)) do table.insert(stages, stage) end
end
return stages
end
-- Mål-afgrøde ("vælg mark + afgrøde, glem den"): planen udfyldes ud fra hvad der står på marken nu.
-- Står målet der allerede som slåafgrøde (græs -> græs): bare slå igen og igen. Ellers: høst/slå det der
-- står, klargør og så målet. Kører altid automatisk. Uden mål returneres planen uændret.
function ADFieldModel.applyTarget(plan, kind, currentFruit)
local copy = copyPlan(plan)
if copy.targetFruit == nil or copy.targetFruit == "" then return copy end
copy.mode = "auto"
copy.sowFruit = copy.targetFruit
if kind == "mowable" then
copy.prepAfterMow = currentFruit ~= copy.targetFruit
copy.sow = copy.prepAfterMow
else
copy.prepAfterMow = false
copy.sow = true
end
return copy
end
-- Vælg mål-afgrøde fra menuen ("" = ingen). Med mål kører marken automatisk; en færdig/klar mark sættes
-- til venter, så den kan starte forfra. Uden mål: tilbage til manuel.
function ADFieldModel.setTarget(field, fruitName)
local copy = copyField(field)
copy.plan.targetFruit = fruitName or ""
copy.plan.mode = copy.plan.targetFruit ~= "" and "auto" or "manual"
if not ADFieldModel.isWorking(field) and field.stage == "done" then
copy.stage = "idle"
end
return copy
end
-- Bar mark med mål-afgrøde starter selv klargøring + såning, når målet kan sås nu.
function ADFieldModel.shouldStartBare(field, kind, plantableNow)
if field.plan.targetFruit == nil or field.plan.targetFruit == "" then return false end
-- kun fra idle: efter egen såning står marken "done" (nysået kan læses som bar) — ingen dobbelt-såning
if kind ~= "bare" or field.stage ~= "idle" then return false end
return plantableNow == true
end
-- Krævet bredde for et sæt: kun læssevognen uden riv skal dække slåmaskinens baner (halmskår ligger i
-- høsterens spor — der er breddekravet ligegyldigt).
function ADFieldModel.requiredWidth(field, role)
if role == "collector" and not field.plan.rake and field.stage ~= "strawCollecting" 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)
-- halm: læssevogn og presser kører høsterens kursus (skåret ligger i mejetærskerens spor)
if field.stage == "strawCollecting" or role == "baler" then return "harvester" end
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 +247,139 @@ 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.harvestVehicleId = 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.
-- currentFruit (valgfri): afgrøden på marken nu; med mål-afgrøde udfyldes planen ud fra den.
function ADFieldModel.start(field, kind, needs, currentFruit)
local copy = copyField(field)
copy.plan = ADFieldModel.applyTarget(copy.plan, kind, currentFruit)
local stages = ADFieldModel.workStages(copy.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", "strawCollecting", "baling", "baleCollecting", "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, strawCollecting = true, baling = 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 +387,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 +396,89 @@ 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),
targetFruit = plan.targetFruit or "", prepAfterMow = flag(plan.prepAfterMow), straw = plan.straw or "chop",
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 "",
harvestVehicle = field.harvestVehicleId 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)
plan.targetFruit = record.targetFruit or ""
plan.prepAfterMow = readFlag(record.prepAfterMow, false)
plan.straw = STRAW_VALUES[record.straw] and record.straw or "chop"
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.harvestVehicleId = readText(record.harvestVehicle)
field.windrowOffset = tonumber(record.windrowOffset)
return field
end
+30 -17
View File
@@ -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", targetFruit = "String", prepAfterMow = "Int", straw = "String",
mowWidth = "Float", mowVehicle = "String", rakeVehicle = "String", harvestVehicle = "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,17 +43,10 @@ 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
xmlFile:setString(ADFieldStorage.ROOT .. "#fleetMode", state.fleetMode or "all")
local vehicleIndex = 0
for vehicleId in pairs(state.fleet) do
xmlFile:setString(string.format("%s.fleet.vehicle(%d)#id", ADFieldStorage.ROOT, vehicleIndex), vehicleId)
@@ -41,18 +58,14 @@ function ADFieldStorage.save(state)
end
function ADFieldStorage.load()
local result = {fields = {}, fleet = {}}
local result = {fields = {}, fleet = {}, fleetMode = "all"}
local path = ADFieldStorage.getPath()
if path == nil or not fileExists(path) then return result end
local xmlFile = XMLFile.load("ADSmartPickupFields", path)
if xmlFile == nil then return result end
result.fleetMode = xmlFile:getString(ADFieldStorage.ROOT .. "#fleetMode") == "ticked" and "ticked" or "all"
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)
+22 -15
View File
@@ -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
+692
View File
@@ -0,0 +1,692 @@
-- 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 = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
-- 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
if field.stage == "strawCollecting" or field.stage == "baling" or field.stage == "baleCollecting" then
return g_fillTypeManager:getFillTypeIndexByName("STRAW")
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"/"harvester"): 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 vehicleIds = {mower = field.mowVehicleId, rake = field.rakeVehicleId, harvester = field.harvestVehicleId}
local vehicleId = vehicleIds[source]
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
-- Ballesamler: Courseplays ballefinder; fuld vogn tømmes af AutoDrive i nærmeste ballelager (palleskur der
-- tager halmballer), som læssevognen. Uden lager stopper den når den er fuld.
local function beginBaleCollector(adEnv, field, info, assignment)
local vehicle = assignment.vehicle
local farmId = vehicle:getOwnerFarmId()
local straw = g_fillTypeManager:getFillTypeIndexByName("STRAW")
local isReachable = function(markerId) return ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId) end
local store = ADSources.findPalletStore(adEnv, vehicle, straw, nil, isReachable, farmId)
if store ~= nil then
pcall(function()
local module = stateModule(vehicle)
module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
module:setFirstMarkerByWayPointId(info.markerId)
module:setSecondMarkerByWayPointId(store.wayPointId)
module:setStartHelper(true)
module.usedHelper = helperCp(adEnv)
enableParkAtJobFinished(vehicle)
module:raiseDirtyFlag()
end)
assignment.adHandlesEnd = true
log(field.id, "ballesamler '%s' kører fulde læs til %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
else
log(field.id, "intet ballelager med plads og vej fundet — ballesamleren stopper når den er fuld")
end
local point = info.points[1] or {x = info.x, z = info.z}
assignment.phase = "starting"
assignment.since = now()
local handle, err = ADCpBridge.startBaleFinder(vehicle, point.x, point.z, function(ok, reason)
if ok then
assignment.phase = "working"
assignment.since = now()
log(field.id, "Courseplay startet (ballesamler)")
else
ADFieldWork.release(assignment)
fail(field.id, "ballesamler: " .. tostring(reason))
end
end)
if handle == nil then
ADFieldWork.release(assignment)
fail(field.id, "ballesamler: " .. 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
if assignment.role == "baleCollector" then return beginBaleCollector(adEnv, field, info, assignment) end
-- høsteren: halmskår efter planen (hakkes = fra; samles op/presses = skår)
if assignment.role == "harvester" then
local keepStraw = field.plan.straw == "collect" or field.plan.straw == "bale"
ADCpBridge.setStrawSwath(vehicle, keepStraw)
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)
elseif assignment.role == "harvester" then
note(field.id, {harvestVehicleId = assignment.vehicleId})
rememberCourse(field, "harvester", 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
+142 -2
View File
@@ -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
+228 -50
View File
@@ -1,44 +1,104 @@
-- 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,
fieldTarget = true, fleetMode = true, fieldStraw = 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",
baler = "spu_field_role_baler", baleCollector = "spu_field_role_baleCollector",
}
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", fieldStraw = "straw"}
SmartPickupFrame.STRAW_VALUES = {"chop", "collect", "bale"}
-- ---- 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 target = field.plan.targetFruit
if target ~= nil and target ~= "" and info.kind == "bare" and field.stage == "idle" and not ADFieldJobs.environment().plantable(target) then
text = text .. " — " .. string.format(self.i18n:getText("spu_field_waitSowing"), self:fruitTitle(target))
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")
-- Hvad moden gør med marken frem mod mål-afgrøden (én linje under valget).
function SmartPickupFrame:targetSummary(field, info)
local target = self:fruitTitle(field.plan.targetFruit)
if info.kind == "bare" then
return string.format(self.i18n:getText("spu_field_target_bare"), target)
end
if info.fruitName == field.plan.targetFruit and info.kind == "mowable" then
return string.format(self.i18n:getText("spu_field_target_keep"), target)
end
return string.format(self.i18n:getText("spu_field_target_switch"), tostring(info.cropTitle or "-"), target)
end
function SmartPickupFrame:fruitTitle(name)
local fruit = g_fruitTypeManager ~= nil and name ~= nil and name ~= "" and g_fruitTypeManager:getFruitTypeByName(name) or nil
return fruit ~= nil and fruit.fillType ~= nil and tostring(fruit.fillType.title) or tostring(name)
end
function SmartPickupFrame:fieldPlanText(field, info)
if field.plan.targetFruit ~= nil and field.plan.targetFruit ~= "" then
return "-> " .. self:fruitTitle(field.plan.targetFruit)
end
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 +109,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 / ingen mål-afgrøde).
function SmartPickupFrame:sowFruitChoices(firstKey)
local texts, values = {self.i18n:getText(firstKey or "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 +165,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 +188,64 @@ 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))
local targetTexts, targetValues = self:sowFruitChoices("spu_field_target_none")
self:addChoiceRow("fieldTarget", i18n:getText("spu_field_target"), targetTexts, targetValues, plan.targetFruit or "")
local hasTarget = plan.targetFruit ~= nil and plan.targetFruit ~= ""
if hasTarget then
self:addTextRow(" " .. self:targetSummary(field, info))
else
self:addToggleRow("fieldMode", i18n:getText("spu_field_mode"), {i18n:getText("spu_field_mode_manual"), i18n:getText("spu_field_mode_auto")}, plan.mode == "auto")
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)
local rigTexts, rigValues = numberTexts(1, ADFieldModel.MAX_RIGS)
self:addChoiceRow("fieldRigs", i18n:getText("spu_field_rigs"), rigTexts, rigValues, plan.rigs or 1)
-- græs-valgene (ensilage/hø, riv) også når målet er en slåafgrøde (fx græs på en kornmark)
local targetMowable = false
if hasTarget and ADFields ~= nil and g_fruitTypeManager ~= nil then
local targetFruit = g_fruitTypeManager:getFruitTypeByName(plan.targetFruit)
targetMowable = targetFruit ~= nil and ADFields.getMowable()[targetFruit.index] ~= nil
end
if info.kind == "mowable" or targetMowable 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)
end
-- klargøring: høst-/bare marker, og græsmarker der skal skifte til en anden mål-afgrøde
if info.kind ~= "mowable" or (hasTarget and info.fruitName ~= plan.targetFruit) then
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)
local strawTexts = {}
for _, value in ipairs(SmartPickupFrame.STRAW_VALUES) do table.insert(strawTexts, i18n:getText("spu_field_straw_" .. value)) end
self:addChoiceRow("fieldStraw", i18n:getText("spu_field_straw"), strawTexts, SmartPickupFrame.STRAW_VALUES, plan.straw or "chop")
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")
if not hasTarget then
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 "")
end
end
if hasTarget or plan.sow then
self:addToggleRow("fieldRoll", i18n:getText("spu_field_roll"), yesNo, plan.roll == true)
end
end
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 +254,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 +297,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,9 +313,30 @@ 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"))
local useAll = ADFieldJobs.state.fleetMode ~= "ticked"
self:addToggleRow("fleetMode", i18n:getText("spu_field_fleetAll"), {i18n:getText("ui_no"), i18n:getText("ui_yes")}, useAll)
local rigs = ADFieldJobs.getRigs(adEnv)
table.sort(rigs, function(a, b)
if a.role ~= b.role then return a.role < b.role end
@@ -165,14 +344,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 +357,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 +372,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 +381,23 @@ 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 data.kind == "fleetMode" then
ADFieldJobs.setFleetMode(isOn and "all" or "ticked")
elseif id == nil then
return
elseif data.kind == "fieldTarget" then
ADFieldJobs.setTarget(id, data.values ~= nil and data.values[state] or "")
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 +410,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 +419,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
+84 -2
View File
@@ -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 &gt; %.0f m (Courseplay)"/>
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m &lt; slåbredde %.1f m"/>
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m &lt; 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,88 @@
<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_field_target" text="Afgrøde på marken (moden passer resten)"/>
<text name="spu_field_target_none" text="Ingen — planlæg selv"/>
<text name="spu_field_target_bare" text="Klargøres og tilsås med %s, så snart det er såtid"/>
<text name="spu_field_target_keep" text="%s står allerede: slås og samles op, hver gang den er klar"/>
<text name="spu_field_target_switch" text="%s får lov at vokse færdig og høstes/slås; derefter klargøres og tilsås med %s"/>
<text name="spu_field_waitSowing" text="venter på såtid for %s"/>
<text name="spu_field_fleetAll" text="Brug alle ledige maskiner (Nej = kun med flueben)"/>
<text name="spu_field_role_baler" text="ballepresser"/>
<text name="spu_field_role_baleCollector" text="ballesamler"/>
<text name="spu_field_stage_strawCollecting" text="Halm samles"/>
<text name="spu_field_stage_baling" text="Presses"/>
<text name="spu_field_stage_baleCollecting" text="Baller samles"/>
<text name="spu_field_straw" text="Halm efter høst"/>
<text name="spu_field_straw_chop" text="Hakkes (spredes på marken)"/>
<text name="spu_field_straw_collect" text="Samles op til silo"/>
<text name="spu_field_straw_bale" text="Presses til baller og samles"/>
<text name="spu_button_start" text="Start run"/>
<text name="spu_button_stop" text="Stop run"/>
<text name="spu_button_new" text="Nyt run"/>
+84 -2
View File
@@ -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 &gt; %.0f m (Courseplay)"/>
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m &lt; mowing width %.1f m"/>
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m &lt; 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,88 @@
<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_field_target" text="Crop on the field (the mod handles the rest)"/>
<text name="spu_field_target_none" text="None — plan it yourself"/>
<text name="spu_field_target_bare" text="Prepared and sown with %s as soon as it is sowing time"/>
<text name="spu_field_target_keep" text="%s is already there: mowed and collected every time it is ready"/>
<text name="spu_field_target_switch" text="%s is left to finish growing and is harvested/mowed; then the field is prepared and sown with %s"/>
<text name="spu_field_waitSowing" text="waiting for sowing time for %s"/>
<text name="spu_field_fleetAll" text="Use all free machines (No = only ticked ones)"/>
<text name="spu_field_role_baler" text="baler"/>
<text name="spu_field_role_baleCollector" text="bale collector"/>
<text name="spu_field_stage_strawCollecting" text="Collecting straw"/>
<text name="spu_field_stage_baling" text="Baling"/>
<text name="spu_field_stage_baleCollecting" text="Collecting bales"/>
<text name="spu_field_straw" text="Straw after harvest"/>
<text name="spu_field_straw_chop" text="Chopped (spread on the field)"/>
<text name="spu_field_straw_collect" text="Collected to a silo"/>
<text name="spu_field_straw_bale" text="Baled and collected"/>
<text name="spu_button_start" text="Start run"/>
<text name="spu_button_stop" text="Stop run"/>
<text name="spu_button_new" text="New run"/>
+2 -1
View File
@@ -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.27.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>
+84 -50
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — brugermanual
Version 1.21.0 · Farming Simulator 25 · tillæg til AutoDrive
Version 1.27.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,111 @@ 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 |
### Vælg og glem: afgrøde på marken
Det nemmeste: vælg **Afgrøde på marken** (fx *Hvede*) øverst under marken. Så passer moden resten:
- **Vokser der noget nu**, får det lov at vokse færdig, bliver passet (gødning, ukrudt, kalk) og høstet
eller slået, når det er klart.
- **Bagefter** klargøres marken og tilsås med din afgrøde. Er det ikke såtid, venter marken ("venter på
såtid for …").
- **Står din afgrøde der allerede** (græs → græs), bliver den bare slået og samlet op, hver gang den er klar.
- **Bar mark:** klargøres og tilsås, så snart det er såtid.
- Derefter kører cyklussen af sig selv: vækst → pleje → høst → klargøring → såning → …
Med en afgrøde valgt kører marken automatisk. Linjen under valget fortæller, hvad moden vil gøre. Du kan
stadig vælge ensilage/hø, halm, antal maskiner, jordbehandling og pleje. Vælg *Ingen — planlæg selv* for
at styre marken manuelt som før.
### 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. Med
**Brug alle ledige maskiner** = Ja (standard) finder moden selv maskinerne; med Nej bruges kun dem med
flueben. 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 |
| **Halm efter høst** (høst) | Hakkes (spredes), Samles op til silo, eller Presses til baller, som en ballesamler kører til et ballelager |
| **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
+175
View File
@@ -0,0 +1,175 @@
# Testplan — Markarbejde (fanen Marker), version 1.27.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.27.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.6 Halm samles op.** Halm efter høst = *Samles op til silo*. Mejetærskeren lægger skår
(Courseplay-indstilling halmskår = til), derefter `harvesting -> strawCollecting`, en læssevogn kører
**høsterens kursus** (`harvester-kursus kopieret`) og kører halmen til en silo der tager STRAW.
- [ ] **3.7 Halm til baller.** Halm = *Presses til baller*: `-> baling` (ballepresser på høsterens
kursus) → `-> baleCollecting` (ballesamler/Universal Autoload, `Courseplay startet (ballesamler)`),
fulde læs til ballelager (`ballesamler … kører fulde læs til …`).
- [ ] **3.8 Halm hakkes.** Halm = *Hakkes*: mejetærskeren lægger IKKE skår (Courseplay halmskår = fra).
- [ ] **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.
## 9. Vælg og glem (afgrøde på marken)
- [ ] **9.1 Kornmark → hvede.** En mark med byg der vokser: vælg *Afgrøde på marken* = Hvede. Linjen siger
"Byg får lov at vokse færdig og høstes; derefter klargøres og tilsås med Hvede". Kørsel skjules (auto).
Når byggen er moden: høst → (halm) → klargøring → såning af **hvede** (ikke byg). Log:
`mål-afgrøde: WHEAT`, `starter (harvest: harvesting)`, `sowing`.
- [ ] **9.2 Bar mark.** Bar mark med mål = Hvede: starter selv klargøring + såning, når det er hvede-såtid.
Uden for såtid: status "venter på såtid for Hvede".
- [ ] **9.3 Græs → græs.** Græsmark med mål = Græs: slås og samles op hver gang, ingen pløjning/såning.
- [ ] **9.4 Græs → hvede.** Græsmark med mål = Hvede: slås og samles op én gang, derefter klargøring
(jordbehandling-valget vises) og såning af hvede.
- [ ] **9.5 Hele året.** Lad en mark med mål køre en hel sæson uden at røre den: vækst → pleje → høst →
klargøring → såning → ... uden at hænge og uden dobbelt-såning.
- [ ] **9.6 Alle ledige maskiner.** Markflåde: *Brug alle ledige maskiner* = Ja (standard): maskiner uden
flueben sendes også ud. = Nej: kun maskiner med flueben. Maskiner du selv kører, maskiner i flows og
maskiner AutoDrive/Courseplay allerede kører, bruges aldrig.
- [ ] **9.7 Fjern målet.** Vælg *Ingen — planlæg selv*: marken går tilbage til Manuel.
## 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 |
| Nysået som "bar" | nysået afgrøde læses som voksende | Marken står "Færdig" efter såning (ingen dobbelt-såning, men ingen næste høst-start før den er moden) |
| Ballesamler | Courseplays BALE_FINDER + AutoDrive til ballelager | Ballesamleren starter ikke, eller kører ikke fulde læs væk |
+84 -51
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — user manual
Version 1.21.0 · Farming Simulator 25 · add-on for AutoDrive
Version 1.27.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,112 @@ 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 |
### Select and forget: crop on the field
The easiest way: pick **Crop on the field** (such as *Wheat*) at the top of the field. The mod does the rest:
- **Whatever grows now** is left to finish, cared for (fertilizer, weeds, lime) and harvested or mowed when
ready.
- **Afterwards** the field is prepared and sown with your crop. If it is not sowing time, the field waits
("waiting for sowing time for …").
- **If your crop is already there** (grass → grass), it is simply mowed and collected every time it is ready.
- **Bare field:** prepared and sown as soon as it is sowing time.
- After that the cycle runs by itself: growth → care → harvest → preparation → sowing → …
With a crop chosen, the field runs automatically. The line below the choice tells you what the mod will do.
You can still choose silage/hay, straw, number of machines, soil work and care. Choose *None — plan it
yourself* to run the field manually as before.
### 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. With
**Use all free machines** = Yes (default) the mod finds the machines itself; with No only ticked ones
are used. 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 |
| **Straw after harvest** (harvest) | Chopped (spread), Collected to a silo, or Baled, and a bale collector takes the bales to a bale store |
| **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 |
+90
View File
@@ -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)
+46 -1
View File
@@ -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,50 @@ 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")
-- FL11: markflådens tilstand: alle ledige maskiner (standard) eller kun med flueben
check("FL11 alle: uden flueben med", F.isEnabled("all", {}, "v1"), true)
check("FL11 flueben: uden flueben ude", F.isEnabled("ticked", {}, "v1"), false)
check("FL11 flueben: med flueben", F.isEnabled("ticked", {v1 = true}, "v1"), true)
check("FL11 ukendt tilstand = alle", F.isEnabled(nil, {}, "v1"), true)
-- FL12: ballepresser og ballesamler (ballevogn/Universal Autoload som Courseplays ballesamler kan bruge)
check("FL12 ballepresser", F.roleFromSpecs({spec_baler = true}), "baler")
check("FL12 ballesamler", F.roleFromSpecs({canBaleCollect = true}), "baleCollector")
check("FL12 ballesamler før aflæsningsvogn", F.roleFromSpecs({canBaleCollect = true, canUnload = true}), "baleCollector")
check("FL12 høster før presser", F.roleFromSpecs({spec_combine = true, spec_baler = true}), "harvester")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+60
View File
@@ -38,5 +38,65 @@ 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")
-- J12: bar mark med mål-afgrøde starter selv når målet kan sås; ellers venter den
local bareInfo = {id = 90, markerId = 9, kind = "bare", readyRatio = 0, needs = {}}
local bareTarget = M.with(M.newField(90), {plan = {targetFruit = "WHEAT", mode = "auto"}})
local canSow = {plantable = function(name) return name == "WHEAT" end}
check("J12 bar + såtid -> start", J.decide(bareTarget, bareInfo, {}, 0, canSow).action, "start")
check("J12 bar + ikke såtid -> intet", J.decide(bareTarget, bareInfo, {}, 0, {plantable = function() return false end}).action, "none")
check("J12 bar uden markør -> vent", J.decide(bareTarget, {id = 90, kind = "bare"}, {}, 0, canSow).action, "wait")
check("J12 voksende mark med mål -> lad den vokse", J.decide(bareTarget, {id = 90, markerId = 9, kind = "harvest", growing = true, readyRatio = 0.3}, {}, 0, canSow).action, "none")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+196 -6
View File
@@ -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,194 @@ 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)
-- M19: mål-afgrøde — "vælg mark + afgrøde, glem den"
local target = M.with(M.newField(80), {plan = {targetFruit = "WHEAT"}})
local wheatNow = M.applyTarget(target.plan, "harvest", "BARLEY")
check("M19 mål -> auto", wheatNow.mode, "auto")
check("M19 høst byg -> så hvede", wheatNow.sow, true)
check("M19 såafgrøde = mål", wheatNow.sowFruit, "WHEAT")
check("M19 kæde: høst -> kultivér -> så", table.concat(M.workStages(wheatNow, "harvest", {}), ","), "harvesting,cultivating,sowing")
local grassToWheat = M.applyTarget(target.plan, "mowable", "GRASS")
check("M19 græs -> hvede: slå, saml op, så klargør og så", table.concat(M.workStages(grassToWheat, "mowable", {}), ","), "mowing,collecting,cultivating,sowing")
local grassTarget = M.applyTarget(M.with(M.newField(81), {plan = {targetFruit = "GRASS"}}).plan, "mowable", "GRASS")
check("M19 græs -> græs: kun slå og saml op", table.concat(M.workStages(grassTarget, "mowable", {}), ","), "mowing,collecting")
check("M19 græs -> græs: ingen såning", grassTarget.sow, false)
local bareTarget = M.applyTarget(target.plan, "bare", nil)
check("M19 bar mark -> klargør og så", table.concat(M.workStages(bareTarget, "bare", {plow = true}), ","), "plowing,sowing")
check("M19 uden mål: plan urørt", M.applyTarget(M.newField(82).plan, "harvest", "BARLEY").mode, "manual")
local started = M.start(target, "harvest", {}, "BARLEY")
check("M19 start anvender målet", started.plan.sowFruit, "WHEAT")
check("M19 start: høst først", started.stage, "harvesting")
check("M19 målet gemmes", M.fromRecord(M.toRecord(started)).plan.targetFruit, "WHEAT")
check("M19 klargøring efter slåning gemmes", M.fromRecord(M.toRecord(M.start(target, "mowable", {}, "GRASS"))).plan.prepAfterMow, true)
-- M20: skal en bar mark med mål starte? (kun når målafgrøden kan sås nu)
check("M20 bar + sås nu -> start", M.shouldStartBare(target, "bare", true), true)
check("M20 bar + ikke såtid -> vent", M.shouldStartBare(target, "bare", false), false)
check("M20 uden mål -> ingen auto-start", M.shouldStartBare(M.newField(83), "bare", true), false)
check("M20 voksende mark -> nej", M.shouldStartBare(target, "harvest", true), false)
check("M20 arbejdende mark -> nej", M.shouldStartBare(started, "bare", true), false)
check("M20 færdig (lige sået) -> nej, ingen dobbelt-såning", M.shouldStartBare(M.with(target, {stage = "done"}), "bare", true), false)
-- M21: vælg mål-afgrøde fra menuen: auto, og en færdig mark nulstilles så den kan starte
local chosen = M.setTarget(M.with(M.newField(84), {stage = "done"}), "CANOLA")
check("M21 mål sat", chosen.plan.targetFruit, "CANOLA"); check("M21 auto", chosen.plan.mode, "auto")
check("M21 færdig -> venter", chosen.stage, "idle")
check("M21 arbejdende mark beholder trin", M.setTarget(started, "CANOLA").stage, "harvesting")
local cleared = M.setTarget(chosen, "")
check("M21 mål fjernet", cleared.plan.targetFruit, ""); check("M21 tilbage til manuel", cleared.plan.mode, "manual")
-- M22: halm efter høst — hakkes (standard), samles op, eller presses til baller og samles
local strawBase = {soil = "none", sow = false, lime = false}
check("M22 hakkes: kun høst", table.concat(M.workStages(strawBase, "harvest", {}), ","), "harvesting")
check("M22 samles op", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "collect"}}).plan, "harvest", {}), ","), "harvesting,strawCollecting")
check("M22 baller", table.concat(M.workStages(M.with(M.newField(1), {plan = {soil = "none", lime = false, straw = "bale"}}).plan, "harvest", {}), ","), "harvesting,baling,baleCollecting")
local baleChain = M.start(M.with(M.newField(2), {plan = {straw = "bale", soil = "cultivate", lime = false}}), "harvest", {})
check("M22 høst -> presses", M.advance(baleChain, {}).stage, "baling")
check("M22 presses -> baller samles", M.advance(M.advance(baleChain, {}), {}).stage, "baleCollecting")
check("M22 -> kultivér", M.advance(M.advance(M.advance(baleChain, {}), {}), {}).stage, "cultivating")
check("M22 roller", M.roleFor("baling") .. "," .. M.roleFor("baleCollecting") .. "," .. M.roleFor("strawCollecting"), "baler,baleCollector,collector")
check("M22 halm-læssevogn følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "strawCollecting"}), "collector"), "harvester")
check("M22 presser følger høsterens kursus", M.courseSource(M.with(M.newField(3), {stage = "baling"}), "baler"), "harvester")
check("M22 græs-læssevogn følger stadig slåkurset", M.courseSource(M.with(M.newField(3), {stage = "collecting"}), "collector"), "mower")
check("M22 halm-læssevogn uden breddekrav", M.requiredWidth(M.with(M.newField(3), {stage = "strawCollecting", mowWidth = 40}), "collector"), nil)
check("M22 presning vejrfølsom", M.isWeatherSensitive("baling", {}, "harvest"), true)
check("M22 ballesamling ikke vejrfølsom", M.isWeatherSensitive("baleCollecting", {}, "harvest"), false)
check("M22 gemmes", M.fromRecord(M.toRecord(M.with(M.newField(4), {plan = {straw = "bale"}}))).plan.straw, "bale")
check("M22 ukendt -> hakkes", M.fromRecord({id = 5, straw = "x"}).plan.straw, "chop")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
+42
View File
@@ -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
View File
@@ -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)
+32
View File
@@ -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)