-- 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