feat(pulje): markarbejdet sender en solo-traktor efter et puljeredskab når ingen færdig kombination er ledig (fase fetching)

This commit is contained in:
2026-09-28 18:05:42 +02:00
parent cdd253ee5f
commit 2d170a30ff
5 changed files with 106 additions and 5 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ ADFieldJobs.now = 0
ADFieldJobs.loaded = false ADFieldJobs.loaded = false
-- awaitingPlayer: høster med manuel transport, spilleren kører den til marken (tæller som i gang) -- awaitingPlayer: høster med manuel transport, spilleren kører den til marken (tæller som i gang)
ADFieldJobs.ACTIVE_PHASES = {driving = true, starting = true, waitingCourse = true, working = true, awaitingPlayer = true} ADFieldJobs.ACTIVE_PHASES = {fetching = true, driving = true, starting = true, waitingCourse = true, working = true, awaitingPlayer = true}
-- Andel græs i mellemhøjde (høstbart, men halv ydelse) der får en afbrudt slåning til at vente. -- Andel græs i mellemhøjde (høstbart, men halv ydelse) der får en afbrudt slåning til at vente.
ADFieldJobs.REGROW_RATIO = 0.1 ADFieldJobs.REGROW_RATIO = 0.1
+95 -4
View File
@@ -129,6 +129,13 @@ end
-- ---- flåde ---- -- ---- 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))
end
function ADFieldWork.getRigs(adEnv) function ADFieldWork.getRigs(adEnv)
local rigs = {} local rigs = {}
local state = ADFieldJobs.state local state = ADFieldJobs.state
@@ -144,12 +151,12 @@ function ADFieldWork.getRigs(adEnv)
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id), enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
-- optaget: vores egen opgave, eller AutoDrive/Courseplay kører den allerede (fx spillerens -- 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 -- egen parkeringstur) — den må ikke kapres midt i en anden kørsel
busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle) busy = isBusyVehicle(vehicle, id),
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),
-- hvorfor optaget (til loggen, når ingen maskine kan sendes ud) -- hvorfor optaget (til loggen, når ingen maskine kan sendes ud)
busyWhy = (state.assignments[id] ~= nil and ("opgave F" .. tostring(state.assignments[id].fieldId))) 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 (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 nil, 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), controlled = isControlledByPlayer(vehicle),
-- manuel transport: spilleren kører den til marken (for bred til vejene med skærebord) -- manuel transport: spilleren kører den til marken (for bred til vejene med skærebord)
manual = (state.manual or {})[id] == true, manual = (state.manual or {})[id] == true,
@@ -279,6 +286,7 @@ function ADFieldWork.release(assignment, phase)
restoreToolOffset(assignment) restoreToolOffset(assignment)
if assignment.handle ~= nil then assignment.handle.cancelled = true end if assignment.handle ~= nil then assignment.handle.cancelled = true end
assignment.phase = phase or "released" assignment.phase = phase or "released"
if ADPool ~= nil and assignment.pool ~= nil then ADPool.release(assignment) end
ADFieldJobs.state.assignments[assignment.vehicleId] = nil ADFieldJobs.state.assignments[assignment.vehicleId] = nil
end end
@@ -379,6 +387,44 @@ local function askPlayer(field, info, assignment, withDialog)
end end
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)
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
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 = false, phase = "fetching", since = now(), width = implement.width, groupSize = 1, leader = true,
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) function ADFieldWork.dispatch(adEnv, field, info, decision)
local role = decision.role local role = decision.role
local ok, reason = preflight(field, role) local ok, reason = preflight(field, role)
@@ -414,6 +460,20 @@ function ADFieldWork.dispatch(adEnv, field, info, decision)
rigs = rig ~= nil and {rig} or nil rigs = rig ~= nil and {rig} or nil
end end
if rigs == nil then if rigs == nil then
-- ingen færdig kombination: en ledig solo-traktor henter et puljeredskab (kun hovedopgaver, ikke tømmevogne)
if not decision.helper and (decision.count or 1) == 1 and ADPool ~= nil
and (why == "noRig" or why == "allBusy" or why == "cannotDo") then
local pooled, poolWhy, poolArgs = ADFieldWork.dispatchPool(adEnv, field, info, role, roleAccept)
if pooled then return end
if poolWhy == "tooWeak" then
note(field.id, {reason = {text = string.format("ingen ledig traktor med ≥%.0f hk til %s-redskabet",
(poolArgs or {})[1] or 0, tostring(role))}, retryAt = 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 if #offNetwork > 0 and (why == "cannotDo" or why == "noRig") then
-- de eneste med rollen står uden for AD-vejnettet -- 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)", note(field.id, {reason = {text = string.format("ingen %s kan køre til %s — står for langt fra AutoDrive-vejnettet (%s)",
@@ -1093,7 +1153,38 @@ function ADFieldWork.follow(adEnv, assignment)
local field = ADFieldJobs.getField(assignment.fieldId) local field = ADFieldJobs.getField(assignment.fieldId)
local info = ADFields.getInfo(assignment.fieldId) local info = ADFields.getInfo(assignment.fieldId)
wake(vehicle) wake(vehicle)
if assignment.phase == "driving" then if assignment.phase == "fetching" then
local hitch = assignment.hitch
if not hitch.done then return end
ADPool.release(assignment)
if not hitch.ok then
-- koblingen fejlede: redskabet i karantæne, traktoren hjem, marken prøver igen
ADPool.quarantine(assignment.pool.implementId, now())
ADFieldWork.release(assignment)
note(assignment.fieldId, {retryAt = 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å: bredden med redskabet på (for bred -> hjem igen)
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
assignment.width, assignment.phase, assignment.since = width, "driving", now()
assignment.startX, assignment.startZ = vehiclePosition(vehicle)
startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil)
assignment.drive = {mode = adEnv.AutoDrive.MODE_DRIVETO, first = info.markerId}
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 if not isAdActive(vehicle) and now() - assignment.since > 2000 then
local x, z = vehiclePosition(vehicle) local x, z = vehiclePosition(vehicle)
local wayPoint = info ~= nil and adEnv.ADGraphManager:getWayPointById(info.markerId) or nil local wayPoint = info ~= nil and adEnv.ADGraphManager:getWayPointById(info.markerId) or nil
+4
View File
@@ -85,6 +85,10 @@
<text name="spu_field_reasonShort_rain" text="venter på tørvejr"/> <text name="spu_field_reasonShort_rain" text="venter på tørvejr"/>
<text name="spu_field_reasonShort_text" text="se detaljer"/> <text name="spu_field_reasonShort_text" text="se detaljer"/>
<text name="spu_field_reason_allBusy" text="venter på ledig %s"/> <text name="spu_field_reason_allBusy" text="venter på ledig %s"/>
<text name="spu_field_reason_noPoolTractor" text="ingen ledig traktor til at hente et redskab til %s"/>
<text name="spu_field_reasonShort_noPoolTractor" text="venter på traktor"/>
<text name="spu_field_reason_fetching" text="henter '%s'"/>
<text name="spu_field_reasonShort_fetching" text="henter redskab"/>
<text name="spu_field_reason_tooWide" text="%s for bred: %.1f m &gt; %.0f m (Courseplay)"/> <text name="spu_field_reason_tooWide" text="%s for bred: %.1f m &gt; %.0f m (Courseplay)"/>
<text name="spu_field_reason_widthUnknown" text="%s: arbejdsbredde %.1f m kan ikke være rigtig — sæt bredden i Courseplay (sendes ikke ud)"/> <text name="spu_field_reason_widthUnknown" text="%s: arbejdsbredde %.1f m kan ikke være rigtig — sæt bredden i Courseplay (sendes ikke ud)"/>
<text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m &lt; slåbredde %.1f m — sæt Riv til eller brug en bredere"/> <text name="spu_field_reason_tooNarrow" text="%s for smal: %.1f m &lt; slåbredde %.1f m — sæt Riv til eller brug en bredere"/>
+4
View File
@@ -85,6 +85,10 @@
<text name="spu_field_reasonShort_rain" text="waiting for dry weather"/> <text name="spu_field_reasonShort_rain" text="waiting for dry weather"/>
<text name="spu_field_reasonShort_text" text="see details"/> <text name="spu_field_reasonShort_text" text="see details"/>
<text name="spu_field_reason_allBusy" text="waiting for a free %s"/> <text name="spu_field_reason_allBusy" text="waiting for a free %s"/>
<text name="spu_field_reason_noPoolTractor" text="no free tractor to fetch an implement for %s"/>
<text name="spu_field_reasonShort_noPoolTractor" text="waiting for tractor"/>
<text name="spu_field_reason_fetching" text="fetching '%s'"/>
<text name="spu_field_reasonShort_fetching" text="fetching implement"/>
<text name="spu_field_reason_tooWide" text="%s too wide: %.1f m &gt; %.0f m (Courseplay)"/> <text name="spu_field_reason_tooWide" text="%s too wide: %.1f m &gt; %.0f m (Courseplay)"/>
<text name="spu_field_reason_widthUnknown" text="%s: work width %.1f m cannot be right — set the width in Courseplay (not sent out)"/> <text name="spu_field_reason_widthUnknown" text="%s: work width %.1f m cannot be right — set the width in Courseplay (not sent out)"/>
<text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m &lt; mowing width %.1f m — turn on Rake or use a wider one"/> <text name="spu_field_reason_tooNarrow" text="%s too narrow: %.1f m &lt; mowing width %.1f m — turn on Rake or use a wider one"/>
+2
View File
@@ -198,5 +198,7 @@ do
check("NP uden position -> første", ADFieldJobs.nearestPoint(pts, nil, nil).x, 1600) check("NP uden position -> første", ADFieldJobs.nearestPoint(pts, nil, nil).x, 1600)
end end
-- pulje: en traktor der henter et redskab tæller som i gang (ellers sendes endnu en ud)
check("pulje: henter -> ingenting", J.decide(mowing, info, {{phase = "fetching"}}, 0).action, "none")
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)