68 | function AIVehicle:load(savegame) |
69 | |
70 | self.onStartAiVehicle = SpecializationUtil.callSpecializationsFunction("onStartAiVehicle"); |
71 | self.onStopAiVehicle = SpecializationUtil.callSpecializationsFunction("onStopAiVehicle"); |
72 | self.getAdditionalAIPrice = SpecializationUtil.callSpecializationsFunction("getAdditionalAIPrice"); |
73 | self.setAIConveyorBeltAngle = SpecializationUtil.callSpecializationsFunction("setAIConveyorBeltAngle"); |
74 | |
75 | self.getDeactivateOnLeave = Utils.overwrittenFunction(self.getDeactivateOnLeave, AIVehicle.getDeactivateOnLeave); |
76 | self.canStartAIVehicle = AIVehicle.canStartAIVehicle; |
77 | self.startAIVehicle = AIVehicle.startAIVehicle; |
78 | self.stopAIVehicle = AIVehicle.stopAIVehicle; |
79 | self.getVehicleData = AIVehicle.getVehicleData; |
80 | self.setDriveStrategies = AIVehicle.setDriveStrategies; |
81 | |
82 | |
83 | self.aiVehicleDirectionNode = self.steeringCenterNode; -- defined and created by ackermann steering |
84 | if self.aiVehicleDirectionNode == nil then |
85 | print("Warning: AIVehicle can't be loaded for "..tostring(self.configFileName)..", because the setup for Ackermann Steering is missing!"); |
86 | end |
87 | |
88 | self.aiIsStarted = false; |
89 | self.isAllowedToDrive = true; |
90 | |
91 | self.aiImplementList = {}; |
92 | self.aiToolsDirtyFlag = true; |
93 | |
94 | self.driveStrategies = {}; |
95 | |
96 | self.trafficCollisionIgnoreList = {}; |
97 | |
98 | self.didNotMoveTimeout = Utils.getNoNil( getXMLFloat(self.xmlFile, "vehicle.ai.didNotMoveTimeout#value"), 5000); |
99 | if getXMLBool(self.xmlFile, "vehicle.ai.didNotMoveTimeout#deactivated") then |
100 | self.didNotMoveTimeout = math.huge; |
101 | end; |
102 | |
103 | self.didNotMoveTimer = self.didNotMoveTimeout; |
104 | |
105 | -- |
106 | local aiLightState = Utils.getNoNil( getXMLInt(self.xmlFile, "vehicle.ai.lightState#index"), 3); |
107 | if self.lightStates ~= nil and #self.lightStates > 0 then |
108 | if self.lightStates[aiLightState] == nil then |
109 | aiLightState = 1; |
110 | end |
111 | self.aiLightsTypesMask = 0 |
112 | for _, lightType in pairs(self.lightStates[aiLightState]) do |
113 | self.aiLightsTypesMask = bitOR(self.aiLightsTypesMask, 2^lightType); |
114 | end |
115 | end |
116 | |
117 | -- used for visual debuging |
118 | self.debugTexts = {}; |
119 | self.debugLines = {}; |
120 | self.debugPoints = {}; |
121 | |
122 | self.pricePerMS = Utils.getNoNil(getXMLFloat(self.xmlFile, "vehicle.ai.pricePerHour"), 2000)/60/60/1000; |
123 | |
124 | self.isConveyorBelt = hasXMLProperty(self.xmlFile, "vehicle.ai.conveyorBelt"); |
125 | if self.isConveyorBelt then |
126 | self.aiConveyorBelt = {}; |
127 | self.aiConveyorBelt.minAngle = Utils.getNoNil(getXMLFloat(self.xmlFile, "vehicle.ai.conveyorBelt#minAngle"), 5); |
128 | self.aiConveyorBelt.maxAngle = Utils.getNoNil(getXMLFloat(self.xmlFile, "vehicle.ai.conveyorBelt#maxAngle"), 45); |
129 | self.aiConveyorBelt.stepSize = Utils.getNoNil(getXMLFloat(self.xmlFile, "vehicle.ai.conveyorBelt#stepSize"), 5); |
130 | self.aiConveyorBelt.currentAngle = self.aiConveyorBelt.minAngle; |
131 | |
132 | self.aiConveyorBelt.speed = Utils.getNoNil(getXMLFloat(self.xmlFile, "vehicle.ai.conveyorBelt#speed"), 1); |
133 | self.aiConveyorBelt.centerWheelIndex = Utils.getNoNil(getXMLInt(self.xmlFile, "vehicle.ai.conveyorBelt#centerWheelIndex"), 1); |
134 | self.aiConveyorBelt.backWheelIndex = Utils.getNoNil(getXMLInt(self.xmlFile, "vehicle.ai.conveyorBelt#backWheelIndex"), 2); |
135 | end; |
136 | |
137 | self.isHired = false; |
138 | self.isHirableBlocked = false; |
139 | end |
215 | function AIVehicle:update(dt) |
216 | |
217 | local activeForInput = not g_gui:getIsGuiVisible() and not g_currentMission.isPlayerFrozen and self.isEntered; |
218 | |
219 | if activeForInput and AIVehicle.aiDebugRendering then |
220 | if #self.debugTexts > 0 then |
221 | for i,text in pairs(self.debugTexts) do |
222 | renderText(0.7, 0.92-(0.02*i), 0.02, text); |
223 | end |
224 | end |
225 | if #self.debugLines > 0 then |
226 | for _,l in pairs(self.debugLines) do |
227 | drawDebugLine(l.s[1],l.s[2],l.s[3], l.c[1],l.c[2],l.c[3], l.e[1],l.e[2],l.e[3], l.c[1],l.c[2],l.c[3]); |
228 | end |
229 | end |
230 | end |
231 | |
232 | if activeForInput then |
233 | if InputBinding.hasEvent(InputBinding.TOGGLE_AI) and not g_currentMission.inGameMessage:getIsVisible() then |
234 | if g_currentMission:getHasPermission("hireAI") then |
235 | if self.aiIsStarted then |
236 | self:stopAIVehicle(AIVehicle.STOP_REASON_USER); |
237 | else |
238 | if self:canStartAIVehicle() then |
239 | self:startAIVehicle(nil, false); |
240 | end |
241 | end |
242 | end |
243 | end |
244 | if self.isConveyorBelt then |
245 | if InputBinding.hasEvent(InputBinding.IMPLEMENT_EXTRA3) and not self.aiIsStarted then |
246 | local newAngle = self.aiConveyorBelt.currentAngle + self.aiConveyorBelt.stepSize |
247 | if newAngle > self.aiConveyorBelt.maxAngle then |
248 | newAngle = self.aiConveyorBelt.minAngle; |
249 | end |
250 | |
251 | self:setAIConveyorBeltAngle(newAngle); |
252 | end |
253 | end; |
254 | end |
255 | |
256 | if self:getIsActive() then |
257 | if self.aiToolsDirtyFlag == true then |
258 | self.aiToolsDirtyFlag = false; |
259 | self:getVehicleData(); |
260 | end |
261 | end |
262 | |
263 | --# only run on server side? |
264 | if not self.isServer then |
265 | return; |
266 | end |
267 | |
268 | if self.aiIsStarted then |
269 | if self.driveStrategies ~= nil and #self.driveStrategies > 0 then |
270 | for i=1,#self.driveStrategies do |
271 | local driveStrategy = self.driveStrategies[i]; |
272 | driveStrategy:update(dt); |
273 | end |
274 | end |
275 | end |
276 | |
277 | if self.isHired then |
278 | self.forceIsActive = true; |
279 | self.stopMotorOnLeave = false; |
280 | self.steeringEnabled = false; |
281 | |
282 | -- check light and turn on dependent on daytime |
283 | if self.aiLightsTypesMask ~= nil then |
284 | |
285 | local dayMinutes = g_currentMission.environment.dayTime/(1000*60); |
286 | local needLights = (dayMinutes > (19*60) or dayMinutes < (6*60)); |
287 | |
288 | if needLights then |
289 | if self.lightsTypesMask ~= self.aiLightsTypesMask then |
290 | self:setLightsTypesMask(self.aiLightsTypesMask); |
291 | end |
292 | else |
293 | if self.lightsTypesMask ~= 0 then |
294 | self:setLightsTypesMask(0); |
295 | end |
296 | end |
297 | |
298 | end |
299 | |
300 | end; |
301 | end |
306 | function AIVehicle:updateTick(dt) |
307 | |
308 | --# only run on server side? |
309 | if not self.isServer then |
310 | return; |
311 | end |
312 | |
313 | if self.isHired and self.isServer and not self.isHirableBlocked then |
314 | local difficultyMultiplier = g_currentMission.missionInfo.buyPriceMultiplier; |
315 | g_currentMission:addSharedMoney(-dt*difficultyMultiplier*self.pricePerMS, "wagePayment"); |
316 | g_currentMission:addMoneyChange(-dt*difficultyMultiplier*self.pricePerMS, FSBaseMission.MONEY_TYPE_AI) |
317 | end; |
318 | |
319 | self.debugTexts = {}; |
320 | self.debugLines = {}; |
321 | |
322 | if self.aiIsStarted then |
323 | if self.driveStrategies ~= nil and #self.driveStrategies > 0 then |
324 | |
325 | local vX,vY,vZ = getWorldTranslation(self.aiVehicleDirectionNode); |
326 | |
327 | local tX, tZ, moveForwards, maxSpeed, distanceToStop; |
328 | for i=1,#self.driveStrategies do |
329 | local driveStrategy = self.driveStrategies[i]; |
330 | tX, tZ, moveForwards, maxSpeed, distanceToStop = driveStrategy:getDriveData(dt, vX,vY,vZ) |
331 | if tX ~= nil or not self.aiIsStarted then |
332 | break; |
333 | end |
334 | end |
335 | |
336 | if tX == nil then |
337 | if self.aiIsStarted then -- check if AI is till active, because it might have been kicked by a strategy |
338 | self:stopAIVehicle(AIVehicle.STOP_REASON_REGULAR); |
339 | end |
340 | return; |
341 | end |
342 | |
343 | local lx, lz = AIVehicleUtil.getDriveDirection(self.aiVehicleDirectionNode, tX, vY, tZ); |
344 | if not moveForwards then |
345 | lx, lz = -lx, -lz; |
346 | end |
347 | |
348 | local acceleration = 1.0; |
349 | |
350 | local minimumSpeed = 5; |
351 | local lookAheadDistance = 5; |
352 | |
353 | local distSpeed = math.max(minimumSpeed, maxSpeed * math.min(1, distanceToStop/lookAheadDistance)); |
354 | local speedLimit, _ = self:getSpeedLimit(); |
355 | maxSpeed = math.min(maxSpeed, distSpeed, speedLimit); |
356 | maxSpeed = math.min(maxSpeed, self.cruiseControl.speed); |
357 | |
358 | self.isAllowedToDrive = maxSpeed ~= 0; |
359 | |
360 | local pX,pY,pZ = worldToLocal(self.aiVehicleDirectionNode, tX,vY,tZ); |
361 | if not moveForwards and self.articulatedAxis ~= nil then |
362 | if self.articulatedAxis.aiRevereserNode ~= nil and self.aiToolReverserDirectionNode == nil then |
363 | pX,pY,pZ = worldToLocal(self.articulatedAxis.aiRevereserNode, tX,vY,tZ); |
364 | end |
365 | end |
366 | |
367 | local doNotSteer = nil; |
368 | if self.isConveyorBelt then |
369 | doNotSteer = true; |
370 | end; |
371 | |
372 | AIVehicleUtil.driveToPoint(self, dt, acceleration, self.isAllowedToDrive, moveForwards, pX, pZ, maxSpeed, doNotSteer); |
373 | |
374 | -- worst case check: did not move but should have moved |
375 | if self.isAllowedToDrive and self:getLastSpeed() < 0.5 then |
376 | self.didNotMoveTimer = self.didNotMoveTimer - dt; |
377 | else |
378 | self.didNotMoveTimer = self.didNotMoveTimeout; |
379 | end |
380 | if self.didNotMoveTimer < 0 then |
381 | self:stopAIVehicle(AIVehicle.STOP_REASON_BLOCKED_BY_OBJECT); |
382 | end |
383 | |
384 | end |
385 | end |
386 | end |
390 | function AIVehicle:draw() |
391 | if g_currentMission:getHasPermission("hireAI") then |
392 | if self.aiIsStarted then |
393 | g_currentMission:addHelpButtonText(g_i18n:getText("action_dismissEmployee"), InputBinding.TOGGLE_AI, nil, GS_PRIO_HIGH); |
394 | else |
395 | if self:canStartAIVehicle() then |
396 | g_currentMission:addHelpButtonText(g_i18n:getText("action_hireEmployee"), InputBinding.TOGGLE_AI, nil, GS_PRIO_HIGH); |
397 | end |
398 | end |
399 | end |
400 | |
401 | if self.isConveyorBelt then |
402 | if self:getIsActiveForInput(true) and not self.aiIsStarted then |
403 | g_currentMission:addHelpButtonText(string.format(g_i18n:getText("action_conveyorBeltChangeAngle"), string.format("%.0f", self.aiConveyorBelt.currentAngle)), InputBinding.IMPLEMENT_EXTRA3, nil, GS_PRIO_NORMAL); |
404 | end |
405 | end; |
406 | -- |
407 | if not self.isServer then |
408 | return; |
409 | end |
410 | if self.aiIsStarted then |
411 | if self.driveStrategies ~= nil and #self.driveStrategies > 0 then |
412 | for i=1,#self.driveStrategies do |
413 | local driveStrategy = self.driveStrategies[i]; |
414 | if driveStrategy.draw ~= nil then |
415 | driveStrategy:draw(); |
416 | end |
417 | end |
418 | end |
419 | end |
420 | end |
508 | function AIVehicle:startAIVehicle(helperIndex, noEventSend) |
509 | if helperIndex ~= nil then |
510 | self.currentHelper = HelperUtil.helperIndexToDesc[helperIndex] |
511 | else |
512 | self.currentHelper = HelperUtil.getRandomHelper() |
513 | end |
514 | |
515 | HelperUtil.useHelper(self.currentHelper) |
516 | |
517 | g_currentMission.missionStats:updateStats("workersHired", 1); |
518 | |
519 | if noEventSend == nil or noEventSend == false then |
520 | if g_server ~= nil then |
521 | g_server:broadcastEvent(AIVehicleSetStartedEvent:new(self, nil, true, self.currentHelper), nil, nil, self); |
522 | else |
523 | g_client:getServerConnection():sendEvent(AIVehicleSetStartedEvent:new(self, nil, true, self.currentHelper)); |
524 | end |
525 | end |
526 | self:onStartAiVehicle(); |
527 | end |
533 | function AIVehicle:stopAIVehicle(reason, noEventSend) |
534 | if noEventSend == nil or noEventSend == false then |
535 | if g_server ~= nil then |
536 | g_server:broadcastEvent(AIVehicleSetStartedEvent:new(self, reason, false), nil, nil, self); |
537 | else |
538 | g_client:getServerConnection():sendEvent(AIVehicleSetStartedEvent:new(self, reason, false)); |
539 | end |
540 | end |
541 | |
542 | if reason ~= nil and reason ~= AIVehicle.STOP_REASON_USER then |
543 | local notificationType = FSBaseMission.INGAME_NOTIFICATION_CRITICAL |
544 | if reason == AIVehicle.STOP_REASON_REGULAR then |
545 | notificationType = FSBaseMission.INGAME_NOTIFICATION_OK |
546 | end |
547 | g_currentMission:addIngameNotification(notificationType, string.format(g_i18n:getText(AIVehicle.REASON_TEXT_MAPPING[reason]), self.currentHelper.name)) |
548 | end |
549 | |
550 | HelperUtil.releaseHelper(self.currentHelper) |
551 | |
552 | g_currentMission.missionStats:updateStats("workersHired", -1); |
553 | |
554 | self:onStopAiVehicle(); |
555 | end |
559 | function AIVehicle:onStartAiVehicle() |
560 | if not self.aiIsStarted then |
561 | if not self.isHired then |
562 | AIVehicle.numHirablesHired = AIVehicle.numHirablesHired + 1; |
563 | end; |
564 | self.isHired = true; |
565 | self.isHirableBlocked = false; |
566 | self.forceIsActive = true; |
567 | self.stopMotorOnLeave = false; |
568 | self.steeringEnabled = false; |
569 | self.disableCharacterOnLeave = false; |
570 | |
571 | if self.vehicleCharacter ~= nil then |
572 | self.vehicleCharacter:delete(); |
573 | self.vehicleCharacter:loadCharacter(self.currentHelper.xmlFilename, getUserRandomizedMpColor(self.currentHelper.name)) |
574 | if self.isEntered then |
575 | self.vehicleCharacter:setCharacterVisibility(false) |
576 | end |
577 | end |
578 | |
579 | local hotspotX, _, hotspotZ = getWorldTranslation(self.rootNode); |
580 | |
581 | local _, textSize = getNormalizedScreenValues(0, 6); |
582 | local _, textOffsetY = getNormalizedScreenValues(0, 11.5); |
583 | local width, height = getNormalizedScreenValues(15,15) |
584 | self.mapAIHotspot = g_currentMission.ingameMap:createMapHotspot("helper", self.currentHelper.name, nil, getNormalizedUVs({776, 520, 240, 240}), {0.052, 0.1248, 0.672, 1}, hotspotX, hotspotZ, width, height, false, false, true, self.components[1].node, true, MapHotspot.CATEGORY_AI, textSize, textOffsetY, {1, 1, 1, 1}, nil, getNormalizedUVs({776, 520, 240, 240}), Overlay.ALIGN_VERTICAL_MIDDLE, 0.7) |
585 | self.aiIsStarted = true; |
586 | |
587 | if self.isServer then |
588 | self:getVehicleData(); |
589 | self:setDriveStrategies(); |
590 | end |
591 | |
592 | self:aiTurnOn(); |
593 | for _,implement in pairs(self.aiImplementList) do |
594 | implement.object:aiTurnOn(); |
595 | end |
596 | end |
597 | end |
601 | function AIVehicle:onStopAiVehicle() |
602 | if self.aiIsStarted then |
603 | if self.isHired then |
604 | AIVehicle.numHirablesHired = math.max(AIVehicle.numHirablesHired - 1, 0); |
605 | end; |
606 | |
607 | self.isHired = false; |
608 | |
609 | self.forceIsActive = false; |
610 | self.stopMotorOnLeave = true; |
611 | self.steeringEnabled = true; |
612 | |
613 | self.disableCharacterOnLeave = true; |
614 | |
615 | if self.vehicleCharacter ~= nil then |
616 | self.vehicleCharacter:delete(); |
617 | end |
618 | |
619 | if self.isEntered or self.isControlled then |
620 | if self.vehicleCharacter ~= nil then |
621 | self.vehicleCharacter:loadCharacter(PlayerUtil.playerIndexToDesc[self.playerIndex].xmlFilename, self.playerColorIndex) |
622 | self.vehicleCharacter:setCharacterVisibility(not self.isEntered) |
623 | end |
624 | end; |
625 | self.currentHelper = nil |
626 | |
627 | if self.mapAIHotspot ~= nil then |
628 | g_currentMission.ingameMap:deleteMapHotspot(self.mapAIHotspot); |
629 | self.mapAIHotspot = nil; |
630 | end |
631 | |
632 | self:setCruiseControlState(Drivable.CRUISECONTROL_STATE_OFF, true); |
633 | if self.isServer then |
634 | WheelsUtil.updateWheelsPhysics(self, 0, self.lastSpeedReal, 0, true, self.requiredDriveMode); |
635 | |
636 | if not g_currentMission.missionInfo.automaticMotorStartEnabled and not self.isControlled then |
637 | self:stopMotor(); |
638 | end |
639 | |
640 | if self.driveStrategies ~= nil and #self.driveStrategies > 0 then |
641 | for i=#self.driveStrategies,1,-1 do |
642 | self.driveStrategies[i]:delete(); |
643 | table.remove(self.driveStrategies, i); |
644 | end |
645 | self.driveStrategies = {}; |
646 | end |
647 | end |
648 | |
649 | self:aiTurnOff(); |
650 | for _,implement in pairs(self.aiImplementList) do |
651 | if implement.object ~= nil then |
652 | implement.object:aiTurnOff(); |
653 | end; |
654 | end |
655 | |
656 | self.aiIsStarted = false; |
657 | end |
658 | end |
669 | function AIVehicle:getVehicleData() |
670 | |
671 | self.aiImplementList = {}; |
672 | |
673 | if self.aiLeftMarker ~= nil and self.aiRightMarker ~= nil and self.aiBackMarker ~= nil then |
674 | table.insert(self.aiImplementList, {object=self}); |
675 | end |
676 | |
677 | AIVehicleUtil.getImplementList(self, self.aiImplementList); |
678 | |
679 | --# check type and relative position of implement |
680 | if self.aiImplementList ~= nil then |
681 | for _,implement in pairs(self.aiImplementList) do |
682 | |
683 | if implement.object.attacherVehicle ~= nil then |
684 | local jointDesc = implement.object.attacherVehicle.attacherJoints[implement.jointDescIndex]; |
685 | if jointDesc.rotationNode ~= nil or jointDesc.aiAllowTurnBackward then |
686 | implement.isTool = true; |
687 | else |
688 | implement.isTool = false; |
689 | end |
690 | else |
691 | implement.isTool = true; |
692 | end |
693 | |
694 | end |
695 | end |
696 | |
697 | end |
701 | function AIVehicle:setDriveStrategies() |
702 | if self.aiImplementList ~= nil and #self.aiImplementList > 0 then |
703 | |
704 | if self.driveStrategies ~= nil and #self.driveStrategies > 0 then |
705 | for i=#self.driveStrategies,1,-1 do |
706 | self.driveStrategies[i]:delete(); |
707 | table.remove(self.driveStrategies, i); |
708 | end |
709 | self.driveStrategies = {}; |
710 | end |
711 | |
712 | local foundCombine = false; |
713 | for _,implement in pairs(self.aiImplementList) do |
714 | if SpecializationUtil.hasSpecialization(Combine, implement.object.specializations) then |
715 | foundCombine = true; |
716 | break; |
717 | end |
718 | end |
719 | foundCombine = foundCombine or SpecializationUtil.hasSpecialization(Combine, self.specializations); |
720 | if foundCombine then |
721 | local driveStrategyCombine = AIDriveStrategyCombine:new(); |
722 | driveStrategyCombine:setAIVehicle(self); |
723 | table.insert(self.driveStrategies, driveStrategyCombine); |
724 | end |
725 | |
726 | local driveStrategyCollision = AIDriveStrategyCollision:new(); |
727 | local driveStrategyStraight = AIDriveStrategyStraight:new(); |
728 | |
729 | driveStrategyCollision:setAIVehicle(self); |
730 | driveStrategyStraight:setAIVehicle(self); |
731 | |
732 | table.insert(self.driveStrategies, driveStrategyCollision); |
733 | table.insert(self.driveStrategies, driveStrategyStraight); |
734 | end |
735 | |
736 | if self.isConveyorBelt then |
737 | if self.driveStrategies ~= nil and #self.driveStrategies > 0 then |
738 | for i=#self.driveStrategies,1,-1 do |
739 | self.driveStrategies[i]:delete(); |
740 | table.remove(self.driveStrategies, i); |
741 | end |
742 | self.driveStrategies = {}; |
743 | end |
744 | |
745 | local aiDriveStrategyConveyor = AIDriveStrategyConveyor:new(); |
746 | |
747 | aiDriveStrategyConveyor:setAIVehicle(self); |
748 | |
749 | table.insert(self.driveStrategies, aiDriveStrategyConveyor); |
750 | end; |
751 | end |