Files
fs25-adsmartpickup/FS25_ADSmartPickup/adFieldWork.lua
T

840 lines
42 KiB
Lua

-- 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
-- AutoDrive der stopper før markøren startes igen så mange gange, før turen opgives
ADFieldWork.DRIVE_RETRIES = 2
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
-- fillType (valgfri, LOAD): varen AutoDrive skal læsse — uden den kører AD til påfyldningen og hjem tom
-- (in-game 26/9: sprøjten kom tom tilbage fra Buying Station Multifruit)
local function startAd(adEnv, vehicle, mode, firstId, secondId, fillType)
local module = stateModule(vehicle)
stopAd(vehicle)
module:setMode(mode)
module:setFirstMarkerByWayPointId(firstId)
if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end
if fillType ~= nil then
module:setFillType(fillType)
module.selectedFillTypes = {fillType}
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)
-- traktorer i et almindeligt flow (æg, gylle, foder ...) hører til den drift og er ikke markflåde
if role ~= nil and id ~= nil and ADRunsController.getRunForVehicle(vehicle) == 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, 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 isAdActive(vehicle) or ADCpBridge.isWorking(vehicle),
controlled = isControlledByPlayer(vehicle),
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
-- tom spreder kan flere ting (gødning og kalk): alle dens roller
roles = specs.sprayRoles, filledRole = specs.filledRole, dumpable = specs.dumpable,
}
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
ADFieldWork.REFILL_ROLES = {seeder = true, fertilizer = true, limer = true, weeder = true}
local function fillTypeName(fillType)
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
end
-- Før udsendelse: forkert vare af (kalk på, skal gøde), og er sættet tomt, findes påfyldningen.
-- Returnerer {fillType, empty, refillId} eller nil (sættet bruger intet fyld: strigle, plov ...).
local function refillPlan(adEnv, fieldId, vehicle, role, x, z)
if not ADFieldWork.REFILL_ROLES[role] then return nil end
local dumped, dumpedName = ADCpBridge.emptySprayerIfWrong(vehicle, role)
if dumped > 0 then
log(fieldId, "'%s' tømt for %.0f l %s (skal bruges til %s)", ADRuns.vehicleLabel(vehicle), dumped, tostring(dumpedName), tostring(role))
end
local fillType = ADCpBridge.getRefillFillType(vehicle, role)
if fillType == nil then return nil end
local empty = ADCpBridge.getRefillLevel(vehicle, fillType) <= 0
local refillId = empty and ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z) or nil
return {fillType = fillType, empty = empty, refillId = refillId}
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
-- faste maskiner: andre markers faste bruges ikke; har marken selv faste med rollen, kun dem
local rigIdsByField = {}
for fieldId, other in pairs(ADFieldJobs.state.fields) do rigIdsByField[fieldId] = other.plan ~= nil and other.plan.rigIds or nil end
local candidates = ADFieldFleet.filterDedicated(ADFieldWork.getRigs(adEnv), field.id, role, rigIdsByField)
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,
}
local refill = refillPlan(adEnv, field.id, rig.vehicle, role, info.x, info.z)
if refill ~= nil and refill.empty and refill.refillId == nil then
-- tom og intet sted at fylde op: ud på marken ville den bare køre kurset uden at sprede
fail(field.id, string.format("'%s' er tom for %s og der er ingen påfyldning med AutoDrive-markør — fyld den op", rig.label, fillTypeName(refill.fillType)))
return
end
ADFieldJobs.state.assignments[rig.id] = assignment
if refill ~= nil and refill.empty then
-- AutoDrive LOAD: påfyldningen (2. markør), fyld op, videre til F-markøren (1.) — så Courseplay
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_LOAD, info.markerId, refill.refillId, refill.fillType)
assignment.drive = {mode = adEnv.AutoDrive.MODE_LOAD, first = info.markerId, second = refill.refillId, fillType = refill.fillType}
local marker = adEnv.ADGraphManager:getMapMarkerByWayPointId(refill.refillId)
log(field.id, "%s '%s' er tom — fyldes med %s ved %s og kører så til %s", role, rig.label, fillTypeName(refill.fillType),
tostring(marker ~= nil and marker.name or refill.refillId), tostring(info.markerName))
else
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
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
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.)
-- Sælger købestationen varen? (placeable.spec_buyingStation.buyingStation; FS25-felter varierer, så flere
-- opslag: udbudte varer, triggernes varer, AI-varer.)
function ADFieldWork.buyingSupports(station, fillType)
if station == nil or fillType == nil then return false end
local ok, result = pcall(function()
for _, key in ipairs({"providedFillTypes", "supportedFillTypes", "fillTypes"}) do
local set = station[key]
if type(set) == "table" and set[fillType] then return true end
end
for _, trigger in pairs(station.loadTriggers or {}) do
if type(trigger.fillTypes) == "table" and trigger.fillTypes[fillType] then return true end
end
if station.getAISupportedFillTypes ~= nil then
local set = station:getAISupportedFillTypes()
if type(set) == "table" and set[fillType] then return true end
end
return false
end)
return ok and result == true
end
-- AD-markøren ved stationen: AutoDrives eget opslag (30 m fra triggeren), ellers nærmeste ikke-Wait
-- markør inden for 40 m af bygningen ("Liquid Buy Station" står ved tanken, ikke ved triggeren).
local function markerAtStation(adEnv, vehicle, station, placeable)
local okAd, wayPointId = pcall(function() return adEnv.ADTriggerManager:getMarkerAtStation(station, vehicle, 30) end)
if okAd and wayPointId ~= nil and wayPointId > 0 then return wayPointId end
local node = placeable ~= nil and placeable.rootNode or nil
if node == nil then return nil end
local px, _, pz = getWorldTranslation(node)
local best, bestDistance = nil, 40 * 40
for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
local isWait = ADSupplyPlanner ~= nil and ADSupplyPlanner.isWaitMarker ~= nil and ADSupplyPlanner.isWaitMarker(marker)
local wayPoint = not isWait and adEnv.ADGraphManager:getWayPointById(marker.id) or nil
if wayPoint ~= nil then
local d = (wayPoint.x - px) ^ 2 + (wayPoint.z - pz) ^ 2
if d < bestDistance then best, bestDistance = marker.id, d end
end
end
return best
end
-- Nærmeste påfyldning af varen med AD-markør og vej: lagre/siloer med varen, og købestationer (købes
-- efter behov — ingen lagerbeholdning, så de fandtes ikke før; in-game 26/9 "Liquid Buy Station").
function ADFieldWork.findRefillMarker(adEnv, vehicle, fillType, x, z)
local farmId = vehicle:getOwnerFarmId()
local best, bestDistance = nil, math.huge
local function consider(wayPointId)
local wayPoint = wayPointId ~= nil and wayPointId > 0 and adEnv.ADGraphManager:getWayPointById(wayPointId) or nil
if wayPoint == nil then return end
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
-- gårdens egne lagre først (stalde/tanke med gylle, lager med gødning): købes kun når intet eget findes
-- (in-game 26/9: 7 mio. l gylle ved BGA'en, men påfyldningen ville købe ved købestationen)
pcall(function()
for _, station in pairs(g_currentMission.storageSystem:getLoadingStations()) do
local supported = station.getAISupportedFillTypes ~= nil and station:getAISupportedFillTypes() or {}
local isShop = station.owningPlaceable ~= nil and station.owningPlaceable.spec_buyingStation ~= nil
if not isShop and g_currentMission.accessHandler:canFarmAccess(farmId, station) and supported[fillType]
and (station:getFillLevel(fillType, farmId) or 0) > 0 then
consider(markerAtStation(adEnv, vehicle, station, station.owningPlaceable))
end
end
end)
if best ~= nil then return best end
pcall(function()
for _, placeable in pairs(g_currentMission.placeableSystem.placeables) do
local spec = placeable.spec_buyingStation
local station = spec ~= nil and spec.buyingStation or nil
if station ~= nil and ADFieldWork.buyingSupports(station, fillType) then
consider(markerAtStation(adEnv, vehicle, station, placeable))
end
end
end)
return best
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, assignment.role)
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
-- aldrig Courseplay med tom tank (den kører kurset uden at sprede/så)
if ADFieldWork.REFILL_ROLES[assignment.role] then
local fillType = ADCpBridge.getRefillFillType(vehicle, assignment.role)
if fillType ~= nil and ADCpBridge.getRefillLevel(vehicle, fillType) <= 0 then
ADFieldWork.release(assignment)
fail(field.id, string.format("'%s' er stadig tom for %s ved marken — fyld den op", ADRuns.vehicleLabel(vehicle), fillTypeName(fillType)))
return
end
end
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)
-- lager til høsterens egen rest: når Courseplay er færdig og aflæsningsvognene stoppes, står der ofte
-- korn i tanken ("needs to be unloaded", in-game 26/9) — så kører AutoDrive resten til lageret
if assignment.storeId == nil and info.harvestFillType ~= nil then
local store = ADFieldWork.chooseStore(adEnv, field, info, vehicle, info.harvestFillType)
if store ~= nil then assignment.storeId = store.id end
end
end
-- vinmark: kurset i gangen (ikke på vinen), ellers kører maskinen ind i stolperne
local vineOffset = ADCpBridge.vineOffsetFor(info.fruitName, assignment.role)
if vineOffset ~= nil and ADCpBridge.setVineOffset(vehicle, vineOffset) then
local skip = ADCpBridge.vineSkipFor(info.fruitName, assignment.role) or 0
ADCpBridge.setVineSkip(vehicle, skip)
log(field.id, "vinmark: Courseplay-kurset lægges %s, springer %d række(r) over", vineOffset == 0 and "på vinen (høster)" or "til venstre for vinen (i gangen)", skip)
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
-- høster der stadig har korn efter rest-turen: AutoDrive kunne ikke tømme den — sig det tydeligt
if assignment.role == "harvester" and restLoad(adEnv, vehicle) > 0 then
local text = string.format("'%s' har stadig korn i tanken (%.0f l) — tøm den (AutoDrive kunne ikke aflæsse den)",
ADRuns.vehicleLabel(vehicle), restLoad(adEnv, vehicle))
log(assignment.fieldId, "%s", text)
ADFieldJobs.notify(string.format("F%s: %s", tostring(assignment.fieldId), text), true)
end
-- 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)
elseif assignment.drive ~= nil and (assignment.driveRetries or 0) < ADFieldWork.DRIVE_RETRIES then
-- AutoDrive stoppede undervejs (spilleren byggede, trafik, ...): kør videre i stedet for at opgive
assignment.driveRetries = (assignment.driveRetries or 0) + 1
assignment.since = now()
local d = assignment.drive
startAd(adEnv, vehicle, d.mode, d.first, d.second, d.fillType)
log(assignment.fieldId, "'%s' stoppede %.0f m fra %s — AutoDrive startes igen (%d/%d)", ADRuns.vehicleLabel(vehicle), distance,
tostring(info ~= nil and info.markerName or "?"), assignment.driveRetries, ADFieldWork.DRIVE_RETRIES)
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))
-- ikke efterladt midt på vejen/ved en mark: hjem til parkeringen
local park = parkMarkerId(adEnv, vehicle)
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
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