1 /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */
2 
3 
4 #include "MobileCAI.h"
5 #include "TransportCAI.h"
6 #include "ExternalAI/EngineOutHandler.h"
7 #include "Game/GameHelper.h"
8 #include "Game/GlobalUnsynced.h"
9 #include "Game/SelectedUnitsHandler.h"
10 #include "Map/Ground.h"
11 #include "Sim/Misc/AirBaseHandler.h"
12 #include "Sim/Misc/LosHandler.h"
13 #include "Sim/Misc/ModInfo.h"
14 #include "Sim/Misc/TeamHandler.h"
15 #include "Sim/MoveTypes/AAirMoveType.h"
16 #include "Sim/Units/UnitDef.h"
17 #include "Sim/Units/Unit.h"
18 #include "Sim/Units/UnitHandler.h"
19 #include "Sim/Units/Groups/Group.h"
20 #include "Sim/Units/UnitTypes/TransportUnit.h"
21 #include "Sim/Weapons/Weapon.h"
22 #include "Sim/Weapons/WeaponDef.h"
23 #include "System/Log/ILog.h"
24 #include "System/myMath.h"
25 #include "System/Util.h"
26 #include <assert.h>
27 
28 #define AUTO_GENERATE_ATTACK_ORDERS 1
29 #define BUGGER_OFF_TTL 200
30 #define MAX_CLOSE_IN_RETRY_TICKS 30
31 #define MAX_USERGOAL_TOLERANCE_DIST 100.0f
32 
33 // MobileCAI is not always assigned to aircraft
GetAirMoveType(const CUnit * owner)34 static AAirMoveType* GetAirMoveType(const CUnit* owner) {
35 	assert(owner->unitDef->IsAirUnit());
36 
37 	if (owner->UsingScriptMoveType()) {
38 		return static_cast<AAirMoveType*>(owner->prevMoveType);
39 	}
40 
41 	return static_cast<AAirMoveType*>(owner->moveType);
42 }
43 
44 
45 
46 CR_BIND_DERIVED(CMobileCAI ,CCommandAI , )
47 CR_REG_METADATA(CMobileCAI, (
48 	CR_MEMBER(goalPos),
49 	CR_MEMBER(goalRadius),
50 	CR_MEMBER(lastBuggerGoalPos),
51 	CR_MEMBER(lastUserGoal),
52 
53 	CR_MEMBER(lastIdleCheck),
54 	CR_MEMBER(tempOrder),
55 
56 	CR_MEMBER(lastPC),
57 
58 	CR_MEMBER(lastBuggerOffTime),
59 	CR_MEMBER(buggerOffPos),
60 	CR_MEMBER(buggerOffRadius),
61 
62 	CR_MEMBER(commandPos1),
63 	CR_MEMBER(commandPos2),
64 
65 	CR_MEMBER(lastCloseInTry),
66 
67 	CR_MEMBER(cancelDistance),
68 	CR_MEMBER(slowGuard),
69 	CR_MEMBER(moveDir),
70 	CR_RESERVED(16)
71 ))
72 
CMobileCAI()73 CMobileCAI::CMobileCAI():
74 	CCommandAI(),
75 	goalPos(-1,-1,-1),
76 	goalRadius(0.0f),
77 	lastBuggerGoalPos(-1,0,-1),
78 	lastUserGoal(ZeroVector),
79 	lastIdleCheck(0),
80 	tempOrder(false),
81 	lastPC(-1),
82 	lastBuggerOffTime(-BUGGER_OFF_TTL),
83 	buggerOffPos(ZeroVector),
84 	buggerOffRadius(0),
85 	commandPos1(ZeroVector),
86 	commandPos2(ZeroVector),
87 	cancelDistance(1024),
88 	lastCloseInTry(-1),
89 	slowGuard(false),
90 	moveDir(gs->randFloat() > 0.5)
91 {}
92 
93 
CMobileCAI(CUnit * owner)94 CMobileCAI::CMobileCAI(CUnit* owner):
95 	CCommandAI(owner),
96 	goalPos(-1,-1,-1),
97 	goalRadius(0.0f),
98 	lastBuggerGoalPos(-1,0,-1),
99 	lastUserGoal(owner->pos),
100 	lastIdleCheck(0),
101 	tempOrder(false),
102 	lastPC(-1),
103 	lastBuggerOffTime(-BUGGER_OFF_TTL),
104 	buggerOffPos(ZeroVector),
105 	buggerOffRadius(0),
106 	commandPos1(ZeroVector),
107 	commandPos2(ZeroVector),
108 	cancelDistance(1024),
109 	lastCloseInTry(-1),
110 	slowGuard(false),
111 	moveDir(gs->randFloat() > 0.5)
112 {
113 	CalculateCancelDistance();
114 
115 	CommandDescription c;
116 
117 	c.id=CMD_LOAD_ONTO;
118 	c.action="loadonto";
119 	c.type=CMDTYPE_ICON_UNIT;
120 	c.name="Load units";
121 	c.mouseicon=c.name;
122 	c.tooltip="Sets the unit to load itself onto a transport";
123 	c.hidden = true;
124 	possibleCommands.push_back(c);
125 	c.hidden = false;
126 
127 	if (owner->unitDef->canmove) {
128 		c.id=CMD_MOVE;
129 		c.action="move";
130 		c.type=CMDTYPE_ICON_FRONT;
131 		c.name="Move";
132 		c.mouseicon=c.name;
133 		c.tooltip="Move: Order the unit to move to a position";
134 		c.params.push_back("1000000"); // max distance
135 		possibleCommands.push_back(c);
136 		c.params.clear();
137 	}
138 
139 	if (owner->unitDef->canPatrol) {
140 		c.id=CMD_PATROL;
141 		c.action="patrol";
142 		c.type=CMDTYPE_ICON_MAP;
143 		c.name="Patrol";
144 		c.mouseicon=c.name;
145 		c.tooltip="Patrol: Order the unit to patrol to one or more waypoints";
146 		possibleCommands.push_back(c);
147 		c.params.clear();
148 	}
149 
150 	if (owner->unitDef->canFight) {
151 		c.id = CMD_FIGHT;
152 		c.action="fight";
153 		c.type = CMDTYPE_ICON_FRONT;
154 		c.name = "Fight";
155 		c.mouseicon=c.name;
156 		c.tooltip = "Fight: Order the unit to take action while moving to a position";
157 		possibleCommands.push_back(c);
158 	}
159 
160 	if (owner->unitDef->canGuard) {
161 		c.id=CMD_GUARD;
162 		c.action="guard";
163 		c.type=CMDTYPE_ICON_UNIT;
164 		c.name="Guard";
165 		c.mouseicon=c.name;
166 		c.tooltip="Guard: Order a unit to guard another unit and attack units attacking it";
167 		possibleCommands.push_back(c);
168 	}
169 
170 	if (owner->unitDef->canfly) {
171 		c.params.clear();
172 		c.id=CMD_AUTOREPAIRLEVEL;
173 		c.action="autorepairlevel";
174 		c.type=CMDTYPE_ICON_MODE;
175 		c.name="Repair level";
176 		c.mouseicon=c.name;
177 		c.params.push_back("1");
178 		c.params.push_back("LandAt 0");
179 		c.params.push_back("LandAt 30");
180 		c.params.push_back("LandAt 50");
181 		c.params.push_back("LandAt 80");
182 		c.tooltip=
183 			"Repair level: Sets at which health level an aircraft will try to find a repair pad";
184 		possibleCommands.push_back(c);
185 		nonQueingCommands.insert(CMD_AUTOREPAIRLEVEL);
186 
187 		c.params.clear();
188 		c.id=CMD_IDLEMODE;
189 		c.action="idlemode";
190 		c.type=CMDTYPE_ICON_MODE;
191 		c.name="Land mode";
192 		c.mouseicon=c.name;
193 		c.params.push_back("1");
194 		c.params.push_back(" Fly ");
195 		c.params.push_back("Land");
196 		c.tooltip="Land mode: Sets what aircraft will do on idle";
197 		possibleCommands.push_back(c);
198 		nonQueingCommands.insert(CMD_IDLEMODE);
199 	}
200 
201 	nonQueingCommands.insert(CMD_SET_WANTED_MAX_SPEED);
202 }
203 
204 
205 
GiveCommandReal(const Command & c,bool fromSynced)206 void CMobileCAI::GiveCommandReal(const Command& c, bool fromSynced)
207 {
208 	if (!AllowedCommand(c, fromSynced))
209 		return;
210 
211 	if (owner->unitDef->IsAirUnit()) {
212 		AAirMoveType* airMT = GetAirMoveType(owner);
213 
214 		if (c.GetID() == CMD_AUTOREPAIRLEVEL) {
215 			if (c.params.empty())
216 				return;
217 
218 			switch ((int) c.params[0]) {
219 				case 0: { airMT->SetRepairBelowHealth(0.0f); break; }
220 				case 1: { airMT->SetRepairBelowHealth(0.3f); break; }
221 				case 2: { airMT->SetRepairBelowHealth(0.5f); break; }
222 				case 3: { airMT->SetRepairBelowHealth(0.8f); break; }
223 			}
224 
225 			for (unsigned int n = 0; n < possibleCommands.size(); n++) {
226 				if (possibleCommands[n].id != CMD_AUTOREPAIRLEVEL)
227 					continue;
228 
229 				possibleCommands[n].params[0] = IntToString(int(c.params[0]), "%d");
230 				break;
231 			}
232 
233 			selectedUnitsHandler.PossibleCommandChange(owner);
234 			return;
235 		}
236 
237 		if (c.GetID() == CMD_IDLEMODE) {
238 			if (c.params.empty())
239 				return;
240 
241 			// toggle between the "land" and "fly" idle-modes
242 			switch ((int) c.params[0]) {
243 				case 0: { airMT->autoLand = false; break; }
244 				case 1: { airMT->autoLand = true; break; }
245 			}
246 
247 			if (!airMT->owner->beingBuilt) {
248 				if (!airMT->autoLand)
249 					airMT->Takeoff();
250 				else
251 					airMT->Land();
252 			}
253 
254 			for (unsigned int n = 0; n < possibleCommands.size(); n++) {
255 				if (possibleCommands[n].id != CMD_IDLEMODE)
256 					continue;
257 
258 				possibleCommands[n].params[0] = IntToString(int(c.params[0]), "%d");
259 				break;
260 			}
261 
262 			selectedUnitsHandler.PossibleCommandChange(owner);
263 			return;
264 		}
265 	}
266 
267 	if (!(c.options & SHIFT_KEY) && nonQueingCommands.find(c.GetID()) == nonQueingCommands.end()) {
268 		tempOrder = false;
269 		StopSlowGuard();
270 	}
271 
272 	CCommandAI::GiveAllowedCommand(c);
273 }
274 
275 
276 /// returns true if the unit has to land
RefuelIfNeeded()277 bool CMobileCAI::RefuelIfNeeded()
278 {
279 	if (owner->unitDef->maxFuel <= 0.0f)
280 		return false;
281 
282 	if (owner->moveType->GetReservedPad() != NULL) {
283 		// we already have a pad
284 		return false;
285 	}
286 
287 	if (owner->currentFuel <= 0.0f) {
288 		// we're completely out of fuel
289 		owner->AttackUnit(NULL, false, false);
290 		inCommand = false;
291 
292 		CAirBaseHandler::LandingPad* lp =
293 			airBaseHandler->FindAirBase(owner, owner->unitDef->minAirBasePower, true);
294 
295 		if (lp != NULL) {
296 			// found a pad
297 			owner->moveType->ReservePad(lp);
298 		} else {
299 			// no pads available, search for a landing
300 			// spot near any that are currently occupied
301 			const float3& landingPos = airBaseHandler->FindClosestAirBasePos(owner, owner->unitDef->minAirBasePower);
302 
303 			if (landingPos != ZeroVector) {
304 				SetGoal(landingPos, owner->pos);
305 			} else {
306 				StopMove();
307 			}
308 		}
309 
310 		return true;
311 	} else if (owner->moveType->WantsRefuel()) {
312 		// current fuel level is below our bingo threshold
313 		// note: force the refuel attempt (irrespective of
314 		// what our currently active command is)
315 
316 		CAirBaseHandler::LandingPad* lp =
317 			airBaseHandler->FindAirBase(owner, owner->unitDef->minAirBasePower, true);
318 
319 		if (lp != NULL) {
320 			StopMove();
321 			owner->AttackUnit(NULL, false, false);
322 			owner->moveType->ReservePad(lp);
323 			inCommand = false;
324 			return true;
325 		}
326 	}
327 
328 	return false;
329 }
330 
331 /// returns true if the unit has to land
LandRepairIfNeeded()332 bool CMobileCAI::LandRepairIfNeeded()
333 {
334 	if (owner->moveType->GetReservedPad() != NULL)
335 		return false;
336 
337 	if (!owner->moveType->WantsRepair())
338 		return false;
339 
340 	// we're damaged, just seek a pad for repairs
341 	CAirBaseHandler::LandingPad* lp =
342 		airBaseHandler->FindAirBase(owner, owner->unitDef->minAirBasePower, true);
343 
344 	if (lp != NULL) {
345 		owner->moveType->ReservePad(lp);
346 		return true;
347 	}
348 
349 	const float3& newGoal = airBaseHandler->FindClosestAirBasePos(owner, owner->unitDef->minAirBasePower);
350 
351 	if (newGoal != ZeroVector) {
352 		SetGoal(newGoal, owner->pos);
353 		return true;
354 	}
355 
356 	return false;
357 }
358 
SlowUpdate()359 void CMobileCAI::SlowUpdate()
360 {
361 	if (gs->paused) // Commands issued may invoke SlowUpdate when paused
362 		return;
363 
364 	if (!owner->UsingScriptMoveType() && owner->unitDef->IsAirUnit()) {
365 		LandRepairIfNeeded() || RefuelIfNeeded();
366 	}
367 
368 
369 	if (!commandQue.empty() && commandQue.front().timeOut < gs->frameNum) {
370 		StopMove();
371 		FinishCommand();
372 		return;
373 	}
374 
375 	if (commandQue.empty()) {
376 		MobileAutoGenerateTarget();
377 
378 		// the attack order could terminate directly and thus cause a loop
379 		if (commandQue.empty() || (commandQue.front()).GetID() == CMD_ATTACK) {
380 			return;
381 		}
382 	}
383 
384 	if (!slowGuard) {
385 		// when slow-guarding, regulate speed through {Start,Stop}SlowGuard
386 		SlowUpdateMaxSpeed();
387 	}
388 
389 	Execute();
390 }
391 
392 /**
393 * @brief Executes the first command in the commandQue
394 */
Execute()395 void CMobileCAI::Execute()
396 {
397 	Command& c = commandQue.front();
398 	switch (c.GetID()) {
399 		case CMD_SET_WANTED_MAX_SPEED: { ExecuteSetWantedMaxSpeed(c);	return; }
400 		case CMD_MOVE:                 { ExecuteMove(c);				return; }
401 		case CMD_PATROL:               { ExecutePatrol(c);				return; }
402 		case CMD_FIGHT:                { ExecuteFight(c);				return; }
403 		case CMD_GUARD:                { ExecuteGuard(c);				return; }
404 		case CMD_LOAD_ONTO:            { ExecuteLoadUnits(c);			return; }
405 		default: {
406 		  CCommandAI::SlowUpdate();
407 		  return;
408 		}
409 	}
410 }
411 
412 /**
413 * @brief executes the set wanted max speed command
414 */
ExecuteSetWantedMaxSpeed(Command & c)415 void CMobileCAI::ExecuteSetWantedMaxSpeed(Command &c)
416 {
417 	if (repeatOrders && (commandQue.size() >= 2) &&
418 			(commandQue.back().GetID() != CMD_SET_WANTED_MAX_SPEED)) {
419 		commandQue.push_back(commandQue.front());
420 	}
421 	FinishCommand();
422 	SlowUpdate();
423 	return;
424 }
425 
426 /**
427 * @brief executes the move command
428 */
ExecuteMove(Command & c)429 void CMobileCAI::ExecuteMove(Command &c)
430 {
431 	const float3 cmdPos = c.GetPos(0);
432 
433 	if (cmdPos != goalPos) {
434 		SetGoal(cmdPos, owner->pos);
435 	}
436 
437 	if ((owner->pos - goalPos).SqLength2D() < cancelDistance || owner->moveType->progressState == AMoveType::Failed) {
438 		FinishCommand();
439 	}
440 	return;
441 }
442 
ExecuteLoadUnits(Command & c)443 void CMobileCAI::ExecuteLoadUnits(Command &c) {
444 	CUnit* unit = unitHandler->GetUnit(c.params[0]);
445 	CTransportUnit* tran = dynamic_cast<CTransportUnit*>(unit);
446 
447 	if (!tran) {
448 		FinishCommand();
449 		return;
450 	}
451 
452 	if (!inCommand) {
453 		inCommand = true;
454 		Command newCommand(CMD_LOAD_UNITS, INTERNAL_ORDER | SHIFT_KEY, owner->id);
455 		tran->commandAI->GiveCommandReal(newCommand);
456 	}
457 	if (owner->GetTransporter() != NULL) {
458 		if (!commandQue.empty())
459 			FinishCommand();
460 		return;
461 	}
462 
463 	if (unit == NULL)
464 		return;
465 
466 	if ((unit->pos - goalPos).SqLength2D() > cancelDistance) {
467 		SetGoal(unit->pos, owner->pos);
468 	}
469 	if ((owner->pos - goalPos).SqLength2D() < cancelDistance) {
470 		StopMove();
471 	}
472 
473 	return;
474 }
475 
476 /**
477 * @brief Executes the Patrol command c
478 */
ExecutePatrol(Command & c)479 void CMobileCAI::ExecutePatrol(Command &c)
480 {
481 	assert(owner->unitDef->canPatrol);
482 	if (c.params.size() < 3) {
483 		LOG_L(L_ERROR, "[MCAI::%s][f=%d][id=%d] CMD_FIGHT #params < 3", __FUNCTION__, gs->frameNum, owner->id);
484 		return;
485 	}
486 	Command temp(CMD_FIGHT, c.options | INTERNAL_ORDER, c.GetPos(0));
487 
488 	commandQue.push_back(c);
489 	commandQue.pop_front();
490 	commandQue.push_front(temp);
491 
492 	Command tmpC(CMD_PATROL);
493 	eoh->CommandFinished(*owner, tmpC);
494 	ExecuteFight(temp);
495 }
496 
497 /**
498 * @brief Executes the Fight command c
499 */
ExecuteFight(Command & c)500 void CMobileCAI::ExecuteFight(Command& c)
501 {
502 	assert((c.options & INTERNAL_ORDER) || owner->unitDef->canFight);
503 
504 	if (c.params.size() == 1 && !owner->weapons.empty()) {
505 		CWeapon* w = owner->weapons.front();
506 
507 		if ((orderTarget != NULL) && !w->AttackUnit(orderTarget, false)) {
508 			CUnit* newTarget = CGameHelper::GetClosestValidTarget(owner->pos, owner->maxRange, owner->allyteam, this);
509 
510 			if ((newTarget != NULL) && w->AttackUnit(newTarget, false)) {
511 				c.params[0] = newTarget->id;
512 
513 				inCommand = false;
514 			} else {
515 				w->AttackUnit(orderTarget, false);
516 			}
517 		}
518 
519 		ExecuteAttack(c);
520 		return;
521 	}
522 
523 	if (tempOrder) {
524 		inCommand = true;
525 		tempOrder = false;
526 	}
527 	if (c.params.size() < 3) {
528 		LOG_L(L_ERROR, "[MCAI::%s][f=%d][id=%d] CMD_FIGHT #params < 3", __FUNCTION__, gs->frameNum, owner->id);
529 		return;
530 	}
531 	if (c.params.size() >= 6) {
532 		if (!inCommand) {
533 			commandPos1 = c.GetPos(3);
534 		}
535 	} else {
536 		// Some hackery to make sure the line (commandPos1,commandPos2) is NOT
537 		// rotated (only shortened) if we reach this because the previous return
538 		// fight command finished by the 'if((curPos-pos).SqLength2D()<(64*64)){'
539 		// condition, but is actually updated correctly if you click somewhere
540 		// outside the area close to the line (for a new command).
541 		commandPos1 = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
542 		if ((owner->pos - commandPos1).SqLength2D() > (96 * 96)) {
543 			commandPos1 = owner->pos;
544 		}
545 	}
546 
547 	float3 pos = c.GetPos(0);
548 
549 	if (!inCommand) {
550 		inCommand = true;
551 		commandPos2 = pos;
552 		lastUserGoal = commandPos2;
553 	}
554 	if (c.params.size() >= 6) {
555 		pos = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
556 	}
557 	if (pos != goalPos) {
558 		SetGoal(pos, owner->pos);
559 	}
560 
561 	if (owner->unitDef->canAttack && owner->fireState >= FIRESTATE_FIREATWILL && !owner->weapons.empty()) {
562 		const float3 curPosOnLine = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
563 		const float searchRadius = owner->maxRange + 100 * owner->moveState * owner->moveState;
564 		CUnit* enemy = CGameHelper::GetClosestValidTarget(curPosOnLine, searchRadius, owner->allyteam, this);
565 
566 		if (enemy != NULL) {
567 			PushOrUpdateReturnFight();
568 
569 			// make the attack-command inherit <c>'s options
570 			// NOTE: see AirCAI::ExecuteFight why we do not set INTERNAL_ORDER
571 			Command c2(CMD_ATTACK, c.options, enemy->id);
572 			commandQue.push_front(c2);
573 
574 			inCommand = false;
575 			tempOrder = true;
576 
577 			// avoid infinite loops (?)
578 			if (lastPC != gs->frameNum) {
579 				lastPC = gs->frameNum;
580 				SlowUpdate();
581 			}
582 			return;
583 		}
584 	}
585 
586 	if ((owner->pos - goalPos).SqLength2D() < (64 * 64)
587 			|| (owner->moveType->progressState == AMoveType::Failed)){
588 		FinishCommand();
589 	}
590 }
591 
IsValidTarget(const CUnit * enemy) const592 bool CMobileCAI::IsValidTarget(const CUnit* enemy) const {
593 	if (!enemy)
594 		return false;
595 
596 	if (enemy == owner)
597 		return false;
598 
599 	if (owner->unitDef->noChaseCategory & enemy->category)
600 		return false;
601 
602 	// don't _auto_ chase neutrals
603 	if (enemy->IsNeutral())
604 		return false;
605 
606 	if (owner->weapons.empty())
607 		return false;
608 
609 	// on "Hold pos", a target can not be valid if there exists no line of fire to it.
610 	// FIXME: even if not on HOLDPOS there are situations where having LOF is not enough
611 	if (owner->moveState == MOVESTATE_HOLDPOS && !owner->weapons.front()->TryTargetRotate(const_cast<CUnit*>(enemy), false))
612 		return false;
613 
614 	// test if any weapon can target the enemy unit
615 	for (std::vector<CWeapon*>::iterator it = owner->weapons.begin(); it != owner->weapons.end(); ++it) {
616 		if ((*it)->TestTarget(enemy->pos, false, const_cast<CUnit*>(enemy))) {
617 			return true;
618 		}
619 	}
620 
621 	return false;
622 }
623 
624 /**
625 * @brief Executes the guard command c
626 */
ExecuteGuard(Command & c)627 void CMobileCAI::ExecuteGuard(Command &c)
628 {
629 	assert(owner->unitDef->canGuard);
630 	assert(!c.params.empty());
631 
632 	const CUnit* guardee = unitHandler->GetUnit(c.params[0]);
633 
634 	if (guardee == NULL) { FinishCommand(); return; }
635 	if (UpdateTargetLostTimer(guardee->id) == 0) { FinishCommand(); return; }
636 	if (guardee->outOfMapTime > (GAME_SPEED * 5)) { FinishCommand(); return; }
637 
638 	const bool pushAttackCommand =
639 		owner->unitDef->canAttack &&
640 		(guardee->lastAttackFrame + 40 < gs->frameNum) &&
641 		IsValidTarget(guardee->lastAttacker);
642 
643 	if (pushAttackCommand) {
644 		Command nc(CMD_ATTACK, c.options, guardee->lastAttacker->id);
645 		commandQue.push_front(nc);
646 
647 		StopSlowGuard();
648 		SlowUpdate();
649 	} else {
650 		const float3 dif = (guardee->pos - owner->pos).SafeNormalize();
651 		const float3 goal = guardee->pos - dif * (guardee->radius + owner->radius + 64.0f);
652 		const bool resetGoal =
653 			((goalPos - goal).SqLength2D() > 1600.0f) ||
654 			(goalPos - owner->pos).SqLength2D() < Square(owner->moveType->GetMaxSpeed() * GAME_SPEED + 1 + SQUARE_SIZE * 2);
655 
656 		if (resetGoal) {
657 			SetGoal(goal, owner->pos);
658 		}
659 
660 		if ((goal - owner->pos).SqLength2D() < 6400.0f) {
661 			StartSlowGuard(guardee->moveType->GetMaxSpeed());
662 
663 			if ((goal - owner->pos).SqLength2D() < 1800.0f) {
664 				StopMove();
665 				NonMoving();
666 			}
667 		} else {
668 			StopSlowGuard();
669 		}
670 	}
671 }
672 
673 
ExecuteStop(Command & c)674 void CMobileCAI::ExecuteStop(Command &c)
675 {
676 	StopMove();
677 	return CCommandAI::ExecuteStop(c);
678 }
679 
680 
681 
ExecuteAttack(Command & c)682 void CMobileCAI::ExecuteAttack(Command &c)
683 {
684 	assert(owner->unitDef->canAttack);
685 
686 	// limit how far away we fly based on our movestate
687 	if (tempOrder && orderTarget) {
688 		const float3& closestPos = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
689 		const float curTargetDist = LinePointDist(closestPos, commandPos2, orderTarget->pos);
690 		const float maxTargetDist = (owner->moveType->GetManeuverLeash() * owner->moveState + owner->maxRange);
691 
692 		if (owner->moveState < MOVESTATE_ROAM && curTargetDist > maxTargetDist) {
693 			StopMove();
694 			FinishCommand();
695 			return;
696 		}
697 	}
698 
699 	if (!inCommand) {
700 		if (c.params.size() == 1) {
701 			CUnit* targetUnit = unitHandler->GetUnit(c.params[0]);
702 
703 			// check if we have valid target parameter and that we aren't attacking ourselves
704 			if (targetUnit == NULL) { StopMove(); FinishCommand(); return; }
705 			if (targetUnit == owner) { StopMove(); FinishCommand(); return; }
706 			if (targetUnit->GetTransporter() != NULL && !modInfo.targetableTransportedUnits) {
707 				StopMove(); FinishCommand(); return;
708 			}
709 
710 			const float3 tgtErrPos = targetUnit->pos + owner->posErrorVector * 128;
711 			const float3 tgtPosDir = (tgtErrPos - owner->pos).Normalize();
712 
713 			// FIXME: don't call SetGoal() if target is already in range of some weapon?
714 			SetGoal(tgtErrPos - tgtPosDir * targetUnit->radius, owner->pos);
715 			SetOrderTarget(targetUnit);
716 			owner->AttackUnit(targetUnit, (c.options & INTERNAL_ORDER) == 0, c.GetID() == CMD_MANUALFIRE);
717 
718 			inCommand = true;
719 		}
720 		else if (c.params.size() >= 3) {
721 			// user gave force-fire attack command
722 			SetGoal(c.GetPos(0), owner->pos);
723 
724 			inCommand = true;
725 		}
726 	}
727 
728 	// if our target is dead or we lost it then stop attacking
729 	// NOTE: unit should actually just continue to target area!
730 	if (targetDied || (c.params.size() == 1 && UpdateTargetLostTimer(int(c.params[0])) == 0)) {
731 		// cancel keeppointingto
732 		StopMove();
733 		FinishCommand();
734 		return;
735 	}
736 
737 
738 	// user clicked on enemy unit (note that we handle aircrafts slightly differently)
739 	if (orderTarget != NULL) {
740 		bool tryTargetRotate  = false;
741 		bool tryTargetHeading = false;
742 
743 		float edgeFactor = 0.0f; // percent offset to target center
744 		const float3 targetMidPosVec = owner->midPos - orderTarget->midPos;
745 
746 		const float targetGoalDist = (orderTarget->pos + owner->posErrorVector * 128.0f).SqDistance2D(goalPos);
747 		const float targetPosDist = Square(10.0f + orderTarget->pos.distance2D(owner->pos) * 0.2f);
748 		const float minPointingDist = std::min(1.0f * owner->losRadius * losHandler->losDiv, owner->maxRange * 0.9f);
749 
750 		// FIXME? targetMidPosMaxDist is 3D, but compared with a 2D value
751 		const float targetMidPosDist2D = targetMidPosVec.Length2D();
752 		// const float targetMidPosMaxDist = owner->maxRange - (Square(orderTarget->speed.w) / owner->unitDef->maxAcc);
753 
754 		if (!owner->weapons.empty()) {
755 			if (!(c.options & ALT_KEY) && SkipParalyzeTarget(orderTarget)) {
756 				StopMove();
757 				FinishCommand();
758 				return;
759 			}
760 		}
761 
762 		for (unsigned int wNum = 0; wNum < owner->weapons.size(); wNum++) {
763 			CWeapon* w = owner->weapons[wNum];
764 
765 			if (c.GetID() == CMD_MANUALFIRE) {
766 				assert(owner->unitDef->canManualFire);
767 
768 				if (!w->weaponDef->manualfire) {
769 					continue;
770 				}
771 			}
772 
773 			tryTargetRotate  = w->TryTargetRotate(orderTarget, (c.options & INTERNAL_ORDER) == 0);
774 			tryTargetHeading = w->TryTargetHeading(GetHeadingFromVector(-targetMidPosVec.x, -targetMidPosVec.z), orderTarget->pos, orderTarget != NULL, orderTarget);
775 
776 			if (tryTargetRotate || tryTargetHeading)
777 				break;
778 
779 			edgeFactor = math::fabs(w->targetBorder);
780 		}
781 
782 		// if w->AttackUnit() returned true then we are already
783 		// in range with our biggest (?) weapon, so stop moving
784 		// also make sure that we're not locked in close-in/in-range state
785 		// loop due to rotates invoked by in-range or out-of-range states
786 		if (tryTargetRotate) {
787 			const bool canChaseTarget = (!tempOrder || owner->moveState != MOVESTATE_HOLDPOS);
788 			const bool targetBehind = (targetMidPosVec.dot(orderTarget->speed) < 0.0f);
789 
790 			if (canChaseTarget && tryTargetHeading && targetBehind) {
791 				SetGoal(owner->pos + (orderTarget->speed * 80), owner->pos, SQUARE_SIZE, orderTarget->speed.w * 1.1f);
792 			} else {
793 				StopMove();
794 
795 				if (gs->frameNum > lastCloseInTry + MAX_CLOSE_IN_RETRY_TICKS) {
796 					owner->moveType->KeepPointingTo(orderTarget->midPos, minPointingDist, true);
797 				}
798 			}
799 
800 			owner->AttackUnit(orderTarget, (c.options & INTERNAL_ORDER) == 0, c.GetID() == CMD_MANUALFIRE);
801 		}
802 
803 		// if we're on hold pos in a temporary order, then none of the close-in
804 		// code below should run, and the attack command is cancelled.
805 		else if (tempOrder && owner->moveState == MOVESTATE_HOLDPOS) {
806 			StopMove();
807 			FinishCommand();
808 			return;
809 		}
810 
811 		// if ((our movetype has type HoverAirMoveType and length of 2D vector from us to target
812 		// less than 90% of our maximum range) OR squared length of 2D vector from us to target
813 		// less than 1024) then we are close enough
814 		else if (targetMidPosDist2D < (owner->maxRange * 0.9f)) {
815 			if (owner->unitDef->IsHoveringAirUnit() || (targetMidPosVec.SqLength2D() < 1024)) {
816 				StopMove();
817 				owner->moveType->KeepPointingTo(orderTarget->midPos, minPointingDist, true);
818 			}
819 
820 			// if (((first weapon range minus first weapon length greater than distance to target)
821 			// and length of 2D vector from us to target less than 90% of our maximum range)
822 			// then we are close enough, but need to move sideways to get a shot.
823 			//assumption is flawed: The unit may be aiming or otherwise unable to shoot
824 			else if (owner->unitDef->strafeToAttack && targetMidPosDist2D < (owner->maxRange * 0.9f)) {
825 				moveDir ^= (owner->moveType->progressState == AMoveType::Failed);
826 
827 				const float sin = moveDir ? 3.0/5 : -3.0/5;
828 				const float cos = 4.0 / 5;
829 
830 				float3 goalDiff;
831 				goalDiff.x = targetMidPosVec.dot(float3(cos, 0, -sin));
832 				goalDiff.z = targetMidPosVec.dot(float3(sin, 0,  cos));
833 				goalDiff *= (targetMidPosDist2D < (owner->maxRange * 0.3f)) ? 1/cos : cos;
834 				goalDiff += orderTarget->pos;
835 				SetGoal(goalDiff, owner->pos);
836 			}
837 		}
838 
839 		// if 2D distance of (target position plus attacker error vector times 128)
840 		// to goal position greater than
841 		// (10 plus 20% of 2D distance between attacker and target) then we need to close
842 		// in on target more
843 		else if (targetGoalDist > targetPosDist) {
844 			// if the target isn't in LOS, go to its approximate position
845 			// otherwise try to go precisely to the target
846 			// this should fix issues with low range weapons (mainly melee)
847 			const float3 errPos = ((orderTarget->losStatus[owner->allyteam] & LOS_INLOS)? ZeroVector: owner->posErrorVector * 128.0f);
848 			const float3 tgtPos = orderTarget->pos + errPos;
849 
850 			const float3 norm = (tgtPos - owner->pos).Normalize();
851 			const float3 goal = tgtPos - norm * (orderTarget->radius * edgeFactor * 0.8f);
852 
853 			SetGoal(goal, owner->pos);
854 
855 			if (lastCloseInTry < gs->frameNum + MAX_CLOSE_IN_RETRY_TICKS)
856 				lastCloseInTry = gs->frameNum;
857 		}
858 	}
859 
860 	// user wants to attack the ground; cycle through our
861 	// weapons until we find one that can accomodate him
862 	else if (c.params.size() >= 3) {
863 		const float3 attackPos = c.GetPos(0);
864 		const float3 attackVec = attackPos - owner->pos;
865 
866 		bool foundWeapon = false;
867 
868 		for (unsigned int wNum = 0; wNum < owner->weapons.size(); wNum++) {
869 			CWeapon* w = owner->weapons[wNum];
870 
871 			if (foundWeapon)
872 				break;
873 
874 			// XXX HACK - special weapon overrides any checks
875 			if (c.GetID() == CMD_MANUALFIRE) {
876 				assert(owner->unitDef->canManualFire);
877 
878 				if (!w->weaponDef->manualfire)
879 					continue;
880 				if (attackVec.SqLength() >= (w->range * w->range))
881 					continue;
882 
883 				// StopMoveAndKeepPointing calls StopMove before KeepPointingTo
884 				// but we want to call it *after* KeepPointingTo to prevent 4131
885 				owner->AttackGround(attackPos, (c.options & INTERNAL_ORDER) == 0, c.GetID() == CMD_MANUALFIRE);
886 				owner->moveType->KeepPointingTo(attackPos, owner->maxRange * 0.9f, true);
887 				StopMove();
888 
889 				foundWeapon = true;
890 			} else {
891 				// NOTE:
892 				//   we call TryTargetHeading which is less restrictive than TryTarget
893 				//   (eg. the former succeeds even if the unit has not already aligned
894 				//   itself with <attackVec>)
895 				if (w->TryTargetHeading(GetHeadingFromVector(attackVec.x, attackVec.z), attackPos, (c.options & INTERNAL_ORDER) == 0, NULL)) {
896 					if (w->TryTargetRotate(attackPos, (c.options & INTERNAL_ORDER) == 0)) {
897 						StopMove();
898 						owner->AttackGround(attackPos, (c.options & INTERNAL_ORDER) == 0, c.GetID() == CMD_MANUALFIRE);
899 
900 						foundWeapon = true;
901 					}
902 
903 					// for gunships, this pitches the nose down such that
904 					// TryTargetRotate (which also checks range for itself)
905 					// has a bigger chance of succeeding
906 					//
907 					// hence it must be called as soon as we get in range
908 					// and may not depend on what TryTargetRotate returns
909 					// (otherwise we might never get a firing solution)
910 					owner->moveType->KeepPointingTo(attackPos, owner->maxRange * 0.9f, true);
911 				}
912 			}
913 		}
914 
915 		#if 0
916 		// no weapons --> no need to stop at an arbitrary distance?
917 		else if (diff.SqLength2D() < 1024) {
918 			StopMove();
919 			owner->moveType->KeepPointingTo(attackPos, owner->maxRange * 0.9f, true);
920 		}
921 		#endif
922 
923 		// if we are unarmed and more than 10 elmos distant
924 		// from target position, then keeping moving closer
925 		if (owner->weapons.empty() && attackPos.SqDistance2D(goalPos) > 100) {
926 			SetGoal(attackPos, owner->pos);
927 		}
928 	}
929 }
930 
931 
932 
933 
GetDefaultCmd(const CUnit * pointed,const CFeature * feature)934 int CMobileCAI::GetDefaultCmd(const CUnit* pointed, const CFeature* feature)
935 {
936 	if (pointed) {
937 		if (!teamHandler->Ally(gu->myAllyTeam,pointed->allyteam)) {
938 			if (owner->unitDef->canAttack) {
939 				return CMD_ATTACK;
940 			}
941 		} else {
942 			const CTransportCAI* tran = dynamic_cast<CTransportCAI*>(pointed->commandAI);
943 
944 			if (tran != NULL && tran->CanTransport(owner)) {
945 				return CMD_LOAD_ONTO;
946 			} else if (owner->unitDef->canGuard) {
947 				return CMD_GUARD;
948 			}
949 		}
950 	}
951 	return CMD_MOVE;
952 }
953 
SetGoal(const float3 & pos,const float3 &,float goalRadius)954 void CMobileCAI::SetGoal(const float3& pos, const float3& /*curPos*/, float goalRadius)
955 {
956 	if (pos.SqDistance(goalPos) < Square(goalRadius))
957 		return;
958 
959 	owner->moveType->StartMoving(goalPos = pos, this->goalRadius = goalRadius);
960 }
961 
SetGoal(const float3 & pos,const float3 &,float goalRadius,float speed)962 void CMobileCAI::SetGoal(const float3& pos, const float3& /*curPos*/, float goalRadius, float speed)
963 {
964 	if (pos.SqDistance(goalPos) < Square(goalRadius))
965 		return;
966 
967 	owner->moveType->StartMoving(goalPos = pos, this->goalRadius = goalRadius, speed);
968 }
969 
SetFrontMoveCommandPos(const float3 & pos)970 bool CMobileCAI::SetFrontMoveCommandPos(const float3& pos)
971 {
972 	if (commandQue.empty())
973 		return false;
974 	if ((commandQue.front()).GetID() != CMD_MOVE)
975 		return false;
976 
977 	(commandQue.front()).SetPos(0, pos);
978 	return true;
979 }
980 
StopMove()981 void CMobileCAI::StopMove()
982 {
983 	owner->moveType->StopMoving();
984 	goalPos = owner->pos;
985 	goalRadius = 0.f;
986 }
987 
StopMoveAndKeepPointing(const float3 & p,const float r,bool b)988 void CMobileCAI::StopMoveAndKeepPointing(const float3& p, const float r, bool b)
989 {
990 	StopMove();
991 	owner->moveType->KeepPointingTo(p, r, b);
992 }
993 
BuggerOff(const float3 & pos,float radius)994 void CMobileCAI::BuggerOff(const float3& pos, float radius)
995 {
996 	if (radius < 0.0f) {
997 		lastBuggerOffTime = gs->frameNum - BUGGER_OFF_TTL;
998 		return;
999 	}
1000 	lastBuggerOffTime = gs->frameNum;
1001 	buggerOffPos = pos;
1002 	buggerOffRadius = radius + owner->radius;
1003 }
1004 
NonMoving()1005 void CMobileCAI::NonMoving()
1006 {
1007 	if (owner->UsingScriptMoveType())
1008 		return;
1009 
1010 	if (lastBuggerOffTime > gs->frameNum - BUGGER_OFF_TTL) {
1011 		float3 dif = owner->pos-buggerOffPos;
1012 		dif.y = 0.0f;
1013 		float length=dif.Length();
1014 		if (!length) {
1015 			length = 0.1f;
1016 			dif = float3(0.1f, 0.0f, 0.0f);
1017 		}
1018 		if (length < buggerOffRadius) {
1019 			float3 goalPos = buggerOffPos + dif * ((buggerOffRadius + 128) / length);
1020 			bool randomize = (goalPos.x == lastBuggerGoalPos.x) && (goalPos.z == lastBuggerGoalPos.z);
1021 			lastBuggerGoalPos.x = goalPos.x;
1022 			lastBuggerGoalPos.z = goalPos.z;
1023 			if (randomize) {
1024 				lastBuggerGoalPos.y += 32.0f; // gradually increase the amplitude of the random factor
1025 				goalPos.x += (2.0f * lastBuggerGoalPos.y) * gs->randFloat() - lastBuggerGoalPos.y;
1026 				goalPos.z += (2.0f * lastBuggerGoalPos.y) * gs->randFloat() - lastBuggerGoalPos.y;
1027 			}
1028 			else
1029 				lastBuggerGoalPos.y = 0.0f;
1030 
1031 			Command c(CMD_MOVE, goalPos);
1032 			//c.options = INTERNAL_ORDER;
1033 			c.timeOut = gs->frameNum + 40;
1034 			commandQue.push_front(c);
1035 			unimportantMove = true;
1036 		}
1037 	}
1038 }
1039 
FinishCommand()1040 void CMobileCAI::FinishCommand()
1041 {
1042 	if (!((commandQue.front()).options & INTERNAL_ORDER)) {
1043 		lastUserGoal = owner->pos;
1044 	}
1045 	StopSlowGuard();
1046 	CCommandAI::FinishCommand();
1047 }
1048 
MobileAutoGenerateTarget()1049 bool CMobileCAI::MobileAutoGenerateTarget()
1050 {
1051 	//FIXME merge with CWeapon::AutoTarget()
1052 	assert(commandQue.empty());
1053 
1054 	const bool canAttack = (owner->unitDef->canAttack && !owner->weapons.empty());
1055 	const float extraRange = 200.0f * owner->moveState * owner->moveState;
1056 	const float maxRangeSq = Square(owner->maxRange + extraRange);
1057 
1058 	#if (AUTO_GENERATE_ATTACK_ORDERS == 1)
1059 	if (canAttack) {
1060 		if (owner->attackTarget != NULL) {
1061 			if (owner->fireState > FIRESTATE_HOLDFIRE) {
1062 				if (owner->pos.SqDistance2D(owner->attackTarget->pos) < maxRangeSq) {
1063 					Command c(CMD_ATTACK, INTERNAL_ORDER, owner->attackTarget->id);
1064 					c.timeOut = gs->frameNum + GAME_SPEED * 5;
1065 					commandQue.push_front(c);
1066 
1067 					commandPos1 = owner->pos;
1068 					commandPos2 = owner->pos;
1069 
1070 					return (tempOrder = true);
1071 				}
1072 			}
1073 		} else {
1074 			if (owner->fireState > FIRESTATE_HOLDFIRE) {
1075 				const bool haveLastAttacker = (owner->lastAttacker != NULL);
1076 				const bool canAttackAttacker = (haveLastAttacker && (owner->lastAttackFrame + GAME_SPEED * 7) > gs->frameNum);
1077 				const bool canChaseAttacker = (haveLastAttacker && !(owner->unitDef->noChaseCategory & owner->lastAttacker->category));
1078 
1079 				if (canAttackAttacker && canChaseAttacker) {
1080 					const float3& P = owner->lastAttacker->pos;
1081 					const float R = owner->pos.SqDistance2D(P);
1082 
1083 					if (R < maxRangeSq) {
1084 						Command c(CMD_ATTACK, INTERNAL_ORDER, owner->lastAttacker->id);
1085 						c.timeOut = gs->frameNum + GAME_SPEED * 5;
1086 						commandQue.push_front(c);
1087 
1088 						commandPos1 = owner->pos;
1089 						commandPos2 = owner->pos;
1090 
1091 						return (tempOrder = true);
1092 					}
1093 				}
1094 			}
1095 
1096 			if (owner->fireState >= FIRESTATE_FIREATWILL && (gs->frameNum >= lastIdleCheck + 10)) {
1097 				const float searchRadius = owner->maxRange + 150.0f * owner->moveState * owner->moveState;
1098 				const CUnit* enemy = CGameHelper::GetClosestValidTarget(owner->pos, searchRadius, owner->allyteam, this);
1099 
1100 				if (enemy != NULL) {
1101 					Command c(CMD_ATTACK, INTERNAL_ORDER, enemy->id);
1102 					c.timeOut = gs->frameNum + GAME_SPEED * 5;
1103 					commandQue.push_front(c);
1104 
1105 					commandPos1 = owner->pos;
1106 					commandPos2 = owner->pos;
1107 
1108 					return (tempOrder = true);
1109 				}
1110 			}
1111 		}
1112 	}
1113 	#endif
1114 
1115 	if (owner->UsingScriptMoveType())
1116 		return false;
1117 
1118 	lastIdleCheck = gs->frameNum;
1119 
1120 	if (owner->haveTarget) {
1121 		NonMoving(); return false;
1122 	}
1123 	if ((owner->pos - lastUserGoal).SqLength2D() <= (MAX_USERGOAL_TOLERANCE_DIST * MAX_USERGOAL_TOLERANCE_DIST)) {
1124 		NonMoving(); return false;
1125 	}
1126 	if (owner->unitDef->IsHoveringAirUnit()) {
1127 		NonMoving(); return false;
1128 	}
1129 
1130 	unimportantMove = true;
1131 	return false;
1132 }
1133 
1134 
1135 
StopSlowGuard()1136 void CMobileCAI::StopSlowGuard() {
1137 	if (!slowGuard)
1138 		return;
1139 
1140 	slowGuard = false;
1141 
1142 	// restore maxWantedSpeed to our current maxSpeed
1143 	// (StartSlowGuard modifies maxWantedSpeed, so we
1144 	// do not know its old value here)
1145 	owner->moveType->SetWantedMaxSpeed(owner->moveType->GetMaxSpeed());
1146 }
1147 
StartSlowGuard(float speed)1148 void CMobileCAI::StartSlowGuard(float speed) {
1149 	if (slowGuard)
1150 		return;
1151 
1152 	slowGuard = true;
1153 
1154 	if (speed <= 0.0f) { return; }
1155 	if (commandQue.empty()) { return; }
1156 	if (owner->moveType->GetMaxSpeed() < speed) { return; }
1157 
1158 	const Command& c = (commandQue.size() > 1)? commandQue[1]: Command(CMD_STOP);
1159 
1160 	// when guarding, temporarily adopt the maximum
1161 	// (forward) speed of the guardee unit as our own
1162 	// WANTED maximum
1163 	if (c.GetID() == CMD_SET_WANTED_MAX_SPEED) {
1164 		owner->moveType->SetWantedMaxSpeed(speed);
1165 	}
1166 }
1167 
1168 
1169 
CalculateCancelDistance()1170 void CMobileCAI::CalculateCancelDistance()
1171 {
1172 	const float tmp = owner->moveType->CalcStaticTurnRadius() + (SQUARE_SIZE << 1);
1173 
1174 	// clamp it a bit because the units don't have to turn at max speed
1175 	cancelDistance = Clamp(tmp * tmp, 1024.0f, 2048.0f);
1176 }
1177 
1178