Skip to content

Combine unloader ignores field-serving check when combine is fully stopped ("pocket"/end-of-course), stealing an unloader assigned to an adjacent field's combine #1320

Description

@juano116-pixel

Version

8.1.0.3

Game Version

1.15.0.0 (Multimap sample map)

SP/MP

SP (Singleplayer)

What happened?

I have two adjacent fields (field 15 and field 23, sharing a border) each with their own combine + unloader tractor(s) running the standard "CP: Descargar Cosechadora" (Combine Unloader) job in "Combinada" mode:

Field 15: wheat combine (Evion 450) + 1 unloader tractor, call threshold 85%.
Field 23: grass/forage combine (Jaguar 990 Terra Trac) + 2 unloader tractors, call threshold 85%.

Both fields were harvested at the same time, with each unloader tractor waiting near its own field.

Intermittently, when the combine on one field reaches a fully-stopped "waiting for unload" state (e.g. it made a pocket at ~95%+, or reached the end of its course while still needing unloading), it calls the unloader tractor that belongs to the neighboring field's combine instead of its own idle tractor(s). The wrongly-called tractor drives all the way to the other field and combine, arrives, and then sits there stuck (unable to unload, since its trailer's fillType doesn't accept the crop from the other combine, and there's no compatibility check either). Meanwhile the field that actually needed unloading never gets its own tractor, because that tractor is now stuck at the wrong combine.

How can we reproduce this?
On a map with two adjacent fields (sharing a border/short distance between them), set up two different combine+unloader jobs, one per field, each with a different crop (e.g. wheat and grass/silage), using "CP: Descargar Cosechadora" job / AIDriveStrategyUnloadCombine ("Combinada" mode), each unloader with a fairly high call threshold (e.g. 85%) so the combine is likely to fully stop and enter a "pocket"/waiting state before its own tractor arrives.
Start both combines harvesting at the same time.
Wait until one of the combines actually stops completely to be unloaded (pocket, or end of course) while the other field's unloader tractor happens to be idle/available nearby.
Observe that the stopped combine calls the OTHER field's idle unloader tractor instead of its own, even though that tractor's trailer cannot accept the crop.

I could also reproduce this by manually stopping my own field's unloader tractor via the CP HUD while its combine was working: shortly after, the other field's idle unloader tractor got called to unload MY combine, confirming the pool of candidate unloaders is not restricted by field membership in this state.

What did you expect to happen?

I expected each combine to only ever call an unloader tractor that is actually assigned to / serving its own field, exactly as it already does during normal (moving) harvesting.

Root cause (found via source review)

In scripts/ai/strategies/AIDriveStrategyCombineCourse.lua, function findUnloader (around line 985-1026):

lua
function AIDriveStrategyCombineCourse:findUnloader(combine, waypoint)
local bestScore = -math.huge
local bestUnloader, bestEte
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles) do
if AIDriveStrategyUnloadCombine.isActiveCpCombineUnloader(vehicle) then
local x, _, z = getWorldTranslation(self.vehicle.rootNode)
local driveStrategy = vehicle:getCpDriveStrategy()
-- During normal fieldwork the unloader must be serving this field.
-- When the combine has already finished the course and is waiting to be unloaded,
-- allow a suitable active unloader to be called directly even if its own field
-- polygon is not currently registered around the combine (e.g. when it is parked
-- at an AutoDrive waiting point outside the field).
local isFinalUnloadCall = combine ~= nil
local servingPositionOk = isFinalUnloadCall or driveStrategy:isServingPosition(x, z, 10)
if servingPositionOk then
...

This function is called from callUnloaderWhenNeeded():

lua
if self:isWaitingForUnload() then
self:debug('callUnloaderWhenNeeded: stopped, need unloader here')
bestUnloader, _ = self:findUnloader(self.vehicle, nil)

isWaitingForUnload() (line ~1350) is true for several states, not just "finished the whole course":

lua
function AIDriveStrategyCombineCourse:isWaitingForUnload()
return self.state == self.states.UNLOADING_ON_FIELD and
(self.unloadState == self.states.WAITING_FOR_UNLOAD_ON_FIELD or
self.unloadState == self.states.WAITING_FOR_UNLOAD_IN_POCKET or
self.unloadState == self.states.WAITING_FOR_UNLOAD_AFTER_PULLED_BACK or
self.unloadState == self.states.WAITING_FOR_UNLOAD_AFTER_FIELDWORK_ENDED or
self.unloadState == self.states.WAITING_FOR_UNLOAD_BEFORE_STARTING_NEXT_ROW)
end

So whenever combine is passed (i.e. any of the above "stopped and waiting" states, including the very common WAITING_FOR_UNLOAD_IN_POCKET, not only the "already finished the whole field" case the comment describes), isFinalUnloadCall becomes true, and servingPositionOk is forced true unconditionally, completely bypassing driveStrategy:isServingPosition(x, z, 10). From that point, findUnloader scores and can pick any active CP combine-unloader vehicle anywhere on the map (filtered only by isAllowedToBeCalled() and fill level < 99%), regardless of which field/combine it is actually assigned to.

The comment above the check suggests this relaxation was intended only for the case of a tractor parked outside the field via AutoDrive after the combine already finished its whole course - but isWaitingForUnload() also returns true for the much more common "made a pocket mid-harvest" and "waiting before next row" states, so the relaxation fires far more broadly than the comment implies, causing legitimate cross-field unloader theft between two independent, simultaneously-running combine/unloader jobs.

Suggested fix

Restrict the bypass specifically to the "finished entire fieldwork course" case (e.g. only when self.unloadState == self.states.WAITING_FOR_UNLOAD_AFTER_FIELDWORK_ENDED), instead of any state covered by the generic isWaitingForUnload(). For WAITING_FOR_UNLOAD_IN_POCKET, WAITING_FOR_UNLOAD_ON_FIELD, WAITING_FOR_UNLOAD_AFTER_PULLED_BACK and WAITING_FOR_UNLOAD_BEFORE_STARTING_NEXT_ROW, the normal isServingPosition field check should still apply, since the combine is still actively working its own field course in those states.

Why this is easy to miss in normal play

This only manifests when (a) an unloader's call threshold is high enough that its combine is likely to fully stop/pocket before the tractor arrives, and (b) another field's idle CP unloader happens to be geographically close enough to score better than would normally be possible (e.g. two adjacent fields sharing a border), which is an uncommon combination in most save games with fields spread further apart.

Attach your log.txt

(attach your own log.txt here when posting - it should show a debug line like findUnloader: idle on my field... or similar for the wrongly picked unloader if DBG_FIELDWORK debug channel is enabled)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions