From 4476e565abca0b172fc78e0b8d4a291504b9183e Mon Sep 17 00:00:00 2001 From: Helge Neumann Date: Mon, 6 Jul 2026 15:10:35 +0200 Subject: [PATCH] Highlight contract fields matching the vehicle's implements on the AI map On the course generator map page, the fields of the player's active field missions now get a pulsing green border when the currently selected vehicle has an implement whose work area type the mission accepts. This makes it easy to spot which of the contract fields (red circles on the map) the vehicle can actually work on when creating a new job. The matching is fully generic and needs no per-mission-type mapping: each field mission already declares the work area types it accepts (mission.workAreaTypes), which is exactly what the base game checks in AbstractFieldMission:getIsWorkAllowed(). We test those against the work area types of the vehicle and all its attached implements. The field polygon of each highlighted mission is fetched once, simplified and cached, and the implement scan is throttled to every 2 seconds, so the per-frame cost of the highlight is negligible. Co-Authored-By: Claude Fable 5 --- scripts/gui/pages/CpCourseGeneratorFrame.lua | 86 ++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/scripts/gui/pages/CpCourseGeneratorFrame.lua b/scripts/gui/pages/CpCourseGeneratorFrame.lua index cac6840d1..4577fbef7 100644 --- a/scripts/gui/pages/CpCourseGeneratorFrame.lua +++ b/scripts/gui/pages/CpCourseGeneratorFrame.lua @@ -122,6 +122,12 @@ function CpCourseGeneratorFrame.new(target, custom_mt) self.cpHotspotSettingValue = 1 --- Draws the current progress, while creating a custom field. self.customFieldPlot = FieldPlot(true) + --- Highlights the fields of active contracts the current vehicle has a matching implement for. + self.missionHighlightPlot = FieldPlot(false) + self.missionHighlightPlot:setVisible(true) + self.missionFieldPolygonCache = {} + self.missionHighlightVehicle = nil + self.missionHighlightWorkAreaTypes = {} self.drawFieldBorderPolygon = Polygon() self.drawDelay = 0 return self @@ -336,6 +342,9 @@ end function CpCourseGeneratorFrame:onFrameOpen() self.ingameMap:setTerrainSize(g_currentMission.terrainSize) + --- Contracts may have changed since the last time, so reset the mission highlight caches. + self.missionFieldPolygonCache = {} + self.missionHighlightVehicle = nil g_messageCenter:subscribe(MessageType.GUI_CP_INGAME_CURRENT_VEHICLE_CHANGED, function(self, vehicle) local _, pageTitle = CpCourseGeneratorSettings.getSettingSetup() @@ -622,6 +631,7 @@ function CpCourseGeneratorFrame:delete() self.fieldSiloAiTargetMapHotspot:delete() self.unloadAiTargetMapHotspot:delete() self.loadAiTargetMapHotspot:delete() + self.missionHighlightPlot:delete() CpCourseGeneratorFrame:superClass().delete(self) end ------------------------------- @@ -695,6 +705,10 @@ function CpCourseGeneratorFrame:onDrawPostIngameMap(element, ingameMap) -- show the custom fields on the AI map g_customFieldManager:draw(ingameMap) end + if vehicle then + -- highlight the fields of active contracts the current vehicle has a matching implement for + self:drawMissionHighlights(ingameMap, vehicle) + end -- show the selected field on the AI screen map when creating a job if self.currentJob and self.currentJob.draw then self.currentJob:draw(ingameMap, self.mode == self.MODE_OVERVIEW) @@ -729,6 +743,78 @@ function CpCourseGeneratorFrame:onDrawPostIngameMap(element, ingameMap) end +--- Reduces the number of polygon vertices for drawing, the map plot doesn't need a high resolution. +local function simplifyPolygonForPlot(polygon, minDistance) + local simplified = {} + local lastX, lastZ + for _, point in ipairs(polygon) do + if lastX == nil or MathUtil.vector2Length(point.x - lastX, point.z - lastZ) >= minDistance then + table.insert(simplified, point) + lastX, lastZ = point.x, point.z + end + end + return simplified +end + +--- Highlights the fields of the player's active contracts that the given vehicle has a matching +--- implement for, with a pulsing green field border. This makes it easy to spot, while creating +--- a job, which of the contract fields (red circles) this vehicle can actually work on. +--- Matching is generic: a contract field mission declares the work area types it accepts +--- (mission.workAreaTypes, for example WorkAreaType.PLOW for plow missions) and we check those +--- against the work area types of the vehicle and all its attached implements. +function CpCourseGeneratorFrame:drawMissionHighlights(ingameMap, vehicle) + if g_localPlayer == nil or g_missionManager == nil then + return + end + if vehicle ~= self.missionHighlightVehicle or g_time > (self.missionHighlightNextUpdateTime or 0) then + -- collect the work area types of the vehicle and all its implements. Redo this every few + -- seconds, so attaching/detaching implements while the menu is open is picked up too. + self.missionHighlightVehicle = vehicle + self.missionHighlightNextUpdateTime = g_time + 2000 + self.missionHighlightWorkAreaTypes = {} + local childVehicles, found = AIUtil.getAllChildVehiclesWithSpecialization(vehicle, WorkArea) + if found then + for _, childVehicle in ipairs(childVehicles) do + for _, workArea in WorkWidthUtil.workAreaIterator(childVehicle) do + if WorkWidthUtil.isValidWorkArea(workArea) then + self.missionHighlightWorkAreaTypes[workArea.type] = true + end + end + end + end + end + -- pulsing green, clearly distinguishable from the red contract circles and the blue course plots + local pulse = 0.6 + 0.4 * math.sin(g_time * 0.005) + self.missionHighlightPlot.lightColor = {0.1 * pulse, pulse, 0.1 * pulse} + self.missionHighlightPlot.darkColor = self.missionHighlightPlot.lightColor + for _, mission in ipairs(g_missionManager.missions) do + if mission.farmId == g_localPlayer.farmId and + (mission.status == MissionStatus.RUNNING or mission.status == MissionStatus.PREPARING) and + mission.workAreaTypes ~= nil and mission.getField ~= nil and mission:getField() ~= nil then + local hasMatchingImplement = false + for workAreaType in pairs(mission.workAreaTypes) do + if self.missionHighlightWorkAreaTypes[workAreaType] then + hasMatchingImplement = true + break + end + end + if hasMatchingImplement then + local fieldPolygon = self.missionFieldPolygonCache[mission] + if fieldPolygon == nil then + -- getting the polygon allocates a table for each vertex, so do it only once per mission + local ok, polygon = pcall(CpFieldUtil.getFieldPolygon, mission:getField()) + fieldPolygon = ok and polygon and simplifyPolygonForPlot(polygon, 5) or false + self.missionFieldPolygonCache[mission] = fieldPolygon + end + if fieldPolygon and #fieldPolygon > 2 then + self.missionHighlightPlot.waypoints = fieldPolygon + self.missionHighlightPlot:draw(ingameMap) + end + end + end + end +end + function CpCourseGeneratorFrame:onClickMap(element ,worldX, worldZ) if self.isPickingLocation then self:executePickingCallback(true, worldX, worldZ)