574 | function AISystem:addObstacle(node, centerOffsetX, centerOffsetY, centerOffsetZ, sizeX, sizeY, sizeZ, brakeAcceleration) |
575 | if self.isServer and self.navigationMap ~= nil and node ~= nil then |
576 | centerOffsetX = centerOffsetX or 0 |
577 | centerOffsetY = centerOffsetY or 0 |
578 | centerOffsetZ = centerOffsetZ or 0 |
579 | sizeX = sizeX or 0 |
580 | sizeY = sizeY or 0 |
581 | sizeZ = sizeZ or 0 |
582 | brakeAcceleration = brakeAcceleration or 0 |
583 | |
584 | addVehicleNavigationPhysicsObstacle(self.navigationMap, node, centerOffsetX, centerOffsetY, centerOffsetZ, sizeX, sizeY, sizeZ, brakeAcceleration) |
585 | end |
586 | end |
180 | function AISystem:addRoadSpline(spline, maxWidth, maxTurningRadius) |
181 | if self.isServer and spline ~= nil then |
182 | if self.navigationMap ~= nil then |
183 | local defaultMaxWidth = maxWidth or self.defaultVehicleMaxWidth |
184 | local defaultMaxTurningRadius = maxTurningRadius or self.defaultVehicleMaxTurningRadius |
185 | addRoadsToVehicleNavigationMap(self.navigationMap, spline, defaultMaxWidth, defaultMaxTurningRadius) |
186 | table.addElement(self.roadSplines, spline) |
187 | |
188 | -- directly set splines visible, e.g. if placeable was newly placed or reloaded while spline debug active |
189 | if self.splinesVisible then |
190 | setVisibility(spline, self.splinesVisible) |
191 | I3DUtil.interateRecursively(spline, function(node) |
192 | setVisibility(node, self.splinesVisible) |
193 | end) |
194 | end |
195 | else |
196 | table.addElement(self.delayedRoadSplines, spline) |
197 | end |
198 | end |
199 | end |
937 | function AISystem:consoleCommandAICheckSplineInterference(stepLength, heightOffset, defaultSplineWidth) |
938 | local usage = "gsAISplineCheckInterference [stepLength] [heightOffset] [defaultSplineWidth]\nUse 'gsDebugManagerClearElements' to remove boxes." |
939 | |
940 | self.debugLastPos = {} |
941 | self:debugDeleteInterferences() |
942 | self.debugInterferencePositions = {} |
943 | self.debugInterferences = {} |
944 | |
945 | stepLength = tonumber(stepLength) or 5 |
946 | heightOffset = tonumber(heightOffset) or 0.2 |
947 | defaultSplineWidth = tonumber(defaultSplineWidth) or self.defaultVehicleMaxWidth |
948 | local collisionMask = CollisionMask.ALL - CollisionFlag.TERRAIN |
949 | local overlapFactor = 0.9 -- slightly overlap to reduce change of unchecked areas in curves |
950 | |
951 | Logging.info("Checking interference: stepLength=%.2f heightOffset=%.2f defaultSplineWidth=%.2f", stepLength, heightOffset, defaultSplineWidth) |
952 | |
953 | local numSplines = 0 |
954 | local testPosition = createTransformGroup("testPosition") |
955 | local splineMaxWidth -- save spline max width as they might be inherited |
956 | |
957 | local function checkInterference(spline) |
958 | numSplines = numSplines + 1 |
959 | local splineWidth = getUserAttribute(spline, "maxWidth") or splineMaxWidth or defaultSplineWidth |
960 | local splineLength = getSplineLength(spline) |
961 | local stepSize = stepLength*overlapFactor / splineLength |
962 | for offset=0, 1+stepSize, stepSize do |
963 | local clampedSplineTime = MathUtil.clamp(offset, 0, 1) |
964 | local wx, wy, wz = getSplinePosition(spline, clampedSplineTime) |
965 | local dx, dy, dz = getSplineDirection(spline, clampedSplineTime) |
966 | setWorldTranslation(testPosition, wx, wy, wz) |
967 | setDirection(testPosition, dx, dy, dz, 0, 1, 0) |
968 | |
969 | local rx, ry, rz = getWorldRotation(testPosition) |
970 | local sx, sy, sz = splineWidth/2, (self.vehicleMaxHeight/2)-heightOffset/2, stepLength/2 |
971 | wy = wy + sy + heightOffset -- offset height by extent and 20 cm to avoid clipping with bridges |
972 | |
973 | self.debugLastPos = { |
974 | rx=rx, ry=ry, rz=rz, |
975 | sx=sx, sy=sy, sz=sz, |
976 | wx=wx, wy=wy, wz=wz, |
977 | spline=spline, |
978 | } |
979 | overlapBox(wx, wy, wz, rx, ry, rz, sx, sy, sz, "splineInterferenceOverlapCallback", self, collisionMask, true, true, true, false) |
980 | end |
981 | end |
982 | |
983 | local function filterSplines(node, depth) |
984 | if I3DUtil.getIsSpline(node) then |
985 | checkInterference(node) |
986 | else |
987 | -- save spline index for transform groups as it's inherited to child splines |
988 | splineMaxWidth = getUserAttribute(node, "maxWidth") |
989 | end |
990 | end |
991 | |
992 | for _, aiSpline in ipairs(self.roadSplines) do |
993 | I3DUtil.interateRecursively(aiSpline, filterSplines) |
994 | end |
995 | |
996 | delete(testPosition) |
997 | local numInterferences = table.size(self.debugInterferences) |
998 | self.debugInterferences = nil |
999 | self.debugLastPos = nil |
1000 | |
1001 | return string.format("Checked %d splines, found %d interferences\n%s", numSplines, numInterferences, usage) |
1002 | end |
867 | function AISystem:consoleCommandAICostmapExport(imageFormatStr) |
868 | if not self.isServer then |
869 | return "gsAICostsExport is a server-only command" |
870 | end |
871 | |
872 | if g_currentMission.missionInfo.savegameDirectory == nil then |
873 | return "Error: Savegame directory does not exist yet, please save the game first" |
874 | end |
875 | |
876 | local imageFormat = BitmapUtil.FORMAT.PIXELMAP |
877 | if imageFormatStr ~= nil then |
878 | imageFormat = BitmapUtil.FORMAT[imageFormatStr:upper()] |
879 | if imageFormat == nil then |
880 | Logging.error("Unknown image format '%s'. Available formats: %s", imageFormatStr, table.concatKeys(BitmapUtil.FORMAT, ", ")) |
881 | return "Error: Costmap export failed" |
882 | end |
883 | end |
884 | |
885 | local terrainSizeHalf = self.mission.terrainSize * 0.5 |
886 | local cellSizeHalf = self.cellSizeMeters / 2 |
887 | local imageSize = self.mission.terrainSize |
888 | |
889 | local isGreymap = imageFormat == BitmapUtil.FORMAT.GREYMAP |
890 | local colorBlocking = self.debug.colors.blocking |
891 | |
892 | local function getPixelsIterator() |
893 | local stepZ = -terrainSizeHalf + cellSizeHalf |
894 | local stepX = -terrainSizeHalf + cellSizeHalf |
895 | |
896 | local pixel = {0, 0, 0} |
897 | |
898 | return function() |
899 | if stepZ > (terrainSizeHalf - cellSizeHalf) then |
900 | return nil |
901 | end |
902 | |
903 | local worldPosY = getTerrainHeightAtWorldPos(self.mission.terrainRootNode, stepX, 0, stepZ) |
904 | local cost, isBlocking = getVehicleNavigationMapCostAtWorldPos(self.navigationMap, stepX, worldPosY, stepZ) |
905 | |
906 | if isGreymap then |
907 | pixel[1] = cost |
908 | else |
909 | if isBlocking then |
910 | pixel[1], pixel[2], pixel[3] = colorBlocking[1], colorBlocking[2], colorBlocking[3] |
911 | else |
912 | pixel[1], pixel[2], pixel[3] = Utils.getGreenRedBlendedColor(cost / AISystem.COSTMAP_MAX_VALUE) |
913 | end |
914 | pixel[1], pixel[2], pixel[3] = pixel[1]*255, pixel[2]*255, pixel[3]*255 |
915 | end |
916 | |
917 | if stepX < (terrainSizeHalf - self.cellSizeMeters) then |
918 | stepX = stepX + self.cellSizeMeters |
919 | else |
920 | stepX = -terrainSizeHalf + cellSizeHalf |
921 | stepZ = stepZ + self.cellSizeMeters |
922 | end |
923 | |
924 | return pixel |
925 | end |
926 | end |
927 | |
928 | if not BitmapUtil.writeBitmapToFileFromIterator(getPixelsIterator, imageSize, imageSize, g_currentMission.missionInfo.savegameDirectory .. "/navigationMap", imageFormat) then |
929 | return "Error: Costmap export failed" |
930 | end |
931 | |
932 | return "Finished costmap export" |
933 | end |
671 | function AISystem:consoleCommandAISetLastTarget() |
672 | if not self.isServer then |
673 | return "gsAISetLastTarget is a server-only command" |
674 | end |
675 | |
676 | if self.debug.target ~= nil then |
677 | local x = self.debug.target.x |
678 | local y = self.debug.target.y |
679 | local z = self.debug.target.z |
680 | local dirX = self.debug.target.dirX |
681 | local dirY = self.debug.target.dirY |
682 | local dirZ = self.debug.target.dirZ |
683 | |
684 | local marker = self.debug.marker |
685 | if marker ~= nil then |
686 | setWorldTranslation(marker, x, y, z) |
687 | setDirection(marker, dirX, dirY, dirZ, 0, 1, 0) |
688 | end |
689 | return "Set target to last target" |
690 | else |
691 | return "No last target found" |
692 | end |
693 | end |
624 | function AISystem:consoleCommandAISetTarget(offsetX, offsetZ) |
625 | if not self.isServer then |
626 | return "gsAISetTarget is a server-only command" |
627 | end |
628 | |
629 | local x, y, z = 0, 0, 0 |
630 | local dirX, dirY, dirZ, _ = 1, 0, 0, nil |
631 | if self.mission.controlPlayer then |
632 | if self.mission.player ~= nil and self.mission.player.isControlled and self.mission.player.rootNode ~= nil and self.mission.player.rootNode ~= 0 then |
633 | x, y, z = getWorldTranslation(self.mission.player.rootNode) |
634 | dirX, dirZ = -math.sin(self.mission.player.rotY), -math.cos(self.mission.player.rotY) |
635 | end |
636 | elseif self.mission.controlledVehicle ~= nil then |
637 | x, y, z = getWorldTranslation(self.mission.controlledVehicle.rootNode) |
638 | dirX, _, dirZ = localDirectionToWorld(self.mission.controlledVehicle.rootNode, 0, 0, 1) |
639 | else |
640 | x, y, z = getWorldTranslation(getCamera()) |
641 | dirX, _, dirZ = localDirectionToWorld(getCamera(), 0, 0, -1) |
642 | end |
643 | |
644 | local normX, _, normZ = MathUtil.crossProduct(0, 1, 0, dirX, dirY, dirZ) |
645 | |
646 | offsetX = tonumber(offsetX) or 0 |
647 | offsetZ = tonumber(offsetZ) or 0 |
648 | |
649 | x = x + dirX * offsetZ + normX * offsetX |
650 | z = z + dirZ * offsetZ + normZ * offsetX |
651 | |
652 | self.debug.target = {} |
653 | self.debug.target.x = x |
654 | self.debug.target.y = y |
655 | self.debug.target.z = z |
656 | self.debug.target.dirX = dirX |
657 | self.debug.target.dirY = dirY |
658 | self.debug.target.dirZ = dirZ |
659 | |
660 | local marker = self.debug.marker |
661 | if marker ~= nil then |
662 | setWorldTranslation(marker, x, y, z) |
663 | setDirection(marker, dirX, dirY, dirZ, 0, 1, 0) |
664 | end |
665 | |
666 | return "Set AI Target" |
667 | end |
697 | function AISystem:consoleCommandAIStart() |
698 | if not self.isServer then |
699 | return "Only available on server" |
700 | end |
701 | |
702 | if self.mission.controlledVehicle == nil then |
703 | return "Please enter a vehicle first" |
704 | end |
705 | |
706 | local target = self.debug.target |
707 | if target == nil then |
708 | return "Please set a target first" |
709 | end |
710 | |
711 | local job = g_currentMission.aiJobTypeManager:createJob(AIJobType.GOTO) |
712 | local angle = MathUtil.getYRotationFromDirection(target.dirX, target.dirZ) |
713 | |
714 | job.vehicleParameter:setVehicle(self.mission.controlledVehicle) |
715 | job.positionAngleParameter:setPosition(target.x, target.z) |
716 | job.positionAngleParameter:setAngle(angle) |
717 | job:setValues() |
718 | |
719 | local success, errorMessage = job:validate(g_currentMission.player.farmId) |
720 | if success then |
721 | self:startJob(job, g_currentMission.player.farmId) |
722 | return "Started ai..." |
723 | else |
724 | return "Error: " .. tostring(errorMessage) |
725 | end |
726 | end |
797 | function AISystem:consoleCommandAIToggleAINodeDebug() |
798 | self.stationsAINodesVisible = not self.stationsAINodesVisible |
799 | |
800 | if self.stationsAINodesVisible then |
801 | self.stationsAINodesDebugElements = {} |
802 | |
803 | for _, unloadingStation in pairs(g_currentMission.storageSystem:getUnloadingStations()) do |
804 | if unloadingStation:isa(UnloadingStation) then |
805 | local x, _, _, _, unloadTrigger = unloadingStation:getAITargetPositionAndDirection(FillType.UNKNOWN) |
806 | if x ~= nil then |
807 | local text = "UnloadingStation: " .. unloadingStation:getName() |
808 | local gizmo = DebugGizmo.new():createWithNode(unloadTrigger.aiNode, text, nil, nil, nil, false, true) |
809 | g_debugManager:addPermanentElement(gizmo) |
810 | table.insert(self.stationsAINodesDebugElements, gizmo) |
811 | end |
812 | end |
813 | end |
814 | |
815 | for _, loadingStation in pairs(g_currentMission.storageSystem:getLoadingStations()) do |
816 | local x, _, _, _, loadTrigger = loadingStation:getAITargetPositionAndDirection(FillType.UNKNOWN) |
817 | if x ~= nil then |
818 | local text = "LoadingStation: " .. loadingStation:getName() |
819 | local gizmo = DebugGizmo.new():createWithNode(loadTrigger.aiNode, text, nil, nil, nil, false, true) |
820 | g_debugManager:addPermanentElement(gizmo) |
821 | table.insert(self.stationsAINodesDebugElements, gizmo) |
822 | end |
823 | end |
824 | else |
825 | for _, gizmo in pairs(self.stationsAINodesDebugElements) do |
826 | g_debugManager:removePermanentElement(gizmo) |
827 | gizmo:delete() |
828 | end |
829 | self.stationsAINodesDebugElements = nil |
830 | end |
831 | |
832 | if self.stationsAINodesVisible then |
833 | Logging.warning("Nodes in reloaded placeables are not updated automatically. Toggle this command again to update the station nodes if placeables were reloaded.") |
834 | end |
835 | return "AISystem.stationsAINodesVisible=" .. tostring(self.stationsAINodesVisible) |
836 | end |
737 | function AISystem:consoleCommandAIToggleSplineVisibility() |
738 | self.splinesVisible = not self.splinesVisible |
739 | |
740 | if self.splinesVisible then |
741 | self.splineDebugElements = {} |
742 | local debugMat = g_debugManager:getDebugMat() |
743 | |
744 | -- collect all splines from AI system |
745 | for _, roadSplineOrTG in ipairs(self.roadSplines) do |
746 | if I3DUtil.getIsSpline(roadSplineOrTG) then |
747 | self.splineDebugElements[roadSplineOrTG] = true |
748 | end |
749 | |
750 | -- iterate rec as node might be a TG with splines as children |
751 | I3DUtil.interateRecursively(roadSplineOrTG, function(node) |
752 | if I3DUtil.getIsSpline(node) then |
753 | self.splineDebugElements[node] = true |
754 | end |
755 | end) |
756 | end |
757 | |
758 | -- set visible and create debug elements |
759 | for spline in pairs(self.splineDebugElements) do |
760 | DebugUtil.setNodeEffectivelyVisible(spline) |
761 | local r, g, b = unpack(DebugUtil.getDebugColor(spline)) |
762 | setMaterial(spline, debugMat, 0) |
763 | setShaderParameter(spline, "color", r, g, b, 0, false) |
764 | setShaderParameter(spline, "alpha", 1, 0, 0, 0, false) |
765 | |
766 | local debugElements = DebugUtil.getSplineDebugElements(spline) |
767 | self.splineDebugElements[spline] = debugElements |
768 | for elemName, debugElement in pairs(debugElements) do |
769 | if elemName == "splineAttributes" then |
770 | debugElement:setColor(r, g, b) |
771 | end |
772 | g_debugManager:addPermanentElement(debugElement) |
773 | end |
774 | end |
775 | else |
776 | -- remove existing debug elements |
777 | for spline, debugElements in pairs(self.splineDebugElements) do |
778 | for _, debugElement in pairs(debugElements) do |
779 | g_debugManager:removePermanentElement(debugElement) |
780 | end |
781 | if entityExists(spline) then |
782 | setVisibility(spline, false) |
783 | end |
784 | end |
785 | self.splineDebugElements = nil |
786 | end |
787 | |
788 | if g_currentMission.trafficSystem ~= nil and g_currentMission.trafficSystem.rootNodeId ~= nil then |
789 | setVisibility(g_currentMission.trafficSystem.rootNodeId, self.splinesVisible) |
790 | end |
791 | |
792 | return "AISystem.splinesVisible=" .. tostring(self.splinesVisible) |
793 | end |
67 | function AISystem:delete() |
68 | --#debug log("AISystem:delete()") |
69 | if self.isServer then |
70 | for i=#self.activeJobs, 1, -1 do |
71 | local job = self.activeJobs[i] |
72 | self:stopJob(job, AIMessageErrorUnknown.new()) |
73 | end |
74 | end |
75 | |
76 | if self.navigationMap ~= nil then |
77 | delete(self.navigationMap) |
78 | self.navigationMap = nil |
79 | end |
80 | |
81 | if self.debug ~= nil and self.debug.marker ~= nil then |
82 | delete(self.debug.marker) |
83 | end |
84 | |
85 | self.mission:unregisterObjectToCallOnMissionStart(self) |
86 | |
87 | removeConsoleCommand("gsAISetTarget") |
88 | removeConsoleCommand("gsAISetLastTarget") |
89 | removeConsoleCommand("gsAIStart") |
90 | removeConsoleCommand("gsAIEnableDebug") |
91 | removeConsoleCommand("gsAISplinesShow") |
92 | removeConsoleCommand("gsAISplinesCheckInterference") |
93 | removeConsoleCommand("gsAIStationsShow") |
94 | removeConsoleCommand("gsAIObstaclesShow") |
95 | removeConsoleCommand("gsAICostsShow") |
96 | removeConsoleCommand("gsAICostsUpdate") |
97 | removeConsoleCommand("gsAICostsExport") |
98 | end |
525 | function AISystem:draw() |
526 | if self.debug.isCostRenderingActive then |
527 | local x,_,z = getWorldTranslation(getCamera(0)) |
528 | if self.mission.controlledVehicle ~= nil then |
529 | local object = self.mission.controlledVehicle |
530 | if self.mission.controlledVehicle.selectedImplement ~= nil then |
531 | object = self.mission.controlledVehicle.selectedImplement.object |
532 | end |
533 | x,_,z = getWorldTranslation(object.components[1].node) |
534 | end |
535 | |
536 | local cellSizeHalf = self.cellSizeMeters * 0.5 |
537 | x = (math.floor(x / self.cellSizeMeters) * self.cellSizeMeters) + cellSizeHalf |
538 | z = (math.floor(z / self.cellSizeMeters) * self.cellSizeMeters) + cellSizeHalf |
539 | |
540 | local range = 15 * self.cellSizeMeters |
541 | local terrainSizeHalf = self.mission.terrainSize * 0.5 |
542 | local minX = math.max(x - range, -terrainSizeHalf + cellSizeHalf) |
543 | local minZ = math.max(z - range, -terrainSizeHalf + cellSizeHalf) |
544 | local maxX = math.min(x + range, terrainSizeHalf - cellSizeHalf) |
545 | local maxZ = math.min(z + range, terrainSizeHalf - cellSizeHalf) |
546 | |
547 | for stepZ = minZ, maxZ, self.cellSizeMeters do |
548 | for stepX = minX, maxX, self.cellSizeMeters do |
549 | |
550 | local worldPosX = stepX |
551 | local worldPosY = getTerrainHeightAtWorldPos(self.mission.terrainRootNode, stepX, 0, stepZ) |
552 | local worldPosZ = stepZ |
553 | |
554 | local cost, isBlocking = getVehicleNavigationMapCostAtWorldPos(self.navigationMap, worldPosX, worldPosY, worldPosZ) |
555 | local color = self.debug.colors.default |
556 | if isBlocking then |
557 | color = self.debug.colors.blocking |
558 | else |
559 | local r, g, b = Utils.getGreenRedBlendedColor(cost / AISystem.COSTMAP_MAX_VALUE) |
560 | |
561 | color[1] = r |
562 | color[2] = g |
563 | color[3] = b |
564 | end |
565 | |
566 | Utils.renderTextAtWorldPosition(worldPosX, worldPosY, worldPosZ, string.format("%.1f", cost), getCorrectTextSize(0.015), 0, color) |
567 | end |
568 | end |
569 | end |
570 | end |
260 | function AISystem:loadFromXMLFile(xmlFilename) |
261 | local xmlFile = XMLFile.load("aiSystemXML", xmlFilename) |
262 | |
263 | local x = xmlFile:getFloat("aiSystem.debug.target#posX") |
264 | local y = xmlFile:getFloat("aiSystem.debug.target#posY") |
265 | local z = xmlFile:getFloat("aiSystem.debug.target#posZ") |
266 | local dirX = xmlFile:getFloat("aiSystem.debug.target#dirX") |
267 | local dirY = xmlFile:getFloat("aiSystem.debug.target#dirY") |
268 | local dirZ = xmlFile:getFloat("aiSystem.debug.target#dirZ") |
269 | |
270 | if x ~= nil and y ~= nil and z ~= nil and dirX ~= nil and dirY ~= nil and dirZ ~= nil then |
271 | self.debug.target = {} |
272 | self.debug.target.x = x |
273 | self.debug.target.y = y |
274 | self.debug.target.z = z |
275 | self.debug.target.dirX = dirX |
276 | self.debug.target.dirY = dirY |
277 | self.debug.target.dirZ = dirZ |
278 | end |
279 | |
280 | xmlFile:delete() |
281 | end |
103 | function AISystem:loadMapData(xmlFile, missionInfo, baseDirectory) |
104 | if g_addCheatCommands then |
105 | if self.isServer then |
106 | addConsoleCommand("gsAISetTarget", "Sets AI Target", "consoleCommandAISetTarget", self) |
107 | addConsoleCommand("gsAISetLastTarget", "Sets AI Target to last position", "consoleCommandAISetLastTarget", self) |
108 | addConsoleCommand("gsAIStart", "Starts driving to target", "consoleCommandAIStart", self) |
109 | end |
110 | addConsoleCommand("gsAIEnableDebug", "Enables AI debugging", "consoleCommandAIEnableDebug", self) |
111 | addConsoleCommand("gsAISplinesShow", "Toggle AI system spline visibility", "consoleCommandAIToggleSplineVisibility", self) |
112 | addConsoleCommand("gsAISplinesCheckInterference", "Check if AI splines interfere with any objects", "consoleCommandAICheckSplineInterference", self) |
113 | addConsoleCommand("gsAIStationsShow", "Toggle AI system stations ai nodes visibility", "consoleCommandAIToggleAINodeDebug", self) |
114 | addConsoleCommand("gsAIObstaclesShow", "Shows the obstacles around the camera", "consoleCommandAIShowObstacles", self) |
115 | addConsoleCommand("gsAICostsShow", "Shows the costs per cell", "consoleCommandAIShowCosts", self) |
116 | addConsoleCommand("gsAICostsUpdate", "Update costmap given width around the camera", "consoleCommandAISetAreaDirty", self) |
117 | addConsoleCommand("gsAICostsExport", "Export costmap to image file", "consoleCommandAICostmapExport", self) |
118 | end |
119 | |
120 | self.cellSizeMeters = 1 |
121 | self.maxSlopeAngle = math.rad(15) |
122 | self.infoLayerName = "navigationCollision" |
123 | self.infoLayerChannel = 0 |
124 | self.aiDrivableCollisionMask = CollisionFlag.AI_DRIVABLE |
125 | self.obstacleCollisionMask = CollisionFlag.STATIC_OBJECTS + CollisionFlag.AI_BLOCKING + CollisionFlag.STATIC_OBJECT |
126 | self.vehicleMaxHeight = 4 |
127 | self.defaultVehicleMaxWidth = 6 |
128 | self.defaultVehicleMaxTurningRadius = 20 -- TODO |
129 | self.isLeftHandTraffic = false |
130 | |
131 | -- load map specific xml with custom settings |
132 | local relFilename = getXMLString(xmlFile, "map.aiSystem#filename") |
133 | if relFilename ~= nil then |
134 | local filepath = Utils.getFilename(relFilename, baseDirectory) |
135 | if filepath ~= nil then |
136 | local xmlFileAISystem = XMLFile.load("mapAISystem", filepath, AISystem.xmlSchema) |
137 | if xmlFileAISystem ~= nil then |
138 | |
139 | self.maxSlopeAngle = xmlFileAISystem:getValue("aiSystem.maxSlopeAngle") or self.maxSlopeAngle |
140 | self.infoLayerName = xmlFileAISystem:getValue("aiSystem.blockedAreaInfoLayer#name") or self.infoLayerName |
141 | self.infoLayerChannel = xmlFileAISystem:getValue("aiSystem.blockedAreaInfoLayer#channel") or self.infoLayerChannel |
142 | self.vehicleMaxHeight = xmlFileAISystem:getValue("aiSystem.vehicleMaxHeight") or self.vehicleMaxHeight |
143 | self.isLeftHandTraffic = Utils.getNoNil(xmlFileAISystem:getValue("aiSystem.isLeftHandTraffic"), self.isLeftHandTraffic) |
144 | |
145 | xmlFileAISystem:delete() |
146 | end |
147 | end |
148 | end |
149 | |
150 | self.debugEnabled = g_isDevelopmentVersion |
151 | self.debug = {} |
152 | self.debug.target = nil |
153 | self.debug.isCostRenderingActive = false |
154 | self.debug.colors = {} |
155 | self.debug.colors.default = {0, 1, 0, 1} |
156 | self.debug.colors.blocking = {1, 0, 0, 1} |
157 | |
158 | self.activeJobs = {} |
159 | self.jobsToRemove = {} |
160 | |
161 | self.roadSplines = {} |
162 | |
163 | g_i3DManager:loadI3DFileAsync("data/shared/aiMarker.i3d", false, false, self.onAIMarkerLoaded, self, nil) |
164 | end |
217 | function AISystem:onTerrainLoad(terrainNode) |
218 | if self.isServer then |
219 | self.navigationMap = createVehicleNavigationMap(self.cellSizeMeters, terrainNode, self.maxSlopeAngle, self.infoLayerName, self.infoLayerChannel, self.aiDrivableCollisionMask, self.obstacleCollisionMask, self.vehicleMaxHeight, self.isLeftHandTraffic) |
220 | |
221 | if self.mission.trafficSystem ~= nil then |
222 | local roadSplineRootNodeId = self.mission.trafficSystem.rootNodeId |
223 | self:addRoadSpline(roadSplineRootNodeId) |
224 | end |
225 | |
226 | for i=#self.delayedRoadSplines, 1, -1 do |
227 | local spline = table.remove(self.delayedRoadSplines, 1) |
228 | self:addRoadSpline(spline) |
229 | end |
230 | |
231 | local missionInfo = self.mission.missionInfo |
232 | local loadFromSave = false |
233 | if missionInfo.isValid and missionInfo:getIsNavigationCollisionValid(self.mission) then |
234 | local path = missionInfo.savegameDirectory .. "/" .. self.filename |
235 | local success = loadVehicleNavigationCostMapFromFile(self.navigationMap, path) |
236 | if success then |
237 | Logging.info("Loaded navigation cost map from savegame") |
238 | loadFromSave = true |
239 | end |
240 | end |
241 | |
242 | if not loadFromSave then |
243 | self.mission:registerObjectToCallOnMissionStart(self) |
244 | end |
245 | end |
246 | |
247 | return true |
248 | end |
57 | function AISystem:registerXMLPaths(schema) |
58 | schema:register(XMLValueType.ANGLE, "aiSystem.maxSlopeAngle", "Maximum terrain angle in degrees which is classified as drivable", 15) |
59 | schema:register(XMLValueType.STRING, "aiSystem.blockedAreaInfoLayer#name", "Map info layer name defining areas which are blocked for AI driving", "navigationCollision") |
60 | schema:register(XMLValueType.INT, "aiSystem.blockedAreaInfoLayer#channel", "Map info layer channel defining areas which are blocked for AI driving", 0) |
61 | schema:register(XMLValueType.FLOAT, "aiSystem.vehicleMaxHeight", "Maximum expected vehicle height used for generating the costmap, e.g. relevant for overhanging collisions", 4) |
62 | schema:register(XMLValueType.BOOL, "aiSystem.isLeftHandTraffic", "Map has left-hand traffic. This setting will only affect collision avoidance, traffic and ai splines need to be set up as left hand in map itself", false) |
63 | end |
285 | function AISystem:save(xmlFilename, usedModNames) |
286 | local xmlFile = XMLFile.create("aiSystemXML", xmlFilename, "aiSystem") |
287 | if xmlFile ~= nil then |
288 | local hasData = false |
289 | if self.debug.target ~= nil then |
290 | local target = self.debug.target |
291 | xmlFile:setFloat("aiSystem.debug.target#posX", target.x) |
292 | xmlFile:setFloat("aiSystem.debug.target#posY", target.y) |
293 | xmlFile:setFloat("aiSystem.debug.target#posZ", target.z) |
294 | xmlFile:setFloat("aiSystem.debug.target#dirX", target.dirX) |
295 | xmlFile:setFloat("aiSystem.debug.target#dirY", target.dirY) |
296 | xmlFile:setFloat("aiSystem.debug.target#dirZ", target.dirZ) |
297 | |
298 | hasData = true |
299 | end |
300 | |
301 | if hasData then |
302 | xmlFile:save() |
303 | end |
304 | |
305 | xmlFile:delete() |
306 | end |
307 | end |
1006 | function AISystem:splineInterferenceOverlapCallback(nodeId) |
1007 | if nodeId ~= 0 and nodeId ~= g_currentMission.terrainRootNode and not CollisionFlag.getHasFlagSet(nodeId, CollisionFlag.VEHICLE) and CollisionFlag.getHasFlagSet(nodeId, CollisionFlag.AI_BLOCKING) then |
1008 | local last = self.debugLastPos |
1009 | |
1010 | local customWidth = getUserAttribute(nodeId, 'maxWidth') |
1011 | local customWidthText = customWidth and string.format(" (maxWidth UserAttribute: %.2f)", customWidth) or "" |
1012 | |
1013 | Logging.info("found interference for spline '%s'%s with object '%s|%s' at %d %d %d", getName(last.spline), customWidthText, getName(getParent(nodeId)), getName(nodeId), last.wx, last.wy, last.wz) |
1014 | |
1015 | local debugFunctionPair = {DebugUtil.drawOverlapBox, {last.wx, last.wy, last.wz, last.rx, last.ry, last.rz, last.sx, last.sy, last.sz}} |
1016 | table.insert(self.debugInterferencePositions, debugFunctionPair) |
1017 | g_debugManager:addPermanentFunction(debugFunctionPair) |
1018 | |
1019 | self.debugInterferences[nodeId] = true |
1020 | end |
1021 | end |
323 | function AISystem:update(dt) |
324 | for i = #self.jobsToRemove, 1, -1 do |
325 | local job = self.jobsToRemove[i] |
326 | local jobId = job.jobId |
327 | |
328 | table.removeElement(self.activeJobs, job) |
329 | table.remove(self.jobsToRemove, i) |
330 | |
331 | g_messageCenter:publish(MessageType.AI_JOB_REMOVED, jobId) |
332 | end |
333 | |
334 | for _, job in ipairs(self.activeJobs) do |
335 | job:update(dt) |
336 | |
337 | if self.isServer and g_currentMission.isRunning then |
338 | local price = job:getPricePerMs() |
339 | if price > 0 then |
340 | local difficultyMultiplier = g_currentMission.missionInfo.buyPriceMultiplier |
341 | if GS_IS_MOBILE_VERSION then |
342 | difficultyMultiplier = difficultyMultiplier * 0.8 |
343 | end |
344 | |
345 | price = price * dt * difficultyMultiplier |
346 | |
347 | g_currentMission:addMoney(-price, job.startedFarmId, MoneyType.AI, true) |
348 | |
349 | local farm = g_farmManager:getFarmById(job.startedFarmId) |
350 | if farm ~= nil then |
351 | if farm:getBalance() + price < 0 then |
352 | self:stopJob(job, AIMessageErrorOutOfMoney.new()) |
353 | end |
354 | end |
355 | end |
356 | end |
357 | end |
358 | end |