diff --git a/Courseplay.lua b/Courseplay.lua index 737948e89..2dcb6efa0 100644 --- a/Courseplay.lua +++ b/Courseplay.lua @@ -157,7 +157,7 @@ end function Courseplay.drawHudMap(map) if g_Courseplay.globalSettings.drawOntoTheHudMap:getValue() then local vehicle = g_currentMission.controlledVehicle - if vehicle and vehicle:getIsEntered() and not g_gui:getIsGuiVisible() and vehicle.spec_courseplaySpec and not vehicle.spec_locomotive then + if vehicle and vehicle:getIsEntered() and not g_gui:getIsGuiVisible() and vehicle.spec_cpAIWorker and not vehicle.spec_locomotive then SpecializationUtil.raiseEvent(vehicle, "onCpDrawHudMap", map) end end @@ -192,14 +192,28 @@ end FSCareerMissionInfo.saveToXMLFile = Utils.prependedFunction(FSCareerMissionInfo.saveToXMLFile, Courseplay.saveToXMLFile) function Courseplay:update(dt) - g_devHelper:update() - g_bunkerSiloManager:update(dt) + g_devHelper:update() + g_bunkerSiloManager:update(dt) + g_triggerManager:update(dt) + if not self.postInit then + -- Doubles the map zoom for 4x Maps. Mainly to make it easier to set targets for unload triggers. + self.postInit = true + local function setIngameMapFix(mapElement) + local factor = 2*mapElement.terrainSize/2048 + mapElement.zoomMax = mapElement.zoomMax * factor + mapElement.zoomDefault = mapElement.zoomDefault * factor + mapElement.mapZoom = mapElement.zoomDefault + end + setIngameMapFix(g_currentMission.inGameMenu.pageAI.ingameMap) + setIngameMapFix(g_currentMission.inGameMenu.pageMapOverview.ingameMap) + end end function Courseplay:draw() if not g_gui:getIsGuiVisible() then g_vineScanner:draw() g_bunkerSiloManager:draw() + g_triggerManager:draw() end g_devHelper:draw() CpDebug:draw() @@ -323,27 +337,19 @@ end function Courseplay.register(typeManager) --- TODO: make this function async. for typeName, typeEntry in pairs(typeManager.types) do - if CourseplaySpec.prerequisitesPresent(typeEntry.specializations) then - typeManager:addSpecialization(typeName, Courseplay.MOD_NAME .. ".courseplaySpec") - end - if CpVehicleSettings.prerequisitesPresent(typeEntry.specializations) then - typeManager:addSpecialization(typeName, Courseplay.MOD_NAME .. ".cpVehicleSettings") - end - if CpCourseGeneratorSettings.prerequisitesPresent(typeEntry.specializations) then - typeManager:addSpecialization(typeName, Courseplay.MOD_NAME .. ".cpCourseGeneratorSettings") - end - if CpCourseManager.prerequisitesPresent(typeEntry.specializations) then - typeManager:addSpecialization(typeName, Courseplay.MOD_NAME .. ".cpCourseManager") - end CpAIWorker.register(typeManager, typeName, typeEntry.specializations) + CpVehicleSettings.register(typeManager, typeName, typeEntry.specializations) + CpCourseGeneratorSettings.register(typeManager, typeName, typeEntry.specializations) + CpCourseManager.register(typeManager, typeName, typeEntry.specializations) CpAIFieldWorker.register(typeManager, typeName, typeEntry.specializations) CpAIBaleFinder.register(typeManager, typeName, typeEntry.specializations) CpAICombineUnloader.register(typeManager, typeName, typeEntry.specializations) - CpAIBunkerSiloWorker.register(typeManager, typeName, typeEntry.specializations) CpAISiloLoaderWorker.register(typeManager, typeName, typeEntry.specializations) + CpAIBunkerSiloWorker.register(typeManager, typeName, typeEntry.specializations) CpGamePadHud.register(typeManager, typeName,typeEntry.specializations) CpHud.register(typeManager, typeName, typeEntry.specializations) CpInfoTexts.register(typeManager, typeName, typeEntry.specializations) + CpShovelPositions.register(typeManager, typeName, typeEntry.specializations) end end TypeManager.finalizeTypes = Utils.prependedFunction(TypeManager.finalizeTypes, Courseplay.register) diff --git a/config/HelpMenu.xml b/config/HelpMenu.xml index 15eb062bd..0be47f0d8 100644 --- a/config/HelpMenu.xml +++ b/config/HelpMenu.xml @@ -222,5 +222,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/MasterTranslations.xml b/config/MasterTranslations.xml index 919c581e4..d86e4a427 100644 --- a/config/MasterTranslations.xml +++ b/config/MasterTranslations.xml @@ -79,6 +79,10 @@ + + + + @@ -298,9 +302,21 @@ - - - + + + + + + + + + + + + + + + @@ -597,6 +613,14 @@ + + + + + + + + @@ -2474,6 +2498,96 @@ The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. ]]> + + + + + + + + + + + + + + + + + diff --git a/config/VehicleConfigurations.xml b/config/VehicleConfigurations.xml index b58b8e625..d50e7001c 100644 --- a/config/VehicleConfigurations.xml +++ b/config/VehicleConfigurations.xml @@ -97,6 +97,13 @@ You can define the following custom settings: Overrides the automatic selection of a moving tool for example for the shield controller of a snowcat. Used to control the target tilt. +- loadingShovelOffset: number -1 : 1 + Offset to the loading and transport shovel position in meter for the height of the shovel. + +- shovelMovingToolIx: number + If the shovel is a high dump shovel then the moving tool index of the correct one is needed. + Usually this index is 1 for the first moving tool. + - modName: Name of the .zip file (without '.zip') In case a Mod has the same .xml filename for the vehicle/implement, as a default giants vehicle/implement, add the name of the .zip file to prevent conflicts. For example: "FS22_exampleMod". @@ -106,8 +113,46 @@ You can define the following custom settings: Ignore bales detected by the forward-looking proximity controller if any implement has this set to true. This can be set for bale pushers to prevent the Courseplay driver stopping when a bale is blocking the way. +- fixWheelLoaderDirectionNodeByMovingToolIx + Fixes the ai direction node for the platinum wheel loaders, as their direction is not changed based on the rotation. + As a fix the parent node of the arm moving tool ix is used. + +- articulatedAxisReverseNodeInverted: boolean + Is the reverse node for articulated axis vehicles rotated by 180 degree? + +- unloadXOffset: number + Offset of the discharge node or a given pipe for unloading into a trailer. + For pipes this overwrites the measured pipe offset. + +- disablePipeMovingToolCorrection: boolean + Disables the pipe height adjustment for trailers of a few pipe implements/vehicles. + --> + + toolOffsetX + noReverse + turnRadius + workingWidth + balerUnloadDistance + directionNodeOffsetZ + implementWheelAlwaysOnGround + ignoreCollisionBoxesWhenFolded + baleCollectorOffset + disableUnfolding + raiseLate + lowerEarly + useVehicleSizeForMarkers + armMovingToolIx + movingToolIx + shovelMovingToolIx + loadingShovelOffset + ignoreBaleCollisionForward + fixWheelLoaderDirectionNodeByMovingToolIx + articulatedAxisReverseNodeInverted + disablePipeMovingToolCorrection + unloadOffsetX + @@ -314,6 +359,13 @@ You can define the following custom settings: ignoreCollisionBoxesWhenFolded = "true" /> + + + @@ -327,6 +379,22 @@ You can define the following custom settings: + + + + + + + + + diff --git a/config/VehicleSettingsSetup.xml b/config/VehicleSettingsSetup.xml index aeb5cb720..adbfe7500 100644 --- a/config/VehicleSettingsSetup.xml +++ b/config/VehicleSettingsSetup.xml @@ -76,6 +76,9 @@ + diff --git a/config/gamePadHud/SiloLoaderGamePadHudPage.xml b/config/gamePadHud/SiloLoaderGamePadHudPage.xml index c406daddd..34fc6f86f 100644 --- a/config/gamePadHud/SiloLoaderGamePadHudPage.xml +++ b/config/gamePadHud/SiloLoaderGamePadHudPage.xml @@ -5,5 +5,7 @@ --> + + diff --git a/config/jobParameters/CombineUnloaderJobParameterSetup.xml b/config/jobParameters/CombineUnloaderJobParameterSetup.xml index 21cd74d8e..92b203fe4 100644 --- a/config/jobParameters/CombineUnloaderJobParameterSetup.xml +++ b/config/jobParameters/CombineUnloaderJobParameterSetup.xml @@ -27,7 +27,7 @@ - + diff --git a/config/jobParameters/JobParameterSetup.xml b/config/jobParameters/JobParameterSetup.xml index fa003f020..ef081c84c 100644 --- a/config/jobParameters/JobParameterSetup.xml +++ b/config/jobParameters/JobParameterSetup.xml @@ -15,16 +15,18 @@ - 1 - 2 - 3 + 1 + 2 + 3 4 + 5 nearest first last bunkerSilo + siloLoader diff --git a/config/jobParameters/SiloLoaderJobParameterSetup.xml b/config/jobParameters/SiloLoaderJobParameterSetup.xml index 6ec615b9c..68040f16b 100644 --- a/config/jobParameters/SiloLoaderJobParameterSetup.xml +++ b/config/jobParameters/SiloLoaderJobParameterSetup.xml @@ -10,6 +10,20 @@ - + + + + + + 1 + 2 + + + trailer + unloadTrigger + + + + diff --git a/img/helpmenu/shovelloadertrigger.dds b/img/helpmenu/shovelloadertrigger.dds new file mode 100644 index 000000000..b0e406bec Binary files /dev/null and b/img/helpmenu/shovelloadertrigger.dds differ diff --git a/modDesc.xml b/modDesc.xml index 6d7920e3a..56bea87ef 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -222,11 +222,24 @@ Changelog 7.1.0.0: + + + + + + + + + + + + + @@ -238,19 +251,20 @@ Changelog 7.1.0.0: - - - - - + + + + + + @@ -259,31 +273,30 @@ Changelog 7.1.0.0: + + + + + + + + + + + + + - - - - - - - - - - - - - - - + @@ -318,19 +331,27 @@ Changelog 7.1.0.0: - - - - - - - + + + + + + + + + + + + + + + @@ -338,6 +359,7 @@ Changelog 7.1.0.0: + @@ -345,14 +367,13 @@ Changelog 7.1.0.0: - - - + + @@ -361,6 +382,7 @@ Changelog 7.1.0.0: + @@ -369,6 +391,7 @@ Changelog 7.1.0.0: + @@ -381,6 +404,7 @@ Changelog 7.1.0.0: + @@ -392,7 +416,6 @@ Changelog 7.1.0.0: - @@ -405,6 +428,7 @@ Changelog 7.1.0.0: + diff --git a/scripts/Course.lua b/scripts/Course.lua index 5b6e579b7..58c09b489 100644 --- a/scripts/Course.lua +++ b/scripts/Course.lua @@ -910,6 +910,7 @@ function Course:appendWaypoints(waypoints) end --- Append another course to the course +---@param other Course function Course:append(other) self:appendWaypoints(other.waypoints) end diff --git a/scripts/CpSettingsUtil.lua b/scripts/CpSettingsUtil.lua index 24d9639bc..945cf28e9 100644 --- a/scripts/CpSettingsUtil.lua +++ b/scripts/CpSettingsUtil.lua @@ -70,7 +70,7 @@ function CpSettingsUtil.init() schema:register(XMLValueType.STRING, "Settings#autoUpdateGui", "Gui gets updated automatically") local key = "Settings.SettingSubTitle(?)" - schema:register(XMLValueType.STRING, key .."#title", "Setting sub title", nil, true) + schema:register(XMLValueType.STRING, key .."#title", "Setting sub title", nil) schema:register(XMLValueType.BOOL, key .."#prefix", "Setting sub title is a prefix", true) schema:register(XMLValueType.STRING, key.."#isDisabled", "Callback function, if the settings is disabled.") -- optional @@ -157,7 +157,7 @@ function CpSettingsUtil.loadSettingsFromSetup(class, filePath) class.pageTitle = setupKey .. "title" end xmlFile:iterate("Settings.SettingSubTitle", function (i, masterKey) - local subTitle = xmlFile:getValue(masterKey.."#title") + local subTitle = xmlFile:getValue(masterKey.."#title", "...") --- This flag can by used to simplify the translation text. local pre = xmlFile:getValue(masterKey.."#prefix", true) if pre then diff --git a/scripts/CpUtil.lua b/scripts/CpUtil.lua index 65f2436ad..aa7de4cdf 100644 --- a/scripts/CpUtil.lua +++ b/scripts/CpUtil.lua @@ -169,11 +169,10 @@ end --- Debug function for implements, that calls CpUtil.debugVehicle, with the root vehicle. ---@param channel number ---@param implement table ----@param ... unknown -function CpUtil.debugImplement(channel, implement, ...) +function CpUtil.debugImplement(channel, implement, str, ...) local rootVehicle = implement and implement.rootVehicle or implement CpUtil.debugVehicle(channel, rootVehicle, - CpUtil.getName(implement) .. ': ' .. string.format(...)) + CpUtil.getName(implement) .. ": " .. str, ...) end function CpUtil.isVehicleDebugActive(vehicle) diff --git a/scripts/ai/AIDriveStrategyCourse.lua b/scripts/ai/AIDriveStrategyCourse.lua index 283a110fd..6012b541d 100644 --- a/scripts/ai/AIDriveStrategyCourse.lua +++ b/scripts/ai/AIDriveStrategyCourse.lua @@ -111,8 +111,7 @@ function AIDriveStrategyCourse:setAIVehicle(vehicle, jobParameters) self:initializeImplementControllers(vehicle) self.ppc = PurePursuitController(vehicle) self.ppc:registerListeners(self, 'onWaypointPassed', 'onWaypointChange') - -- TODO_22 properly implement this in courseplaySpec - self.storage = vehicle.spec_courseplaySpec + self.storage = vehicle.spec_cpAIWorker self.settings = vehicle:getCpSettings() self.courseGeneratorSettings = vehicle:getCourseGeneratorSettings() @@ -602,13 +601,13 @@ end --------------------------------------------------------------------------------------------------------------------------- function AIDriveStrategyCourse:disableCollisionDetection() if self.vehicle then - CourseplaySpec.disableCollisionDetection(self.vehicle) + CpAIWorker.disableCollisionDetection(self.vehicle) end end function AIDriveStrategyCourse:enableCollisionDetection() if self.vehicle then - CourseplaySpec.enableCollisionDetection(self.vehicle) + CpAIWorker.enableCollisionDetection(self.vehicle) end end diff --git a/scripts/ai/AIDriveStrategyShovelSiloLoader.lua b/scripts/ai/AIDriveStrategyShovelSiloLoader.lua new file mode 100644 index 000000000..1f28a76aa --- /dev/null +++ b/scripts/ai/AIDriveStrategyShovelSiloLoader.lua @@ -0,0 +1,704 @@ +--[[ +This file is part of Courseplay (https://github.com/Courseplay/Courseplay_FS22) +Copyright (C) 2023 Courseplay Dev Team + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +]] + +--[[ + +This drive strategy implements: + - Loading from an bunker silo or a heap on a field with a wheel loader. + - Dumping the picked up fill level to an unload trigger oder a trailer. + - Automatically setting the shovel/arm Positions of the wheel loader. + +]] + + + +---@class AIDriveStrategyShovelSiloLoader : AIDriveStrategyCourse +---@field shovelController ShovelController +AIDriveStrategyShovelSiloLoader = {} +local AIDriveStrategyShovelSiloLoader_mt = Class(AIDriveStrategyShovelSiloLoader, AIDriveStrategyCourse) + +---------------------------------------------------------------- +--- State properties +---------------------------------------------------------------- +--[[ + shovelPosition : number (1-4) + shovelMovingSpeed : number|nil speed while the shovel/ front loader is moving +]] + + +---------------------------------------------------------------- +--- States +---------------------------------------------------------------- + +AIDriveStrategyShovelSiloLoader.myStates = { + DRIVING_ALIGNMENT_COURSE = {shovelPosition = ShovelController.POSITIONS.TRANSPORT}, + DRIVING_INTO_SILO = {shovelPosition = ShovelController.POSITIONS.LOADING, shovelMovingSpeed = 0}, + DRIVING_OUT_OF_SILO = {shovelPosition = ShovelController.POSITIONS.TRANSPORT}, + DRIVING_TEMPORARY_OUT_OF_SILO = {shovelPosition = ShovelController.POSITIONS.TRANSPORT}, + WAITING_FOR_TRAILER = {shovelPosition = ShovelController.POSITIONS.TRANSPORT}, + DRIVING_TO_UNLOAD_POSITION = {shovelPosition = ShovelController.POSITIONS.TRANSPORT}, + DRIVING_TO_UNLOAD_TRAILER = {shovelPosition = ShovelController.POSITIONS.TRANSPORT}, + DRIVING_TO_UNLOAD = {shovelPosition = ShovelController.POSITIONS.PRE_UNLOADING, shovelMovingSpeed = 0}, + UNLOADING = {shovelPosition = ShovelController.POSITIONS.UNLOADING, shovelMovingSpeed = 0}, + REVERSING_AWAY_FROM_UNLOAD = {shovelPosition = ShovelController.POSITIONS.PRE_UNLOADING, shovelMovingSpeed = 0}, +} + +AIDriveStrategyShovelSiloLoader.maxValidTrailerDistanceToSiloFront = 30 +AIDriveStrategyShovelSiloLoader.searchForTrailerDelaySec = 10 +AIDriveStrategyShovelSiloLoader.distShovelTrailerPreUnload = 7 +AIDriveStrategyShovelSiloLoader.distShovelUnloadStationPreUnload = 8 +AIDriveStrategyShovelSiloLoader.isStuckMs = 1000 * 15 +function AIDriveStrategyShovelSiloLoader.new(customMt) + if customMt == nil then + customMt = AIDriveStrategyShovelSiloLoader_mt + end + local self = AIDriveStrategyCourse.new(customMt) + AIDriveStrategyCourse.initStates(self, AIDriveStrategyShovelSiloLoader.myStates) + self.state = self.states.INITIAL + self.debugChannel = CpDebug.DBG_SILO + return self +end + +function AIDriveStrategyShovelSiloLoader:delete() + AIDriveStrategyShovelSiloLoader:superClass().delete(self) + if self.siloController then + self.siloController:delete() + self.siloController = nil + end + CpUtil.destroyNode(self.heapNode) + CpUtil.destroyNode(self.unloadPositionNode) + CpUtil.destroyNode(self.siloFrontNode) + self.isStuckTimer:delete() +end + +function AIDriveStrategyShovelSiloLoader:getGeneratedCourse(jobParameters) + return nil +end + +---@param bunkerSilo CpBunkerSilo +---@param heapSilo CpHeapBunkerSilo +function AIDriveStrategyShovelSiloLoader:setSiloAndHeap(bunkerSilo, heapSilo) + self.bunkerSilo = bunkerSilo + self.heapSilo = heapSilo +end + +---@param unloadTrigger CpTrigger +---@param unloadStation table +function AIDriveStrategyShovelSiloLoader:setUnloadTriggerAndStation(unloadTrigger, unloadStation) + self.unloadTrigger = unloadTrigger + self.unloadStation = unloadStation +end + +function AIDriveStrategyShovelSiloLoader:startWithoutCourse(jobParameters) + + -- to always have a valid course (for the traffic conflict detector mainly) + self.course = Course.createStraightForwardCourse(self.vehicle, 25) + self:startCourse(self.course, 1) + + self.jobParameters = jobParameters + self.unloadPositionNode = CpUtil.createNode("unloadPositionNode", 0, 0, 0) + + --- Is the unload target a trailer? + self.isUnloadingAtTrailerActive = jobParameters.unloadAt:getValue() == CpSiloLoaderJobParameters.UNLOAD_TRAILER + if not self.isUnloadingAtTrailerActive then + self:debug("Starting shovel silo to unload into unload trigger.") + --- Uses the exactFillRootNode from the trigger + --- and the direction of the unload position marker + --- to place the unload position node slightly in front. + local x, y, z = getWorldTranslation(self.unloadTrigger:getFillUnitExactFillRootNode(1)) + setTranslation(self.unloadPositionNode, x, y, z) + local position = jobParameters.unloadPosition + local dirX, dirZ = position:getDirection() + setDirection(self.unloadPositionNode, dirX, 0, dirZ, 0, 0, 1) + local dx, dy, dz = localToWorld(self.unloadPositionNode, 0, 0, -math.max( + self.distShovelUnloadStationPreUnload, self.turningRadius, 1.5 * AIUtil.getLength(self.vehicle))) + setTranslation(self.unloadPositionNode, dx, dy, dz) + else + self:debug("Starting shovel silo to unload into trailer.") + end + if self.bunkerSilo ~= nil then + self:debug("Bunker silo was found.") + self.silo = self.bunkerSilo + else + self:debug("Heap was found.") + self.silo = self.heapSilo + end + --- fill level, when the driver is started + self.fillLevelLeftOverSinceStart = self.silo:getTotalFillLevel() + + local cx, cz = self.silo:getFrontCenter() + local dirX, dirZ = self.silo:getLengthDirection() + local yRot = MathUtil.getYRotationFromDirection(dirX, dirZ) + self.siloFrontNode = CpUtil.createNode("siloFrontNode", cx, cz, yRot) + self.siloAreaToAvoid = PathfinderUtil.NodeArea(self.siloFrontNode, -self.silo:getWidth()/2 - 3, + -3, self.silo:getWidth() + 6, self.silo:getLength() + 6) + + self.siloController = CpBunkerSiloLoaderController(self.silo, self.vehicle, self) + + self.vehicle:raiseAIEvent("onAIFieldWorkerStart", "onAIImplementStart") +end + +----------------------------------------------------------------------------------------------------------------------- +--- Implement handling +----------------------------------------------------------------------------------------------------------------------- +function AIDriveStrategyShovelSiloLoader:initializeImplementControllers(vehicle) + self:addImplementController(vehicle, MotorController, Motorized, {}, nil) + self:addImplementController(vehicle, WearableController, Wearable, {}, nil) + ---@type table, ShovelController + self.shovelImplement, self.shovelController = self:addImplementController(vehicle, ShovelController, Shovel, {}, nil) + +end + +--- Fuel save only allowed when no trailer is there to unload into. +function AIDriveStrategyShovelSiloLoader:isFuelSaveAllowed() + return self.state == self.states.WAITING_FOR_TRAILER +end + +----------------------------------------------------------------------------------------------------------------------- +--- Static parameters (won't change while driving) +----------------------------------------------------------------------------------------------------------------------- +function AIDriveStrategyShovelSiloLoader:setAllStaticParameters() + self.reverser = AIReverseDriver(self.vehicle, self.ppc) + self.proximityController = ProximityController(self.vehicle, self:getWorkWidth()) + self.proximityController:registerIgnoreObjectCallback(self, self.ignoreProximityObject) + Markers.setMarkerNodes(self.vehicle) + self.frontMarkerNode, self.backMarkerNode, self.frontMarkerDistance, self.backMarkerDistance = + Markers.getMarkerNodes(self.vehicle) + self.siloEndProximitySensor = SingleForwardLookingProximitySensorPack(self.vehicle, self.shovelController:getShovelNode(), 5, 1) + self.heapNode = CpUtil.createNode("heapNode", 0, 0, 0, nil) + self.lastTrailerSearch = 0 + self.isStuckTimer = Timer.new(self.isStuckMs) + self.isStuckTimer:setFinishCallback(function () + if self.frozen then + return + end + if self.state == self.states.DRIVING_INTO_SILO then + self:debug("Was stuck trying to drive into the bunker silo.") + self:startDrivingOutOfSilo() + self:setNewState(self.states.DRIVING_TEMPORARY_OUT_OF_SILO) + end + end) +end + +----------------------------------------------------------------------------------------------------------------------- +--- Event listeners +----------------------------------------------------------------------------------------------------------------------- +function AIDriveStrategyShovelSiloLoader:onWaypointPassed(ix, course) + if course:isLastWaypointIx(ix) then + if self.state == self.states.DRIVING_ALIGNMENT_COURSE then + local course = self:getRememberedCourseAndIx() + self:startCourse(course, 1) + self:setNewState(self.states.DRIVING_INTO_SILO) + elseif self.state == self.states.DRIVING_INTO_SILO then + self:startDrivingOutOfSilo() + elseif self.state == self.states.DRIVING_OUT_OF_SILO then + if self.isUnloadingAtTrailerActive then + self:setNewState(self.states.WAITING_FOR_TRAILER) + else + self:startPathfindingToUnloadPosition() + end + elseif self.state == self.states.DRIVING_TEMPORARY_OUT_OF_SILO then + self:startDrivingToSilo({self.siloController:getLastTarget()}) + elseif self.state == self.states.DRIVING_TO_UNLOAD_TRAILER then + self:approachTrailerForUnloading() + elseif self.state == self.states.DRIVING_TO_UNLOAD_POSITION then + self:approachUnloadStationForUnloading() + elseif self.state == self.states.DRIVING_TO_UNLOAD then + self:setNewState(self.states.UNLOADING) + elseif self.state == self.states.REVERSING_AWAY_FROM_UNLOAD then + if self.shovelController:isEmpty() then + self:startDrivingToSilo() + else + self:setNewState(self.states.WAITING_FOR_TRAILER) + end + end + end +end + +--- this the part doing the actual work on the field after/before all +--- implements are started/lowered etc. +function AIDriveStrategyShovelSiloLoader:getDriveData(dt, vX, vY, vZ) + self:updateLowFrequencyImplementControllers() + local moveForwards = not self.ppc:isReversing() + local gx, gz, _ + if not moveForwards then + local maxSpeed + gx, gz, maxSpeed = self:getReverseDriveData() + self:setMaxSpeed(maxSpeed) + else + gx, _, gz = self.ppc:getGoalPointPosition() + end + if self.state == self.states.INITIAL then + if self.silo:getTotalFillLevel() <=0 then + self:debug("Stopping the driver, as the silo is empty.") + self.vehicle:stopCurrentAIJob(AIMessageSuccessFinishedJob.new()) + return + end + if self.shovelController:isFull() then + if self.isUnloadingAtTrailerActive then + self:setNewState(self.states.WAITING_FOR_TRAILER) + else + self:startPathfindingToUnloadPosition() + end + else + self:startDrivingToSilo() + end + self:setMaxSpeed(0) + elseif self.state == self.states.DRIVING_ALIGNMENT_COURSE then + self:setMaxSpeed(self.settings.fieldSpeed:getValue()) + elseif self.state == self.states.WAITING_FOR_PATHFINDER then + self:setMaxSpeed(0) + elseif self.state == self.states.DRIVING_INTO_SILO then + self:setMaxSpeed(self.settings.bunkerSiloSpeed:getValue()) + if AIUtil.isStopped(self.vehicle) and not self.proximityController:isStopped() then + --- Updates the is stuck timer + self.isStuckTimer:startIfNotRunning() + end + local _, _, closestObject = self.siloEndProximitySensor:getClosestObjectDistanceAndRootVehicle() + local isEndReached, maxSpeed = self.siloController:isEndReached(self.shovelController:getShovelNode(), 0) + if self.silo:isTheSameSilo(closestObject) or isEndReached then + self:debug("End wall detected or bunker silo end is reached.") + self:startDrivingOutOfSilo() + end + if self.shovelController:isFull() then + self:debug("Shovel is full, starting to drive out of the silo.") + self:startDrivingOutOfSilo() + end + elseif self.state == self.states.DRIVING_OUT_OF_SILO then + self:setMaxSpeed(self.settings.bunkerSiloSpeed:getValue()) + elseif self.state == self.states.DRIVING_TEMPORARY_OUT_OF_SILO then + self:setMaxSpeed(self.settings.bunkerSiloSpeed:getValue()) + elseif self.state == self.states.DRIVING_TO_UNLOAD_POSITION then + self:setMaxSpeed(self.settings.fieldSpeed:getValue()) + elseif self.state == self.states.DRIVING_TO_UNLOAD_TRAILER then + self:setMaxSpeed(self.settings.fieldSpeed:getValue()) + elseif self.state == self.states.WAITING_FOR_TRAILER then + self:setMaxSpeed(0) + if (g_time - self.lastTrailerSearch) > self.searchForTrailerDelaySec * 1000 then + self:searchForTrailerToUnloadInto() + self.lastTrailerSearch = g_time + end + elseif self.state == self.states.DRIVING_TO_UNLOAD then + self:setMaxSpeed(self.settings.reverseSpeed:getValue()) + local refNode + if self.isUnloadingAtTrailerActive then + refNode = self.targetTrailer.exactFillRootNode + else + refNode = self.unloadTrigger:getFillUnitExactFillRootNode(1) + end + if self.shovelController:isShovelOverTrailer(refNode) then + self:setNewState(self.states.UNLOADING) + self:setMaxSpeed(0) + end + if not self.isUnloadingAtTrailerActive then + if self.shovelController:isShovelOverTrailer(refNode, 3) and self.shovelController:canDischarge(self.unloadTrigger) then + self:setNewState(self.states.UNLOADING) + self:setMaxSpeed(0) + end + end + elseif self.state == self.states.UNLOADING then + self:setMaxSpeed(0) + if self:hasFinishedUnloading() then + self:startReversingAwayFromUnloading() + end + elseif self.state == self.states.REVERSING_AWAY_FROM_UNLOAD then + self:setMaxSpeed(self.settings.fieldSpeed:getValue()) + end + if self.state.properties.shovelPosition then + if not self.frozen and self.shovelController:moveShovelToPosition(self.state.properties.shovelPosition) then + if self.state.properties.shovelMovingSpeed ~= nil then + self:setMaxSpeed(self.state.properties.shovelMovingSpeed) + end + end + end + self:limitSpeed() + self:checkProximitySensors(moveForwards) + return gx, gz, moveForwards, self.maxSpeed, 100 +end + +function AIDriveStrategyShovelSiloLoader:update(dt) + if CpDebug:isChannelActive(CpDebug.DBG_SILO, self.vehicle) then + if self.siloFrontNode and self.state == self.states.WAITING_FOR_TRAILER then + DebugUtil.drawDebugCircleAtNode(self.siloFrontNode, self.maxValidTrailerDistanceToSiloFront, + math.ceil(self.maxValidTrailerDistanceToSiloFront), nil, false, {0, 3, 0}) + end + if self.course:isTemporary() then + self.course:draw() + elseif self.ppc:getCourse():isTemporary() then + self.ppc:getCourse():draw() + end + if self.silo then + self.silo:drawDebug() + self.siloAreaToAvoid:drawDebug() + end + self.siloController:draw() + if self.heapSilo then + CpUtil.drawDebugNode(self.heapNode, false, 3) + end + if self.targetTrailer then + CpUtil.drawDebugNode(self.targetTrailer.exactFillRootNode, false, 3, "ExactFillRootNode") + end + CpUtil.drawDebugNode(self.unloadPositionNode, false, 3) + end + self:updateImplementControllers(dt) + AIDriveStrategyCourse.update(self) +end + +function AIDriveStrategyShovelSiloLoader:updateCpStatus(status) + status:setSiloLoaderStatus(self.silo:getTotalFillLevel(), self.fillLevelLeftOverSinceStart) +end + +--- Ignores the bunker silo and the unload target for the proximity sensors. +function AIDriveStrategyShovelSiloLoader:ignoreProximityObject(object, vehicle) + if self.silo:isTheSameSilo(object) then + return true + end + --- This ignores the terrain. + if object == nil then + return true + end + if object == self.unloadStation then + return true + end + if self.unloadTrigger and self.unloadTrigger:isTheSameObject(object) then + return true + end + if self.targetTrailer then + if object == self.targetTrailer.trailer then + return true + end + end + return false +end + +function AIDriveStrategyShovelSiloLoader:getProximitySensorWidth() + -- a bit less as size.width always has plenty of buffer + return self.vehicle.size.width - 0.5 +end + +function AIDriveStrategyShovelSiloLoader:getWorkWidth() + return self.settings.bunkerSiloWorkWidth:getValue() +end + +function AIDriveStrategyShovelSiloLoader:setNewState(newState) + self:debug("Changed State from %s to %s", self.state.name, newState.name) + self.state = newState +end + +--- Is the trailer valid or not? +---@param trailer table +---@param trailerToIgnore table|nil +---@return boolean +---@return table|nil +function AIDriveStrategyShovelSiloLoader:isValidTrailer(trailer, trailerToIgnore) + local function debug(...) + self:debug("%s attached to: %s => %s", CpUtil.getName(trailer), + trailer.rootVehicle and CpUtil.getName(trailer.rootVehicle) or "no root vehicle", string.format(...)) + end + if not SpecializationUtil.hasSpecialization(Trailer, trailer.specializations) then + return false + end + if trailer.rootVehicle and not AIUtil.isStopped(trailer.rootVehicle) then + self:debug("is not stopped!", CpUtil.getName(trailer)) + return false + end + if trailerToIgnore and table.hasElement(trailerToIgnore, trailer) then + debug("will be ignored!", CpUtil.getName(trailer)) + return false + end + local canLoad, fillUnitIndex, fillType, exactFillRootNode = + ImplementUtil.getCanLoadTo(trailer, self.shovelImplement, + nil, debug) + if not canLoad or exactFillRootNode == nil then + debug("can't be used!", CpUtil.getName(trailer)) + return false + end + return true, { fillUnitIndex = fillUnitIndex, + fillType = fillType, + exactFillRootNode = exactFillRootNode, + trailer = trailer } +end + +--- Gets the closest trailer data with the distance +---@param trailerToIgnore table|nil optional trailers that will be ignored. +---@return table|nil +---@return number +function AIDriveStrategyShovelSiloLoader:getClosestTrailerAndDistance(trailerToIgnore) + local closestDistance = math.huge + local closestTrailerData = nil + for i, vehicle in pairs(g_currentMission.vehicles) do + local dist = calcDistanceFrom(vehicle.rootNode, self.siloFrontNode) + if dist < closestDistance then + local valid, trailerData = self:isValidTrailer(vehicle, trailerToIgnore) + if valid then + closestDistance = dist + closestTrailerData = trailerData + end + end + end + return closestTrailerData, closestDistance +end + +--- Searches for trailer to unload into in a maximum radius relative to the silo front center. +function AIDriveStrategyShovelSiloLoader:searchForTrailerToUnloadInto() + self:debug("Searching for an trailer nearby.") + local trailerData, dist = self:getClosestTrailerAndDistance({}) + if not trailerData then + self:debug("No valid trailer found anywhere!") + self:setInfoText(InfoTextManager.WAITING_FOR_UNLOADER) + return + end + local trailer = trailerData.trailer + if dist > self.maxValidTrailerDistanceToSiloFront then + self:debug("Closest Trailer %s attached to %s with the distance %.2fm/%.2fm found is to far away!", + CpUtil.getName(trailer), trailer.rootVehicle and CpUtil.getName(trailer.rootVehicle) or "no root vehicle", + dist, self.maxValidTrailerDistanceToSiloFront) + self:setInfoText(InfoTextManager.WAITING_FOR_UNLOADER) + return + end + self:clearInfoText(InfoTextManager.WAITING_FOR_UNLOADER) + --- Sets the unload position node in front of the closest side of the trailer. + self:debug("Found a valid trailer %s within distance %.2f", CpUtil.getName(trailer), dist) + self.targetTrailer = trailerData + local _, _, distShovelDirectionNode = localToLocal(self.shovelController:getShovelNode(), self.vehicle:getAIDirectionNode(), 0, 0, 0) + local dirX, _, dirZ = localDirectionToWorld(trailer.rootNode, 0, 0, 1) + local yRot = MathUtil.getYRotationFromDirection(dirX, dirZ) + local dx, _, dz = localToLocal(self.shovelController:getShovelNode(), trailer.rootNode, 0, 0, 0) + if dx > 0 then + local x, y, z = localToWorld(trailer.rootNode, math.abs(distShovelDirectionNode) + self.distShovelTrailerPreUnload, 0, 0) + setTranslation(self.unloadPositionNode, x, y, z) + setRotation(self.unloadPositionNode, 0, MathUtil.getValidLimit(yRot - math.pi/2), 0) + else + local x, y, z = localToWorld(trailer.rootNode, -math.abs(distShovelDirectionNode) - self.distShovelTrailerPreUnload, 0, 0) + setTranslation(self.unloadPositionNode, x, y, z) + setRotation(self.unloadPositionNode, 0, MathUtil.getValidLimit(yRot + math.pi/2), 0) + end + self:startPathfindingToTrailer() +end + +---------------------------------------------------------------- +--- Pathfinding +---------------------------------------------------------------- + +--- Find an alignment path to the silo lane course. +---@param course table silo lane course +function AIDriveStrategyShovelSiloLoader:startPathfindingToStart(course) + if not self.pathfinder or not self.pathfinder:isActive() then + self:setNewState(self.states.WAITING_FOR_PATHFINDER) + self:rememberCourse(course, 1) + local done, path + local fm = self:getFrontAndBackMarkers() + self.pathfinder, done, path = PathfinderUtil.startPathfindingFromVehicleToWaypoint( + self.vehicle, course, 1, 0, -(fm + 4), + true, nil, nil, + nil, 0, self.siloAreaToAvoid) + if done then + return self:onPathfindingDoneToStart(path) + else + self:setPathfindingDoneCallback(self, self.onPathfindingDoneToStart) + end + else + self:debug('Pathfinder already active') + end + return true +end + +function AIDriveStrategyShovelSiloLoader:onPathfindingDoneToStart(path) + if path and #path > 2 then + self:debug("Found alignment path to the course for the silo.") + local alignmentCourse = Course(self.vehicle, CourseGenerator.pointsToXzInPlace(path), true) + alignmentCourse:adjustForTowedImplements(2) + self:startCourse(alignmentCourse, 1) + self:setNewState(self.states.DRIVING_ALIGNMENT_COURSE) + else + self:debug("No path to the silo found!") + self.vehicle:stopCurrentAIJob(AIMessageCpErrorNoPathFound.new()) + --- TODO: Might need to consider a retry to another silo lane + end +end + +--- Starts Pathfinding to the position node in front of a unload trigger. +function AIDriveStrategyShovelSiloLoader:startPathfindingToUnloadPosition() + if not self.pathfinder or not self.pathfinder:isActive() then + self:setNewState(self.states.WAITING_FOR_PATHFINDER) + local done, path, goalNodeInvalid + self.pathfinder, done, path, goalNodeInvalid = PathfinderUtil.startPathfindingFromVehicleToNode( + self.vehicle, self.unloadPositionNode, + 0, 0, true, + nil, {}, nil, + 0, self.siloAreaToAvoid, false + ) + if done then + return self:onPathfindingDoneToUnloadPosition(path, goalNodeInvalid) + else + self:setPathfindingDoneCallback(self, self.onPathfindingDoneToUnloadPosition) + end + else + self:debug('Pathfinder already active') + end + return true +end + +function AIDriveStrategyShovelSiloLoader:onPathfindingDoneToUnloadPosition(path, goalNodeInvalid) + if path and #path > 2 then + self:debug("Found path to unloading station.") + local course = Course(self.vehicle, CourseGenerator.pointsToXzInPlace(path), true) + course:adjustForTowedImplements(2) + self:startCourse(course, 1) + self:setNewState(self.states.DRIVING_TO_UNLOAD_POSITION) + else + self:debug("Failed to drive close to unload position.") + self.vehicle:stopCurrentAIJob(AIMessageCpErrorNoPathFound.new()) + end +end + +--- Starts Pathfinding to the position node in front of the trailer side. +function AIDriveStrategyShovelSiloLoader:startPathfindingToTrailer() + if not self.pathfinder or not self.pathfinder:isActive() then + self:setNewState(self.states.WAITING_FOR_PATHFINDER) + local done, path, goalNodeInvalid + self.pathfinder, done, path, goalNodeInvalid = PathfinderUtil.startPathfindingFromVehicleToNode( + self.vehicle, self.unloadPositionNode, + 0, 0, true, + nil, {}, nil, + 0, self.siloAreaToAvoid, false + ) + if done then + return self:onPathfindingDoneToTrailer(path, goalNodeInvalid) + else + self:setPathfindingDoneCallback(self, self.onPathfindingDoneToTrailer) + end + else + self:debug('Pathfinder already active') + end + return true +end + +function AIDriveStrategyShovelSiloLoader:onPathfindingDoneToTrailer(path, goalNodeInvalid) + if path and #path > 2 then + self:debug("Found path to trailer %s.", CpUtil.getName(self.targetTrailer.trailer)) + local course = Course(self.vehicle, CourseGenerator.pointsToXzInPlace(path), true) + self:startCourse(course, 1) + self:setNewState(self.states.DRIVING_TO_UNLOAD_TRAILER) + else + self:debug("Failed to find path to trailer!") + ---self:setNewState(self.states.WAITING_FOR_TRAILER) + --- Later on we might try another approach? + self.vehicle:stopCurrentAIJob(AIMessageCpErrorNoPathFound.new()) + end +end +---------------------------------------------------------------- +--- Silo work +---------------------------------------------------------------- + +--- Starts driving into the silo lane +function AIDriveStrategyShovelSiloLoader:startDrivingToSilo(target) + --- Creates a straight course in the silo. + local startPos, endPos + if target then + startPos, endPos = unpack(target) + else + startPos, endPos = self.siloController:getTarget(self:getWorkWidth()) + end + local x, z = unpack(startPos) + local dx, dz = unpack(endPos) + local siloCourse = Course.createFromTwoWorldPositions(self.vehicle, x, z, dx, dz, + 0, 0, 3, 3, false) + local distance = siloCourse:getDistanceBetweenVehicleAndWaypoint(self.vehicle, 1) + if distance > self.turningRadius then + self:debug("Start driving to silo with pathfinder.") + self:startPathfindingToStart(siloCourse) + else + self:debug("Start driving into the silo directly.") + self:startCourse(siloCourse, 1) + self:setNewState(self.states.DRIVING_INTO_SILO) + end +end + +function AIDriveStrategyShovelSiloLoader:startDrivingOutOfSilo() + --- Creates the straight reverse course. + local startPos, endPos = self.siloController:getLastTarget() + local x, z = unpack(endPos) + local dx, dz = unpack(startPos) + local reverseCourse = Course.createFromTwoWorldPositions(self.vehicle, x, z, dx, dz, + 0, 0, 6, 3, true) + local ix = reverseCourse:getNextRevWaypointIxFromVehiclePosition(1, self.vehicle:getAIDirectionNode(), 10) + if ix == 1 then + ix = reverseCourse:getNumberOfWaypoints() + end + self:startCourse(reverseCourse, ix) + self:setNewState(self.states.DRIVING_OUT_OF_SILO) +end + +--- Drives from the position node in front of the trailer to the trailer, so the unloading can begin after that. +function AIDriveStrategyShovelSiloLoader:approachTrailerForUnloading() + local dx, _, dz = getWorldTranslation(self.targetTrailer.exactFillRootNode) + local x, _, z = getWorldTranslation(self.vehicle:getAIDirectionNode()) + local course = Course.createFromTwoWorldPositions(self.vehicle, x, z, dx, dz, + 0, -3, 0, 3, false) + local firstWpIx = course:getNearestWaypoints(self.vehicle:getAIDirectionNode()) + self:startCourse(course, firstWpIx) + self:setNewState(self.states.DRIVING_TO_UNLOAD) + self.shovelController:calculateMinimalUnloadingHeight(self.targetTrailer.exactFillRootNode) +end + +--- Drives from the position node in front of the trigger to the unload trigger, so the unloading can begin after that. +function AIDriveStrategyShovelSiloLoader:approachUnloadStationForUnloading() + local dx, _, dz = getWorldTranslation(self.unloadTrigger:getFillUnitExactFillRootNode()) + local x, _, z = getWorldTranslation(self.vehicle:getAIDirectionNode()) + local course = Course.createFromTwoWorldPositions(self.vehicle, x, z, dx, dz, + 0, -3, 3, 2, false) + local firstWpIx = course:getNearestWaypoints(self.vehicle:getAIDirectionNode()) + self:startCourse(course, firstWpIx) + self:setNewState(self.states.DRIVING_TO_UNLOAD) + self.shovelController:calculateMinimalUnloadingHeight(self.unloadTrigger:getFillUnitExactFillRootNode()) +end + +---------------------------------------------------------------- +--- Unloading +---------------------------------------------------------------- + +--- Is the unloading finished? +---@return boolean +function AIDriveStrategyShovelSiloLoader:hasFinishedUnloading() + if self.shovelController:isEmpty() then + self:debug("Finished unloading, as the shovel is empty.") + return true + end + if self.isUnloadingAtTrailerActive then + if self.targetTrailer.trailer:getFillUnitFreeCapacity(self.targetTrailer.fillUnitIndex) <= 0 then + self:debug("Trailer is full, abort unloading into trailer %s.", CpUtil.getName(self.targetTrailer.trailer)) + self.targetTrailer = nil + return true + end + else + if self.unloadTrigger:getFillUnitFreeCapacity(1, self.shovelController:getDischargeFillType(), self.vehicle:getOwnerFarmId()) <= 0 then + self:debugSparse("Unload Trigger is full.") + end + end + return false +end + +--- Starts reverse straight to make some space to the trailer or unload trigger. +function AIDriveStrategyShovelSiloLoader:startReversingAwayFromUnloading() + local _, _, spaceToTrailer = localToLocal(self.shovelController:getShovelNode(), self.vehicle:getAIDirectionNode(), 0, 0, 0) + local course = Course.createStraightReverseCourse(self.vehicle, 2*spaceToTrailer, + 0, self.vehicle:getAIDirectionNode() ) + self:startCourse(course, 1) + self:setNewState(self.states.REVERSING_AWAY_FROM_UNLOAD) +end diff --git a/scripts/ai/AIReverseDriver.lua b/scripts/ai/AIReverseDriver.lua index a8c4eb3da..249767095 100644 --- a/scripts/ai/AIReverseDriver.lua +++ b/scripts/ai/AIReverseDriver.lua @@ -276,4 +276,4 @@ function AIReverseDriver:calculateHitchCorrectionAngle(crossTrackError, orientat end return correctionAngle -end +end \ No newline at end of file diff --git a/scripts/ai/ImplementUtil.lua b/scripts/ai/ImplementUtil.lua index 8536cd752..82defb206 100644 --- a/scripts/ai/ImplementUtil.lua +++ b/scripts/ai/ImplementUtil.lua @@ -366,18 +366,18 @@ end ---@param tool table moving tool ---@param dt number ---@param rotTarget number target rotation in radiant +---@return boolean function ImplementUtil.moveMovingToolToRotation(implement, tool, dt, rotTarget) if tool.rotSpeed == nil then - return + return false end local spec = implement.spec_cylindered tool.curRot[1], tool.curRot[2], tool.curRot[3] = getRotation(tool.node) local oldRot = tool.curRot[tool.rotationAxis] local diff = rotTarget - oldRot - local rotSpeed = MathUtil.clamp(diff * tool.rotSpeed, tool.rotSpeed/3, 0.5) - if diff < 0 then - rotSpeed=rotSpeed*(-1) - end + local dir = MathUtil.sign(diff) + local rotSpeed = MathUtil.clamp( math.abs(diff) * math.abs(tool.rotSpeed), math.abs(tool.rotSpeed)/3, 0.5 ) + rotSpeed = dir * rotSpeed if math.abs(diff) < 0.03 or rotSpeed == 0 then ImplementUtil.stopMovingTool(implement, tool) return false @@ -404,9 +404,16 @@ function ImplementUtil.stopMovingTool(implement, tool) end function ImplementUtil.getLevelerNode(object) - return object.spec_leveler and object.spec_leveler.nodes and object.spec_leveler.nodes[1] and object.spec_leveler.nodes[1] + return object.spec_leveler and object.spec_leveler.nodes and object.spec_leveler.nodes[1] +end + +function ImplementUtil.getShovelNode(object) + return object.spec_shovel and object.spec_shovel.shovelNodes and object.spec_shovel.shovelNodes[1] end +--- Visually displays the bale collector offset +---@param vehicle table +---@param offset number function ImplementUtil.showBaleCollectorOffset(vehicle, offset) local implement = AIUtil.getImplementWithSpecialization(vehicle, BaleLoader) if not implement then @@ -417,4 +424,88 @@ function ImplementUtil.showBaleCollectorOffset(vehicle, offset) local dx, dy, dz = localToWorld(vehicle:getAIDirectionNode(), -offset, 3, 2) DebugUtil.drawDebugLine(x, y, z, dx, dy, dz, 1, 0, 0) end -end \ No newline at end of file +end + +--- Checks if loading from an implement to another is possible. +---@param loadTargetImplement table +---@param implementToLoadFrom table +---@param dischargeNode table|nil optional otherwise the current selected node is used. +---@param debugFunc function|nil +---@return boolean is loading possible? +---@return number|nil target implement fill unit ix to load into. +---@return number|nil fill type to load +---@return number|nil target exact fill root node +---@return number|nil alternative fill type, when the implement gets turned on +function ImplementUtil.getCanLoadTo(loadTargetImplement, implementToLoadFrom, dischargeNode, debugFunc) + + local function debug(str, ...) + if debugFunc then + debugFunc(str, ...) + end + end + + if dischargeNode == nil then + dischargeNode = implementToLoadFrom:getCurrentDischargeNode() + end + if dischargeNode == nil then + debug("No valid discharge node found!") + return false, nil, nil, nil + end + + local fillType = implementToLoadFrom:getDischargeFillType(dischargeNode) + local alternativeFillType + if implementToLoadFrom.spec_turnOnVehicle then + --- The discharge node flips when the implement gets turned on. + --- The fill type might be different then. + local turnOnDischargeNode = implementToLoadFrom.spec_turnOnVehicle.activateableDischargeNode + if turnOnDischargeNode then + alternativeFillType = implementToLoadFrom:getDischargeFillType(turnOnDischargeNode) + end + end + if fillType == nil or fillType == FillType.UNKNOWN then + debug("No valid fill type to load!") + return false, nil, nil, nil + end + + --- Is the fill unit a valid load target? + ---@param fillUnitIndex number + ---@return boolean + ---@return number|nil + ---@return number|nil + local function canLoad(fillUnitIndex) + if not loadTargetImplement:getFillUnitSupportsFillType(fillUnitIndex, fillType) and + not loadTargetImplement:getFillUnitSupportsFillType(fillUnitIndex, alternativeFillType) then + debug("Fill unit(%d) doesn't support fill type %s", fillUnitIndex, g_fillTypeManager:getFillTypeNameByIndex(fillType)) + return false + end + if not loadTargetImplement:getFillUnitAllowsFillType(fillUnitIndex, fillType) and + not loadTargetImplement:getFillUnitAllowsFillType(fillUnitIndex, alternativeFillType) then + debug("Fill unit(%d) doesn't allow fill type %s", fillUnitIndex, g_fillTypeManager:getFillTypeNameByIndex(fillType)) + return false + end + if loadTargetImplement.getFillUnitFreeCapacity and + loadTargetImplement:getFillUnitFreeCapacity(fillUnitIndex, fillType, implementToLoadFrom:getActiveFarm()) <= 0 and + loadTargetImplement:getFillUnitFreeCapacity(fillUnitIndex, alternativeFillType, implementToLoadFrom:getActiveFarm()) <= 0 then + debug("Fill unit(%d) is full with fill type %s!", fillUnitIndex, g_fillTypeManager:getFillTypeNameByIndex(fillType)) + return false + end + if loadTargetImplement.getIsFillAllowedFromFarm and + not loadTargetImplement:getIsFillAllowedFromFarm(implementToLoadFrom:getActiveFarm()) then + debug("Fill unit(%d) filling to target farm %s from %s not allowed!", + fillUnitIndex, loadTargetImplement:getOwnerFarmId(), implementToLoadFrom:getActiveFarm()) + return false + end + return true, fillUnitIndex, loadTargetImplement:getFillUnitExactFillRootNode(fillUnitIndex) + end + + local validTarget, targetFillUnitIndex, exactFillRootNode + for fillUnitIndex, fillUnit in pairs(loadTargetImplement:getFillUnits()) do + validTarget, targetFillUnitIndex, exactFillRootNode = canLoad(fillUnitIndex) + if validTarget then + break + end + end + + return validTarget, targetFillUnitIndex, fillType, exactFillRootNode, alternativeFillType +end + diff --git a/scripts/ai/Markers.lua b/scripts/ai/Markers.lua index 764d04301..217a04583 100644 --- a/scripts/ai/Markers.lua +++ b/scripts/ai/Markers.lua @@ -15,6 +15,13 @@ Markers = {} +function Markers.registerConsoleCommands() + g_devHelper.consoleCommands:registerConsoleCommand("cpFrontAndBackerMarkerCalculate", + "Calculates the front and back markers", "consoleCommandReload", Markers) + g_devHelper.consoleCommands:registerConsoleCommand("cpFrontAndBackerMarkerPrintDebug", + "Print Marker data", "consoleCommandPrintDebug", Markers) +end +Markers.registerConsoleCommands() -- a global table with the vehicle as the key to persist the marker nodes we don't want to leak through jobs -- and also don't want to deal with keeping track when to delete them g_vehicleMarkers = {} @@ -36,23 +43,23 @@ local function setBackMarkerNode(vehicle, measuredBackDistance) if AIUtil.hasImplementsOnTheBack(vehicle) then local lastImplement lastImplement, backMarkerOffset = AIUtil.getLastAttachedImplement(vehicle) - referenceNode = AIUtil.getDirectionNode(vehicle) + referenceNode = AIUtil.getDirectionNode(vehicle) CpUtil.debugVehicle(CpDebug.DBG_IMPLEMENTS, vehicle, 'Using the last implement\'s rear distance for the back marker node, %d m from root node', backMarkerOffset) elseif measuredBackDistance then - referenceNode = AIUtil.getDirectionNode(vehicle) + referenceNode = AIUtil.getDirectionNode(vehicle) backMarkerOffset = -measuredBackDistance CpUtil.debugVehicle(CpDebug.DBG_IMPLEMENTS, vehicle, 'back marker node on measured back distance %.1f', measuredBackDistance) elseif reverserNode then -- if there is a reverser node, use that, mainly because that most likely will turn with an implement -- or with the back component of an articulated vehicle. Just need to find out the distance correctly - local dx, _, dz = localToLocal(reverserNode, AIUtil.getDirectionNode(vehicle), 0, 0, 0) + local dx, _, dz = localToLocal(reverserNode, AIUtil.getDirectionNode(vehicle), 0, 0, 0) local dBetweenRootAndReverserNode = MathUtil.vector2Length(dx, dz) backMarkerOffset = dBetweenRootAndReverserNode - vehicle.size.length / 2 - vehicle.size.lengthOffset referenceNode = reverserNode CpUtil.debugVehicle(CpDebug.DBG_IMPLEMENTS, vehicle, 'Using the %s node for the back marker node %d m from root node (%d m between root and reverser)', debugText, backMarkerOffset, dBetweenRootAndReverserNode) else - referenceNode = AIUtil.getDirectionNode(vehicle) + referenceNode = AIUtil.getDirectionNode(vehicle) backMarkerOffset = - vehicle.size.length / 2 + vehicle.size.lengthOffset CpUtil.debugVehicle(CpDebug.DBG_IMPLEMENTS, vehicle, 'Using the vehicle\'s root node for the back marker node, %d m from root node', backMarkerOffset) end @@ -120,3 +127,31 @@ function Markers.getMarkerNodes(vehicle) local backMarker = Markers.getBackMarkerNode(vehicle) return frontMarker, backMarker, g_vehicleMarkers[vehicle].frontMarkerOffset, g_vehicleMarkers[vehicle].backMarkerOffset end + +-------------------------------------------- +--- Console Commands +-------------------------------------------- + +function Markers:consoleCommandReload(backDistance) + local vehicle = g_currentMission.controlledVehicle + if not vehicle then + CpUtil.info("No valid vehicle entered!") + return + end + if backDistance then + backDistance = tonumber(backDistance) + end + Markers.setMarkerNodes(vehicle, backDistance) + Markers:consoleCommandPrintDebug() +end + +function Markers:consoleCommandPrintDebug() + local vehicle = g_currentMission.controlledVehicle + if not vehicle then + CpUtil.info("No valid vehicle entered!") + return + end + local _, frontMarkerDistance = Markers.getFrontMarkerNode(vehicle) + local _, backMarkerDistance = Markers.getBackMarkerNode(vehicle) + CpUtil.infoVehicle(vehicle, "Front distance: %.2f, back distance: %.2f", frontMarkerDistance, backMarkerDistance) +end diff --git a/scripts/ai/ProximityController.lua b/scripts/ai/ProximityController.lua index 4045723dc..3cad859df 100644 --- a/scripts/ai/ProximityController.lua +++ b/scripts/ai/ProximityController.lua @@ -161,10 +161,6 @@ end ---@return boolean|nil direction is forwards if true or nil ---@return number maximum speed adjusted to slow down (or 0 to stop) when obstacles are ahead, otherwise maxSpeed function ProximityController:getDriveData(maxSpeed, moveForwards) - - --- Resets the traffic info text. - self.vehicle:resetCpActiveInfoText(InfoTextManager.BLOCKED_BY_OBJECT) - local d, vehicle, object, range, deg, dAvg, hitTerrain = math.huge, nil, nil, 10, 0, 0, false local pack = moveForwards and self.forwardLookingProximitySensorPack or self.backwardLookingProximitySensorPack if pack then @@ -173,12 +169,14 @@ function ProximityController:getDriveData(maxSpeed, moveForwards) end if self:ignoreObject(object, vehicle, moveForwards, hitTerrain) then self:setState(self.states.NO_OBSTACLE, 'No obstacle') + self.vehicle:resetCpActiveInfoText(InfoTextManager.BLOCKED_BY_OBJECT) return nil, nil, nil, maxSpeed end if object then --- Makes sure the detected object is not an implement or the root vehicle. for _, childVehicle in pairs(self.vehicle:getChildVehicles()) do if object == childVehicle then + self.vehicle:resetCpActiveInfoText(InfoTextManager.BLOCKED_BY_OBJECT) return nil, nil, nil, maxSpeed end end @@ -228,8 +226,9 @@ function ProximityController:getDriveData(maxSpeed, moveForwards) end if self.showBlockedByObjectMessageTimer:get() then self.vehicle:setCpInfoTextActive(InfoTextManager.BLOCKED_BY_OBJECT) + else + self.vehicle:resetCpActiveInfoText(InfoTextManager.BLOCKED_BY_OBJECT) end - return nil, nil, nil, maxSpeed end diff --git a/scripts/ai/controllers/PipeController.lua b/scripts/ai/controllers/PipeController.lua index 2ed47da1e..2040c0574 100644 --- a/scripts/ai/controllers/PipeController.lua +++ b/scripts/ai/controllers/PipeController.lua @@ -293,7 +293,11 @@ function PipeController:measurePipeProperties() currentPipeState, targetPipeState, pipeAnimTime) self:printFoldableDebug() self:printPipeDebug() - + local configOffsetX = g_vehicleConfigurations:get(self.implement, "unloadOffsetX" ) + if configOffsetX ~= nil then + self:debug("Setting pipe x offset to configured %.2f x offset", configOffsetX) + self.pipeOffsetX = configOffsetX + end end --- Unfolds the pipe completely to measure the pipe properties. @@ -403,6 +407,13 @@ end function PipeController:setupMoveablePipe() self.validMovingTools = {} + self.hasPipeMovingTools = false + self.tempBaseNode = CpUtil.createNode("tempBaseNode", 0, 0, 0) + self.tempDependedNode = CpUtil.createNode("tempDependedNode", 0, 0, 0) + if g_vehicleConfigurations:get(self.implement, "disablePipeMovingToolCorrection") then + --- Moveable pipe will not be adjusted based on a nearby trailer. + return + end if self.cylinderedSpec and self.pipeSpec.numAutoAimingStates <= 0 then for i, m in ipairs(self.cylinderedSpec.movingTools) do -- Gets only the pipe moving tools. @@ -437,9 +448,6 @@ function PipeController:setupMoveablePipe() end end - self.tempBaseNode = CpUtil.createNode("tempBaseNode", 0, 0, 0) - self.tempDependedNode = CpUtil.createNode("tempDependedNode", 0, 0, 0) - self:debug("Number of moveable pipe elements found: %d", #self.validMovingTools) end diff --git a/scripts/ai/controllers/ShovelController.lua b/scripts/ai/controllers/ShovelController.lua index 7a840dd6e..22d4ca547 100644 --- a/scripts/ai/controllers/ShovelController.lua +++ b/scripts/ai/controllers/ShovelController.lua @@ -1,14 +1,94 @@ ---@class ShovelController : ImplementController ShovelController = CpObject(ImplementController) -function ShovelController:init(vehicle, implement) +ShovelController.POSITIONS = { + DEACTIVATED = 0, + LOADING = 1, + TRANSPORT = 2, + PRE_UNLOADING = 3, + UNLOADING = 4, +} +ShovelController.MAX_TRIGGER_HEIGHT = 8 +ShovelController.MIN_TRIGGER_HEIGHT = 1 +ShovelController.TRIGGER_HEIGHT_RAYCAST_COLLISION_MASK = CollisionFlag.STATIC_WORLD + CollisionFlag.STATIC_OBJECTS + + CollisionFlag.STATIC_OBJECT + CollisionFlag.VEHICLE + +function ShovelController:init(vehicle, implement, isConsoleCommand) ImplementController.init(self, vehicle, implement) self.shovelSpec = self.implement.spec_shovel - self.shovelNode = self.shovelSpec.shovelNodes[1] + self.shovelNode = ImplementUtil.getShovelNode(implement) + self.turnOnSpec = self.implement.spec_turnOnVehicle + self.isConsoleCommand = isConsoleCommand + --- Sugar can unlading is still WIP + self.isSugarCaneTrailer = self.implement.spec_trailer ~= nil + self.sugarCaneTrailer = { + isDischargeActive = false, + isDischargingTimer = CpTemporaryObject(false), + movingTool = nil, + isMovingToolDirty = false, + isDischargingToGround = false + } + if self.isSugarCaneTrailer then + --- Find the moving tool for the sugar cane trailer + for i, tool in pairs(implement.cylindered.movingTools) do + if tool.axis then + self.sugarCaneTrailer.movingTool = tool + end + end + end +end + +function ShovelController:getDriveData() + local maxSpeed + if self.isSugarCaneTrailer then + --- Sugar cane trailer discharge + if self.sugarCaneTrailer.isDischargeActive then + if self.sugarCaneTrailer.isDischargingTimer:get() then + --- Waiting until the discharging stopped or + --- the trailer is empty + maxSpeed = 0 + self:debugSparse("Waiting for unloading!") + end + + -- if self.trailerSpec.tipState == Trailer.TIPSTATE_OPENING then + -- --- Trailer not yet ready to unload. + -- maxSpeed = 0 + -- self:debugSparse("Waiting for trailer animation opening!") + -- end + if self:isEmpty() then + --- Waiting for the trailer animation to finish. + maxSpeed = 0 + self:debugSparse("Waiting for trailer animation closing!") + end + else + -- ImplementUtil.moveMovingToolToRotation(self.implement, + -- self.sugarCaneTrailerMovingTool, dt , ) + end + end + return nil, nil, nil, maxSpeed end -function ShovelController:update() - +function ShovelController:update(dt) + if self.isSugarCaneTrailer then + --- Sugar cane trailer discharge + if self.sugarCaneTrailer.isDischargeActive then + if self:isEmpty() then + self:finishedSugarCaneTrailerDischarge() + end + if self.implement:getCanDischargeToGround(self.dischargeData.dischargeNode) then + --- Update discharge timer + self.sugarCaneTrailer.isDischargingTimer:set(true, 500) + if not self:isDischarging() then + -- self.implement:setDischargeState(Dischargeable.DISCHARGE_STATE_GROUND) + end + end + -- ImplementUtil.moveMovingToolToRotation(self.implement, + -- self.sugarCaneTrailerMovingTool, dt , ) + else + -- ImplementUtil.moveMovingToolToRotation(self.implement, + -- self.sugarCaneTrailerMovingTool, dt , ) + end + end end function ShovelController:getShovelNode() @@ -16,11 +96,11 @@ function ShovelController:getShovelNode() end function ShovelController:isFull() - return self:getFillLevelPercentage() >= 0.98 + return self:getFillLevelPercentage() >= 99 end function ShovelController:isEmpty() - return self:getFillLevelPercentage() <= 0.01 + return self:getFillLevelPercentage() <= 1 end function ShovelController:getFillLevelPercentage() @@ -40,6 +120,249 @@ function ShovelController:getShovelFillType() return self.shovelSpec.loadingFillType end -function ShovelController:isReadyToLoad() - return self:getShovelFillType() == FillType.UNKNOWN and self:getFillLevelPercentage() < 0.5 +function ShovelController:getDischargeFillType() + return self.implement:getDischargeFillType(self:getDischargeNode()) +end + +function ShovelController:getDischargeNode() + return self.implement:getCurrentDischargeNode() +end + +--- Checks if the shovel raycast has found an unload target. +---@param targetTrigger CpTrigger|nil +---@return boolean +function ShovelController:canDischarge(targetTrigger) + local dischargeNode = self:getDischargeNode() + local spec = self.implement.spec_dischargeable + if not spec.isAsyncRaycastActive then + local oldNode = dischargeNode.raycast.node + dischargeNode.raycast.node = self.implement.spec_attachable.attacherJoint.node + self.implement:updateRaycast(dischargeNode) + dischargeNode.raycast.node = oldNode + end + if targetTrigger and targetTrigger:getTrigger() ~= self.implement:getDischargeTargetObject(dischargeNode) then + return false + end + return dischargeNode.dischargeHit +end + +--- Is the shovel node over the trailer? +---@param refNode number +---@param margin number|nil +---@return boolean +function ShovelController:isShovelOverTrailer(refNode, margin) + local node = self:getShovelNode() + local _, _, distShovelToRoot = localToLocal(node, self.implement.rootVehicle:getAIDirectionNode(), 0, 0, 0) + local _, _, distTrailerToRoot = localToLocal(refNode, self.implement.rootVehicle:getAIDirectionNode(), 0, 0, 0) + margin = margin or 0 + if self:isHighDumpShovel() then + margin = margin + 1 + end + return ( distTrailerToRoot - distShovelToRoot ) < margin +end + +function ShovelController:isHighDumpShovel() + return g_vehicleConfigurations:get(self.implement, "shovelMovingToolIx") ~= nil +end + +--- Calculates the minimal unloading height for the trigger. +---@param triggerNode number|nil +---@return boolean +function ShovelController:calculateMinimalUnloadingHeight(triggerNode) + local sx, sy, sz = getWorldTranslation(self.vehicle:getAIDirectionNode()) + local tx, ty, tz + if triggerNode then + tx, ty, tz = getWorldTranslation(triggerNode) + else + local dirX, _, dirZ = localDirectionToWorld(self.vehicle:getAIDirectionNode(), 0, 0, 1) + local _, frontMarkerDistance = Markers.getFrontMarkerNode(self.vehicle) + tx, ty, tz = sx + dirX * (frontMarkerDistance + 4), sy, sz + dirZ * (frontMarkerDistance + 4) + end + local length = MathUtil.vector2Length(tx - sx, tz - sz) + 0.25 + local dx, dy, dz = tx - sx, ty - sy, tz -sz + local _, terrainHeight, _ = getWorldTranslation(self.vehicle.rootNode) + local maxHeightObjectHit = 0 + for i=self.MIN_TRIGGER_HEIGHT, self.MAX_TRIGGER_HEIGHT, 0.1 do + self.objectWasHit = false + raycastAll(sx, terrainHeight + i, sz, dx, 0, dz, + "calculateMinimalUnloadingHeightRaycastCallback", + length, self, + self.TRIGGER_HEIGHT_RAYCAST_COLLISION_MASK) + if self.objectWasHit then + maxHeightObjectHit = i + end + end + if maxHeightObjectHit > 0 then + self:debug("Finished raycast with minimal height: %.2f", maxHeightObjectHit) + self.implement:setCpShovelMinimalUnloadHeight(maxHeightObjectHit + 0.5) + return true + end + self:debug("Could not find a valid minimal height, so we use the maximum: %.2f", self.MAX_TRIGGER_HEIGHT) + self.implement:setCpShovelMinimalUnloadHeight(self.MAX_TRIGGER_HEIGHT) + return false +end + +--- Callback checks if an object was hit. +function ShovelController:calculateMinimalUnloadingHeightRaycastCallback(hitObjectId, x, y, z, distance, nx, ny, nz, subShapeIndex, shapeId, isLast) + if hitObjectId then + local object = g_currentMission.nodeToObject[hitObjectId] + if object then + if object ~= self.vehicle and object ~= self.implement then + self:debug("Object: %s was hit!", CpUtil.getName(object)) + self.objectWasHit = true + return true + end + else + self:debug("Shape was hit!") + self.objectWasHit = true + return true + end + return false + end + return false +end + +function ShovelController:delete() + if self.implement.cpResetShovelState then + self.implement:cpResetShovelState() + end +end + +--- Applies the given shovel position and +--- enables shovels that need an activation for unloading. +---@param pos number shovel position 1-4 +---@return boolean reached? +function ShovelController:moveShovelToPosition(pos) + if self.turnOnSpec then + if pos == ShovelController.POSITIONS.UNLOADING then + if not self.implement:getIsTurnedOn() and self.implement:getCanBeTurnedOn() then + self.implement:setIsTurnedOn(true) + self:debug("Turning on the shovel.") + end + if not self.implement:getDischargeState() ~= Dischargeable.DISCHARGE_STATE_OBJECT then + self.implement:setDischargeState(Dischargeable.DISCHARGE_STATE_OBJECT) + end + return false + else + if self.implement:getIsTurnedOn() then + self.implement:setIsTurnedOn(false) + self:debug("turning off the shovel.") + end + end + end + if self.implement.cpSetShovelState == nil then + return false + end + self.implement:cpSetShovelState(pos) + return self.implement:areCpShovelPositionsDirty() +end + +-------------------------------------------- +--- WIP! Sugar cane trailer functions +-------------------------------------------- + +--- Gets the dischargeNode and offset from a selected tip side. +---@param tipSideID number +---@param isTippingToGroundNeeded boolean +---@return table|nil dischargeNodeIndex +---@return table|nil dischargeNode +---@return number|nil xOffset +function ShovelController:getDischargeNodeAndOffsetForTipSide(tipSideID, isTippingToGroundNeeded) + local dischargeNode = self:getDischargeNode() + return dischargeNode.index, dischargeNode, self:getDischargeXOffset(dischargeNode) +end + +--- Gets the x offset of the discharge node relative to the implement root. +function ShovelController:getDischargeXOffset(dischargeNode) + local node = dischargeNode.node + local xOffset, _ ,_ = localToLocal(node, self.implement.rootNode, 0, 0, 0) + return xOffset +end + +--- Starts AI Discharge to an object/trailer. +---@param dischargeNode table discharge node to use. +---@return boolean success +function ShovelController:startDischarge(dischargeNode) + self.sugarCaneTrailer.isDischargeActive = true + return true +end + +--- Starts discharging to the ground if possible. +function ShovelController:startDischargeToGround(dischargeNode) + self.sugarCaneTrailer.isDischargeActive = true + self.sugarCaneTrailer.isDischargingToGround = true + -- self.isDischargingToGround = true + -- self.dischargeData = { + -- dischargeNode = dischargeNode, + -- } + -- local tipSide = self.trailerSpec.dischargeNodeIndexToTipSide[dischargeNode.index] + -- if tipSide ~= nil then + -- self.implement:setPreferedTipSide(tipSide.index) + -- end + return true +end + +--- Callback for the drive strategy, when the unloading finished. +function ShovelController:setFinishDischargeCallback(finishDischargeCallback) + self.sugarCaneTrailer.finishDischargeCallback = finishDischargeCallback +end + +--- Callback for ai discharge. +function ShovelController:finishedSugarCaneTrailerDischarge() + self:debug("Finished unloading.") + if self.sugarCaneTrailer.finishDischargeCallback then + self.sugarCaneTrailer.finishDischargeCallback(self.driveStrategy, self) + end + self.sugarCaneTrailer.isDischargeActive = false + self.sugarCaneTrailer.isDischargingToGround = false +end + +function ShovelController:prepareForUnload() + return true +end + +function ShovelController:isDischarging() + return self.implement:getDischargeState() ~= Dischargeable.DISCHARGE_STATE_OFF +end + +--- Gets the discharge node z offset relative to the root vehicle direction node. +function ShovelController:getUnloadOffsetZ(dischargeNode) + local node = dischargeNode.node + local dist = ImplementUtil.getDistanceToImplementNode(self.vehicle:getAIDirectionNode(), + self.implement, node) + return dist +end + +-------------------------------------------- +--- Debug functions +-------------------------------------------- + +function ShovelController:printShovelDebug() + self:debug("--Shovel Debug--") + if self.shovelNode then + self:debug("Fill unit index: %d, max pickup angle: %.2f, width: %.2f", + self.shovelNode.fillUnitIndex, + math.deg(self.shovelNode.maxPickupAngle), + self.shovelNode.width) + if self.shovelNode.movingToolActivation then + self:debug("Has moving tool activation => open factor: %.2f, inverted: %s", + self.shovelNode.movingToolActivation.openFactor, + tostring(self.shovelNode.movingToolActivation.isInverted)) + end + if self.shovelSpec.shovelDischargeInfo.node then + self:debug("min angle: %.2f, max angle: %.2f", + math.deg(self.shovelSpec.shovelDischargeInfo.minSpeedAngle), + math.deg(self.shovelSpec.shovelDischargeInfo.maxSpeedAngle)) + end + end + self:debug("--Shovel Debug finished--") +end + +function ShovelController:debug(...) + if self.isConsoleCommand then + --- Ignore vehicle debug setting, if the pipe controller was created by a console command. + self:info(...) + return + end + ImplementController.debug(self, ...) end \ No newline at end of file diff --git a/scripts/ai/controllers/TrailerController.lua b/scripts/ai/controllers/TrailerController.lua index d667e53a1..88e5c70a7 100644 --- a/scripts/ai/controllers/TrailerController.lua +++ b/scripts/ai/controllers/TrailerController.lua @@ -59,9 +59,9 @@ end --- Gets the dischargeNode and offset from a selected tip side. ---@param tipSideID number ---@param isTippingToGroundNeeded boolean ----@return table dischargeNodeIndex ----@return table dischargeNode ----@return number xOffset +---@return table|nil dischargeNodeIndex +---@return table|nil dischargeNode +---@return number|nil xOffset function TrailerController:getDischargeNodeAndOffsetForTipSide(tipSideID, isTippingToGroundNeeded) local tipSide = self.trailerSpec.tipSides[tipSideID] if not tipSide then diff --git a/scripts/ai/jobs/CpAIJob.lua b/scripts/ai/jobs/CpAIJob.lua index 9d36dfb35..4212b4ace 100644 --- a/scripts/ai/jobs/CpAIJob.lua +++ b/scripts/ai/jobs/CpAIJob.lua @@ -364,8 +364,8 @@ function CpAIJob.registerJob(AIJobTypeManager) AIJobTypeManager:registerJobType(CpAIJobBaleFinder.name, CpAIJobBaleFinder.jobName, CpAIJobBaleFinder) AIJobTypeManager:registerJobType(CpAIJobFieldWork.name, CpAIJobFieldWork.jobName, CpAIJobFieldWork) AIJobTypeManager:registerJobType(CpAIJobCombineUnloader.name, CpAIJobCombineUnloader.jobName, CpAIJobCombineUnloader) - AIJobTypeManager:registerJobType(CpAIJobBunkerSilo.name, CpAIJobBunkerSilo.jobName, CpAIJobBunkerSilo) AIJobTypeManager:registerJobType(CpAIJobSiloLoader.name, CpAIJobSiloLoader.jobName, CpAIJobSiloLoader) + AIJobTypeManager:registerJobType(CpAIJobBunkerSilo.name, CpAIJobBunkerSilo.jobName, CpAIJobBunkerSilo) end diff --git a/scripts/ai/jobs/CpAIJobSiloLoader.lua b/scripts/ai/jobs/CpAIJobSiloLoader.lua index 60499cae4..dc3fdc8ca 100644 --- a/scripts/ai/jobs/CpAIJobSiloLoader.lua +++ b/scripts/ai/jobs/CpAIJobSiloLoader.lua @@ -1,5 +1,5 @@ ---- Job for stationary loader. ----@class CpAIJobSiloLoader : CpAIJobFieldWork +--- AI Job for silo loader like the ropa maus or wheel loaders. +---@class CpAIJobSiloLoader : CpAIJob ---@field heapPlot HeapPlot ---@field heapNode number CpAIJobSiloLoader = { @@ -36,6 +36,7 @@ function CpAIJobSiloLoader:setupJobParameters() CpAIJobSiloLoader:superClass().setupJobParameters(self) self:setupCpJobParameters(CpSiloLoaderJobParameters(self)) self.cpJobParameters.loadPosition:setSnappingAngle(math.pi/8) -- AI menu snapping angle of 22.5 degree. + self.cpJobParameters.unloadPosition:setSnappingAngle(math.pi/8) -- AI menu snapping angle of 22.5 degree. end function CpAIJobSiloLoader:getIsAvailableForVehicle(vehicle) @@ -46,8 +47,8 @@ function CpAIJobSiloLoader:getCanStartJob() return self.hasValidPosition end ----@param vehicle Vehicle ----@param mission Mission +---@param vehicle table +---@param mission table ---@param farmId number ---@param isDirectStart boolean disables the drive to by giants ---@param isStartPositionInvalid boolean resets the drive to target position by giants and the field position to the vehicle position. @@ -68,8 +69,15 @@ function CpAIJobSiloLoader:applyCurrentState(vehicle, mission, farmId, isDirectS self.cpJobParameters.loadPosition:setPosition(x, z) self.cpJobParameters.loadPosition:setAngle(angle) end -end + local x, z = self.cpJobParameters.unloadPosition:getPosition() + + -- no unload position use the vehicle's current position + if x == nil or z == nil then + x, _, z = getWorldTranslation(vehicle.rootNode) + self.cpJobParameters.unloadPosition:setPosition(x, z) + end +end function CpAIJobSiloLoader:setValues() CpAIJob.setValues(self) @@ -95,7 +103,10 @@ function CpAIJobSiloLoader:validate(farmId) self.heapPlot:setVisible(false) self.heap = nil self.bunkerSilo = nil + self.unloadStation = nil + self.unloadTrigger = nil self.hasValidPosition = false + self:getCpJobParameters().unloadStation:setValue(-1) local isValid, errorMessage = CpAIJob.validate(self, farmId) if not isValid then return isValid, errorMessage @@ -120,7 +131,33 @@ function CpAIJobSiloLoader:validate(farmId) else return false, g_i18n:getText("CP_error_no_heap_found") end - + if not AIUtil.hasChildVehicleWithSpecialization(vehicle, ConveyorBelt) then + if self.cpJobParameters.unloadAt:getValue() == CpSiloLoaderJobParameters.UNLOAD_TRIGGER then + --- Validates the unload trigger setup + local found, unloadTrigger, unloadStation = self:getUnloadTriggerAt(self.cpJobParameters.unloadPosition) + if found then + self.unloadStation = unloadStation + self.unloadTrigger = unloadTrigger + if unloadStation == nil then + return false, g_i18n:getText("CP_error_no_unload_trigger_found") + end + local id = NetworkUtil.getObjectId(unloadStation) + if id ~= nil then + CpUtil.debugVehicle(CpDebug.DBG_SILO, vehicle, + "Found a valid unload trigger: %s for %s", + CpUtil.getName(unloadTrigger), CpUtil.getName(unloadStation)) + self:getCpJobParameters().unloadStation:setValue(id) + self:getCpJobParameters().unloadStation:validateUnloadingStation() + end + else + return false, g_i18n:getText("CP_error_no_unload_trigger_found") + end + local unloadPosition = self:getCpJobParameters().unloadPosition + if unloadPosition.x == nil or unloadPosition.angle == nil then + return false, g_i18n:getText("CP_error_no_unload_trigger_found") + end + end + end return isValid, errorMessage end @@ -147,7 +184,80 @@ function CpAIJobSiloLoader:getBunkerSiloOrHeap(loadPosition, node) return found, nil, heapSilo end +--- Gets the unload trigger at the unload position. +---@param unloadPosition CpAIParameterPositionAngle +---@return boolean found? +---@return table|nil Trigger +---@return table|nil unloadStation +function CpAIJobSiloLoader:getUnloadTriggerAt(unloadPosition) + local x, z = unloadPosition:getPosition() + local dirX, dirZ = unloadPosition:getDirection() + if x == nil or dirX == nil then + return false + end + local fillType + local silo = self.heap or self.bunkerSilo + if silo then + fillType = silo:getFillType() + end + local found, trigger, station = g_triggerManager:getDischargeableUnloadTriggerAt( x, z, dirX, dirZ, 5, 25) + if found and fillType ~= nil then + --- Additional check if the fill type of the silo + --- matches with the fill type of the unload target. + if not trigger:getIsFillTypeAllowed(fillType) then + --- Fill type is not supported by the trigger. + found = false + local convertedOutputFillTypes = self:getConvertedFillTypes() + for _, convertedFillType in ipairs(convertedOutputFillTypes) do + --- Checks possible found fill type conversions + if trigger:getIsFillTypeAllowed(convertedFillType) then + found = true + break + end + end + end + end + return found, trigger, station +end + function CpAIJobSiloLoader:drawSilos(map) self.heapPlot:draw(map) g_bunkerSiloManager:drawSilos(map, self.bunkerSilo) + if self.cpJobParameters.unloadAt:getValue() == CpSiloLoaderJobParameters.UNLOAD_TRIGGER then + local fillTypes = self:getConvertedFillTypes() + local silo = self.heap or self.bunkerSilo + if silo then + table.insert(fillTypes, silo:getFillType()) + end + g_triggerManager:drawDischargeableTriggers(map, self.unloadTrigger, fillTypes) + end +end + + +--- Gets all the unloading stations. +function CpAIJobSiloLoader:getUnloadingStations() + local unloadingStations = {} + for _, unloadingStation in pairs(g_currentMission.storageSystem:getUnloadingStations()) do + --- TODO: Maybe a few stations need to be ignored? + --- For example stations that have no possible correct fill type + table.insert(unloadingStations, unloadingStation) + end + return unloadingStations +end + +--- Gets converted fill types if there are any. +---@return table +function CpAIJobSiloLoader:getConvertedFillTypes() + local fillTypes = {} + local vehicle = self:getVehicle() + if vehicle then + local shovels, found = AIUtil.getAllChildVehiclesWithSpecialization(vehicle, Shovel) + local spec = found and shovels[1].spec_turnOnVehicle + if spec and spec.activateableDischargeNode and spec.activateableDischargeNode.fillTypeConverter then + for _, data in pairs(spec.activateableDischargeNode.fillTypeConverter) do + table.insert(fillTypes, data.targetFillTypeIndex) + end + end + end + return fillTypes end \ No newline at end of file diff --git a/scripts/ai/jobs/CpJobParameters.lua b/scripts/ai/jobs/CpJobParameters.lua index 58e4bf271..39f871a82 100644 --- a/scripts/ai/jobs/CpJobParameters.lua +++ b/scripts/ai/jobs/CpJobParameters.lua @@ -127,6 +127,28 @@ function CpJobParameters:isBunkerSiloHudModeDisabled() return self:isAIMenuJob() end +function CpJobParameters:isSiloLoadingHudModeDisabled() + local vehicle = self.job:getVehicle() + if vehicle then + if not vehicle:getCanStartCpSiloLoaderWorker() then + return true + end + end + return self:isAIMenuJob() +end + +function CpJobParameters:isFieldWorkHudModeDisabled() + local vehicle = self.job:getVehicle() + if vehicle then + if (AIUtil.hasChildVehicleWithSpecialization(vehicle, Leveler) and + not AIUtil.hasChildVehicleWithSpecialization(vehicle, Shovel)) or + AIUtil.hasChildVehicleWithSpecialization(vehicle, ConveyorBelt) then + return true + end + end + return false +end + --- Callback raised by a setting and executed as an vehicle event. ---@param callbackStr string event to be raised ---@param setting AIParameterSettingList setting that raised the callback. @@ -137,7 +159,6 @@ function CpJobParameters:raiseCallback(callbackStr, setting, ...) end end - function CpJobParameters:__tostring() return tostring(self.settings) end @@ -205,8 +226,8 @@ function CpCombineUnloaderJobParameters:isFieldUnloadDisabled() return self.useGiantsUnload:getValue() end -function CpCombineUnloaderJobParameters:isUnloadStationSelectorDisabled() - return self:isGiantsUnloadDisabled() or not self.useGiantsUnload:getValue() +function CpCombineUnloaderJobParameters:isUnloadStationSelectorVisible() + return not self:isGiantsUnloadDisabled() and self.useGiantsUnload:getValue() end function CpCombineUnloaderJobParameters:isFieldUnloadPositionSelectorDisabled() @@ -218,7 +239,6 @@ function CpCombineUnloaderJobParameters:isFieldUnloadTipSideDisabled() return self:isFieldUnloadDisabled() or self:hasPipe() or not self.useFieldUnload:getValue() end - function CpCombineUnloaderJobParameters:hasPipe() local vehicle = self.job:getVehicle() if vehicle then @@ -311,6 +331,9 @@ end --- AI parameters for the bunker silo job. ---@class CpSiloLoaderJobParameters : CpJobParameters +---@field unloadAt AIParameterSettingList +---@field UNLOAD_TRAILER number +---@field UNLOAD_TRIGGER number CpSiloLoaderJobParameters = CpObject(CpJobParameters) function CpSiloLoaderJobParameters:init(job) @@ -326,3 +349,34 @@ end function CpSiloLoaderJobParameters.getSettings(vehicle) return vehicle.spec_cpAISiloLoaderWorker.cpJob:getCpJobParameters() end + +function CpSiloLoaderJobParameters:isShovelSiloLoadDisabled() + local vehicle = self.job:getVehicle() + if vehicle then + return AIUtil.hasChildVehicleWithSpecialization(vehicle, ConveyorBelt) + end + return false +end + +function CpSiloLoaderJobParameters:isUnloadPositionDisabled() + return self:isShovelSiloLoadDisabled() or self.unloadAt:getValue() == CpSiloLoaderJobParameters.UNLOAD_TRAILER +end + +function CpSiloLoaderJobParameters:isUnloadStationDisabled() + return true +end + +function CpSiloLoaderJobParameters:generateUnloadingStations(setting, oldIx) + local unloadingStationIds = {} + local texts = {} + table.insert(unloadingStationIds, -1) + table.insert(texts, "---") + if self.job then + for i, unloadingStation in ipairs(self.job:getUnloadingStations()) do + local id = NetworkUtil.getObjectId(unloadingStation) + table.insert(unloadingStationIds, id) + table.insert(texts, unloadingStation:getName() or "") + end + end + return unloadingStationIds, texts, oldIx +end \ No newline at end of file diff --git a/scripts/ai/parameters/CpAIParameterPositionAngle.lua b/scripts/ai/parameters/CpAIParameterPositionAngle.lua index 46ccc8b02..8561c3698 100644 --- a/scripts/ai/parameters/CpAIParameterPositionAngle.lua +++ b/scripts/ai/parameters/CpAIParameterPositionAngle.lua @@ -109,7 +109,7 @@ end function CpAIParameterPosition:isAlmostEqualTo(otherPosition) local x, z = otherPosition:getPosition() if x ~= nil and self.x ~= nil then - return MathUtil.vector2Length(self.x - x, self.z - z) <= 1 + return MathUtil.vector2Length(self.x - x, self.z - z) <= 1 end return false end @@ -121,7 +121,8 @@ CpAIParameterPositionAngle = CpObject(CpAIParameterPosition) CpAIParameterPositionAngle.POSITION_TYPES = { DRIVE_TO = 0, --- with angle FIELD_OR_SILO = 1, --- without angle - UNLOAD = 2 --- with angle + UNLOAD = 2, --- with angle + LOAD = 3 --- with angle } ---@param data table ---@param vehicle table diff --git a/scripts/ai/parameters/CpAIParameterUnloadingStation.lua b/scripts/ai/parameters/CpAIParameterUnloadingStation.lua index 1afe6ecd4..22c39d61c 100644 --- a/scripts/ai/parameters/CpAIParameterUnloadingStation.lua +++ b/scripts/ai/parameters/CpAIParameterUnloadingStation.lua @@ -41,7 +41,7 @@ end --- Applies the current position to the map hotspot. function CpAIParameterUnloadingStation:applyToMapHotspot(mapHotspot) - if not self:getCanBeChanged() then + if not self:getIsVisible() then return false end local unloadingStation = self:getUnloadingStation() diff --git a/scripts/ai/tasks/CpAITaskSiloLoader.lua b/scripts/ai/tasks/CpAITaskSiloLoader.lua index b4a6feb0c..ddfc5333a 100644 --- a/scripts/ai/tasks/CpAITaskSiloLoader.lua +++ b/scripts/ai/tasks/CpAITaskSiloLoader.lua @@ -1,5 +1,6 @@ --- Bunker silo task ---@class CpAITaskSiloLoader +---@field job table CpAITaskSiloLoader = {} local CpAITaskSiloLoader_mt = Class(CpAITaskSiloLoader, AITask) @@ -32,7 +33,8 @@ end function CpAITaskSiloLoader:start() if self.isServer then - self.vehicle:startCpSiloLoaderWorker(self.job:getCpJobParameters(), self.silo, self.heap) + local _, unloadTrigger, unloadStation = self.job:getUnloadTriggerAt(self.job:getCpJobParameters().unloadPosition) + self.vehicle:startCpSiloLoaderWorker(self.job:getCpJobParameters(), self.silo, self.heap, unloadTrigger, unloadStation) end CpAITaskSiloLoader:superClass().start(self) diff --git a/scripts/config/VehicleConfigurations.lua b/scripts/config/VehicleConfigurations.lua index fb8a8509b..cef9e48fe 100644 --- a/scripts/config/VehicleConfigurations.lua +++ b/scripts/config/VehicleConfigurations.lua @@ -27,32 +27,15 @@ along with this program. If not, see . --- ---@class VehicleConfigurations : CpObject VehicleConfigurations = CpObject() -VehicleConfigurations.XML_KEY = "VehicleConfigurations.Vehicle" +VehicleConfigurations.BASE_KEY = "VehicleConfigurations" +VehicleConfigurations.XML_KEY = VehicleConfigurations.BASE_KEY .. ".Vehicle" +VehicleConfigurations.XML_CONFIGURATION_KEY = VehicleConfigurations.BASE_KEY .. ".Configurations.Configuration" VehicleConfigurations.MOD_NAME = g_currentModName ---- All attributes and the data type. -VehicleConfigurations.attributes = { - toolOffsetX = XMLValueType.FLOAT, - noReverse = XMLValueType.BOOL, - turnRadius = XMLValueType.FLOAT, - workingWidth = XMLValueType.FLOAT, - balerUnloadDistance = XMLValueType.FLOAT, - directionNodeOffsetZ = XMLValueType.FLOAT, - implementWheelAlwaysOnGround = XMLValueType.BOOL, - ignoreCollisionBoxesWhenFolded = XMLValueType.BOOL, - baleCollectorOffset = XMLValueType.FLOAT, - disableUnfolding = XMLValueType.BOOL, - raiseLate = XMLValueType.BOOL, - lowerEarly = XMLValueType.BOOL, - useVehicleSizeForMarkers = XMLValueType.BOOL, - armMovingToolIx = XMLValueType.INT, - movingToolIx = XMLValueType.INT, - ignoreBaleCollisionForward = XMLValueType.BOOL, -} - function VehicleConfigurations:init() self.vehicleConfigurations = {} self.modVehicleConfigurations = {} + self.attributes = {} if g_currentMission then self:loadFromXml() end @@ -63,17 +46,18 @@ function VehicleConfigurations:registerXmlSchema() self.xmlSchema = XMLSchema.new("vehicleConfigurations") self.xmlSchema:register(XMLValueType.STRING,self.XML_KEY.."(?)#name","Configuration name") self.xmlSchema:register(XMLValueType.STRING,self.XML_KEY.."(?)#modName","Mod name") --- Optional to avoid conflict for xml files with the same name. - for name,xmlType in pairs(VehicleConfigurations.attributes) do - self.xmlSchema:register(xmlType,self.XML_KEY.."(?)#"..name,"Configuration value") - end + self.xmlSchema:register(XMLValueType.STRING,self.XML_CONFIGURATION_KEY.."(?)#type","Configuration value type") + self.xmlSchema:register(XMLValueType.STRING,self.XML_CONFIGURATION_KEY.."(?)","Configuration name") end function VehicleConfigurations:loadFromXml() - self:registerXmlSchema() + self.vehicleConfigurations = {} + self.modVehicleConfigurations = {} + self.attributes = {} self.xmlFileName = Utils.getFilename('config/VehicleConfigurations.xml', Courseplay.BASE_DIRECTORY) - self.xmlFile = self:loadXmlFile(self.xmlFileName) + self:registerXmlSchema() + self.xmlFile = self:loadXmlFile(self.xmlFileName, true) self.userXmlFileName = getUserProfileAppPath() .. 'modSettings/'..VehicleConfigurations.MOD_NAME..'/vehicleConfigurations.xml' - self.userXmlFile = self:loadXmlFile(self.userXmlFileName) end @@ -112,19 +96,39 @@ function VehicleConfigurations:readVehicle(xmlFile, vehicleElement) end end -function VehicleConfigurations:loadXmlFile(fileName) + +function VehicleConfigurations:loadXmlFile(fileName, loadConfig) CpUtil.info('Loading vehicle configuration from %s ...', fileName) local xmlFile = XMLFile.loadIfExists("vehicleConfigurationsXmlFile",fileName, self.xmlSchema) if xmlFile then - xmlFile:iterate(self.XML_KEY, function (ix, key) - self:readVehicle(xmlFile, key) - end) + if not loadConfig or self:loadConfigurations(xmlFile) then + xmlFile:iterate(self.XML_KEY, function (ix, key) + self:readVehicle(xmlFile, key) + end) + end xmlFile:delete() else CpUtil.info('Vehicle configuration file %s does not exist.', fileName) end end +function VehicleConfigurations:loadConfigurations(xmlFile) + self.attributes = {} + xmlFile:iterate(self.XML_CONFIGURATION_KEY, function(ix, key) + local type = xmlFile:getValue(key .. "#type"):upper() + local name = xmlFile:getValue(key) + self.attributes[name] = XMLValueType[type] + if self.attributes[name] == nil then + CpUtil.info("Vehicle configuration %s has no valid type for %s!", name, type) + end + end) + for name, xmlType in pairs(self.attributes) do + CpUtil.info("Registered %s", name) + self.xmlSchema:register(xmlType, self.XML_KEY.."(?)#"..name, "Configuration value") + end + return true +end + --- Get a custom configuration value for a single vehicle/implement --- @param object table vehicle or implement object. This function uses the object's configFileName to uniquely --- identify the vehicle/implement. @@ -132,20 +136,23 @@ end --- @return any|nil the value of the configuration attribute or nil if there's no custom config for it function VehicleConfigurations:get(object, attribute) if not self:isValidAttribute(attribute) then - CpUtil.infoImplement(object, "The given attribute name: %s is not valid!", attribute) + CpUtil.infoImplement(object, "The given vehicle config attribute name: %s is not valid!", attribute) return end + local function getConfigName(data) + if data[object.configFileNameClean] then + return data[object.configFileNameClean][attribute] + elseif data[object.configFileNameClean..".xml"] then + return data[object.configFileNameClean..".xml"][attribute] + end + end if object and object.configFileNameClean then local modName = object.customEnvironment - if self.modVehicleConfigurations[modName] then + if modName and self.modVehicleConfigurations[modName] then --- If a mod name was given, then also check the xml filename. - if self.modVehicleConfigurations[modName][object.configFileNameClean] then - return self.modVehicleConfigurations[modName][object.configFileNameClean][attribute] - end - elseif self.vehicleConfigurations[object.configFileNameClean] then - return self.vehicleConfigurations[object.configFileNameClean][attribute] - elseif self.vehicleConfigurations[object.configFileNameClean..".xml"] then - return self.vehicleConfigurations[object.configFileNameClean..".xml"][attribute] + return getConfigName(self.modVehicleConfigurations[modName]) + else + return getConfigName(self.vehicleConfigurations) end end end @@ -221,6 +228,12 @@ function VehicleConfigurations:registerConsoleCommands() g_devHelper.consoleCommands:registerConsoleCommand("cpVehicleConfigurationsListAttributes", "Prints all valid attribute names", "consoleCommandPrintAttributeNames", self) + g_devHelper.consoleCommands:registerConsoleCommand("cpVehicleConfigurationsPrintAttributes", + "Prints all normal attributes", + "consoleCommandPrintAllNormalVehicleConfigurations", self) + g_devHelper.consoleCommands:registerConsoleCommand("cpVehicleConfigurationsPrintModAttributes", + "Prints all mod name attributes", + "consoleCommandPrintAllModNameVehicleConfigurations", self) end function VehicleConfigurations:consoleCommandReload() @@ -253,7 +266,10 @@ function VehicleConfigurations:consoleCommandPrintSingleAttributeValuesForVehicl local values = self:queryAttributeValues(vehicle, attribute) for _, data in pairs(values) do if data.found then - CpUtil.infoVehicle(data.implement, "%s", tostring(data.value)) + CpUtil.infoVehicle(data.implement, "(%s => Mod: %s) %s", + tostring(data.implement.configFileNameClean), + tostring(data.implement.customEnvironment ~= nil and data.implement.customEnvironment or false), + tostring(data.value)) else CpUtil.infoVehicle(data.implement, "not found") end @@ -272,16 +288,40 @@ function VehicleConfigurations:consoleCommandPrintAllAttributeValuesForVehicleAn CpUtil.info("Found the following for %s: ....", attribute) for _, data in pairs(values) do if data.found then - CpUtil.infoVehicle(data.implement, "%s", tostring(data.value)) + CpUtil.infoVehicle(data.implement, "(%s => Mod: %s) %s", + tostring(data.implement.configFileNameClean), + tostring(data.implement.customEnvironment ~= nil and data.implement.customEnvironment or false), + tostring(data.value)) end end end end end +function VehicleConfigurations:consoleCommandPrintAllModNameVehicleConfigurations() + for modName, configurations in pairs(self.modVehicleConfigurations) do + for name, configs in pairs(configurations) do + for n, v in pairs(configs) do + CpUtil.info("(%s => Mod: %s) %s => %s", + name, modName, n, v) + end + end + end +end + +function VehicleConfigurations:consoleCommandPrintAllNormalVehicleConfigurations() + for name, configs in pairs(self.vehicleConfigurations) do + for n, v in pairs(configs) do + CpUtil.info("(%s) %s => %s", + name, n, v) + end + end +end + function VehicleConfigurations:consoleCommandPrintAttributeNames() for attribute, xmlValueType in pairs(self.attributes) do - CpUtil.info("Attribute: %s => %s", attribute, XMLValueType.TYPES[xmlValueType].name) + CpUtil.info("Attribute: %s => %s", + attribute, XMLValueType.TYPES[xmlValueType].name) end end diff --git a/scripts/dev/DevHelper.lua b/scripts/dev/DevHelper.lua index 00c8fcc7a..e21336e55 100644 --- a/scripts/dev/DevHelper.lua +++ b/scripts/dev/DevHelper.lua @@ -39,15 +39,26 @@ function DevHelper:debug(...) CpUtil.info(string.format(...)) end +--- Makes sure deleting of the selected vehicle can be detected +function DevHelper:removedSelectedVehicle() + self.vehicle = nil +end + function DevHelper:update() if not self.isEnabled then return end local lx, lz, hasCollision, vehicle -- make sure not calling this for something which does not have courseplay installed (only ones with spec_aiVehicle) - if g_currentMission.controlledVehicle and g_currentMission.controlledVehicle.spec_aiVehicle then - + if g_currentMission.controlledVehicle and g_currentMission.controlledVehicle.spec_cpAIWorker then + if self.vehicle ~= g_currentMission.controlledVehicle then + if self.vehicle then + self.vehicle:removeDeleteListener(self, "removedSelectedVehicle") + end + --self.vehicleData = PathfinderUtil.VehicleData(g_currentMission.controlledVehicle, true) + end self.vehicle = g_currentMission.controlledVehicle + self.vehicle:addDeleteListener(self, "removedSelectedVehicle") self.node = g_currentMission.controlledVehicle:getAIDirectionNode() lx, _, lz = localDirectionToWorld(self.node, 0, 0, 1) @@ -262,10 +273,22 @@ function DevHelper:showAIMarkers() CpUtil.drawDebugNode(frontMarker, false, 3) CpUtil.drawDebugNode(backMarker, false, 3) - if self.vehicle:getAIDirectionNode() then - CpUtil.drawDebugNode(self.vehicle:getAIDirectionNode(), false , 3, "AiDirectionNode") + local directionNode = self.vehicle:getAIDirectionNode() + if directionNode then + CpUtil.drawDebugNode(self.vehicle:getAIDirectionNode(), false , 4, "AiDirectionNode") end - + local reverseNode = self.vehicle:getAIReverserNode() + if reverseNode then + CpUtil.drawDebugNode(reverseNode, false , 4.5, "AiReverseNode") + end + local steeringNode = self.vehicle:getAISteeringNode() + if steeringNode then + CpUtil.drawDebugNode(steeringNode, false , 5, "AiSteeringNode") + end + local articulatedAxisReverseNode = AIUtil.getArticulatedAxisVehicleReverserNode(self.vehicle) + if articulatedAxisReverseNode then + CpUtil.drawDebugNode(articulatedAxisReverseNode, false , 5.5, "AiArticulatedAxisReverseNode") + end end function DevHelper:togglePpcControlledNode() diff --git a/scripts/editor/CourseEditor.lua b/scripts/editor/CourseEditor.lua index e11782857..a2e34485e 100644 --- a/scripts/editor/CourseEditor.lua +++ b/scripts/editor/CourseEditor.lua @@ -114,7 +114,9 @@ function CourseEditor:showYesNoDialog(title, callbackFunc) end function CourseEditor:delete() - self.courseDisplay:delete() + if self.courseDisplay then + self.courseDisplay:delete() + end end --- Updates the course display, when a waypoint change happened. diff --git a/scripts/gui/CoursePlot.lua b/scripts/gui/CoursePlot.lua index 8b9edded8..6069f97f0 100644 --- a/scripts/gui/CoursePlot.lua +++ b/scripts/gui/CoursePlot.lua @@ -79,45 +79,6 @@ function CoursePlot:setStopPosition( x, z ) self.stopPosition.x, self.stopPosition.z = x, z end -function CoursePlot:worldToScreen(map, worldX, worldZ, isHudMap) - local objectX = (worldX + map.worldCenterOffsetX) / map.worldSizeX * 0.5 + 0.25 - local objectZ = (worldZ + map.worldCenterOffsetZ) / map.worldSizeZ * 0.5 + 0.25 - local x, y, _, _ = map.fullScreenLayout:getMapObjectPosition(objectX, objectZ, 0, 0, 0, true) - local rot = 0 - local visible = true - if isHudMap then - --- The plot is displayed in the hud. - objectX = (worldX + map.worldCenterOffsetX) / map.worldSizeX * map.mapExtensionScaleFactor + map.mapExtensionOffsetX - objectZ = (worldZ + map.worldCenterOffsetZ) / map.worldSizeZ * map.mapExtensionScaleFactor + map.mapExtensionOffsetZ - - x, y, rot, visible = map.layout:getMapObjectPosition(objectX, objectZ, 0, 0, 0, false) - if map.state == IngameMap.STATE_MINIMAP_ROUND and map.layout.rotateWithMap then - x, y, rot, visible = self:getMapObjectPositionCircleLayoutFix(map.layout, objectX, objectZ, 0, 0, 0, false) - end - end - return x, y, rot, visible -end - ---- Giants was not so kind, as to allow getting the positions even, if the object is outside the map range ... ---- This is a custom version for: IngameMapLayoutCircle:getMapObjectPosition(...) -function CoursePlot:getMapObjectPositionCircleLayoutFix(layout, objectU, objectV, width, height, rot, persistent) - local mapWidth, mapHeight = layout:getMapSize() - local mapX, mapY = layout:getMapPosition() - local objectX = objectU * mapWidth + mapX - local objectY = (1 - objectV) * mapHeight + mapY - objectX, objectY, rot = layout:rotateWithMap(objectX, objectY, rot, persistent) - objectX = objectX - width * 0.5 - objectY = objectY - height * 0.5 - - return objectX, objectY, rot, true -end - -function CoursePlot:screenToWorld( x, y ) - local worldX = ((x - self.x) / self.scaleX) - self.worldOffsetX - local worldZ = ((y - self.y - self.height) / -self.scaleZ) - self.worldOffsetZ - return worldX, worldZ -end - -- Draw the waypoints in the screen area defined in new(), the bottom left corner -- is at worldX/worldZ coordinates, the size shown is worldWidth wide (and high) function CoursePlot:drawPoints(map, points, isHudMap) @@ -129,14 +90,14 @@ function CoursePlot:drawPoints(map, points, isHudMap) if points and #points > 1 then -- I know this is in helpers.lua already but that code has too many dependencies -- on global variables and vehicle.cp. - local wp, np, startX, startY, endX, endY, dx, dz, dx2D, dy2D, width, rotation, r, g, b, sv, ev + local wp, np, startX, startY, endX, endY, dx, dz, dx2D, dy2D, width, rotation, r, g, b, sv, ev, _ -- render a line between subsequent waypoints for i = 1, #points - 1 do wp = points[ i ] np = points[ i + 1 ] - startX, startY, _, sv = self:worldToScreen(map, wp.x, wp.z, isHudMap) - endX, endY, _, ev = self:worldToScreen(map, np.x, np.z, isHudMap) + startX, startY, _, sv = CpGuiUtil.worldToScreen(map, wp.x, wp.z, isHudMap) + endX, endY, _, ev = CpGuiUtil.worldToScreen(map, np.x, np.z, isHudMap) -- render only if it is on the plot area if startX and startY and endX and endY then @@ -176,7 +137,7 @@ function CoursePlot:draw(map, isHudMap) -- render a sign marking the end of the course if self.stopPosition.x and self.stopPosition.z then - local x, y, rotation = self:worldToScreen( map,self.stopPosition.x, self.stopPosition.z, isHudMap) + local x, y, rotation = CpGuiUtil.worldToScreen( map,self.stopPosition.x, self.stopPosition.z, isHudMap) if x and y then setOverlayColor( self.stopSignOverlayId, 1, 1, 1, 1 ) renderOverlay( self.stopSignOverlayId, @@ -188,7 +149,7 @@ function CoursePlot:draw(map, isHudMap) -- render a sign marking the current position used as a starting location for the course if self.startPosition.x and self.startPosition.z then - local x, y, rotation = self:worldToScreen(map, self.startPosition.x, self.startPosition.z, isHudMap) + local x, y, rotation = CpGuiUtil.worldToScreen(map, self.startPosition.x, self.startPosition.z, isHudMap) if x and y then setOverlayColor( self.startSignOverlayId, 1, 1, 1, 0.8 ) renderOverlay( self.startSignOverlayId, diff --git a/scripts/gui/CpAIFrameExtended.lua b/scripts/gui/CpAIFrameExtended.lua index 99f2d296d..c3d48e1e1 100644 --- a/scripts/gui/CpAIFrameExtended.lua +++ b/scripts/gui/CpAIFrameExtended.lua @@ -14,19 +14,33 @@ CpInGameMenuAIFrameExtended.curDrawPositions={} CpInGameMenuAIFrameExtended.drawDelay = g_updateLoopIndex CpInGameMenuAIFrameExtended.DELAY = 1 CpInGameMenuAIFrameExtended.hotspotFilterState = {} +--- Hotspots visible, while drawing a custom field border. CpInGameMenuAIFrameExtended.validCustomFieldCreationHotspots = { - --- Hotspots visible, while drawing a custom field border. - MapHotspot.CATEGORY_FIELD, --- MapHotspot.CATEGORY_UNLOADING, --- MapHotspot.CATEGORY_LOADING, --- MapHotspot.CATEGORY_PRODUCTION, - MapHotspot.CATEGORY_AI, - MapHotspot.CATEGORY_COMBINE, - MapHotspot.CATEGORY_STEERABLE, - MapHotspot.CATEGORY_PLAYER, --- MapHotspot.CATEGORY_SHOP, --- MapHotspot.CATEGORY_OTHER - CustomFieldHotspot.CATEGORY + [MapHotspot.CATEGORY_FIELD] = true, +-- [MapHotspot.CATEGORY_UNLOADING] = true, +-- [MapHotspot.CATEGORY_LOADING] = true, +-- [MapHotspot.CATEGORY_PRODUCTION] = true, + [MapHotspot.CATEGORY_AI] = true, + [MapHotspot.CATEGORY_COMBINE] = true, + [MapHotspot.CATEGORY_STEERABLE] = true, + [MapHotspot.CATEGORY_PLAYER] = true, +-- MapHotspot.CATEGORY_SHOP] = true, +-- MapHotspot.CATEGORY_OTHER] = true, + [CustomFieldHotspot.CATEGORY] = true +} + +--- Hotspots visible, while picking a loading position. +CpInGameMenuAIFrameExtended.validPickingLoadingPositionHotspots = { + [MapHotspot.CATEGORY_FIELD] = true, +-- [MapHotspot.CATEGORY_UNLOADING] = true, +-- [MapHotspot.CATEGORY_LOADING] = true, +-- [MapHotspot.CATEGORY_PRODUCTION] = true, + [MapHotspot.CATEGORY_AI] = true, + [MapHotspot.CATEGORY_COMBINE] = true, + [MapHotspot.CATEGORY_STEERABLE] = true, + [MapHotspot.CATEGORY_PLAYER] = true, +-- MapHotspot.CATEGORY_SHOP] = true, +-- MapHotspot.CATEGORY_OTHER] = true, } function CpInGameMenuAIFrameExtended:onAIFrameLoadMapFinished() @@ -101,7 +115,7 @@ function CpInGameMenuAIFrameExtended:onAIFrameLoadMapFinished() g_messageCenter:subscribe(MessageType.GUI_AFTER_CLOSE, onCloseInGameMenu, g_currentMission.inGameMenu) g_messageCenter:subscribe(MessageType.GUI_BEFORE_OPEN, onOpenInGameMenu, g_currentMission.inGameMenu) --- Closes the course generator settings with the back button. - local function onClickBack(pageAI,superFunc) + local function onClickBack(pageAI,superFunc) if pageAI.mode == CpInGameMenuAIFrameExtended.MODE_COURSE_GENERATOR then pageAI:onClickOpenCloseCourseGenerator() return @@ -111,16 +125,26 @@ function CpInGameMenuAIFrameExtended:onAIFrameLoadMapFinished() return end CpInGameMenuAIFrameExtended.resetHotspots(self) - return superFunc(pageAI) + superFunc(pageAI) + if pageAI:getIsPicking() then + self:updateParameterValueTexts() + end + end - self.buttonBack.onClickCallback = Utils.overwrittenFunction(self.buttonBack.onClickCallback,onClickBack) - self.ingameMapBase.drawHotspotsOnly = Utils.appendedFunction(self.ingameMapBase.drawHotspotsOnly , CpInGameMenuAIFrameExtended.draw) + self.buttonBack.onClickCallback = Utils.overwrittenFunction( + self.buttonBack.onClickCallback,onClickBack) - --- Adds a second map hotspot for field position. + self.ingameMapBase.drawHotspotsOnly = Utils.appendedFunction( + self.ingameMapBase.drawHotspotsOnly , CpInGameMenuAIFrameExtended.draw) + + --- Adds the ai target hotspot. + self.driveToAiTargetMapHotspot = AITargetHotspot.new() self.fieldSiloAiTargetMapHotspot = AITargetHotspot.new() self.fieldSiloAiTargetMapHotspot.icon:setUVs(CpInGameMenuAIFrameExtended.positionUvs) --- Without angle - self.unloadAiTargetMapHotspot = AITargetHotspot.new() + self.loadAiTargetMapHotspot = AITargetHotspot.new() + + self.rawAiTargetMapHotspot = self.aiTargetMapHotspot self.ingameMap.onClickHotspotCallback = Utils.appendedFunction(self.ingameMap.onClickHotspotCallback, CpInGameMenuAIFrameExtended.onClickHotspot) @@ -131,6 +155,7 @@ end InGameMenuAIFrame.onLoadMapFinished = Utils.appendedFunction(InGameMenuAIFrame.onLoadMapFinished, CpInGameMenuAIFrameExtended.onAIFrameLoadMapFinished) +--- Creates alternative buttons, which are put into the button layout. function CpInGameMenuAIFrameExtended:setupButtons() local function createBtn(prefab, text, callback) local btn = prefab:clone(prefab.parent) @@ -195,6 +220,18 @@ end InGameMenuAIFrame.updateContextInputBarVisibility = Utils.appendedFunction(InGameMenuAIFrame.updateContextInputBarVisibility,CpInGameMenuAIFrameExtended.updateContextInputBarVisibility) +function CpInGameMenuAIFrameExtended:setJobMenuVisible(visible) + if not visible then + --- Removes the map hotspot, if the job menu of the vehicle is closed. + g_currentMission:removeMapHotspot(self.driveToAiTargetMapHotspot) + g_currentMission:removeMapHotspot(self.fieldSiloAiTargetMapHotspot) + g_currentMission:removeMapHotspot(self.unloadAiTargetMapHotspot) + g_currentMission:removeMapHotspot(self.loadAiTargetMapHotspot) + g_currentMission:removeMapHotspot(self.aiTargetMapHotspot) + end +end +InGameMenuAIFrame.setJobMenuVisible = Utils.appendedFunction(InGameMenuAIFrame.setJobMenuVisible, CpInGameMenuAIFrameExtended.setJobMenuVisible) + function CpInGameMenuAIFrameExtended:isCreateFieldBorderBtnVisible() local visible = self.mode == CpInGameMenuAIFrameExtended.MODE_DRAW_FIELD_BORDER or self.mode == InGameMenuAIFrame.MODE_OVERVIEW and self.currentHotspot == nil @@ -254,6 +291,7 @@ function InGameMenuAIFrame:onClickOpenCloseCourseGenerator() end end +--- Generates the correct course generator layout and binds the settings to the gui elements. function CpInGameMenuAIFrameExtended:bindCourseGeneratorSettings() local vehicle = InGameMenuMapUtil.getHotspotVehicle(self.currentHotspot) if vehicle ~=nil and vehicle.getCourseGeneratorSettings then @@ -319,59 +357,64 @@ end --- Updates the visibility of the vehicle settings on select/unselect of a vehicle in the ai menu page. --- Also updates the field position map hotspot. function CpInGameMenuAIFrameExtended:setMapSelectionItem(hotspot) + g_currentMission:removeMapHotspot(self.driveToAiTargetMapHotspot) g_currentMission:removeMapHotspot(self.fieldSiloAiTargetMapHotspot) g_currentMission:removeMapHotspot(self.unloadAiTargetMapHotspot) + g_currentMission:removeMapHotspot(self.loadAiTargetMapHotspot) g_currentMission:removeMapHotspot(self.aiTargetMapHotspot) - if hotspot ~= nil then - local vehicle = InGameMenuMapUtil.getHotspotVehicle(hotspot) - self.lastVehicle = vehicle - self.hudVehicle = nil - if vehicle then - if vehicle.getJob ~= nil then - local job = vehicle:getJob() - - if job ~= nil then - if job.getCpJobParameters ~= nil then - local parameters = job:getCpJobParameters():getAiTargetMapHotspotParameters() - for i, param in pairs(parameters) do - if param:is_a(CpAIParameterPosition) then - if param:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.DRIVE_TO then - if param:applyToMapHotspot(self.aiTargetMapHotspot) then - g_currentMission:addMapHotspot(self.aiTargetMapHotspot) - end - elseif param:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.FIELD_OR_SILO then - if param:applyToMapHotspot(self.fieldSiloAiTargetMapHotspot) then - g_currentMission:addMapHotspot(self.fieldSiloAiTargetMapHotspot) - end - elseif param:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.UNLOAD then - if param:applyToMapHotspot(self.unloadAiTargetMapHotspot) then - g_currentMission:addMapHotspot(self.unloadAiTargetMapHotspot) - end - end - elseif param:is_a(CpAIParameterUnloadingStation) then - g_currentMission:removeMapHotspot(self.aiUnloadingMarkerHotspot) - if param:applyToMapHotspot(self.aiUnloadingMarkerHotspot) then - g_currentMission:addMapHotspot(self.aiUnloadingMarkerHotspot) - end - end - end + if hotspot == nil then + return + end + local vehicle = InGameMenuMapUtil.getHotspotVehicle(hotspot) + self.lastVehicle = vehicle + self.hudVehicle = nil + if vehicle == nil or vehicle.getJob == nil then + return + end + local job = vehicle:getJob() + if job == nil or job.getCpJobParameters == nil then + return + end + if vehicle:getIsCpActive() then + local parameters = job:getCpJobParameters():getAiTargetMapHotspotParameters() + for i, param in pairs(parameters) do + if param:is_a(CpAIParameterPosition) then + if param:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.DRIVE_TO then + if param:applyToMapHotspot(self.driveToAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.driveToAiTargetMapHotspot) end - if job.getTarget ~= nil then - local x, z, rot = job:getTarget() - - self.aiTargetMapHotspot:setWorldPosition(x, z) - - if rot ~= nil then - self.aiTargetMapHotspot:setWorldRotation(rot + math.pi) - end - - g_currentMission:addMapHotspot(self.aiTargetMapHotspot) + elseif param:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.FIELD_OR_SILO then + if param:applyToMapHotspot(self.fieldSiloAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.fieldSiloAiTargetMapHotspot) + end + elseif param:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.LOAD then + if param:applyToMapHotspot(self.loadAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.loadAiTargetMapHotspot) + end + elseif param:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.UNLOAD then + if param:applyToMapHotspot(self.unloadAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.unloadAiTargetMapHotspot) end end - + elseif param:is_a(CpAIParameterUnloadingStation) then + g_currentMission:removeMapHotspot(self.aiUnloadingMarkerHotspot) + if param:applyToMapHotspot(self.aiUnloadingMarkerHotspot) then + g_currentMission:addMapHotspot(self.aiUnloadingMarkerHotspot) + end end end end + if job.getTarget ~= nil then + local x, z, rot = job:getTarget() + + self.aiTargetMapHotspot:setWorldPosition(x, z) + + if rot ~= nil then + self.aiTargetMapHotspot:setWorldRotation(rot + math.pi) + end + + g_currentMission:addMapHotspot(self.aiTargetMapHotspot) + end g_currentMission.inGameMenu:updatePages() end InGameMenuAIFrame.setMapSelectionItem = Utils.appendedFunction(InGameMenuAIFrame.setMapSelectionItem, CpInGameMenuAIFrameExtended.setMapSelectionItem) @@ -399,8 +442,10 @@ function CpInGameMenuAIFrameExtended:onAIFrameClose() self.courseGeneratorLayout:setVisible(false) self.contextBox:setVisible(true) self.lastHotspot = self.currentHotspot + g_currentMission:removeMapHotspot(self.driveToAiTargetMapHotspot) g_currentMission:removeMapHotspot(self.fieldSiloAiTargetMapHotspot) g_currentMission:removeMapHotspot(self.unloadAiTargetMapHotspot) + g_currentMission:removeMapHotspot(self.loadAiTargetMapHotspot) CpInGameMenuAIFrameExtended.unbindCourseGeneratorSettings(self) g_currentMission.inGameMenu:updatePages() end @@ -462,72 +507,94 @@ function CpInGameMenuAIFrameExtended:draw() end function CpInGameMenuAIFrameExtended:delete() + if self.driveToAiTargetMapHotspot then + self.driveToAiTargetMapHotspot:delete() + self.driveToAiTargetMapHotspot = nil + end if self.fieldSiloAiTargetMapHotspot ~= nil then self.fieldSiloAiTargetMapHotspot:delete() - self.fieldSiloAiTargetMapHotspot = nil end - if self.unloadAiTargetMapHotspot ~= nil then self.unloadAiTargetMapHotspot:delete() - self.unloadAiTargetMapHotspot = nil end + if self.loadAiTargetMapHotspot ~= nil then + self.loadAiTargetMapHotspot:delete() + self.loadAiTargetMapHotspot = nil + end end InGameMenuAIFrame.delete = Utils.appendedFunction(InGameMenuAIFrame.delete,CpInGameMenuAIFrameExtended.delete) + +function CpInGameMenuAIFrameExtended:onClickPositionParameter(superFunc, element, ...) + local parameter = element.aiParameter + if parameter:getCanBeChanged() then + --- Checks if the position setting is not disabled + superFunc(self, element, ...) + end +end +InGameMenuAIFrame.onClickPositionParameter = Utils.overwrittenFunction( + InGameMenuAIFrame.onClickPositionParameter, CpInGameMenuAIFrameExtended.onClickPositionParameter) +InGameMenuAIFrame.onClickPositionRotationParameter = Utils.overwrittenFunction( + InGameMenuAIFrame.onClickPositionRotationParameter, CpInGameMenuAIFrameExtended.onClickPositionParameter) + + --- Ugly hack to swap the main AI hotspot with the field position hotspot, --- as only the main hotspot can be moved by the player. function CpInGameMenuAIFrameExtended:startPickingPosition(superFunc, parameter, callback, ...) if parameter and parameter.getPositionType then CpInGameMenuAIFrameExtended.resetHotspots(self) - if parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.FIELD_OR_SILO then - local mapHotspot = self.aiTargetMapHotspot + if parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.DRIVE_TO then + self.aiTargetMapHotspot = self.driveToAiTargetMapHotspot + self.currentPickingMapHotspotType = CpAIParameterPositionAngle.POSITION_TYPES.DRIVE_TO + elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.FIELD_OR_SILO then self.aiTargetMapHotspot = self.fieldSiloAiTargetMapHotspot - self.fieldSiloAiTargetMapHotspot = mapHotspot self.currentPickingMapHotspotType = CpAIParameterPositionAngle.POSITION_TYPES.FIELD_OR_SILO elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.UNLOAD then - local mapHotspot = self.aiTargetMapHotspot self.aiTargetMapHotspot = self.unloadAiTargetMapHotspot - self.unloadAiTargetMapHotspot = mapHotspot self.currentPickingMapHotspotType = CpAIParameterPositionAngle.POSITION_TYPES.UNLOAD + CpInGameMenuAIFrameExtended.hotspotFilterState = {} + CpGuiUtil.saveAndDisableHotspotFilters(self.ingameMapBase, CpInGameMenuAIFrameExtended.hotspotFilterState) + CpGuiUtil.applyHotspotFilters(self.ingameMapBase, CpInGameMenuAIFrameExtended.validPickingLoadingPositionHotspots) + elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.LOAD then + self.aiTargetMapHotspot = self.loadAiTargetMapHotspot + self.currentPickingMapHotspotType = CpAIParameterPositionAngle.POSITION_TYPES.LOAD end end callback = Utils.appendedFunction(callback,function (finished, x, z) CpInGameMenuAIFrameExtended.resetHotspots(self) + self:updateParameterValueTexts() end) - + g_currentMission:removeMapHotspot(self.aiTargetMapHotspot) superFunc(self, parameter, callback, ...) end -InGameMenuAIFrame.startPickPosition = Utils.overwrittenFunction(InGameMenuAIFrame.startPickPosition,CpInGameMenuAIFrameExtended.startPickingPosition) -InGameMenuAIFrame.startPickPositionAndRotation = Utils.overwrittenFunction(InGameMenuAIFrame.startPickPositionAndRotation,CpInGameMenuAIFrameExtended.startPickingPosition) +InGameMenuAIFrame.startPickPosition = Utils.overwrittenFunction(InGameMenuAIFrame.startPickPosition, + CpInGameMenuAIFrameExtended.startPickingPosition) + +InGameMenuAIFrame.startPickPositionAndRotation = Utils.overwrittenFunction(InGameMenuAIFrame.startPickPositionAndRotation, + CpInGameMenuAIFrameExtended.startPickingPosition) function CpInGameMenuAIFrameExtended:resetHotspots() - if self.currentPickingMapHotspotType == CpAIParameterPositionAngle.POSITION_TYPES.FIELD_OR_SILO then - local mapHotspot = self.aiTargetMapHotspot - self.aiTargetMapHotspot = self.fieldSiloAiTargetMapHotspot - self.fieldSiloAiTargetMapHotspot = mapHotspot - self.currentPickingMapHotspotType = nil - elseif self.currentPickingMapHotspotType == CpAIParameterPositionAngle.POSITION_TYPES.UNLOAD then - local mapHotspot = self.aiTargetMapHotspot - self.aiTargetMapHotspot = self.unloadAiTargetMapHotspot - self.unloadAiTargetMapHotspot = mapHotspot - self.currentPickingMapHotspotType = nil + self.aiTargetMapHotspot = self.rawAiTargetMapHotspot + self.currentPickingMapHotspotType = nil + if CpInGameMenuAIFrameExtended.hotspotFilterState then + CpGuiUtil.applyHotspotFilters(self.ingameMapBase, CpInGameMenuAIFrameExtended.hotspotFilterState) end + CpInGameMenuAIFrameExtended.hotspotFilterState = nil end - --- Added support for the cp field target position. function CpInGameMenuAIFrameExtended:updateParameterValueTexts(superFunc, ...) if self.currentJobElements == nil then return end g_currentMission:removeMapHotspot(self.aiTargetMapHotspot) - g_currentMission:removeMapHotspot(self.aiLoadingMarkerHotspot) - g_currentMission:removeMapHotspot(self.aiUnloadingMarkerHotspot) + g_currentMission:removeMapHotspot(self.driveToAiTargetMapHotspot) g_currentMission:removeMapHotspot(self.fieldSiloAiTargetMapHotspot) g_currentMission:removeMapHotspot(self.unloadAiTargetMapHotspot) + g_currentMission:removeMapHotspot(self.loadAiTargetMapHotspot) for _, element in ipairs(self.currentJobElements) do local parameter = element.aiParameter local parameterType = parameter:getType() @@ -539,8 +606,8 @@ function CpInGameMenuAIFrameExtended:updateParameterValueTexts(superFunc, ...) elseif parameter.is_a and parameter:is_a(CpAIParameterPosition) then element:setText(parameter:getString()) if parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.DRIVE_TO then - if parameter:applyToMapHotspot(self.aiTargetMapHotspot) then - g_currentMission:addMapHotspot(self.aiTargetMapHotspot) + if parameter:applyToMapHotspot(self.driveToAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.driveToAiTargetMapHotspot) end elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.FIELD_OR_SILO then if parameter:applyToMapHotspot(self.fieldSiloAiTargetMapHotspot) then @@ -550,72 +617,93 @@ function CpInGameMenuAIFrameExtended:updateParameterValueTexts(superFunc, ...) if parameter:applyToMapHotspot(self.unloadAiTargetMapHotspot) then g_currentMission:addMapHotspot(self.unloadAiTargetMapHotspot) end + elseif parameter:getPositionType() == CpAIParameterPositionAngle.POSITION_TYPES.LOAD then + if parameter:applyToMapHotspot(self.loadAiTargetMapHotspot) then + g_currentMission:addMapHotspot(self.loadAiTargetMapHotspot) + end end elseif parameterType == AIParameterType.POSITION or parameterType == AIParameterType.POSITION_ANGLE then element:setText(parameter:getString()) - g_currentMission:addMapHotspot(self.aiTargetMapHotspot) + g_currentMission:addMapHotspot(self.rawAiTargetMapHotspot) local x, z = parameter:getPosition() - self.aiTargetMapHotspot:setWorldPosition(x, z) + self.rawAiTargetMapHotspot:setWorldPosition(x, z) if parameterType == AIParameterType.POSITION_ANGLE then local angle = parameter:getAngle() + math.pi - self.aiTargetMapHotspot:setWorldRotation(angle) + self.rawAiTargetMapHotspot:setWorldRotation(angle) end - else - element:updateTitle() + end + end +end +InGameMenuAIFrame.updateParameterValueTexts = Utils.overwrittenFunction(InGameMenuAIFrame.updateParameterValueTexts, + CpInGameMenuAIFrameExtended.updateParameterValueTexts) - if parameterType == AIParameterType.UNLOADING_STATION then - if parameter.applyToMapHotspot then - if parameter:applyToMapHotspot(self.aiUnloadingMarkerHotspot) then - g_currentMission:addMapHotspot(self.aiUnloadingMarkerHotspot) - end - else - local unloadingStation = parameter:getUnloadingStation() +function CpInGameMenuAIFrameExtended:updateWarnings() + g_currentMission:removeMapHotspot(self.aiLoadingMarkerHotspot) + g_currentMission:removeMapHotspot(self.aiUnloadingMarkerHotspot) + for _, element in ipairs(self.currentJobElements) do + local parameter = element.aiParameter + local parameterType = parameter:getType() + if parameterType == AIParameterType.TEXT then + local title = element:getDescendantByName("title") - if unloadingStation ~= nil then - local placeable = unloadingStation.owningPlaceable + title:setText(parameter:getString()) + elseif parameterType == AIParameterType.UNLOADING_STATION then + element:updateTitle() + if parameter.applyToMapHotspot then + if parameter:applyToMapHotspot(self.aiUnloadingMarkerHotspot) then + g_currentMission:addMapHotspot(self.aiUnloadingMarkerHotspot) + end + else + local unloadingStation = parameter:getUnloadingStation() - if placeable ~= nil and placeable.getHotspot ~= nil then - local hotspot = placeable:getHotspot(1) + if unloadingStation ~= nil then + local placeable = unloadingStation.owningPlaceable - if hotspot ~= nil then - local x, z = hotspot:getWorldPosition() + if placeable ~= nil and placeable.getHotspot ~= nil then + local hotspot = placeable:getHotspot(1) - self.aiUnloadingMarkerHotspot:setWorldPosition(x, z) - g_currentMission:addMapHotspot(self.aiUnloadingMarkerHotspot) - end + if hotspot ~= nil then + local x, z = hotspot:getWorldPosition() + self.aiUnloadingMarkerHotspot:setWorldPosition(x, z) + g_currentMission:addMapHotspot(self.aiUnloadingMarkerHotspot) end end end - elseif parameterType == AIParameterType.LOADING_STATION then - local loadingStation = parameter:getLoadingStation() + end + elseif parameterType == AIParameterType.UNLOADING_STATION then + element:updateTitle() + local loadingStation = parameter:getLoadingStation() - if loadingStation ~= nil and parameter:getCanBeChanged() then - local placeable = loadingStation.owningPlaceable + if loadingStation ~= nil and parameter:getCanBeChanged() then + local placeable = loadingStation.owningPlaceable - if placeable ~= nil and placeable.getHotspot ~= nil then - local hotspot = placeable:getHotspot(1) + if placeable ~= nil and placeable.getHotspot ~= nil then + local hotspot = placeable:getHotspot(1) - if hotspot ~= nil then - local x, z = hotspot:getWorldPosition() + if hotspot ~= nil then + local x, z = hotspot:getWorldPosition() - self.aiLoadingMarkerHotspot:setWorldPosition(x, z) - g_currentMission:addMapHotspot(self.aiLoadingMarkerHotspot) - end + self.aiLoadingMarkerHotspot:setWorldPosition(x, z) + g_currentMission:addMapHotspot(self.aiLoadingMarkerHotspot) end end end end - element:setDisabled(not parameter:getCanBeChanged()) end end -InGameMenuAIFrame.updateParameterValueTexts = Utils.overwrittenFunction(InGameMenuAIFrame.updateParameterValueTexts, - CpInGameMenuAIFrameExtended.updateParameterValueTexts) + +InGameMenuAIFrame.updateWarnings = Utils.appendedFunction(InGameMenuAIFrame.updateWarnings, + CpInGameMenuAIFrameExtended.updateWarnings) + +-------------------------------------------- +--- Custom fields +-------------------------------------------- --- Enables clickable field hotspots. function CpInGameMenuAIFrameExtended:onClickHotspot(element,hotspot) @@ -657,24 +745,18 @@ function InGameMenuAIFrame:onClickCreateFieldBorder() g_customFieldManager:addField(CpInGameMenuAIFrameExtended.curDrawPositions) CpInGameMenuAIFrameExtended.curDrawPositions = {} --- Restore hotspot filters here: - for k, v in pairs(self.ingameMapBase.filter) do - self.ingameMapBase:setHotspotFilter(k, CpInGameMenuAIFrameExtended.hotspotFilterState[k]) + if CpInGameMenuAIFrameExtended.hotspotFilterState then + CpGuiUtil.applyHotspotFilters(self.ingameMapBase, CpInGameMenuAIFrameExtended.hotspotFilterState) end - + CpInGameMenuAIFrameExtended.hotspotFilterState = nil else CpInGameMenuAIFrameExtended.curDrawPositions = {} self.drawingCustomFieldHeader:setVisible(true) self.mode = CpInGameMenuAIFrameExtended.MODE_DRAW_FIELD_BORDER CpInGameMenuAIFrameExtended.hotspotFilterState = {} --- Change the hotspot filter here: - for k, v in pairs(self.ingameMapBase.filter) do - CpInGameMenuAIFrameExtended.hotspotFilterState[k] = v - - self.ingameMapBase:setHotspotFilter(k, false) - end - for _,v in ipairs(CpInGameMenuAIFrameExtended.validCustomFieldCreationHotspots) do - self.ingameMapBase:setHotspotFilter(v, true) - end + CpGuiUtil.saveAndDisableHotspotFilters(self.ingameMapBase, CpInGameMenuAIFrameExtended.hotspotFilterState) + CpGuiUtil.applyHotspotFilters(self.ingameMapBase, CpInGameMenuAIFrameExtended.validCustomFieldCreationHotspots) end end diff --git a/scripts/gui/CpGamePadHudScreen.lua b/scripts/gui/CpGamePadHudScreen.lua index 276724dfe..836c11611 100644 --- a/scripts/gui/CpGamePadHudScreen.lua +++ b/scripts/gui/CpGamePadHudScreen.lua @@ -192,6 +192,9 @@ function CpGamePadHudFieldWorkScreen:update(dt, ...) if self.vehicle:getCanStartCpBunkerSiloWorker() and self.vehicle:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_BUNKER_SILO and not AIUtil.hasChildVehicleWithSpecialization(self.vehicle, Leveler) then self.vehicle:reopenCpGamePadHud() + elseif self.vehicle:getCanStartCpSiloLoaderWorker() and self.vehicle:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_SILO_LOADING + and not AIUtil.hasChildVehicleWithSpecialization(self.vehicle, ConveyorBelt) then + self.vehicle:reopenCpGamePadHud() end end @@ -260,7 +263,9 @@ end function CpGamePadHudSiloLoaderScreen:update(dt, ...) CpGamePadHudSiloLoaderScreen:superClass().update(self, dt, ...) - + if not self.vehicle:getCanStartCpSiloLoaderWorker() or self.vehicle:getCpStartingPointSetting():getValue() ~= CpJobParameters.START_AT_SILO_LOADING then + self.vehicle:reopenCpGamePadHud() + end end function CpGamePadHudSiloLoaderScreen:drawWorkWidth() diff --git a/scripts/gui/CpGuiUtil.lua b/scripts/gui/CpGuiUtil.lua index 4a3fd484b..489cadcd7 100644 --- a/scripts/gui/CpGuiUtil.lua +++ b/scripts/gui/CpGuiUtil.lua @@ -169,6 +169,15 @@ function CpGuiUtil.getGenericSubTitleElementFromLayout(rootElement) return CpGuiUtil.getFirstElementWithProfileName(rootElement, "settingsMenuSubtitle") end +--- Gets the rgb color in 0-1 from 0-255. +---@param r number +---@param g number +---@param b number +---@param alpha number|nil +---@return number +---@return number +---@return number +---@return number function CpGuiUtil.getNormalizedRgb(r, g, b, alpha) return r / 255, g / 255, b / 255, alpha end @@ -205,6 +214,83 @@ function CpGuiUtil.setTarget(element, target) element.targetName = target.name end +--- Apply the defined filter to the map. +---@param map table ingame menu map +---@param hotspots table +function CpGuiUtil.applyHotspotFilters(map, hotspots) + for k, v in pairs(hotspots) do + map:setHotspotFilter(k, v) + end +end + +--- Saves the hotspot filters and disables these on the map. +---@param map table +---@param hotspots table +function CpGuiUtil.saveAndDisableHotspotFilters(map, hotspots) + for k, v in pairs(map.filter) do + map:setHotspotFilter(k, false) + hotspots[k] = v + end +end + +------------------------------------------------ +--- Plots +------------------------------------------------ + +--- Translates the world coordinates to screen coordinates. +---@param map table +---@param worldX number +---@param worldZ number +---@param isHudMap boolean|nil If the hud minimap is used. +---@return number +---@return number +---@return number +---@return boolean +function CpGuiUtil.worldToScreen(map, worldX, worldZ, isHudMap) + local objectX = (worldX + map.worldCenterOffsetX) / map.worldSizeX * 0.5 + 0.25 + local objectZ = (worldZ + map.worldCenterOffsetZ) / map.worldSizeZ * 0.5 + 0.25 + local x, y, _, _ = map.fullScreenLayout:getMapObjectPosition(objectX, objectZ, 0, 0, 0, true) + local rot = 0 + local visible = true + if isHudMap then + --- The plot is displayed in the hud. + objectX = (worldX + map.worldCenterOffsetX) / map.worldSizeX * map.mapExtensionScaleFactor + map.mapExtensionOffsetX + objectZ = (worldZ + map.worldCenterOffsetZ) / map.worldSizeZ * map.mapExtensionScaleFactor + map.mapExtensionOffsetZ + + x, y, rot, visible = map.layout:getMapObjectPosition(objectX, objectZ, 0, 0, 0, false) + if map.state == IngameMap.STATE_MINIMAP_ROUND and map.layout.rotateWithMap then + x, y, rot, visible = CpGuiUtil.getMapObjectPositionCircleLayoutFix(map.layout, objectX, objectZ, 0, 0, 0, false) + end + end + return x, y, rot, visible +end + +--- Giants was not so kind, as to allow getting the positions even, if the object is outside the map range ... +--- This is a custom version for: IngameMapLayoutCircle:getMapObjectPosition(...) +---@param layout table +---@param objectU number +---@param objectV number +---@param width number +---@param height number +---@param rot number +---@param persistent boolean +---@return number +---@return number +---@return number +---@return boolean +function CpGuiUtil.getMapObjectPositionCircleLayoutFix(layout, objectU, objectV, width, height, rot, persistent) + local mapWidth, mapHeight = layout:getMapSize() + local mapX, mapY = layout:getMapPosition() + local objectX = objectU * mapWidth + mapX + local objectY = (1 - objectV) * mapHeight + mapY + objectX, objectY, rot = layout:rotateWithMap(objectX, objectY, rot, persistent) + objectX = objectX - width * 0.5 + objectY = objectY - height * 0.5 + + return objectX, objectY, rot, true +end + + ------------------------------------------------ --- Hud ------------------------------------------------ @@ -426,3 +512,23 @@ function CpGuiUtil.openGlobalSettingsGui(vehicle) local inGameMenu = CpGuiUtil.preOpeningInGameMenu(vehicle) inGameMenu:goToPage(inGameMenu.pageCpGlobalSettings) end + +CpGuiUtil.UNIT_EXTENSIONS = { + "k", + "M", + "G", + "T" +} +--- Converts the number into kilo, mega, giga or tera units with the correct symbol. +---@param num number +---@return number +---@return string +function CpGuiUtil.getFixedUnitValueWithUnitSymbol(num) + for i=4, 1, -1 do + local delta = math.pow(10, 3 * i) + if num >= delta then + return num / delta, CpGuiUtil.UNIT_EXTENSIONS[i] + end + end + return num, "" +end \ No newline at end of file diff --git a/scripts/gui/CpStatus.lua b/scripts/gui/CpStatus.lua index e756d37e6..ce261d9a8 100644 --- a/scripts/gui/CpStatus.lua +++ b/scripts/gui/CpStatus.lua @@ -132,11 +132,9 @@ function CpStatus:getCompactionText(withoutPercentageSymbol) end function CpStatus:getSiloFillLevelPercentageLeftOver(withoutPercentageSymbol) - if self.isActive and self.fillLevelPercentageLeftOver ~=nil then - if withoutPercentageSymbol then - return tostring(self.fillLevelPercentageLeftOver) - end - return string.format('%d%%', self.fillLevelPercentageLeftOver) + if self.isActive and self.fillLevelLeftOver ~= nil then + local value, unitExtension = CpGuiUtil.getFixedUnitValueWithUnitSymbol(self.fillLevelLeftOver) + return string.format("%.1f %s%s", value, unitExtension, g_i18n:getText("unit_literShort")) end return '--' end diff --git a/scripts/gui/FieldPlot.lua b/scripts/gui/FieldPlot.lua index fa8f3a415..ed7f3694e 100644 --- a/scripts/gui/FieldPlot.lua +++ b/scripts/gui/FieldPlot.lua @@ -48,7 +48,7 @@ function FieldPlot:draw(map) local lastWp = self.waypoints and #self.waypoints>0 and self.waypoints[#self.waypoints] if self.drawLastWp and lastWp then - local x, y = self:worldToScreen(map, lastWp.x, lastWp.z ) + local x, y = CpGuiUtil.worldToScreen(map, lastWp.x, lastWp.z ) local signSizeMeters = 1.5* self.lineThickness local zoom = map.fullScreenLayout:getIconZoom() local width, height = signSizeMeters * map.uiScale * zoom, signSizeMeters * map.uiScale * zoom * g_screenAspectRatio diff --git a/scripts/gui/UnloadingTriggerPlot.lua b/scripts/gui/UnloadingTriggerPlot.lua new file mode 100644 index 000000000..18e69421d --- /dev/null +++ b/scripts/gui/UnloadingTriggerPlot.lua @@ -0,0 +1,53 @@ +--- Draws an unloading triggers as a X Symbol on the in game menu map. +---@class UnloadingTriggerPlot +UnloadingTriggerPlot = CpObject() + +function UnloadingTriggerPlot:init(node) + self.courseOverlayId = createImageOverlay('dataS/scripts/shared/graph_pixel.dds') + self.isVisible = false + -- the normal FS22 blue -- 0.9900 0.4640 0.0010 1 + --self.color = {CpGuiUtil.getNormalizedRgb(42, 193, 237)} + self.color = {CpGuiUtil.getNormalizedRgb(255, 128, 0)} + -- a lighter shade of the same color + self.lightColor = {CpGuiUtil.getNormalizedRgb(213, 255, 0)} + -- a darker shade of the same color + self.darkColor = {CpGuiUtil.getNormalizedRgb(19, 87, 107)} + self.lineThickness = 4 / g_screenHeight -- 4 pixels + self.isHighlighted = false + local _ + self.x, _, self.z = getWorldTranslation(node) +end + +function UnloadingTriggerPlot:draw(map) + local r, g, b = unpack(self.color) + if self.isHighlighted then + r, g, b = unpack(self.lightColor) + end + setOverlayColor( self.courseOverlayId, r, g, b, 0.8 ) + -- local x, y = CpGuiUtil.worldToScreen(map, self.x, self.z, false) + + local s1x, s1y = CpGuiUtil.worldToScreen(map, self.x - 5, self.z - 5, false) + local e1x, e1y = CpGuiUtil.worldToScreen(map, self.x + 5, self.z + 5, false) + local s2x, s2y = CpGuiUtil.worldToScreen(map, self.x + 5, self.z - 5, false) + local e2x, e2y = CpGuiUtil.worldToScreen(map, self.x - 5, self.z + 5, false) + + --- Create a coss through the node position + local mapRotation = map.layout:getMapRotation() + local dx2D = e1x - s1x + local dy2D = ( e1y - s1y ) / g_screenAspectRatio + local width = MathUtil.vector2Length(dx2D, dy2D) + local rotation = MathUtil.getYRotationFromDirection(10, 10) - math.pi * 0.5 + mapRotation + setOverlayRotation( self.courseOverlayId, rotation, 0, 0 ) + renderOverlay( self.courseOverlayId, s1x, s1y, width, self.lineThickness ) + + dx2D = e2x - s2x + dy2D = ( e2y - s2y ) / g_screenAspectRatio + width = MathUtil.vector2Length(dx2D, dy2D) + rotation = MathUtil.getYRotationFromDirection(-10, 10) - math.pi * 0.5 + mapRotation + setOverlayRotation( self.courseOverlayId, rotation, 0, 0 ) + renderOverlay( self.courseOverlayId, s2x, s2y, width, self.lineThickness ) +end + +function UnloadingTriggerPlot:setHighlighted(highlighted) + self.isHighlighted = highlighted +end \ No newline at end of file diff --git a/scripts/gui/hud/CpBaseHud.lua b/scripts/gui/hud/CpBaseHud.lua index 9e3df27a4..5c1e2284b 100644 --- a/scripts/gui/hud/CpBaseHud.lua +++ b/scripts/gui/hud/CpBaseHud.lua @@ -424,11 +424,13 @@ function CpBaseHud:getActiveHudPage(vehicle) return self.combineUnloaderLayout elseif vehicle:getCanStartCpBaleFinder() and not vehicle:hasCpCourse() then return self.baleFinderLayout - elseif vehicle:getCanStartCpBunkerSiloWorker() and vehicle:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_BUNKER_SILO - or AIUtil.hasChildVehicleWithSpecialization(vehicle, Leveler) then - return self.bunkerSiloWorkerLayout - elseif vehicle:getCanStartCpSiloLoaderWorker() then + elseif vehicle:getCanStartCpSiloLoaderWorker() and (vehicle:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_SILO_LOADING + or AIUtil.hasChildVehicleWithSpecialization(vehicle, ConveyorBelt)) then return self.siloLoaderWorkerLayout + elseif vehicle:getCanStartCpBunkerSiloWorker() and (vehicle:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_BUNKER_SILO or + (AIUtil.hasChildVehicleWithSpecialization(vehicle, Leveler) + and not AIUtil.hasChildVehicleWithSpecialization(vehicle, Shovel))) then + return self.bunkerSiloWorkerLayout else return self.fieldworkLayout end diff --git a/scripts/gui/hud/CpBunkerSiloWorkerHudPage.lua b/scripts/gui/hud/CpBunkerSiloWorkerHudPage.lua index b61ee59c4..60e60011a 100644 --- a/scripts/gui/hud/CpBunkerSiloWorkerHudPage.lua +++ b/scripts/gui/hud/CpBunkerSiloWorkerHudPage.lua @@ -101,5 +101,13 @@ function CpBunkerSiloWorkerHudPageElement:updateContent(vehicle, status) end function CpBunkerSiloWorkerHudPageElement:isStartingPointBtnDisabled(vehicle) - return AIUtil.hasAIImplementWithSpecialization(vehicle, Leveler) + return AIUtil.hasChildVehicleWithSpecialization(vehicle, Leveler) + and not AIUtil.hasChildVehicleWithSpecialization(vehicle, Shovel) end + +function CpBunkerSiloWorkerHudPageElement:getStartingPointBtnText(vehicle) + if self:isStartingPointBtnDisabled(vehicle) then + return vehicle:getCpStartText() + end + return vehicle:getCpStartingPointSetting():getString() +end \ No newline at end of file diff --git a/scripts/gui/hud/CpSiloLoaderWorkerHudPage.lua b/scripts/gui/hud/CpSiloLoaderWorkerHudPage.lua index d6af9cdbd..4d5068109 100644 --- a/scripts/gui/hud/CpSiloLoaderWorkerHudPage.lua +++ b/scripts/gui/hud/CpSiloLoaderWorkerHudPage.lua @@ -35,14 +35,17 @@ function CpSiloLoaderWorkerHudPageElement:setupElements(baseHud, vehicle, lines, end) - --- Progress of fill level removed since the start of the driver. + --- Displays the fill level of current worked on heap. local x, y = unpack(lines[4].left) self.fillLevelProgressLabel = CpTextHudElement.new(self , x , y, CpBaseHud.defaultFontSize) self.fillLevelProgressLabel:setTextDetails(g_i18n:getText("CP_siloLoader_fillLevelProgress")) - --- Progress of fill level removed since the start of the driver. + --- Displays the fill level of current worked on heap. local x, y = unpack(lines[4].right) self.fillLevelProgressText = CpTextHudElement.new(self, x, y, CpBaseHud.defaultFontSize, RenderText.ALIGN_RIGHT) + --- Shovel loading height offset. + self.loadingShovelHeightOffsetBtn = baseHud:addLineTextButton(self, 2, CpBaseHud.defaultFontSize, + vehicle:getCpSettings().loadingShovelHeightOffset) CpGuiUtil.addCopyAndPasteButtons(self, baseHud, vehicle, lines, wMargin, hMargin, 1) @@ -85,6 +88,11 @@ function CpSiloLoaderWorkerHudPageElement:updateContent(vehicle, status) self.workWidthBtn:setTextDetails(workWidth:getTitle(), workWidth:getString()) self.workWidthBtn:setVisible(workWidth:getIsVisible()) + local loadingHeightOffset = vehicle:getCpSettings().loadingShovelHeightOffset + self.loadingShovelHeightOffsetBtn:setTextDetails(loadingHeightOffset:getTitle(), loadingHeightOffset:getString()) + self.loadingShovelHeightOffsetBtn:setVisible(loadingHeightOffset:getIsVisible()) + self.loadingShovelHeightOffsetBtn:setDisabled(loadingHeightOffset:getIsDisabled()) + self.fillLevelProgressText:setTextDetails(status:getSiloFillLevelPercentageLeftOver()) --- Update copy and paste buttons @@ -145,4 +153,15 @@ function CpSiloLoaderWorkerHudPageElement:arePositionEqual(parameters, otherPara return false end return true +end + +function CpSiloLoaderWorkerHudPageElement:isStartingPointBtnDisabled(vehicle) + return AIUtil.hasChildVehicleWithSpecialization(vehicle, ConveyorBelt) or vehicle:getIsCpActive() +end + +function CpSiloLoaderWorkerHudPageElement:getStartingPointBtnText(vehicle) + if self:isStartingPointBtnDisabled(vehicle) then + return vehicle:getCpStartText() + end + return vehicle:getCpStartingPointSetting():getString() end \ No newline at end of file diff --git a/scripts/pathfinder/PathfinderUtil.lua b/scripts/pathfinder/PathfinderUtil.lua index 29a5c062d..32b19e645 100644 --- a/scripts/pathfinder/PathfinderUtil.lua +++ b/scripts/pathfinder/PathfinderUtil.lua @@ -663,8 +663,21 @@ function PathfinderUtil.initializeTrailerHeading(start, vehicleData) end end ----@param start State3D +---@param vehicle table ---@param goal State3D +---@param allowReverse boolean +---@param fieldNum number|nil +---@param vehiclesToIgnore table|nil +---@param objectsToIgnore table|nil +---@param maxFruitPercent number|nil +---@param offFieldPenalty number|nil +---@param areaToAvoid PathfinderUtil.Area|nil +---@param mustBeAccurate boolean|nil +---@param areaToIgnoreFruit PathfinderUtil.Area|nil +---@return PathfinderInterface pathfinder +---@return boolean done finished pathfinding? +---@return table|nil path that was found? +---@return boolean|nil goalNodeInvalid function PathfinderUtil.startPathfindingFromVehicleToGoal(vehicle, goal, allowReverse, fieldNum, vehiclesToIgnore, objectsToIgnore, @@ -789,6 +802,10 @@ end --- in front of the vehicle when it stops working on that row before the turn starts. Negative values mean the --- vehicle is towing the implements and is past the end of the row when the implement reaches the end of the row. ---@param turnOnField boolean is turn on field allowed? +---@return PathfinderInterface pathfinder +---@return boolean done finished pathfinding? +---@return table|nil path that was found? +---@return boolean|nil goalNodeInvalid function PathfinderUtil.findPathForTurn(vehicle, startOffset, goalReferenceNode, goalOffset, turnRadius, allowReverse, courseWithHeadland, workingWidth, backMarkerDistance, turnOnField) local x, z, yRot = PathfinderUtil.getNodePositionAndDirection(vehicle:getAIDirectionNode(), 0, startOffset or 0) @@ -839,6 +856,8 @@ end ---@param zOffset number offset in meters relative to the goal node (forward positive, backward negative) --- Together with the goalReferenceNode defines the goal ---@param turnRadius number vehicle turning radius +---@return table|nil path +---@return number length function PathfinderUtil.findAnalyticPath(solver, vehicleDirectionNode, startOffset, goalReferenceNode, xOffset, zOffset, turnRadius) local x, z, yRot = PathfinderUtil.getNodePositionAndDirection(vehicleDirectionNode, 0, startOffset or 0) @@ -871,8 +890,8 @@ function PathfinderUtil.getNodePositionAndDirection(node, xOffset, zOffset) end ---@param vehicle table ----@param xOffset|nil ----@param zOffset|nil +---@param xOffset number|nil +---@param zOffset number|nil ---@return State3D position/heading of vehicle function PathfinderUtil.getVehiclePositionAsState3D(vehicle, xOffset, zOffset) local x, z, yRot = PathfinderUtil.getNodePositionAndDirection( AIUtil.getDirectionNode(vehicle), xOffset, zOffset) @@ -895,13 +914,17 @@ end ---@param xOffset number side offset of the goal from the goalWaypoint ---@param zOffset number length offset of the goal from the goalWaypoint ---@param allowReverse boolean allow reverse driving ----@param fieldNum number if > 0, the pathfinding is restricted to the given field and its vicinity. Otherwise the +---@param fieldNum number|nil if > 0, the pathfinding is restricted to the given field and its vicinity. Otherwise the --- pathfinding considers any collision-free path valid, also outside of the field. ----@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.NodeArea nodes in this area will be penalized so the path will most likely avoid it ----@param areaToIgnoreFruit PathfinderUtil.Area area to ignore fruit +---@param vehiclesToIgnore table[]|nil list of vehicles to ignore for the collision detection (optional) +---@param maxFruitPercent number|nil maximum percentage of fruit present before a node is marked as invalid (optional) +---@param offFieldPenalty number|nil penalty to apply to nodes off the field +---@param areaToAvoid PathfinderUtil.NodeArea|nil nodes in this area will be penalized so the path will most likely avoid it +---@param areaToIgnoreFruit PathfinderUtil.Area|nil area to ignore fruit +---@return PathfinderInterface pathfinder +---@return boolean done finished pathfinding? +---@return table|nil path that was found? +---@return boolean|nil goalNodeInvalid function PathfinderUtil.startPathfindingFromVehicleToWaypoint(vehicle, course, goalWaypointIx, xOffset, zOffset, allowReverse, fieldNum, vehiclesToIgnore, maxFruitPercent, @@ -927,6 +950,10 @@ end ---@param offFieldPenalty number|nil penalty to apply to nodes off the field ---@param areaToAvoid PathfinderUtil.NodeArea|nil nodes in this area will be penalized so the path will most likely avoid it ---@param mustBeAccurate boolean|nil must be accurately find the goal position/angle (optional) +---@return PathfinderInterface pathfinder +---@return boolean done finished pathfinding? +---@return table|nil path that was found? +---@return boolean|nil goalNodeInvalid function PathfinderUtil.startPathfindingFromVehicleToNode(vehicle, goalNode, xOffset, zOffset, allowReverse, fieldNum, vehiclesToIgnore, maxFruitPercent, @@ -948,6 +975,10 @@ end ---@param fieldNum number if other than 0 or nil the pathfinding is restricted to the given field and its vicinity ---@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) +---@return PathfinderInterface pathfinder +---@return boolean done finished pathfinding? +---@return table|nil path that was found? +---@return boolean|nil goalNodeInvalid function PathfinderUtil.startAStarPathfindingFromVehicleToNode(vehicle, goalNode, xOffset, zOffset, fieldNum, vehiclesToIgnore, maxFruitPercent) diff --git a/scripts/silo/BunkerSiloVehicleController.lua b/scripts/silo/BunkerSiloVehicleController.lua index caf5ac979..ff013ec0f 100644 --- a/scripts/silo/BunkerSiloVehicleController.lua +++ b/scripts/silo/BunkerSiloVehicleController.lua @@ -29,8 +29,6 @@ function CpBunkerSiloVehicleController:init(silo, vehicle, driveStrategy, direct ^ | ]] - - if dsz > dhz then self:debug("Silo needs to be inverted.") self.isInverted = true @@ -44,8 +42,6 @@ function CpBunkerSiloVehicleController:init(silo, vehicle, driveStrategy, direct | v | wx sx ]] - - self.isInverted = true elseif dsz < 0 and dhz > 0 then self:debug("Start distance: dsz: %.2f, dhz: %.2f", dsz, dhz) @@ -317,6 +313,24 @@ CpBunkerSiloLoaderController = CpObject(CpBunkerSiloVehicleController) function CpBunkerSiloLoaderController:init(silo, vehicle, driveStrategy) CpBunkerSiloVehicleController.init(self, silo, vehicle, driveStrategy, vehicle:getAIDirectionNode()) + + + local sx, sz = self.silo:getStartPosition() + local hx, hz = self.silo:getHeightPosition() + local dx, _, dz = getWorldTranslation(vehicle:getAIDirectionNode()) + self.isInverted = false + + if MathUtil.vector2Length(sx-dx, sz-dz) < MathUtil.vector2Length(hx-dx, hz-dz) then + if self.silo.siloMode == CpBunkerSilo.SIDE_MODES.ONE_SIDED_INVERTED then + self.isInverted = true + end + else + self.isInverted = true + if self.silo.siloMode == CpBunkerSilo.SIDE_MODES.ONE_SIDED then + self.isInverted = false + end + end + end --- Gets the next line with the most fill level. diff --git a/scripts/silo/BunkerSiloWrapper.lua b/scripts/silo/BunkerSiloWrapper.lua index 6c6062985..2ddfe1a4f 100644 --- a/scripts/silo/BunkerSiloWrapper.lua +++ b/scripts/silo/BunkerSiloWrapper.lua @@ -65,68 +65,98 @@ function CpSilo:init(sx, sz, wx, wz, hx, hz) end +---@return number sx +---@return number sz function CpSilo:getStartPosition() return self.sx, self.sz end +---@return number wx +---@return number wz function CpSilo:getWidthPosition() return self.wx, self.wz end +---@return number hx +---@return number hz function CpSilo:getHeightPosition() return self.hx, self.hz end +---@return number width function CpSilo:getWidth() return self.width end +---@return number length function CpSilo:getLength() return self.length end +---@return number dirX +---@return number dirZ function CpSilo:getLengthDirection() return self.dirXLength, self.dirZLength end +---@return number dirX +---@return number dirZ function CpSilo:getWidthDirection() return self.dirXWidth, self.dirZWidth end +---@return number cx +---@return number cz function CpSilo:getCenter() local cx, cz = self:getFrontCenter() return cx + self.dirXLength * self.length/2, cz + self.dirZLength * self.length/2 end +---@return number fcx +---@return number fcz function CpSilo:getFrontCenter() local width = self:getWidth() return self.sx + self.dirXWidth * width/2, self.sz + self.dirZWidth * width/2 end +---@return number bcx +---@return number bcz function CpSilo:getBackCenter() local length = self:getLength() local fcx, fcz = self:getFrontCenter() return fcx + self.dirXLength * length/2, fcz + self.dirZLength * length/2 end ---- Is the point directly in the silo area. +--- Is the point directly in the silo area? +---@param x number +---@param z number +---@return boolean function CpSilo:isPointInSilo(x, z) return self:isPointInArea(x, z, self.area) end +---@param node number +---@return boolean function CpSilo:isNodeInSilo(node) local x, _, z = getWorldTranslation(node) return self:isPointInArea(x, z, self.area) end +---@param vehicle table +---@return boolean function CpSilo:isVehicleInSilo(vehicle) return self:isNodeInSilo(vehicle.rootNode) end +---@param x number +---@param z number +---@param area table +---@return boolean function CpSilo:isPointInArea(x, z, area) return CpMathUtil.isPointInPolygon(area, x, z) end +---@return table area function CpSilo:getArea() return self.area end @@ -175,6 +205,10 @@ function CpSilo:getTotalFillLevel() return 0 end +function CpSilo:isTheSameSilo() + --- override +end + --- Heap Bunker Silo --- Wrapper for a heap. ---@class CpHeapBunkerSilo :CpSilo @@ -388,6 +422,14 @@ function CpBunkerSilo:getNode() return self.silo.interactionTriggerNode end +function CpBunkerSilo:getFillType() + return self.silo.outputFillType +end + +function CpBunkerSilo:getTotalFillLevel() + return self.silo.fillLevel +end + function CpBunkerSilo:delete() for _, controller in pairs(self.controllers) do controller:setBunkerSiloInvalid() @@ -666,11 +708,3 @@ function CpBunkerSilo:getDebugData() end return data end - -function CpBunkerSilo:getFillType() - return self.silo.outputFillType -end - -function CpBunkerSilo:getTotalFillLevel() - return self.silo.fillLevel -end \ No newline at end of file diff --git a/scripts/specializations/CourseplaySpec.lua b/scripts/specializations/CourseplaySpec.lua deleted file mode 100644 index 8894d1472..000000000 --- a/scripts/specializations/CourseplaySpec.lua +++ /dev/null @@ -1,109 +0,0 @@ ---- Cp ai driver spec - ----@class CourseplaySpec -CourseplaySpec = {} - -CourseplaySpec.MOD_NAME = g_currentModName - -CourseplaySpec.KEY = "."..CourseplaySpec.MOD_NAME..".courseplaySpec." - -function CourseplaySpec.initSpecialization() - local schema = Vehicle.xmlSchemaSavegame -end - -function CourseplaySpec.prerequisitesPresent(specializations) - return SpecializationUtil.hasSpecialization(AIFieldWorker, specializations) -end - -function CourseplaySpec.registerEventListeners(vehicleType) - SpecializationUtil.registerEventListener(vehicleType, "onRegisterActionEvents", CourseplaySpec) - SpecializationUtil.registerEventListener(vehicleType, "onLoad", CourseplaySpec) - SpecializationUtil.registerEventListener(vehicleType, "onPostLoad", CourseplaySpec) - SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", CourseplaySpec) - SpecializationUtil.registerEventListener(vehicleType, "onEnterVehicle", CourseplaySpec) - SpecializationUtil.registerEventListener(vehicleType, "onLeaveVehicle", CourseplaySpec) - SpecializationUtil.registerEventListener(vehicleType, "onDraw", CourseplaySpec) -end - -function CourseplaySpec.registerEvents(vehicleType) - SpecializationUtil.registerEvent(vehicleType, "onCpUnitChanged") - SpecializationUtil.registerEvent(vehicleType, "onCpDrawHudMap") -end - -function CourseplaySpec.registerOverwrittenFunctions(vehicleType) - -end - - -function CourseplaySpec:onRegisterActionEvents(isActiveForInput, isActiveForInputIgnoreSelection) - -end - - - ------------------------------------------------------------------------------------------------------------------------- ---- Event listeners ---------------------------------------------------------------------------------------------------------------------------- -function CourseplaySpec:onLoad(savegame) - --- Register the spec: spec_courseplaySpec - local specName = CourseplaySpec.MOD_NAME .. ".courseplaySpec" - self.spec_courseplaySpec = self["spec_" .. specName] - g_messageCenter:subscribe(MessageType.SETTING_CHANGED[GameSettings.SETTING.USE_MILES], CourseplaySpec.onUnitChanged, self) - g_messageCenter:subscribe(MessageType.SETTING_CHANGED[GameSettings.SETTING.USE_ACRE], CourseplaySpec.onUnitChanged, self) - g_messageCenter:subscribe(MessageType.CP_DISTANCE_UNIT_CHANGED, CourseplaySpec.onUnitChanged, self) -end - -function CourseplaySpec:onUnitChanged() - SpecializationUtil.raiseEvent(self,"onCpUnitChanged") -end - -function CourseplaySpec:onPostLoad(savegame) - -end - -function CourseplaySpec:saveToXMLFile(xmlFile, baseKey, usedModNames) - -end - -function CourseplaySpec:onEnterVehicle(isControlling) - -end - -function CourseplaySpec:onLeaveVehicle(wasEntered) - -end - -function CourseplaySpec:isCollisionDetectionEnabled() - return self.collisionDetectionEnabled -end - -function CourseplaySpec:enableCollisionDetection() - self.collisionDetectionEnabled = true -end - -function CourseplaySpec:disableCollisionDetection() - self.collisionDetectionEnabled = false -end - ---- This is to be able to disable the built-in AIDriveStrategyCollision check from our drive strategies -function CourseplaySpec:getCollisionCheckActive(superFunc,...) - if self.collisionDetectionEnabled then - return superFunc(self,...) - else - return false - end -end - ---- Enriches the status data for the hud here. -function CourseplaySpec:onUpdateTick() - -end - -function CourseplaySpec:onDraw() - -end - - -AIDriveStrategyCollision.getCollisionCheckActive = Utils.overwrittenFunction( - AIDriveStrategyCollision.getCollisionCheckActive, CourseplaySpec.getCollisionCheckActive -) diff --git a/scripts/specializations/CpAIBaleFinder.lua b/scripts/specializations/CpAIBaleFinder.lua index 535e1197e..51adf5f96 100644 --- a/scripts/specializations/CpAIBaleFinder.lua +++ b/scripts/specializations/CpAIBaleFinder.lua @@ -19,7 +19,7 @@ function CpAIBaleFinder.initSpecialization() end function CpAIBaleFinder.prerequisitesPresent(specializations) - return SpecializationUtil.hasSpecialization(CpAIFieldWorker, specializations) + return SpecializationUtil.hasSpecialization(CpAIWorker, specializations) end function CpAIBaleFinder.register(typeManager,typeName,specializations) @@ -135,9 +135,9 @@ function CpAIBaleFinder:getCanStartCp(superFunc) end --- Only use the bale finder, if the cp field work job is not possible. -function CpAIBaleFinder:getCpStartableJob(superFunc) +function CpAIBaleFinder:getCpStartableJob(superFunc, ...) local spec = self.spec_cpAIBaleFinder - return superFunc(self) or self:getCanStartCpBaleFinder() and spec.cpJob + return superFunc(self, ...) or self:getCanStartCpBaleFinder() and spec.cpJob end function CpAIBaleFinder:getCpStartText(superFunc) diff --git a/scripts/specializations/CpAIBunkerSiloWorker.lua b/scripts/specializations/CpAIBunkerSiloWorker.lua index 6b73042db..1132a81f2 100644 --- a/scripts/specializations/CpAIBunkerSiloWorker.lua +++ b/scripts/specializations/CpAIBunkerSiloWorker.lua @@ -18,7 +18,7 @@ function CpAIBunkerSiloWorker.initSpecialization() end function CpAIBunkerSiloWorker.prerequisitesPresent(specializations) - return SpecializationUtil.hasSpecialization(CpAIFieldWorker, specializations) + return SpecializationUtil.hasSpecialization(CpAIWorker, specializations) end function CpAIBunkerSiloWorker.register(typeManager,typeName,specializations) @@ -95,11 +95,13 @@ end --- Is the bunker silo allowed? function CpAIBunkerSiloWorker:getCanStartCpBunkerSiloWorker() + if AIUtil.hasChildVehicleWithSpecialization(self, Shovel) then + return false + end return not self:getCanStartCpFieldWork() and not self:getCanStartCpBaleFinder() and not self:getCanStartCpCombineUnloader() and not self:getCanStartCpSiloLoaderWorker() - and (not self:hasCpCourse() or AIUtil.hasChildVehicleWithSpecialization(self, Leveler, nil)) end function CpAIBunkerSiloWorker:getCanStartCp(superFunc) @@ -112,7 +114,7 @@ function CpAIBunkerSiloWorker:getCpStartableJob(superFunc, isStartedByHud) if isStartedByHud and not AIUtil.hasChildVehicleWithSpecialization(self, Leveler) then job = self:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_BUNKER_SILO and job end - return superFunc(self) or job + return superFunc(self, isStartedByHud) or job end function CpAIBunkerSiloWorker:getCpStartText(superFunc) diff --git a/scripts/specializations/CpAICombineUnloader.lua b/scripts/specializations/CpAICombineUnloader.lua index ced1a7148..1a1d20bfe 100644 --- a/scripts/specializations/CpAICombineUnloader.lua +++ b/scripts/specializations/CpAICombineUnloader.lua @@ -215,9 +215,9 @@ function CpAICombineUnloader:getCanStartCp(superFunc) return superFunc(self) or self:getCanStartCpCombineUnloader() and not self:getIsCpCourseRecorderActive() end -function CpAICombineUnloader:getCpStartableJob(superFunc) +function CpAICombineUnloader:getCpStartableJob(superFunc, ...) local spec = self.spec_cpAICombineUnloader - return superFunc(self) or self:getCanStartCpCombineUnloader() and spec.cpJob + return superFunc(self, ...) or self:getCanStartCpCombineUnloader() and spec.cpJob end function CpAICombineUnloader:getCpStartText(superFunc) diff --git a/scripts/specializations/CpAIFieldWorker.lua b/scripts/specializations/CpAIFieldWorker.lua index 4fd0569f7..1c6a9bb2c 100644 --- a/scripts/specializations/CpAIFieldWorker.lua +++ b/scripts/specializations/CpAIFieldWorker.lua @@ -39,8 +39,7 @@ function CpAIFieldWorker.registerEventListeners(vehicleType) SpecializationUtil.registerEventListener(vehicleType, "onCpFull", CpAIFieldWorker) SpecializationUtil.registerEventListener(vehicleType, "onCpFinished", CpAIFieldWorker) - SpecializationUtil.registerEventListener(vehicleType, "onPostDetachImplement", CpAIFieldWorker) - SpecializationUtil.registerEventListener(vehicleType, "onPostAttachImplement", CpAIFieldWorker) + SpecializationUtil.registerEventListener(vehicleType, "onStateChange", CpAIFieldWorker) SpecializationUtil.registerEventListener(vehicleType, 'onCpCourseChange', CpAIFieldWorker) SpecializationUtil.registerEventListener(vehicleType, 'onCpADStartedByPlayer', CpAIFieldWorker) @@ -107,22 +106,20 @@ function CpAIFieldWorker:saveToXMLFile(xmlFile, baseKey, usedModNames) spec.cpJobStartAtLastWp:getCpJobParameters():saveToXMLFile(xmlFile, baseKey.. ".cpJobStartAtLastWp") end -function CpAIFieldWorker:onCpCourseChange() - local spec = self.spec_cpAIFieldWorker - spec.cpJob:getCpJobParameters():validateSettings() -end - -function CpAIFieldWorker:onPostDetachImplement() +function CpAIFieldWorker:onStateChange(state, data) local spec = self.spec_cpAIFieldWorker - spec.cpJob:getCpJobParameters():validateSettings() + if state == Vehicle.STATE_CHANGE_ATTACH then + spec.cpJob:getCpJobParameters():validateSettings() + elseif state == Vehicle.STATE_CHANGE_DETACH then + spec.cpJob:getCpJobParameters():validateSettings() + end end -function CpAIFieldWorker:onPostAttachImplement() +function CpAIFieldWorker:onCpCourseChange() local spec = self.spec_cpAIFieldWorker spec.cpJob:getCpJobParameters():validateSettings() end - function CpAIFieldWorker:getCpStartingPointSetting() local spec = self.spec_cpAIFieldWorker return spec.cpJob:getCpJobParameters().startAt @@ -299,9 +296,9 @@ function CpAIFieldWorker:getCanStartCp(superFunc) end --- Gets the field work job for the hud or start action event. -function CpAIFieldWorker:getCpStartableJob(superFunc) +function CpAIFieldWorker:getCpStartableJob(superFunc, ...) local spec = self.spec_cpAIFieldWorker - return self:getCanStartCpFieldWork() and self:hasCpCourse() and spec.cpJob or superFunc(self) + return self:getCanStartCpFieldWork() and self:hasCpCourse() and spec.cpJob or superFunc(self, ...) end function CpAIFieldWorker:getCpStartText(superFunc) diff --git a/scripts/specializations/CpAISiloLoaderWorker.lua b/scripts/specializations/CpAISiloLoaderWorker.lua index 972bf1174..4ec9f4486 100644 --- a/scripts/specializations/CpAISiloLoaderWorker.lua +++ b/scripts/specializations/CpAISiloLoaderWorker.lua @@ -1,6 +1,8 @@ local modName = CpAISiloLoaderWorker and CpAISiloLoaderWorker.MOD_NAME -- for reload +--- Specialization for the silo loader job +--- Used for shovel loader and big silo loader, like the Ropa NarwaRo. ---@class CpAISiloLoaderWorker CpAISiloLoaderWorker = {} @@ -92,11 +94,11 @@ function CpAISiloLoaderWorker:onUpdate(dt) local spec = self.spec_cpAISiloLoaderWorker end ---- Is the bunker silo allowed? function CpAISiloLoaderWorker:getCanStartCpSiloLoaderWorker() - return not self:getCanStartCpFieldWork() and not self:getCanStartCpBaleFinder() and not self:hasCpCourse() - and not self:getCanStartCpCombineUnloader() and AIUtil.hasChildVehicleWithSpecialization(self, Shovel) - and AIUtil.hasChildVehicleWithSpecialization(self, ConveyorBelt) + return not self:getCanStartCpFieldWork() + and not self:getCanStartCpBaleFinder() + and not self:getCanStartCpCombineUnloader() + and AIUtil.hasChildVehicleWithSpecialization(self, Shovel) end function CpAISiloLoaderWorker:getCanStartCp(superFunc) @@ -105,7 +107,11 @@ end function CpAISiloLoaderWorker:getCpStartableJob(superFunc, isStartedByHud) local spec = self.spec_cpAISiloLoaderWorker - return superFunc(self) or self:getCanStartCpSiloLoaderWorker() and spec.cpJob + local job = self:getCanStartCpSiloLoaderWorker() and spec.cpJob + if isStartedByHud and not AIUtil.hasChildVehicleWithSpecialization(self, ConveyorBelt) then + job = self:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_SILO_LOADING and job + end + return superFunc(self, isStartedByHud) or job end function CpAISiloLoaderWorker:getCpStartText(superFunc) @@ -166,17 +172,23 @@ function CpAISiloLoaderWorker:startCpAtLastWp(superFunc, ...) end end -function CpAISiloLoaderWorker:startCpSiloLoaderWorker(jobParameters, bunkerSilo, heap) +function CpAISiloLoaderWorker:startCpSiloLoaderWorker(jobParameters, bunkerSilo, heap, unloadTrigger, unloadStation) if self.isServer then - local strategy = AIDriveStrategySiloLoader.new() - -- this also starts the strategy + local strategy + if SpecializationUtil.hasSpecialization(ConveyorBelt, self.specializations) then + CpUtil.debugVehicle(CpDebug.DBG_SILO, self, "Starting a silo loader strategy.") + strategy = AIDriveStrategySiloLoader.new() + else + CpUtil.debugVehicle(CpDebug.DBG_SILO, self, "Starting a shovel silo loader strategy.") + strategy = AIDriveStrategyShovelSiloLoader.new() + strategy:setUnloadTriggerAndStation(unloadTrigger, unloadStation) + end strategy:setSiloAndHeap(bunkerSilo, heap) strategy:setAIVehicle(self, jobParameters) - CpUtil.debugVehicle(CpDebug.DBG_SILO, self, "Starting silo worker job.") self:startCpWithStrategy(strategy) end end function CpAISiloLoaderWorker:stopCpSiloLoaderWorker() self:stopCpDriver() -end \ No newline at end of file +end diff --git a/scripts/specializations/CpAIWorker.lua b/scripts/specializations/CpAIWorker.lua index 2034afe03..5e39040fc 100644 --- a/scripts/specializations/CpAIWorker.lua +++ b/scripts/specializations/CpAIWorker.lua @@ -28,6 +28,9 @@ function CpAIWorker.register(typeManager, typeName, specializations) end function CpAIWorker.registerEvents(vehicleType) + SpecializationUtil.registerEvent(vehicleType, "onCpUnitChanged") + SpecializationUtil.registerEvent(vehicleType, "onCpDrawHudMap") + SpecializationUtil.registerEvent(vehicleType, "onCpFinished") SpecializationUtil.registerEvent(vehicleType, "onCpEmpty") SpecializationUtil.registerEvent(vehicleType, "onCpFull") @@ -42,11 +45,10 @@ function CpAIWorker.registerEventListeners(vehicleType) SpecializationUtil.registerEventListener(vehicleType, "onRegisterActionEvents", CpAIWorker) SpecializationUtil.registerEventListener(vehicleType, "onLoad", CpAIWorker) SpecializationUtil.registerEventListener(vehicleType, "onLoadFinished", CpAIWorker) - SpecializationUtil.registerEventListener(vehicleType, "onPreDetachImplement", CpAIWorker) - SpecializationUtil.registerEventListener(vehicleType, "onPostAttachImplement", CpAIWorker) SpecializationUtil.registerEventListener(vehicleType, "onUpdate", CpAIWorker) SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", CpAIWorker) SpecializationUtil.registerEventListener(vehicleType, "onLeaveVehicle", CpAIWorker) + SpecializationUtil.registerEventListener(vehicleType, "onPreDelete", CpAIWorker) --- Autodrive events SpecializationUtil.registerEventListener(vehicleType, "onStopAutoDrive", CpAIWorker) SpecializationUtil.registerEventListener(vehicleType, "onStartAutoDrive", CpAIWorker) @@ -76,10 +78,14 @@ function CpAIWorker.registerOverwrittenFunctions(vehicleType) SpecializationUtil.registerOverwrittenFunction(vehicleType, 'stopCurrentAIJob', CpAIWorker.stopCurrentAIJob) SpecializationUtil.registerOverwrittenFunction(vehicleType, 'getCanMotorRun', CpAIWorker.getCanMotorRun) SpecializationUtil.registerOverwrittenFunction(vehicleType, 'stopFieldWorker', CpAIWorker.stopFieldWorker) + SpecializationUtil.registerOverwrittenFunction(vehicleType, "getAIReverserNode", CpAIWorker.getAIReverserNode) + SpecializationUtil.registerOverwrittenFunction(vehicleType, "getAIDirectionNode", CpAIWorker.getAIDirectionNode) end ------------------------------------------------------------------------------------------------------------------------- + +--------------------------------------------------- --- Event listeners ---------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------- + function CpAIWorker:onLoad(savegame) --- Register the spec: spec_CpAIWorker self.spec_cpAIWorker = self["spec_" .. CpAIWorker.SPEC_NAME] @@ -87,22 +93,19 @@ function CpAIWorker:onLoad(savegame) --- Flag to make sure the motor isn't being turned on again by giants code, when we want it turned off. spec.motorDisabled = false spec.driveStrategy = nil + g_messageCenter:subscribe(MessageType.SETTING_CHANGED[GameSettings.SETTING.USE_MILES], CpAIWorker.onUnitChanged, self) + g_messageCenter:subscribe(MessageType.SETTING_CHANGED[GameSettings.SETTING.USE_ACRE], CpAIWorker.onUnitChanged, self) + g_messageCenter:subscribe(MessageType.CP_DISTANCE_UNIT_CHANGED, CpAIWorker.onUnitChanged, self) end -function CpAIWorker:onLoadFinished() - +function CpAIWorker:onUnitChanged() + SpecializationUtil.raiseEvent(self,"onCpUnitChanged") end -function CpAIWorker:onPreDetachImplement(implement) - local spec = self.spec_cpAIWorker -end - -function CpAIWorker:onPostAttachImplement(object) - local spec = self.spec_cpAIWorker - +function CpAIWorker:onLoadFinished() + end - function CpAIWorker:onLeaveVehicle(wasEntered) if wasEntered then CpJobSyncOnLeaveEvent.sendEvent(self) @@ -148,6 +151,18 @@ function CpAIWorker:onRegisterActionEvents(isActiveForInput, isActiveForInputIgn end end +function CpAIWorker:onUpdateTick(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected) + CpAIWorker.updateActionEvents(self) +end + +function CpAIWorker:onPreDelete() + +end + +----------------------------------------------- +--- Action input events +----------------------------------------------- + --- Updates the action event visibility and text. function CpAIWorker:updateActionEvents() local spec = self.spec_cpAIWorker @@ -185,72 +200,6 @@ function CpAIWorker:updateActionEvents() end end -function CpAIWorker:onUpdateTick(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected) - CpAIWorker.updateActionEvents(self) -end - - ---- Used to enable/disable release of the helper ---- and handles post release functionality with for example auto drive. ---- TODO: This function is a mess and desperately needs a better solution! -function CpAIWorker:stopCurrentAIJob(superFunc, message, ...) - if message then - CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, "stop message: %s", message:getMessage()) - else - CpUtil.infoVehicle(self, "no stop message was given.") - return superFunc(self, message, ...) - end - local releaseMessage, hasFinished, event, isOnlyShownOnPlayerStart = g_infoTextManager:getInfoTextDataByAIMessage(message) - - CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, "finished: %s, event: %s", - tostring(hasFinished), tostring(event)) - - local wasCpActive = self:getIsCpActive() - if wasCpActive then - local driveStrategy = self:getCpDriveStrategy() - if driveStrategy then - -- TODO: this isn't needed if we do not return a 0 < maxSpeed < 0.5, should either be exactly 0 or greater than 0.5 - local maxSpeed = driveStrategy and driveStrategy:getMaxSpeed() - if message:isa(AIMessageErrorBlockedByObject) then - if self.spec_aiFieldWorker.didNotMoveTimer and self.spec_aiFieldWorker.didNotMoveTimer < 0 then - if maxSpeed and maxSpeed < 1 then - -- disable the Giants timeout which dismisses the AI worker if it does not move for 5 seconds - -- since we often stop for instance in convoy mode when waiting for another vehicle to turn - -- (when we do this, we set our maxSpeed to 0). So we also check our maxSpeed, this way the Giants timer will - -- fire if we are blocked (thus have a maxSpeed > 0 but not moving) - CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, 'Overriding the Giants did not move timer, with speed: %.2f', maxSpeed) - return - else - CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, 'Giants did not move timer triggered, with speed: %.2f!', maxSpeed) - end - end - end - driveStrategy:onFinished() - end - end - self:resetCpAllActiveInfoTexts() - --- Only add the info text, if it's available and nobody is in the vehicle. - if not self:getIsControlled() and releaseMessage and not isOnlyShownOnPlayerStart then - self:setCpInfoTextActive(releaseMessage) - end - superFunc(self, message,...) - if wasCpActive then - if event then - SpecializationUtil.raiseEvent(self, event) - end - if hasFinished and self:getCpSettings().foldImplementAtEnd:getValue() then - --- Folds implements at the end if the setting is active. - self:prepareForAIDriving() - end - - end -end - - ------------------------------------------------ ---- Action input events ------------------------------------------------ - function CpAIWorker:changeStartingPoint() local startingPointSetting = self:getCpStartingPointSetting() startingPointSetting:setNextItem() @@ -265,7 +214,7 @@ function CpAIWorker:changeCourseVisibility() end function CpAIWorker:startStopCpActionEvent() - self:cpStartStopDriver() + self:cpStartStopDriver(true) end --- Directly starts a cp job or stops a currently active job. @@ -301,6 +250,10 @@ function CpAIWorker:cpStartStopDriver(isStartedByHud) end end +----------------------------------------------- +--- Status getter functions +----------------------------------------------- + --- Is a cp worker active ? --- Every cp job should be an instance of type CpAIJob. function CpAIWorker:getIsCpActive() @@ -309,23 +262,24 @@ end --- Is cp drive to field work active function CpAIWorker:getIsCpDriveToFieldWorkActive() - return self:getIsCpActive() and self.driveToFieldWorkStartStrategy ~= nil + local spec = self.spec_cpAIWorker + return self:getIsCpActive() and spec.driveToTask ~=nil end --- Is a cp job ready to be started? function CpAIWorker:getCanStartCp() - return false + --- override end --- Gets the job to be started by the hud or the keybinding. function CpAIWorker:getCpStartableJob() - + --- override end --- Gets the additional action event start text, --- for example the starting point. function CpAIWorker:getCpStartText() - return "" + --- override end --- Makes sure giants isn't turning the motor back on, when we have turned it off. @@ -336,6 +290,66 @@ function CpAIWorker:getCanMotorRun(superFunc, ...) return superFunc(self, ...) end +----------------------------------------------- +--- Strategy handling +----------------------------------------------- + + +--- Used to enable/disable release of the helper +--- and handles post release functionality with for example auto drive. +--- TODO: this might by only called on the client, so +--- server depended code has to be moved to stopJob or similar code. +function CpAIWorker:stopCurrentAIJob(superFunc, message, ...) + if message then + CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, "stop message: %s", message:getMessage()) + else + CpUtil.infoVehicle(self, "no stop message was given.") + return superFunc(self, message, ...) + end + local releaseMessage, hasFinished, event, isOnlyShownOnPlayerStart = g_infoTextManager:getInfoTextDataByAIMessage(message) + + CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, "finished: %s, event: %s", + tostring(hasFinished), tostring(event)) + local wasCpActive = self:getIsCpActive() + if wasCpActive then + local driveStrategy = self:getCpDriveStrategy() + if driveStrategy then + -- TODO: this isn't needed if we do not return a 0 < maxSpeed < 0.5, should either be exactly 0 or greater than 0.5 + local maxSpeed = driveStrategy and driveStrategy:getMaxSpeed() + if message:isa(AIMessageErrorBlockedByObject) then + if self.spec_aiFieldWorker.didNotMoveTimer and self.spec_aiFieldWorker.didNotMoveTimer < 0 then + if maxSpeed and maxSpeed < 1 then + -- disable the Giants timeout which dismisses the AI worker if it does not move for 5 seconds + -- since we often stop for instance in convoy mode when waiting for another vehicle to turn + -- (when we do this, we set our maxSpeed to 0). So we also check our maxSpeed, this way the Giants timer will + -- fire if we are blocked (thus have a maxSpeed > 0 but not moving) + CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, 'Overriding the Giants did not move timer, with speed: %.2f', maxSpeed) + return + else + CpUtil.debugVehicle(CpDebug.DBG_FIELDWORK, self, 'Giants did not move timer triggered, with speed: %.2f!', maxSpeed) + end + end + end + driveStrategy:onFinished() + end + end + self:resetCpAllActiveInfoTexts() + --- Only add the info text, if it's available and nobody is in the vehicle. + if not self:getIsControlled() and releaseMessage and not isOnlyShownOnPlayerStart then + self:setCpInfoTextActive(releaseMessage) + end + superFunc(self, message,...) + if wasCpActive then + if event then + SpecializationUtil.raiseEvent(self, event) + end + if hasFinished and self:getCpSettings().foldImplementAtEnd:getValue() then + --- Folds implements at the end if the setting is active. + self:prepareForAIDriving() + end + end +end + function CpAIWorker:startCpDriveTo(task, jobParameters) local spec = self.spec_cpAIWorker spec.driveToTask = task @@ -454,6 +468,7 @@ function CpAIWorker:cpHold(ms) end end +---@param strategy AIDriveStrategyCourse function CpAIWorker:startCpWithStrategy(strategy) local spec = self.spec_cpAIWorker spec.driveStrategy = strategy @@ -464,12 +479,10 @@ function CpAIWorker:stopCpDriver() --- Reset the flag. local spec = self.spec_cpAIWorker spec.motorDisabled = false - if spec.driveStrategy then spec.driveStrategy:delete() spec.driveStrategy = nil end - if self.isServer then WheelsUtil.updateWheelsPhysics(self, 0, 0, 0, true, true) end @@ -484,9 +497,7 @@ function CpAIWorker:stopCpDriver() if actionController ~= nil then actionController:resetCurrentState() end - self:raiseAIEvent("onAIFieldWorkerEnd", "onAIImplementEnd") - end function CpAIWorker:getCpDriveStrategy() @@ -494,6 +505,69 @@ function CpAIWorker:getCpDriveStrategy() return spec.driveStrategy end +--- Fixes the ai reverse node rotation for articulated axis vehicles, +--- if the node is pointing backwards and not forwards. +--- TODO: Consider consolidation with AIUtil.getArticulatedAxisVehicleReverserNode +function CpAIWorker:getAIReverserNode(superFunc) + local spec = self.spec_cpAIWorker + if not self:getIsCpActive() then return superFunc(self) end + if self.spec_articulatedAxis and self.spec_articulatedAxis.aiRevereserNode then + if g_vehicleConfigurations:get(self, "articulatedAxisReverseNodeInverted") then + if not spec.articulatedAxisReverseNode then + spec.articulatedAxisReverseNode = CpUtil.createNode( + "cpAiRevereserNode", 0, 0, 0, + getParent(self.spec_articulatedAxis.aiRevereserNode)) + end + return spec.articulatedAxisReverseNode + end + end + return superFunc(self) +end + +--- Fixes the Direction for the platinum wheel loader, as +--- their direction is not updated base on the rotation. +--- So we use the parent node of the arm tool node. +---@param superFunc any +function CpAIWorker:getAIDirectionNode(superFunc) + if not self:getIsCpActive() then return superFunc(self) end + local movingToolIx = g_vehicleConfigurations:get(self, "fixWheelLoaderDirectionNodeByMovingToolIx") + if movingToolIx ~= nil then + return getParent(self.spec_cylindered.movingTools[movingToolIx].node) + end + return superFunc(self) +end + + + +--- TODO: Do we really need the AIDriveStrategyCollision from giants, as this one is only active for fieldwork? +--- Maybe there is already a unique cp logic implemented, that catches the use cases. +function CpAIWorker:isCollisionDetectionEnabled() + local spec = self.spec_cpAIWorker + return spec.collisionDetectionEnabled +end + +function CpAIWorker:enableCollisionDetection() + local spec = self.spec_cpAIWorker + spec.collisionDetectionEnabled = true +end + +function CpAIWorker:disableCollisionDetection() + local spec = self.spec_cpAIWorker + spec.collisionDetectionEnabled = false +end + +function CpAIWorker:getCollisionCheckActive(superFunc,...) + local spec = self.vehicle.spec_cpAIWorker + if spec.collisionDetectionEnabled then + return superFunc(self,...) + else + return false + end +end +AIDriveStrategyCollision.getCollisionCheckActive = Utils.overwrittenFunction( + AIDriveStrategyCollision.getCollisionCheckActive, CpAIWorker.getCollisionCheckActive +) + function CpAIWorker:stopFieldWorker(superFunc, ...) --- Reset the flag. self.spec_cpAIWorker.motorDisabled = false diff --git a/scripts/specializations/CpCourseGeneratorSettings.lua b/scripts/specializations/CpCourseGeneratorSettings.lua index b443bfc22..7b23a4b9c 100644 --- a/scripts/specializations/CpCourseGeneratorSettings.lua +++ b/scripts/specializations/CpCourseGeneratorSettings.lua @@ -6,9 +6,12 @@ CpCourseGeneratorSettings = {} CpCourseGeneratorSettings.MOD_NAME = g_currentModName -CpCourseGeneratorSettings.KEY = "."..CpCourseGeneratorSettings.MOD_NAME..".cpCourseGeneratorSettings" CpCourseGeneratorSettings.SETTINGS_KEY = ".settings" CpCourseGeneratorSettings.VINE_SETTINGS_KEY = ".vineSettings" +CpCourseGeneratorSettings.NAME = ".cpCourseGeneratorSettings" +CpCourseGeneratorSettings.SPEC_NAME = CpCourseGeneratorSettings.MOD_NAME .. CpCourseGeneratorSettings.NAME +CpCourseGeneratorSettings.KEY = "." .. CpCourseGeneratorSettings.MOD_NAME .. CpCourseGeneratorSettings.NAME + function CpCourseGeneratorSettings.initSpecialization() local schema = Vehicle.xmlSchemaSavegame --- Old save format @@ -27,9 +30,14 @@ function CpCourseGeneratorSettings.initSpecialization() CpCourseGeneratorSettings.registerConsoleCommands() end +function CpCourseGeneratorSettings.register(typeManager,typeName,specializations) + if CpCourseGeneratorSettings.prerequisitesPresent(specializations) then + typeManager:addSpecialization(typeName, CpCourseGeneratorSettings.SPEC_NAME) + end +end function CpCourseGeneratorSettings.prerequisitesPresent(specializations) - return SpecializationUtil.hasSpecialization(AIFieldWorker, specializations) + return SpecializationUtil.hasSpecialization(CpAIWorker, specializations) end function CpCourseGeneratorSettings.registerEvents(vehicleType) @@ -79,8 +87,7 @@ end function CpCourseGeneratorSettings:onLoad(savegame) --- Register the spec: spec_cpCourseGeneratorSettings - local specName = CpCourseGeneratorSettings.MOD_NAME .. ".cpCourseGeneratorSettings" - self.spec_cpCourseGeneratorSettings = self["spec_" .. specName] + self.spec_cpCourseGeneratorSettings = self["spec_" .. CpCourseGeneratorSettings.SPEC_NAME] local spec = self.spec_cpCourseGeneratorSettings spec.gui = g_currentMission.inGameMenu.pageAI --- Clones the generic settings to create different settings containers for each vehicle. diff --git a/scripts/specializations/CpCourseManager.lua b/scripts/specializations/CpCourseManager.lua index 25ede6744..1a1ef23bf 100644 --- a/scripts/specializations/CpCourseManager.lua +++ b/scripts/specializations/CpCourseManager.lua @@ -5,8 +5,9 @@ CpCourseManager = {} CpCourseManager.MOD_NAME = g_currentModName - -CpCourseManager.KEY = "."..CpCourseManager.MOD_NAME..".cpCourseManager" +CpCourseManager.NAME = ".cpCourseManager" +CpCourseManager.SPEC_NAME = CpCourseManager.MOD_NAME .. CpCourseManager.NAME +CpCourseManager.KEY = "." .. CpCourseManager.MOD_NAME .. CpCourseManager.NAME CpCourseManager.xmlKey = "Course" CpCourseManager.rootKey = "AssignedCourses" CpCourseManager.rootKeyFileManager = "Courses" @@ -43,8 +44,14 @@ function CpCourseManager.initSpecialization() schema:register(XMLValueType.INT, key .. "#assignedCoursesID", "Assigned Courses id.") end +function CpCourseManager.register(typeManager,typeName,specializations) + if CpCourseManager.prerequisitesPresent(specializations) then + typeManager:addSpecialization(typeName, CpCourseManager.SPEC_NAME) + end +end + function CpCourseManager.prerequisitesPresent(specializations) - return SpecializationUtil.hasSpecialization(AIFieldWorker, specializations) + return SpecializationUtil.hasSpecialization(CpAIWorker, specializations) end function CpCourseManager.registerEventListeners(vehicleType) @@ -113,8 +120,7 @@ end function CpCourseManager:onLoad(savegame) --- Register the spec: spec_cpCourseManager - local specName = CpCourseManager.MOD_NAME .. ".cpCourseManager" - self.spec_cpCourseManager = self["spec_" .. specName] + self.spec_cpCourseManager = self["spec_" .. CpCourseManager.SPEC_NAME] local spec = self.spec_cpCourseManager spec.coursePlot = CoursePlot(g_currentMission.inGameMenu.ingameMap) diff --git a/scripts/specializations/CpGamePadHud.lua b/scripts/specializations/CpGamePadHud.lua index 1afa7e2ec..c9fdc8994 100644 --- a/scripts/specializations/CpGamePadHud.lua +++ b/scripts/specializations/CpGamePadHud.lua @@ -44,7 +44,7 @@ function CpGamePadHud.initSpecialization() end function CpGamePadHud.prerequisitesPresent(specializations) - return SpecializationUtil.hasSpecialization(AIFieldWorker, specializations) + return SpecializationUtil.hasSpecialization(CpAIWorker, specializations) end function CpGamePadHud.register(typeManager, typeName, specializations) @@ -194,11 +194,13 @@ function CpGamePadHud:actionEventOpenCloseDisplay() page = CpGamePadHud.UNLOADER_PAGE elseif self:getCanStartCpBaleFinder() then page = CpGamePadHud.BALE_LOADER_PAGE - elseif self:getCanStartCpBunkerSiloWorker() and self:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_BUNKER_SILO - or AIUtil.hasChildVehicleWithSpecialization(self, Leveler) then - page = CpGamePadHud.BUNKER_SILO_PAGE - elseif self:getCanStartCpSiloLoaderWorker() then + elseif self:getCanStartCpSiloLoaderWorker() and (self:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_SILO_LOADING or + AIUtil.hasChildVehicleWithSpecialization(self, ConveyorBelt)) then page = CpGamePadHud.SILO_LOADER_PAGE + elseif self:getCanStartCpBunkerSiloWorker() and (self:getCpStartingPointSetting():getValue() == CpJobParameters.START_AT_BUNKER_SILO + or (AIUtil.hasChildVehicleWithSpecialization(self, Leveler) + and not AIUtil.hasChildVehicleWithSpecialization(self, Shovel))) then + page = CpGamePadHud.BUNKER_SILO_PAGE else page = CpGamePadHud.FIELDWORK_PAGE end diff --git a/scripts/specializations/CpHud.lua b/scripts/specializations/CpHud.lua index fbef606ef..23f7b63df 100644 --- a/scripts/specializations/CpHud.lua +++ b/scripts/specializations/CpHud.lua @@ -16,7 +16,7 @@ function CpHud.initSpecialization() end function CpHud.prerequisitesPresent(specializations) - return SpecializationUtil.hasSpecialization(CpAIFieldWorker, specializations) + return SpecializationUtil.hasSpecialization(CpAIWorker, specializations) end function CpHud.register(typeManager,typeName,specializations) diff --git a/scripts/specializations/CpInfoTexts.lua b/scripts/specializations/CpInfoTexts.lua index 9aaf790e9..8b78daecc 100644 --- a/scripts/specializations/CpInfoTexts.lua +++ b/scripts/specializations/CpInfoTexts.lua @@ -19,7 +19,7 @@ end function CpInfoTexts.prerequisitesPresent(specializations) - return SpecializationUtil.hasSpecialization(CpAIFieldWorker, specializations) + return SpecializationUtil.hasSpecialization(CpAIWorker, specializations) end function CpInfoTexts.registerEventListeners(vehicleType) diff --git a/scripts/specializations/CpShovelPositions.lua b/scripts/specializations/CpShovelPositions.lua new file mode 100644 index 000000000..0ac058084 --- /dev/null +++ b/scripts/specializations/CpShovelPositions.lua @@ -0,0 +1,713 @@ +--[[ + This specialization is used to control the shovel position into 4 stages: + - Loading position 0.2m above the ground. + - Transport position + - Pre unloading position + - Unloading position + + TODO: + - Fine tuning + - Testing from different front loaders ... + - Add Telescopic handlers support. +]]-- + +---@class CpShovelPositions +CpShovelPositions = { + DEACTIVATED = 0, + LOADING = 1, + TRANSPORT = 2, + PRE_UNLOAD = 3, + UNLOADING = 4, + NUM_STATES = 4, + LOADING_POSITION = { + ARM_LIMITS = { + 0, + 0.1 + }, + SHOVEL_LIMITS = { + 88, + 92 + }, + }, + TRANSPORT_POSITION = { + ARM_LIMITS = { + 0.1, + 0.20 + }, + SHOVEL_LIMITS = { + 53, + 57 + }, + }, + PRE_UNLOAD_POSITION = { + ARM_LIMITS = { + 2.7, + 2.8 + }, + SHOVEL_LIMITS = { + 43, + 47 + }, + }, + DEBUG = true +} +CpShovelPositions.MOD_NAME = g_currentModName +CpShovelPositions.NAME = ".cpShovelPositions" +CpShovelPositions.SPEC_NAME = CpShovelPositions.MOD_NAME .. CpShovelPositions.NAME +CpShovelPositions.KEY = "." .. CpShovelPositions.SPEC_NAME + +function CpShovelPositions.initSpecialization() + local schema = Vehicle.xmlSchemaSavegame + CpShovelPositions.initConsoleCommands() +end + +function CpShovelPositions.prerequisitesPresent(specializations) + return SpecializationUtil.hasSpecialization(Shovel, specializations) and not + SpecializationUtil.hasSpecialization(Trailer, specializations) and not + SpecializationUtil.hasSpecialization(ConveyorBelt, specializations) +end + +function CpShovelPositions.register(typeManager, typeName, specializations) + if CpShovelPositions.prerequisitesPresent(specializations) then + typeManager:addSpecialization(typeName, CpShovelPositions.SPEC_NAME) + end +end + +function CpShovelPositions.registerEventListeners(vehicleType) + SpecializationUtil.registerEventListener(vehicleType, "onLoad", CpShovelPositions) + SpecializationUtil.registerEventListener(vehicleType, "onDraw", CpShovelPositions) + SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", CpShovelPositions) + SpecializationUtil.registerEventListener(vehicleType, "onPostAttach", CpShovelPositions) +end + +function CpShovelPositions.registerFunctions(vehicleType) + SpecializationUtil.registerFunction(vehicleType, "cpSetShovelState", CpShovelPositions.cpSetShovelState) + SpecializationUtil.registerFunction(vehicleType, "cpSetTemporaryShovelState", CpShovelPositions.cpSetTemporaryShovelState) + SpecializationUtil.registerFunction(vehicleType, "cpSetTemporaryLoadingShovelState", CpShovelPositions.cpSetTemporaryLoadingShovelState) + SpecializationUtil.registerFunction(vehicleType, "cpResetShovelState", CpShovelPositions.cpResetShovelState) + SpecializationUtil.registerFunction(vehicleType, "cpSetupShovelPositions", CpShovelPositions.cpSetupShovelPositions) + SpecializationUtil.registerFunction(vehicleType, "areCpShovelPositionsDirty", CpShovelPositions.areCpShovelPositionsDirty) + SpecializationUtil.registerFunction(vehicleType, "setCpShovelMinimalUnloadHeight", CpShovelPositions.setCpShovelMinimalUnloadHeight) +end + +-------------------------------------------- +--- Event Listener +-------------------------------------------- + +function CpShovelPositions:onLoad(savegame) + --- Register the spec: spec_ShovelPositions + self.spec_cpShovelPositions = self["spec_" .. CpShovelPositions.SPEC_NAME] + local spec = self.spec_cpShovelPositions + --- Current shovel state. + spec.state = CpShovelPositions.DEACTIVATED + spec.isDirty = false + spec.minimalShovelUnloadHeight = 4 + spec.highDumpMovingToolIx = g_vehicleConfigurations:get(self, "shovelMovingToolIx") + spec.isHighDumpShovel = spec.highDumpMovingToolIx ~= nil +end + +function CpShovelPositions:onPostAttach() + if self.spec_cpShovelPositions then + CpShovelPositions.cpSetupShovelPositions(self) + end +end + +function CpShovelPositions:onDraw() + +end + +function CpShovelPositions:onUpdateTick(dt) + local spec = self.spec_cpShovelPositions + if spec.shovelToolIx == nil or spec.armToolIx == nil or self.rootVehicle == nil then + return + end + if spec.state == CpShovelPositions.LOADING then + CpUtil.try(CpShovelPositions.updateLoadingPosition, self, dt) + elseif spec.state == CpShovelPositions.TRANSPORT then + CpUtil.try(CpShovelPositions.updateTransportPosition, self, dt) + elseif spec.state == CpShovelPositions.PRE_UNLOAD then + CpUtil.try(CpShovelPositions.updatePreUnloadPosition, self, dt) + elseif spec.state == CpShovelPositions.UNLOADING then + CpUtil.try(CpShovelPositions.updateUnloadingPosition, self, dt) + end + if spec.resetStateWhenReached and not self:areCpShovelPositionsDirty() then + self:cpSetShovelState(CpShovelPositions.DEACTIVATED) + spec.resetStateWhenReached = false + end +end + +--- Changes the current shovel state position. +function CpShovelPositions:cpSetShovelState(state) + if not self.isServer then return end + local spec = self.spec_cpShovelPositions + spec.resetWhenReached = false + if spec.state ~= state then + spec.state = state + if state == CpShovelPositions.DEACTIVATED then + ImplementUtil.stopMovingTool(spec.armVehicle, spec.armTool) + ImplementUtil.stopMovingTool(spec.shovelVehicle, spec.shovelTool) + end + end +end + +function CpShovelPositions:cpSetTemporaryShovelState(state) + if not self.isServer then return end + local spec = self.spec_cpShovelPositions + self:cpSetShovelState(state) + spec.resetStateWhenReached = true +end + +function CpShovelPositions:cpSetTemporaryLoadingShovelState() + self:cpSetTemporaryShovelState(CpShovelPositions.LOADING) +end + +--- Deactivates the shovel position control. +function CpShovelPositions:cpResetShovelState() + if not self.isServer then return end + CpShovelPositions.debug(self, "Reset shovelPositionState.") + local spec = self.spec_cpShovelPositions + spec.state = CpShovelPositions.DEACTIVATED + ImplementUtil.stopMovingTool(spec.armVehicle, spec.armTool) + ImplementUtil.stopMovingTool(spec.shovelVehicle, spec.shovelTool) +end + +--- Is the current target shovel position not yet reached? +function CpShovelPositions:areCpShovelPositionsDirty() + local spec = self.spec_cpShovelPositions + return spec.isDirty +end + +--- Sets the relevant moving tools. +function CpShovelPositions:cpSetupShovelPositions() + local spec = self.spec_cpShovelPositions + spec.shovelToolIx = nil + spec.armToolIx = nil + spec.shovelTool = nil + spec.armTool = nil + local rootVehicle = self:getRootVehicle() + local childVehicles = rootVehicle:getChildVehicles() + for _, vehicle in ipairs(childVehicles) do + if vehicle.spec_cylindered then + for i, tool in pairs(vehicle.spec_cylindered.movingTools) do + if tool.controlGroupIndex ~= nil then + if tool.axis == "AXIS_FRONTLOADER_ARM" then + spec.armToolIx = i + spec.armTool = tool + spec.armVehicle = vehicle + spec.armProjectionNode = CpUtil.createNode("CpShovelArmProjectionNode", + 0, 0, 0, vehicle.rootNode) + spec.armToolRefNode = CpUtil.createNode("CpShovelArmToolRefNode", + 0, 0, 0, vehicle.rootNode) + elseif tool.axis == "AXIS_FRONTLOADER_TOOL" then + spec.shovelToolIx = i + spec.shovelTool = tool + spec.shovelVehicle = vehicle + elseif tool.axis == "AXIS_FRONTLOADER_ARM2" then + --- WIP for telescope loaders + spec.armExtendToolIx = i + spec.armExtendTool = tool + spec.armExtendVehicle = vehicle + end + end + end + end + end +end + +--- Controls the shovel rotation. +--- Also opens/closes the shovel if needed. +---@param dt number +---@param targetAngle number +---@return boolean isDirty +function CpShovelPositions:controlShovelPosition(dt, targetAngle) + local spec = self.spec_cpShovelPositions + local shovelData = ImplementUtil.getShovelNode(self) + local isDirty = false + if shovelData.movingToolActivation and not spec.isHighDumpShovel then + --- The shovel has a moving tool for grabbing. + --- So it needs to be opened for loading and unloading. + for i, tool in pairs(self.spec_cylindered.movingTools) do + if tool.axis and tool.rotMax ~= nil and tool.rotMin ~= nil then + if spec.state == CpShovelPositions.LOADING or spec.state == CpShovelPositions.UNLOADING then + --- Opens the shovel for loading and unloading + isDirty = ImplementUtil.moveMovingToolToRotation(self, tool, dt, + tool.invertAxis and tool.rotMin or tool.rotMax) + else + --- Closes the shovel after loading or unloading + isDirty = ImplementUtil.moveMovingToolToRotation(self, tool, dt, + tool.invertAxis and tool.rotMax or tool.rotMin) + end + break + end + end + end + local curRot = {} + curRot[1], curRot[2], curRot[3] = getRotation(spec.shovelTool.node) + local oldShovelRot = curRot[spec.shovelTool.rotationAxis] + local goalAngle = MathUtil.clamp(oldShovelRot + targetAngle, spec.shovelTool.rotMin, spec.shovelTool.rotMax) + return ImplementUtil.moveMovingToolToRotation(spec.shovelVehicle, + spec.shovelTool, dt, goalAngle) or isDirty +end + +--- Performs the unloading with a high dump shovel. +--- This kind of a shovel has a moving tool for unloading. +---@param dt number +---@return boolean isDirty +---@return number angle +---@return number targetAngle +function CpShovelPositions:unfoldHighDumpShovel(dt) + local highDumpShovelTool = self.spec_cylindered.movingTools[self.spec_cpShovelPositions.highDumpMovingToolIx] + local _, dy, _ = localDirectionToWorld(getParent(highDumpShovelTool.node), 0, 0, 1) + local angle = math.acos(dy) or 0 + local targetAngle = math.pi/2 - math.pi/6 + if CpShovelPositions.controlShovelPosition(self, dt, targetAngle - angle) then + return true, angle, targetAngle + end + local isDirty = ImplementUtil.moveMovingToolToRotation(self, highDumpShovelTool, dt, + highDumpShovelTool.invertAxis and highDumpShovelTool.rotMin or highDumpShovelTool.rotMax) + return isDirty, angle, targetAngle +end + +--- Folds the high dump shovel back to the normal position. +---@param dt number +---@return boolean isDirty +function CpShovelPositions:foldHighDumpShovel(dt) + local highDumpShovelTool = self.spec_cylindered.movingTools[self.spec_cpShovelPositions.highDumpMovingToolIx] + return ImplementUtil.moveMovingToolToRotation(self, highDumpShovelTool, dt, + highDumpShovelTool.invertAxis and highDumpShovelTool.rotMax or highDumpShovelTool.rotMin) +end + +--- Sets the current shovel position values, like the arm and shovel rotations. +---@param dt number +---@param shovelLimits table +---@param armLimits table +---@param heightOffset number|nil +---@return boolean +function CpShovelPositions:setShovelPosition(dt, shovelLimits, armLimits, heightOffset) + heightOffset = heightOffset or 0 + local spec = self.spec_cpShovelPositions + local min, max = unpack(shovelLimits) + --- Target angle of the shovel node, which is at the end of the shovel. + local targetAngle = math.rad(min) + math.rad(max - min)/2 + min, max = unpack(armLimits) + --- Target height of the arm. + --- This is relative to the attacher joint of the shovel. + local targetHeight = min + (max - min)/2 + local shovelTool = spec.shovelTool + local armTool = spec.armTool + local shovelVehicle = spec.shovelVehicle + local armVehicle = spec.armVehicle + local minimalTargetHeight = spec.minimalShovelUnloadHeight + local curRot = {} + curRot[1], curRot[2], curRot[3] = getRotation(shovelTool.node) + local oldShovelRot = curRot[shovelTool.rotationAxis] + + curRot = {} + curRot[1], curRot[2], curRot[3] = getRotation(armTool.node) + local oldArmRot = curRot[armTool.rotationAxis] + + local armProjectionNode = spec.armProjectionNode + local armToolRefNode = spec.armToolRefNode + + local radiusArmToolToShovelTool = calcDistanceFrom(shovelTool.node, armTool.node) + + local attacherJointNode = self.spec_attachable.attacherJoint.node + local angle, shovelNode = CpShovelPositions.getShovelData(self) + --local _, shovelY, _ = localToLocal(self.rootNode, attacherJointNode, 0, 0, 0) + local _, shovelY, _ = localToLocal(armVehicle.rootNode, shovelVehicle.rootNode, 0, 0, 0) + + --- All values will be calculated in the coordinate system from the vehicle root node. + + local _, ty, tz = localToLocal(getChildAt(armTool.node, 0), armVehicle.rootNode, 0, 0, 0) + local ax, ay, az = localToLocal(armTool.node, armVehicle.rootNode, 0, 0, 0) + local wx, _, wz = getWorldTranslation(armVehicle.rootNode) + + local by = shovelY + if self.spec_foliageBending and self.spec_foliageBending.bendingNodes[1] then + --- The foliage bending area can be used to get relatively exact dimensions of the shovel. + local bending = self.spec_foliageBending.bendingNodes[1] + if bending.id ~= nil and bending.node ~= nil then + --- Trying to find the lowest point of shovel. + --- This might be front tip or near the attacher. + local sx, _, sz = localToWorld(shovelTool.node, 0, 0, 0) + local bx1, by1, bz1 = localToWorld(bending.node, 0, 0, bending.minZ) + local bx2, by2, bz2 = localToWorld(bending.node, 0, 0, bending.maxZ) + DebugUtil.drawDebugLine(bx1, by1, bz1, bx2, by2, bz2, 0, 0, 1) + if by1 < by2 then + _, by, _ = worldToLocal(shovelTool.node, sx, by1, sz) + DebugUtil.drawDebugLine(sx, by1, sz, sx, by1 - by, sz, 0, 0, 1) + else + _, by, _ = worldToLocal(shovelTool.node, sx, by2, sz) + DebugUtil.drawDebugLine(sx, by2, sz, sx, by2 - by, sz, 0, 0, 1) + end + end + else + local bx1, by1, bz1 = localToWorld(self.rootNode, 0, 0, self.size.lengthOffset + self.size.length/2) + local bx2, by2, bz2 = localToWorld(self.rootNode, 0, 0, self.size.lengthOffset - self.size.length/2) + DebugUtil.drawDebugLine(bx1, by1, bz1, bx2, by2, bz2, 0, 0, 1) + local sx, _, sz = localToWorld(shovelTool.node, 0, 0, 0) + if by1 < by2 then + _, by, _ = worldToLocal(shovelTool.node, sx, by1, sz) + else + _, by, _ = worldToLocal(shovelTool.node, sx, by2, sz) + end + end + local deltaY = 0 + if spec.state == CpShovelPositions.PRE_UNLOAD or spec.state == CpShovelPositions.UNLOADING then + targetHeight = minimalTargetHeight + end + local sx, sy, sz = 0, -by + targetHeight + heightOffset, 0 + local ex, ey, ez = 0, sy, 20 + local wsx, wsy, wsz = localToWorld(armVehicle.rootNode, sx, sy, sz) + local wex, wey, wez = localToWorld(armVehicle.rootNode, ex, ey, ez) + local _, terrainHeight, _ = getWorldTranslation(self.rootVehicle.rootNode, 0, 0, 0) + + local yMax = ay + radiusArmToolToShovelTool + local yMin = ay - radiusArmToolToShovelTool + if sy > yMax then + --- Makes sure the target height is still reachable + sy = yMax - 0.01 + ey = yMax - 0.01 + end + if sy < yMin then + --- Makes sure the target height is still reachable + sy = yMin + 0.01 + ey = yMin + 0.01 + end + local hasIntersection, i1z, i1y, i2z, i2y = MathUtil.getCircleLineIntersection( + az, ay, radiusArmToolToShovelTool, + sz, sy, ez, ey) + + local isDirty, skipArm + if spec.state == CpShovelPositions.UNLOADING or spec.state == CpShovelPositions.PRE_UNLOAD then + if spec.isHighDumpShovel then + if spec.state == CpShovelPositions.UNLOADING then + isDirty, angle, targetAngle = CpShovelPositions.unfoldHighDumpShovel(self, dt) + end + else + isDirty = CpShovelPositions.controlShovelPosition(self, dt, targetAngle - angle) + end + if spec.state == CpShovelPositions.UNLOADING and isDirty then + skipArm = true + end + end + if spec.state ~= CpShovelPositions.UNLOADING then + if spec.isHighDumpShovel then + isDirty = CpShovelPositions.foldHighDumpShovel(self, dt) or isDirty + if isDirty then + skipArm = true + end + end + end + + local alpha, oldRotRelativeArmRot = 0, 0 + if hasIntersection then + --- Controls the arm height + setTranslation(armProjectionNode, 0, i1y, i1z) + setTranslation(armToolRefNode, ax, ay, az) + local _, shy, shz = localToLocal(shovelTool.node, armVehicle.rootNode, 0, 0, 0) + local dirZ, dirY = MathUtil.vector2Normalize(shz - az, shy - ay) + oldRotRelativeArmRot = MathUtil.getYRotationFromDirection(-dirZ, dirY) + math.pi/2 + + alpha = math.atan2(i1y - ay, i1z - az) + local beta = -math.atan2(i2y - ay, i2z - az) + local a = MathUtil.clamp(oldArmRot - MathUtil.getAngleDifference( + alpha, oldRotRelativeArmRot), armTool.rotMin, armTool.rotMax) + if not skipArm then + isDirty = ImplementUtil.moveMovingToolToRotation( + armVehicle, armTool, dt, a) or isDirty + end + end + + if spec.state ~= CpShovelPositions.UNLOADING then + isDirty = isDirty or CpShovelPositions.controlShovelPosition(self, dt, targetAngle - angle) + end + + --- Debug information + if g_currentMission.controlledVehicle == shovelVehicle.rootVehicle and + CpDebug:isChannelActive(CpDebug.DBG_SILO, shovelVehicle.rootVehicle) then + DebugUtil.drawDebugLine(wsx, wsy, wsz, wex, wey, wez) + DebugUtil.drawDebugLine(wsx, terrainHeight + minimalTargetHeight , wsz, + wex, terrainHeight + minimalTargetHeight, wez, 0, 0, 1) + DebugUtil.drawDebugCircleAtNode(armVehicle.rootNode, radiusArmToolToShovelTool, + 30, nil, true, {ax, ay, az}) + CpUtil.drawDebugNode(armVehicle.rootNode) + CpUtil.drawDebugNode(armTool.node) + CpUtil.drawDebugNode(shovelTool.node) + CpUtil.drawDebugNode(armProjectionNode) + CpUtil.drawDebugNode(armToolRefNode) + + local debugData = {} + if hasIntersection then + table.insert(debugData, { + value = "", + name = "Arm Rotation:" }) + table.insert(debugData, { + name = "alpha", value = math.deg(alpha) }) + table.insert(debugData, { + name = "deltaAlphaOld", value = math.deg(MathUtil.getAngleDifference(alpha, oldArmRot)) }) + table.insert(debugData, { + name = "old", value = math.deg(oldArmRot) }) + table.insert(debugData, { + name = "deltaAlpha", value = math.deg(MathUtil.getAngleDifference(alpha, oldRotRelativeArmRot)) }) + table.insert(debugData, { + name = "deltaY", value = deltaY}) + table.insert(debugData, { + name = "shovelY", value = shovelY}) + table.insert(debugData, { + name = "dirRot", value = math.deg(oldRotRelativeArmRot) }) + table.insert(debugData, { + name = "distAlpha", value = MathUtil.vector2Length(i1z - tz, i1y - ty) }) + + table.insert(debugData, { + value = "", + name = "", + columnOffset = 0.12 + }) + end + table.insert(debugData, { + value = "", + name = "Shovel Rotation:" }) + table.insert(debugData, { + name = "angle", value = math.deg(angle) }) + table.insert(debugData, { + name = "deltaAngle", value = math.deg(targetAngle - angle) }) + table.insert(debugData, { + name = "targetAngle", value = math.deg(targetAngle) }) + table.insert(debugData, { + value = "", + name = "Diff:" }) + table.insert(debugData, { + name = "unload height", value = minimalTargetHeight }) + + DebugUtil.renderTable(0.4, 0.4, 0.018, + debugData, 0) + + if self.spec_foliageBending ~= nil then + local offset = 0.25 + + for _, bendingNode in ipairs(self.spec_foliageBending.bendingNodes) do + if bendingNode.id ~= nil then + DebugUtil.drawDebugRectangle(bendingNode.node, bendingNode.minX, bendingNode.maxX, bendingNode.minZ, bendingNode.maxZ, bendingNode.yOffset, 1, 0, 0) + DebugUtil.drawDebugRectangle(bendingNode.node, bendingNode.minX - offset, bendingNode.maxX + offset, bendingNode.minZ - offset, bendingNode.maxZ + offset, bendingNode.yOffset, 0, 1, 0) + DebugUtil.drawDebugNode(bendingNode.node, "Bending node") + end + end + end + + + end + return isDirty +end + +function CpShovelPositions:updateLoadingPosition(dt) + local spec = self.spec_cpShovelPositions + local angle = CpShovelPositions.getShovelData(self) + local heightOffset = self.rootVehicle.getCpSettings and self.rootVehicle:getCpSettings().loadingShovelHeightOffset:getValue() + local isDirty + if angle then + isDirty = CpShovelPositions.setShovelPosition(self, dt, + CpShovelPositions.LOADING_POSITION.SHOVEL_LIMITS, + CpShovelPositions.LOADING_POSITION.ARM_LIMITS, heightOffset) + end + spec.isDirty = isDirty +end + +function CpShovelPositions:updateTransportPosition(dt) + local spec = self.spec_cpShovelPositions + local angle = CpShovelPositions.getShovelData(self) + local heightOffset = self.rootVehicle.getCpSettings and self.rootVehicle:getCpSettings().loadingShovelHeightOffset:getValue() + local isDirty + if angle then + isDirty = CpShovelPositions.setShovelPosition(self, dt, + CpShovelPositions.TRANSPORT_POSITION.SHOVEL_LIMITS, + CpShovelPositions.TRANSPORT_POSITION.ARM_LIMITS, heightOffset) + end + spec.isDirty = isDirty +end + +function CpShovelPositions:updatePreUnloadPosition(dt) + local spec = self.spec_cpShovelPositions + local angle = CpShovelPositions.getShovelData(self) + local isDirty + if angle then + isDirty = CpShovelPositions.setShovelPosition(self, dt, + CpShovelPositions.PRE_UNLOAD_POSITION.SHOVEL_LIMITS, + CpShovelPositions.PRE_UNLOAD_POSITION.ARM_LIMITS, nil) + end + spec.isDirty = isDirty +end + +function CpShovelPositions:updateUnloadingPosition(dt) + local spec = self.spec_cpShovelPositions + local angle, _, maxAngle = CpShovelPositions.getShovelData(self) + local isDirty + if angle and maxAngle then + isDirty = CpShovelPositions.setShovelPosition(self, dt, + {math.deg(maxAngle), math.deg(maxAngle) + 2}, + CpShovelPositions.PRE_UNLOAD_POSITION.ARM_LIMITS, nil) + end + spec.isDirty = isDirty +end + +--- Sets the unload height target of the lowest part of the shovel. +---@param height number +function CpShovelPositions:setCpShovelMinimalUnloadHeight(height) + local spec = self.spec_cpShovelPositions + spec.minimalShovelUnloadHeight = height +end + +--- Gets all relevant shovel data. +function CpShovelPositions:getShovelData() + local shovelSpec = self.spec_shovel + local info = shovelSpec.shovelDischargeInfo + if info == nil or info.node == nil then + CpShovelPositions.debug(self, "Info or node not found!") + return + end + if info.maxSpeedAngle == nil or info.minSpeedAngle == nil then + CpShovelPositions.debug(self, "maxSpeedAngle or minSpeedAngle not found!") + return + end + if shovelSpec.shovelNodes[1] == nil then + CpShovelPositions.debug(self, "Shovel nodes index 0 not found!") + return + end + local _, dy, _ = localDirectionToWorld(info.node, 0, 0, 1) + local angle = math.acos(dy) + local factor = math.max(0, math.min(1, (angle - info.minSpeedAngle) / (info.maxSpeedAngle - info.minSpeedAngle))) + return angle, shovelSpec.shovelNodes[1].node, info.maxSpeedAngle, info.minSpeedAngle, factor +end + +function CpShovelPositions.debug(implement, ...) + if CpShovelPositions.DEBUG then + CpUtil.debugImplement(CpDebug.DBG_SILO, implement, ...) + end +end + + +-------------------------------------------- +--- Console Commands +-------------------------------------------- + +function CpShovelPositions.initConsoleCommands() + g_devHelper.consoleCommands:registerConsoleCommand("cpShovelPositionsPrintShovelDebug", + "Prints debug information for the shovel", + "consoleCommandPrintShovelDebug", CpShovelPositions) + g_devHelper.consoleCommands:registerConsoleCommand("cpShovelPositionsSetState", + "Set's the current shovel state", + "consoleCommandSetShovelState", CpShovelPositions) + g_devHelper.consoleCommands:registerConsoleCommand("cpShovelPositionsSetArmLimit", + "Set's the arm max limit", + "consoleCommandSetPreUnloadArmLimit", CpShovelPositions) + g_devHelper.consoleCommands:registerConsoleCommand('cpShovelPositionsSetMinimalUnloadHeight', + 'Sets the minimal unload height to a fixed value', + 'consoleCommandSetMinimalUnloadHeight', CpShovelPositions) + g_devHelper.consoleCommands:registerConsoleCommand('cpShovelPositionsMeasureAndSetMinUnloadHeight', + 'Measures and sets the minimal unload height', + 'consoleCommandMeasureAndSetMinimalUnloadHeight', CpShovelPositions) +end + +local function executeConsoleCommand(func, ...) + local vehicle = g_currentMission.controlledVehicle + if not vehicle then + CpUtil.info("Not entered a valid vehicle!") + return false + end + -- if vehicle:getIsAIActive() then + -- CpUtil.infoVehicle(vehicle, "Error, AI is active!") + -- return false + -- end + local shovels, found = AIUtil.getAllChildVehiclesWithSpecialization(vehicle, Shovel) + if not found then + CpUtil.infoVehicle(vehicle, "No shovel implement found!") + return false + end + return func(shovels[1], ...) +end + +function CpShovelPositions:consoleCommandSetShovelState(state) + return executeConsoleCommand(function(shovelImplement, state) + state = tonumber(state) + if state == nil or state < 0 or state > CpShovelPositions.NUM_STATES then + CpUtil.infoVehicle(shovelImplement, "No valid state(0 - %d) was given!", CpShovelPositions.NUM_STATES) + return false + end + shovelImplement:cpSetShovelState(state) + end, state) +end + +function CpShovelPositions:consoleCommandSetPreUnloadArmLimit(min, max) + return executeConsoleCommand(function(shovelImplement, min, max) + min = tonumber(min) + max = tonumber(max) + if min == nil or max == nil then + CpUtil.infoVehicle(shovelImplement, "No valid limits given! min: %s, max: %s", tostring(min), tostring(max)) + return false + end + CpShovelPositions.PRE_UNLOAD_POSITION.ARM_LIMITS = { min, max } + end, min, max) +end + +function CpShovelPositions:consoleCommandPrintShovelDebug(cylinderedDepth) + return executeConsoleCommand(function(shovelImplement) + --- Position debug + CpUtil.infoImplement(shovelImplement, "-- Position debug --") + local spec = shovelImplement.spec_cpShovelPositions + if spec then + CpUtil.infoImplement(shovelImplement, " arm tool %s -> %s", + CpUtil.getName(spec.armVehicle), tostring(spec.armToolIx)) + CpUtil.infoImplement(shovelImplement, " shovel tool %s -> %s", + CpUtil.getName(spec.shovelVehicle), tostring(spec.shovelToolIx)) + local highDumpShovelIx = g_vehicleConfigurations:get(shovelImplement, "shovelMovingToolIx") + CpUtil.infoImplement(shovelImplement, " shovel high dump %s -> %s", + CpUtil.getName(shovelImplement), tostring(highDumpShovelIx)) + end + + CpUtil.infoImplement(shovelImplement, "-- Position debug --") + --- Shovel debug + local controller = ShovelController(shovelImplement.rootVehicle, shovelImplement, true) + controller:printShovelDebug() + controller:delete() + --- Cylindered debug here + CpUtil.infoImplement(shovelImplement, "-- Cylindered debug --") + cylinderedDepth = cylinderedDepth and tonumber(cylinderedDepth) or 0 + + local childVehicles = shovelImplement.rootVehicle:getChildVehicles() + for _, vehicle in ipairs(childVehicles) do + if vehicle.spec_cylindered then + for ix, tool in pairs(vehicle.spec_cylindered.movingTools) do + CpUtil.infoImplement(shovelImplement, " %s => ix: %d ", + CpUtil.getName(vehicle), ix) + if cylinderedDepth > 0 then + CpUtil.infoImplement(shovelImplement, " %s", + DebugUtil.debugTableToString(tool, " ", 0, cylinderedDepth)) + end + CpUtil.infoImplement(shovelImplement, " %s => ix: %d finished", + CpUtil.getName(vehicle), ix) + end + end + end + CpUtil.infoImplement(shovelImplement, "-- Cylindered debug finished --") + end) +end + +function CpShovelPositions:consoleCommandSetMinimalUnloadHeight(height) + return executeConsoleCommand(function(shovelImplement, height) + height = tonumber(height) + if height == nil then + CpUtil.infoVehicle(shovelImplement, "No valid height given! height: %s", tostring(height)) + return false + end + local spec = shovelImplement.spec_cpShovelPositions + spec.minimalShovelUnloadHeight = height + end, height) +end + +function CpShovelPositions:consoleCommandMeasureAndSetMinimalUnloadHeight() + return executeConsoleCommand(function(shovelImplement) + local controller = ShovelController(shovelImplement.rootVehicle, shovelImplement, true) + controller:calculateMinimalUnloadingHeight() + controller:delete() + end) +end \ No newline at end of file diff --git a/scripts/specializations/CpVehicleSettings.lua b/scripts/specializations/CpVehicleSettings.lua index 8ee13db07..7d2e397c8 100644 --- a/scripts/specializations/CpVehicleSettings.lua +++ b/scripts/specializations/CpVehicleSettings.lua @@ -6,9 +6,12 @@ CpVehicleSettings = {} CpVehicleSettings.MOD_NAME = g_currentModName -CpVehicleSettings.KEY = "."..CpVehicleSettings.MOD_NAME..".cpVehicleSettings" CpVehicleSettings.SETTINGS_KEY = ".settings" CpVehicleSettings.USER_KEY = ".users" +CpVehicleSettings.NAME = ".cpVehicleSettings" +CpVehicleSettings.SPEC_NAME = CpVehicleSettings.MOD_NAME .. CpVehicleSettings.NAME +CpVehicleSettings.KEY = "." .. CpVehicleSettings.MOD_NAME .. CpVehicleSettings.NAME + function CpVehicleSettings.initSpecialization() local schema = Vehicle.xmlSchemaSavegame --- Old xml schema for settings @@ -30,13 +33,19 @@ function CpVehicleSettings.initSpecialization() CpVehicleSettings.registerConsoleCommands() end +function CpVehicleSettings.register(typeManager,typeName,specializations) + if CpVehicleSettings.prerequisitesPresent(specializations) then + typeManager:addSpecialization(typeName, CpVehicleSettings.SPEC_NAME) + end +end function CpVehicleSettings.prerequisitesPresent(specializations) - return SpecializationUtil.hasSpecialization(AIFieldWorker, specializations) + return SpecializationUtil.hasSpecialization(CpAIWorker, specializations) end function CpVehicleSettings.registerEvents(vehicleType) SpecializationUtil.registerEvent(vehicleType, 'onCpUserSettingChanged') + SpecializationUtil.registerEvent(vehicleType, 'onCpLoadingShovelOffsetSettingChanged') end @@ -44,13 +53,12 @@ function CpVehicleSettings.registerEventListeners(vehicleType) -- SpecializationUtil.registerEventListener(vehicleType, "onRegisterActionEvents", CpVehicleSettings) SpecializationUtil.registerEventListener(vehicleType, "onLoad", CpVehicleSettings) SpecializationUtil.registerEventListener(vehicleType, "onLoadFinished", CpVehicleSettings) - SpecializationUtil.registerEventListener(vehicleType, "onPreDetachImplement", CpVehicleSettings) - SpecializationUtil.registerEventListener(vehicleType, "onPostAttachImplement", CpVehicleSettings) SpecializationUtil.registerEventListener(vehicleType, "onCpUnitChanged", CpVehicleSettings) SpecializationUtil.registerEventListener(vehicleType, "onReadStream", CpVehicleSettings) SpecializationUtil.registerEventListener(vehicleType, "onWriteStream", CpVehicleSettings) SpecializationUtil.registerEventListener(vehicleType, "onStateChange", CpVehicleSettings) SpecializationUtil.registerEventListener(vehicleType, "onCpUserSettingChanged", CpVehicleSettings) + SpecializationUtil.registerEventListener(vehicleType, 'onCpLoadingShovelOffsetSettingChanged', CpVehicleSettings) end function CpVehicleSettings.registerFunctions(vehicleType) @@ -74,8 +82,7 @@ end function CpVehicleSettings:onLoad(savegame) --- Register the spec: spec_CpVehicleSettings - local specName = CpVehicleSettings.MOD_NAME .. ".cpVehicleSettings" - self.spec_cpVehicleSettings = self["spec_" .. specName] + self.spec_cpVehicleSettings = self["spec_" .. CpVehicleSettings.SPEC_NAME] local spec = self.spec_cpVehicleSettings --- Clones the generic settings to create different settings containers for each vehicle. @@ -90,43 +97,11 @@ function CpVehicleSettings:onLoadFinished() spec.wasLoaded = nil end ---- TODO: These are only applied on a implement an not on a single vehicle. ---- This means self driving vehicle are not getting these vehicle configuration values. -function CpVehicleSettings:onPostAttachImplement(object) - --- Only apply these values, if were are not loading from a savegame. - local spec = self.spec_cpVehicleSettings - if spec.wasLoaded then - return - end - - CpVehicleSettings.setAutomaticWorkWidthAndOffset(self) - CpVehicleSettings.setAutomaticBunkerSiloWorkWidth(self) - CpVehicleSettings.setAutomaticBaleCollectorOffset(self) - - CpVehicleSettings.setFromVehicleConfiguration(self, object, spec.raiseImplementLate, 'raiseLate') - CpVehicleSettings.setFromVehicleConfiguration(self, object, spec.lowerImplementEarly, 'lowerEarly') - CpVehicleSettings.setFromVehicleConfiguration(self, object, spec.bunkerSiloWorkWidth, 'workingWidth') - CpVehicleSettings.validateSettings(self) -end - -function CpVehicleSettings:onPreDetachImplement(implement) - --- Only apply these values, if were are not loading from a savegame. - local spec = self.spec_cpVehicleSettings - if spec.wasLoaded then - return - end - - CpVehicleSettings.setAutomaticWorkWidthAndOffset(self, implement.object) - CpVehicleSettings.setAutomaticBunkerSiloWorkWidth(self, implement.object) - - CpVehicleSettings.resetToDefault(self, implement.object, spec.raiseImplementLate, 'raiseLate', false) - CpVehicleSettings.resetToDefault(self, implement.object, spec.lowerImplementEarly, 'lowerEarly', false) - CpVehicleSettings.validateSettings(self) -end --- Changes the sprayer work width on fill type change, as it might depend on the loaded fill type. --- For example Lime and Fertilizer might have a different work width. function CpVehicleSettings:onStateChange(state, data) + local spec = self.spec_cpVehicleSettings if state == Vehicle.STATE_CHANGE_FILLTYPE_CHANGE and self:getIsSynchronized() then local _, hasSprayer = AIUtil.getAllChildVehiclesWithSpecialization(self, Sprayer, nil) if hasSprayer then @@ -137,6 +112,29 @@ function CpVehicleSettings:onStateChange(state, data) self:getCourseGeneratorSettings().workWidth:setFloatValue(width) end end + elseif state == Vehicle.STATE_CHANGE_ATTACH then + CpVehicleSettings.setAutomaticWorkWidthAndOffset(self) + CpVehicleSettings.setAutomaticBunkerSiloWorkWidth(self) + CpVehicleSettings.setAutomaticBaleCollectorOffset(self) + CpVehicleSettings.setFromVehicleConfiguration(self, data.attachedVehicle, + spec.raiseImplementLate, 'raiseLate') + CpVehicleSettings.setFromVehicleConfiguration(self, data.attachedVehicle, + spec.lowerImplementEarly, 'lowerEarly') + CpVehicleSettings.setFromVehicleConfiguration(self, data.attachedVehicle, + spec.bunkerSiloWorkWidth, 'workingWidth') + CpVehicleSettings.setFromVehicleConfiguration(self, data.attachedVehicle, + spec.loadingShovelHeightOffset, 'loadingShovelOffset') + + CpVehicleSettings.validateSettings(self) + elseif state == Vehicle.STATE_CHANGE_DETACH then + CpVehicleSettings.setAutomaticWorkWidthAndOffset(self, data.attachedVehicle) + CpVehicleSettings.setAutomaticBunkerSiloWorkWidth(self, data.attachedVehicle) + + CpVehicleSettings.resetToDefault(self, data.attachedVehicle, spec.raiseImplementLate, + 'raiseLate', false) + CpVehicleSettings.resetToDefault(self, data.attachedVehicle, spec.lowerImplementEarly, + 'lowerEarly', false) + CpVehicleSettings.validateSettings(self) end end @@ -369,7 +367,7 @@ function CpVehicleSettings:areCourseSettingsVisible() end function CpVehicleSettings:areBunkerSiloSettingsVisible() - return self:getCanStartCpBunkerSiloWorker() + return self:getCanStartCpBunkerSiloWorker() or self:getCanStartCpSiloLoaderWorker() end function CpVehicleSettings:areCombineUnloaderSettingsVisible() @@ -394,6 +392,22 @@ function CpVehicleSettings:setAutomaticBaleCollectorOffset() spec.baleCollectorOffset:setFloatValue(offset) end +function CpVehicleSettings:isLoadingShovelOffsetSettingVisible() + return self:getCanStartCpSiloLoaderWorker() and not AIUtil.hasChildVehicleWithSpecialization(self, ConveyorBelt) +end + +function CpVehicleSettings:isLoadingShovelOffsetSettingDisabled() + return not AIUtil.isStopped(self) +end + +function CpVehicleSettings:onCpLoadingShovelOffsetSettingChanged() + local shovels, found = AIUtil.getAllChildVehiclesWithSpecialization(self, Shovel) + if not found then + return false + end + shovels[1]:cpSetTemporaryLoadingShovelState() +end + --- Saves the user value changed on the server. function CpVehicleSettings:onCpUserSettingChanged(setting) if not self.isServer then diff --git a/scripts/trigger/TriggerManager.lua b/scripts/trigger/TriggerManager.lua new file mode 100644 index 000000000..20386277f --- /dev/null +++ b/scripts/trigger/TriggerManager.lua @@ -0,0 +1,167 @@ +--- Stores all the relevant giants triggers expect bunker silos. +--- For now only unload triggers are supported. +---@class TriggerManager +TriggerManager = CpObject() +TriggerManager.DEBUG = false +function TriggerManager:init() + ---@type table + self.unloadTriggers = {} + ---@type table + self.dischargeableUnloadTriggers = {} +end + +--- Adds an unload trigger. +---@param silo table UnloadTrigger +function TriggerManager:addUnloadingSilo(silo) + if silo.exactFillRootNode ~= nil then + self.unloadTriggers[silo.exactFillRootNode] = CpTrigger(silo, silo.exactFillRootNode) + if silo:getIsToolTypeAllowed(ToolType.DISCHARGEABLE) then + self.dischargeableUnloadTriggers[silo.exactFillRootNode] = self.unloadTriggers[silo.exactFillRootNode] + end + end +end + +--- Removes the unload trigger, as it got removed for example sold. +---@param silo table UnloadTrigger +function TriggerManager:removeUnloadingSilo(silo) + if silo.exactFillRootNode ~= nil then + if self.unloadTriggers[silo.exactFillRootNode] then + self.unloadTriggers[silo.exactFillRootNode]:delete() + self.unloadTriggers[silo.exactFillRootNode] = nil + self.dischargeableUnloadTriggers[silo.exactFillRootNode] = nil + end + end +end + +--- Gets the unload trigger from the exactFillRootNode. +---@param node number exactFillRootNode +---@return CpTrigger +function TriggerManager:getUnloadTriggerForNode(node) + return self.unloadTriggers[node] +end + +--- Gets the first trigger found in the defined area. +---@param triggers table +---@param x number +---@param z number +---@param dirX number +---@param dirZ number +---@param width number +---@param length number +---@return boolean +---@return CpTrigger|nil +---@return table|nil +function TriggerManager:getTriggerAt(triggers, x, z, dirX, dirZ, width, length) + local angle = MathUtil.getYRotationFromDirection(dirX, dirZ) + local dirWX, dirWZ = MathUtil.getDirectionFromYRotation(angle + math.pi/2) + local sx, sz = x + dirWX * width/2, z + dirWZ * width/2 + + local area = { + { + x = sx, + z = sz + }, + { + x = sx - dirWX * width, + z = sz - dirWZ * width + }, + { + x = sx - dirWX * width + dirX * length, + z = sz - dirWZ * width + dirZ * length + }, + { + x = sx + dirX * length, + z = sz + dirZ * length + }, + { + x = sx, + z = sz + }, + } + local dx, _, dz + for node, trigger in pairs(triggers) do + dx, _, dz = getWorldTranslation(node) + if CpMathUtil.isPointInPolygon(area, dx, dz) then + return true, trigger, trigger:getTarget() + end + end + return false +end + +--- Gets the first unload trigger found in the defined area. +---@param x number +---@param z number +---@param dirX number +---@param dirZ number +---@param width number +---@param length number +---@return boolean +---@return CpTrigger|nil +---@return table|nil +function TriggerManager:getUnloadTriggerAt(x, z, dirX, dirZ, width, length) + return self:getTriggerAt(self.unloadTriggers, x, z, dirX, dirZ, width, length) +end + +--- Gets the first dischargeable unload trigger found in the defined area. +---@param x number +---@param z number +---@param dirX number +---@param dirZ number +---@param width number +---@param length number +---@return boolean found? +---@return CpTrigger|nil unload trigger +---@return table|nil unload station/placeable +function TriggerManager:getDischargeableUnloadTriggerAt(x, z, dirX, dirZ, width, length) + return self:getTriggerAt(self.dischargeableUnloadTriggers, x, z, dirX, dirZ, width, length) +end + +function TriggerManager:update(dt) + +end + +--- Draws all bunker silos onto the ai map. +---@param map table map to draw to. +---@param selected CpTrigger silo that gets highlighted. +function TriggerManager:drawUnloadTriggers(map, selected) + for _, trigger in pairs(self.unloadTriggers) do + trigger:drawPlot(map, selected) + end +end + +--- Draws all bunker silos onto the ai map. +---@param map table map to draw to. +---@param selected CpTrigger silo that gets highlighted. +---@param fillTypes table|nil fill type that needs to be supported. +function TriggerManager:drawDischargeableTriggers(map, selected, fillTypes) + for _, trigger in pairs(self.dischargeableUnloadTriggers) do + trigger:drawPlot(map, selected, fillTypes) + end +end + +function TriggerManager:draw() + for node, trigger in pairs(self.unloadTriggers) do + if self.DEBUG then + local text = string.format("%s:\n %d", getName(node), node ) + CpUtil.drawDebugNode(node, false, 2, text) + end + end +end + +g_triggerManager = TriggerManager() + +local function addUnloadingSilo(silo, superFunc, ...) + local ret = superFunc(silo, ...) + g_triggerManager:addUnloadingSilo(silo) + return ret +end + +UnloadTrigger.load = Utils.overwrittenFunction(UnloadTrigger.load, addUnloadingSilo) + + +local function removeUnloadingSilo(silo, ...) + g_triggerManager:removeUnloadingSilo(silo) +end + +UnloadTrigger.delete = Utils.prependedFunction(UnloadTrigger.delete, removeUnloadingSilo) + diff --git a/scripts/trigger/TriggerWrapper.lua b/scripts/trigger/TriggerWrapper.lua new file mode 100644 index 000000000..549db2ab9 --- /dev/null +++ b/scripts/trigger/TriggerWrapper.lua @@ -0,0 +1,69 @@ + +--- Wrapper for a trigger. +---@class CpTrigger +CpTrigger = CpObject() + +function CpTrigger:init(trigger, node) + self.trigger = trigger + self.node = node + self.plot = UnloadingTriggerPlot(self.node) +end + +function CpTrigger:delete() + +end + +function CpTrigger:getNode() + return self.node +end + +function CpTrigger:getTrigger() + return self.trigger +end + +function CpTrigger:getTarget() + return self.trigger:getTarget() +end + +---@param fillUnitIndex number +---@return number +function CpTrigger:getFillUnitExactFillRootNode(fillUnitIndex) + return self.trigger:getFillUnitExactFillRootNode(fillUnitIndex) +end + +function CpTrigger:getFillUnitFreeCapacity(fillUnitIndex, fillTypeIndex, farmId) + return self.trigger:getFillUnitFreeCapacity(fillUnitIndex, fillTypeIndex, farmId) +end + +--- Is the fill type allowed ? +---@param fillType any +---@return boolean +function CpTrigger:getIsFillTypeAllowed(fillType) + return self.trigger:getIsFillTypeAllowed(fillType) +end + +---@param map table +---@param selectedTrigger CpTrigger +---@param fillTypes table|nil +function CpTrigger:drawPlot(map, selectedTrigger, fillTypes) + if fillTypes then + local found = false + for i, fillType in pairs(fillTypes) do + if self.trigger:getIsFillTypeAllowed(fillType) then + found = true + end + end + if not found then + return + end + end + self.plot:setHighlighted(self == selectedTrigger) + self.plot:draw(map) +end + +--- Is the trigger part of the given object? +function CpTrigger:isTheSameObject(otherObject) + if self.trigger:getTarget().owningPlaceable == otherObject then + return true + end +end diff --git a/translations/translation_br.xml b/translations/translation_br.xml index 5ddb82098..944682db2 100644 --- a/translations/translation_br.xml +++ b/translations/translation_br.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -816,6 +822,16 @@ A carreta pode ser descarregado usando Courseplay Para iniciar, o marcador de carregamento no menu AI precisa ser definido. O marcador detecta montes ou silos e os destaca no mapa. Agora é possível iniciar o auxiliar. +"/> + + + + diff --git a/translations/translation_cs.xml b/translations/translation_cs.xml index 35d8fd82a..d5940bfa2 100644 --- a/translations/translation_cs.xml +++ b/translations/translation_cs.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -800,6 +806,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_ct.xml b/translations/translation_ct.xml index de972a2f7..f228f09fd 100644 --- a/translations/translation_ct.xml +++ b/translations/translation_ct.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -799,6 +805,16 @@ Courseplay能夠在青貯筒倉中壓實或分佈糠秕,或在楔形筒倉中 要啟動AI選單上的裝載標記,需要進行設定。 這個標記可以檢測到堆或筒倉,並在地圖上突出顯示它們。 那麼現在可以啟用小幫手了。 +"/> + + + + diff --git a/translations/translation_cz.xml b/translations/translation_cz.xml index 865c96a0c..0054bb17a 100644 --- a/translations/translation_cz.xml +++ b/translations/translation_cz.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -797,6 +803,16 @@ Nakladač může být vyložen pomocí CP. Chcete-li spustit nakládací značku v nabídce AI, je třeba ji nastavit. Značka detekuje hromady nebo silážní jámy a zvýrazňuje je na mapě. Nyní je možné spustit pracovníka. +"/> + + + + diff --git a/translations/translation_da.xml b/translations/translation_da.xml index 78d396796..4a0da7b09 100644 --- a/translations/translation_da.xml +++ b/translations/translation_da.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -810,6 +816,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_de.xml b/translations/translation_de.xml index 920aaac57..2a15b5188 100644 --- a/translations/translation_de.xml +++ b/translations/translation_de.xml @@ -25,6 +25,7 @@ + @@ -92,7 +93,10 @@ - + + + + @@ -177,6 +181,8 @@ + + @@ -823,6 +829,41 @@ Bedenkt dabei, dass das automatische Abtanken mit CP in Fahrtrichtung des Laders Als erstes muss auf der Helferkarte der Lade-Marker gesetzt werden. Dieses erkennt vorhandende Haufen oder Silos und markiert den ausgewählten. Anschließend kann der Helfer gestartet werden. +"/> + + + + diff --git a/translations/translation_ea.xml b/translations/translation_ea.xml index 28bdebd77..858cb1456 100644 --- a/translations/translation_ea.xml +++ b/translations/translation_ea.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -809,6 +815,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_en.xml b/translations/translation_en.xml index 056b09749..b84ef98c0 100644 --- a/translations/translation_en.xml +++ b/translations/translation_en.xml @@ -25,6 +25,7 @@ + @@ -92,7 +93,10 @@ - + + + + @@ -177,6 +181,8 @@ + + @@ -823,6 +829,43 @@ Keep in mind that the automatic unloading with CP goes in the same direction as To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_es.xml b/translations/translation_es.xml index e8639b195..921039eb2 100644 --- a/translations/translation_es.xml +++ b/translations/translation_es.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -808,6 +814,16 @@ El cargador se puede descargar con un descargador de Courseplay. Para iniciar el marcador de carga, en el menú AI debe configurarse. El marcador detecta montones o silos y los resalta en el mapa. Ahora es posible iniciar el ayudante. +"/> + + + + diff --git a/translations/translation_fc.xml b/translations/translation_fc.xml index f5dfdcdfd..4c3014c70 100644 --- a/translations/translation_fc.xml +++ b/translations/translation_fc.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -809,6 +815,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_fi.xml b/translations/translation_fi.xml index 18a5c3445..cbf81baeb 100644 --- a/translations/translation_fi.xml +++ b/translations/translation_fi.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -809,6 +815,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_fr.xml b/translations/translation_fr.xml index cad16a88e..b1c0c8bfb 100644 --- a/translations/translation_fr.xml +++ b/translations/translation_fr.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -788,6 +794,16 @@ La chargeuse peut être vidée par une tâche de déchargement Courseplay. + + + + diff --git a/translations/translation_hu.xml b/translations/translation_hu.xml index b21bfd0a1..13002836c 100644 --- a/translations/translation_hu.xml +++ b/translations/translation_hu.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -806,6 +812,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_it.xml b/translations/translation_it.xml index 6db335ca6..6e23a5e16 100644 --- a/translations/translation_it.xml +++ b/translations/translation_it.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -812,6 +818,16 @@ Il caricatore può essere scaricato con uno scaricatore Courseplay. Per avviare l'indicatore di caricamento nel menu AI è necessario impostare. Il marcatore rileva cumuli o silos e li evidenzia sulla mappa. Ora è possibile avviare l'helper. +"/> + + + + diff --git a/translations/translation_jp.xml b/translations/translation_jp.xml index cb2541d09..a5720e9bf 100644 --- a/translations/translation_jp.xml +++ b/translations/translation_jp.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -806,6 +812,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_kr.xml b/translations/translation_kr.xml index 45800dfbb..cf23f5518 100644 --- a/translations/translation_kr.xml +++ b/translations/translation_kr.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -809,6 +815,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_nl.xml b/translations/translation_nl.xml index 62b0470bb..5efc6fbe4 100644 --- a/translations/translation_nl.xml +++ b/translations/translation_nl.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -806,6 +812,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_no.xml b/translations/translation_no.xml index 07bb8d178..7a0f717f4 100644 --- a/translations/translation_no.xml +++ b/translations/translation_no.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -809,6 +815,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_pl.xml b/translations/translation_pl.xml index 4fb5e7f30..ac53e7c7b 100644 --- a/translations/translation_pl.xml +++ b/translations/translation_pl.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -804,6 +810,16 @@ Załadunkowy możne zostać rozładowany poprzez rozładunkowego Courseplay. Aby rozpocząć załadunek, punkt załadunkowy musi zostać ustawiony w menu SI. Znacznik wykrywa pryzmę lub silos i podświetla to na mapie. Terze możesz rozpocząć pracę pomocnika. +"/> + + + + diff --git a/translations/translation_pt.xml b/translations/translation_pt.xml index 765e19c9e..a767432e6 100644 --- a/translations/translation_pt.xml +++ b/translations/translation_pt.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -801,6 +807,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_ro.xml b/translations/translation_ro.xml index 976b29198..fb5282a21 100644 --- a/translations/translation_ro.xml +++ b/translations/translation_ro.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -809,6 +815,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + + diff --git a/translations/translation_ru.xml b/translations/translation_ru.xml index b2c114421..79d07c5dd 100644 --- a/translations/translation_ru.xml +++ b/translations/translation_ru.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -809,6 +815,16 @@ Courseplay способен прессовать или распределять Чтобы начать необходимо установить погрузочный маркер в меню ИИ. Маркер обнаруживает кучи или бункеры и выделяет их на карте. Теперь можно запускать помощника. +"/> + + + + diff --git a/translations/translation_sv.xml b/translations/translation_sv.xml index 63bbb2930..16b167639 100644 --- a/translations/translation_sv.xml +++ b/translations/translation_sv.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -804,6 +810,16 @@ Lastaren kan lossas med en Courseplay-avlastare. För att starta laddningsmarkören på AI-menyn måste ställas in. Markören upptäcker högar eller silos och markerar dem på kartan. Nu är det möjligt att starta hjälparen. +"/> + + + + diff --git a/translations/translation_tr.xml b/translations/translation_tr.xml index 12c65739d..74aba105c 100644 --- a/translations/translation_tr.xml +++ b/translations/translation_tr.xml @@ -25,6 +25,7 @@ + @@ -93,6 +94,9 @@ + + + @@ -177,6 +181,8 @@ + + @@ -806,6 +812,16 @@ The loader can be unloaded with an Courseplay unloader. To start the loading-marker on the AI menu needs to be set. The marker detects heaps or silos and highlights them on the map. Now it's possible to start the helper. +"/> + + + +