1 /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */
2 
3 
4 #include "AirCAI.h"
5 #include "Game/GameHelper.h"
6 #include "Game/GlobalUnsynced.h"
7 #include "Game/SelectedUnitsHandler.h"
8 #include "Map/Ground.h"
9 #include "Sim/Misc/ModInfo.h"
10 #include "Sim/Misc/TeamHandler.h"
11 #include "Sim/MoveTypes/StrafeAirMoveType.h"
12 #include "Sim/Units/Unit.h"
13 #include "Sim/Units/UnitDef.h"
14 #include "Sim/Units/UnitHandler.h"
15 #include "Sim/Units/Groups/Group.h"
16 #include "Sim/Weapons/Weapon.h"
17 #include "Sim/Weapons/WeaponDefHandler.h"
18 #include "System/myMath.h"
19 #include "System/Log/ILog.h"
20 #include "System/Util.h"
21 
22 #include <cassert>
23 #define AUTO_GENERATE_ATTACK_ORDERS 1
24 
25 // AirCAI is always assigned to StrafeAirMoveType aircraft
GetStrafeAirMoveType(const CUnit * owner)26 static CStrafeAirMoveType* GetStrafeAirMoveType(const CUnit* owner) {
27 	assert(owner->unitDef->IsAirUnit());
28 
29 	if (owner->UsingScriptMoveType()) {
30 		return static_cast<CStrafeAirMoveType*>(owner->prevMoveType);
31 	}
32 
33 	return static_cast<CStrafeAirMoveType*>(owner->moveType);
34 }
35 
36 
37 
38 CR_BIND_DERIVED(CAirCAI, CMobileCAI, )
39 CR_REG_METADATA(CAirCAI, (
40 	CR_MEMBER(basePos),
41 	CR_MEMBER(baseDir),
42 
43 	CR_MEMBER(activeCommand),
44 	CR_MEMBER(targetAge),
45 
46 	CR_MEMBER(lastPC1),
47 	CR_MEMBER(lastPC2),
48 	CR_RESERVED(16)
49 ))
50 
CAirCAI()51 CAirCAI::CAirCAI()
52 	: CMobileCAI()
53 	, activeCommand(0)
54 	, targetAge(0)
55 	, lastPC1(-1)
56 	, lastPC2(-1)
57 {}
58 
CAirCAI(CUnit * owner)59 CAirCAI::CAirCAI(CUnit* owner)
60 	: CMobileCAI(owner)
61 	, activeCommand(0)
62 	, targetAge(0)
63 	, lastPC1(-1)
64 	, lastPC2(-1)
65 {
66 	cancelDistance = 16000;
67 	CommandDescription c;
68 
69 	if (owner->unitDef->canAttack) {
70 		c.id = CMD_AREA_ATTACK;
71 		c.action = "areaattack";
72 		c.type = CMDTYPE_ICON_AREA;
73 		c.name = "Area attack";
74 		c.mouseicon = c.name;
75 		c.tooltip = "Sets the aircraft to attack enemy units within a circle";
76 		possibleCommands.push_back(c);
77 	}
78 
79 	basePos = owner->pos;
80 	goalPos = owner->pos;
81 }
82 
GiveCommandReal(const Command & c,bool fromSynced)83 void CAirCAI::GiveCommandReal(const Command& c, bool fromSynced)
84 {
85 	// take care not to allow aircraft to be ordered to move out of the map
86 	if ((c.GetID() != CMD_MOVE) && !AllowedCommand(c, true)) {
87 		return;
88 	} else if (c.GetID() == CMD_MOVE && c.params.size() >= 3 &&
89 			(c.params[0] < 0.0f || c.params[2] < 0.0f
90 			 || c.params[0] > gs->mapx*SQUARE_SIZE
91 			 || c.params[2] > gs->mapy*SQUARE_SIZE))
92 	{
93 		return;
94 	}
95 
96 	if (c.GetID() == CMD_SET_WANTED_MAX_SPEED) {
97 		return;
98 	}
99 
100 	{
101 		CStrafeAirMoveType* airMT = GetStrafeAirMoveType(owner);
102 
103 		if (c.GetID() == CMD_AUTOREPAIRLEVEL) {
104 			if (c.params.empty())
105 				return;
106 
107 			switch ((int) c.params[0]) {
108 				case 0: { airMT->SetRepairBelowHealth(0.0f); break; }
109 				case 1: { airMT->SetRepairBelowHealth(0.3f); break; }
110 				case 2: { airMT->SetRepairBelowHealth(0.5f); break; }
111 				case 3: { airMT->SetRepairBelowHealth(0.8f); break; }
112 			}
113 
114 			for (unsigned int n = 0; n < possibleCommands.size(); n++) {
115 				if (possibleCommands[n].id != CMD_AUTOREPAIRLEVEL)
116 					continue;
117 
118 				possibleCommands[n].params[0] = IntToString(int(c.params[0]), "%d");
119 				break;
120 			}
121 
122 			selectedUnitsHandler.PossibleCommandChange(owner);
123 			return;
124 		}
125 
126 		if (c.GetID() == CMD_IDLEMODE) {
127 			if (c.params.empty())
128 				return;
129 
130 			switch ((int) c.params[0]) {
131 				case 0: { airMT->autoLand = false; break; }
132 				case 1: { airMT->autoLand = true;  break; }
133 			}
134 
135 			for (unsigned int n = 0; n < possibleCommands.size(); n++) {
136 				if (possibleCommands[n].id != CMD_IDLEMODE)
137 					continue;
138 
139 				possibleCommands[n].params[0] = IntToString(int(c.params[0]), "%d");
140 				break;
141 			}
142 
143 			selectedUnitsHandler.PossibleCommandChange(owner);
144 			return;
145 		}
146 	}
147 
148 	if (!(c.options & SHIFT_KEY)
149 			&& nonQueingCommands.find(c.GetID()) == nonQueingCommands.end())
150 	{
151 		activeCommand = 0;
152 		tempOrder = false;
153 	}
154 
155 	if (c.GetID() == CMD_AREA_ATTACK && c.params.size() < 4) {
156 		Command c2(CMD_ATTACK, c.options);
157 		c2.params = c.params;
158 		CCommandAI::GiveAllowedCommand(c2);
159 		return;
160 	}
161 
162 	CCommandAI::GiveAllowedCommand(c);
163 }
164 
SlowUpdate()165 void CAirCAI::SlowUpdate()
166 {
167 	// Commands issued may invoke SlowUpdate when paused
168 	if (gs->paused)
169 		return;
170 
171 	if (!commandQue.empty() && (commandQue.front().timeOut < gs->frameNum)) {
172 		FinishCommand();
173 		return;
174 	}
175 
176 	// avoid the invalid (CStrafeAirMoveType*) cast
177 	if (owner->UsingScriptMoveType())
178 		return;
179 
180 	const bool wantToRefuel = (LandRepairIfNeeded() || RefuelIfNeeded());
181 
182 	#if (AUTO_GENERATE_ATTACK_ORDERS == 1)
183 	if (commandQue.empty()) {
184 		if (!AirAutoGenerateTarget(GetStrafeAirMoveType(owner))) {
185 			// if no target found, queue is still empty so bail now
186 			return;
187 		}
188 	}
189 	#endif
190 
191 	// FIXME: check owner->UsingScriptMoveType() and skip rest if true?
192 	AAirMoveType* myPlane = GetStrafeAirMoveType(owner);
193 	Command& c = commandQue.front();
194 
195 	if (c.GetID() == CMD_WAIT) {
196 		if ((myPlane->aircraftState == AAirMoveType::AIRCRAFT_FLYING)
197 		    	&& !owner->unitDef->DontLand() && myPlane->autoLand)
198 		{
199 			StopMove();
200 		}
201 		return;
202 	}
203 
204 	if (c.GetID() != CMD_STOP && c.GetID() != CMD_AUTOREPAIRLEVEL &&
205 		c.GetID() != CMD_IDLEMODE && c.GetID() != CMD_SET_WANTED_MAX_SPEED)
206 	{
207 		myPlane->Takeoff();
208 	}
209 
210 	if (wantToRefuel) {
211 		switch (c.GetID()) {
212 			case CMD_AREA_ATTACK:
213 			case CMD_ATTACK:
214 			case CMD_FIGHT:
215 				return;
216 		}
217 	}
218 
219 	switch (c.GetID()) {
220 		case CMD_AREA_ATTACK: {
221 			ExecuteAreaAttack(c);
222 			return;
223 		}
224 		default: {
225 			CMobileCAI::Execute();
226 			return;
227 		}
228 	}
229 }
230 
AirAutoGenerateTarget(AAirMoveType * myPlane)231 bool CAirCAI::AirAutoGenerateTarget(AAirMoveType* myPlane) {
232 	assert(commandQue.empty());
233 	assert(myPlane->owner == owner);
234 
235 	const UnitDef* ownerDef = owner->unitDef;
236 	const bool autoLand = !ownerDef->DontLand() && myPlane->autoLand;
237 	const bool autoAttack = ((owner->fireState >= FIRESTATE_FIREATWILL) && (owner->moveState != MOVESTATE_HOLDPOS));
238 
239 	if (myPlane->aircraftState == AAirMoveType::AIRCRAFT_FLYING && autoLand) {
240 		StopMove();
241 	}
242 
243 	if (ownerDef->canAttack && autoAttack && owner->maxRange > 0) {
244 		if (ownerDef->IsFighterAirUnit()) {
245 			const float3 P = owner->pos + (owner->speed * 10.0);
246 			const float R = 1000.0f * owner->moveState;
247 			const CUnit* enemy = CGameHelper::GetClosestEnemyAircraft(NULL, P, R, owner->allyteam);
248 
249 			if (IsValidTarget(enemy)) {
250 				Command nc(CMD_ATTACK, INTERNAL_ORDER, enemy->id);
251 				commandQue.push_front(nc);
252 				inCommand = false;
253 				return true;
254 			}
255 		} else {
256 			const float3 P = owner->pos + (owner->speed * 20.0f);
257 			const float R = 500.0f * owner->moveState;
258 			const CUnit* enemy = CGameHelper::GetClosestValidTarget(P, R, owner->allyteam, this);
259 
260 			if (enemy != NULL) {
261 				Command nc(CMD_ATTACK, INTERNAL_ORDER, enemy->id);
262 				commandQue.push_front(nc);
263 				inCommand = false;
264 				return true;
265 			}
266 		}
267 	}
268 
269 	return false;
270 }
271 
272 
273 
ExecuteFight(Command & c)274 void CAirCAI::ExecuteFight(Command& c)
275 {
276 	assert((c.options & INTERNAL_ORDER) || owner->unitDef->canFight);
277 
278 	// FIXME: check owner->UsingScriptMoveType() and skip rest if true?
279 	AAirMoveType* myPlane = GetStrafeAirMoveType(owner);
280 
281 	assert(owner == myPlane->owner);
282 
283 	if (tempOrder) {
284 		tempOrder = false;
285 		inCommand = true;
286 	}
287 
288 	if (c.params.size() < 3) {
289 		LOG_L(L_ERROR, "[ACAI::%s][f=%d][id=%d] CMD_FIGHT #params < 3", __FUNCTION__, gs->frameNum, owner->id);
290 		return;
291 	}
292 
293 	if (c.params.size() >= 6) {
294 		if (!inCommand) {
295 			commandPos1 = c.GetPos(3);
296 		}
297 	} else {
298 		// HACK to make sure the line (commandPos1,commandPos2) is NOT
299 		// rotated (only shortened) if we reach this because the previous return
300 		// fight command finished by the 'if((curPos-pos).SqLength2D()<(127*127)){'
301 		// condition, but is actually updated correctly if you click somewhere
302 		// outside the area close to the line (for a new command).
303 		commandPos1 = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
304 
305 		if ((owner->pos - commandPos1).SqLength2D() > (150 * 150)) {
306 			commandPos1 = owner->pos;
307 		}
308 	}
309 
310 	goalPos = c.GetPos(0);
311 
312 	if (!inCommand) {
313 		inCommand = true;
314 		commandPos2 = goalPos;
315 	}
316 	if (c.params.size() >= 6) {
317 		goalPos = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
318 	}
319 
320 	// CMD_FIGHT is pretty useless if !canAttack, but we try to honour the modders wishes anyway...
321 	if (owner->unitDef->canAttack && (owner->fireState >= FIRESTATE_FIREATWILL)
322 			&& (owner->moveState != MOVESTATE_HOLDPOS) && (owner->maxRange > 0))
323 	{
324 		CUnit* enemy = NULL;
325 
326 		if (owner->unitDef->IsFighterAirUnit()) {
327 			const float3 P = ClosestPointOnLine(commandPos1, commandPos2, owner->pos + owner->speed*10);
328 			const float R = 1000.0f * owner->moveState;
329 
330 			enemy = CGameHelper::GetClosestEnemyAircraft(NULL, P, R, owner->allyteam);
331 		}
332 		if (IsValidTarget(enemy) && (owner->moveState != MOVESTATE_MANEUVER
333 				|| LinePointDist(commandPos1, commandPos2, enemy->pos) < 1000))
334 		{
335 			// make the attack-command inherit <c>'s options
336 			// (if <c> is internal, then so are the attacks)
337 			//
338 			// this is needed because CWeapon code will not
339 			// fire on "internal" targets if the weapon has
340 			// noAutoTarget set (although the <enemy> CUnit*
341 			// is technically not a user-target, we treat it
342 			// as such) even when explicitly told to fight
343 			Command nc(CMD_ATTACK, c.options, enemy->id);
344 			commandQue.push_front(nc);
345 
346 			tempOrder = true;
347 			inCommand = false;
348 
349 			if (lastPC1 != gs->frameNum) { // avoid infinite loops
350 				lastPC1 = gs->frameNum;
351 				SlowUpdate();
352 			}
353 			return;
354 		} else {
355 			const float3 P = ClosestPointOnLine(commandPos1, commandPos2, owner->pos + owner->speed * 20);
356 			const float R = 500.0f * owner->moveState;
357 
358 			enemy = CGameHelper::GetClosestValidTarget(P, R, owner->allyteam, this);
359 
360 			if (enemy != NULL) {
361 				PushOrUpdateReturnFight();
362 
363 				// make the attack-command inherit <c>'s options
364 				Command nc(CMD_ATTACK, c.options, enemy->id);
365 				commandQue.push_front(nc);
366 
367 				tempOrder = true;
368 				inCommand = false;
369 
370 				// avoid infinite loops (?)
371 				if (lastPC2 != gs->frameNum) {
372 					lastPC2 = gs->frameNum;
373 					SlowUpdate();
374 				}
375 				return;
376 			}
377 		}
378 	}
379 
380 	myPlane->goalPos = goalPos;
381 
382 	const CStrafeAirMoveType* airMT = (!owner->UsingScriptMoveType())? static_cast<const CStrafeAirMoveType*>(myPlane): NULL;
383 	const float radius = (airMT != NULL)? std::max(airMT->turnRadius + 2*SQUARE_SIZE, 128.f) : 127.f;
384 
385 	// we're either circling or will get to the target in 8 frames
386 	if ((owner->pos - goalPos).SqLength2D() < (radius * radius)
387 			|| (owner->pos + owner->speed*8 - goalPos).SqLength2D() < 127*127)
388 	{
389 		FinishCommand();
390 	}
391 }
392 
ExecuteAttack(Command & c)393 void CAirCAI::ExecuteAttack(Command& c)
394 {
395 	assert(owner->unitDef->canAttack);
396 	targetAge++;
397 
398 	if (tempOrder && owner->moveState == MOVESTATE_MANEUVER) {
399 		// limit how far away we fly
400 		if (orderTarget && LinePointDist(commandPos1, commandPos2, orderTarget->pos) > 1500) {
401 			owner->AttackUnit(NULL, false, false);
402 			FinishCommand();
403 			return;
404 		}
405 	}
406 
407 	if (inCommand) {
408 		if (targetDied || (c.params.size() == 1 && UpdateTargetLostTimer(int(c.params[0])) == 0)) {
409 			FinishCommand();
410 			return;
411 		}
412 		if (orderTarget != NULL) {
413 			if (orderTarget->unitDef->canfly && orderTarget->IsCrashing()) {
414 				owner->AttackUnit(NULL, false, false);
415 				FinishCommand();
416 				return;
417 			}
418 			if (!(c.options & ALT_KEY) && SkipParalyzeTarget(orderTarget)) {
419 				owner->AttackUnit(NULL, false, false);
420 				FinishCommand();
421 				return;
422 			}
423 		}
424 	} else {
425 		targetAge = 0;
426 
427 		if (c.params.size() == 1) {
428 			CUnit* targetUnit = unitHandler->GetUnit(c.params[0]);
429 
430 			if (targetUnit == NULL) { FinishCommand(); return; }
431 			if (targetUnit == owner) { FinishCommand(); return; }
432 			if (targetUnit->GetTransporter() != NULL && !modInfo.targetableTransportedUnits) {
433 				FinishCommand(); return;
434 			}
435 
436 			SetGoal(targetUnit->pos, owner->pos, cancelDistance);
437 			SetOrderTarget(targetUnit);
438 			owner->AttackUnit(targetUnit, (c.options & INTERNAL_ORDER) == 0, false);
439 
440 			inCommand = true;
441 		} else {
442 			SetGoal(c.GetPos(0), owner->pos, cancelDistance);
443 			owner->AttackGround(c.GetPos(0), (c.options & INTERNAL_ORDER) == 0, false);
444 
445 			inCommand = true;
446 		}
447 	}
448 }
449 
ExecuteAreaAttack(Command & c)450 void CAirCAI::ExecuteAreaAttack(Command& c)
451 {
452 	assert(owner->unitDef->canAttack);
453 
454 	// FIXME: check owner->UsingScriptMoveType() and skip rest if true?
455 	AAirMoveType* myPlane = GetStrafeAirMoveType(owner);
456 
457 	if (targetDied) {
458 		targetDied = false;
459 		inCommand = false;
460 	}
461 
462 	const float3& pos = c.GetPos(0);
463 	const float radius = c.params[3];
464 
465 	if (inCommand) {
466 		if (myPlane->aircraftState == AAirMoveType::AIRCRAFT_LANDED)
467 			inCommand = false;
468 
469 		if (orderTarget && orderTarget->pos.SqDistance2D(pos) > Square(radius)) {
470 			inCommand = false;
471 
472 			// target wandered out of the attack-area
473 			SetOrderTarget(NULL);
474 			SelectNewAreaAttackTargetOrPos(c);
475 		}
476 	} else {
477 		if (myPlane->aircraftState != AAirMoveType::AIRCRAFT_LANDED) {
478 			inCommand = true;
479 
480 			SelectNewAreaAttackTargetOrPos(c);
481 		}
482 	}
483 }
484 
ExecuteGuard(Command & c)485 void CAirCAI::ExecuteGuard(Command& c)
486 {
487 	assert(owner->unitDef->canGuard);
488 
489 	const CUnit* guardee = unitHandler->GetUnit(c.params[0]);
490 
491 	if (guardee == NULL) { FinishCommand(); return; }
492 	if (UpdateTargetLostTimer(guardee->id) == 0) { FinishCommand(); return; }
493 	if (guardee->outOfMapTime > (GAME_SPEED * 5)) { FinishCommand(); return; }
494 
495 	const bool pushAttackCommand =
496 		(owner->maxRange > 0.0f) &&
497 		owner->unitDef->canAttack &&
498 		((guardee->lastAttackFrame + 40) < gs->frameNum) &&
499 		IsValidTarget(guardee->lastAttacker);
500 
501 	if (pushAttackCommand) {
502 		Command nc(CMD_ATTACK, c.options | INTERNAL_ORDER, guardee->lastAttacker->id);
503 		commandQue.push_front(nc);
504 		SlowUpdate();
505 	} else {
506 		Command c2(CMD_MOVE, c.options | INTERNAL_ORDER);
507 		c2.timeOut = gs->frameNum + 60;
508 
509 		if (guardee->pos.IsInBounds()) {
510 			c2.PushPos(guardee->pos);
511 		} else {
512 			float3 clampedGuardeePos = guardee->pos;
513 
514 			clampedGuardeePos.ClampInBounds();
515 
516 			c2.PushPos(clampedGuardeePos);
517 		}
518 
519 		commandQue.push_front(c2);
520 	}
521 }
522 
GetDefaultCmd(const CUnit * pointed,const CFeature * feature)523 int CAirCAI::GetDefaultCmd(const CUnit* pointed, const CFeature* feature)
524 {
525 	if (pointed) {
526 		if (!teamHandler->Ally(gu->myAllyTeam, pointed->allyteam)) {
527 			if (owner->unitDef->canAttack) {
528 				return CMD_ATTACK;
529 			}
530 		} else {
531 			if (owner->unitDef->canGuard) {
532 				return CMD_GUARD;
533 			}
534 		}
535 	}
536 	return CMD_MOVE;
537 }
538 
IsValidTarget(const CUnit * enemy) const539 bool CAirCAI::IsValidTarget(const CUnit* enemy) const {
540 	if (!CMobileCAI::IsValidTarget(enemy)) return false;
541 	if (enemy->IsCrashing()) return false;
542 	return (GetStrafeAirMoveType(owner)->isFighter || !enemy->unitDef->canfly);
543 }
544 
545 
546 
FinishCommand()547 void CAirCAI::FinishCommand()
548 {
549 	targetAge = 0;
550 	CCommandAI::FinishCommand();
551 }
552 
BuggerOff(const float3 & pos,float radius)553 void CAirCAI::BuggerOff(const float3& pos, float radius)
554 {
555 	if (!owner->UsingScriptMoveType()) {
556 		static_cast<AAirMoveType*>(owner->moveType)->Takeoff();
557 	} else {
558 		CMobileCAI::BuggerOff(pos, radius);
559 	}
560 }
561 
SetGoal(const float3 & pos,const float3 & curPos,float goalRadius)562 void CAirCAI::SetGoal(const float3& pos, const float3& curPos, float goalRadius)
563 {
564 	owner->moveType->SetGoal(pos);
565 	CMobileCAI::SetGoal(pos, curPos, goalRadius);
566 }
567 
SelectNewAreaAttackTargetOrPos(const Command & ac)568 bool CAirCAI::SelectNewAreaAttackTargetOrPos(const Command& ac) {
569 	assert(ac.GetID() == CMD_AREA_ATTACK || (ac.GetID() == CMD_ATTACK && ac.GetParamsCount() >= 3));
570 
571 	if (ac.GetID() == CMD_ATTACK) {
572 		FinishCommand();
573 		return false;
574 	}
575 
576 	const float3& pos = ac.GetPos(0);
577 	const float radius = ac.params[3];
578 
579 	std::vector<int> enemyUnitIDs;
580 	CGameHelper::GetEnemyUnits(pos, radius, owner->allyteam, enemyUnitIDs);
581 
582 	if (enemyUnitIDs.empty()) {
583 		float3 attackPos = pos + (gs->randVector() * radius);
584 		attackPos.y = CGround::GetHeightAboveWater(attackPos.x, attackPos.z);
585 
586 		owner->AttackGround(attackPos, (ac.options & INTERNAL_ORDER) == 0, false);
587 		SetGoal(attackPos, owner->pos);
588 	} else {
589 		// note: the range of randFloat() is inclusive of 1.0f
590 		const unsigned int unitIdx = std::min<int>(gs->randFloat() * enemyUnitIDs.size(), enemyUnitIDs.size() - 1);
591 		const unsigned int unitID = enemyUnitIDs[unitIdx];
592 
593 		CUnit* targetUnit = unitHandler->GetUnitUnsafe(unitID);
594 
595 		SetOrderTarget(targetUnit);
596 		owner->AttackUnit(targetUnit, (ac.options & INTERNAL_ORDER) == 0, false);
597 		SetGoal(targetUnit->pos, owner->pos);
598 	}
599 
600 	return true;
601 }
602 
603