diff --git a/CombineAIDriver.lua b/CombineAIDriver.lua index 2b99ed607..efc703244 100644 --- a/CombineAIDriver.lua +++ b/CombineAIDriver.lua @@ -883,6 +883,22 @@ function CombineAIDriver:createPullBackCourse() end end +--- Get the area the unloader should avoid when approaching the combine. +--- Main (and for now, only) use case is to prevent the unloader to cross in front of the combine after the +--- combine pulled back full with pipe in the fruit, making room for the unloader on its left side. +--- @return table, number, number, number, number node, xOffset, zOffset, width, length : the area to avoid is +--- a length x width m rectangle, the rectangle's bottom right corner (when looking from node) is at xOffset/zOffset +--- from node. +function CombineAIDriver:getAreaToAvoid() + if self:isWaitingForUnloadAfterPulledBack() then + local xOffset = self.vehicle.cp.workWidth / 2 + local zOffset = 0 + local length = self.pullBackDistanceEnd + local width = self.pullBackRightSideOffset + return PathfinderUtil.Area(AIDriverUtil.getDirectionNode(self.vehicle), xOffset, zOffset, width, length) + end +end + function CombineAIDriver:createPullBackReturnCourse() -- nothing fancy here either, just move forward a few meters before returning to the fieldwork course local referenceNode = AIDriverUtil.getDirectionNode(self.vehicle) @@ -1334,7 +1350,7 @@ function CombineAIDriver:startSelfUnload() self.pathfinder, done, path = PathfinderUtil.startPathfindingFromVehicleToNode( self.vehicle, targetNode, offsetX, offsetZ, self:getAllowReversePathfinding(), - fieldNum, {}, nil, nil, true) + fieldNum, {}, nil, nil, nil, true) if done then return self:onPathfindingDone(path) else @@ -1661,15 +1677,25 @@ end function CombineAIDriver:onDraw() - if not courseplay.debugChannels[courseplay.DBG_IMPLEMENTS] then return end + if courseplay.debugChannels[courseplay.DBG_IMPLEMENTS] then - local dischargeNode = self:getCurrentDischargeNode() - if dischargeNode then - DebugUtil.drawDebugNode(dischargeNode.node, 'discharge') + local dischargeNode = self:getCurrentDischargeNode() + if dischargeNode then + DebugUtil.drawDebugNode(dischargeNode.node, 'discharge') + end + + if self.aiDriverData.backMarkerNode then + DebugUtil.drawDebugNode(self.aiDriverData.backMarkerNode, 'back marker') + end end - if self.aiDriverData.backMarkerNode then - DebugUtil.drawDebugNode(self.aiDriverData.backMarkerNode, 'back marker') + if courseplay.debugChannels[courseplay.DBG_PATHFINDER] then + local areaToAvoid = self:getAreaToAvoid() + if areaToAvoid then + local x, y, z = localToWorld(areaToAvoid.node, areaToAvoid.xOffset, 0, areaToAvoid.zOffset) + cpDebug:drawLine(x, y + 1.2, z, 10, 10, 10, x, y + 1.2, z + areaToAvoid.length) + cpDebug:drawLine(x + areaToAvoid.width, y + 1.2, z, 10, 10, 10, x + areaToAvoid.width, y + 1.2, z + areaToAvoid.length) + end end UnloadableFieldworkAIDriver.onDraw(self) diff --git a/CombineUnloadAIDriver.lua b/CombineUnloadAIDriver.lua index 811ef30f5..3d5e1b20a 100644 --- a/CombineUnloadAIDriver.lua +++ b/CombineUnloadAIDriver.lua @@ -1586,16 +1586,16 @@ function CombineUnloadAIDriver:startPathfinding( -- the Giants coordinate system and the waypoint uses the course's conventions. This is confusing, should use -- the same reference everywhere self.pathfinder, done, path, goalNodeInvalid = PathfinderUtil.startPathfindingFromVehicleToWaypoint( - self.vehicle, target, -xOffset or 0, zOffset or 0, self.allowReversePathfinding, + self.vehicle, target, -xOffset or 0, zOffset or 0, false, fieldNum, vehiclesToIgnore, self.vehicle.cp.settings.useRealisticDriving:is(true) and self.maxFruitPercent or math.huge, - self.offFieldPenalty) + self.offFieldPenalty, self.combineToUnload.cp.driver:getAreaToAvoid()) else self.pathfinder, done, path, goalNodeInvalid = PathfinderUtil.startPathfindingFromVehicleToNode( - self.vehicle, target, xOffset or 0, zOffset or 0, self.allowReversePathfinding, + self.vehicle, target, xOffset or 0, zOffset or 0, false, fieldNum, vehiclesToIgnore, self.vehicle.cp.settings.useRealisticDriving:is(true) and self.maxFruitPercent or math.huge, - self.offFieldPenalty) + self.offFieldPenalty, self.combineToUnload.cp.driver:getAreaToAvoid()) end if done then return pathfindingCallbackFunc(self, path, goalNodeInvalid) @@ -1687,7 +1687,9 @@ function CombineUnloadAIDriver:checkForCombineProximity() -- do not swerve for our combine towards the end of the course, -- otherwise we won't be able to align with it when coming from -- the wrong angle - if self.course:getDistanceToLastWaypoint(self.course:getCurrentWaypointIx()) < 20 then + -- Increased distance from 20 to 75, so we don't swerve for our combine + -- when we are coming from the front and drive to close to our combine + if self.course:getDistanceToLastWaypoint(self.course:getCurrentWaypointIx()) < 75 then if not self.doNotSwerveForVehicle:get() then self:debug('Disable swerve for %s', nameNum(self.combineToUnload)) end diff --git a/base.lua b/base.lua index 216a32036..f879bbdaf 100644 --- a/base.lua +++ b/base.lua @@ -181,7 +181,6 @@ function courseplay:onLoad(savegame) local directionNodeOffset, isTruck = courseplay:getVehicleDirectionNodeOffset(self, DirectionNode); if directionNodeOffset ~= 0 then - self.cp.oldDirectionNode = DirectionNode; -- Only used for debugging. DirectionNode = courseplay:createNewLinkedNode(self, "realDirectionNode", DirectionNode); setTranslation(DirectionNode, 0, 0, directionNodeOffset); end; @@ -457,17 +456,14 @@ function courseplay:onDraw() --DEBUG SHOW DIRECTIONNODE if courseplay.debugChannels[courseplay.DBG_PPC] then -- For debugging when setting the directionNodeZOffset. (Visual points shown for old node) - if self.cp.oldDirectionNode then - local ox,oy,oz = getWorldTranslation(self.cp.oldDirectionNode); - cpDebug:drawPoint(ox, oy+4, oz, 0.9098, 0.6902 , 0.2706); - end; - if self.cp.driver then - self.cp.driver:onDraw() - end local nx,ny,nz = getWorldTranslation(self.cp.directionNode); cpDebug:drawPoint(nx, ny+4, nz, 0.6196, 0.3490 , 0); - end; - + end; + + if self.cp.driver then + self.cp.driver:onDraw() + end + if self:getIsActive() then if self.cp.hud.show then courseplay.hud:setContent(self); diff --git a/course-generator/PathfinderUtil.lua b/course-generator/PathfinderUtil.lua index 0c838d9e1..94bb23a08 100644 --- a/course-generator/PathfinderUtil.lua +++ b/course-generator/PathfinderUtil.lua @@ -22,9 +22,12 @@ PathfinderUtil.dubinsSolver = DubinsSolver() PathfinderUtil.reedSheppSolver = ReedsSheppSolver() PathfinderUtil.defaultOffFieldPenalty = 7.5 +PathfinderUtil.defaultAreaToAvoidPenalty = 2000 PathfinderUtil.visualDebugLevel = 0 +------------------------------------------------------------------------------------------------------------------------ ---Size/turn radius all other information on the vehicle and its implements +------------------------------------------------------------------------------------------------------------------------ ---@class PathfinderUtil.VehicleData PathfinderUtil.VehicleData = CpObject() @@ -59,8 +62,8 @@ function PathfinderUtil.VehicleData:init(vehicle, withImplements, buffer) rootVehicle = self.trailer:getRootVehicle(), dFront = buffer or 0, dRear = - self.trailer.sizeLength - (buffer or 0), - dLeft = self.trailer.sizeWidth / 2, - dRight = -self.trailer.sizeWidth / 2 + dLeft = self.trailer.sizeWidth / 2 + (buffer or 0), + dRight = -self.trailer.sizeWidth / 2 - (buffer or 0) } local inputAttacherJoint = self.trailer:getActiveInputAttacherJoint() if inputAttacherJoint then @@ -149,6 +152,9 @@ function PathfinderUtil.VehicleData:calculateSizeOfObjectList(vehicle, implement -- self.dFront, self.dRear, self.dLeft, self.dRight) end +------------------------------------------------------------------------------------------------------------------------ +-- Helpers +------------------------------------------------------------------------------------------------------------------------ --- Is this position on a field (any field)? function PathfinderUtil.isPosOnField(x, y, z) if not y then @@ -200,7 +206,9 @@ function PathfinderUtil.isWorldPositionOwned(posX, posZ) return (farmland and farmland.isOwned) or missionAllowed end +------------------------------------------------------------------------------------------------------------------------ --- Pathfinder context +------------------------------------------------------------------------------------------------------------------------ ---@class PathfinderUtil.Context PathfinderUtil.Context = CpObject() function PathfinderUtil.Context:init(vehicle, vehiclesToIgnore, objectsToIgnore) @@ -266,7 +274,9 @@ function PathfinderUtil.getNormalWorldRotation(x, z) return xRot, yRot, zRot end - +------------------------------------------------------------------------------------------------------------------------ +-- PathfinderUtil.CollisionDetector +--------------------------------------------------------------------------------------------------------------------------- ---@class PathfinderUtil.CollisionDetector PathfinderUtil.CollisionDetector = CpObject() @@ -358,7 +368,30 @@ function PathfinderUtil.hasFruit(x, z, length, width) end return false end - +--------------------------------------------------------------------------------------------------------------------------- +-- A generic rectangular area oriented by a node +--------------------------------------------------------------------------------------------------------------------------- +--- @class PathfinderUtil.Area +PathfinderUtil.Area = CpObject() + +function PathfinderUtil.Area:init(node, xOffset, zOffset, width, length) + self.node = node + self.xOffset, self.zOffset = xOffset, zOffset + self.width, self.length = width, length +end + +--- Is (x, z) world coordinate in the area? +function PathfinderUtil.Area:contains(x, z) + local y = getTerrainHeightAtWorldPos(g_currentMission.terrainRootNode, x, 0, z) + local dx, _, dz = worldToLocal(self.node, x, y, z) + if self.xOffset < dx and dx < self.xOffset + self.width and + self.zOffset < dz and dz < self.zOffset + self.length then + --print(x, z, dx, dz, self.xOffset, self.width, 'contains') + return true + else + return false + end +end --[[ Pathfinding is controlled by the constraints (validity and penalty) below. The pathfinder will call these functions @@ -396,16 +429,21 @@ field or driving to/from the field edge on an unload/refill course. ---@class PathfinderConstraints : PathfinderConstraintInterface PathfinderConstraints = CpObject(PathfinderConstraintInterface) -function PathfinderConstraints:init(context, maxFruitPercent, offFieldPenalty, fieldNum) +function PathfinderConstraints:init(context, maxFruitPercent, offFieldPenalty, fieldNum, areaToAvoid) self.context = context self.maxFruitPercent = maxFruitPercent or 50 self.offFieldPenalty = offFieldPenalty or PathfinderUtil.defaultOffFieldPenalty self.fieldNum = fieldNum or 0 + self.areaToAvoid = areaToAvoid + self.areaToAvoidPenaltyCount = 0 self.initialMaxFruitPercent = self.maxFruitPercent self.initialOffFieldPenalty = self.offFieldPenalty self:resetCounts() - courseplay.debugFormat(courseplay.DBG_PATHFINDER, 'Pathfinder constraints: off field penalty %.1f, max fruit percent: %d, field number %d', - self.offFieldPenalty, self.maxFruitPercent, self.fieldNum) + local areaText = self.areaToAvoid and + string.format('%.1f x %.1f m', self.areaToAvoid.length, self.areaToAvoid.width) or 'none' + courseplay.debugFormat(courseplay.DBG_PATHFINDER, + 'Pathfinder constraints: off field penalty %.1f, max fruit percent: %d, field number %d, area to avoid %s', + self.offFieldPenalty, self.maxFruitPercent, self.fieldNum, areaText) end function PathfinderConstraints:resetCounts() @@ -421,7 +459,7 @@ end function PathfinderConstraints:getNodePenalty(node) -- tweak these two parameters to set up how far the path will be from the field or fruit boundary -- size of the area to check for field/fruit - local areaSize = 3 + local areaSize = 4 -- minimum ratio of the area checked must be on field/clear of fruit local minRequiredAreaRatio = 0.8 local penalty = 0 @@ -450,7 +488,11 @@ function PathfinderConstraints:getNodePenalty(node) self.fruitPenaltyNodeCount = self.fruitPenaltyNodeCount + 1 end end - self.totalNodeCount = self.totalNodeCount + 1 + if self.areaToAvoid and self.areaToAvoid:contains(node.x, -node.y) then + penalty = penalty + PathfinderUtil.defaultAreaToAvoidPenalty + self.areaToAvoidPenaltyCount = self.areaToAvoidPenaltyCount + 1 + end + self.totalNodeCount = self.totalNodeCount + 1 return penalty end @@ -522,8 +564,10 @@ function PathfinderConstraints:relaxConstraints() end function PathfinderConstraints:showStatistics() - courseplay.debugFormat(courseplay.DBG_PATHFINDER, 'Nodes: %d, Penalties: fruit: %d, off-field: %d, collisions: %d', - self.totalNodeCount, self.fruitPenaltyNodeCount, self.offFieldPenaltyNodeCount, self.collisionNodeCount) + courseplay.debugFormat(courseplay.DBG_PATHFINDER, + 'Nodes: %d, Penalties: fruit: %d, off-field: %d, collisions: %d, area to avoid: %d', + self.totalNodeCount, self.fruitPenaltyNodeCount, self.offFieldPenaltyNodeCount, self.collisionNodeCount, + self.areaToAvoidPenaltyCount) courseplay.debugFormat(courseplay.DBG_PATHFINDER, ' max fruit %.1f %%, off-field penalty: %.1f', self.maxFruitPercent, self.offFieldPenalty) end @@ -550,7 +594,7 @@ end function PathfinderUtil.startPathfindingFromVehicleToGoal(vehicle, goal, allowReverse, fieldNum, vehiclesToIgnore, objectsToIgnore, - maxFruitPercent, offFieldPenalty, mustBeAccurate) + maxFruitPercent, offFieldPenalty, areaToAvoid, mustBeAccurate) local start = PathfinderUtil.getVehiclePositionAsState3D(vehicle) @@ -563,7 +607,7 @@ function PathfinderUtil.startPathfindingFromVehicleToGoal(vehicle, goal, local constraints = PathfinderConstraints(context, maxFruitPercent or (vehicle.cp.settings.useRealisticDriving:is(true) and 50 or math.huge), offFieldPenalty or PathfinderUtil.defaultOffFieldPenalty, - fieldNum) + fieldNum, areaToAvoid) return PathfinderUtil.startPathfinding(start, goal, context, constraints, allowReverse, mustBeAccurate) end @@ -709,15 +753,17 @@ end ---@param vehiclesToIgnore table[] list of vehicles to ignore for the collision detection (optional) ---@param maxFruitPercent number maximum percentage of fruit present before a node is marked as invalid (optional) ---@param offFieldPenalty number penalty to apply to nodes off the field +---@param areaToAvoid PathfinderUtil.Area nodes in this area will be penalized so the path will most likely avoid it function PathfinderUtil.startPathfindingFromVehicleToWaypoint(vehicle, goalWaypoint, xOffset, zOffset, allowReverse, - fieldNum, vehiclesToIgnore, maxFruitPercent, offFieldPenalty) + fieldNum, vehiclesToIgnore, maxFruitPercent, + offFieldPenalty, areaToAvoid) local goal = State3D(goalWaypoint.x, -goalWaypoint.z, courseGenerator.fromCpAngleDeg(goalWaypoint.angle)) local offset = Vector(zOffset, -xOffset) goal:add(offset:rotate(goal.t)) return PathfinderUtil.startPathfindingFromVehicleToGoal( - vehicle, goal, allowReverse, fieldNum, vehiclesToIgnore, {}, maxFruitPercent, offFieldPenalty) + vehicle, goal, allowReverse, fieldNum, vehiclesToIgnore, {}, maxFruitPercent, offFieldPenalty, areaToAvoid) end ------------------------------------------------------------------------------------------------------------------------ --- Interface function to start the pathfinder in the game. The goal is a point at sideOffset meters from the goal node @@ -732,16 +778,17 @@ end ---@param vehiclesToIgnore table[] list of vehicles to ignore for the collision detection (optional) ---@param maxFruitPercent number maximum percentage of fruit present before a node is marked as invalid (optional) ---@param offFieldPenalty number penalty to apply to nodes off the field +---@param areaToAvoid PathfinderUtil.Area nodes in this area will be penalized so the path will most likely avoid it ---@param mustBeAccurate boolean must be accurately find the goal position/angle (optional) function PathfinderUtil.startPathfindingFromVehicleToNode(vehicle, goalNode, xOffset, zOffset, allowReverse, - fieldNum, vehiclesToIgnore, maxFruitPercent, offFieldPenalty, - mustBeAccurate) + fieldNum, vehiclesToIgnore, maxFruitPercent, + offFieldPenalty, areaToAvoid, mustBeAccurate) x, z, yRot = PathfinderUtil.getNodePositionAndDirection(goalNode, xOffset, zOffset) local goal = State3D(x, -z, courseGenerator.fromCpAngle(yRot)) return PathfinderUtil.startPathfindingFromVehicleToGoal( vehicle, goal, allowReverse, fieldNum, - vehiclesToIgnore, {}, maxFruitPercent, offFieldPenalty, mustBeAccurate) + vehiclesToIgnore, {}, maxFruitPercent, offFieldPenalty, areaToAvoid, mustBeAccurate) end ------------------------------------------------------------------------------------------------------------------------ diff --git a/img/CoursePlay_Icon.dds b/img/CoursePlay_Icon.dds deleted file mode 100644 index 9395077ec..000000000 Binary files a/img/CoursePlay_Icon.dds and /dev/null differ diff --git a/img/icon_courseplay.dds b/img/icon_courseplay.dds new file mode 100644 index 000000000..0097c8a6d Binary files /dev/null and b/img/icon_courseplay.dds differ diff --git a/img/store.dds b/img/store.dds deleted file mode 100644 index 0f0c7e430..000000000 Binary files a/img/store.dds and /dev/null differ diff --git a/modDesc.xml b/modDesc.xml index 57df07311..13b6ed2e5 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -1,6 +1,6 @@ - 6.03.00058 + 6.03.00059 <!-- en=English de=German fr=French es=Spanish ru=Russian pl=Polish it=Italian br=Brazilian-Portuguese cs=Chinese(Simplified) ct=Chinese(Traditional) cz=Czech nl=Netherlands hu=Hungary jp=Japanese kr=Korean pt=Portuguese ro=Romanian tr=Turkish --> <en>CoursePlay SIX</en> @@ -70,7 +70,7 @@ Courseplay non sostituisce il sistema di gioco in gioco, ma crea un nuovo lavora </description> <multiplayer supported="true" /> - <iconFilename>img/store.dds</iconFilename> + <iconFilename>img/icon_courseplay.dds</iconFilename> <l10n filenamePrefix="translations/translation" /> <!-- load translations from "translations/translation_<language>.xml file --> <extraSourceFiles>