Files
fs25-adsmartpickup/FS25_ADSmartPickup/adCpBridge.lua
T

665 lines
31 KiB
Lua

-- AD Courseplay Bridge
-- Den eneste kode der rører Courseplay: start markarbejde på en mark (grænse -> kursus -> start, alt
-- async), og læs om et sæt arbejder. Opskriften er bevist i spillet i AI-Bonden (AIHelperGlue.startCP):
-- CP kalder grænse-callbacken to gange (først false ved fejl) — kun første kald tæller.
ADCpBridge = {}
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
end
local function collect(vehicle, list)
list = list or {}
table.insert(list, vehicle)
if vehicle.getAttachedImplements ~= nil then
for _, implement in ipairs(vehicle:getAttachedImplements()) do
if implement.object ~= nil then collect(implement.object, list) end
end
end
return list
end
-- Hvilke varer der hører til en rolle (påfyldning af en tom spreder/sprøjte), i foretrukken rækkefølge.
ADCpBridge.ROLE_FILLTYPES = {
fertilizer = {"FERTILIZER", "LIQUIDFERTILIZER", "MANURE", "LIQUIDMANURE", "DIGESTATE"},
limer = {"LIME"},
weeder = {"HERBICIDE"},
}
ADCpBridge.ROLE_ORDER = {"fertilizer", "limer", "weeder"}
-- Sprøjtens/sprederens roller: alle roller dens fyldenhed kan tage (Bredal-spreder: gødning OG kalk).
-- Primær rolle = fyldets rolle (har den kalk på, er den kalkspreder), ellers den første den kan tage.
-- Returnerer primær, sæt {[rolle] = true}, fyldets rolle (nil = tom); nil hvis ingen roller.
function ADCpBridge.sprayRolesFor(filledName, supportedNames)
local roles, first = {}, nil
for _, role in ipairs(ADCpBridge.ROLE_ORDER) do
for _, name in ipairs(ADCpBridge.ROLE_FILLTYPES[role]) do
if supportedNames[name] then
roles[role] = true
first = first or role
break
end
end
end
local filledRole = filledName ~= nil and ADCpBridge.sprayRoleForFillType(filledName) or nil
if filledRole ~= nil then roles[filledRole] = true end
local primary = filledRole or first
return primary, primary ~= nil and roles or nil, filledRole
end
local function sprayerNames(object)
local unitIndex = object:getSprayerFillUnitIndex()
local filledName = nil
local level = object:getFillUnitFillLevel(unitIndex) or 0
local capacity = object.getFillUnitCapacity ~= nil and (object:getFillUnitCapacity(unitIndex) or 0) or 0
if level > 0 then
filledName = g_fillTypeManager:getFillTypeNameByIndex(object:getFillUnitFillType(unitIndex))
end
local supported = {}
local unit = object.spec_fillUnit ~= nil and object.spec_fillUnit.fillUnits ~= nil and object.spec_fillUnit.fillUnits[unitIndex] or nil
for fillType, isSupported in pairs(unit ~= nil and unit.supportedFillTypes or {}) do
if isSupported then
local name = g_fillTypeManager:getFillTypeNameByIndex(fillType)
if name ~= nil then supported[name] = true end
end
end
return filledName, supported, level, capacity
end
local function sprayRole(object)
if object.spec_sprayer == nil or object.getSprayerFillUnitIndex == nil then return nil end
local ok, role, roles, filledRole, wrongFillDumpable = pcall(function()
local filledName, supported, level, capacity = sprayerNames(object)
local r, rs, fr = ADCpBridge.sprayRolesFor(filledName, supported)
return r, rs, fr, ADCpBridge.isDumpable(level, capacity)
end)
if not ok then return nil end
return role, roles, filledRole, wrongFillDumpable
end
-- Må en forkert vare hældes ud? Kun en lille rest: højst 10 % af tanken og aldrig over 2.000 l
-- (in-game 26/9: 100.000 l flydende gødning og 4.000 l sprøjtemiddel blev hældt ud for at skifte opgave).
ADCpBridge.MAX_DUMP_LITERS = 2000
ADCpBridge.MAX_DUMP_SHARE = 0.10
function ADCpBridge.isDumpable(level, capacity)
level = level or 0
if level <= 0 then return true end
if level > ADCpBridge.MAX_DUMP_LITERS then return false end
return capacity == nil or capacity <= 0 or level <= capacity * ADCpBridge.MAX_DUMP_SHARE
end
-- Tøm sprøjten/sprederen for en vare der ikke hører til opgaven (kalk på, skal gøde). FS25 kan ikke
-- tippe gødning/kalk af, så resten kasseres. Returnerer liter og varenavn (0 hvis intet blev tømt).
function ADCpBridge.emptySprayerIfWrong(vehicle, role)
local liters, name = 0, nil
for _, object in ipairs(collect(vehicle)) do
if object.spec_sprayer ~= nil and object.getSprayerFillUnitIndex ~= nil then
pcall(function()
local filledName, _, level, capacity = sprayerNames(object)
if filledName == nil or ADCpBridge.sprayRoleForFillType(filledName) == role then return end
if not ADCpBridge.isDumpable(level, capacity) then return end
local unit = object:getSprayerFillUnitIndex()
object:addFillUnitFillLevel(object:getOwnerFarmId(), unit, -level, object:getFillUnitFillType(unit), ToolType.UNDEFINED, nil)
liters, name = liters + level, filledName
end)
end
end
return liters, name
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
if specs.sprayRole == nil then specs.sprayRole, specs.sprayRoles, specs.filledRole, specs.dumpable = sprayRole(object) end
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
-- fyld; en TOM spreder får rollens vare (gødning ved gødskning, kalk ved kalkning), hvis dens fyldenhed
-- kan tage den. nil = intet at fylde på (strigle, plov ...).
function ADCpBridge.getRefillFillType(vehicle, role)
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()
local filledName, supported = sprayerNames(object)
if filledName ~= nil and (role == nil or ADCpBridge.sprayRoleForFillType(filledName) == role) then
return g_fillTypeManager:getFillTypeIndexByName(filledName)
end
for _, name in ipairs(ADCpBridge.ROLE_FILLTYPES[role] or {}) do
if supported[name] then return g_fillTypeManager:getFillTypeIndexByName(name) end
end
return nil
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
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
end
end
return best
end
function ADCpBridge.isWorking(vehicle)
if vehicle.getIsCpFieldWorkActive ~= nil and vehicle:getIsCpFieldWorkActive() then return true end
return vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() or false
end
-- 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)
-- reuse = start på køretøjets eget nuværende kursus uden ny beregning (ADCourseCache fandt det passende)
-- onDone(ok, reason) kaldes én gang; reason "copied"/"reused" ved succes med kopieret/genbrugt kursus.
-- Vinmarker (druer/oliven): Courseplays kursus ligger som standard PÅ vinen — en sprøjte/spreder/harve kører
-- så ind i stolperne ("Blocked by an object", in-game 26/9 F22). Alt der arbejder i gangene køres til
-- venstre for vinen; høsteren kører hen over rækken. nil = ikke en vinmark (rør intet).
ADCpBridge.VINE_FRUITS = {GRAPE = true, GRAPEB = true, OLIVE = true, OLIVEB = true}
function ADCpBridge.vineOffsetFor(fruitName, role)
if fruitName == nil or not ADCpBridge.VINE_FRUITS[string.upper(tostring(fruitName))] then return nil end
if role == "harvester" then return 0 end
return -1
end
-- Vinrækker der springes over: sprøjter/spredere arbejder på begge sider af gangen på én gang, så de kører kun
-- i hver anden gang (1). Andre redskaber (harve, mulcher) arbejder mod én side (0). nil = ikke en vinmark.
ADCpBridge.VINE_BOTH_SIDES = {fertilizer = true, weeder = true, limer = true}
function ADCpBridge.vineSkipFor(fruitName, role)
if ADCpBridge.vineOffsetFor(fruitName, role) == nil then return nil end
return ADCpBridge.VINE_BOTH_SIDES[role] and 1 or 0
end
function ADCpBridge.setVineSkip(vehicle, value)
if vehicle == nil or vehicle.getCpVineSettings == nil or value == nil then return false end
return (pcall(function() vehicle:getCpVineSettings().vineRowsToSkip:setValue(value) end))
end
function ADCpBridge.setVineOffset(vehicle, value)
if vehicle == nil or vehicle.getCpVineSettings == nil or value == nil then return false end
return (pcall(function() vehicle:getCpVineSettings().vineCenterOffset:setValue(value) end))
end
function ADCpBridge.start(vehicle, fieldX, fieldZ, onDone, opts)
opts = opts or {}
if not ADCpBridge.isAvailable() then
return nil, "Courseplay (FIELDWORK_CP) findes ikke"
end
local farmId = vehicle:getOwnerFarmId()
local job = g_currentMission.aiJobTypeManager:createJob(AIJobType.FIELDWORK_CP)
if job == nil then return nil, "kunne ikke oprette Courseplay-job" end
job:applyCurrentState(vehicle, g_currentMission, farmId, false, false)
local vx, _, vz = getWorldTranslation(vehicle.rootNode)
local params = job.cpJobParameters
if params ~= nil and params.fieldPosition ~= nil then params.fieldPosition:setPosition(fieldX, fieldZ) end
if params ~= nil and params.startPosition ~= nil then params.startPosition:setPosition(vx, vz) end
-- startpunkt i kurset: CP's standard er "first" (forfra) — "nearest"/"last" på en påbegyndt mark
local startAtValues = CpFieldWorkJobParameters ~= nil and {first = CpFieldWorkJobParameters.START_AT_FIRST_POINT,
nearest = CpFieldWorkJobParameters.START_AT_NEAREST_POINT, last = CpFieldWorkJobParameters.START_AT_LAST_POINT} or {}
if opts.startAt ~= nil and startAtValues[opts.startAt] ~= nil and params ~= nil and params.startAt ~= nil then
pcall(function() params.startAt:setValue(startAtValues[opts.startAt]) end)
end
job:setValues()
local handle = {cancelled = false, fired = false, finished = false}
local function finish(ok, reason)
if handle.finished then return end
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 (handle.reusedCourse and "reused" or nil))
end
local function generateAndStart()
if handle.cancelled then return end
-- genbrug: maskinens eget kursus (CP's validate kræver kun hasCpCourse) — ingen ny beregning
if opts.reuse and ADCpBridge.getCourse(vehicle) ~= nil then
handle.reusedCourse = true
return startWithCourse()
end
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)
-- kursusberegningen kører over mange frames på store marker: moden venter længere imens
handle.generating = true
job:onClickGenerateFieldWorkCourse(function(generated)
handle.generating = false
if handle.cancelled then return end
if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end
startWithCourse()
end)
end
handle.onBoundary = function(_, success, message)
if handle.fired then return end
handle.fired = true
if handle.cancelled then return end
if not success then return finish(false, "Courseplay fandt ikke markgrænsen: " .. tostring(message)) end
generateAndStart()
end
job:registerFieldBoundaryDetectionCallback(handle, handle.onBoundary)
local isValid, isRunning, message = job:detectFieldBoundary()
if isValid and not isRunning then
generateAndStart()
elseif not isRunning then
return nil, "Courseplay: " .. tostring(message)
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
-- positioner med retning: CP's applyCurrentState sætter retningen, men fejler den (pcall ovenfor) bliver den
-- aldrig sat, og spillet afviser jobbet med "Angle missing" (in-game 27/9 F15, hvert 5. min i en time)
pcall(function()
local dirX, _, dirZ = localDirectionToWorld(vehicle.rootNode, 0, 0, 1)
local angle = MathUtil.getYRotationFromDirection(dirX, dirZ)
for _, position in ipairs({params.startPosition, params.fieldUnloadPosition}) do
if position ~= nil and position.getAngle ~= nil and position:getAngle() == nil then
if position.getPosition ~= nil and select(1, position:getPosition()) == nil then
local vx, _, vz = getWorldTranslation(vehicle.rootNode)
position:setPosition(vx, vz)
end
position:setAngle(angle)
end
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