feat(marker): job-løkke delt i adFieldJobs (beslutning) + adFieldWork (udsendelse) — høst m. aflæsningsvogne, klargøring, pleje, multi-tool
This commit is contained in:
@@ -1,61 +1,86 @@
|
|||||||
-- AD Field Jobs
|
-- AD Field Jobs
|
||||||
-- Markarbejdets job-løkke (hvert sekund): opdater klar-andel, start marker, send sæt ud, følg dem
|
-- Markarbejdets job-løkke (hvert sekund): opdater markerne, start kæder og pleje, beslut hvilke sæt der
|
||||||
-- gennem AutoDrive-kørsel -> Courseplay-arbejde -> hjem, og gå videre til næste trin. decide() er ren
|
-- skal ud, og gå videre til næste trin. decide() er ren og testet; udsendelse og opfølgning af sættene
|
||||||
-- og testet; resten oversætter til AutoDrive/Courseplay og er pcall'et pr. mark og pr. sæt.
|
-- (AutoDrive/Courseplay) ligger i adFieldWork. Alt spil-nært er pcall'et pr. mark og pr. sæt.
|
||||||
|
|
||||||
ADFieldJobs = {}
|
ADFieldJobs = {}
|
||||||
|
|
||||||
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
|
||||||
ADFieldJobs.TICK_MS = 1000
|
ADFieldJobs.TICK_MS = 1000
|
||||||
ADFieldJobs.ARRIVE_DISTANCE = 30
|
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige ture pr. sæt går marken videre.
|
||||||
ADFieldJobs.IDLE_CONFIRM_MS = 5000
|
ADFieldJobs.MAX_PASSES = 2
|
||||||
ADFieldJobs.START_TIMEOUT_MS = 60000
|
|
||||||
ADFieldJobs.PLAYER_DRIVING_KMH = 3
|
|
||||||
-- riven skal være udfoldet og i gang, før nedlægningsområdet måles
|
|
||||||
ADFieldJobs.WINDROW_MEASURE_MS = 10000
|
|
||||||
-- Courseplay kan efterlade hjørner på skæve marker: efter så mange færdige slå-ture går marken videre.
|
|
||||||
ADFieldJobs.MAX_MOW_PASSES = 2
|
|
||||||
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}}
|
ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}}
|
||||||
-- mark-id -> {mower = kursus, rake = kursus}: kopier af slåmaskinens/rivens Courseplay-kursus (kun i
|
-- mark-id -> {mower = kursus, rake = kursus, <trin> = fælles multi-tool-kursus}: kopier (kun i hukommelsen;
|
||||||
-- hukommelsen; efter genindlæsning hentes de fra maskinen, som CP selv gemmer kurset på)
|
-- efter genindlæsning hentes slå-/rivekurset fra maskinen, som CP selv gemmer kurset på)
|
||||||
ADFieldJobs.courses = {}
|
ADFieldJobs.courses = {}
|
||||||
|
ADFieldJobs.finished = {}
|
||||||
ADFieldJobs.timer = 0
|
ADFieldJobs.timer = 0
|
||||||
ADFieldJobs.now = 0
|
ADFieldJobs.now = 0
|
||||||
ADFieldJobs.loaded = false
|
ADFieldJobs.loaded = false
|
||||||
|
|
||||||
local ACTIVE_PHASES = {driving = true, starting = true, working = true}
|
ADFieldJobs.ACTIVE_PHASES = {driving = true, starting = true, waitingCourse = true, working = true}
|
||||||
|
|
||||||
-- ---- ren beslutning ----
|
-- ---- ren beslutning ----
|
||||||
|
|
||||||
|
-- Er afgrøden taget (trin med afgrøde)? Slåning: under 50 % står. Høst: marken er bar eller under 50 %.
|
||||||
|
local function cropGone(stage, info)
|
||||||
|
if info == nil then return false end
|
||||||
|
if stage == "harvesting" and info.kind == "bare" then return true end
|
||||||
|
return (info.readyRatio or 0) < ADFieldModel.CUT_RATIO
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Beslutning for én mark. assignments = sæt på marken ({phase, stage, helper}); kun sæt på det aktuelle
|
||||||
|
-- trin tæller (en aflæsningsvogn på vej hjem fra høsten forstyrrer ikke kultiveringen).
|
||||||
|
-- Svar: {action = "start"|"care"|"advance"|"dispatch"|"wait"|"none", role, count, helper, stage, care}.
|
||||||
function ADFieldJobs.decide(field, info, assignments, now)
|
function ADFieldJobs.decide(field, info, assignments, now)
|
||||||
|
local hasMarker = info ~= nil and info.markerId ~= nil
|
||||||
if field.stage == "ready" then
|
if field.stage == "ready" then
|
||||||
if not ADFieldModel.shouldStart(field) then return {action = "none"} end
|
if not ADFieldModel.shouldStart(field) then return {action = "none"} end
|
||||||
if info == nil or info.markerId == nil then return {action = "wait"} end
|
if not hasMarker then return {action = "wait"} end
|
||||||
return {action = "start"}
|
return {action = "start"}
|
||||||
end
|
end
|
||||||
if not ADFieldModel.isWorking(field) then return {action = "none"} end
|
if not ADFieldModel.isWorking(field) then
|
||||||
local active, finished = 0, 0
|
local care = ADFieldModel.careNeed(field, info)
|
||||||
|
if care == nil then return {action = "none"} end
|
||||||
|
if field.plan.mode ~= "auto" and not field.startRequested then return {action = "none", care = care} end
|
||||||
|
if not hasMarker then return {action = "wait", care = care} end
|
||||||
|
return {action = "care", stage = care}
|
||||||
|
end
|
||||||
|
local active, finished, helpers = 0, 0, 0
|
||||||
for _, assignment in ipairs(assignments) do
|
for _, assignment in ipairs(assignments) do
|
||||||
if ACTIVE_PHASES[assignment.phase] then active = active + 1 end
|
if assignment.stage == nil or assignment.stage == field.stage then
|
||||||
if assignment.phase == "finished" or assignment.phase == "returning" then finished = finished + 1 end
|
local isActive = ADFieldJobs.ACTIVE_PHASES[assignment.phase]
|
||||||
|
if assignment.helper then
|
||||||
|
if isActive then helpers = helpers + 1 end
|
||||||
|
else
|
||||||
|
if isActive then active = active + 1 end
|
||||||
|
if assignment.phase == "finished" then finished = finished + 1 end
|
||||||
end
|
end
|
||||||
-- slået mark uden sæt på (sluppet undervejs, genindlæst): slåningen er gjort, videre til næste trin
|
|
||||||
if active == 0 and field.stage == "mowing" and info ~= nil and info.mowable and (info.readyRatio or 1) < ADFieldModel.CUT_RATIO then
|
|
||||||
return {action = "advance"}
|
|
||||||
end
|
end
|
||||||
if active == 0 and finished > 0 then
|
|
||||||
local stillStanding = field.stage == "mowing" and info ~= nil and (info.readyRatio or 0) >= ADFieldModel.CUT_RATIO
|
|
||||||
if not stillStanding or finished >= ADFieldJobs.MAX_MOW_PASSES then return {action = "advance"} end
|
|
||||||
end
|
end
|
||||||
if info == nil or info.markerId == nil then return {action = "wait"} end
|
local isCropStage = ADFieldModel.CROP_STAGES[field.stage] == true
|
||||||
local role = ADFieldModel.wantsRig(field, active, now)
|
if active == 0 then
|
||||||
if role ~= nil then return {action = "dispatch", role = role} end
|
-- afgrøden er væk (sluppet undervejs, genindlæst, eller færdig): videre
|
||||||
|
if isCropStage and cropGone(field.stage, info) then return {action = "advance"} end
|
||||||
|
if finished > 0 then
|
||||||
|
local maxPasses = ADFieldJobs.MAX_PASSES * ADFieldModel.maxRigs(field.stage, field.plan)
|
||||||
|
if not isCropStage or finished >= maxPasses then return {action = "advance"} end
|
||||||
|
end
|
||||||
|
if not hasMarker then return {action = "wait"} end
|
||||||
|
if (field.retryAt or 0) > now then return {action = "none"} end
|
||||||
|
return {action = "dispatch", role = ADFieldModel.roleFor(field.stage), count = ADFieldModel.maxRigs(field.stage, field.plan), helper = false}
|
||||||
|
end
|
||||||
|
-- hovedsættene arbejder: fyld op med hjælpere (aflæsningsvogne) en ad gangen
|
||||||
|
local helperRole = ADFieldModel.helperFor(field.stage)
|
||||||
|
if helperRole ~= nil and helpers < ADFieldModel.maxHelpers(field.stage, field.plan) and hasMarker and (field.retryAt or 0) <= now then
|
||||||
|
return {action = "dispatch", role = helperRole, count = 1, helper = true}
|
||||||
|
end
|
||||||
return {action = "none"}
|
return {action = "none"}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---- tilstand ----
|
-- ---- tilstand ----
|
||||||
|
|
||||||
local function log(fieldId, fmt, ...)
|
function ADFieldJobs.log(fieldId, fmt, ...)
|
||||||
Logging.info(ADFieldJobs.LOG_PREFIX .. "F%s " .. fmt, tostring(fieldId), ...)
|
Logging.info(ADFieldJobs.LOG_PREFIX .. "F%s " .. fmt, tostring(fieldId), ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -63,10 +88,15 @@ function ADFieldJobs.getField(id)
|
|||||||
return ADFieldJobs.state.fields[id] or ADFieldModel.newField(id)
|
return ADFieldJobs.state.fields[id] or ADFieldModel.newField(id)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function setField(field)
|
function ADFieldJobs.setField(field)
|
||||||
ADFieldJobs.state.fields[field.id] = field
|
ADFieldJobs.state.fields[field.id] = field
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Ændr felter på marken (kopi via modellen).
|
||||||
|
function ADFieldJobs.note(fieldId, changes)
|
||||||
|
ADFieldJobs.setField(ADFieldModel.with(ADFieldJobs.getField(fieldId), changes))
|
||||||
|
end
|
||||||
|
|
||||||
function ADFieldJobs.assignmentsFor(fieldId)
|
function ADFieldJobs.assignmentsFor(fieldId)
|
||||||
local list = {}
|
local list = {}
|
||||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||||
@@ -91,20 +121,16 @@ function ADFieldJobs.load()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function ADFieldJobs.setPlan(id, changes)
|
function ADFieldJobs.setPlan(id, changes)
|
||||||
setField(ADFieldModel.with(ADFieldJobs.getField(id), {plan = changes}))
|
ADFieldJobs.note(id, {plan = changes})
|
||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADFieldJobs.requestStart(id)
|
function ADFieldJobs.requestStart(id)
|
||||||
setField(ADFieldModel.with(ADFieldJobs.getField(id), {startRequested = true}))
|
ADFieldJobs.note(id, {startRequested = true})
|
||||||
end
|
|
||||||
|
|
||||||
local function note(fieldId, changes)
|
|
||||||
setField(ADFieldModel.with(ADFieldJobs.getField(fieldId), changes))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADFieldJobs.forceReady(id)
|
function ADFieldJobs.forceReady(id)
|
||||||
setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
ADFieldJobs.setField(ADFieldModel.forceReady(ADFieldJobs.getField(id)))
|
||||||
end
|
end
|
||||||
|
|
||||||
function ADFieldJobs.toggleFleet(vehicleId)
|
function ADFieldJobs.toggleFleet(vehicleId)
|
||||||
@@ -112,406 +138,63 @@ function ADFieldJobs.toggleFleet(vehicleId)
|
|||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---- AutoDrive ----
|
-- GUI'et henter flåde og gæringssiloer herfra (ligger i adFieldWork).
|
||||||
|
function ADFieldJobs.getRigs(adEnv) return ADFieldWork.getRigs(adEnv) end
|
||||||
|
function ADFieldJobs.getFermenters(...) return ADFieldWork.getFermenters(...) end
|
||||||
|
|
||||||
local function stateModule(vehicle)
|
function ADFieldJobs.forgetFinished(fieldId)
|
||||||
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: Fastrac'en kørte først, da spilleren satte sig ind). Samme greb
|
|
||||||
-- som flows (applyAndStart) + raiseActive, bevist i 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 startAd(adEnv, vehicle, mode, firstId, secondId, withCp)
|
|
||||||
local module = stateModule(vehicle)
|
|
||||||
stopAd(vehicle)
|
|
||||||
module:setMode(mode)
|
|
||||||
module:setFirstMarkerByWayPointId(firstId)
|
|
||||||
if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end
|
|
||||||
module:setStartHelper(withCp == true)
|
|
||||||
if withCp then
|
|
||||||
module.usedHelper = adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
|
||||||
if vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished ~= nil then
|
|
||||||
vehicle.ad.settings.enableParkAtJobFinished.current = 2
|
|
||||||
vehicle.ad.settings.enableParkAtJobFinished.new = 2
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local farmId = vehicle:getOwnerFarmId()
|
|
||||||
if module.setActualFarmId ~= nil then module:setActualFarmId(farmId) 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
|
|
||||||
|
|
||||||
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 ADFieldJobs.getRigs(adEnv)
|
|
||||||
local rigs = {}
|
|
||||||
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
|
||||||
local role = ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle))
|
|
||||||
local id = ADRunsController.getVehicleId(vehicle)
|
|
||||||
if role ~= nil and id ~= nil then
|
|
||||||
local x, z = vehiclePosition(vehicle)
|
|
||||||
table.insert(rigs, {
|
|
||||||
id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle),
|
|
||||||
enabled = ADFieldJobs.state.fleet[id] == true,
|
|
||||||
-- optaget: vores egen opgave, et Smart Pickup-flow, eller AutoDrive/Courseplay kører den allerede
|
|
||||||
-- (fx spillerens egen parkeringstur) — den må ikke kapres midt i en anden kørsel
|
|
||||||
busy = ADFieldJobs.state.assignments[id] ~= nil or ADRunsController.getRunForVehicle(vehicle) ~= nil
|
|
||||||
or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle),
|
|
||||||
controlled = isControlledByPlayer(vehicle),
|
|
||||||
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return rigs
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ---- silo ----
|
|
||||||
|
|
||||||
-- Gårdens gæringssiloer for markens skår, nærmest marken først (produktioner skår -> SILAGE).
|
|
||||||
function ADFieldJobs.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
|
|
||||||
|
|
||||||
-- Tvunget lager -> de valgte; ensilage -> gæringssiloer (produktion skår -> ensilage) før almindelige
|
|
||||||
-- siloer (in-game 25/9: græs endte i en Farma-silo); hø -> kun siloer, aldrig gæringssiloer (flere af dem
|
|
||||||
-- laver også hø om til ensilage).
|
|
||||||
local function chooseStore(adEnv, field, info, vehicle)
|
|
||||||
local fillType = field.plan.product == "hay" and info.hay or info.windrow
|
|
||||||
local isReachable = function(markerId) return ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId) end
|
|
||||||
if #field.plan.storeBuildingIds == 0 and field.plan.product ~= "hay" then
|
|
||||||
local fermenters = ADFieldJobs.getFermenters(adEnv, vehicle:getOwnerFarmId(), 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
|
|
||||||
if #field.plan.storeBuildingIds > 0 then
|
|
||||||
local markerIds = {}
|
|
||||||
for _, buildingId in ipairs(field.plan.storeBuildingIds) do
|
|
||||||
local building = ADBuildings.find(adEnv, vehicle:getOwnerFarmId(), buildingId)
|
|
||||||
for _, marker in ipairs(building ~= nil and ADBuildings.getMarkers(building, "unload") or {}) do
|
|
||||||
table.insert(markerIds, marker.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ADLoadSwap.findForcedDumpMarker(adEnv, vehicle, fillType, 1, markerIds, isReachable)
|
|
||||||
end
|
|
||||||
return ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, 1, info.markerId, isReachable, nil, nil, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- ---- udsendelse og opfølgning ----
|
|
||||||
|
|
||||||
-- Færdige sæt for en mark glemmes, når marken går videre eller stoppes. Listen er begrænset: en mark
|
|
||||||
-- der sendes ud igen uden at gå videre (hjørner), går videre efter MAX_MOW_PASSES færdige ture.
|
|
||||||
local function forgetFinished(fieldId)
|
|
||||||
local remaining = {}
|
local remaining = {}
|
||||||
for _, done in ipairs(ADFieldJobs.finished or {}) do
|
for _, done in ipairs(ADFieldJobs.finished) do
|
||||||
if done.fieldId ~= fieldId then table.insert(remaining, done) end
|
if done.fieldId ~= fieldId then table.insert(remaining, done) end
|
||||||
end
|
end
|
||||||
ADFieldJobs.finished = remaining
|
ADFieldJobs.finished = remaining
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Læssevognens oprindelige CP-forskydning sættes tilbage, når mod'en slipper den.
|
|
||||||
local function restoreToolOffset(assignment)
|
|
||||||
if assignment.restoreToolOffset ~= nil then
|
|
||||||
ADCpBridge.setToolOffset(assignment.vehicle, assignment.restoreToolOffset)
|
|
||||||
assignment.restoreToolOffset = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function release(assignment, phase)
|
|
||||||
restoreToolOffset(assignment)
|
|
||||||
assignment.phase = phase or "released"
|
|
||||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function dispatch(adEnv, field, info, role)
|
|
||||||
local candidates = ADFieldJobs.getRigs(adEnv)
|
|
||||||
local rig, reason, args = ADFieldFleet.pick(candidates, role, info.x, info.z, ADFieldModel.requiredWidth(field, role))
|
|
||||||
if rig == nil then
|
|
||||||
note(field.id, {reason = {key = "spu_field_reason_" .. reason, role = role, args = args}})
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local assignment = {vehicleId = rig.id, vehicle = rig.vehicle, fieldId = field.id, role = role, phase = "driving", since = ADFieldJobs.now, width = rig.width}
|
|
||||||
ADFieldJobs.state.assignments[rig.id] = assignment
|
|
||||||
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil, false)
|
|
||||||
log(field.id, "%s '%s' sendt til %s (%.1f m)", role, rig.label, tostring(info.markerName), rig.width)
|
|
||||||
note(field.id, {reason = false})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Kurset som et trin skal genbruge ("mower"/"rake"): gemt kopi, ellers maskinens nuværende kursus
|
|
||||||
-- (efter genindlæsning).
|
|
||||||
local function sourceCourse(adEnv, field, source)
|
|
||||||
local saved = ADFieldJobs.courses[field.id]
|
|
||||||
if saved ~= nil and saved[source] ~= nil then return saved[source] end
|
|
||||||
local vehicleId = source == "rake" and field.rakeVehicleId or field.mowVehicleId
|
|
||||||
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 rememberCourse(field, source, vehicle, vehicleId)
|
|
||||||
local course = ADCpBridge.getCourse(vehicle)
|
|
||||||
local ok, copy = false, nil
|
|
||||||
if course ~= nil and course.copy ~= nil then ok, copy = pcall(course.copy, course) end
|
|
||||||
ADFieldJobs.courses[field.id] = ADFieldJobs.courses[field.id] or {}
|
|
||||||
ADFieldJobs.courses[field.id][source] = ok and copy or course
|
|
||||||
if source == "rake" then
|
|
||||||
note(field.id, {rakeVehicleId = vehicleId})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function beginWork(adEnv, field, info, assignment)
|
|
||||||
local vehicle = assignment.vehicle
|
|
||||||
if assignment.role == "collector" then
|
|
||||||
local store, why = chooseStore(adEnv, field, info, vehicle)
|
|
||||||
if store == nil then
|
|
||||||
release(assignment)
|
|
||||||
note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(why)}})
|
|
||||||
log(field.id, "intet lager: %s", tostring(why))
|
|
||||||
return
|
|
||||||
end
|
|
||||||
assignment.storeId = store.id
|
|
||||||
-- AD overtager ved "fuld": Hent og aflever F -> silo, hjælper = Courseplay
|
|
||||||
local module = stateModule(vehicle)
|
|
||||||
module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
|
|
||||||
module:setFirstMarkerByWayPointId(info.markerId)
|
|
||||||
module:setSecondMarkerByWayPointId(store.id)
|
|
||||||
module:setStartHelper(true)
|
|
||||||
module.usedHelper = adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
|
||||||
if vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished ~= nil then
|
|
||||||
vehicle.ad.settings.enableParkAtJobFinished.current = 2
|
|
||||||
vehicle.ad.settings.enableParkAtJobFinished.new = 2
|
|
||||||
end
|
|
||||||
module:raiseDirtyFlag()
|
|
||||||
log(field.id, "læssevogn '%s' aflæsser i %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
|
|
||||||
end
|
|
||||||
local point = info.points[1] or {x = info.x, z = info.z}
|
|
||||||
assignment.phase = "starting"
|
|
||||||
assignment.since = ADFieldJobs.now
|
|
||||||
local source = ADFieldModel.courseSource(field, assignment.role)
|
|
||||||
local course = source ~= nil and sourceCourse(adEnv, field, source) or nil
|
|
||||||
if source ~= nil and course == nil then
|
|
||||||
log(field.id, "intet %s-kursus kendt — Courseplay laver et nyt kursus til %s", source, assignment.role)
|
|
||||||
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 handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason)
|
|
||||||
if ok then
|
|
||||||
assignment.phase = "working"
|
|
||||||
assignment.since = ADFieldJobs.now
|
|
||||||
if assignment.role == "mower" then
|
|
||||||
note(field.id, {mowWidth = assignment.width, mowVehicleId = assignment.vehicleId})
|
|
||||||
rememberCourse(field, "mower", vehicle, assignment.vehicleId)
|
|
||||||
elseif assignment.role == "rake" then
|
|
||||||
rememberCourse(field, "rake", vehicle, assignment.vehicleId)
|
|
||||||
end
|
|
||||||
log(field.id, "Courseplay startet (%s%s)", assignment.role, reason == "copied" and (", " .. tostring(source) .. "-kursus kopieret") or "")
|
|
||||||
else
|
|
||||||
release(assignment)
|
|
||||||
note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(reason)}})
|
|
||||||
log(field.id, "Courseplay fejlede: %s", tostring(reason))
|
|
||||||
end
|
|
||||||
end, course)
|
|
||||||
if handle == nil then
|
|
||||||
release(assignment)
|
|
||||||
log(field.id, "Courseplay kunne ikke startes: %s", tostring(err))
|
|
||||||
note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(err)}})
|
|
||||||
return
|
|
||||||
end
|
|
||||||
assignment.handle = handle
|
|
||||||
end
|
|
||||||
|
|
||||||
local function follow(adEnv, assignment)
|
|
||||||
local vehicle = assignment.vehicle
|
|
||||||
-- Kun når spilleren selv KØRER (hverken Courseplay eller AutoDrive styrer): at sætte sig ind for at
|
|
||||||
-- kigge på HUD'en mens CP slår, må ikke slippe sættet (in-game 25/9: F22 blev aldrig sendt hjem).
|
|
||||||
local steeredByAi = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) or assignment.phase == "starting"
|
|
||||||
-- ...og at sidde stille i førersædet når AutoDrive lige er nået frem, er heller ikke at køre selv
|
|
||||||
-- (in-game 25/9: læssevognen blev sluppet ved ankomsten til F22). Spilleren kører = den bevæger sig.
|
|
||||||
local speed = vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() or 0
|
|
||||||
if isControlledByPlayer(vehicle) and not steeredByAi and speed > ADFieldJobs.PLAYER_DRIVING_KMH then
|
|
||||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
|
||||||
log(assignment.fieldId, "'%s' frigivet — spilleren kører det", ADRuns.vehicleLabel(vehicle))
|
|
||||||
return release(assignment)
|
|
||||||
end
|
|
||||||
local field = ADFieldJobs.getField(assignment.fieldId)
|
|
||||||
local info = ADFields.getInfo(assignment.fieldId)
|
|
||||||
-- hold sættet vågent mens vi har det (AutoDrive-kørsel, CP-start, arbejde, AD-aflæsning)
|
|
||||||
wake(vehicle)
|
|
||||||
if assignment.phase == "driving" then
|
|
||||||
if not isAdActive(vehicle) and ADFieldJobs.now - assignment.since > 2000 then
|
|
||||||
local x, z = vehiclePosition(vehicle)
|
|
||||||
local wayPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
|
|
||||||
local distance = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) or math.huge
|
|
||||||
if distance <= ADFieldJobs.ARRIVE_DISTANCE then
|
|
||||||
beginWork(adEnv, field, info, assignment)
|
|
||||||
else
|
|
||||||
log(assignment.fieldId, "'%s' nåede ikke %s (%.0f m væk)", ADRuns.vehicleLabel(vehicle), tostring(info.markerName), distance)
|
|
||||||
release(assignment)
|
|
||||||
note(assignment.fieldId, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS,
|
|
||||||
reason = {text = string.format("%s nåede ikke %s", ADRuns.vehicleLabel(vehicle), tostring(info.markerName))}})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif assignment.phase == "starting" then
|
|
||||||
if ADFieldJobs.now - assignment.since > ADFieldJobs.START_TIMEOUT_MS then
|
|
||||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
|
||||||
release(assignment)
|
|
||||||
note(assignment.fieldId, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = "Courseplay svarede ikke (60 s)"}})
|
|
||||||
log(assignment.fieldId, "Courseplay svarede ikke inden 60 s")
|
|
||||||
end
|
|
||||||
elseif assignment.phase == "working" then
|
|
||||||
if assignment.role == "rake" and field.windrowOffset == nil and ADFieldJobs.now - assignment.since >= ADFieldJobs.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
|
|
||||||
local busy = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle)
|
|
||||||
if busy then
|
|
||||||
assignment.idleSince = nil
|
|
||||||
else
|
|
||||||
assignment.idleSince = assignment.idleSince or ADFieldJobs.now
|
|
||||||
local rest = assignment.role == "collector" and assignment.storeId ~= nil and not assignment.deliveredRest and restLoad(adEnv, vehicle) or 0
|
|
||||||
if rest > 0 and ADFieldJobs.now - assignment.idleSince >= ADFieldJobs.IDLE_CONFIRM_MS then
|
|
||||||
-- CP færdig med læs på: AD tømmer kun selv med parkering sat, så vi sender resten til silo
|
|
||||||
assignment.deliveredRest = true
|
|
||||||
assignment.idleSince = nil
|
|
||||||
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, assignment.storeId, nil, false)
|
|
||||||
log(assignment.fieldId, "'%s' kører rest-læs (%.0f l) til silo", ADRuns.vehicleLabel(vehicle), rest)
|
|
||||||
elseif ADFieldJobs.now - assignment.idleSince >= ADFieldJobs.IDLE_CONFIRM_MS then
|
|
||||||
log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
|
|
||||||
local park = assignment.role ~= "collector" and parkMarkerId(adEnv, vehicle) or nil
|
|
||||||
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil, false) end
|
|
||||||
restoreToolOffset(assignment)
|
|
||||||
assignment.phase = "finished"
|
|
||||||
ADFieldJobs.finished = ADFieldJobs.finished or {}
|
|
||||||
table.insert(ADFieldJobs.finished, assignment)
|
|
||||||
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ADFieldJobs.stopField(adEnv, id)
|
|
||||||
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(id)) do
|
|
||||||
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
|
||||||
pcall(stopAd, assignment.vehicle)
|
|
||||||
pcall(function() if assignment.vehicle.stopCurrentAIJob ~= nil and ADCpBridge.isWorking(assignment.vehicle) then assignment.vehicle:stopCurrentAIJob(AIMessageSuccessStoppedByUser.new()) end end)
|
|
||||||
release(assignment)
|
|
||||||
end
|
|
||||||
forgetFinished(id)
|
|
||||||
setField(ADFieldModel.stop(ADFieldJobs.getField(id)))
|
|
||||||
ADFieldJobs.save()
|
|
||||||
log(id, "stoppet fra menuen")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function finishedFor(fieldId)
|
local function finishedFor(fieldId)
|
||||||
local list = {}
|
local list = {}
|
||||||
for _, assignment in ipairs(ADFieldJobs.finished or {}) do
|
for _, assignment in ipairs(ADFieldJobs.finished) do
|
||||||
if assignment.fieldId == fieldId then table.insert(list, assignment) end
|
if assignment.fieldId == fieldId then table.insert(list, assignment) end
|
||||||
end
|
end
|
||||||
return list
|
return list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ADFieldJobs.stopField(adEnv, id)
|
||||||
|
ADFieldWork.stopAll(adEnv, id)
|
||||||
|
ADFieldJobs.forgetFinished(id)
|
||||||
|
ADFieldJobs.setField(ADFieldModel.stop(ADFieldJobs.getField(id)))
|
||||||
|
ADFieldJobs.save()
|
||||||
|
ADFieldJobs.log(id, "stoppet fra menuen")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Seneste beslutning pr. mark (GUI viser pleje-behov og grunde).
|
||||||
|
ADFieldJobs.lastDecision = {}
|
||||||
|
|
||||||
local function stepField(adEnv, info)
|
local function stepField(adEnv, info)
|
||||||
local field = ADFieldModel.onReadiness(ADFieldJobs.getField(info.id), info.readyRatio or 0)
|
local field = ADFieldModel.onReadiness(ADFieldJobs.getField(info.id), info.readyRatio or 0)
|
||||||
setField(field)
|
ADFieldJobs.setField(field)
|
||||||
local assignments = ADFieldJobs.assignmentsFor(info.id)
|
local assignments = ADFieldJobs.assignmentsFor(info.id)
|
||||||
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
|
||||||
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now)
|
local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now)
|
||||||
|
ADFieldJobs.lastDecision[info.id] = decision
|
||||||
if decision.action == "start" then
|
if decision.action == "start" then
|
||||||
setField(ADFieldModel.start(field))
|
local started = ADFieldModel.start(field, info.kind, info.needs)
|
||||||
log(info.id, "starter (%s)", field.plan.product)
|
started.lastFruit = info.kind == "harvest" and info.fruitName or field.lastFruit
|
||||||
|
ADFieldJobs.setField(started)
|
||||||
|
ADFieldJobs.log(info.id, "starter (%s: %s)", tostring(info.kind), started.stage)
|
||||||
|
ADFieldJobs.save()
|
||||||
|
elseif decision.action == "care" then
|
||||||
|
ADFieldJobs.setField(ADFieldModel.startCare(field, decision.stage))
|
||||||
|
ADFieldJobs.log(info.id, "pleje: %s", decision.stage)
|
||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
elseif decision.action == "advance" then
|
elseif decision.action == "advance" then
|
||||||
local nextField = ADFieldModel.advance(field)
|
local nextField = ADFieldModel.advance(field, info.needs)
|
||||||
setField(nextField)
|
ADFieldJobs.setField(nextField)
|
||||||
forgetFinished(info.id)
|
ADFieldJobs.forgetFinished(info.id)
|
||||||
log(info.id, "%s -> %s", field.stage, nextField.stage)
|
ADFieldWork.windDownHelpers(adEnv, info.id, field.stage)
|
||||||
|
ADFieldJobs.log(info.id, "%s -> %s", field.stage, nextField.stage)
|
||||||
ADFieldJobs.save()
|
ADFieldJobs.save()
|
||||||
elseif decision.action == "dispatch" then
|
elseif decision.action == "dispatch" then
|
||||||
dispatch(adEnv, field, info, decision.role)
|
ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -524,16 +207,14 @@ function ADFieldJobs.tick(adEnv, dt)
|
|||||||
local farmId = g_currentMission:getFarmId()
|
local farmId = g_currentMission:getFarmId()
|
||||||
ADFields.scanNext(adEnv, farmId)
|
ADFields.scanNext(adEnv, farmId)
|
||||||
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
for _, assignment in pairs(ADFieldJobs.state.assignments) do
|
||||||
local ok, err = pcall(follow, adEnv, assignment)
|
local ok, err = pcall(ADFieldWork.follow, adEnv, assignment)
|
||||||
if not ok then
|
if not ok then
|
||||||
Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s opfølgning fejlede: %s", tostring(assignment.fieldId), tostring(err))
|
Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s opfølgning fejlede: %s", tostring(assignment.fieldId), tostring(err))
|
||||||
release(assignment)
|
ADFieldWork.release(assignment)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
for _, info in ipairs(ADFields.list()) do
|
for _, info in ipairs(ADFields.list()) do
|
||||||
if info.mowable or ADFieldModel.isWorking(ADFieldJobs.getField(info.id)) then
|
|
||||||
local ok, err = pcall(stepField, adEnv, info)
|
local ok, err = pcall(stepField, adEnv, info)
|
||||||
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,561 @@
|
|||||||
|
-- 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
|
||||||
|
|
||||||
|
local function log(...) ADFieldJobs.log(...) end
|
||||||
|
local function note(...) ADFieldJobs.note(...) end
|
||||||
|
local function now() return ADFieldJobs.now end
|
||||||
|
|
||||||
|
-- ---- AutoDrive ----
|
||||||
|
|
||||||
|
local function stateModule(vehicle)
|
||||||
|
return vehicle.ad ~= nil and vehicle.ad.stateModule or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isAdActive(vehicle)
|
||||||
|
local module = stateModule(vehicle)
|
||||||
|
return module ~= nil and module:isActive()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function stopAd(vehicle)
|
||||||
|
if isAdActive(vehicle) then vehicle:stopAutoDrive() end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parkerede køretøjer SOVER i FS25 (motoren kalder ikke onUpdate), så AutoDrives køreopgave står stille,
|
||||||
|
-- til nogen vækker dem (in-game 25/9). Samme greb som flows (applyAndStart) + raiseActive fra AI-Bonden.
|
||||||
|
local function wake(vehicle, kick)
|
||||||
|
if vehicle.raiseActive ~= nil then vehicle:raiseActive() end
|
||||||
|
if kick and vehicle.isServer and SpecializationUtil ~= nil and SpecializationUtil.raiseEvent ~= nil then
|
||||||
|
SpecializationUtil.raiseEvent(vehicle, "onUpdate", 16, false, false, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function enableParkAtJobFinished(vehicle)
|
||||||
|
local setting = vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished or nil
|
||||||
|
if setting ~= nil then
|
||||||
|
setting.current = 2
|
||||||
|
setting.new = 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function helperCp(adEnv)
|
||||||
|
return adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function startAd(adEnv, vehicle, mode, firstId, secondId)
|
||||||
|
local module = stateModule(vehicle)
|
||||||
|
stopAd(vehicle)
|
||||||
|
module:setMode(mode)
|
||||||
|
module:setFirstMarkerByWayPointId(firstId)
|
||||||
|
if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end
|
||||||
|
module:setStartHelper(false)
|
||||||
|
if module.setActualFarmId ~= nil then module:setActualFarmId(vehicle:getOwnerFarmId()) end
|
||||||
|
module:raiseDirtyFlag()
|
||||||
|
local current = module:getCurrentMode()
|
||||||
|
if current ~= nil and current.start ~= nil then
|
||||||
|
current:start(adEnv.AutoDrive.USER_PLAYER or 1)
|
||||||
|
else
|
||||||
|
vehicle:startAutoDrive()
|
||||||
|
end
|
||||||
|
wake(vehicle, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- AD-parkering: sidder på redskabet og kopieres af AD til traktorens stateModule (markør-INDEKS).
|
||||||
|
local function parkMarkerId(adEnv, vehicle)
|
||||||
|
local module = stateModule(vehicle)
|
||||||
|
local index = module ~= nil and module.getParkDestinationAtJobFinished ~= nil and module:getParkDestinationAtJobFinished() or nil
|
||||||
|
if index == nil or index < 1 then return nil end
|
||||||
|
local marker = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||||
|
return marker ~= nil and marker.id or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function restLoad(adEnv, vehicle)
|
||||||
|
local ok, level = pcall(function()
|
||||||
|
return (adEnv.AutoDrive.getAllFillLevels(adEnv.AutoDrive.getAllUnits(vehicle)))
|
||||||
|
end)
|
||||||
|
return ok and tonumber(level) or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local function vehiclePosition(vehicle)
|
||||||
|
local x, _, z = getWorldTranslation(vehicle.rootNode)
|
||||||
|
return x, z
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isControlledByPlayer(vehicle)
|
||||||
|
return (vehicle.getIsControlled ~= nil and vehicle:getIsControlled()) or false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- flåde ----
|
||||||
|
|
||||||
|
function ADFieldWork.getRigs(adEnv)
|
||||||
|
local rigs = {}
|
||||||
|
local state = ADFieldJobs.state
|
||||||
|
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
|
||||||
|
local specs = ADCpBridge.getSpecs(vehicle)
|
||||||
|
local role = ADFieldFleet.roleFromSpecs(specs)
|
||||||
|
local id = ADRunsController.getVehicleId(vehicle)
|
||||||
|
if role ~= nil and id ~= nil then
|
||||||
|
local x, z = vehiclePosition(vehicle)
|
||||||
|
local rig = {
|
||||||
|
id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle),
|
||||||
|
enabled = state.fleet[id] == true,
|
||||||
|
-- optaget: vores egen opgave, et Smart Pickup-flow, eller AutoDrive/Courseplay kører den allerede
|
||||||
|
-- (fx spillerens egen parkeringstur) — den må ikke kapres midt i en anden kørsel
|
||||||
|
busy = state.assignments[id] ~= nil or ADRunsController.getRunForVehicle(vehicle) ~= nil
|
||||||
|
or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle),
|
||||||
|
controlled = isControlledByPlayer(vehicle),
|
||||||
|
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
||||||
|
}
|
||||||
|
if role == "harvester" then rig.fruits = ADCpBridge.getHarvestFruits(vehicle) or {} end
|
||||||
|
if role == "seeder" then
|
||||||
|
rig.seedFruits = ADCpBridge.getSeedFruits(vehicle)
|
||||||
|
rig.hasSeeds = ADCpBridge.hasSeeds(vehicle)
|
||||||
|
end
|
||||||
|
if role == "weeder" then rig.mechanical = ADCpBridge.isMechanicalWeeder(vehicle) end
|
||||||
|
table.insert(rigs, rig)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return rigs
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ---- lager ----
|
||||||
|
|
||||||
|
-- Gårdens gæringssiloer for markens skår, nærmest marken først (produktioner skår -> SILAGE).
|
||||||
|
function ADFieldWork.getFermenters(adEnv, farmId, windrowFillType, x, z)
|
||||||
|
local silage = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeIndexByName("SILAGE") or nil
|
||||||
|
local result = {}
|
||||||
|
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||||
|
local spec = building.kind == "factory" and building.placeable ~= nil and building.placeable.spec_productionPoint or nil
|
||||||
|
local point = spec ~= nil and spec.productionPoint or nil
|
||||||
|
if point ~= nil and ADFieldModel.isFermenter(point.productions, windrowFillType, silage) then
|
||||||
|
local bx, _, bz = getWorldTranslation(building.placeable.rootNode)
|
||||||
|
table.insert(result, {building = building, distance = (bx - (x or bx)) ^ 2 + (bz - (z or bz)) ^ 2})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(result, function(a, b) return a.distance < b.distance end)
|
||||||
|
local buildings = {}
|
||||||
|
for _, entry in ipairs(result) do table.insert(buildings, entry.building) end
|
||||||
|
return buildings
|
||||||
|
end
|
||||||
|
|
||||||
|
local function unloadMarkerIds(buildings)
|
||||||
|
local markerIds = {}
|
||||||
|
for _, building in ipairs(buildings) do
|
||||||
|
for _, marker in ipairs(ADBuildings.getMarkers(building, "unload") or {}) do
|
||||||
|
table.insert(markerIds, marker.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return markerIds
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hvilken vare markens trin fører til lager: skår (ensilage), tørskår (hø) eller høstet afgrøde.
|
||||||
|
function ADFieldWork.storeFillType(field, info)
|
||||||
|
if field.stage == "harvesting" then return info.harvestFillType end
|
||||||
|
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"): 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 vehicleId = source == "rake" and field.rakeVehicleId or field.mowVehicleId
|
||||||
|
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))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hvad et sæt skal kunne ud over rollen (afgrøde, frø, ukrudtets størrelse).
|
||||||
|
local function acceptFor(field, info, role)
|
||||||
|
if role == "harvester" then
|
||||||
|
local fruit = info.fruitIndex
|
||||||
|
return function(candidate) return fruit ~= nil and candidate.fruits ~= nil and candidate.fruits[fruit] == true end
|
||||||
|
elseif role == "seeder" then
|
||||||
|
local fruitIndex = ADFieldWork.sowFruitIndex(field)
|
||||||
|
return function(candidate) return candidate.hasSeeds and fruitIndex ~= nil and candidate.seedFruits[fruitIndex] == true end
|
||||||
|
elseif role == "weeder" then
|
||||||
|
local large = (info.weed or 0) > ADFieldModel.WEED_MECHANICAL_MAX
|
||||||
|
return function(candidate) return not (large and candidate.mechanical) end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldWork.sowFruitIndex(field)
|
||||||
|
local name = ADFieldModel.sowFruit(field)
|
||||||
|
if name == nil or g_fruitTypeManager == nil then return nil end
|
||||||
|
local fruit = g_fruitTypeManager:getFruitTypeByName(name)
|
||||||
|
return fruit ~= nil and fruit.index or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kan trinnet overhovedet begynde? (såning: afgrøde valgt og i såperioden)
|
||||||
|
local function preflight(field, role)
|
||||||
|
if role == "seeder" then
|
||||||
|
local fruitIndex = ADFieldWork.sowFruitIndex(field)
|
||||||
|
if fruitIndex == nil then return false, {key = "spu_field_reason_noSowFruit"} end
|
||||||
|
if not ADCpBridge.isPlantableNow(fruitIndex) then
|
||||||
|
return false, {key = "spu_field_reason_notPlantable", name = ADFieldModel.sowFruit(field)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldWork.dispatch(adEnv, field, info, decision)
|
||||||
|
local role = decision.role
|
||||||
|
local ok, reason = preflight(field, role)
|
||||||
|
if not ok then
|
||||||
|
note(field.id, {retryAt = now() + ADFieldModel.RETRY_MS, reason = reason})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local candidates = ADFieldWork.getRigs(adEnv)
|
||||||
|
local required = ADFieldModel.requiredWidth(field, role)
|
||||||
|
local accept = acceptFor(field, info, role)
|
||||||
|
local rigs, why, args
|
||||||
|
if (decision.count or 1) > 1 then
|
||||||
|
rigs, why, args = ADFieldFleet.pickMany(candidates, role, decision.count, info.x, info.z, required, accept)
|
||||||
|
else
|
||||||
|
local rig
|
||||||
|
rig, why, args = ADFieldFleet.pick(candidates, role, info.x, info.z, required, accept)
|
||||||
|
rigs = rig ~= nil and {rig} or nil
|
||||||
|
end
|
||||||
|
if rigs == nil then
|
||||||
|
note(field.id, {reason = {key = "spu_field_reason_" .. tostring(why), role = role, args = args}})
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local count = #rigs
|
||||||
|
local lanes = ADFieldModel.lanePositions(count)
|
||||||
|
for index, rig in ipairs(rigs) do
|
||||||
|
local assignment = {
|
||||||
|
vehicleId = rig.id, vehicle = rig.vehicle, fieldId = field.id, role = role, stage = field.stage,
|
||||||
|
helper = decision.helper == true, phase = "driving", since = now(), width = rig.width,
|
||||||
|
groupSize = decision.helper and 1 or count, lane = count > 1 and lanes[index] or nil, leader = index == 1,
|
||||||
|
fillType = decision.helper and info.harvestFillType or nil,
|
||||||
|
}
|
||||||
|
ADFieldJobs.state.assignments[rig.id] = assignment
|
||||||
|
startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
|
||||||
|
log(field.id, "%s '%s' sendt til %s (%.1f m%s)", role, rig.label, tostring(info.markerName), rig.width or 0,
|
||||||
|
assignment.lane ~= nil and string.format(", bane %d", assignment.lane) or "")
|
||||||
|
end
|
||||||
|
note(field.id, {reason = false})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Læssevogn: AutoDrive overtager ved "fuld" (Hent og aflever F -> lager, hjælper = Courseplay).
|
||||||
|
local function prepareCollector(adEnv, field, info, assignment)
|
||||||
|
local vehicle = assignment.vehicle
|
||||||
|
local store, why = ADFieldWork.chooseStore(adEnv, field, info, vehicle, ADFieldWork.storeFillType(field, info))
|
||||||
|
if store == nil then
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
fail(field.id, "intet lager: " .. tostring(why))
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
assignment.storeId = store.id
|
||||||
|
local module = stateModule(vehicle)
|
||||||
|
module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
|
||||||
|
module:setFirstMarkerByWayPointId(info.markerId)
|
||||||
|
module:setSecondMarkerByWayPointId(store.id)
|
||||||
|
module:setStartHelper(true)
|
||||||
|
module.usedHelper = helperCp(adEnv)
|
||||||
|
enableParkAtJobFinished(vehicle)
|
||||||
|
module:raiseDirtyFlag()
|
||||||
|
log(field.id, "læssevogn '%s' aflæsser i %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
-- Multi-tool: følgere venter på lederens kursus for trinnet og kopierer det med deres bane.
|
||||||
|
local function courseOptions(adEnv, field, assignment)
|
||||||
|
local source = ADFieldModel.courseSource(field, assignment.role)
|
||||||
|
if source ~= nil then
|
||||||
|
local course = sourceCourse(adEnv, field, source)
|
||||||
|
if course == nil then
|
||||||
|
log(field.id, "intet %s-kursus kendt — Courseplay laver et nyt kursus til %s", source, assignment.role)
|
||||||
|
end
|
||||||
|
return {course = course, lane = assignment.lane}, source
|
||||||
|
end
|
||||||
|
if (assignment.groupSize or 1) > 1 then
|
||||||
|
if assignment.leader then
|
||||||
|
return {multiTools = assignment.groupSize, lane = assignment.lane}, nil
|
||||||
|
end
|
||||||
|
local shared = coursesOf(field.id)[field.stage]
|
||||||
|
if shared == nil then return nil, nil end
|
||||||
|
return {course = shared, lane = assignment.lane}, "multi"
|
||||||
|
end
|
||||||
|
return {}, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function beginWork(adEnv, field, info, assignment)
|
||||||
|
local vehicle = assignment.vehicle
|
||||||
|
if assignment.role == "unloader" then return beginUnloader(adEnv, field, info, assignment) end
|
||||||
|
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" and not prepareCollector(adEnv, field, info, assignment) then return 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)
|
||||||
|
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
|
||||||
|
log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
|
||||||
|
-- læssevognen parkeres af AutoDrive selv (onCpFinished med parkering sat)
|
||||||
|
local park = assignment.role ~= "collector" and parkMarkerId(adEnv, vehicle) or nil
|
||||||
|
if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
|
||||||
|
restoreToolOffset(assignment)
|
||||||
|
assignment.phase = "finished"
|
||||||
|
table.insert(ADFieldJobs.finished, assignment)
|
||||||
|
ADFieldJobs.state.assignments[assignment.vehicleId] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function followWorking(adEnv, field, assignment)
|
||||||
|
local vehicle = assignment.vehicle
|
||||||
|
if assignment.role == "rake" and field.windrowOffset == nil and now() - assignment.since >= ADFieldWork.WINDROW_MEASURE_MS
|
||||||
|
and ADCpBridge.isWorking(vehicle) then
|
||||||
|
local offset = ADCpBridge.getWindrowOffset(vehicle)
|
||||||
|
note(assignment.fieldId, {windrowOffset = offset or 0})
|
||||||
|
log(assignment.fieldId, offset ~= nil and "river lægger skåret %.1f m fra sin kursuslinje (venstre +)" or "river: nedlægningsområde ikke fundet — regner med midten (%.1f m)", offset or 0)
|
||||||
|
end
|
||||||
|
if ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) then
|
||||||
|
assignment.idleSince = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
assignment.idleSince = assignment.idleSince or now()
|
||||||
|
if now() - assignment.idleSince < ADFieldWork.IDLE_CONFIRM_MS then return end
|
||||||
|
local rest = assignment.storeId ~= nil and not assignment.deliveredRest and restLoad(adEnv, vehicle) or 0
|
||||||
|
if rest > 0 then
|
||||||
|
-- CP færdig/stoppet med læs på: AD tømmer kun selv med parkering sat, så vi sender resten til lager
|
||||||
|
assignment.deliveredRest = true
|
||||||
|
assignment.idleSince = nil
|
||||||
|
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, assignment.storeId, nil)
|
||||||
|
log(assignment.fieldId, "'%s' kører rest-læs (%.0f l) til lager", ADRuns.vehicleLabel(vehicle), rest)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
finishAssignment(adEnv, assignment)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldWork.follow(adEnv, assignment)
|
||||||
|
local vehicle = assignment.vehicle
|
||||||
|
-- Kun når spilleren selv KØRER (bevæger sig uden Courseplay/AutoDrive) slippes sættet — at sidde i
|
||||||
|
-- førersædet og kigge må ikke slippe det (in-game 25/9).
|
||||||
|
local steeredByAi = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle) or assignment.phase == "starting"
|
||||||
|
local speed = vehicle.getLastSpeed ~= nil and vehicle:getLastSpeed() or 0
|
||||||
|
if isControlledByPlayer(vehicle) and not steeredByAi and speed > ADFieldWork.PLAYER_DRIVING_KMH then
|
||||||
|
log(assignment.fieldId, "'%s' frigivet — spilleren kører det", ADRuns.vehicleLabel(vehicle))
|
||||||
|
return ADFieldWork.release(assignment)
|
||||||
|
end
|
||||||
|
local field = ADFieldJobs.getField(assignment.fieldId)
|
||||||
|
local info = ADFields.getInfo(assignment.fieldId)
|
||||||
|
wake(vehicle)
|
||||||
|
if assignment.phase == "driving" then
|
||||||
|
if not isAdActive(vehicle) and now() - assignment.since > 2000 then
|
||||||
|
local x, z = vehiclePosition(vehicle)
|
||||||
|
local wayPoint = info ~= nil and adEnv.ADGraphManager:getWayPointById(info.markerId) or nil
|
||||||
|
local distance = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) or math.huge
|
||||||
|
if distance <= ADFieldWork.ARRIVE_DISTANCE then
|
||||||
|
beginWork(adEnv, field, info, assignment)
|
||||||
|
else
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
fail(assignment.fieldId, string.format("%s nåede ikke %s (%.0f m væk)", ADRuns.vehicleLabel(vehicle), tostring(info ~= nil and info.markerName or "?"), distance))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif assignment.phase == "waitingCourse" then
|
||||||
|
if now() - assignment.since > ADFieldWork.COURSE_WAIT_MS then
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
fail(assignment.fieldId, "multi-tool: lederens kursus kom ikke (3 min)")
|
||||||
|
elseif coursesOf(field.id)[field.stage] ~= nil then
|
||||||
|
beginWork(adEnv, field, info, assignment)
|
||||||
|
end
|
||||||
|
elseif assignment.phase == "starting" then
|
||||||
|
if now() - assignment.since > ADFieldWork.START_TIMEOUT_MS then
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
fail(assignment.fieldId, "Courseplay svarede ikke (60 s)")
|
||||||
|
end
|
||||||
|
elseif assignment.phase == "working" then
|
||||||
|
followWorking(adEnv, field, assignment)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Høsten er færdig: aflæsningsvognene stoppes; har de rest, kører den til lager, ellers parkering.
|
||||||
|
function ADFieldWork.windDownHelpers(adEnv, fieldId, stage)
|
||||||
|
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
||||||
|
if assignment.helper and assignment.stage == stage then
|
||||||
|
if assignment.handle ~= nil then assignment.handle.cancelled = true end
|
||||||
|
ADCpBridge.stop(assignment.vehicle)
|
||||||
|
assignment.phase = "working"
|
||||||
|
assignment.idleSince = nil
|
||||||
|
log(fieldId, "aflæsningsvogn '%s' stoppes — høsten er færdig", ADRuns.vehicleLabel(assignment.vehicle))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldWork.stopAll(adEnv, fieldId)
|
||||||
|
for _, assignment in ipairs(ADFieldJobs.assignmentsFor(fieldId)) do
|
||||||
|
pcall(stopAd, assignment.vehicle)
|
||||||
|
ADCpBridge.stop(assignment.vehicle)
|
||||||
|
ADFieldWork.release(assignment)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -14,7 +14,7 @@ SmartPickupFrame.ROLE_KEYS = {mower = "spu_field_role_mower", tedder = "spu_fiel
|
|||||||
function SmartPickupFrame:fieldReasonText(reason)
|
function SmartPickupFrame:fieldReasonText(reason)
|
||||||
if type(reason) ~= "table" then return nil end
|
if type(reason) ~= "table" then return nil end
|
||||||
if reason.text ~= nil then return reason.text end
|
if reason.text ~= nil then return reason.text end
|
||||||
local role = reason.role ~= nil and self.i18n:getText(SmartPickupFrame.ROLE_KEYS[reason.role] or "spu_field_role_mower") or ""
|
local role = reason.name or (reason.role ~= nil and self.i18n:getText(SmartPickupFrame.ROLE_KEYS[reason.role] or "spu_field_role_mower")) or ""
|
||||||
local template = self.i18n:getText(reason.key)
|
local template = self.i18n:getText(reason.key)
|
||||||
local args = reason.args or {}
|
local args = reason.args or {}
|
||||||
local ok, text = pcall(string.format, template, role, args[1] or 0, args[2] or 0)
|
local ok, text = pcall(string.format, template, role, args[1] or 0, args[2] or 0)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<sourceFile filename="adFields.lua"/>
|
<sourceFile filename="adFields.lua"/>
|
||||||
<sourceFile filename="adCpBridge.lua"/>
|
<sourceFile filename="adCpBridge.lua"/>
|
||||||
<sourceFile filename="adFieldStorage.lua"/>
|
<sourceFile filename="adFieldStorage.lua"/>
|
||||||
|
<sourceFile filename="adFieldWork.lua"/>
|
||||||
<sourceFile filename="adFieldJobs.lua"/>
|
<sourceFile filename="adFieldJobs.lua"/>
|
||||||
<sourceFile filename="adSmartPickup.lua"/>
|
<sourceFile filename="adSmartPickup.lua"/>
|
||||||
</extraSourceFiles>
|
</extraSourceFiles>
|
||||||
|
|||||||
@@ -43,5 +43,37 @@ check("J7 slået uden sæt -> videre", J.decide(mowing, {id = 109, markerId = 5,
|
|||||||
check("J7 stående uden sæt -> slå", J.decide(mowing, info, {}, 0).action, "dispatch")
|
check("J7 stående uden sæt -> slå", J.decide(mowing, info, {}, 0).action, "dispatch")
|
||||||
check("J7 slået men sæt i gang -> vent", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {{phase = "working"}}, 0).action, "none")
|
check("J7 slået men sæt i gang -> vent", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.1}, {{phase = "working"}}, 0).action, "none")
|
||||||
|
|
||||||
|
-- J8: høst — høster først (hovedrolle), aflæsningsvogne kun mens høsteren arbejder
|
||||||
|
local harvestInfo = {id = 50, markerId = 7, kind = "harvest", readyRatio = 1, fruitIndex = 3}
|
||||||
|
local harvesting = M.start(M.onReadiness(M.newField(50), 1), "harvest", {})
|
||||||
|
local d = J.decide(harvesting, harvestInfo, {}, 0)
|
||||||
|
check("J8 send høster", d.role, "harvester"); check("J8 ikke hjælper", d.helper, false)
|
||||||
|
local withHarvester = {{phase = "working", stage = "harvesting", helper = false}}
|
||||||
|
local dHelper = J.decide(harvesting, harvestInfo, withHarvester, 0)
|
||||||
|
check("J8 så aflæsningsvogn", dHelper.role, "unloader"); check("J8 hjælper", dHelper.helper, true)
|
||||||
|
check("J8 vogn på plads -> intet", J.decide(harvesting, harvestInfo, {withHarvester[1], {phase = "working", stage = "harvesting", helper = true}}, 0).action, "none")
|
||||||
|
-- høsteren færdig, afgrøden væk (marken er nu bar) -> videre, selv om vognen stadig kører
|
||||||
|
local afterInfo = {id = 50, markerId = 7, kind = "bare", readyRatio = 0}
|
||||||
|
check("J8 høst færdig -> videre", J.decide(harvesting, afterInfo, {{phase = "finished", stage = "harvesting"}, {phase = "working", stage = "harvesting", helper = true}}, 0).action, "advance")
|
||||||
|
-- sæt fra et tidligere trin (vogn på vej hjem) tæller ikke på det nye trin
|
||||||
|
local cultivating = M.advance(harvesting, {})
|
||||||
|
check("J8 gammel vogn tæller ikke", J.decide(cultivating, afterInfo, {{phase = "working", stage = "harvesting", helper = true}}, 0).role, "cultivator")
|
||||||
|
|
||||||
|
-- J9: multi-tool — hele holdet sendes samlet; mens nogen kører, sendes ingen nye hovedsæt
|
||||||
|
local multiMow = M.with(mowing, {plan = {rigs = 3}})
|
||||||
|
local dMulti = J.decide(multiMow, info, {}, 0)
|
||||||
|
check("J9 tre slåmaskiner", dMulti.count, 3)
|
||||||
|
check("J9 én kører stadig -> ingen nye", J.decide(multiMow, info, {{phase = "working"}, {phase = "finished"}}, 0).action, "none")
|
||||||
|
|
||||||
|
-- J10: pleje under vækst — Auto starter, Manuel viser behovet
|
||||||
|
local growingInfo = {id = 60, markerId = 8, kind = "harvest", growing = true, fert = 0, fertMax = 2, lime = 2, weed = 0, readyRatio = 0.2}
|
||||||
|
local autoField = M.with(M.newField(60), {plan = {mode = "auto"}})
|
||||||
|
local care = J.decide(autoField, growingInfo, {}, 0)
|
||||||
|
check("J10 auto -> pleje", care.action, "care"); check("J10 gødning", care.stage, "fertilizing")
|
||||||
|
local manualCare = J.decide(M.newField(60), growingInfo, {}, 0)
|
||||||
|
check("J10 manuel -> intet, men behov vist", manualCare.action, "none"); check("J10 behov", manualCare.care, "fertilizing")
|
||||||
|
check("J10 Start nu -> pleje", J.decide(M.with(M.newField(60), {startRequested = true}), growingInfo, {}, 0).action, "care")
|
||||||
|
check("J10 uden markør -> vent", J.decide(autoField, {id = 60, kind = "harvest", growing = true, fert = 0, fertMax = 2}, {}, 0).action, "wait")
|
||||||
|
|
||||||
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
os.exit(failures == 0 and 0 or 1)
|
os.exit(failures == 0 and 0 or 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user