Manual Combine Unloader function & other small tweaks#1243
Manual Combine Unloader function & other small tweaks#1243antler22 wants to merge 5 commits intoCourseplay:mainfrom
Conversation
Updated README to reflect forked status and added details about manual combine unloading and US units.
Grain cart manual call button, US units, CP unload threshold dropped to 20%, pathfinding for grain cart
antler22
left a comment
There was a problem hiding this comment.
Hidden Whitespace.. I think?
| if not self.isValid then | ||
| return | ||
| end | ||
| if not self.settings.fuelSave:getValue() then |
There was a problem hiding this comment.
LIkely this PR needs a rebase.
There was a problem hiding this comment.
You're right. The MotorController changes weren't intentional for this PR — they crept in as part of a big umbrella commit. Happy to rebase and clean that up so it doesn't appear in the diff at all.
| if self.ppc then | ||
| self.ppc.offTrackGracePeriodMs = 20000 | ||
| end | ||
| if CollisionAvoidanceController then |
There was a problem hiding this comment.
The comments pretty much explain, but what I was noticing when pathfinder fails or cannot find a path, it releases the CP driver. If driving a combine manually, its inconvenient to tab to the vehicle again and restart. So I wanted CP to try harder/longer to find a path.
There was a problem hiding this comment.
I am talking about the if CollisionAvoidanceController and if ProximityController parts. I can't imagine any scenario where those wouldn't be loaded.
| CpUtil.errorVehicle(self.vehicle, 'Courseplay: CollisionAvoidanceController not loaded (mod conflict?). Collision avoidance disabled.') | ||
| self.collisionAvoidanceController = { isCollisionWarningActive = function() return false end } | ||
| end | ||
| if ProximityController then |
There was a problem hiding this comment.
Similar to the last comment. Added defensively to try and avoid releasing the CP driver. Might be overly guarded now?
There was a problem hiding this comment.
I am talking about the if CollisionAvoidanceController and if ProximityController parts. I can't imagine any scenario where those wouldn't be loaded.
There was a problem hiding this comment.
This is one of the most important modules of CP. I'm not able to review the changes until GitHub shows a proper diff.
There was a problem hiding this comment.
The actual changes are small: a grace-period class constant (offTrackGracePeriodMs), a offTrackShutdownSince state variable in init(), and the soft-recovery block that replaces the single-line hard shutdown. I'll rebuild now
…f, remove unnecessary guards - Rename cpIsCallGrainCartActive -> cpIsManualCombineCallingUnloader per pvaiko's naming suggestion - Rename cpToggleCallGrainCart -> cpToggleManualUnloader for consistency - Rename CallGrainCartEvent -> CpManualUnloaderEvent (class + file) - Update translation key CP_callGrainCart -> CP_callManualUnloader; visible text now reads 'Call Unloader' - Rebase PurePursuitController.lua on current upstream base so PR diff shows only our actual additions (~35 lines) instead of the full file - Remove unnecessary if CollisionAvoidanceController / if ProximityController guards in AIDriveStrategyUnloadCombine.setAIVehicle() — these classes are always loaded - Revert MotorController.lua to upstream (changes were unrelated to this feature and crept in by mistake) Co-Authored-By: Claude Sonnet 4-6 <noreply@anthropic.com>
This feature adds a "Call Grain Cart" button to manually-driven combines (i.e. combines that the player is driving themselves, not Courseplay-controlled). When activated, a nearby Courseplay-managed grain cart automatically:
The design goal is that the player only touches the button once — the grain cart handles everything until the pipe is closed.
Files Changed
scripts/ai/CpManualCombineProxy.luaAIDriveStrategyCombineCourseinterfacescripts/specializations/CpAIFieldWorker.luascripts/specializations/CpAIWorker.luascripts/ai/strategies/AIDriveStrategyUnloadCombine.luascripts/ai/PurePursuitController.luascripts/pathfinder/PathfinderUtil.luahasFruit()config/VehicleSettingsSetup.xml1. New File:
scripts/ai/CpManualCombineProxy.luaA new class that implements the full
AIDriveStrategyCombineCourseinterface for manually-driven combines. This allowsAIDriveStrategyUnloadCombineto interact with a manual combine using the same method calls it uses for CP-driven combines, without any nil checks or special-casing scattered through the unloader code.Key design decisions
isManualProxy() → trueA marker method so the unloader strategy can identify a manual proxy without re-querying the vehicle. Used to gate manual-only behavior throughout
AIDriveStrategyUnloadCombine.getFillLevelPercentage() → 1Always reports 100% full. The farmer is in full control — the grain cart must never leave because of a low fill level. The only valid exit condition is
isUnloadFinished().isUnloadFinished()Requires the discharge to be continuously off for 2 seconds before returning true. This prevents a momentary swerve or brief pipe misalignment from prematurely ending the session. Once it returns true, the grain cart departs and the proxy re-summons it if the button is still active.
willWaitForUnloadToFinish()3-second debounce before reporting the combine as "stopped." A GPS micro-correction or terrain hitch lasting less than 3 seconds is ignored. Without this, a brief stop would flip the grain cart from
UNLOADING_MOVING_COMBINEtoUNLOADING_STOPPED_COMBINE, triggering unnecessary state cycling.registerUnloader(driver)/deregisterUnloader()Uses a
CpTemporaryObjectwith a 1-second TTL. The grain cart callsregisterUnloaderevery frame while it has an active combine. When the cart releases (soft recovery or natural exit), the TTL expires and the proxy'scallUnloaderWhenNeeded()can re-summon within ~2.5 seconds.callUnloaderWhenNeeded()Runs every 1500 ms. If no unloader is registered, searches active CP unloader vehicles via
AIDriveStrategyUnloadCombine.isActiveCpCombineUnloader(), scores by fill level and distance, and callsstrategy:call(self.vehicle, nil)on the best candidate. This is the mechanism that auto-resumes after soft recovery.getFruitAtSides() → nil, nilForage harvesters crash
calculateAutoAimPipeOffsetX()if this returns nil beforecheckFruit()has run. Returningnil, nilis safe for standard combines and avoids the crash for any chopper that might somehow reach this code path.isTurning() → falseThe unloader has special "wait during combine turn" logic that is inappropriate when the farmer is manually steering. Always returning false keeps the grain cart tracking the pipe rather than holding position at the headland.
2. Modified:
scripts/specializations/CpAIFieldWorker.luaWhat was added
cpIsCallGrainCartActive()— Returnstruewhenspec.cpManualCombineProxy ~= nil.cpToggleCallGrainCart()— Creates a newCpManualCombineProxy(activate) or deletes the existing one (deactivate). Guards against activation when CP is already active on the combine.cpGetManualCombineProxy()— Returns the current proxy instance.onUpdate(dt)— Drives the proxy's update loop each tick (course refresh + unloader call cycle). Auto-deactivates the proxy ifgetIsCpActive()becomes true on the combine.3. Modified:
scripts/specializations/CpAIWorker.lua"Call Grain Cart" button disabled for forage harvesters
Forage harvesters (choppers) have an auto-aiming spout and behave very differently from grain combines. Calling a grain cart unloader on a chopper causes errors. The button and keybind are now hidden when the vehicle has
pipeSpec.numAutoAimingStates > 0.4. Modified:
scripts/ai/strategies/AIDriveStrategyUnloadCombine.luaThis is the most significant set of changes. All modifications are backward-compatible with CP-driven combines — manual-only code is gated by
combineStrategy:isManualProxy().4a.
setAIVehicle()— Extended PPC grace periodUnloaders track moving targets (combine position changes, rendezvous shifts, post-turn realignment). The default 10-second off-track grace period is too short for this use case. Extended to 20 seconds to give the pathfinder and steering more time to recover before any shutdown is considered.
4b.
onOffTrackShutdown()— New soft-recovery methodCalled by the PPC (see section 5) when the off-track grace period expires. Transitions the grain cart to
IDLEand releases the combine. The proxy'scallUnloaderWhenNeeded()re-summons within ~2.5 seconds. The user never has to walk over to the grain cart to restart it manually.This is safe for both manual and CP-driven combines: CP combines call
unloader:call()again when they need service; the proxy does the same via its update loop.4c.
driveBesideCombine()— Direct steering goal for manual combinesThe problem with course-based steering for manual combines:
AIDriveStrategyUnloadCombinenormally steers by following a copy of the combine's fieldwork course offset to the pipe side. Manual combines have no fieldwork course — only a static placeholder is available. As the combine curves or S-bends, the grain cart drifts far from the stale placeholder, and the PPC cannot correct.The solution — live goal point from the pipe reference node:
For manual combines, a goal point is computed every frame regardless of
dz. The goal is alwaysnormalLookAheadDistancemeters ahead of the cart's current longitudinal position in the combine's own local frame, at the pipe's lateral offset. As the combine turns,getPipeOffsetReferenceNode()rotates with it — so the goal point rotates too, and the cart naturally follows curves.Why
normalLookAheadDistanceinstead ofgetLookaheadDistance():getLookaheadDistance()inflates the lookahead up to 2× the base value when cross-track error is large (which it always is, since the cart is far from the stale placeholder course). A 12 m lookahead makes the cart too slow to respond to gentle heading changes.normalLookAheadDistance(≈5–6 m) is constant and un-inflated, enabling tight S-curve tracking.CP-driven combines: zero behavior change. The
isManualbranch is not taken, and the existingdz > 5gate applies as before.4d.
unloadMovingCombine()— Off-track suppression during manual unloadingBecause the placeholder course is intentionally stale (steering is derived from the live pipe reference node, not the course), the grain cart WILL drift far from the placeholder during curves. Without suppression, the PPC's off-track detection would fire. This is suppressed per-tick:
The 5000 ms TTL (much longer than any realistic frame interval) ensures there is no gap between ticks where the check can briefly re-enable.
4e.
startCourseFollowingCombine()— Placeholder course for manual combinesThe PPC requires a course object to be initialised. For manual combines, a placeholder is built from the combine's current position in its current heading direction (100 m straight forward). This course is never used for steering —
driveBesideCombine()returns the live goal point every frame, overriding the PPC's course-based calculation.No periodic refresh of this course is needed or performed.
4f.
ignoreProximityObject()— Terrain hits during approach and unloadingWindrows and straw swaths are height-map physics objects. The proximity sensor's raycasts hit them as
hitTerrain = true, slowing the grain cart to a crawl mid-approach. Terrain hits are now ignored in the relevant states:4g.
calculateAutoAimPipeOffsetX()— Nil guard for forage harvestersgetFruitAtSides()can returnnilbeforecheckFruit()has run (e.g., when a chopper just started). This caused a Lua arithmetic error on line 1022:4h.
driveToCombine()— Smarter approach redirectThe original approach redirect fired every 10 seconds unconditionally, causing the grain cart to swerve even on a stable straight approach. The redirect now only fires when the combine's pipe reference node has moved >15 m from the last redirect target:
Redirect tracking (
lastApproachRedirectX/Z,lastApproachRedirectTime) is reset after each successful pathfinding completion so the next approach starts fresh.4i. Fill level exit condition — discharge check
Previously, the
fillPct <= 0.1check could fire at exactly 10% fill while the pipe was still open, causing a tarp-open/close cycle. Thenot isDischargingguard ensures the cart only leaves after the pipe has actually closed. (For manual combines this is moot sincegetFillLevelPercentage()always returns 1, but the fix is correct for all combines.)5. Modified:
scripts/ai/PurePursuitController.luaSoft-recovery hook before hard shutdown
Before calling
vehicle:stopCurrentAIJob(AIMessageCpError.new()), the PPC now checks whether the current drive strategy implementsonOffTrackShutdown():If the strategy returns
truefromonOffTrackShutdown(), the PPC resets its shutdown timer and continues running. This hook is intentionally generic — any strategy can implement it to provide graceful degradation instead of a hard stop.6. Modified:
scripts/pathfinder/PathfinderUtil.luaDefensive windrow filtering in
hasFruit()Added name-based checks to skip windrow, swath, straw and chaff fill types in the fruit detection loop:
Note: Windrows are height-map physics objects, not fruit density map objects, so
getFruitArea()will not detect them regardless of this filter. The filter is defensive coding only and has no functional effect on current game versions. It is safe to remove if the maintainer prefers.7. Modified:
config/VehicleSettingsSetup.xmlcallUnloaderPercentminimum lowered to 20%The minimum threshold for "Call Unloader at" is reduced from 60% to 20%. High-yield crops (e.g., corn) fill combines faster; a lower call threshold lets CP-driven combines summon unloaders earlier, keeping combines harvesting continuously without stopping to wait for an unloader.
Interaction with CP-Driven Combines
All changes are backward-compatible. The two-combine scenario (one manual + one CP-driven, served by one or two grain carts) works correctly:
combineStrategy:isManualProxy().callUnloaderPercentslider change applies to all combines regardless of control mode.