diff --git a/FS25_ADSmartPickup/adFieldJobs.lua b/FS25_ADSmartPickup/adFieldJobs.lua
index d67cd20..99b3e20 100644
--- a/FS25_ADSmartPickup/adFieldJobs.lua
+++ b/FS25_ADSmartPickup/adFieldJobs.lua
@@ -28,7 +28,7 @@ ADFieldJobs.now = 0
ADFieldJobs.loaded = false
-- 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.
ADFieldJobs.REGROW_RATIO = 0.1
diff --git a/FS25_ADSmartPickup/adFieldWork.lua b/FS25_ADSmartPickup/adFieldWork.lua
index 9f42c8f..725bc9c 100644
--- a/FS25_ADSmartPickup/adFieldWork.lua
+++ b/FS25_ADSmartPickup/adFieldWork.lua
@@ -129,6 +129,13 @@ 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))
+end
+
function ADFieldWork.getRigs(adEnv)
local rigs = {}
local state = ADFieldJobs.state
@@ -144,12 +151,12 @@ function ADFieldWork.getRigs(adEnv)
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
-- optaget: vores egen opgave, eller AutoDrive/Courseplay kører den allerede (fx spillerens
-- egen parkeringstur) — den må ikke kapres midt i en anden kørsel
- busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
- or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),
+ 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 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),
-- manuel transport: spilleren kører den til marken (for bred til vejene med skærebord)
manual = (state.manual or {})[id] == true,
@@ -279,6 +286,7 @@ function ADFieldWork.release(assignment, phase)
restoreToolOffset(assignment)
if assignment.handle ~= nil then assignment.handle.cancelled = true 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
@@ -379,6 +387,44 @@ local function askPlayer(field, info, assignment, withDialog)
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)
local role = decision.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
end
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
-- 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)",
@@ -1093,7 +1153,38 @@ function ADFieldWork.follow(adEnv, assignment)
local field = ADFieldJobs.getField(assignment.fieldId)
local info = ADFields.getInfo(assignment.fieldId)
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
local x, z = vehiclePosition(vehicle)
local wayPoint = info ~= nil and adEnv.ADGraphManager:getWayPointById(info.markerId) or nil
diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml
index 0a2d595..3f8635e 100644
--- a/FS25_ADSmartPickup/l10n/l10n_da.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_da.xml
@@ -85,6 +85,10 @@
+
+
+
+
diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml
index 2c5cc30..9ce1e9c 100644
--- a/FS25_ADSmartPickup/l10n/l10n_en.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_en.xml
@@ -85,6 +85,10 @@
+
+
+
+
diff --git a/tests/test_adFieldJobs.lua b/tests/test_adFieldJobs.lua
index a2fd084..5059575 100644
--- a/tests/test_adFieldJobs.lua
+++ b/tests/test_adFieldJobs.lua
@@ -198,5 +198,7 @@ do
check("NP uden position -> første", ADFieldJobs.nearestPoint(pts, nil, nil).x, 1600)
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"))
os.exit(failures == 0 and 0 or 1)