1344 lines
74 KiB
Lua
1344 lines
74 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
|
|
-- længere fra AD-vejnettet end dette kan AutoDrive ikke finde en rute ("Could not calculate path", in-game 26/9:
|
|
-- kultivatoren stod 666 m fra nærmeste waypoint); sådan en maskine sendes ikke ud
|
|
ADFieldWork.MAX_OFF_NETWORK = 60
|
|
-- flyttede køretøjet sig mindre end dette før AD stoppede, fandt AD ingen rute — ingen genforsøg
|
|
ADFieldWork.NO_MOVE_DISTANCE = 5
|
|
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
|
|
local lastNoRig = {}
|
|
|
|
-- ---- 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
|
|
|
|
ADFieldWork.startAd = startAd
|
|
ADFieldWork.enableParkAtJobFinished = enableParkAtJobFinished
|
|
|
|
-- 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
|
|
-- parkeringen kan ligge på vognen/redskabet (advd.parkDestination); AD kopierer den først til traktoren når
|
|
-- redskabet kobles på/vælges — efter indlæsning er kopien ofte tom (in-game 27/9: 20 af 25 "uden parkering")
|
|
if index == nil or index < 1 then
|
|
local ok, units = pcall(adEnv.AutoDrive.getAllUnits, vehicle)
|
|
for _, unit in ipairs(ok and units or {}) do
|
|
local own = unit.advd ~= nil and unit.advd.parkDestination or nil
|
|
if type(own) == "number" and own >= 1 then index = own break end
|
|
end
|
|
end
|
|
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
|
|
ADFieldWork.parkMarkerId = parkMarkerId
|
|
|
|
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 ----
|
|
|
|
-- Optaget for markarbejdet: vores egen opgave, AutoDrive/Courseplay kører den, lånt af marksilo-flowet,
|
|
-- eller på hjemtur fra puljen (sætter redskab af).
|
|
local function isBusyVehicle(vehicle, id)
|
|
return ADFieldJobs.state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
|
|
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)) or (ADPool ~= nil and ADPool.isReturning(vehicle))
|
|
or (ADFlowWagons ~= nil and ADFlowWagons.isEmptying(vehicle))
|
|
end
|
|
|
|
ADFieldWork.isBusyVehicle = isBusyVehicle
|
|
|
|
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
|
|
-- (et stoppet flows traktorer er ledige — maskinpulje/flow-vogne)
|
|
if role ~= nil and id ~= nil and (ADPool ~= nil and ADPool.isFreeFromFlows(vehicle) or 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 = isBusyVehicle(vehicle, id),
|
|
-- hvorfor optaget (til loggen, når ingen maskine kan sendes ud)
|
|
busyWhy = (state.assignments[id] ~= nil and ("opgave F" .. tostring(state.assignments[id].fieldId)))
|
|
or (isAdActive(vehicle) and "AutoDrive kører") or (ADCpBridge.isWorking(vehicle) and "Courseplay kører")
|
|
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle) and "lånt af marksilo-flow")
|
|
or (ADPool ~= nil and ADPool.isReturning(vehicle) and "sætter redskab af") or nil,
|
|
controlled = isControlledByPlayer(vehicle),
|
|
-- manuel transport: spilleren kører den til marken (for bred til vejene med skærebord)
|
|
manual = (state.manual or {})[id] == true,
|
|
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
|
-- tom spreder kan flere ting (gødning og kalk): alle dens roller
|
|
roles = specs.sprayRoles or ADFieldFleet.rolesFor(role), 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
|
|
-- henter stadig puljeredskabet: afbryd koblingen, så det ikke kobles på uden en opgave
|
|
if assignment.phase == "fetching" and assignment.hitch ~= nil and not assignment.hitch.done and ADHitch ~= nil then
|
|
ADHitch.cancel(assignment.vehicle)
|
|
end
|
|
assignment.phase = phase or "released"
|
|
if ADPool ~= nil and assignment.pool ~= nil then ADPool.release(assignment) end
|
|
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).
|
|
-- Afstand fra (x, z) til nærmeste AD-waypoint (nil hvis vejnettet ikke kan læses).
|
|
function ADFieldWork.distanceToNetwork(adEnv, x, z)
|
|
-- gemmes 30 s pr. 10 m-celle (15.000 waypoints pr. opslag)
|
|
if ADCacheGet ~= nil then
|
|
local key = string.format("%d:%d", math.floor(x / 10), math.floor(z / 10))
|
|
return ADCacheGet("networkDistance", key, 30000, function() return ADFieldWork.distanceToNetworkUncached(adEnv, x, z) end)
|
|
end
|
|
return ADFieldWork.distanceToNetworkUncached(adEnv, x, z)
|
|
end
|
|
|
|
function ADFieldWork.distanceToNetworkUncached(adEnv, x, z)
|
|
local ok, best = pcall(function()
|
|
local wayPoints = adEnv.ADGraphManager:getWayPoints()
|
|
local bestD = math.huge
|
|
for _, wp in pairs(wayPoints or {}) do
|
|
local d = (wp.x - x) ^ 2 + (wp.z - z) ^ 2
|
|
if d < bestD then bestD = d end
|
|
end
|
|
return math.sqrt(bestD)
|
|
end)
|
|
return ok and best ~= math.huge and best or nil
|
|
end
|
|
|
|
local function acceptFor(field, info, role)
|
|
if role == "harvester" then
|
|
local fruit = info.fruitIndex
|
|
return function(candidate) return ADManualTransport.acceptsHarvester(candidate, fruit) 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
|
|
|
|
-- Manuel transport: bed spilleren køre høsteren til marken (dialog første gang, ellers besked i hjørnet).
|
|
local function askPlayer(field, info, assignment, withDialog)
|
|
local text = string.format("F%s skal høstes: kør '%s' til %s, montér skærebordet og stig ud — så overtager Smart Pickup",
|
|
tostring(field.id), ADRuns.vehicleLabel(assignment.vehicle), tostring(info.markerName or ("F" .. tostring(field.id))))
|
|
assignment.remindedAt = now()
|
|
log(field.id, "venter på spilleren: %s", text)
|
|
if withDialog and InfoDialog ~= nil and InfoDialog.show ~= nil then
|
|
pcall(InfoDialog.show, "Smart Pickup: " .. text)
|
|
else
|
|
ADFieldJobs.notify(text, true)
|
|
end
|
|
end
|
|
|
|
-- Puljekombination: nærmeste puljeredskab til marken + nærmeste ledige traktor med nok hk. Traktoren kører til
|
|
-- "Connect X", kobler på og fortsætter så som en almindelig udsendelse (fase fetching -> driving).
|
|
function ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept, decision)
|
|
local implements, tractors = ADPool.collect(adEnv, function(vehicle)
|
|
return isBusyVehicle(vehicle, ADRunsController.getVehicleId(vehicle))
|
|
end)
|
|
local accept = function(implement)
|
|
-- samme felter som getRigs giver sæt, så markens krav (frø, afgrøde, ukrudt) tjekkes på redskabet selv
|
|
local candidate = {role = implement.role, roles = implement.roles, vehicle = implement.vehicle}
|
|
if role == "seeder" then
|
|
candidate.seedFruits = ADCpBridge.getSeedFruits(implement.vehicle)
|
|
candidate.hasSeeds = ADCpBridge.hasSeeds(implement.vehicle)
|
|
end
|
|
if role == "weeder" then candidate.mechanical = ADCpBridge.isMechanicalWeeder(implement.vehicle) end
|
|
if roleAccept ~= nil and not roleAccept(candidate) then return false end
|
|
-- tømmevogn: skal kunne bære høstens vare
|
|
if role == "unloader" and not ADPool.canCarry(implement, info.harvestFillType) then return false end
|
|
local required = ADFieldModel.requiredWidth(field, role)
|
|
return (implement.width or 0) <= 0 or ADFieldModel.checkWidth(role, implement.width, required)
|
|
end
|
|
local pair, why, args = ADPool.pairFor(role, info.x, info.z, implements, tractors, ADPool.blocked(now()), accept)
|
|
if pair == nil then return false, why, args end
|
|
local tractor, implement = pair.tractor, pair.implement
|
|
local assignment = {
|
|
vehicleId = tractor.id, vehicle = tractor.vehicle, fieldId = field.id, role = role, stage = field.stage,
|
|
helper = decision ~= nil and decision.helper == true, phase = "fetching", since = now(), width = implement.width,
|
|
groupSize = 1, leader = true, fillType = decision ~= nil and decision.helper and info.harvestFillType or nil,
|
|
startX = tractor.x, startZ = tractor.z,
|
|
pool = {implementId = implement.id, implementName = implement.name, connectId = implement.connectId},
|
|
hitch = {done = false},
|
|
}
|
|
ADFieldJobs.state.assignments[tractor.id] = assignment
|
|
ADPool.reserve(assignment, {implement.id, tractor.id})
|
|
ADHitch.fetch(adEnv, tractor.vehicle, implement.connectId, function(_, ok, detail)
|
|
assignment.hitch = {done = true, ok = ok, detail = detail}
|
|
end)
|
|
note(field.id, {reason = {key = "spu_field_reason_fetching", name = implement.name}})
|
|
log(field.id, "%s: '%s' henter '%s' (%s)", role, tractor.name, implement.name, "pulje")
|
|
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
|
|
-- 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 roleAccept = acceptFor(field, info, role)
|
|
-- kun maskiner AutoDrive kan køre fra: tæt på vejnettet
|
|
local offNetwork = {}
|
|
local accept = function(candidate)
|
|
if roleAccept ~= nil and not roleAccept(candidate) then return false end
|
|
-- manuel transport: spilleren kører den — afstand til AutoDrive-vejnettet er ligegyldig
|
|
if role == "harvester" and candidate.manual then return true end
|
|
local d = candidate.x ~= nil and ADFieldWork.distanceToNetwork(adEnv, candidate.x, candidate.z) or nil
|
|
if d ~= nil and d > ADFieldWork.MAX_OFF_NETWORK then
|
|
table.insert(offNetwork, string.format("'%s' %.0f m", tostring(candidate.label), d))
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
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
|
|
-- ingen færdig kombination: en ledig solo-traktor henter et puljeredskab (kun hovedopgaver, ikke tømmevogne)
|
|
-- marken har egne faste maskiner til rollen: den venter på dem (som uden puljen)
|
|
local ownIds, hasOwnForRole = {}, false
|
|
for _, id in ipairs(field.plan ~= nil and field.plan.rigIds or {}) do ownIds[id] = true end
|
|
for _, candidate in ipairs(candidates) do
|
|
if ownIds[candidate.id] and (candidate.role == role or (candidate.roles ~= nil and candidate.roles[role])) then hasOwnForRole = true end
|
|
end
|
|
if ADPool ~= nil and ADPool.mayUsePool(decision, hasOwnForRole)
|
|
and (why == "noRig" or why == "allBusy" or why == "cannotDo") then
|
|
local pooled, poolWhy, poolArgs = ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept, decision)
|
|
if pooled then return end
|
|
if poolWhy == "tooWeak" then
|
|
note(field.id, {reason = {text = string.format("ingen ledig traktor med ≥%.0f hk til '%s'",
|
|
(poolArgs or {})[1] or 0, tostring((poolArgs or {})[2] or role))}, retryAt = ADPool.alignedRetry(now(), ADFieldWork.NO_RIG_RETRY_MS)})
|
|
return
|
|
end
|
|
if poolWhy == "noPoolTractor" and why ~= "allBusy" then
|
|
why, args = poolWhy, poolArgs
|
|
end
|
|
end
|
|
if #offNetwork > 0 and (why == "cannotDo" or why == "noRig") then
|
|
-- de eneste med rollen står uden for AD-vejnettet
|
|
note(field.id, {reason = {text = string.format("ingen %s kan køre til %s — står for langt fra AutoDrive-vejnettet (%s)",
|
|
tostring(role), tostring(info.markerName), table.concat(offNetwork, ", "))}, retryAt = ADPool.alignedRetry(now(), ADFieldWork.NO_RIG_RETRY_MS)})
|
|
return
|
|
end
|
|
note(field.id, {reason = {key = "spu_field_reason_" .. tostring(why), role = role, args = args}, retryAt = ADPool.alignedRetry(now(), ADFieldWork.NO_RIG_RETRY_MS)})
|
|
-- også i loggen (én gang pr. ændring): før stod grunden kun i fanen, og loggen var tavs (in-game 27/9)
|
|
local noRigKey = tostring(role) .. ":" .. tostring(why)
|
|
if lastNoRig[field.id] ~= noRigKey then
|
|
lastNoRig[field.id] = noRigKey
|
|
-- hvem har rollen, og hvorfor kan de ikke bruges (in-game 27/9: "allBusy" uden at sige hvem)
|
|
local who = {}
|
|
for _, candidate in ipairs(candidates) do
|
|
if candidate.role == role or (candidate.roles ~= nil and candidate.roles[role]) then
|
|
local state = candidate.controlled and "spiller i førersædet" or candidate.busyWhy
|
|
or (not candidate.enabled and "ikke i flåden") or (roleAccept ~= nil and not roleAccept(candidate) and "kan ikke (afgrøde/frø)") or "ledig"
|
|
table.insert(who, string.format("'%s': %s", tostring(candidate.label), state))
|
|
end
|
|
end
|
|
log(field.id, "ingen maskine sendt til %s: %s%s", tostring(role), tostring(why), #who > 0 and (" — " .. table.concat(who, "; ")) or "")
|
|
end
|
|
return
|
|
end
|
|
lastNoRig[field.id] = nil
|
|
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,
|
|
}
|
|
if role == "harvester" and rig.manual and not assignment.helper then
|
|
-- for bred til vejene: spilleren kører den selv til marken og monterer skærebordet
|
|
assignment.phase, assignment.manual = "awaitingPlayer", true
|
|
assignment.startX, assignment.startZ = rig.x, rig.z
|
|
ADFieldJobs.state.assignments[rig.id] = assignment
|
|
askPlayer(field, info, assignment, true)
|
|
else
|
|
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
|
|
assignment.startX, assignment.startZ = rig.x, rig.z
|
|
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
|
|
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)
|
|
ADHitch.returnHome(adEnv, vehicle)
|
|
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)
|
|
-- punktet nærmest traktoren: CP lægger kursets start dér (ikke i den anden ende af marken)
|
|
local vx, vz = vehiclePosition(assignment.vehicle)
|
|
local point = ADFieldJobs.nearestPoint(info.points, vx, vz) 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)
|
|
ADHitch.returnHome(adEnv, vehicle)
|
|
fail(field.id, "aflæsningsvogn: " .. tostring(reason))
|
|
end
|
|
end)
|
|
if handle == nil then
|
|
ADFieldWork.release(assignment)
|
|
ADHitch.returnHome(adEnv, vehicle)
|
|
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
|
|
-- punktet nærmest traktoren: CP lægger kursets start dér (ikke i den anden ende af marken)
|
|
local vx, vz = vehiclePosition(assignment.vehicle)
|
|
local point = ADFieldJobs.nearestPoint(info.points, vx, vz) 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)
|
|
-- høsteren sendes IKKE selv til lager med resten (in-game 27/9: den er for stor til at komme til lageret).
|
|
-- Courseplay venter selv ved kursets ende til tømmevognene har tømt tanken (WAITING_FOR_UNLOAD_AFTER_
|
|
-- FIELDWORK_ENDED), og tømmevognene bliver på marken, så længe høsterens CP-job kører.
|
|
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
|
|
-- punktet nærmest traktoren: CP lægger kursets start dér (ikke i den anden ende af marken)
|
|
local vx, vz = vehiclePosition(assignment.vehicle)
|
|
local point = ADFieldJobs.nearestPoint(info.points, vx, vz) or {x = info.x, z = info.z}
|
|
-- genbrug af Courseplay-kurser: maskinens eget / en andens matchende kursus frem for ny beregning
|
|
local cacheContext
|
|
opts, cacheContext = ADCourseCache.prepare(adEnv, field, info, assignment, opts)
|
|
-- påbegyndt mark: fortsæt/start nærmest i stedet for forfra (CP's standard)
|
|
opts = opts or {}
|
|
local remembered = vehicle.getCpLastRememberedWaypointIx ~= nil and vehicle:getCpLastRememberedWaypointIx() ~= nil
|
|
opts.startAt = ADFieldJobs.startAtFor({reuse = opts.reuse == true, remembered = remembered, stage = field.stage,
|
|
standingRatio = info.standingRatio})
|
|
if opts.startAt ~= "first" then log(field.id, "'%s' starter kurset ved %s punkt (marken er påbegyndt)", ADRuns.vehicleLabel(vehicle), opts.startAt == "last" and "sidste" or "nærmeste") end
|
|
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()
|
|
ADCourseCache.afterStart(vehicle, assignment.vehicleId, cacheContext)
|
|
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 source ~= nil 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)
|
|
if ADHitch ~= nil then ADHitch.returnHome(adEnv, vehicle) end
|
|
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)
|
|
-- manuel transport: ALDRIG ud på vejen med AutoDrive (skærebordet sidder på) — spilleren henter den
|
|
-- puljeredskab bag traktoren: sæt det af på sin parkering, traktoren hjem (afhænger kun af hvad der hænger
|
|
-- bag, så det virker også efter genindlæsning); ellers som før
|
|
local pooled = ADPool ~= nil and ADPool.shouldReturnHome(assignment) and ADHitch ~= nil
|
|
and ADHitch.returnHome(adEnv, vehicle, function(_, ok, detail)
|
|
if not ok then log(assignment.fieldId, "'%s' kom ikke hjem fra puljen: %s", ADRuns.vehicleLabel(vehicle), tostring(detail)) end
|
|
end)
|
|
local park = not pooled and not assignment.adHandlesEnd and not assignment.manual and parkMarkerId(adEnv, vehicle) or nil
|
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
if pooled then log(assignment.fieldId, "'%s' sætter redskabet af og kører hjem", ADRuns.vehicleLabel(vehicle)) end
|
|
if assignment.manual then
|
|
ADFieldJobs.notify(string.format("F%s høstet — afmontér skærebordet og kør '%s' hjem", tostring(assignment.fieldId), ADRuns.vehicleLabel(vehicle)), true)
|
|
end
|
|
restoreToolOffset(assignment)
|
|
assignment.phase = "finished"
|
|
table.insert(ADFieldJobs.finished, assignment)
|
|
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
|
end
|
|
|
|
local MANUAL_REASON_TEXT = {
|
|
notThere = "kør den til marken",
|
|
noHeader = "montér skærebordet",
|
|
wrongHeader = "skærebordet kan ikke høste afgrøden — montér det rigtige",
|
|
playerInside = "stig ud, så overtager Smart Pickup",
|
|
}
|
|
|
|
-- Manuel transport: overtag når høsteren står på marken med passende skærebord og spilleren er steget ud
|
|
-- (eller når spilleren trykker "Start nu"); påmind hvert 5. minut indtil da.
|
|
local function followManual(adEnv, field, info, assignment)
|
|
if field == nil or info == nil then return ADFieldWork.release(assignment) end
|
|
local vehicle = assignment.vehicle
|
|
local x, z = vehiclePosition(vehicle)
|
|
local atField = #(info.verts or {}) >= 3 and ADFieldJobs.insidePolygon(x, z, info.verts)
|
|
if not atField and info.markerId ~= nil then
|
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
|
atField = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) <= ADFieldWork.ARRIVE_DISTANCE
|
|
end
|
|
local forced = ADFieldJobs.takeoverRequested[field.id] == true
|
|
ADFieldJobs.takeoverRequested[field.id] = nil
|
|
local ok, why = ADManualTransport.readiness({atField = atField, fruits = ADCpBridge.getHarvestFruits(vehicle) or {},
|
|
fruit = info.fruitIndex, controlled = isControlledByPlayer(vehicle), forced = forced})
|
|
if ok then
|
|
log(field.id, "overtager '%s'%s", ADRuns.vehicleLabel(vehicle), forced and " (Start nu)" or "")
|
|
return beginWork(adEnv, field, info, assignment)
|
|
end
|
|
local reason = MANUAL_REASON_TEXT[why] or tostring(why)
|
|
note(field.id, {reason = {text = string.format("venter på dig: '%s' — %s", ADRuns.vehicleLabel(vehicle), reason)}})
|
|
if forced then
|
|
ADFieldJobs.notify(string.format("F%s: kan ikke starte '%s' endnu — %s", tostring(field.id), ADRuns.vehicleLabel(vehicle), reason), true)
|
|
elseif ADManualTransport.shouldRemind(assignment.remindedAt, now()) then
|
|
askPlayer(field, info, assignment, false)
|
|
end
|
|
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
|
|
assignment.seenWorking = true
|
|
return
|
|
end
|
|
-- genoptaget efter indlæsning: CP starter asynkront — vent til den kører, før "står stille" betyder færdig
|
|
if assignment.resumed and not assignment.seenWorking then
|
|
if now() - assignment.since > ADFieldWork.START_TIMEOUT_MS then
|
|
ADFieldWork.release(assignment)
|
|
fail(assignment.fieldId, string.format("'%s': Courseplay fortsatte ikke efter genindlæsning", ADRuns.vehicleLabel(vehicle)))
|
|
end
|
|
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
|
|
|
|
-- Efter genindlæsning: spillet stopper alle arbejdere ved gem, og Courseplay genstarter dem ikke (in-game 27/9).
|
|
-- En maskine med CP-kursus og et husket waypoint (CP nulstiller det når kurset er kørt færdigt) var midt i et job:
|
|
-- start den fra sidste waypoint med CP's eget "fortsæt"-job (med gemt liste fra ADCpResume: netop de maskiner
|
|
-- der kørte CP ved gem, fra nærmeste waypoint) (HUD'ens eget startpunkt røres ikke). Står den på en
|
|
-- mark hvis aktuelle trin er maskinens rolle, meldes den ind på marken igen, så den følges og trinnet afsluttes.
|
|
function ADFieldWork.resumeAfterLoad(adEnv)
|
|
-- gemt liste over maskiner hvor CP kørte (ADCpResume): den afgør alene hvem der startes, fra nærmeste waypoint
|
|
local toStart, cpVehicles = nil, {}
|
|
if ADCpResume ~= nil then toStart, cpVehicles = ADCpResume.takeToStart(adEnv) end
|
|
local startSet, handled = {}, {}
|
|
for _, id in ipairs(toStart or {}) do startSet[id] = true end
|
|
local fields, stageById = {}, {}
|
|
for _, info in ipairs(ADFields.list()) do
|
|
local field = ADFieldJobs.getField(info.id)
|
|
if field ~= nil and ADFieldModel.isWorking(field) then
|
|
table.insert(fields, {id = info.id, stage = field.stage, verts = info.verts})
|
|
stageById[info.id] = field.stage
|
|
end
|
|
end
|
|
for _, rig in ipairs(ADFieldWork.getRigs(adEnv)) do
|
|
local vehicle = rig.vehicle
|
|
local wasWorking = vehicle.startCpAtLastWp ~= nil and vehicle.hasCpCourse ~= nil and vehicle:hasCpCourse()
|
|
and vehicle.getCpLastRememberedWaypointIx ~= nil and vehicle:getCpLastRememberedWaypointIx() ~= nil
|
|
if toStart ~= nil then wasWorking = startSet[rig.id] == true end
|
|
handled[rig.id] = true
|
|
local fieldId, distance = nil, nil
|
|
if wasWorking and toStart ~= nil then
|
|
fieldId = ADFieldJobs.resumeField(rig.x, rig.z, rig.role, fields)
|
|
elseif wasWorking then
|
|
fieldId = ADFieldJobs.resumeField(rig.x, rig.z, rig.role, fields)
|
|
local course = ADCpBridge.getCourse(vehicle)
|
|
local okWp, wp = pcall(function() return course ~= nil and course:getWaypoint(vehicle:getCpLastRememberedWaypointIx()) or nil end)
|
|
if okWp and wp ~= nil and wp.x ~= nil then distance = math.sqrt((wp.x - rig.x) ^ 2 + (wp.z - rig.z) ^ 2) end
|
|
end
|
|
if wasWorking and toStart == nil and not rig.busy and not rig.controlled and not ADFieldJobs.shouldResume({fieldId = fieldId, distance = distance}) then
|
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartes ikke: husket waypoint fra et gammelt job (%s væk, ikke på en mark i gang)",
|
|
tostring(rig.label), distance ~= nil and string.format("%.0f m", distance) or "ukendt afstand")
|
|
elseif wasWorking and not rig.busy and not rig.controlled then
|
|
local ok, started
|
|
if toStart ~= nil then ok, started = pcall(ADCpResume.startNearest, vehicle)
|
|
else ok, started = pcall(vehicle.startCpAtLastWp, vehicle) end
|
|
if not ok then
|
|
Logging.warning(ADFieldJobs.LOG_PREFIX .. "'%s' kunne ikke genstartes: %s", tostring(rig.label), tostring(started))
|
|
elseif started then
|
|
if fieldId ~= nil then
|
|
ADFieldJobs.state.assignments[rig.id] = {vehicleId = rig.id, vehicle = vehicle, fieldId = fieldId, role = rig.role,
|
|
stage = stageById[fieldId], helper = false, phase = "working", since = now(), width = rig.width,
|
|
groupSize = 1, leader = true, resumed = true, manual = rig.manual == true}
|
|
-- lager til læssevognens rest, som ved en normal start
|
|
local assignment = ADFieldJobs.state.assignments[rig.id]
|
|
local field, info = ADFieldJobs.getField(fieldId), ADFields.getInfo(fieldId)
|
|
local fillType = nil
|
|
-- kun læssevognen kører selv hjem med læsset; høsteren tømmes af tømmevognene på marken
|
|
if rig.role == "collector" then fillType = ADFieldWork.storeFillType(field, info) end
|
|
if fillType ~= nil then
|
|
local okStore, store = pcall(ADFieldWork.chooseStore, adEnv, field, info, vehicle, fillType)
|
|
if okStore and store ~= nil then assignment.storeId = store.id end
|
|
end
|
|
log(fieldId, "'%s' fortsætter efter genindlæsning (Courseplay fra sidste waypoint)%s", tostring(rig.label),
|
|
assignment.storeId ~= nil and "" or (fillType ~= nil and " — intet lager til resten fundet" or ""))
|
|
else
|
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartet fra %s waypoint efter genindlæsning", tostring(rig.label),
|
|
toStart ~= nil and "nærmeste" or "sidste")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- CP-maskiner uden markrolle (fx balleopsamler, tømmevogn i CP-job): start dem også
|
|
for _, id in ipairs(toStart or {}) do
|
|
local vehicle = cpVehicles[id]
|
|
if not handled[id] and vehicle ~= nil then
|
|
local ok, started = pcall(ADCpResume.startNearest, vehicle)
|
|
if ok and started then
|
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartet fra nærmeste waypoint efter genindlæsning", tostring(ADRuns.vehicleLabel(vehicle)))
|
|
elseif not ok then
|
|
Logging.warning(ADFieldJobs.LOG_PREFIX .. "'%s' kunne ikke genstartes: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(started))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Markmaskiner der stadig hedder modelnavnet ("Unrealistic Engine 3") får et navn efter rollen ("Roeoptager 2").
|
|
-- Navne spilleren/moden har givet røres aldrig; flow-traktorer navngives af deres flow (ikke i markflåden).
|
|
local BEET_FRUITS = {SUGARBEET = true, POTATO = true, CARROT = true, PARSNIP = true, BEETROOT = true}
|
|
local function harvesterSubtype(rig)
|
|
for fruit in pairs(rig.fruits or {}) do
|
|
local desc = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruit) or nil
|
|
if desc ~= nil and BEET_FRUITS[desc.name] then return "beet" end
|
|
end
|
|
local chaff = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeIndexByName("CHAFF") or nil
|
|
local ok, isForage = pcall(function()
|
|
local vehicle = rig.vehicle
|
|
for index in ipairs(vehicle.getFillUnits ~= nil and vehicle:getFillUnits() or {}) do
|
|
if chaff ~= nil and vehicle:getFillUnitSupportsFillType(index, chaff) then return true end
|
|
end
|
|
return false
|
|
end)
|
|
return ok and isForage and "forage" or nil
|
|
end
|
|
|
|
function ADFieldWork.autoNameFleet(adEnv)
|
|
local names = {}
|
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do table.insert(names, ADRuns.vehicleLabel(vehicle)) end
|
|
for _, rig in ipairs(ADFieldWork.getRigs(adEnv)) do
|
|
local vehicle = rig.vehicle
|
|
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
|
local model = vehicle.getName ~= nil and tostring(vehicle:getName()) or nil
|
|
-- puljetraktor med et lånt redskab: ikke opkaldt efter redskabet (in-game 28/9: Challenger blev "Kultivator 1")
|
|
local pooled = ADHitch ~= nil and ADPool ~= nil and #ADHitch.poolParkIds(adEnv, vehicle) > 0
|
|
if stateModule ~= nil and stateModule.setName ~= nil and model ~= nil and ADRuns.vehicleLabel(vehicle) == model and not rig.controlled and not pooled then
|
|
local name = ADNaming.fleetName(rig.role, rig.role == "harvester" and harvesterSubtype(rig) or nil, names)
|
|
if name ~= nil then
|
|
stateModule:setName(name)
|
|
if stateModule.raiseDirtyFlag ~= nil then stateModule:raiseDirtyFlag() end
|
|
table.insert(names, name)
|
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' navngivet '%s' (markflåde, %s)", model, name, tostring(rig.role))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Høsten er færdig, men den høster der høstede marken har korn i tanken og står ved marken: start dens CP-job igen
|
|
-- (sidste/nærmeste punkt), så CP venter ved kursets ende til tømmevognene har tømt den. Én gang pr. mark.
|
|
function ADFieldWork.tryEmptyHarvester(adEnv, field, info)
|
|
local vehicleId = field.harvestVehicleId
|
|
if vehicleId == nil or info == nil then return false end
|
|
local rig = nil
|
|
for _, candidate in ipairs(ADFieldWork.getRigs(adEnv)) do
|
|
if candidate.id == vehicleId then rig = candidate break end
|
|
end
|
|
if rig == nil then return false end
|
|
local fill = restLoad(adEnv, rig.vehicle)
|
|
local near = #(info.verts or {}) >= 3 and ADFieldJobs.insidePolygon(rig.x, rig.z, info.verts)
|
|
if not near and info.markerId ~= nil then
|
|
local wayPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
|
near = wayPoint ~= nil and math.sqrt((wayPoint.x - rig.x) ^ 2 + (wayPoint.z - rig.z) ^ 2) <= 100
|
|
end
|
|
if not ADFieldJobs.shouldEmptyHarvester({fill = fill, near = near, busy = rig.busy, controlled = rig.controlled,
|
|
tried = ADFieldJobs.emptyAttempts[field.id] == true}) then
|
|
return false
|
|
end
|
|
ADFieldJobs.emptyAttempts[field.id] = true
|
|
local assignment = {vehicleId = rig.id, vehicle = rig.vehicle, fieldId = field.id, role = "harvester", stage = field.stage,
|
|
helper = false, phase = "driving", since = now(), width = rig.width, groupSize = 1, leader = true,
|
|
manual = rig.manual == true, emptying = true}
|
|
ADFieldJobs.state.assignments[rig.id] = assignment
|
|
log(field.id, "høsten er færdig, men '%s' har %.0f l i tanken — tømmes på marken, før marken går videre", tostring(rig.label), fill)
|
|
beginWork(adEnv, field, info, assignment)
|
|
return true
|
|
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 assignment.phase ~= "awaitingPlayer" and 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 == "fetching" then
|
|
local hitch = assignment.hitch
|
|
if not hitch.done then return end
|
|
ADPool.release(assignment)
|
|
if info == nil or field == nil then
|
|
-- marken findes ikke længere (fx solgt): redskabet tilbage, traktoren hjem
|
|
ADFieldWork.release(assignment)
|
|
if hitch.ok then ADHitch.returnHome(adEnv, vehicle) end
|
|
return
|
|
end
|
|
if not hitch.ok then
|
|
-- koblingen fejlede: den skyldige i karantæne (kom traktoren ikke frem, er det traktoren), hjem, prøv igen
|
|
local blamed = ADPool.failureBlames(hitch.detail) == "tractor" and assignment.vehicleId or assignment.pool.implementId
|
|
ADPool.quarantine(blamed, now())
|
|
ADFieldWork.release(assignment)
|
|
note(assignment.fieldId, {retryAt = ADPool.alignedRetry(now(), ADFieldWork.NO_RIG_RETRY_MS),
|
|
reason = {text = string.format("kunne ikke koble '%s' på (%s) — prøver et andet om lidt",
|
|
tostring(assignment.pool.implementName), tostring(hitch.detail))}})
|
|
local park = parkMarkerId(adEnv, vehicle)
|
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
return
|
|
end
|
|
-- koblet på: sammenfoldet til vejen, bredden med redskabet på (for bred -> hjem igen)
|
|
pcall(ADHitch.foldForTransport, vehicle)
|
|
local width = ADCpBridge.getWorkWidth(vehicle)
|
|
local okWidth, widthReason, widthArgs = ADFieldModel.checkWidth(assignment.role, width, ADFieldModel.requiredWidth(field, assignment.role))
|
|
if not okWidth then
|
|
ADFieldWork.release(assignment)
|
|
note(assignment.fieldId, {retryAt = now() + ADFieldModel.RETRY_MS, reason = {key = "spu_field_reason_" .. tostring(widthReason), role = assignment.role, args = widthArgs}})
|
|
ADHitch.returnHome(adEnv, vehicle)
|
|
return
|
|
end
|
|
-- som almindelig udsendelse: tom såmaskine/sprøjte fyldes op på vejen, uden påfyldning køres den hjem igen
|
|
local refill = refillPlan(adEnv, assignment.fieldId, vehicle, assignment.role, info.x, info.z)
|
|
local nextDrive = ADPool.nextDrive(refill)
|
|
if nextDrive == "empty" then
|
|
ADFieldWork.release(assignment)
|
|
ADHitch.returnHome(adEnv, vehicle)
|
|
fail(assignment.fieldId, string.format("'%s' er tom for %s og der er ingen påfyldning med AutoDrive-markør — fyld den op",
|
|
tostring(assignment.pool.implementName), fillTypeName(refill.fillType)))
|
|
return
|
|
end
|
|
assignment.width, assignment.phase, assignment.since = width, "driving", now()
|
|
assignment.startX, assignment.startZ = vehiclePosition(vehicle)
|
|
if nextDrive == "load" then
|
|
startAd(adEnv, 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}
|
|
else
|
|
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
|
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
|
|
end
|
|
note(assignment.fieldId, {reason = false})
|
|
log(assignment.fieldId, "%s '%s' har '%s' på — kører til %s (%.1f m)", assignment.role, ADRuns.vehicleLabel(vehicle),
|
|
tostring(assignment.pool.implementName), tostring(info.markerName), width or 0)
|
|
elseif 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.startX ~= nil and math.sqrt((x - assignment.startX) ^ 2 + (z - assignment.startZ) ^ 2) < ADFieldWork.NO_MOVE_DISTANCE
|
|
and (assignment.driveRetries or 0) >= 1 then
|
|
-- AD startede og stoppede uden at flytte sig (to gange): ingen rute — genforsøg hjælper ikke
|
|
ADFieldWork.release(assignment)
|
|
fail(assignment.fieldId, string.format("AutoDrive fandt ingen rute fra '%s' til %s — står den uden for vejnettet?", ADRuns.vehicleLabel(vehicle), tostring(info ~= nil and info.markerName or "?")))
|
|
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 (puljeredskab sættes af først)
|
|
if not (ADHitch ~= nil and ADHitch.returnHome(adEnv, vehicle)) then
|
|
local park = parkMarkerId(adEnv, vehicle)
|
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
end
|
|
end
|
|
end
|
|
elseif assignment.phase == "waitingCourse" then
|
|
if now() - assignment.since > ADFieldWork.COURSE_WAIT_MS then
|
|
ADFieldWork.release(assignment)
|
|
ADHitch.returnHome(adEnv, vehicle)
|
|
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
|
|
local generating = assignment.handle ~= nil and assignment.handle.generating == true
|
|
if ADFieldJobs.startTimedOut(now() - assignment.since, generating) then
|
|
ADFieldWork.release(assignment)
|
|
ADHitch.returnHome(adEnv, vehicle)
|
|
fail(assignment.fieldId, generating and "Courseplay blev ikke færdig med kurset (5 min)" or "Courseplay svarede ikke (60 s)")
|
|
end
|
|
elseif assignment.phase == "working" then
|
|
followWorking(adEnv, field, assignment)
|
|
elseif assignment.phase == "awaitingPlayer" then
|
|
followManual(adEnv, field, info, 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 and assignment.phase == "fetching" then
|
|
-- høsten sluttede mens puljevognen blev hentet: afbryd, vognen tilbage hvis den nåede at komme på
|
|
local vehicle = assignment.vehicle
|
|
ADFieldWork.release(assignment)
|
|
if not ADHitch.returnHome(adEnv, vehicle) then
|
|
local park = parkMarkerId(adEnv, vehicle)
|
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
end
|
|
log(fieldId, "tømmevogn '%s' afbrudt under hentning — høsten er færdig", ADRuns.vehicleLabel(vehicle))
|
|
elseif 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
|
|
|
|
-- Stop alle markarbejdets opgaver (Stop alt og parkér). Returnerer {{vehicle, manual}}; markernes trin bevares,
|
|
-- så arbejdet fortsætter efter pausen.
|
|
function ADFieldWork.stopAllAssignments(adEnv)
|
|
local stopped = {}
|
|
local list = {}
|
|
for _, assignment in pairs(ADFieldJobs.state.assignments) do table.insert(list, assignment) end
|
|
for _, assignment in ipairs(list) do
|
|
pcall(stopAd, assignment.vehicle)
|
|
pcall(ADCpBridge.stop, assignment.vehicle)
|
|
ADFieldWork.release(assignment)
|
|
-- puljeredskab: sættes af på sin plads og traktoren kører hjem (i stedet for almindelig parkering)
|
|
local okPool, pooled = pcall(ADHitch.returnHome, adEnv, assignment.vehicle)
|
|
table.insert(stopped, {vehicle = assignment.vehicle, manual = assignment.manual == true, pooled = okPool and pooled == true})
|
|
end
|
|
return stopped
|
|
end
|
|
|
|
function ADFieldWork.stopAll(adEnv, fieldId)
|
|
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
|
pcall(stopAd, assignment.vehicle)
|
|
ADCpBridge.stop(assignment.vehicle)
|
|
local wasFetching = assignment.phase == "fetching"
|
|
ADFieldWork.release(assignment)
|
|
local okHome, pooled = pcall(ADHitch.returnHome, adEnv, assignment.vehicle)
|
|
if wasFetching and not (okHome and pooled) then
|
|
local park = parkMarkerId(adEnv, assignment.vehicle)
|
|
if park ~= nil then pcall(startAd, adEnv, assignment.vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
|
end
|
|
end
|
|
end
|