-- 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 -- Vinhøster (fx Grégoire GL86): afgrøden vælges i konfigurationen og står i spec_vineCutter, ikke i spec_cutter -- (in-game 29/9: F22 hvide druer "none can do it" — moddet så aldrig vinhøstere som høstere). function ADCpBridge.vineFruitSet(spec) local fruits = {} if spec == nil then return fruits end for _, key in ipairs({"inputFruitTypeIndex", "fruitTypeIndex", "fruitType"}) do if type(spec[key]) == "number" then fruits[spec[key]] = true end end if type(spec.fruitTypes) == "table" then for key, value in pairs(spec.fruitTypes) do if type(value) == "number" then fruits[value] = true elseif value == true and type(key) == "number" then fruits[key] = true end end end return fruits end local vineLogged = setmetatable({}, {__mode = "k"}) -- Afgrøder høsteren kan tage (alle skærebord/optagere/vinhøstere 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 if object.spec_vineCutter ~= nil then local vine = ADCpBridge.vineFruitSet(object.spec_vineCutter) for fruit in pairs(vine) do fruits[fruit] = true end if next(vine) == nil and not vineLogged[object] then -- ukendt feltnavn i denne spilversion: vis nøglerne, så reglen kan rettes vineLogged[object] = true local keys = {} for key, value in pairs(object.spec_vineCutter) do table.insert(keys, tostring(key) .. "=" .. type(value)) end table.sort(keys) Logging.info("ADSmartPickup: vinhøster uden afgrøde fundet — spec_vineCutter: %s", table.concat(keys, ", ")) end 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 -- Vinhøster uden AI-markører: bredden er vinrækken (3 m = Courseplays vin-gitter for druer). ADCpBridge.VINE_ROW_WIDTH = 3 function ADCpBridge.widthOrVine(width, hasVineCutter) -- under markarbejdets minimum (1,5 m) — også en lille bredde fra Courseplay (in-game 29/9: stadig widthUnknown) if hasVineCutter and (width or 0) < 1.5 then return ADCpBridge.VINE_ROW_WIDTH end return width end function ADCpBridge.getWorkWidth(vehicle) local hasVine = false for _, object in ipairs(collect(vehicle)) do if object.spec_vineCutter ~= nil then hasVine = true end end local cpWidth = courseplayWidth(vehicle) if cpWidth ~= nil then return ADCpBridge.widthOrVine(cpWidth, hasVine) 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 ADCpBridge.widthOrVine(best, hasVine) end -- AutoDrive kører også som AI-job i FS25: aktiv AD er ikke Courseplay (in-game 29/9: Milk Store ved Cow Wait -- med AD aktiv blev meldt 'Courseplay kører' og ventede på sig selv). function ADCpBridge.isWorking(vehicle) if vehicle.getIsCpFieldWorkActive ~= nil and vehicle:getIsCpFieldWorkActive() then return true end local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil if module ~= nil and module.isActive ~= nil and module:isActive() then return false 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 -- Forager: nye kurser får altid HEADLANDS runder og starter på forageren, så der ikke køres ind i træer, hegn -- o.l. i hjørnerne (brugerkrav 29/9). Kan kurset ikke laves med forager (smal mark), prøves én gang uden. ADCpBridge.HEADLANDS = 2 function ADCpBridge.setHeadlands(vehicle, n) if vehicle.getCourseGeneratorSettings == nil then return false end return (pcall(function() local settings = vehicle:getCourseGeneratorSettings() settings.numberOfHeadlands:setValue(n) if settings.startOnHeadland ~= nil then settings.startOnHeadland:setValue(n > 0) end end)) end -- Har kurset forager? (ukendt API = antag ja, så intet genberegnes unødigt) function ADCpBridge.courseHasHeadlands(course) if course == nil then return false end if course.getNumberOfHeadlands == nil then return true end local ok, n = pcall(course.getNumberOfHeadlands, course) return not ok or type(n) ~= "number" or n > 0 end -- Kursusbredde til Courseplay: højst 50 m (CP's grænse; Unrealistic-redskaber kan sættes bredere). nil = urørt. ADCpBridge.CP_MAX_WIDTH = 50 function ADCpBridge.cpCourseWidth(width) if type(width) == "number" and width > ADCpBridge.CP_MAX_WIDTH then return ADCpBridge.CP_MAX_WIDTH end return nil end local function clampCourseWidth(vehicle) pcall(function() local setting = vehicle:getCourseGeneratorSettings().workWidth local capped = ADCpBridge.cpCourseWidth(setting:getValue()) if capped ~= nil then setting:setValue(capped) Logging.info("ADSmartPickup: kursusbredde for '%s' begrænset til %d m (Courseplays maksimum)", tostring(vehicle.getName ~= nil and vehicle:getName() or "?"), capped) end end) 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, HOPS = 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 -- eget kursus genbruges kun med forager (ellers nyt kursus med 2 runder) if opts.reuse and ADCpBridge.getCourse(vehicle) ~= nil and ADCpBridge.courseHasHeadlands(ADCpBridge.getCourse(vehicle)) 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) clampCourseWidth(vehicle) -- kursusberegningen kører over mange frames på store marker: moden venter længere imens local function generate(headlands) ADCpBridge.setHeadlands(vehicle, headlands) handle.generating = true job:onClickGenerateFieldWorkCourse(function(generated) handle.generating = false if handle.cancelled then return end if not generated and headlands > 0 then Logging.info("ADSmartPickup: kursus med %d forager-runder kunne ikke laves — prøver uden forager", headlands) return generate(0) end if not generated then return finish(false, "Courseplay kunne ikke lave kursus") end startWithCourse() end) end generate(ADCpBridge.HEADLANDS) 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