1 #include "Strategic_Movement.h"
2 #include "Animation_Data.h"
3 #include "Auto_Resolve.h"
4 #include "BloodCatSpawnsModel.h"
5 #include "Campaign.h"
6 #include "ContentManager.h"
7 #include "Dialogue_Control.h"
8 #include "Faces.h"
9 #include "FileMan.h"
10 #include "Font_Control.h"
11 #include "Game_Clock.h"
12 #include "Game_Events.h"
13 #include "GameInstance.h"
14 #include "GameSettings.h"
15 #include "Inventory_Choosing.h"
16 #include "Items.h"
17 #include "JAScreens.h"
18 #include "LoadSaveData.h"
19 #include "Map_Information.h"
20 #include "Map_Screen_Helicopter.h"
21 #include "Map_Screen_Interface.h"
22 #include "Map_Screen_Interface_Border.h"
23 #include "Map_Screen_Interface_Bottom.h"
24 #include "MapScreen.h"
25 #include "Meanwhile.h"
26 #include "MercProfile.h"
27 #include "Message.h"
28 #include "Music_Control.h"
29 #include "Overhead.h"
30 #include "Player_Command.h"
31 #include "PreBattle_Interface.h"
32 #include "Queen_Command.h"
33 #include "Quests.h"
34 #include "Soldier_Macros.h"
35 #include "Squads.h"
36 #include "Strategic.h"
37 #include "StrategicMap_Secrets.h"
38 #include "Strategic_AI.h"
39 #include "Strategic_Pathing.h"
40 #include "Tactical_Save.h"
41 #include "Text.h"
42 #include "Town_Militia.h"
43 #include "Video.h"
44 #include <algorithm>
45 #include <iterator>
46 #include <stdexcept>
47 #include <string_theory/format>
48 #include <string_theory/string>
49 
50 
51 // the delay for a group about to arrive
52 #define ABOUT_TO_ARRIVE_DELAY 5
53 
54 GROUP *gpGroupList;
55 
56 static GROUP* gpPendingSimultaneousGroup = NULL;
57 
58 extern BOOLEAN gubNumAwareBattles;
59 extern INT8 SquadMovementGroups[ ];
60 extern INT8 gubVehicleMovementGroups[ ];
61 
62 BOOLEAN gfDelayAutoResolveStart = FALSE;
63 
64 
65 static BOOLEAN gfRandomizingPatrolGroup = FALSE;
66 
67 UINT8 gubNumGroupsArrivedSimultaneously = 0;
68 
69 //Doesn't require text localization.  This is for debug strings only.
70 static const char* const gszTerrain[NUM_TRAVTERRAIN_TYPES] =
71 {
72 	"TOWN",
73 	"ROAD",
74 	"PLAINS",
75 	"SAND",
76 	"SPARSE",
77 	"DENSE",
78 	"SWAMP",
79 	"WATER",
80 	"HILLS",
81 	"GROUNDBARRIER",
82 	"NS_RIVER",
83 	"EW_RIVER",
84 	"EDGEOFWORLD"
85 };
86 
87 BOOLEAN gfUndergroundTacticalTraversal = FALSE;
88 
89 // remembers which player group is the Continue/Stop prompt about?  No need to save as long as you can't save while prompt ON
90 static GROUP* gpGroupPrompting = NULL;
91 
92 static UINT32 uniqueIDMask[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
93 
94 
95 static GROUP* gpInitPrebattleGroup = NULL;
96 
97 // waiting for input from user
98 static BOOLEAN gfWaitingForInput = FALSE;
99 
100 
101 static UINT8 AddGroupToList(GROUP* pGroup);
102 
103 
104 //Player grouping functions
105 //.........................
106 //Creates a new player group, returning the unique ID of that group.  This is the first
107 //step before adding waypoints and members to the player group.
CreateNewPlayerGroupDepartingFromSector(UINT8 const ubSectorX,UINT8 const ubSectorY)108 GROUP* CreateNewPlayerGroupDepartingFromSector(UINT8 const ubSectorX, UINT8 const ubSectorY)
109 {
110 	AssertMsg( ubSectorX >= 1 && ubSectorX <= 16, String( "CreateNewPlayerGroup with out of range sectorX value of %d", ubSectorX ) );
111 	AssertMsg( ubSectorY >= 1 && ubSectorY <= 16, String( "CreateNewPlayerGroup with out of range sectorY value of %d", ubSectorY ) );
112 	GROUP* const pNew = new GROUP{};
113 	pNew->pPlayerList = NULL;
114 	pNew->pWaypoints = NULL;
115 	pNew->ubSectorX = pNew->ubNextX = ubSectorX;
116 	pNew->ubSectorY = pNew->ubNextY = ubSectorY;
117 	pNew->ubOriginalSector = (UINT8)SECTOR( ubSectorX, ubSectorY );
118 	pNew->fPlayer = TRUE;
119 	pNew->ubMoveType = ONE_WAY;
120 	pNew->ubNextWaypointID = 0;
121 	pNew->ubTransportationMask = FOOT;
122 	pNew->fVehicle = FALSE;
123 	pNew->ubCreatedSectorID = pNew->ubOriginalSector;
124 	pNew->ubSectorIDOfLastReassignment = 255;
125 
126 	AddGroupToList(pNew);
127 	return pNew;
128 }
129 
130 
CreateNewVehicleGroupDepartingFromSector(UINT8 const ubSectorX,UINT8 const ubSectorY)131 GROUP* CreateNewVehicleGroupDepartingFromSector(UINT8 const ubSectorX, UINT8 const ubSectorY)
132 {
133 	AssertMsg( ubSectorX >= 1 && ubSectorX <= 16, String( "CreateNewVehicleGroup with out of range sectorX value of %d", ubSectorX ) );
134 	AssertMsg( ubSectorY >= 1 && ubSectorY <= 16, String( "CreateNewVehicleGroup with out of range sectorY value of %d", ubSectorY ) );
135 	GROUP* const pNew = new GROUP{};
136 	pNew->pWaypoints = NULL;
137 	pNew->ubSectorX = pNew->ubNextX = ubSectorX;
138 	pNew->ubSectorY = pNew->ubNextY = ubSectorY;
139 	pNew->ubOriginalSector = (UINT8)SECTOR( ubSectorX, ubSectorY );
140 	pNew->ubMoveType = ONE_WAY;
141 	pNew->ubNextWaypointID = 0;
142 	pNew->fVehicle = TRUE;
143 	pNew->fPlayer = TRUE;
144 	pNew->pPlayerList = NULL;
145 	pNew->ubCreatedSectorID = pNew->ubOriginalSector;
146 	pNew->ubSectorIDOfLastReassignment = 255;
147 
148 	// get the type
149 	pNew->ubTransportationMask = CAR;
150 
151 	AddGroupToList(pNew);
152 	return pNew;
153 }
154 
155 
AddPlayerToGroup(GROUP & g,SOLDIERTYPE & s)156 void AddPlayerToGroup(GROUP& g, SOLDIERTYPE& s)
157 {
158 	AssertMsg(g.fPlayer, "Attempting AddPlayerToGroup() on an ENEMY group!");
159 
160 	PLAYERGROUP* const p = new PLAYERGROUP{};
161 	p->pSoldier = &s;
162 	p->next     = 0;
163 
164 	s.ubGroupID = g.ubGroupID;
165 
166 	PLAYERGROUP* i = g.pPlayerList;
167 	if (!i)
168 	{
169 		g.pPlayerList = p;
170 		g.ubGroupSize = 1;
171 		g.ubPrevX     = s.ubPrevSectorID % 16 + 1;
172 		g.ubPrevY     = s.ubPrevSectorID / 16 + 1;
173 		g.ubSectorX   = s.sSectorX;
174 		g.ubSectorY   = s.sSectorY;
175 		g.ubSectorZ   = s.bSectorZ;
176 	}
177 	else
178 	{
179 		for (; i->next; i = i->next)
180 		{
181 			AssertMsg(i->pSoldier->ubProfile != s.ubProfile, String("Attempting to add an already existing merc to group (ubProfile=%d).", s.ubProfile));
182 		}
183 		i->next = p;
184 
185 		++g.ubGroupSize;
186 	}
187 }
188 
189 
190 static void CancelEmptyPersistentGroupMovement(GROUP&);
191 
192 
RemovePlayerFromPGroup(GROUP & g,SOLDIERTYPE & s)193 void RemovePlayerFromPGroup(GROUP& g, SOLDIERTYPE& s)
194 {
195 	AssertMsg(g.fPlayer, "Attempting RemovePlayerFromGroup() on an ENEMY group!");
196 
197 	for (PLAYERGROUP** i = &g.pPlayerList; *i; i = &(*i)->next)
198 	{
199 		PLAYERGROUP* const p = *i;
200 		if (p->pSoldier != &s) continue;
201 
202 		*i = p->next;
203 		delete p;
204 
205 		s.ubPrevSectorID = SECTOR(g.ubPrevX, g.ubPrevY);
206 		s.ubGroupID      = 0;
207 
208 		if (--g.ubGroupSize == 0)
209 		{
210 			if (!g.fPersistant)
211 			{
212 				RemoveGroup(g);
213 			}
214 			else
215 			{
216 				CancelEmptyPersistentGroupMovement(g);
217 			}
218 		}
219 		break;
220 	}
221 }
222 
223 
RemovePlayerFromGroup(SOLDIERTYPE & s)224 void RemovePlayerFromGroup(SOLDIERTYPE& s)
225 {
226 	GROUP* const pGroup = GetGroup(s.ubGroupID);
227 
228 	//KM : August 6, 1999 Patch fix
229 	//     Because the release build has no assertions, it was still possible for the group to be null,
230 	//     causing a crash.  Instead of crashing, it'll simply return false.
231 	if (!pGroup) return;
232 	//end
233 
234 	AssertMsg(pGroup, String("Attempting to RemovePlayerFromGroup( %d, %d ) from non-existant group", s.ubGroupID, s.ubProfile));
235 
236 	RemovePlayerFromPGroup(*pGroup, s);
237 }
238 
239 
240 static void SetLocationOfAllPlayerSoldiersInGroup(GROUP const&, INT16 x, INT16 y, INT8 z);
241 
242 
GroupReversingDirectionsBetweenSectors(GROUP * pGroup,UINT8 ubSectorX,UINT8 ubSectorY,BOOLEAN fBuildingWaypoints)243 BOOLEAN GroupReversingDirectionsBetweenSectors( GROUP *pGroup, UINT8 ubSectorX, UINT8 ubSectorY, BOOLEAN fBuildingWaypoints )
244 {
245 	// if we're not between sectors, or we are but we're continuing in the same direction as before
246 	if ( !GroupBetweenSectorsAndSectorXYIsInDifferentDirection( pGroup, ubSectorX, ubSectorY ) )
247 	{
248 		// then there's no need to reverse directions
249 		return FALSE;
250 	}
251 
252 	//The new direction is reversed, so we have to go back to the sector we just left.
253 
254 	//Search for the arrival event, and kill it!
255 	DeleteStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->ubGroupID );
256 
257 	//Adjust the information in the group to reflect the new movement.
258 	pGroup->ubPrevX = pGroup->ubNextX;
259 	pGroup->ubPrevY = pGroup->ubNextY;
260 	pGroup->ubNextX = pGroup->ubSectorX;
261 	pGroup->ubNextY = pGroup->ubSectorY;
262 	pGroup->ubSectorX = pGroup->ubPrevX;
263 	pGroup->ubSectorY = pGroup->ubPrevY;
264 
265 	if( pGroup->fPlayer )
266 	{
267 		// ARM: because we've changed the group's ubSectoryX and ubSectorY, we must now also go and change the sSectorX and
268 		// sSectorY of all the soldiers in this group so that they stay in synch.  Otherwise pathing and movement problems
269 		// will result since the group is in one place while the merc is in another...
270 		SetLocationOfAllPlayerSoldiersInGroup(*pGroup, pGroup->ubSectorX, pGroup->ubSectorY, 0);
271 	}
272 
273 
274 	// IMPORTANT: The traverse time doesn't change just because we reverse directions!  It takes the same time no matter
275 	// which direction you're going in!  This becomes critical in case the player reverse directions again before moving!
276 
277 	// The time it takes to arrive there will be exactly the amount of time we have been moving away from it.
278 	pGroup->setArrivalTime(pGroup->uiTraverseTime - pGroup->uiArrivalTime + GetWorldTotalMin() * 2);
279 
280 	// if they're not already there
281 	if( pGroup->uiArrivalTime > GetWorldTotalMin() )
282 	{
283 		//Post the replacement event to move back to the previous sector!
284 		AddStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->uiArrivalTime, pGroup->ubGroupID );
285 
286 		if( pGroup->fPlayer )
287 		{
288 			if( ( pGroup->uiArrivalTime - ABOUT_TO_ARRIVE_DELAY ) > GetWorldTotalMin( ) )
289 			{
290 				// Post the about to arrive event
291 				AddStrategicEvent( EVENT_GROUP_ABOUT_TO_ARRIVE, pGroup->uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, pGroup->ubGroupID );
292 			}
293 		}
294 	}
295 	else
296 	{
297 		// IMPORTANT: this can't be called during RebuildWayPointsForGroupPath(), since it will clear the mercpath
298 		// prematurely by assuming the mercs are now at their final destination when only the first waypoint is in place!!!
299 		// To handle this situation, RebuildWayPointsForGroupPath() will issue it's own call after it's ready for it.
300 		if ( !fBuildingWaypoints )
301 		{
302 			// never really left.  Must set check for battle TRUE in order for HandleNonCombatGroupArrival() to run!
303 			GroupArrivedAtSector(*pGroup, TRUE, TRUE);
304 		}
305 	}
306 
307 
308 	return TRUE;
309 }
310 
311 
312 
GroupBetweenSectorsAndSectorXYIsInDifferentDirection(GROUP * pGroup,UINT8 ubSectorX,UINT8 ubSectorY)313 BOOLEAN GroupBetweenSectorsAndSectorXYIsInDifferentDirection( GROUP *pGroup, UINT8 ubSectorX, UINT8 ubSectorY )
314 {
315 	INT32 currDX, currDY, newDX, newDY;
316 	UINT8 ubNumUnalignedAxes = 0;
317 
318 
319 	if( !pGroup->fBetweenSectors )
320 		return( FALSE );
321 
322 
323 	// Determine the direction the group is currently traveling in
324 	currDX = pGroup->ubNextX - pGroup->ubSectorX;
325 	currDY = pGroup->ubNextY - pGroup->ubSectorY;
326 
327 	//Determine the direction the group would need to travel in to reach the given sector
328 	newDX = ubSectorX - pGroup->ubSectorX;
329 	newDY = ubSectorY - pGroup->ubSectorY;
330 
331 	// clip the new dx/dy values to +/- 1
332 	if( newDX )
333 	{
334 		ubNumUnalignedAxes++;
335 		newDX /= ABS( newDX );
336 	}
337 	if( newDY )
338 	{
339 		ubNumUnalignedAxes++;
340 		newDY /= ABS( newDY );
341 	}
342 
343 	// error checking
344 	if( ubNumUnalignedAxes > 1 )
345 	{
346 		AssertMsg( FALSE, String( "Checking a diagonal move for direction change, groupID %d. AM-0", pGroup->ubGroupID ) );
347 		return FALSE;
348 	}
349 
350 	// Compare the dx/dy's.  If they're exactly the same, group is travelling in the same direction as before, so we're not
351 	// changing directions.
352 	// Note that 90-degree orthogonal changes are considered changing direction, as well as the full 180-degree reversal.
353 	// That's because the party must return to the previous sector in each of those cases, too.
354 	if( currDX == newDX && currDY == newDY )
355 		return( FALSE );
356 
357 
358 	// yes, we're between sectors, and we'd be changing direction to go to the given sector
359 	return( TRUE );
360 }
361 
362 
363 static void InitiateGroupMovementToNextSector(GROUP* pGroup);
364 
365 
366 /* Appends a waypoint to the end of the list. Waypoint MUST be on the same
367  * horizontal or vertical level as the last waypoint added. */
AddWaypointToPGroup(GROUP * const g,UINT8 const x,UINT8 const y)368 BOOLEAN AddWaypointToPGroup(GROUP* const g, UINT8 const x, UINT8 const y) // Same, but overloaded
369 {
370 	AssertMsg(1 <= x && x <= 16, String("AddWaypointToPGroup with out of range sectorX value of %d", x));
371 	AssertMsg(1 <= y && y <= 16, String("AddWaypointToPGroup with out of range sectorY value of %d", y));
372 
373 	if (!g) return FALSE;
374 
375 	/* At this point, we have the group, and a valid coordinate. Now we must
376 	 * determine that this waypoint will be aligned exclusively to either the x or
377 	 * y axis of the last waypoint in the list. */
378 	UINT8     n_aligned_axes      = 0;
379 	bool      reversing_direction = FALSE;
380 	WAYPOINT* wp                  = g->pWaypoints;
381 	if (!wp)
382 	{
383 		if (GroupReversingDirectionsBetweenSectors(g, x, y, TRUE))
384 		{
385 			if (g->fPlayer)
386 			{ /* Because we reversed, we must add the new current sector back at the
387 				 * head of everyone's mercpath */
388 				AddSectorToFrontOfMercPathForAllSoldiersInGroup(g, g->ubSectorX, g->ubSectorY);
389 			}
390 
391 			/* Very special case that requiring specific coding. Check out the
392 			 * comments at the above function for more information. */
393 			reversing_direction = TRUE;
394 			// ARM:  Kris - new rulez.  Must still fall through and add a waypoint anyway!!!
395 		}
396 		else
397 		{ // No waypoints, so compare against the current location.
398 			if (g->ubSectorX == x) ++n_aligned_axes;
399 			if (g->ubSectorY == y) ++n_aligned_axes;
400 		}
401 	}
402 	else
403 	{	//we do have a waypoint list, so go to the last entry
404 		while (wp->next)
405 		{
406 			wp = wp->next;
407 		}
408 		// Now, we are pointing to the last waypoint in the list
409 		if (wp->x == x) ++n_aligned_axes;
410 		if (wp->y == y) ++n_aligned_axes;
411 	}
412 
413 	if (!reversing_direction)
414 	{
415 		if (n_aligned_axes == 0)
416 		{
417 			AssertMsg(FALSE, String("Invalid DIAGONAL waypoint being added for groupID %d. AM-0", g->ubGroupID));
418 			return FALSE;
419 		}
420 
421 		if (n_aligned_axes >= 2)
422 		{
423 			AssertMsg(FALSE, String("Invalid IDENTICAL waypoint being added for groupID %d. AM-0", g->ubGroupID));
424 			return FALSE;
425 		}
426 
427 		// Has to be different in exactly 1 axis to be a valid new waypoint
428 		Assert(n_aligned_axes == 1);
429 	}
430 
431 	WAYPOINT* const new_wp = new WAYPOINT{};
432 	new_wp->x    = x;
433 	new_wp->y    = y;
434 	new_wp->next = 0;
435 
436 	if (wp)
437 	{ // Add the waypoint to the end of the list
438 		wp->next = new_wp;
439 	}
440 	else
441 	{ // We are adding the first waypoint.
442 		g->pWaypoints = new_wp;
443 
444 		/* Important: The first waypoint added actually initiates the group's
445 		 * movement to the next sector. */
446 		/* Don't do this if we have reversed directions! In that case, the required
447 		 * work has already been done back there */
448 		if (!reversing_direction)
449 		{ /* We need to calculate the next sector the group is moving to and post an
450 			 * event for it. */
451 			InitiateGroupMovementToNextSector(g);
452 		}
453 	}
454 
455 	if (g->fPlayer)
456 	{ // Also, nuke any previous "tactical traversal" information.
457 		CFOR_EACH_PLAYER_IN_GROUP(curr, g)
458 		{
459 			curr->pSoldier->ubStrategicInsertionCode = 0;
460 		}
461 	}
462 
463 	return TRUE;
464 }
465 
466 
467 // NOTE: This does NOT expect a strategic sector ID
AddWaypointIDToPGroup(GROUP * pGroup,UINT8 ubSectorID)468 BOOLEAN AddWaypointIDToPGroup( GROUP *pGroup, UINT8 ubSectorID )
469 {
470 	UINT8 ubSectorX, ubSectorY;
471 	ubSectorX = SECTORX( ubSectorID );
472 	ubSectorY = SECTORY( ubSectorID );
473 	return AddWaypointToPGroup( pGroup, ubSectorX, ubSectorY );
474 }
475 
476 
AddWaypointStrategicIDToPGroup(GROUP * pGroup,UINT32 uiSectorID)477 BOOLEAN AddWaypointStrategicIDToPGroup( GROUP *pGroup, UINT32 uiSectorID )
478 {
479 	UINT8 ubSectorX, ubSectorY;
480 	ubSectorX = ( UINT8 ) GET_X_FROM_STRATEGIC_INDEX( uiSectorID );
481 	ubSectorY = ( UINT8 ) GET_Y_FROM_STRATEGIC_INDEX( uiSectorID );
482 	return AddWaypointToPGroup( pGroup, ubSectorX, ubSectorY );
483 }
484 
485 
486 //Enemy grouping functions -- private use by the strategic AI.
487 //............................................................
CreateNewEnemyGroupDepartingFromSector(UINT32 uiSector,UINT8 ubNumAdmins,UINT8 ubNumTroops,UINT8 ubNumElites)488 GROUP* CreateNewEnemyGroupDepartingFromSector( UINT32 uiSector, UINT8 ubNumAdmins, UINT8 ubNumTroops, UINT8 ubNumElites )
489 {
490 	AssertMsg( uiSector <= 255, String( "CreateNewEnemyGroup with out of range value of %d", uiSector ) );
491 	GROUP* const pNew = new GROUP{};
492 	pNew->pEnemyGroup = new ENEMYGROUP{};
493 	pNew->pWaypoints = NULL;
494 	pNew->ubSectorX = (UINT8)SECTORX( uiSector );
495 	pNew->ubSectorY = (UINT8)SECTORY( uiSector );
496 	pNew->ubOriginalSector = (UINT8)uiSector;
497 	pNew->fPlayer = FALSE;
498 	pNew->ubMoveType = CIRCULAR;
499 	pNew->ubNextWaypointID = 0;
500 	pNew->pEnemyGroup->ubNumAdmins = ubNumAdmins;
501 	pNew->pEnemyGroup->ubNumTroops = ubNumTroops;
502 	pNew->pEnemyGroup->ubNumElites = ubNumElites;
503 	pNew->ubGroupSize = (UINT8)(ubNumTroops + ubNumElites);
504 	pNew->ubTransportationMask = FOOT;
505 	pNew->fVehicle = FALSE;
506 	pNew->ubCreatedSectorID = pNew->ubOriginalSector;
507 	pNew->ubSectorIDOfLastReassignment = 255;
508 
509 	AddGroupToList(pNew);
510 	return pNew;
511 }
512 
513 //INTERNAL LIST MANIPULATION FUNCTIONS
514 
515 //When adding any new group to the list, this is what must be done:
516 //1)  Find the first unused ID (unique)
517 //2)  Assign that ID to the new group
518 //3)  Insert the group at the end of the list.
AddGroupToList(GROUP * const g)519 static UINT8 AddGroupToList(GROUP* const g)
520 {
521 	// First, find a unique ID
522 	for (UINT8 id = 0; ++id;)
523 	{
524 		const UINT32 index = id / 32;
525 		const UINT32 bit   = id % 32;
526 		const UINT32 mask  = 1 << bit;
527 		if (uniqueIDMask[index] & mask) continue;
528 
529 		// Found a free id
530 		g->ubGroupID         = id;
531 		uniqueIDMask[index] |= mask;
532 
533 		// Append group to list
534 		GROUP** i = &gpGroupList;
535 		while (*i != NULL) i = &(*i)->next;
536 		*i = g;
537 
538 		return id;
539 	}
540 	throw std::runtime_error("Out of group IDs");
541 }
542 
543 
544 /* Destroys the waypoint list, detaches group from list, then deallocated the
545  * memory for the group */
RemoveGroupFromList(GROUP * const g)546 static void RemoveGroupFromList(GROUP* const g)
547 {
548 	for (GROUP** i = &gpGroupList; *i != NULL; i = &(*i)->next)
549 	{
550 		if (*i != g) continue;
551 
552 		// Found the group, so now remove it.
553 		*i = g->next;
554 
555 		// Clear the unique group ID
556 		const UINT32 index = g->ubGroupID / 32;
557 		const UINT32 bit   = g->ubGroupID % 32;
558 		const UINT32 mask  = 1 << bit;
559 		Assert(uniqueIDMask[index] & mask);
560 		uniqueIDMask[index] &= ~mask;
561 
562 		delete g;
563 		return;
564 	}
565 	SLOGA("Trying to remove a strategic group that isn't in the list!");
566 }
567 
568 
GetGroup(UINT8 ubGroupID)569 GROUP* GetGroup( UINT8 ubGroupID )
570 {
571 	FOR_EACH_GROUP(curr)
572 	{
573 		if( curr->ubGroupID == ubGroupID )
574 			return curr;
575 	}
576 	return NULL;
577 }
578 
579 
580 class CharacterDialogueEventBeginPrebattleInterface : public CharacterDialogueEvent
581 {
582 	public:
CharacterDialogueEventBeginPrebattleInterface(SOLDIERTYPE & soldier,GROUP * const initiating_battle_group)583 		CharacterDialogueEventBeginPrebattleInterface(SOLDIERTYPE& soldier, GROUP* const initiating_battle_group) :
584 			CharacterDialogueEvent(soldier),
585 			initiating_battle_group_(initiating_battle_group)
586 		{}
587 
Execute()588 		bool Execute()
589 		{
590 			if (!MayExecute()) return true;
591 
592 			SOLDIERTYPE const& s = soldier_;
593 			ExecuteCharacterDialogue(s.ubProfile, QUOTE_ENEMY_PRESENCE, s.face, DIALOGUE_TACTICAL_UI, TRUE, false);
594 
595 			// Setup face with data!
596 			FACETYPE& f = *gpCurrentTalkingFace;
597 			f.uiFlags                   |= FACE_TRIGGER_PREBATTLE_INT;
598 			f.u.initiating_battle.group  = initiating_battle_group_;
599 
600 			return false;
601 		}
602 
603 	private:
604 		GROUP* const initiating_battle_group_;
605 };
606 
607 
HandleImportantPBIQuote(SOLDIERTYPE & s,GROUP * const initiating_battle_group)608 static void HandleImportantPBIQuote(SOLDIERTYPE& s, GROUP* const initiating_battle_group)
609 {
610 	// Wake merc up for THIS quote
611 	bool const asleep = s.fMercAsleep;
612 	if (asleep) MakeCharacterDialogueEventSleep(s, false);
613 	DialogueEvent::Add(new CharacterDialogueEventBeginPrebattleInterface(s, initiating_battle_group));
614 	if (asleep) MakeCharacterDialogueEventSleep(s, true);
615 }
616 
617 
618 //If this is called, we are setting the game up to bring up the prebattle interface.  Before doing so,
619 //one of the involved mercs will pipe up.  When he is finished, we automatically go into the mapscreen,
620 //regardless of the mode we are in.
PrepareForPreBattleInterface(GROUP * pPlayerDialogGroup,GROUP * pInitiatingBattleGroup)621 static void PrepareForPreBattleInterface(GROUP* pPlayerDialogGroup, GROUP* pInitiatingBattleGroup)
622 {
623 	// ATE; Changed alogrithm here...
624 	// We first loop through the group and save ubID's ov valid guys to talk....
625 	// ( Can't if sleeping, unconscious, and EPC, etc....
626 	UINT8 ubNumMercs = 0;
627 
628 	if( fDisableMapInterfaceDueToBattle )
629 	{
630 		SLOGA("fDisableMapInterfaceDueToBattle is set before attempting to bring up PBI.\
631 			Please send PRIOR save if possible and details on anything that just happened before this battle." );
632 		return;
633 	}
634 
635 	// Pipe up with quote...
636 	AssertMsg( pPlayerDialogGroup, "Didn't get a player dialog group for prebattle interface." );
637 
638 	AssertMsg(pPlayerDialogGroup->pPlayerList, String( "Player group %d doesn't have *any* players in it!  (Finding dialog group)", pPlayerDialogGroup->ubGroupID));
639 
640 	SOLDIERTYPE* mercs_in_group[20];
641 	CFOR_EACH_PLAYER_IN_GROUP(pPlayer, pPlayerDialogGroup)
642 	{
643 		SOLDIERTYPE* const pSoldier = pPlayer->pSoldier;
644 
645 		if (pSoldier->bLife >= OKLIFE && !IsMechanical(*pSoldier) && !AM_AN_EPC(pSoldier))
646 		{
647 			mercs_in_group[ubNumMercs++] = pSoldier;
648 		}
649 	}
650 
651 	//Set music
652 	SetMusicMode( MUSIC_TACTICAL_ENEMYPRESENT );
653 
654 	if( (gfTacticalTraversal && pInitiatingBattleGroup == gpTacticalTraversalGroup) ||
655 		(pInitiatingBattleGroup && !pInitiatingBattleGroup->fPlayer &&
656 		pInitiatingBattleGroup->ubSectorX == gWorldSectorX &&
657 		pInitiatingBattleGroup->ubSectorY == gWorldSectorY && !gbWorldSectorZ) )
658 	{	// At least say quote....
659 		if ( ubNumMercs > 0 )
660 		{
661 			if( pPlayerDialogGroup->uiFlags & GROUPFLAG_JUST_RETREATED_FROM_BATTLE )
662 			{
663 				gfCantRetreatInPBI = TRUE;
664 			}
665 
666 			SOLDIERTYPE* const chosen = mercs_in_group[Random(ubNumMercs)];
667 			gpTacticalTraversalChosenSoldier = chosen;
668 
669 			if( !gfTacticalTraversal )
670 			{
671 				HandleImportantPBIQuote(*chosen, pInitiatingBattleGroup);
672 			}
673 
674 			InterruptTime();
675 			PauseGame();
676 			LockPauseState(LOCK_PAUSE_PREBATTLE_CURRENT_SQUAD);
677 
678 			if( !gfTacticalTraversal )
679 				fDisableMapInterfaceDueToBattle = TRUE;
680 		}
681 		return;
682 	}
683 
684 
685 	// Randomly pick a valid merc from the list we have created!
686 	if ( ubNumMercs > 0 )
687 	{
688 		if( pPlayerDialogGroup->uiFlags & GROUPFLAG_JUST_RETREATED_FROM_BATTLE )
689 		{
690 			gfCantRetreatInPBI = TRUE;
691 		}
692 
693 		SOLDIERTYPE* const chosen = mercs_in_group[Random(ubNumMercs)];
694 		HandleImportantPBIQuote(*chosen, pInitiatingBattleGroup);
695 		InterruptTime();
696 		PauseGame();
697 		LockPauseState(LOCK_PAUSE_PREBATTLE);
698 
699 		// disable exit from mapscreen and what not until face done talking
700 		fDisableMapInterfaceDueToBattle = TRUE;
701 	}
702 	else
703 	{
704 		// ATE: What if we have unconscious guys, etc....
705 		// We MUST start combat, but donot play quote...
706 		InitPreBattleInterface(pInitiatingBattleGroup, true);
707 	}
708 }
709 
710 
711 static void HandleOtherGroupsArrivingSimultaneously(UINT8 x, UINT8 y, UINT8 z);
712 static void NotifyPlayerOfBloodcatBattle(UINT8 ubSectorX, UINT8 ubSectorY);
713 static BOOLEAN TestForBloodcatAmbush(GROUP const*);
714 static void TriggerPrebattleInterface(MessageBoxReturnValue);
715 static BOOLEAN PossibleToCoordinateSimultaneousGroupArrivals(GROUP* pFirstGroup);
716 
717 
CheckConditionsForBattle(GROUP * pGroup)718 static BOOLEAN CheckConditionsForBattle(GROUP* pGroup)
719 {
720 	GROUP *pPlayerDialogGroup = NULL;
721 	SOLDIERTYPE *pSoldier;
722 	BOOLEAN fBattlePending = FALSE;
723 	BOOLEAN fAliveMerc = FALSE;
724 	BOOLEAN fMilitiaPresent = FALSE;
725 	BOOLEAN fCombatAbleMerc = FALSE;
726 	BOOLEAN fBloodCatAmbush = FALSE;
727 
728 	if( gfWorldLoaded )
729 	{ //look for people arriving in the currently loaded sector.  This handles reinforcements.
730 		const GROUP* const curr = FindPlayerMovementGroupInSector(gWorldSectorX, gWorldSectorY);
731 		if( !gbWorldSectorZ && PlayerMercsInSector( (UINT8)gWorldSectorX, (UINT8)gWorldSectorY, gbWorldSectorZ ) &&
732 			pGroup->ubSectorX == gWorldSectorX && pGroup->ubSectorY == gWorldSectorY &&
733 			curr )
734 		{ //Reinforcements have arrived!
735 			if( gTacticalStatus.fEnemyInSector )
736 			{
737 				HandleArrivalOfReinforcements( pGroup );
738 				return( TRUE );
739 			}
740 		}
741 	}
742 
743 	if( !DidGameJustStart() )
744 	{
745 		gubEnemyEncounterCode = NO_ENCOUNTER_CODE;
746 	}
747 
748 	HandleOtherGroupsArrivingSimultaneously( pGroup->ubSectorX, pGroup->ubSectorY, pGroup->ubSectorZ );
749 
750 	FOR_EACH_PLAYER_GROUP(i)
751 	{
752 		GROUP& g = *i;
753 		if (g.ubGroupSize)
754 		{
755 			if (!g.fBetweenSectors)
756 			{
757 				if (g.ubSectorX == pGroup->ubSectorX && g.ubSectorY == pGroup->ubSectorY && !g.ubSectorZ)
758 				{
759 					if (!GroupHasInTransitDeadOrPOWMercs(g) &&
760 						(!IsGroupTheHelicopterGroup(g) || !fHelicopterIsAirBorne) &&
761 						(!g.fVehicle || DoesVehicleGroupHaveAnyPassengers(g)))
762 					{
763 						//Now, a player group is in this sector.  Determine if the group contains any mercs that can fight.
764 						//Vehicles, EPCs and the robot doesn't count.  Mercs below OKLIFE do.
765 						CFOR_EACH_PLAYER_IN_GROUP(pPlayer, &g)
766 						{
767 							pSoldier = pPlayer->pSoldier;
768 							if( !(pSoldier->uiStatusFlags & SOLDIER_VEHICLE) )
769 							{
770 								if( !AM_A_ROBOT( pSoldier ) &&
771 									!AM_AN_EPC( pSoldier ) &&
772 									pSoldier->bLife >= OKLIFE )
773 								{
774 									fCombatAbleMerc = TRUE;
775 								}
776 								if( pSoldier->bLife > 0 )
777 								{
778 									fAliveMerc = TRUE;
779 								}
780 							}
781 						}
782 						if( !pPlayerDialogGroup && fCombatAbleMerc )
783 						{
784 							pPlayerDialogGroup = &g;
785 						}
786 						if( fCombatAbleMerc )
787 						{
788 							break;
789 						}
790 					}
791 				}
792 			}
793 		}
794 	}
795 
796 	if( pGroup->fPlayer )
797 	{
798 		pPlayerDialogGroup = pGroup;
799 
800 		if( NumEnemiesInSector( pGroup->ubSectorX, pGroup->ubSectorY ) )
801 		{
802 			fBattlePending = TRUE;
803 		}
804 
805 		if( pGroup->uiFlags & GROUPFLAG_HIGH_POTENTIAL_FOR_AMBUSH && fBattlePending )
806 		{ //This group has just arrived in a new sector from an adjacent sector that he retreated from
807 			//If this battle is an encounter type battle, then there is a 90% chance that the battle will
808 			//become an ambush scenario.
809 			gfHighPotentialForAmbush = TRUE;
810 		}
811 
812 		//If there are bloodcats in this sector, then it internally checks and handles it
813 		if( TestForBloodcatAmbush( pGroup ) )
814 		{
815 			fBloodCatAmbush = TRUE;
816 			fBattlePending = TRUE;
817 		}
818 
819 		if( fBattlePending && (!fBloodCatAmbush || gubEnemyEncounterCode == ENTERING_BLOODCAT_LAIR_CODE) )
820 		{
821 			if( PossibleToCoordinateSimultaneousGroupArrivals( pGroup ) )
822 			{
823 				return FALSE;
824 			}
825 		}
826 	}
827 	else
828 	{
829 		if( CountAllMilitiaInSector( pGroup->ubSectorX, pGroup->ubSectorY ) )
830 		{
831 			fMilitiaPresent = TRUE;
832 			fBattlePending = TRUE;
833 		}
834 		if( fAliveMerc )
835 		{
836 			fBattlePending = TRUE;
837 		}
838 	}
839 
840 	if( !fAliveMerc && !fMilitiaPresent )
841 	{ //empty vehicle, everyone dead, don't care.  Enemies don't care.
842 		return FALSE;
843 	}
844 
845 	if( fBattlePending )
846 	{	//A battle is pending, but the player's could be all unconcious or dead.
847 		//Go through every group until we find at least one concious merc.  The looping will determine
848 		//if there are any live mercs and/or concious ones.  If there are no concious mercs, but alive ones,
849 		//then we will go straight to autoresolve, where the enemy will likely annihilate them or capture them.
850 		//If there are no alive mercs, then there is nothing anybody can do.  The enemy will completely ignore
851 		//this, and continue on.
852 
853 		if( gubNumGroupsArrivedSimultaneously )
854 		{ //Because this is a battle case, clear all the group flags
855 			FOR_EACH_GROUP(curr)
856 			{
857 				if (gubNumGroupsArrivedSimultaneously == 0) break;
858 				if( curr->uiFlags & GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY )
859 				{
860 					curr->uiFlags &= ~GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY;
861 					gubNumGroupsArrivedSimultaneously--;
862 				}
863 			}
864 		}
865 
866 		gpInitPrebattleGroup = pGroup;
867 
868 		if( gubEnemyEncounterCode == BLOODCAT_AMBUSH_CODE || gubEnemyEncounterCode == ENTERING_BLOODCAT_LAIR_CODE )
869 		{
870 			NotifyPlayerOfBloodcatBattle( pGroup->ubSectorX, pGroup->ubSectorY );
871 			return TRUE;
872 		}
873 
874 		if( !fCombatAbleMerc )
875 		{ //Prepare for instant autoresolve.
876 			gfDelayAutoResolveStart = TRUE;
877 			gfUsePersistantPBI = TRUE;
878 			if( fMilitiaPresent )
879 			{
880 				NotifyPlayerOfInvasionByEnemyForces( pGroup->ubSectorX, pGroup->ubSectorY, 0, TriggerPrebattleInterface );
881 			}
882 			else
883 			{
884 				ST::string pSectorStr = GetSectorIDString(pGroup->ubSectorX, pGroup->ubSectorY, pGroup->ubSectorZ, TRUE);
885 				ST::string str = st_format_printf(gpStrategicString[ STR_DIALOG_ENEMIES_ATTACK_UNCONCIOUSMERCS ], pSectorStr);
886 				DoScreenIndependantMessageBox( str, MSG_BOX_FLAG_OK, TriggerPrebattleInterface );
887 			}
888 		}
889 
890 		if( pPlayerDialogGroup )
891 		{
892 			PrepareForPreBattleInterface( pPlayerDialogGroup, pGroup );
893 		}
894 		return TRUE;
895 	}
896 	return FALSE;
897 }
898 
899 
900 class DialogueEventTriggerPrebattleInterface : public DialogueEvent
901 {
902 	public:
DialogueEventTriggerPrebattleInterface(GROUP * const init_prebattle_group)903 		DialogueEventTriggerPrebattleInterface(GROUP* const init_prebattle_group) :
904 			init_prebattle_group_(init_prebattle_group)
905 		{}
906 
Execute()907 		bool Execute()
908 		{
909 			UnLockPauseState();
910 			InitPreBattleInterface(init_prebattle_group_, true);
911 			return false;
912 		}
913 
914 	private:
915 		GROUP* const init_prebattle_group_;
916 };
917 
918 
TriggerPrebattleInterface(MessageBoxReturnValue const ubResult)919 static void TriggerPrebattleInterface(MessageBoxReturnValue const ubResult)
920 {
921 	StopTimeCompression();
922 	DialogueEvent::Add(new DialogueEventTriggerPrebattleInterface(gpInitPrebattleGroup));
923 	gpInitPrebattleGroup = NULL;
924 }
925 
926 
927 //This will get called after a battle is auto-resolved or automatically after arriving
928 //at the next sector during a move and the area is clear.
CalculateNextMoveIntention(GROUP * pGroup)929 void CalculateNextMoveIntention( GROUP *pGroup )
930 {
931 	INT32 i;
932 	WAYPOINT *wp;
933 
934 	Assert( pGroup );
935 
936 	//TEMP:  Ignore resting...
937 
938 	//Should be surely an enemy group that has just made a new decision to go elsewhere!
939 	if( pGroup->fBetweenSectors )
940 	{
941 		return;
942 	}
943 
944 	if( !pGroup->pWaypoints )
945 	{
946 		return;
947 	}
948 
949 	//Determine if we are at a waypoint.
950 	i = pGroup->ubNextWaypointID;
951 	wp = pGroup->pWaypoints;
952 	while( i-- )
953 	{ //Traverse through the waypoint list to the next waypoint ID
954 		Assert( wp );
955 		wp = wp->next;
956 	}
957 	Assert( wp );
958 
959 	//We have the next waypoint, now check if we are actually there.
960 	if( pGroup->ubSectorX == wp->x && pGroup->ubSectorY == wp->y )
961 	{ //We have reached the next waypoint, so now determine what the next waypoint is.
962 		switch( pGroup->ubMoveType )
963 		{
964 			case ONE_WAY:
965 				if( !wp->next )
966 				{ //No more waypoints, so we've reached the destination.
967 					return;
968 				}
969 				//Advance destination to next waypoint ID
970 				pGroup->ubNextWaypointID++;
971 				break;
972 			case CIRCULAR:
973 				wp = wp->next;
974 				if( !wp )
975 				{	//reached the end of the patrol route.  Set to the first waypoint in list, indefinately.
976 					//NOTE:  If the last waypoint isn't exclusively aligned to the x or y axis of the first
977 					//			 waypoint, there will be an assertion failure inside the waypoint movement code.
978 					pGroup->ubNextWaypointID = 0;
979 				}
980 				else
981 					pGroup->ubNextWaypointID++;
982 				break;
983 			case ENDTOEND_FORWARDS:
984 				wp = wp->next;
985 				if( !wp )
986 				{
987 					AssertMsg( pGroup->ubNextWaypointID, "EndToEnd patrol group needs more than one waypoint!" );
988 					pGroup->ubNextWaypointID--;
989 					pGroup->ubMoveType = ENDTOEND_BACKWARDS;
990 				}
991 				else
992 					pGroup->ubNextWaypointID++;
993 				break;
994 			case ENDTOEND_BACKWARDS:
995 				if( !pGroup->ubNextWaypointID )
996 				{
997 					pGroup->ubNextWaypointID++;
998 					pGroup->ubMoveType = ENDTOEND_FORWARDS;
999 				}
1000 				else
1001 					pGroup->ubNextWaypointID--;
1002 				break;
1003 		}
1004 	}
1005 	InitiateGroupMovementToNextSector( pGroup );
1006 }
1007 
1008 
1009 /* Based on how long movement took, mercs gain a bit of life experience for
1010  * travelling */
AwardExperienceForTravelling(GROUP & g)1011 static void AwardExperienceForTravelling(GROUP& g)
1012 {
1013 	UINT32 const traverse_time = g.uiTraverseTime;
1014 	CFOR_EACH_PLAYER_IN_GROUP(i, &g)
1015 	{
1016 		if (!i->pSoldier)    continue;
1017 		SOLDIERTYPE& s = *i->pSoldier;
1018 		if (IsMechanical(s)) continue;
1019 		if (AM_AN_EPC(&s))   continue;
1020 
1021 		if (s.bLifeMax < 100)
1022 		{
1023 			/* Amount was originally based on getting 100 - bLifeMax points for 12
1024 			 * hours of travel (720), but changed to flat rate since StatChange makes
1025 			 * roll vs 100-lifemax as well */
1026 			UINT32 const points = traverse_time / (450 / (100 - s.bLifeMax));
1027 			if (points > 0) StatChange(s, HEALTHAMT, (UINT8)points, FROM_SUCCESS);
1028 		}
1029 
1030 		if (s.bStrength < 100)
1031 		{
1032 			UINT32 const carried_percent = CalculateCarriedWeight(&s);
1033 			if (carried_percent > 50)
1034 			{
1035 				UINT32 const points = traverse_time / (450 / (100 - s.bStrength));
1036 				StatChange(s, STRAMT, points * (carried_percent - 50) / 100, FROM_SUCCESS);
1037 			}
1038 		}
1039 	}
1040 }
1041 
1042 
AddCorpsesToBloodcatLair(INT16 sSectorX,INT16 sSectorY)1043 static void AddCorpsesToBloodcatLair(INT16 sSectorX, INT16 sSectorY)
1044 {
1045 	ROTTING_CORPSE_DEFINITION		Corpse;
1046 	Corpse = ROTTING_CORPSE_DEFINITION{};
1047 
1048 	// Setup some values!
1049 	Corpse.ubBodyType        = REGMALE;
1050 	Corpse.sHeightAdjustment = 0;
1051 	Corpse.bVisible          = TRUE;
1052 
1053 	Corpse.HeadPal  = "BROWNHEAD";
1054 	Corpse.VestPal  = "YELLOWVEST";
1055 	Corpse.SkinPal  = "PINKSKIN";
1056 	Corpse.PantsPal = "GREENPANTS";
1057 
1058 
1059 	Corpse.bDirection = (INT8)Random(8);
1060 
1061 	// Set time of death
1062 	// Make sure they will be rotting!
1063 	Corpse.uiTimeOfDeath = GetWorldTotalMin( ) - ( 2 * NUM_SEC_IN_DAY / 60 );
1064 	// Set type
1065 	Corpse.ubType  = (UINT8)SMERC_JFK;
1066 	Corpse.usFlags = ROTTING_CORPSE_FIND_SWEETSPOT_FROM_GRIDNO;
1067 
1068 	// 1st gridno
1069 	Corpse.sGridNo 	= 14319;
1070 	//Add the rotting corpse info to the sectors unloaded rotting corpse file
1071 	AddRottingCorpseToUnloadedSectorsRottingCorpseFile( sSectorX, sSectorY, 0, &Corpse);
1072 
1073 	// 2nd gridno
1074 	Corpse.sGridNo = 9835;
1075 	//Add the rotting corpse info to the sectors unloaded rotting corpse file
1076 	AddRottingCorpseToUnloadedSectorsRottingCorpseFile( sSectorX, sSectorY, 0, &Corpse);
1077 
1078 
1079 	// 3rd gridno
1080 	Corpse.sGridNo = 11262;
1081 	//Add the rotting corpse info to the sectors unloaded rotting corpse file
1082 	AddRottingCorpseToUnloadedSectorsRottingCorpseFile( sSectorX, sSectorY, 0, &Corpse);
1083 }
1084 
1085 
1086 static void HandleNonCombatGroupArrival(GROUP&, bool main_group, bool never_left);
1087 static void ReportVehicleOutOfGas(VEHICLETYPE const&, UINT8 x, UINT8 y);
1088 static void SpendVehicleFuel(SOLDIERTYPE&, INT16 fuel_spent);
1089 static INT16 VehicleFuelRemaining(SOLDIERTYPE const&);
1090 
1091 
GroupArrivedAtSector(GROUP & g,BOOLEAN const check_for_battle,BOOLEAN const never_left)1092 void GroupArrivedAtSector(GROUP& g, BOOLEAN const check_for_battle, BOOLEAN const never_left)
1093 {
1094 	gfWaitingForInput = FALSE;
1095 
1096 	if (g.fPlayer)
1097 	{
1098 		// Set the fact we have visited the sector
1099 		if (PLAYERGROUP const* curr = g.pPlayerList)
1100 		{
1101 			INT8 const assignment = curr->pSoldier->bAssignment;
1102 			if (assignment < ON_DUTY) ResetDeadSquadMemberList(assignment);
1103 		}
1104 
1105 		if (g.fVehicle)
1106 		{
1107 			VEHICLETYPE const& v = GetVehicleFromMvtGroup(g);
1108 			if (!IsHelicopter(v) && !g.pPlayerList)
1109 			{ /* Nobody here, better just get out now. With vehicles, arriving empty
1110 				* is probably ok, since passengers might have been killed but vehicle
1111 				* lived. */
1112 				return;
1113 			}
1114 		}
1115 		else
1116 		{
1117 			if (!g.pPlayerList)
1118 			{ // Nobody here, better just get out now
1119 				SLOGA("Player group %d arrived in sector empty.", g.ubGroupID);
1120 				return;
1121 			}
1122 		}
1123 	}
1124 
1125 	UINT8 const x = g.ubNextX;
1126 	UINT8 const y = g.ubNextY;
1127 	UINT8 const z = g.ubSectorZ;
1128 
1129 	// Check for exception cases which
1130 	bool const exception_queue =
1131 		gTacticalStatus.bBoxingState != NOT_BOXING &&
1132 		!g.fPlayer                                 &&
1133 		x == 5 && y == 4 && z == 0;
1134 
1135 	/* First check if the group arriving is going to queue another battle.
1136 	 * NOTE: We can't have more than one battle ongoing at a time. */
1137 	if (exception_queue ||
1138 		(check_for_battle && gTacticalStatus.fEnemyInSector && FindPlayerMovementGroupInSector(gWorldSectorX, gWorldSectorY) && (x != gWorldSectorX || y != gWorldSectorY || gbWorldSectorZ > 0)) ||
1139 		AreInMeanwhile() ||
1140 		/* KM: Aug 11, 1999 -- Patch fix: Added additional checks to prevent a 2nd
1141 			* battle in the case where the player is involved in a potential battle
1142 			* with bloodcats/civilians */
1143 		(check_for_battle && HostileCiviliansPresent()) ||
1144 		(check_for_battle && HostileBloodcatsPresent()))
1145 	{ /* Queue battle! Delay arrival by a random value ranging from 3-5 minutes,
1146 		* so it doesn't get the player too suspicious after it happens to him a few
1147 		* times, which, by the way, is a rare occurrence. */
1148 		if (AreInMeanwhile())
1149 		{ /* Tack on only 1 minute if we are in a meanwhile scene. This effectively
1150 			* prevents any battle from occurring while inside a meanwhile scene. */
1151 			++g.uiArrivalTime;
1152 		}
1153 		else
1154 		{
1155 			g.uiArrivalTime += Random(3) + 3;
1156 		}
1157 
1158 		if (!AddStrategicEvent(EVENT_GROUP_ARRIVAL, g.uiArrivalTime, g.ubGroupID))
1159 			SLOGA("Failed to add movement event.");
1160 
1161 		if (g.fPlayer && g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin())
1162 		{
1163 			AddStrategicEvent(EVENT_GROUP_ABOUT_TO_ARRIVE, g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, g.ubGroupID);
1164 		}
1165 		return;
1166 	}
1167 
1168 	// Update the position of the group
1169 	g.ubPrevX   = g.ubSectorX;
1170 	g.ubPrevY   = g.ubSectorY;
1171 	g.ubSectorX = x;
1172 	g.ubSectorY = y;
1173 	g.ubNextX   = 0;
1174 	g.ubNextY   = 0;
1175 
1176 	if (g.fPlayer)
1177 	{
1178 		// Award life 'experience' for traveling, based on travel time.
1179 		if (!g.fVehicle)
1180 		{ // Gotta be walking to get tougher
1181 			AwardExperienceForTravelling(g);
1182 		}
1183 		else if (!IsGroupTheHelicopterGroup(g))
1184 		{
1185 			VEHICLETYPE const& v  = GetVehicleFromMvtGroup(g);
1186 			SOLDIERTYPE&       vs = GetSoldierStructureForVehicle(v);
1187 
1188 			SpendVehicleFuel(vs, g.uiTraverseTime * 6);
1189 
1190 			if (VehicleFuelRemaining(vs) == 0)
1191 			{
1192 				ReportVehicleOutOfGas(v, x, y);
1193 				// Nuke the group's path, so they don't continue moving.
1194 				ClearMercPathsAndWaypointsForAllInGroup(g);
1195 			}
1196 		}
1197 	}
1198 
1199 	g.uiTraverseTime      = 0;
1200 	g.setArrivalTime(0);
1201 	g.fBetweenSectors     = FALSE;
1202 	fMapPanelDirty        = TRUE;
1203 	fMapScreenBottomDirty = TRUE;
1204 
1205 	bool group_destroyed = false;
1206 	if (g.fPlayer)
1207 	{
1208 		// If this is the last sector along player group's movement path (no more waypoints)
1209 		if (GroupAtFinalDestination(&g))
1210 		{ // Clear their strategic movement (mercpaths and waypoints)
1211 			ClearMercPathsAndWaypointsForAllInGroup(g);
1212 		}
1213 
1214 		// If on surface
1215 		if (z == 0)
1216 		{
1217 			// check for discovering secret locations
1218 			if (GetMapSecretBySectorID(SECTOR(x, y)))
1219 			{
1220 				SetSectorSecretAsFound(SECTOR(x, y));
1221 			}
1222 		}
1223 
1224 		UINT8 insertion_direction;
1225 		UINT8 strategic_insertion_code;
1226 		if (x < g.ubPrevX)
1227 		{
1228 			insertion_direction      = SOUTHWEST;
1229 			strategic_insertion_code = INSERTION_CODE_EAST;
1230 		}
1231 		else if (x > g.ubPrevX)
1232 		{
1233 			insertion_direction      = NORTHEAST;
1234 			strategic_insertion_code = INSERTION_CODE_WEST;
1235 		}
1236 		else if (y < g.ubPrevY)
1237 		{
1238 			insertion_direction      = NORTHWEST;
1239 			strategic_insertion_code = INSERTION_CODE_SOUTH;
1240 		}
1241 		else if (y > g.ubPrevY)
1242 		{
1243 			insertion_direction      = SOUTHEAST;
1244 			strategic_insertion_code = INSERTION_CODE_NORTH;
1245 		}
1246 		else
1247 		{
1248 			SLOGA("GroupArrivedAtSector: group arrives in sector where it already has been");
1249 			return;
1250 		}
1251 
1252 		bool    const  here = x == gWorldSectorX && y == gWorldSectorY && z == gbWorldSectorZ;
1253 		ST::string who;
1254 		if (!g.fVehicle)
1255 		{
1256 			// non-vehicle player group
1257 			CFOR_EACH_PLAYER_IN_GROUP(i, &g)
1258 			{
1259 				SOLDIERTYPE& s = *i->pSoldier;
1260 				s.fBetweenSectors      = FALSE;
1261 				s.sSectorX             = x;
1262 				s.sSectorY             = y;
1263 				s.bSectorZ             = z;
1264 				s.ubPrevSectorID       = SECTOR(g.ubPrevX, g.ubPrevY);
1265 				s.ubInsertionDirection = insertion_direction;
1266 
1267 				// don't override if a tactical traversal
1268 				if (s.ubStrategicInsertionCode != INSERTION_CODE_PRIMARY_EDGEINDEX &&
1269 						s.ubStrategicInsertionCode != INSERTION_CODE_SECONDARY_EDGEINDEX)
1270 				{
1271 					s.ubStrategicInsertionCode = strategic_insertion_code;
1272 				}
1273 
1274 				// Remove head from their mapscreen path list
1275 				if (s.pMercPath) s.pMercPath = RemoveHeadFromStrategicPath(s.pMercPath);
1276 
1277 				/* ATE: Check if this sector is currently loaded, if so, add them to the
1278 				 * tactical engine */
1279 				if (here) UpdateMercInSector(s, x, y, z);
1280 			}
1281 
1282 			// If there's anybody in the group
1283 			if (g.pPlayerList)
1284 			{
1285 				SOLDIERTYPE const& s = *g.pPlayerList->pSoldier;
1286 				who =
1287 					s.bAssignment >= ON_DUTY ? s.name : // A loner
1288 					pAssignmentStrings[s.bAssignment];  // Squad
1289 			}
1290 		}
1291 		else
1292 		{ // Vehicle player group
1293 			VEHICLETYPE& v = GetVehicleFromMvtGroup(g);
1294 			// Remove head from vehicle's mapscreen path list
1295 			if (v.pMercPath) v.pMercPath = RemoveHeadFromStrategicPath(v.pMercPath);
1296 
1297 			// Update vehicle position
1298 			SetVehicleSectorValues(v, x, y);
1299 			v.fBetweenSectors = FALSE;
1300 
1301 			if (!IsHelicopter(v))
1302 			{
1303 				SOLDIERTYPE& vs = GetSoldierStructureForVehicle(v);
1304 				vs.fBetweenSectors          = FALSE;
1305 				vs.sSectorX                 = x;
1306 				vs.sSectorY                 = y;
1307 				vs.bSectorZ                 = z;
1308 				vs.ubInsertionDirection     = insertion_direction;
1309 				vs.ubStrategicInsertionCode = strategic_insertion_code;
1310 
1311 				// If this sector is currently loaded, add vehicle to the tactical engine
1312 				if (here) UpdateMercInSector(vs, x, y, z);
1313 
1314 				// Set directions of insertion
1315 				CFOR_EACH_PLAYER_IN_GROUP(i, &g)
1316 				{
1317 					SOLDIERTYPE& s = *i->pSoldier;
1318 					s.fBetweenSectors = FALSE;
1319 					s.sSectorX = x;
1320 					s.sSectorY = y;
1321 					s.bSectorZ = z;
1322 					s.ubInsertionDirection = insertion_direction;
1323 					s.ubStrategicInsertionCode = strategic_insertion_code;
1324 
1325 					// If this sector is currently loaded, add passenger to the tactical engine
1326 					if (here) UpdateMercInSector(s, x, y, z);
1327 				}
1328 			}
1329 			else
1330 			{
1331 				if (HandleHeliEnteringSector(v.sSectorX, v.sSectorY))
1332 				{ // Helicopter destroyed
1333 					group_destroyed = true;
1334 				}
1335 			}
1336 
1337 			if (!group_destroyed) who = pVehicleStrings[v.ubVehicleType];
1338 		}
1339 
1340 		if (!who.empty())
1341 		{ /* Don't print any messages when arriving underground (there's no delay
1342 			 * involved) or if we never left (cancel) */
1343 			if (GroupAtFinalDestination(&g) && z == 0 && !never_left)
1344 			{
1345 				ScreenMsg(FONT_MCOLOR_DKRED, MSG_INTERFACE, st_format_printf(pMessageStrings[MSG_ARRIVE], who, pMapVertIndex[y], pMapHortIndex[x]));
1346 			}
1347 		}
1348 
1349 		if (!group_destroyed)
1350 		{
1351 			// On foot, or in a vehicle other than the chopper
1352 			if (!g.fVehicle || !IsGroupTheHelicopterGroup(g))
1353 			{
1354 				// ATE: Add a few corpse to the bloodcat lair
1355 				auto spawns = GCM->getBloodCatSpawnsOfSector( SECTOR(x, y) );
1356 				if ( spawns != NULL && spawns->isLair &&
1357 					!GetSectorFlagStatus(x, y, z, SF_ALREADY_VISITED))
1358 				{
1359 					AddCorpsesToBloodcatLair(x, y);
1360 				}
1361 
1362 				// Mark the sector as visited already
1363 				SetSectorFlag(x, y, z, SF_ALREADY_VISITED);
1364 			}
1365 		}
1366 
1367 		// Update character info
1368 		fTeamPanelDirty          = TRUE;
1369 		fCharacterInfoPanelDirty = TRUE;
1370 	}
1371 
1372 	if (!group_destroyed)
1373 	{
1374 		/* Determine if a battle should start. If a battle does start, or get's
1375 		 * delayed, then we will keep the group in memory including all waypoints,
1376 		 * until after the battle is resolved.  At that point, we will continue the
1377 		 * processing. */
1378 		if (check_for_battle && !CheckConditionsForBattle(&g) && !gfWaitingForInput)
1379 		{
1380 			HandleNonCombatGroupArrival(g, true, never_left);
1381 
1382 			if (gubNumGroupsArrivedSimultaneously != 0)
1383 			{
1384 				FOR_EACH_GROUP_SAFE(i)
1385 				{
1386 					GROUP& g = *i;
1387 					if (!(g.uiFlags & GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY)) continue;
1388 					--gubNumGroupsArrivedSimultaneously;
1389 					HandleNonCombatGroupArrival(g, false, false);
1390 					if (gubNumGroupsArrivedSimultaneously == 0) break;
1391 				}
1392 			}
1393 		}
1394 		else
1395 		{ // Handle cases for pre-battle conditions
1396 			g.uiFlags = 0;
1397 			if (gubNumAwareBattles != 0)
1398 			{ /* When the AI is looking for the players, and a battle is initiated,
1399 				* then decrement the value, otherwise the queen will continue searching
1400 				* to infinity. */
1401 				--gubNumAwareBattles;
1402 			}
1403 		}
1404 	}
1405 	gfWaitingForInput = FALSE;
1406 }
1407 
1408 
HandleNonCombatGroupArrival(GROUP & g,bool const main_group,bool const never_left)1409 static void HandleNonCombatGroupArrival(GROUP& g, bool const main_group, bool const never_left)
1410 {
1411 	if (StrategicAILookForAdjacentGroups(&g))
1412 	{ /* The routine actually just deleted the enemy group (player's don't get
1413 		* deleted), so we are done! */
1414 		return;
1415 	}
1416 
1417 	if (g.fPlayer)
1418 	{ // The group will always exist after the AI was processed.
1419 
1420 		bool const is_heli_group = g.fVehicle && IsGroupTheHelicopterGroup(g);
1421 		if (!is_heli_group)
1422 		{ // Take control of sector
1423 			SetThisSectorAsPlayerControlled(g.ubSectorX, g.ubSectorY, g.ubSectorZ, FALSE);
1424 		}
1425 
1426 		// If this is the last sector along their movement path (no more waypoints)
1427 		if (GroupAtFinalDestination(&g))
1428 		{
1429 			// If currently selected sector has nobody in it
1430 			if (PlayerMercsInSector(sSelMapX, sSelMapY, iCurrentMapSectorZ) == 0)
1431 			{ // Make this sector strategically selected
1432 				ChangeSelectedMapSector(g.ubSectorX, g.ubSectorY, g.ubSectorZ);
1433 			}
1434 
1435 			if (!is_heli_group) // Else Skyrider speaks for heli movement
1436 			{
1437 				StopTimeCompression();
1438 
1439 				// If traversing tactically, or we never left (just canceling), don't do this
1440 				if (!gfTacticalTraversal && !never_left)
1441 				{
1442 					RandomMercInGroupSaysQuote(g, QUOTE_MERC_REACHED_DESTINATION);
1443 				}
1444 			}
1445 		}
1446 		/* Look for NPCs to stop for, anyone is too tired to keep going, if all OK
1447 		 * rebuild waypoints & continue movement
1448 		 * NOTE: Only the main group (first group arriving) will stop for NPCs, it's
1449 		 * just too much hassle to stop them all */
1450 		PlayerGroupArrivedSafelyInSector(g, main_group);
1451 	}
1452 	else
1453 	{
1454 		if (!g.fDebugGroup)
1455 		{
1456 			CalculateNextMoveIntention(&g);
1457 		}
1458 		else
1459 		{
1460 			RemoveGroup(g);
1461 		}
1462 	}
1463 
1464 	g.uiFlags = 0; // Clear the non-persistant flags
1465 }
1466 
1467 
1468 /* Because a battle is about to start, we need to go through the event list and
1469  * look for other groups that may arrive at the same time -- enemies or players,
1470  * and blindly add them to the sector without checking for battle conditions, as
1471  * it has already determined that a new battle is about to start. */
HandleOtherGroupsArrivingSimultaneously(UINT8 const x,UINT8 const y,UINT8 const z)1472 static void HandleOtherGroupsArrivingSimultaneously(UINT8 const x, UINT8 const y, UINT8 const z)
1473 {
1474 	UINT32 const now = GetWorldTotalSeconds();
1475 	gubNumGroupsArrivedSimultaneously = 0;
1476 restart:
1477 	for (STRATEGICEVENT* i = gpEventList; i && i->uiTimeStamp <= now; i = i->next)
1478 	{
1479 		if (i->ubCallbackID != EVENT_GROUP_ARRIVAL) continue;
1480 		if (i->ubFlags & SEF_DELETION_PENDING)      continue;
1481 
1482 		GROUP& g = *GetGroup((UINT8)i->uiParam);
1483 		if (g.ubNextX != x || g.ubNextY != y || g.ubSectorZ != z) continue;
1484 		if (!g.fBetweenSectors) continue;
1485 
1486 		GroupArrivedAtSector(g, FALSE, FALSE);
1487 		g.uiFlags |= GROUPFLAG_GROUP_ARRIVED_SIMULTANEOUSLY;
1488 		++gubNumGroupsArrivedSimultaneously;
1489 		DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1490 		goto restart;
1491 	}
1492 }
1493 
1494 
1495 static void DelayEnemyGroupsIfPathsCross(GROUP& player_group);
1496 
1497 
1498 /* The user has just approved to plan a simultaneous arrival. So we will
1499  * syncronize all of the involved groups so that they arrive at the same time
1500  * (which is the time the final group would arrive). */
PrepareGroupsForSimultaneousArrival()1501 static void PrepareGroupsForSimultaneousArrival()
1502 {
1503 	GROUP& first_group = *gpPendingSimultaneousGroup;
1504 
1505 	/* For all of the groups that haven't arrived yet, determine which one is
1506 	 * going to take the longest. */
1507 	UINT32 latest_arrival_time = 0;
1508 	FOR_EACH_PLAYER_GROUP(i)
1509 	{
1510 		GROUP& g = *i;
1511 		if (&g == &first_group)                 continue;
1512 		if (!g.fBetweenSectors)                 continue;
1513 		if (g.ubNextX != first_group.ubSectorX) continue;
1514 		if (g.ubNextY != first_group.ubSectorY) continue;
1515 		if (IsGroupTheHelicopterGroup(g))       continue;
1516 		latest_arrival_time = MAX(g.uiArrivalTime, latest_arrival_time);
1517 		g.uiFlags |= GROUPFLAG_SIMULTANEOUSARRIVAL_APPROVED | GROUPFLAG_MARKER;
1518 	}
1519 
1520 	/* Now, go through the list again, and reset their arrival event to the latest
1521 	 * arrival time. */
1522 	FOR_EACH_GROUP(i)
1523 	{
1524 		GROUP& g = *i;
1525 		if (!(g.uiFlags & GROUPFLAG_MARKER)) continue;
1526 
1527 		DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1528 
1529 		/* NOTE: This can cause the arrival time to be > GetWorldTotalMin() +
1530 		 * TraverseTime, so keep that in mind if you have any code that uses these 3
1531 		 * values to figure out how far along its route a group is! */
1532 		g.setArrivalTime(latest_arrival_time);
1533 		AddStrategicEvent(EVENT_GROUP_ARRIVAL, g.uiArrivalTime, g.ubGroupID);
1534 
1535 		if (g.fPlayer && g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin())
1536 		{
1537 			AddStrategicEvent(EVENT_GROUP_ABOUT_TO_ARRIVE, g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, g.ubGroupID);
1538 		}
1539 
1540 		DelayEnemyGroupsIfPathsCross(g);
1541 		g.uiFlags &= ~GROUPFLAG_MARKER;
1542 	}
1543 
1544 	/* We still have the first group that has arrived. Because they are set up to
1545 	 * be in the destination sector, we will "warp" them back to the last sector,
1546 	 * and also setup a new arrival time for them. */
1547 	first_group.ubNextX         = first_group.ubSectorX;
1548 	first_group.ubNextY         = first_group.ubSectorY;
1549 	first_group.ubSectorX       = first_group.ubPrevX;
1550 	first_group.ubSectorY       = first_group.ubPrevY;
1551 	first_group.setArrivalTime(latest_arrival_time);
1552 	first_group.fBetweenSectors = TRUE;
1553 
1554 	if (first_group.fVehicle)
1555 	{
1556 		VEHICLETYPE& v = GetVehicleFromMvtGroup(first_group);
1557 		v.fBetweenSectors = TRUE;
1558 
1559 		if (!IsHelicopter(v))
1560 		{
1561 			SOLDIERTYPE& vs = GetSoldierStructureForVehicle(v);
1562 			vs.fBetweenSectors = TRUE;
1563 		}
1564 	}
1565 
1566 	AddStrategicEvent(EVENT_GROUP_ARRIVAL, first_group.uiArrivalTime, first_group.ubGroupID);
1567 
1568 	if (first_group.fPlayer && first_group.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin())
1569 	{
1570 		AddStrategicEvent(EVENT_GROUP_ABOUT_TO_ARRIVE, first_group.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, first_group.ubGroupID);
1571 	}
1572 	DelayEnemyGroupsIfPathsCross(first_group);
1573 }
1574 
1575 
1576 static void PlanSimultaneousGroupArrivalCallback(MessageBoxReturnValue);
1577 
1578 
1579 /* See if there are other groups OTW.  If so, and if we haven't asked the user
1580  * yet to plan a simultaneous attack, do so now, and readjust the groups
1581  * accordingly.  If it is possible to do so, then we will set up the gui, and
1582  * postpone the prebattle interface. */
PossibleToCoordinateSimultaneousGroupArrivals(GROUP * const first_group)1583 static BOOLEAN PossibleToCoordinateSimultaneousGroupArrivals(GROUP* const first_group)
1584 {
1585 	// If the user has already been asked, then don't ask the question again!
1586 	if (first_group->uiFlags & (GROUPFLAG_SIMULTANEOUSARRIVAL_APPROVED | GROUPFLAG_SIMULTANEOUSARRIVAL_CHECKED)) return FALSE;
1587 	if (IsGroupTheHelicopterGroup(*first_group)) return FALSE;
1588 
1589 	/* Count the number of groups that are scheduled to arrive in the same sector
1590 	 * and are currently adjacent to the sector in question. */
1591 	UINT8 n_nearby_groups = 0;
1592 	FOR_EACH_PLAYER_GROUP(i)
1593 	{
1594 		GROUP& g = *i;
1595 		if (&g == first_group)                                 continue;
1596 		if (!g.fBetweenSectors)                                continue;
1597 		if (g.ubNextX != first_group->ubSectorX)               continue;
1598 		if (g.ubNextY != first_group->ubSectorY)               continue;
1599 		if (g.uiFlags & GROUPFLAG_SIMULTANEOUSARRIVAL_CHECKED) continue;
1600 		if (IsGroupTheHelicopterGroup(g))                      continue;
1601 		g.uiFlags |= GROUPFLAG_SIMULTANEOUSARRIVAL_CHECKED;
1602 		++n_nearby_groups;
1603 	}
1604 
1605 	if (n_nearby_groups == 0) return FALSE;
1606 
1607 	// Postpone the battle until the user answers the dialog.
1608 	InterruptTime();
1609 	PauseGame();
1610 	LockPauseState(LOCK_PAUSE_SIMULTANEOUS_ARRIVAL);
1611 	gpPendingSimultaneousGroup = first_group;
1612 
1613 	ST::string pStr =
1614 		n_nearby_groups == 1 ? gpStrategicString[STR_DETECTED_SINGULAR] :
1615 		gpStrategicString[STR_DETECTED_PLURAL];
1616 	ST::string enemy_type =
1617 		gubEnemyEncounterCode == ENTERING_BLOODCAT_LAIR_CODE ? gpStrategicString[STR_PB_BLOODCATS] :
1618 		gpStrategicString[STR_PB_ENEMIES];
1619 	/* header, sector, singular/plural str, confirmation string.
1620 	 * Ex:  Enemies have been detected in sector J9 and another squad is about to
1621 	 *      arrive.  Do you wish to coordinate a simultaneous arrival? */
1622 	ST::string str = st_format_printf(pStr, enemy_type, 'A' + first_group->ubSectorY - 1, first_group->ubSectorX);
1623 	str += ST::format(" {}", gpStrategicString[STR_COORDINATE]);
1624 	DoMapMessageBox(MSG_BOX_BASIC_STYLE, str, guiCurrentScreen, MSG_BOX_FLAG_YESNO, PlanSimultaneousGroupArrivalCallback);
1625 	gfWaitingForInput = TRUE;
1626 	return TRUE;
1627 }
1628 
1629 
PlanSimultaneousGroupArrivalCallback(MessageBoxReturnValue const bMessageValue)1630 static void PlanSimultaneousGroupArrivalCallback(MessageBoxReturnValue const bMessageValue)
1631 {
1632 	if( bMessageValue == MSG_BOX_RETURN_YES )
1633 	{
1634 		PrepareGroupsForSimultaneousArrival();
1635 	}
1636 	else
1637 	{
1638 		PrepareForPreBattleInterface( gpPendingSimultaneousGroup, gpPendingSimultaneousGroup );
1639 	}
1640 	UnLockPauseState();
1641 	UnPauseGame();
1642 }
1643 
1644 
DelayEnemyGroupsIfPathsCross(GROUP & player_group)1645 static void DelayEnemyGroupsIfPathsCross(GROUP& player_group)
1646 {
1647 	FOR_EACH_ENEMY_GROUP(i)
1648 	{
1649 		GROUP& g = *i;
1650 		// Check to see if this group will arrive in next sector before the player group.
1651 		if (g.uiArrivalTime >= player_group.uiArrivalTime) continue;
1652 		// Check to see if enemy group will cross paths with player group.
1653 		if (g.ubNextX   != player_group.ubSectorX) continue;
1654 		if (g.ubNextY   != player_group.ubSectorY) continue;
1655 		if (g.ubSectorX != player_group.ubNextX)   continue;
1656 		if (g.ubSectorY != player_group.ubNextY)   continue;
1657 
1658 		/* The enemy group will cross paths with the player, so find and delete the
1659 		 * arrival event and repost it in the future (like a minute or so after the
1660 		 * player arrives) */
1661 		DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1662 
1663 		/* NOTE: This can cause the arrival time to be > GetWorldTotalMin() +
1664 		 * TraverseTime, so keep that in mind if you have any code that uses these 3
1665 		 * values to figure out how far along its route a group is! */
1666 		g.setArrivalTime(player_group.uiArrivalTime + 1 + Random(10));
1667 		if (!AddStrategicEvent(EVENT_GROUP_ARRIVAL, g.uiArrivalTime, g.ubGroupID))
1668 			SLOGA("Failed to add movement event.");
1669 	}
1670 }
1671 
1672 
1673 //Calculates and posts an event to move the group to the next sector.
InitiateGroupMovementToNextSector(GROUP * pGroup)1674 static void InitiateGroupMovementToNextSector(GROUP* pGroup)
1675 {
1676 	INT32 dx, dy;
1677 	INT32 i;
1678 	UINT8 ubDirection;
1679 	UINT8 ubSector;
1680 	WAYPOINT *wp;
1681 	UINT32 uiSleepMinutes = 0;
1682 
1683 	Assert( pGroup );
1684 	i = pGroup->ubNextWaypointID;
1685 	wp = pGroup->pWaypoints;
1686 	while( i-- )
1687 	{ //Traverse through the waypoint list to the next waypoint ID
1688 		Assert( wp );
1689 		wp = wp->next;
1690 	}
1691 	Assert( wp );
1692 	//We now have the correct waypoint.
1693 	//Analyse the group and determine which direction it will move from the current sector.
1694 	dx = wp->x - pGroup->ubSectorX;
1695 	dy = wp->y - pGroup->ubSectorY;
1696 	if( dx && dy )
1697 	{ //Can't move diagonally!
1698 		SLOGA("Attempting to move to waypoint in a diagonal direction from sector %d,%d to sector %d,%d",
1699 			pGroup->ubSectorX, pGroup->ubSectorY, wp->x, wp->y );
1700 	}
1701 	//Clip dx/dy value so that the move is for only one sector.
1702 	if( dx >= 1 )
1703 	{
1704 		ubDirection = EAST_STRATEGIC_MOVE;
1705 		dx = 1;
1706 	}
1707 	else if( dy >= 1 )
1708 	{
1709 		ubDirection = SOUTH_STRATEGIC_MOVE;
1710 		dy = 1;
1711 	}
1712 	else if( dx <= -1 )
1713 	{
1714 		ubDirection = WEST_STRATEGIC_MOVE;
1715 		dx = -1;
1716 	}
1717 	else if( dy <= -1 )
1718 	{
1719 		ubDirection = NORTH_STRATEGIC_MOVE;
1720 		dy = -1;
1721 	}
1722 	else
1723 	{
1724 		SLOGA("InitiateGroupMovementToNextSector: Attempting to move to waypoint %d, %d that you are already at!", wp->x, wp->y);
1725 		return;
1726 	}
1727 	//All conditions for moving to the next waypoint are now good.
1728 	pGroup->ubNextX = (UINT8)( dx + pGroup->ubSectorX );
1729 	pGroup->ubNextY = (UINT8)( dy + pGroup->ubSectorY );
1730 	//Calc time to get to next waypoint...
1731 	ubSector = (UINT8)SECTOR( pGroup->ubSectorX, pGroup->ubSectorY );
1732 	if( !pGroup->ubSectorZ )
1733 	{
1734 		BOOLEAN fCalcRegularTime = TRUE;
1735 		if( !pGroup->fPlayer )
1736 		{ //Determine if the enemy group is "sleeping".  If so, then simply delay their arrival time by the amount of time
1737 			//they are going to be sleeping for.
1738 			if( GetWorldHour() >= 21 || GetWorldHour() <= 4 )
1739 			{ //It is definitely night time.
1740 				if( Chance( 67 ) )
1741 				{ //2 in 3 chance of going to sleep.
1742 					pGroup->uiTraverseTime = GetSectorMvtTimeForGroup( ubSector, ubDirection, pGroup );
1743 					uiSleepMinutes = 360 + Random( 121 ); //6-8 hours sleep
1744 					fCalcRegularTime = FALSE;
1745 				}
1746 			}
1747 		}
1748 		if( fCalcRegularTime )
1749 		{
1750 			pGroup->uiTraverseTime = GetSectorMvtTimeForGroup( ubSector, ubDirection, pGroup );
1751 		}
1752 	}
1753 	else
1754 	{
1755 		pGroup->uiTraverseTime = 1;
1756 	}
1757 
1758 	AssertMsg(pGroup->uiTraverseTime != TRAVERSE_TIME_IMPOSSIBLE, String("Group %d (%s) attempting illegal move from %c%d to %c%d (%s).",
1759 			pGroup->ubGroupID, ( pGroup->fPlayer ) ? "Player" : "AI",
1760 			pGroup->ubSectorY+'A', pGroup->ubSectorX, pGroup->ubNextY+'A', pGroup->ubNextX,
1761 			gszTerrain[SectorInfo[ubSector].ubTraversability[ubDirection]] ) );
1762 
1763 	// add sleep, if any
1764 	pGroup->uiTraverseTime += uiSleepMinutes;
1765 
1766 	if( gfTacticalTraversal && gpTacticalTraversalGroup == pGroup )
1767 	{
1768 		if( gfUndergroundTacticalTraversal )
1769 		{	//underground movement between sectors takes 1 minute.
1770 			pGroup->uiTraverseTime = 1;
1771 		}
1772 		else
1773 		{ //strategic movement between town sectors takes 5 minutes.
1774 			pGroup->uiTraverseTime = 5;
1775 		}
1776 	}
1777 
1778 	// if group isn't already between sectors
1779 	if ( !pGroup->fBetweenSectors )
1780 	{
1781 		// put group between sectors
1782 		pGroup->fBetweenSectors	= TRUE;
1783 		// and set it's arrival time
1784 		pGroup->setArrivalTime(GetWorldTotalMin() + pGroup->uiTraverseTime);
1785 	}
1786 	// NOTE: if the group is already between sectors, DON'T MESS WITH ITS ARRIVAL TIME!  THAT'S NOT OUR JOB HERE!!!
1787 
1788 
1789 	// special override for AI patrol initialization only
1790 	if( gfRandomizingPatrolGroup )
1791 	{ //We're initializing the patrol group, so randomize the enemy groups to have extremely quick and varying
1792 		//arrival times so that their initial positions aren't easily determined.
1793 		pGroup->uiTraverseTime = 1 + Random( pGroup->uiTraverseTime - 1 );
1794 		pGroup->setArrivalTime(GetWorldTotalMin() + pGroup->uiTraverseTime);
1795 	}
1796 
1797 
1798 	if (pGroup->fVehicle)
1799 	{
1800 		// vehicle, set fact it is between sectors too
1801 		VEHICLETYPE& v = GetVehicleFromMvtGroup(*pGroup);
1802 		v.fBetweenSectors = TRUE;
1803 
1804 		if (!IsHelicopter(v))
1805 		{
1806 			SOLDIERTYPE& vs = GetSoldierStructureForVehicle(v);
1807 			vs.fBetweenSectors = TRUE;
1808 			RemoveSoldierFromTacticalSector(vs);
1809 		}
1810 	}
1811 
1812 	//Post the event!
1813 	if( !AddStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->uiArrivalTime, pGroup->ubGroupID ) )
1814 		SLOGA("Failed to add movement event.");
1815 
1816 	//For the case of player groups, we need to update the information of the soldiers.
1817 	if( pGroup->fPlayer )
1818 	{
1819 		if( pGroup->uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin( ) )
1820 		{
1821 			AddStrategicEvent( EVENT_GROUP_ABOUT_TO_ARRIVE, pGroup->uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, pGroup->ubGroupID );
1822 		}
1823 
1824 		CFOR_EACH_PLAYER_IN_GROUP(curr, pGroup)
1825 		{
1826 			SOLDIERTYPE& s = *curr->pSoldier;
1827 			s.fBetweenSectors = TRUE;
1828 			RemoveSoldierFromTacticalSector(s);
1829 		}
1830 		CheckAndHandleUnloadingOfCurrentWorld();
1831 
1832 		//If an enemy group will be crossing paths with the player group, delay the enemy group's arrival time so that
1833 		//the player will always encounter that group.
1834 		if( !pGroup->ubSectorZ )
1835 		{
1836 			DelayEnemyGroupsIfPathsCross(*pGroup);
1837 		}
1838 	}
1839 }
1840 
1841 
RemoveGroupWaypoints(GROUP & g)1842 void RemoveGroupWaypoints(GROUP& g)
1843 {
1844 	// If there aren't any waypoints to delete, then return
1845 	if (!g.pWaypoints) return;
1846 
1847 	// Remove all of the waypoints
1848 	for (WAYPOINT* i = g.pWaypoints; i;)
1849 	{
1850 		WAYPOINT* const del = i;
1851 		i = i->next;
1852 		delete del;
1853 	}
1854 
1855 	g.ubNextWaypointID = 0;
1856 	g.pWaypoints       = 0;
1857 }
1858 
1859 
1860 // Set this groups previous sector values
SetGroupPrevSectors(GROUP & g,UINT8 const x,UINT8 const y)1861 static void SetGroupPrevSectors(GROUP& g, UINT8 const x, UINT8 const y)
1862 {
1863 	g.ubPrevX = x;
1864 	g.ubPrevY = y;
1865 }
1866 
1867 
1868 static BOOLEAN gfRemovingAllGroups = FALSE;
1869 
1870 
RemoveGroup(GROUP & g)1871 void RemoveGroup(GROUP& g)
1872 {
1873 	if (g.fPersistant && !gfRemovingAllGroups)
1874 	{
1875 		CancelEmptyPersistentGroupMovement(g);
1876 		return;
1877 		DoScreenIndependantMessageBox("Strategic Info Warning:  Attempting to delete a persistant group.", MSG_BOX_FLAG_OK, NULL);
1878 	}
1879 
1880 	RemoveGroupWaypoints(g);
1881 
1882 	// Remove the arrival event if applicable.
1883 	DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1884 
1885 	// Determine what type of group we have (because it requires different methods)
1886 	if (g.fPlayer)
1887 	{
1888 		while (g.pPlayerList)
1889 		{
1890 			PLAYERGROUP* const pPlayer = g.pPlayerList;
1891 			g.pPlayerList = g.pPlayerList->next;
1892 			delete pPlayer;
1893 		}
1894 	}
1895 	else
1896 	{
1897 		RemoveGroupFromStrategicAILists(g);
1898 		delete g.pEnemyGroup;
1899 	}
1900 
1901 	RemoveGroupFromList(&g);
1902 
1903 	/* safety check: if this group is the BattleGroup, invalid the pointer */
1904 	if(gpBattleGroup == &g)
1905 	{
1906 		gpBattleGroup = 0;
1907 	}
1908 }
1909 
1910 
RemoveAllGroups()1911 void RemoveAllGroups()
1912 {
1913 	gfRemovingAllGroups = TRUE;
1914 	while( gpGroupList )
1915 	{
1916 		RemoveGroup(*gpGroupList);
1917 	}
1918 	gfRemovingAllGroups = FALSE;
1919 }
1920 
1921 
SetGroupSectorValue(INT16 const x,INT16 const y,INT16 const z,GROUP & g)1922 void SetGroupSectorValue(INT16 const x, INT16 const y, INT16 const z, GROUP& g)
1923 {
1924 	RemoveGroupWaypoints(g);
1925 
1926 	// Set sector x and y to passed values
1927 	g.ubSectorX       = x;
1928 	g.ubSectorY       = y;
1929 	g.ubNextX         = x;
1930 	g.ubNextY         = y;
1931 	g.ubSectorZ       = z;
1932 	g.fBetweenSectors = FALSE;
1933 
1934 	// Set next sectors same as current
1935 	g.ubOriginalSector = SECTOR(x, y);
1936 	DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1937 
1938 	// Set all of the mercs in the group so that they are in the new sector, too.
1939 	CFOR_EACH_PLAYER_IN_GROUP(i, &g)
1940 	{
1941 		SOLDIERTYPE& s = *i->pSoldier;
1942 		s.sSectorX        = x;
1943 		s.sSectorY        = y;
1944 		s.bSectorZ        = z;
1945 		s.fBetweenSectors = FALSE;
1946 	}
1947 
1948 	CheckAndHandleUnloadingOfCurrentWorld();
1949 }
1950 
1951 
SetEnemyGroupSector(GROUP & g,UINT8 const sector_id)1952 void SetEnemyGroupSector(GROUP& g, UINT8 const sector_id)
1953 {
1954 	// Make sure it is valid
1955 	DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
1956 
1957 	if (!gfRandomizingPatrolGroup) RemoveGroupWaypoints(g);
1958 
1959 	g.ubSectorX = g.ubNextX = SECTORX(sector_id);
1960 	g.ubSectorY = g.ubNextY = SECTORY(sector_id);
1961 	g.ubSectorZ = 0;
1962 	g.fBetweenSectors = FALSE;
1963 }
1964 
1965 
1966 // Set groups next sector x,y value, used ONLY for teleporting groups
SetGroupNextSectorValue(INT16 const x,INT16 const y,GROUP & g)1967 static void SetGroupNextSectorValue(INT16 const x, INT16 const y, GROUP& g)
1968 {
1969 	RemoveGroupWaypoints(g);
1970 	// Set sector x and y to passed values
1971 	g.ubNextX         = x;
1972 	g.ubNextY         = y;
1973 	g.fBetweenSectors = FALSE;
1974 	// Set next sectors same as current
1975 	g.ubOriginalSector = SECTOR(g.ubSectorX, g.ubSectorY);
1976 }
1977 
1978 
CalculateTravelTimeOfGroup(GROUP const * const pGroup)1979 INT32 CalculateTravelTimeOfGroup(GROUP const* const pGroup)
1980 {
1981 	INT32 iDelta;
1982 	UINT32 uiEtaTime = 0;
1983 	WAYPOINT *pNode = NULL;
1984 	WAYPOINT pCurrent, pDest;
1985 
1986 	// check if valid group
1987 	if( pGroup == NULL )
1988 	{
1989 		// return current time
1990 		return( uiEtaTime );
1991 	}
1992 
1993 	// set up next node
1994 	pNode = pGroup-> pWaypoints;
1995 
1996 	// now get the delta in current sector and next sector
1997 	iDelta = ( INT32 )( SECTOR( pGroup->ubSectorX, pGroup->ubSectorY ) - SECTOR( pGroup->ubNextX, pGroup->ubNextY ) );
1998 
1999 	if( iDelta == 0 )
2000 	{
2001 		// not going anywhere...return current time
2002 		return( uiEtaTime );
2003 	}
2004 
2005 
2006 	// if already on the road
2007 	if ( pGroup->fBetweenSectors )
2008 	{
2009 		// to get travel time to the first sector, use the arrival time, this way it accounts for delays due to simul. arrival
2010 		if ( pGroup->uiArrivalTime >= GetWorldTotalMin( ) )
2011 		{
2012 			uiEtaTime += ( pGroup->uiArrivalTime - GetWorldTotalMin( ) );
2013 		}
2014 
2015 		// first waypoint is NEXT sector
2016 		pCurrent.x = pGroup->ubNextX;
2017 		pCurrent.y = pGroup->ubNextY;
2018 	}
2019 	else
2020 	{
2021 		// first waypoint is CURRENT sector
2022 		pCurrent.x = pGroup->ubSectorX;
2023 		pCurrent.y = pGroup->ubSectorY;
2024 	}
2025 
2026 	while( pNode )
2027 	{
2028 		pDest.x = pNode->x;
2029 		pDest.y = pNode->y;
2030 
2031 		// update eta time by the path between these 2 waypts
2032 		uiEtaTime += FindTravelTimeBetweenWaypoints( &pCurrent, &pDest, pGroup );
2033 
2034 		pCurrent.x = pNode->x;
2035 		pCurrent.y = pNode->y;
2036 
2037 		// next waypt
2038 		pNode = pNode->next;
2039 	}
2040 
2041 	return( uiEtaTime );
2042 }
2043 
2044 
FindTravelTimeBetweenWaypoints(WAYPOINT const * const pSource,WAYPOINT const * const pDest,GROUP const * const pGroup)2045 INT32 FindTravelTimeBetweenWaypoints(WAYPOINT const* const pSource, WAYPOINT const* const pDest,  GROUP const * const pGroup)
2046 {
2047 	UINT8 ubStart=0, ubEnd = 0;
2048 	INT32 iDelta = 0;
2049 	INT32 iCurrentCostInTime = 0;
2050 	UINT8 ubCurrentSector = 0;
2051 	UINT8 ubDirection;
2052 	INT32 iThisCostInTime;
2053 
2054 
2055 	// find travel time between waypoints
2056 	if( !pSource || !pDest )
2057 	{
2058 		// no change
2059 		return( iCurrentCostInTime );
2060 	}
2061 
2062 	// get start and end setor values
2063 	ubStart = SECTOR( pSource->x, pSource->y );
2064 	ubEnd   = SECTOR( pDest->x,   pDest->y );
2065 
2066 	// are we in fact moving?
2067 	if( ubStart == ubEnd )
2068 	{
2069 		// no
2070 		return( iCurrentCostInTime );
2071 	}
2072 
2073 	iDelta = ( INT32 )( ubEnd - ubStart );
2074 
2075 	// which direction are we moving?
2076 	if( iDelta > 0 )
2077 	{
2078 		if( iDelta % ( SOUTH_MOVE - 2 ) == 0 )
2079 		{
2080 			iDelta = ( SOUTH_MOVE - 2 ) ;
2081 			ubDirection = SOUTH_STRATEGIC_MOVE;
2082 		}
2083 		else
2084 		{
2085 			iDelta = EAST_MOVE;
2086 			ubDirection = EAST_STRATEGIC_MOVE;
2087 		}
2088 	}
2089 	else
2090 	{
2091 		if( iDelta % ( NORTH_MOVE + 2 ) == 0 )
2092 		{
2093 			iDelta = ( NORTH_MOVE + 2 );
2094 			ubDirection = NORTH_STRATEGIC_MOVE;
2095 		}
2096 		else
2097 		{
2098 			iDelta = WEST_MOVE;
2099 			ubDirection = WEST_STRATEGIC_MOVE;
2100 		}
2101 	}
2102 
2103 	for( ubCurrentSector = ubStart; ubCurrentSector != ubEnd; ubCurrentSector += ( INT8 ) iDelta )
2104 	{
2105 		// find diff between current and next
2106 		iThisCostInTime = GetSectorMvtTimeForGroup( ubCurrentSector, ubDirection, pGroup );
2107 
2108 		AssertMsg(iThisCostInTime != static_cast<INT32>(TRAVERSE_TIME_IMPOSSIBLE), String("Group %d (%s) attempting illegal move from sector %d, dir %d (%s).",
2109 					pGroup->ubGroupID, ( pGroup->fPlayer ) ? "Player" : "AI",
2110 					ubCurrentSector, ubDirection,
2111 					gszTerrain[SectorInfo[ubCurrentSector].ubTraversability[ubDirection]] ) );
2112 
2113 		// accumulate it
2114 		iCurrentCostInTime += iThisCostInTime;
2115 	}
2116 
2117 	return( iCurrentCostInTime );
2118 }
2119 
2120 
2121 #define FOOT_TRAVEL_TIME    89
2122 #define CAR_TRAVEL_TIME     30
2123 #define TRUCK_TRAVEL_TIME   32
2124 #define TRACKED_TRAVEL_TIME 46
2125 #define AIR_TRAVEL_TIME     10
2126 
2127 
2128 // Changes: direction contains the strategic move value, not the delta value.
GetSectorMvtTimeForGroup(UINT8 const ubSector,UINT8 const direction,GROUP const * const g)2129 INT32 GetSectorMvtTimeForGroup(UINT8 const ubSector, UINT8 const direction, GROUP const* const g)
2130 {
2131 	/* Determine the group's method(s) of transportation.  If more than one, we
2132 	 * will always use the highest time. */
2133 	UINT8 const transport_mask     = g->ubTransportationMask;
2134 	UINT8 const traverse_type      = SectorInfo[ubSector].ubTraversability[direction];
2135 	INT32       best_traverse_time = 1000000;
2136 
2137 	if (traverse_type == EDGEOFWORLD) return TRAVERSE_TIME_IMPOSSIBLE;
2138 
2139 	/* ARM: Made air-only travel take its normal time per sector even through
2140 	 * towns.  Because Skyrider charges by the sector, not by flying time, it's
2141 	 * annoying when his default route detours through a town to save time, but
2142 	 * costs extra money. This isn't exactly unrealistic, since the chopper
2143 	 * shouldn't be faster flying over a town anyway. Not that other kinds of
2144 	 * travel should be either - but the towns represents a kind of warping of our
2145 	 * space-time scale as it is. */
2146 	if (traverse_type == TOWN && transport_mask != AIR)
2147 		return 5; // Very fast, and vehicle types don't matter.
2148 
2149 	if (transport_mask & FOOT)
2150 	{
2151 		UINT8 traverse_mod;
2152 		switch (traverse_type)
2153 		{
2154 			case ROAD:     traverse_mod = 100; break;
2155 			case PLAINS:   traverse_mod =  85; break;
2156 			case SAND:     traverse_mod =  50; break;
2157 			case SPARSE:   traverse_mod =  70; break;
2158 			case DENSE:    traverse_mod =  60; break;
2159 			case SWAMP:    traverse_mod =  35; break;
2160 			case WATER:    traverse_mod =  25; break;
2161 			case HILLS:    traverse_mod =  50; break;
2162 			case NS_RIVER: traverse_mod =  25; break;
2163 			case EW_RIVER: traverse_mod =  25; break;
2164 			default:       return TRAVERSE_TIME_IMPOSSIBLE;
2165 		}
2166 		INT32 const traverse_time = FOOT_TRAVEL_TIME * 100 / traverse_mod;
2167 		if (best_traverse_time > traverse_time)
2168 			best_traverse_time = traverse_time;
2169 
2170 		if (g->fPlayer)
2171 		{
2172 			INT32 highest_encumbrance = 100;
2173 			CFOR_EACH_PLAYER_IN_GROUP(curr, g)
2174 			{
2175 				SOLDIERTYPE const* const s = curr->pSoldier;
2176 				if (s->bAssignment == VEHICLE) continue;
2177 				/* Soldier is on foot and travelling.  Factor encumbrance into movement
2178 				 * rate. */
2179 				INT32 const encumbrance = CalculateCarriedWeight(s);
2180 				if (highest_encumbrance < encumbrance)
2181 				{
2182 					highest_encumbrance = encumbrance;
2183 				}
2184 			}
2185 			best_traverse_time = best_traverse_time * highest_encumbrance / 100;
2186 		}
2187 	}
2188 
2189 	if (transport_mask & CAR)
2190 	{
2191 		UINT8 traverse_mod;
2192 		switch (traverse_type)
2193 		{
2194 			case ROAD: traverse_mod = 100; break;
2195 			default:   return TRAVERSE_TIME_IMPOSSIBLE;
2196 		}
2197 		INT32 const traverse_time = CAR_TRAVEL_TIME * 100 / traverse_mod;
2198 		if (best_traverse_time > traverse_time)
2199 			best_traverse_time = traverse_time;
2200 	}
2201 
2202 	if (transport_mask & TRUCK)
2203 	{
2204 		UINT8 traverse_mod;
2205 		switch (traverse_type)
2206 		{
2207 			case ROAD:   traverse_mod = 100; break;
2208 			case PLAINS: traverse_mod =  75; break;
2209 			case SPARSE: traverse_mod =  60; break;
2210 			case HILLS:  traverse_mod =  50; break;
2211 			default:     return TRAVERSE_TIME_IMPOSSIBLE;
2212 		}
2213 		INT32 const traverse_time = TRUCK_TRAVEL_TIME * 100 / traverse_mod;
2214 		if (best_traverse_time > traverse_time)
2215 			best_traverse_time = traverse_time;
2216 	}
2217 
2218 	if (transport_mask & TRACKED)
2219 	{
2220 		UINT8 traverse_mod;
2221 		switch (traverse_type)
2222 		{
2223 			case ROAD:     traverse_mod = 100; break;
2224 			case PLAINS:   traverse_mod = 100; break;
2225 			case SAND:     traverse_mod =  70; break;
2226 			case SPARSE:   traverse_mod =  60; break;
2227 			case HILLS:    traverse_mod =  60; break;
2228 			case NS_RIVER: traverse_mod =  20; break;
2229 			case EW_RIVER: traverse_mod =  20; break;
2230 			case WATER:    traverse_mod =  10; break;
2231 			default:       return TRAVERSE_TIME_IMPOSSIBLE;
2232 		}
2233 		INT32 const traverse_time = TRACKED_TRAVEL_TIME * 100 / traverse_mod;
2234 		if (best_traverse_time > traverse_time)
2235 			best_traverse_time = traverse_time;
2236 	}
2237 
2238 	if (transport_mask & AIR)
2239 	{
2240 		INT32 const traverse_time = AIR_TRAVEL_TIME;
2241 		if (best_traverse_time > traverse_time)
2242 			best_traverse_time = traverse_time;
2243 	}
2244 
2245 	return best_traverse_time;
2246 }
2247 
2248 
2249 // Counts the number of live mercs in any given sector.
PlayerMercsInSector(UINT8 const x,UINT8 const y,UINT8 const z)2250 UINT8 PlayerMercsInSector(UINT8 const x, UINT8 const y, UINT8 const z)
2251 {
2252 	UINT8 n_mercs = 0;
2253 	CFOR_EACH_PLAYER_GROUP(g)
2254 	{
2255 		if (g->fBetweenSectors) continue;
2256 		if (g->ubSectorX != x || g->ubSectorY != y || g->ubSectorZ != z) continue;
2257 		/* We have a group, make sure that it isn't a group containing only dead
2258 		 * members. */
2259 		CFOR_EACH_PLAYER_IN_GROUP(p, g)
2260 		{
2261 			SOLDIERTYPE const* const s = p->pSoldier;
2262 			if (s->bLife == 0)                      continue;
2263 			// Robots count as mercs here, because they can fight, but vehicles don't
2264 			if (s->uiStatusFlags & SOLDIER_VEHICLE) continue;
2265 			n_mercs++;
2266 		}
2267 	}
2268 	return n_mercs;
2269 }
2270 
2271 
PlayerGroupsInSector(UINT8 const x,UINT8 const y,UINT8 const z)2272 UINT8 PlayerGroupsInSector(UINT8 const x, UINT8 const y, UINT8 const z)
2273 {
2274 	UINT8 n_groups = 0;
2275 	CFOR_EACH_PLAYER_GROUP(g)
2276 	{
2277 		if (g->fBetweenSectors) continue;
2278 		if (g->ubSectorX != x || g->ubSectorY != y || g->ubSectorZ != z) continue;
2279 		/* We have a group, make sure that it isn't a group containing only dead
2280 		 * members. */
2281 		CFOR_EACH_PLAYER_IN_GROUP(p, g)
2282 		{
2283 			if (p->pSoldier->bLife == 0) continue;
2284 			++n_groups;
2285 			break;
2286 		}
2287 	}
2288 	return n_groups;
2289 }
2290 
2291 
2292 // is the player group with this id in motion?
PlayerIDGroupInMotion(UINT8 const id)2293 bool PlayerIDGroupInMotion(UINT8 const id)
2294 {
2295 	GROUP* const g = GetGroup(id);
2296 	return g && PlayerGroupInMotion(g);
2297 }
2298 
2299 
2300 // is the player group in motion?
PlayerGroupInMotion(GROUP const * const pGroup)2301 BOOLEAN PlayerGroupInMotion(GROUP const* const pGroup)
2302 {
2303 	return( pGroup -> fBetweenSectors );
2304 }
2305 
2306 
2307 /* Add this group to the current battle fray!
2308  * NOTE: For enemies, only MAX_STRATEGIC_TEAM_SIZE at a time can be in a battle,
2309  * so if it ever gets past that, god help the player, but we'll have to insert
2310  * them as those slots free up. */
HandleArrivalOfReinforcements(GROUP const * const g)2311 void HandleArrivalOfReinforcements(GROUP const* const g)
2312 {
2313 	if (g->fPlayer)
2314 	{ /* We don't have to worry about filling up the player slots, because it is
2315 		 * impossible to have more player's in the game than the number of slots
2316 		 * available for the player. */
2317 
2318 		/* First, determine which entrypoint to use, based on the travel direction
2319 		 * of the group */
2320 		UINT8         const x = g->ubSectorX;
2321 		UINT8         const y = g->ubSectorY;
2322 		InsertionCode const strategic_insertion_code =
2323 			x < g->ubPrevX ? INSERTION_CODE_EAST  :
2324 			x > g->ubPrevX ? INSERTION_CODE_WEST  :
2325 			y < g->ubPrevY ? INSERTION_CODE_SOUTH :
2326 			y > g->ubPrevY ? INSERTION_CODE_NORTH :
2327 			throw std::logic_error("reinforcements come from same sector");
2328 
2329 		bool first = true;
2330 		CFOR_EACH_PLAYER_IN_GROUP(p, g)
2331 		{
2332 			SOLDIERTYPE& s = *p->pSoldier;
2333 			s.ubStrategicInsertionCode = strategic_insertion_code;
2334 			UpdateMercInSector(s, x, y, 0);
2335 
2336 			// Do arrives quote
2337 			if (first) TacticalCharacterDialogue(&s, QUOTE_MERC_REACHED_DESTINATION);
2338 			first = false;
2339 		}
2340 		ScreenMsg(FONT_YELLOW, MSG_INTERFACE, g_langRes->Message[STR_PLAYER_REINFORCEMENTS]);
2341 	}
2342 	else
2343 	{
2344 		gfPendingEnemies = TRUE;
2345 		ResetMortarsOnTeamCount();
2346 		AddPossiblePendingEnemiesToBattle();
2347 	}
2348 }
2349 
2350 
PlayersBetweenTheseSectors(INT16 const sec_src,INT16 const sec_dst,INT32 * const n_enter,INT32 * const n_exit,BOOLEAN * const about_to_arrive_enter)2351 BOOLEAN PlayersBetweenTheseSectors(INT16 const sec_src, INT16 const sec_dst, INT32* const n_enter, INT32* const n_exit, BOOLEAN* const about_to_arrive_enter)
2352 {
2353 	*n_enter               = 0;
2354 	*n_exit                = 0;
2355 	*about_to_arrive_enter = FALSE;
2356 
2357 	GROUP const* const bg         = gpBattleGroup;
2358 	INT16        const sec_battle = bg ? SECTOR(bg->ubSectorX, bg->ubSectorY) : -1;
2359 
2360 	/* Get number of characters entering/existing between these two sectors.
2361 	 * Special conditions during pre-battle interface to return where this
2362 	 * function is used to show potential retreating directions instead! */
2363 
2364 	CFOR_EACH_PLAYER_GROUP(i)
2365 	{
2366 		GROUP const& g = *i;
2367 		bool  const is_heli_group = IsGroupTheHelicopterGroup(g);
2368 
2369 		/* If this group is aboard the helicopter and we're showing the airspace
2370 		 * layer, don't count any mercs aboard the chopper, because the chopper icon
2371 		 * itself serves the function of showing the location/size of this group. */
2372 		if (is_heli_group && fShowAircraftFlag) continue;
2373 
2374 		/* If only showing retreat paths, ignore groups not in the battle sector.
2375 		 * If NOT showing retreat paths, ignore groups not between sectors. */
2376 		if (gfDisplayPotentialRetreatPaths ? sec_battle != sec_src : !g.fBetweenSectors) continue;
2377 
2378 		UINT8 n_mercs = g.ubGroupSize;
2379 		if (n_mercs == 0) // Skip empty persistent groups
2380 		{
2381 			Assert(g.fPersistant);
2382 			continue;
2383 		}
2384 
2385 		INT16 const sec_prev = IS_VALID_SECTOR(g.ubPrevX, g.ubPrevY) ? SECTOR(g.ubPrevX,   g.ubPrevY) : -1;
2386 		INT16 const sec_cur  = SECTOR(g.ubSectorX, g.ubSectorY);
2387 		INT16 const sec_next = IS_VALID_SECTOR(g.ubNextX, g.ubNextY) ? SECTOR(g.ubNextX,   g.ubNextY) : -1;
2388 
2389 		bool const may_retreat_from_battle =
2390 			sec_battle == sec_src && sec_cur == sec_src && sec_prev == sec_dst;
2391 
2392 		bool const retreating_from_battle =
2393 			sec_battle == sec_dst && sec_cur == sec_dst && sec_prev == sec_src;
2394 
2395 		if (may_retreat_from_battle || (sec_cur == sec_src && sec_next == sec_dst))
2396 		{
2397 			// If it's a valid vehicle, but not the helicopter (which can fly empty)
2398 			if (g.fVehicle && !is_heli_group)
2399 			{ // subtract 1, we don't wanna count the vehicle itself for purposes of showing a number on the map
2400 				n_mercs--;
2401 			}
2402 
2403 			*n_enter += n_mercs;
2404 
2405 			if (may_retreat_from_battle ||
2406 					g.uiArrivalTime - GetWorldTotalMin() <= ABOUT_TO_ARRIVE_DELAY)
2407 			{
2408 				*about_to_arrive_enter = TRUE;
2409 			}
2410 		}
2411 		else if (retreating_from_battle || (sec_cur == sec_dst && sec_next == sec_src))
2412 		{
2413 			// If it's a valid vehicle, but not the helicopter (which can fly empty)
2414 			if (g.fVehicle && !is_heli_group)
2415 			{ // subtract 1, we don't wanna count the vehicle itself for purposes of showing a number on the map
2416 				n_mercs--;
2417 			}
2418 
2419 			*n_exit += n_mercs;
2420 		}
2421 	}
2422 
2423 	// if there was actually anyone leaving this sector and entering next
2424 	return *n_enter > 0;
2425 }
2426 
2427 
MoveAllGroupsInCurrentSectorToSector(UINT8 const x,UINT8 const y,UINT8 const z)2428 void MoveAllGroupsInCurrentSectorToSector(UINT8 const x, UINT8 const y, UINT8 const z)
2429 {
2430 	FOR_EACH_PLAYER_GROUP(g)
2431 	{
2432 		if (g->ubSectorX != gWorldSectorX)  continue;
2433 		if (g->ubSectorY != gWorldSectorY)  continue;
2434 		if (g->ubSectorZ != gbWorldSectorZ) continue;
2435 		if (g->fBetweenSectors)             continue;
2436 
2437 		// This player group is in the currently loaded sector
2438 		g->ubSectorX = x;
2439 		g->ubSectorY = y;
2440 		g->ubSectorZ = z;
2441 		CFOR_EACH_PLAYER_IN_GROUP(p, g)
2442 		{
2443 			p->pSoldier->sSectorX        = x;
2444 			p->pSoldier->sSectorY        = y;
2445 			p->pSoldier->bSectorZ        = z;
2446 			p->pSoldier->fBetweenSectors = FALSE;
2447 		}
2448 	}
2449 	CheckAndHandleUnloadingOfCurrentWorld();
2450 }
2451 
2452 
2453 static void SaveEnemyGroupStruct(HWFILE, GROUP const&);
2454 static void SavePlayerGroupList(HWFILE, GROUP const*);
2455 static void SaveWayPointList(HWFILE, GROUP const*);
2456 
2457 
SaveStrategicMovementGroupsToSaveGameFile(HWFILE const f)2458 void SaveStrategicMovementGroupsToSaveGameFile(HWFILE const f)
2459 {
2460 	// Save the number of movement groups to the saved game file
2461 	UINT32 uiNumberOfGroups = 0;
2462 	CFOR_EACH_GROUP(g) ++uiNumberOfGroups;
2463 	FileWrite(f, &uiNumberOfGroups, sizeof(UINT32));
2464 
2465 	CFOR_EACH_GROUP(g)
2466 	{
2467 		BYTE data[84];
2468 		DataWriter d{data};
2469 		INJ_BOOL(d, g->fDebugGroup)
2470 		INJ_BOOL(d, g->fPlayer)
2471 		INJ_BOOL(d, g->fVehicle)
2472 		INJ_BOOL(d, g->fPersistant)
2473 		INJ_U8(d, g->ubGroupID)
2474 		INJ_U8(d, g->ubGroupSize)
2475 		INJ_U8(d, g->ubSectorX)
2476 		INJ_U8(d, g->ubSectorY)
2477 		INJ_U8(d, g->ubSectorZ)
2478 		INJ_U8(d, g->ubNextX)
2479 		INJ_U8(d, g->ubNextY)
2480 		INJ_U8(d, g->ubPrevX)
2481 		INJ_U8(d, g->ubPrevY)
2482 		INJ_U8(d, g->ubOriginalSector)
2483 		INJ_BOOL(d, g->fBetweenSectors)
2484 		INJ_U8(d, g->ubMoveType)
2485 		INJ_U8(d, g->ubNextWaypointID)
2486 		INJ_SKIP(d, 3)
2487 		INJ_U32(d, g->uiArrivalTime)
2488 		INJ_U32(d, g->uiTraverseTime)
2489 		INJ_SKIP(d, 8)
2490 		INJ_U8(d, g->ubTransportationMask)
2491 		INJ_SKIP(d, 3)
2492 		INJ_U32(d, g->uiFlags)
2493 		INJ_U8(d, g->ubCreatedSectorID)
2494 		INJ_U8(d, g->ubSectorIDOfLastReassignment)
2495 		INJ_SKIP(d, 38)
2496 		Assert(d.getConsumed() == lengthof(data));
2497 
2498 		FileWrite(f, data, sizeof(data));
2499 
2500 		// Save the linked list, for the current type of group
2501 		if (g->fPlayer)
2502 		{
2503 			if (g->ubGroupSize) SavePlayerGroupList(f, g);
2504 		}
2505 		else
2506 		{
2507 			SaveEnemyGroupStruct(f, *g);
2508 		}
2509 
2510 		SaveWayPointList(f, g);
2511 	}
2512 
2513 	// Save the unique id mask
2514 	FileWrite(f, uniqueIDMask, sizeof(uniqueIDMask));
2515 }
2516 
2517 
2518 static void LoadEnemyGroupStructFromSavedGame(HWFILE, GROUP&);
2519 static void LoadPlayerGroupList(HWFILE, GROUP*);
2520 static void LoadWayPointList(HWFILE, GROUP*);
2521 
2522 
LoadStrategicMovementGroupsFromSavedGameFile(HWFILE const f)2523 void LoadStrategicMovementGroupsFromSavedGameFile(HWFILE const f)
2524 {
2525 	Assert(gpGroupList == NULL);
2526 
2527 	// Load the number of nodes in the list
2528 	UINT32 uiNumberOfGroups;
2529 	FileRead(f, &uiNumberOfGroups, sizeof(UINT32));
2530 
2531 	//loop through all the nodes and add them to the LL
2532 	GROUP** anchor = &gpGroupList;
2533 	for (UINT32 i = uiNumberOfGroups; i != 0; --i)
2534 	{
2535 		GROUP* const g = new GROUP{};
2536 
2537 		BYTE data[84];
2538 		FileRead(f, data, sizeof(data));
2539 
2540 		DataReader d{data};
2541 		EXTR_BOOL(d, g->fDebugGroup)
2542 		EXTR_BOOL(d, g->fPlayer)
2543 		EXTR_BOOL(d, g->fVehicle)
2544 		EXTR_BOOL(d, g->fPersistant)
2545 		EXTR_U8(d, g->ubGroupID)
2546 		EXTR_U8(d, g->ubGroupSize)
2547 		EXTR_U8(d, g->ubSectorX)
2548 		EXTR_U8(d, g->ubSectorY)
2549 		EXTR_U8(d, g->ubSectorZ)
2550 		EXTR_U8(d, g->ubNextX)
2551 		EXTR_U8(d, g->ubNextY)
2552 		EXTR_U8(d, g->ubPrevX)
2553 		EXTR_U8(d, g->ubPrevY)
2554 		EXTR_U8(d, g->ubOriginalSector)
2555 		EXTR_BOOL(d, g->fBetweenSectors)
2556 		EXTR_U8(d, g->ubMoveType)
2557 		EXTR_U8(d, g->ubNextWaypointID)
2558 		EXTR_SKIP(d, 3)
2559 		EXTR_U32(d, g->uiArrivalTime)
2560 		EXTR_U32(d, g->uiTraverseTime)
2561 		EXTR_SKIP(d, 8)
2562 		EXTR_U8(d, g->ubTransportationMask)
2563 		EXTR_SKIP(d, 3)
2564 		EXTR_U32(d, g->uiFlags)
2565 		EXTR_U8(d, g->ubCreatedSectorID)
2566 		EXTR_U8(d, g->ubSectorIDOfLastReassignment)
2567 		EXTR_SKIP(d, 38)
2568 		Assert(d.getConsumed() == lengthof(data));
2569 
2570 		if (g->fPlayer)
2571 		{
2572 			// If there is a player list, add it
2573 			if (g->ubGroupSize) LoadPlayerGroupList(f, g);
2574 		}
2575 		else // Else it's an enemy group
2576 		{
2577 			LoadEnemyGroupStructFromSavedGame(f, *g);
2578 		}
2579 
2580 		LoadWayPointList(f, g);
2581 
2582 		// Add the node to the list
2583 		*anchor = g;
2584 		anchor  = &g->next;
2585 	}
2586 
2587 	//@@@ TEMP!
2588 	//Rebuild the uniqueIDMask as a very old bug broke the uniqueID assignments in extremely rare cases.
2589 	std::fill(std::begin(uniqueIDMask), std::end(uniqueIDMask), 0);
2590 	CFOR_EACH_GROUP(g)
2591 	{
2592 		const UINT32 index = g->ubGroupID / 32;
2593 		const UINT32 bit   = g->ubGroupID % 32;
2594 		const UINT32 mask  = 1 << bit;
2595 		uniqueIDMask[index] += mask;
2596 	}
2597 
2598 	// Skip over saved unique id mask
2599 	FileSeek(f, 32, FILE_SEEK_FROM_CURRENT);
2600 }
2601 
2602 
2603 // Saves the Player's group list to the saved game file
SavePlayerGroupList(HWFILE const f,GROUP const * const g)2604 static void SavePlayerGroupList(HWFILE const f, GROUP const* const g)
2605 {
2606 	// Save the number of nodes in the list
2607 	UINT32 uiNumberOfNodesInList = 0;
2608 	CFOR_EACH_PLAYER_IN_GROUP(p, g) ++uiNumberOfNodesInList;
2609 	FileWrite(f, &uiNumberOfNodesInList, sizeof(UINT32));
2610 
2611 	// Loop through and save only the players profile id
2612 	CFOR_EACH_PLAYER_IN_GROUP(p, g)
2613 	{
2614 		// Save the ubProfile ID for this node
2615 		const UINT32 uiProfileID = p->pSoldier->ubProfile;
2616 		FileWrite(f, &uiProfileID, sizeof(UINT32));
2617 	}
2618 }
2619 
2620 
2621 //Loads the LL for the playerlist from the savegame file
LoadPlayerGroupList(HWFILE const f,GROUP * const g)2622 static void LoadPlayerGroupList(HWFILE const f, GROUP* const g)
2623 {
2624 	// Load the number of nodes in the player list
2625 	UINT32 node_count;
2626 	FileRead(f, &node_count, sizeof(UINT32));
2627 
2628 	PLAYERGROUP** anchor = &g->pPlayerList;
2629 	for (UINT32 i = node_count; i != 0; --i)
2630 	{
2631 		PLAYERGROUP* const pg = new PLAYERGROUP{};
2632 
2633 		UINT32 profile_id;
2634 		FileRead(f, &profile_id, sizeof(UINT32));
2635 
2636 		SOLDIERTYPE* const s = FindSoldierByProfileIDOnPlayerTeam(profile_id);
2637 		//Should never happen
2638 		//Assert(s != NULL);
2639 		pg->pSoldier = s;
2640 		pg->next     = NULL;
2641 
2642 		*anchor = pg;
2643 		anchor  = &pg->next;
2644 	}
2645 }
2646 
2647 
2648 // Saves the enemy group struct to the saved game file
SaveEnemyGroupStruct(HWFILE const f,GROUP const & g)2649 static void SaveEnemyGroupStruct(HWFILE const f, GROUP const& g)
2650 {
2651 	BYTE              data[29];
2652 	DataWriter d{data};
2653 	ENEMYGROUP const& eg = *g.pEnemyGroup;
2654 	INJ_U8(  d, eg.ubNumTroops)
2655 	INJ_U8(  d, eg.ubNumElites)
2656 	INJ_U8(  d, eg.ubNumAdmins)
2657 	INJ_SKIP(d, 1)
2658 	INJ_U8(  d, eg.ubPendingReinforcements)
2659 	INJ_U8(  d, eg.ubAdminsInBattle)
2660 	INJ_U8(  d, eg.ubIntention)
2661 	INJ_U8(  d, eg.ubTroopsInBattle)
2662 	INJ_U8(  d, eg.ubElitesInBattle)
2663 	INJ_SKIP(d, 20)
2664 	Assert(d.getConsumed() == lengthof(data));
2665 
2666 	FileWrite(f, data, sizeof(data));
2667 }
2668 
2669 
2670 // Loads the enemy group struct from the saved game file
LoadEnemyGroupStructFromSavedGame(HWFILE const f,GROUP & g)2671 static void LoadEnemyGroupStructFromSavedGame(HWFILE const f, GROUP& g)
2672 {
2673 	BYTE data[29];
2674 	FileRead(f, data, sizeof(data));
2675 
2676 	ENEMYGROUP* const eg = new ENEMYGROUP{};
2677 	DataReader d{data};
2678 	EXTR_U8(  d, eg->ubNumTroops)
2679 	EXTR_U8(  d, eg->ubNumElites)
2680 	EXTR_U8(  d, eg->ubNumAdmins)
2681 	EXTR_SKIP(d, 1)
2682 	EXTR_U8(  d, eg->ubPendingReinforcements)
2683 	EXTR_U8(  d, eg->ubAdminsInBattle)
2684 	EXTR_U8(  d, eg->ubIntention)
2685 	EXTR_U8(  d, eg->ubTroopsInBattle)
2686 	EXTR_U8(  d, eg->ubElitesInBattle)
2687 	EXTR_SKIP(d, 20)
2688 	Assert(d.getConsumed() == lengthof(data));
2689 
2690 	g.pEnemyGroup = eg;
2691 }
2692 
2693 
SaveWayPointList(HWFILE const f,GROUP const * const g)2694 static void SaveWayPointList(HWFILE const f, GROUP const* const g)
2695 {
2696 	// Save the number of waypoints
2697 	UINT32 uiNumberOfWayPoints = 0;
2698 	for (const WAYPOINT* w = g->pWaypoints; w != NULL; w = w->next)
2699 	{
2700 		++uiNumberOfWayPoints;
2701 	}
2702 	FileWrite(f, &uiNumberOfWayPoints, sizeof(UINT32));
2703 
2704 	for (const WAYPOINT* w = g->pWaypoints; w != NULL; w = w->next)
2705 	{
2706 		BYTE  data[8];
2707 		DataWriter d{data};
2708 		INJ_U8(  d, w->x)
2709 		INJ_U8(  d, w->y)
2710 		INJ_SKIP(d, 6)
2711 		Assert(d.getConsumed() == lengthof(data));
2712 
2713 		FileWrite(f, data, sizeof(data));
2714 	}
2715 }
2716 
2717 
LoadWayPointList(HWFILE const f,GROUP * const g)2718 static void LoadWayPointList(HWFILE const f, GROUP* const g)
2719 {
2720 	// Load the number of waypoints
2721 	UINT32 uiNumberOfWayPoints;
2722 	FileRead(f, &uiNumberOfWayPoints, sizeof(UINT32));
2723 
2724 	WAYPOINT** anchor = &g->pWaypoints;
2725 	for (UINT32 i = uiNumberOfWayPoints; i != 0; --i)
2726 	{
2727 		WAYPOINT* const w = new WAYPOINT{};
2728 
2729 		BYTE data[8];
2730 		FileRead(f, data, sizeof(data));
2731 
2732 		DataReader d{data};
2733 		EXTR_U8(  d, w->x)
2734 		EXTR_U8(  d, w->y)
2735 		EXTR_SKIP(d, 6)
2736 		Assert(d.getConsumed() == lengthof(data));
2737 
2738 		// Add the node to the list
2739 		*anchor = w;
2740 		anchor  = &w->next;
2741 	}
2742 	*anchor = NULL;
2743 }
2744 
2745 
CalculateGroupRetreatSector(GROUP * pGroup)2746 void CalculateGroupRetreatSector( GROUP *pGroup )
2747 {
2748 	SECTORINFO *pSector;
2749 	UINT32 uiSectorID;
2750 
2751 	uiSectorID = SECTOR( pGroup->ubSectorX, pGroup->ubSectorY );
2752 	pSector = &SectorInfo[ uiSectorID ];
2753 
2754 	if( pSector->ubTraversability[ NORTH_STRATEGIC_MOVE ] != GROUNDBARRIER &&
2755 			pSector->ubTraversability[ NORTH_STRATEGIC_MOVE ] != EDGEOFWORLD )
2756 	{
2757 		pGroup->ubPrevX = pGroup->ubSectorX;
2758 		pGroup->ubPrevY = pGroup->ubSectorY - 1;
2759 	}
2760 	else if( pSector->ubTraversability[ EAST_STRATEGIC_MOVE ] != GROUNDBARRIER &&
2761 			pSector->ubTraversability[ EAST_STRATEGIC_MOVE ] != EDGEOFWORLD )
2762 	{
2763 		pGroup->ubPrevX = pGroup->ubSectorX + 1;
2764 		pGroup->ubPrevY = pGroup->ubSectorY;
2765 	}
2766 	else if( pSector->ubTraversability[ WEST_STRATEGIC_MOVE ] != GROUNDBARRIER &&
2767 			pSector->ubTraversability[ WEST_STRATEGIC_MOVE ] != EDGEOFWORLD )
2768 	{
2769 		pGroup->ubPrevX = pGroup->ubSectorX - 1;
2770 		pGroup->ubPrevY = pGroup->ubSectorY;
2771 	}
2772 	else if( pSector->ubTraversability[ SOUTH_STRATEGIC_MOVE ] != GROUNDBARRIER &&
2773 			pSector->ubTraversability[ SOUTH_STRATEGIC_MOVE ] != EDGEOFWORLD )
2774 	{
2775 		pGroup->ubPrevX = pGroup->ubSectorX;
2776 		pGroup->ubPrevY = pGroup->ubSectorY + 1;
2777 	}
2778 	else
2779 	{
2780 		SLOGA("Player group cannot retreat from sector %c%d ", pGroup->ubSectorY+'A'-1, pGroup->ubSectorX);
2781 		return;
2782 	}
2783 	if( pGroup->fPlayer )
2784 	{ //update the previous sector for the mercs
2785 		CFOR_EACH_PLAYER_IN_GROUP(pPlayer, pGroup)
2786 		{
2787 			pPlayer->pSoldier->ubPrevSectorID = (UINT8)SECTOR( pGroup->ubPrevX, pGroup->ubPrevY );
2788 		}
2789 	}
2790 }
2791 
2792 //Called when all checks have been made for the group (if possible to retreat, etc.)  This function
2793 //blindly determines where to move the group.
RetreatGroupToPreviousSector(GROUP & g)2794 void RetreatGroupToPreviousSector(GROUP& g)
2795 {
2796 	AssertMsg(!g.fBetweenSectors, "Can't retreat a group when between sectors!");
2797 
2798 	UINT8 direction = 255;
2799 	if (g.ubPrevX != 16 || g.ubPrevY != 16)
2800 	{ // Group has a previous sector
2801 		g.ubNextX = g.ubPrevX;
2802 		g.ubNextY = g.ubPrevY;
2803 
2804 		// Determine the correct direction
2805 		INT32 const dx = g.ubNextX - g.ubSectorX;
2806 		INT32 const dy = g.ubNextY - g.ubSectorY;
2807 		if      (dx ==  0 && dy == -1) direction = NORTH_STRATEGIC_MOVE;
2808 		else if (dx ==  1 && dy ==  0) direction = EAST_STRATEGIC_MOVE;
2809 		else if (dx ==  0 && dy ==  1) direction = SOUTH_STRATEGIC_MOVE;
2810 		else if (dx == -1 && dy ==  0) direction = WEST_STRATEGIC_MOVE;
2811 		else
2812 		{
2813 			throw std::runtime_error(ST::format("Player group attempting illegal retreat from {}{} to {}{}.", (char)(g.ubSectorY + 'A' - 1), g.ubSectorX, (char)(g.ubNextY + 'A' - 1), g.ubNextX).to_std_string());
2814 		}
2815 	}
2816 	else
2817 	{ // Group doesn't have a previous sector. Create one, then recurse
2818 		CalculateGroupRetreatSector(&g);
2819 		RetreatGroupToPreviousSector(g);
2820 		// XXX direction is invalid, causes out-of-bounds access below
2821 	}
2822 
2823 	// Calc time to get to next waypoint
2824 	UINT8 const sector = SECTOR(g.ubSectorX, g.ubSectorY);
2825 	g.uiTraverseTime = GetSectorMvtTimeForGroup(sector, direction, &g);
2826 	AssertMsg(g.uiTraverseTime != TRAVERSE_TIME_IMPOSSIBLE, String("Group %d (%s) attempting illegal move from %c%d to %c%d (%s).", g.ubGroupID, g.fPlayer ? "Player" : "AI", g.ubSectorY + 'A', g.ubSectorX, g.ubNextY + 'A', g.ubNextX, gszTerrain[SectorInfo[sector].ubTraversability[direction]]));
2827 
2828 	// Because we are in the strategic layer, don't make the arrival instantaneous (towns)
2829 	if (g.uiTraverseTime == 0) g.uiTraverseTime = 5;
2830 
2831 	g.setArrivalTime(GetWorldTotalMin() + g.uiTraverseTime);
2832 	g.fBetweenSectors = TRUE;
2833 	g.uiFlags        |= GROUPFLAG_JUST_RETREATED_FROM_BATTLE;
2834 
2835 	if (g.fVehicle)
2836 	{ // Vehicle, set fact it is between sectors too
2837 		VEHICLETYPE& v = GetVehicleFromMvtGroup(g);
2838 		v.fBetweenSectors = TRUE;
2839 	}
2840 
2841 	if (!AddStrategicEvent(EVENT_GROUP_ARRIVAL, g.uiArrivalTime, g.ubGroupID))
2842 		SLOGA("Failed to add movement event.");
2843 
2844 	// For the case of player groups, we need to update the information of the soldiers.
2845 	if (g.fPlayer)
2846 	{
2847 		if (g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY > GetWorldTotalMin())
2848 		{
2849 			AddStrategicEvent(EVENT_GROUP_ABOUT_TO_ARRIVE, g.uiArrivalTime - ABOUT_TO_ARRIVE_DELAY, g.ubGroupID);
2850 		}
2851 
2852 		CFOR_EACH_PLAYER_IN_GROUP(i, &g)
2853 		{
2854 			SOLDIERTYPE& s = *i->pSoldier;
2855 			s.fBetweenSectors = TRUE;
2856 			RemoveSoldierFromTacticalSector(s);
2857 		}
2858 	}
2859 }
2860 
2861 
FindEnemyMovementGroupInSector(const UINT8 ubSectorX,const UINT8 ubSectorY)2862 GROUP* FindEnemyMovementGroupInSector(const UINT8 ubSectorX, const UINT8 ubSectorY)
2863 {
2864 	FOR_EACH_ENEMY_GROUP(g)
2865 	{
2866 		if (g->ubSectorX == ubSectorX &&
2867 				g->ubSectorY == ubSectorY &&
2868 				g->ubSectorZ == 0)
2869 		{
2870 			return g;
2871 		}
2872 	}
2873 	return NULL;
2874 }
2875 
2876 
FindPlayerMovementGroupInSector(const UINT8 x,const UINT8 y)2877 GROUP* FindPlayerMovementGroupInSector(const UINT8 x, const UINT8 y)
2878 {
2879 	FOR_EACH_PLAYER_GROUP(i)
2880 	{
2881 		GROUP& g = *i;
2882 		// NOTE: These checks must always match the INVOLVED group checks in PBI!!!
2883 		if (g.ubGroupSize != 0 &&
2884 			!g.fBetweenSectors &&
2885 			g.ubSectorX   == x &&
2886 			g.ubSectorY   == y &&
2887 			g.ubSectorZ   == 0 &&
2888 			!GroupHasInTransitDeadOrPOWMercs(g) &&
2889 			(!IsGroupTheHelicopterGroup(g) || !fHelicopterIsAirBorne))
2890 		{
2891 			return &g;
2892 		}
2893 	}
2894 	return NULL;
2895 }
2896 
2897 
GroupAtFinalDestination(const GROUP * const pGroup)2898 BOOLEAN GroupAtFinalDestination(const GROUP* const pGroup)
2899 {
2900 	WAYPOINT *wp;
2901 
2902 	if( pGroup->ubMoveType != ONE_WAY )
2903 		return FALSE; //Group will continue to patrol, hence never stops.
2904 
2905 	//Determine if we are at the final waypoint.
2906 	wp = GetFinalWaypoint( pGroup );
2907 
2908 	if( !wp )
2909 	{ //no waypoints, so the group is at it's destination.  This happens when
2910 		//an enemy group is created in the destination sector (which is legal for
2911 		//staging groups which always stop adjacent to their real sector destination)
2912 		return TRUE;
2913 	}
2914 
2915 	// if we're there
2916 	if( ( pGroup->ubSectorX == wp->x ) && ( pGroup->ubSectorY == wp->y ) )
2917 	{
2918 		return TRUE;
2919 	}
2920 
2921 	return FALSE;
2922 }
2923 
2924 
GetFinalWaypoint(const GROUP * const pGroup)2925 WAYPOINT* GetFinalWaypoint(const GROUP* const pGroup)
2926 {
2927 	WAYPOINT *wp;
2928 
2929 	Assert( pGroup );
2930 
2931 	//Make sure they're on a one way route, otherwise this request is illegal
2932 	Assert( pGroup->ubMoveType == ONE_WAY );
2933 
2934 	wp = pGroup->pWaypoints;
2935 	if( wp )
2936 	{
2937 		while( wp->next )
2938 		{
2939 			wp = wp->next;
2940 		}
2941 	}
2942 
2943 	return( wp );
2944 }
2945 
2946 
2947 static void ResetMovementForEnemyGroup(GROUP* pGroup);
2948 
2949 
2950 //The sector supplied resets ALL enemy groups in the sector specified.  See comments in
2951 //ResetMovementForEnemyGroup() for more details on what the resetting does.
ResetMovementForEnemyGroupsInLocation(UINT8 ubSectorX,UINT8 ubSectorY)2952 void ResetMovementForEnemyGroupsInLocation( UINT8 ubSectorX, UINT8 ubSectorY )
2953 {
2954 	INT16 sSectorX, sSectorY, sSectorZ;
2955 
2956 	GetCurrentBattleSectorXYZ( &sSectorX, &sSectorY, &sSectorZ );
2957 	FOR_EACH_GROUP_SAFE(pGroup)
2958 	{
2959 		if( !pGroup->fPlayer )
2960 		{
2961 			if( pGroup->ubSectorX == sSectorX && pGroup->ubSectorY == sSectorY )
2962 			{
2963 				ResetMovementForEnemyGroup( pGroup );
2964 			}
2965 		}
2966 	}
2967 }
2968 
2969 
2970 //This function is used to reset the location of the enemy group if they are
2971 //currently between sectors.  If they were 50% of the way from sector A10 to A11,
2972 //then after this function is called, then that group would be 0% of the way from
2973 //sector A10 to A11.  In no way does this function effect the strategic path for
2974 //the group.
ResetMovementForEnemyGroup(GROUP * pGroup)2975 static void ResetMovementForEnemyGroup(GROUP* pGroup)
2976 {
2977 	//Validate that the group is an enemy group and that it is moving.
2978 	if( pGroup->fPlayer )
2979 	{
2980 		return;
2981 	}
2982 	if( !pGroup->fBetweenSectors || !pGroup->ubNextX || !pGroup->ubNextY )
2983 	{ //Reset the group's assignment by moving it to the group's original sector as it's pending group.
2984 		RepollSAIGroup( pGroup );
2985 		return;
2986 	}
2987 
2988 	//Cancel the event that is posted.
2989 	DeleteStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->ubGroupID );
2990 
2991 	//Calculate the new arrival time (all data pertaining to movement should be valid)
2992 	if( pGroup->uiTraverseTime > 400 )
2993 	{ //The group was likely sleeping which makes for extremely long arrival times.  Shorten it
2994 		//arbitrarily.  Doesn't really matter if this isn't accurate.
2995 		pGroup->uiTraverseTime = 90;
2996 	}
2997 	pGroup->setArrivalTime(GetWorldTotalMin() + pGroup->uiTraverseTime);
2998 
2999 	//Add a new event
3000 	AddStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->uiArrivalTime, pGroup->ubGroupID );
3001 }
3002 
3003 
UpdatePersistantGroupsFromOldSave(UINT32 uiSavedGameVersion)3004 void UpdatePersistantGroupsFromOldSave( UINT32 uiSavedGameVersion )
3005 {
3006 	INT32   cnt;
3007 	BOOLEAN fDoChange = FALSE;
3008 
3009 	// ATE: If saved game is < 61, we need to do something better!
3010 	if( uiSavedGameVersion < 61 )
3011 	{
3012 		for( cnt = 0; cnt < 55; cnt++ )
3013 		{
3014 			// create mvt groups
3015 			GROUP* const pGroup = GetGroup(cnt);
3016 			if( pGroup != NULL && pGroup->fPlayer )
3017 			{
3018 				pGroup->fPersistant = TRUE;
3019 			}
3020 		}
3021 
3022 		fDoChange = TRUE;
3023 	}
3024 	else if( uiSavedGameVersion < 63 )
3025 	{
3026 		for( cnt = 0; cnt <  NUMBER_OF_SQUADS; cnt++ )
3027 		{
3028 			// create mvt groups
3029 			GROUP* const pGroup = GetGroup(SquadMovementGroups[cnt]);
3030 			if ( pGroup != NULL )
3031 			{
3032 				pGroup->fPersistant = TRUE;
3033 			}
3034 		}
3035 
3036 		for( cnt = 0; cnt <  MAX_VEHICLES; cnt++ )
3037 		{
3038 			GROUP* const pGroup = GetGroup(gubVehicleMovementGroups[cnt]);
3039 			if ( pGroup != NULL )
3040 			{
3041 				pGroup->fPersistant = TRUE;
3042 			}
3043 		}
3044 
3045 		fDoChange = TRUE;
3046 	}
3047 
3048 	if ( fDoChange )
3049 	{
3050 		//Remove all empty groups
3051 		FOR_EACH_GROUP_SAFE(i)
3052 		{
3053 			GROUP& g = *i;
3054 			if (g.ubGroupSize == 0 && !g.fPersistant) RemoveGroup(g);
3055 		}
3056 	}
3057 }
3058 
3059 //Determines if any particular group WILL be moving through a given sector given it's current
3060 //position in the route and the pGroup->ubMoveType must be ONE_WAY.  If the group is currently
3061 //IN the sector, or just left the sector, it will return FALSE.
GroupWillMoveThroughSector(GROUP * pGroup,UINT8 ubSectorX,UINT8 ubSectorY)3062 BOOLEAN GroupWillMoveThroughSector( GROUP *pGroup, UINT8 ubSectorX, UINT8 ubSectorY )
3063 {
3064 	WAYPOINT *wp;
3065 	INT32 i, dx, dy;
3066 	UINT8 ubOrigX, ubOrigY;
3067 
3068 	Assert( pGroup );
3069 	AssertMsg( pGroup->ubMoveType == ONE_WAY, String( "GroupWillMoveThroughSector() -- Attempting to test group with an invalid move type.  ubGroupID: %d, ubMoveType: %d, sector: %c%d -- KM:0",
3070 			pGroup->ubGroupID, pGroup->ubMoveType, pGroup->ubSectorY + 'A' - 1, pGroup->ubSectorX ) );
3071 
3072 	//Preserve the original sector values, as we will be temporarily modifying the group's ubSectorX/Y values
3073 	//as we traverse the waypoints.
3074 	ubOrigX = pGroup->ubSectorX;
3075 	ubOrigY = pGroup->ubSectorY;
3076 
3077 	i = pGroup->ubNextWaypointID;
3078 	wp = pGroup->pWaypoints;
3079 
3080 	if( !wp )
3081 	{ //This is a floating group!?
3082 		return FALSE;
3083 	}
3084 	while( i-- )
3085 	{ //Traverse through the waypoint list to the next waypoint ID
3086 		Assert( wp );
3087 		wp = wp->next;
3088 	}
3089 	Assert( wp );
3090 
3091 
3092 	while( wp )
3093 	{
3094 		while( pGroup->ubSectorX != wp->x || pGroup->ubSectorY != wp->y )
3095 		{
3096 			//We now have the correct waypoint.
3097 			//Analyse the group and determine which direction it will move from the current sector.
3098 			dx = wp->x - pGroup->ubSectorX;
3099 			dy = wp->y - pGroup->ubSectorY;
3100 			if( dx && dy )
3101 			{ //Can't move diagonally!
3102 				SLOGA("GroupWillMoveThroughSector() -- Attempting to process waypoint in a diagonal direction from sector %c%d to sector %c%d for group at sector %c%d",
3103 					pGroup->ubSectorY + 'A', pGroup->ubSectorX, wp->y + 'A' - 1, wp->x, ubOrigY + 'A' - 1, ubOrigX);
3104 				pGroup->ubSectorX = ubOrigX;
3105 				pGroup->ubSectorY = ubOrigY;
3106 				return TRUE;
3107 			}
3108 			if( !dx && !dy ) //Can't move to position currently at!
3109 			{
3110 				SLOGA("GroupWillMoveThroughSector() -- Attempting to process same waypoint at %c%d for group at %c%d",
3111 					wp->y + 'A' - 1, wp->x, ubOrigY + 'A' - 1, ubOrigX);
3112 				pGroup->ubSectorX = ubOrigX;
3113 				pGroup->ubSectorY = ubOrigY;
3114 				return TRUE;
3115 			}
3116 			//Clip dx/dy value so that the move is for only one sector.
3117 			if( dx >= 1 )
3118 			{
3119 				dx = 1;
3120 			}
3121 			else if( dy >= 1 )
3122 			{
3123 				dy = 1;
3124 			}
3125 			else if( dx <= -1 )
3126 			{
3127 				dx = -1;
3128 			}
3129 			else if( dy <= -1 )
3130 			{
3131 				dy = -1;
3132 			}
3133 			//Advance the sector value
3134 			pGroup->ubSectorX = (UINT8)( dx + pGroup->ubSectorX );
3135 			pGroup->ubSectorY = (UINT8)( dy + pGroup->ubSectorY );
3136 			//Check to see if it the sector we are checking to see if this group will be moving through.
3137 			if( pGroup->ubSectorX == ubSectorX && pGroup->ubSectorY == ubSectorY )
3138 			{
3139 				pGroup->ubSectorX = ubOrigX;
3140 				pGroup->ubSectorY = ubOrigY;
3141 				return TRUE;
3142 			}
3143 		}
3144 		//Advance to the next waypoint.
3145 		wp = wp->next;
3146 	}
3147 	pGroup->ubSectorX = ubOrigX;
3148 	pGroup->ubSectorY = ubOrigY;
3149 	return FALSE;
3150 }
3151 
VehicleFuelRemaining(SOLDIERTYPE const & vs)3152 static INT16 VehicleFuelRemaining(SOLDIERTYPE const& vs)
3153 {
3154 	Assert(vs.uiStatusFlags & SOLDIER_VEHICLE);
3155 	return vs.sBreathRed;
3156 }
3157 
3158 
SpendVehicleFuel(SOLDIERTYPE & vs,INT16 const fuel_spent)3159 static void SpendVehicleFuel(SOLDIERTYPE& vs, INT16 const fuel_spent)
3160 {
3161 	Assert(vs.uiStatusFlags & SOLDIER_VEHICLE);
3162 	vs.sBreathRed  = MAX(0, vs.sBreathRed - fuel_spent);
3163 	vs.bBreath     = (vs.sBreathRed + 99) / 100;
3164 }
3165 
3166 
AddFuelToVehicle(SOLDIERTYPE * pSoldier,SOLDIERTYPE * pVehicle)3167 void AddFuelToVehicle(SOLDIERTYPE* pSoldier, SOLDIERTYPE* pVehicle)
3168 {
3169 	OBJECTTYPE *pItem;
3170 	INT16 sFuelNeeded, sFuelAvailable, sFuelAdded;
3171 	pItem = &pSoldier->inv[ HANDPOS ];
3172 	if( pItem->usItem != GAS_CAN )
3173 	{
3174 		return;
3175 	}
3176 	//Soldier has gas can, so now add gas to vehicle while removing gas from the gas can.
3177 	//A gas can with 100 status translate to 50% of a fillup.
3178 	if( pVehicle->sBreathRed == 10000 )
3179 	{ //Message for vehicle full?
3180 		return;
3181 	}
3182 	// pItem->bStatus is always present, no need to check for it
3183 	//Fill 'er up.
3184 	sFuelNeeded = 10000 - pVehicle->sBreathRed;
3185 	sFuelAvailable = pItem->bStatus[0] * 50;
3186 	sFuelAdded = MIN( sFuelNeeded, sFuelAvailable );
3187 	//Add to vehicle
3188 	pVehicle->sBreathRed += sFuelAdded;
3189 	pVehicle->bBreath = (INT8)(pVehicle->sBreathRed / 100);
3190 	//Subtract from item
3191 	pItem->bStatus[0] = (INT8)(pItem->bStatus[0] - sFuelAdded / 50);
3192 	if( !pItem->bStatus[0] )
3193 	{ //Gas can is empty, so toast the item.
3194 		DeleteObj( pItem );
3195 	}
3196 }
3197 
3198 
ReportVehicleOutOfGas(VEHICLETYPE const & v,UINT8 const x,UINT8 const y)3199 static void ReportVehicleOutOfGas(VEHICLETYPE const& v, UINT8 const x, UINT8 const y)
3200 {
3201 	// Report that the vehicle that just arrived is out of gas
3202 	ST::string str = st_format_printf(gzLateLocalizedString[STR_LATE_05], pVehicleStrings[v.ubVehicleType], y + 'A' - 1, x);
3203 	DoScreenIndependantMessageBox(str, MSG_BOX_FLAG_OK, 0);
3204 }
3205 
3206 
SetLocationOfAllPlayerSoldiersInGroup(GROUP const & g,INT16 const x,INT16 const y,INT8 const z)3207 static void SetLocationOfAllPlayerSoldiersInGroup(GROUP const& g, INT16 const x, INT16 const y, INT8 const z)
3208 {
3209 	CFOR_EACH_PLAYER_IN_GROUP(i, &g)
3210 	{
3211 		if (!i->pSoldier) continue;
3212 		SOLDIERTYPE& s = *i->pSoldier;
3213 		s.sSectorX = x;
3214 		s.sSectorY = y;
3215 		s.bSectorZ = z;
3216 	}
3217 
3218 	if (g.fVehicle)
3219 	{
3220 		VEHICLETYPE& v = GetVehicleFromMvtGroup(g);
3221 		v.sSectorX = x;
3222 		v.sSectorY = y;
3223 		v.sSectorZ = z;
3224 
3225 		if (!IsHelicopter(v))
3226 		{
3227 			SOLDIERTYPE& vs = GetSoldierStructureForVehicle(v);
3228 			/* These are apparently unnecessary, since vehicles are part of the
3229 				* pPlayerList in a vehicle group. Oh well. */
3230 			vs.sSectorX = x;
3231 			vs.sSectorY = y;
3232 			vs.bSectorZ = z;
3233 		}
3234 	}
3235 }
3236 
3237 
RandomizePatrolGroupLocation(GROUP * pGroup)3238 void RandomizePatrolGroupLocation( GROUP *pGroup )
3239 {	//Make sure this is an enemy patrol group
3240 	WAYPOINT *wp;
3241 	UINT8 ubMaxWaypointID = 0;
3242 	UINT8 ubTotalWaypoints;
3243 	UINT8 ubChosen;
3244 	UINT8 ubSectorID;
3245 
3246 	//return; //disabled for now
3247 
3248 	Assert( !pGroup->fPlayer );
3249 	Assert( pGroup->ubMoveType == ENDTOEND_FORWARDS );
3250 	Assert( pGroup->pEnemyGroup->ubIntention == PATROL );
3251 
3252 	//Search for the event, and kill it (if it exists)!
3253 	DeleteStrategicEvent( EVENT_GROUP_ARRIVAL, pGroup->ubGroupID );
3254 
3255 	//count the group's waypoints
3256 	wp = pGroup->pWaypoints;
3257 	while( wp )
3258 	{
3259 		if( wp->next )
3260 		{
3261 			ubMaxWaypointID++;
3262 		}
3263 		wp = wp->next;
3264 	}
3265 	//double it (they go back and forth) -- it's using zero based indices, so you have to add one to get the number of actual
3266 	//waypoints in one direction.
3267 	ubTotalWaypoints = (UINT8)((ubMaxWaypointID) * 2);
3268 
3269 	//pick the waypoint they start at
3270 	ubChosen = (UINT8)Random( ubTotalWaypoints );
3271 
3272 	if( ubChosen >= ubMaxWaypointID )
3273 	{ //They chose a waypoint going in the reverse direction, so translate it
3274 		//to an actual waypointID and switch directions.
3275 		pGroup->ubMoveType = ENDTOEND_BACKWARDS;
3276 		pGroup->ubNextWaypointID = ubChosen - ubMaxWaypointID;
3277 		ubChosen = pGroup->ubNextWaypointID + 1;
3278 	}
3279 	else
3280 	{
3281 		pGroup->ubMoveType = ENDTOEND_FORWARDS;
3282 		pGroup->ubNextWaypointID = ubChosen + 1;
3283 	}
3284 
3285 	//Traverse through the waypoint list again, to extract the location they are at.
3286 	wp = pGroup->pWaypoints;
3287 	while( wp && ubChosen )
3288 	{
3289 		ubChosen--;
3290 		wp = wp->next;
3291 	}
3292 
3293 	//logic error if this fails.  We should have a null value for ubChosen
3294 	Assert( !ubChosen );
3295 	Assert( wp );
3296 
3297 	//Move the group to the location of this chosen waypoint.
3298 	ubSectorID = (UINT8)SECTOR( wp->x, wp->y );
3299 
3300 	//Set up this global var to randomize the arrival time of the group from
3301 	//1 minute to actual traverse time between the sectors.
3302 	gfRandomizingPatrolGroup = TRUE;
3303 
3304 	SetEnemyGroupSector(*pGroup, ubSectorID);
3305 	InitiateGroupMovementToNextSector( pGroup );
3306 
3307 	//Immediately turn off the flag once finished.
3308 	gfRandomizingPatrolGroup = FALSE;
3309 
3310 }
3311 
3312 
3313 //Whenever a player group arrives in a sector, and if bloodcats exist in the sector,
3314 //roll the dice to see if this will become an ambush random encounter.
TestForBloodcatAmbush(GROUP const * const pGroup)3315 static BOOLEAN TestForBloodcatAmbush(GROUP const* const pGroup)
3316 {
3317 	SECTORINFO *pSector;
3318 	INT32 iHoursElapsed;
3319 	UINT8 ubSectorID;
3320 	UINT8 ubChance;
3321 	INT8 bDifficultyMaxCats;
3322 	INT8 bProgressMaxCats;
3323 	INT8 bNumMercMaxCats;
3324 	BOOLEAN fAlreadyAmbushed = FALSE;
3325 
3326 	if( pGroup->ubSectorZ )
3327 	{ //no ambushes underground (no bloodcats either)
3328 		return FALSE;
3329 	}
3330 
3331 	ubSectorID = (UINT8)SECTOR( pGroup->ubSectorX, pGroup->ubSectorY );
3332 	pSector = &SectorInfo[ ubSectorID ];
3333 
3334 	ubChance = 5 * gGameOptions.ubDifficultyLevel;
3335 
3336 	bool bIsLair = false, bIsArena = false;
3337 	auto spawns = GCM->getBloodCatSpawnsOfSector( ubSectorID );
3338 	if (spawns != NULL) {
3339 		bIsLair = spawns->isLair;   // SEC_I16
3340 		bIsArena = spawns->isArena; // SEC_N5
3341 	}
3342 
3343 	iHoursElapsed = (GetWorldTotalMin() - pSector->uiTimeCurrentSectorWasLastLoaded) / 60;
3344 	if( bIsLair || bIsArena )
3345 	{ //These are special maps -- we use all placements.
3346 		if( pSector->bBloodCats == -1 )
3347 		{
3348 			pSector->bBloodCats = pSector->bBloodCatPlacements;
3349 		}
3350 		else if( pSector->bBloodCats > 0 && pSector->bBloodCats < pSector->bBloodCatPlacements )
3351 		{ //Slowly have them recuperate if we haven't been here for a long time.  The population will
3352 			//come back up to the maximum if left long enough.
3353 			INT32 iBloodCatDiff;
3354 			iBloodCatDiff = pSector->bBloodCatPlacements - pSector->bBloodCats;
3355 			pSector->bBloodCats += (INT8)MIN( iHoursElapsed / 18, iBloodCatDiff );
3356 		}
3357 		//Once 0, the bloodcats will never recupe.
3358 	}
3359 	else if( pSector->bBloodCats == -1 )
3360 	{ //If we haven't been ambushed by bloodcats yet...
3361 		if( gfAutoAmbush || PreChance( ubChance ) )
3362 		{
3363 			//randomly choose from 5-8, 7-10, 9-12 bloodcats based on easy, normal, and hard, respectively
3364 			bDifficultyMaxCats = (INT8)( Random( 4 ) + gGameOptions.ubDifficultyLevel*2 + 3 );
3365 
3366 			//maximum of 3 bloodcats or 1 for every 6%, 5%, 4% progress based on easy, normal, and hard, respectively
3367 			bProgressMaxCats = (INT8)MAX( CurrentPlayerProgressPercentage() / (7 - gGameOptions.ubDifficultyLevel), 3 );
3368 
3369 			//make sure bloodcats don't outnumber mercs by a factor greater than 2
3370 			bNumMercMaxCats = (INT8)(PlayerMercsInSector( pGroup->ubSectorX, pGroup->ubSectorY, pGroup->ubSectorZ ) * 2);
3371 
3372 			//choose the lowest number of cats calculated by difficulty and progress.
3373 			pSector->bBloodCats = (INT8)MIN( bDifficultyMaxCats, bProgressMaxCats );
3374 
3375 			if( gGameOptions.ubDifficultyLevel != DIF_LEVEL_HARD )
3376 			{ //if not hard difficulty, ensure cats never outnumber mercs by a factor of 2 (min 3 bloodcats)
3377 				pSector->bBloodCats = (INT8)MIN( pSector->bBloodCats, bNumMercMaxCats );
3378 				pSector->bBloodCats = (INT8)MAX( pSector->bBloodCats, 3 );
3379 			}
3380 
3381 			//ensure that there aren't more bloodcats than placements
3382 			pSector->bBloodCats = (INT8)MIN( pSector->bBloodCats, pSector->bBloodCatPlacements );
3383 		}
3384 	}
3385 	else if( !bIsLair )
3386 	{
3387 		if( !gfAutoAmbush && PreChance( 95 ) )
3388 		{ //already ambushed here.  But 5% chance of getting ambushed again!
3389 			fAlreadyAmbushed = TRUE;
3390 		}
3391 	}
3392 
3393 	if( !fAlreadyAmbushed && !bIsArena && pSector->bBloodCats > 0 &&
3394 			!pGroup->fVehicle && !NumEnemiesInSector( pGroup->ubSectorX, pGroup->ubSectorY ) )
3395 	{
3396 		if( !bIsLair || !gubFact[ FACT_PLAYER_KNOWS_ABOUT_BLOODCAT_LAIR ] )
3397 		{
3398 			gubEnemyEncounterCode = BLOODCAT_AMBUSH_CODE;
3399 		}
3400 		else
3401 		{
3402 			gubEnemyEncounterCode = ENTERING_BLOODCAT_LAIR_CODE;
3403 		}
3404 		return TRUE;
3405 	}
3406 	else
3407 	{
3408 		gubEnemyEncounterCode = NO_ENCOUNTER_CODE;
3409 		return FALSE;
3410 	}
3411 }
3412 
3413 
NotifyPlayerOfBloodcatBattle(UINT8 ubSectorX,UINT8 ubSectorY)3414 static void NotifyPlayerOfBloodcatBattle(UINT8 ubSectorX, UINT8 ubSectorY)
3415 {
3416 	ST::string str;
3417 	ST::string zTempString;
3418 	if( gubEnemyEncounterCode == BLOODCAT_AMBUSH_CODE )
3419 	{
3420 		zTempString = GetSectorIDString(ubSectorX, ubSectorY, 0, TRUE);
3421 		str = st_format_printf(pMapErrorString[ 12 ], zTempString);
3422 	}
3423 	else if( gubEnemyEncounterCode == ENTERING_BLOODCAT_LAIR_CODE )
3424 	{
3425 		str = pMapErrorString[ 13 ];
3426 	}
3427 
3428 	if( guiCurrentScreen == MAP_SCREEN )
3429 	{	//Force render mapscreen (need to update the position of the group before the dialog appears.
3430 		fMapPanelDirty = TRUE;
3431 		MapScreenHandle();
3432 		InvalidateScreen();
3433 		RefreshScreen();
3434 	}
3435 
3436 	gfUsePersistantPBI = TRUE;
3437 	DoScreenIndependantMessageBox( str, MSG_BOX_FLAG_OK, TriggerPrebattleInterface );
3438 }
3439 
3440 
PlaceGroupInSector(GROUP & g,INT16 const prev_x,INT16 const prev_y,INT16 const next_x,INT16 const next_y,INT8 const z,bool const check_for_battle)3441 void PlaceGroupInSector(GROUP& g, INT16 const prev_x, INT16 const prev_y, INT16 const next_x, INT16 const next_y, INT8 const z, bool const check_for_battle)
3442 {
3443 	ClearMercPathsAndWaypointsForAllInGroup(g);
3444 	// Change where they are and where they're going
3445 	SetGroupPrevSectors(g, prev_x, prev_y);
3446 	SetGroupSectorValue(prev_x, prev_y, z, g);
3447 	SetGroupNextSectorValue(next_x, next_y, g);
3448 	// Call arrive event
3449 	GroupArrivedAtSector(g, check_for_battle, FALSE);
3450 }
3451 
setArrivalTime(UINT32 arrival_time)3452 void GROUP::setArrivalTime(UINT32 arrival_time)
3453 {
3454 	/* Please centralize all changes to the arrival times of groups through here,
3455 	 * especially the helicopter group! */
3456 
3457 	/* If this group is the helicopter group, we have to make sure that its
3458 	 * arrival time is never greater than the sum of the current time and its
3459 	 * traverse time, because those 3 values are used to plot its map position!
3460 	 * Because of this the chopper groups must NEVER be delayed for any reason -
3461 	 * it gets excluded from simultaneous arrival logic */
3462 
3463 	/* Also note that non-chopper groups can currently be delayed such that this
3464 	 * assetion would fail - enemy groups by DelayEnemyGroupsIfPathsCross(), and
3465 	 * player groups via PrepareGroupsForSimultaneousArrival().  So we skip the
3466 	 * assert. */
3467 
3468 	if (IsGroupTheHelicopterGroup(*this))
3469 	{
3470 		// Make sure it's valid (NOTE: the correct traverse time must be set first!)
3471 		UINT32 const now = GetWorldTotalMin();
3472 		if (arrival_time > now + this->uiTraverseTime)
3473 		{
3474 			AssertMsg(FALSE, String( "setArrivalTime: Setting invalid arrival time %d for group %d, WorldTime = %d, TraverseTime = %d", arrival_time, this->ubGroupID, now, this->uiTraverseTime));
3475 			// Fix it if assertions are disabled
3476 			arrival_time = now + this->uiTraverseTime;
3477 		}
3478 	}
3479 
3480 	this->uiArrivalTime = arrival_time;
3481 }
3482 
3483 
3484 // Non-persistent groups should be simply removed instead
CancelEmptyPersistentGroupMovement(GROUP & g)3485 static void CancelEmptyPersistentGroupMovement(GROUP& g)
3486 {
3487 	Assert(g.ubGroupSize == 0);
3488 	Assert(g.fPersistant);
3489 
3490 	/* Don't do this for vehicle groups - the chopper can keep flying empty, while
3491 	 * other vehicles still exist and teleport to nearest sector instead */
3492 	if (g.fVehicle) return;
3493 
3494 	// Prevent it from arriving empty
3495 	DeleteStrategicEvent(EVENT_GROUP_ARRIVAL, g.ubGroupID);
3496 
3497 	RemoveGroupWaypoints(g);
3498 	g.uiTraverseTime  = 0;
3499 	g.setArrivalTime(0);
3500 	g.fBetweenSectors = FALSE;
3501 	g.ubPrevX         = 0;
3502 	g.ubPrevY         = 0;
3503 	g.ubSectorX       = 0;
3504 	g.ubSectorY       = 0;
3505 	g.ubNextX         = 0;
3506 	g.ubNextY         = 0;
3507 }
3508 
3509 
3510 static bool HandlePlayerGroupEnteringSectorToCheckForNPCsOfNote(GROUP&);
3511 
3512 
3513 // look for NPCs to stop for, anyone is too tired to keep going, if all OK rebuild waypoints & continue movement
PlayerGroupArrivedSafelyInSector(GROUP & g,BOOLEAN const fCheckForNPCs)3514 void PlayerGroupArrivedSafelyInSector(GROUP& g, BOOLEAN const fCheckForNPCs)
3515 {
3516 	BOOLEAN fPlayerPrompted = FALSE;
3517 
3518 	Assert(g.fPlayer);
3519 
3520 	// if we haven't already checked for NPCs, and the group isn't empty
3521 	if (fCheckForNPCs && HandlePlayerGroupEnteringSectorToCheckForNPCsOfNote(g))
3522 	{
3523 		// wait for player to answer/confirm prompt before doing anything else
3524 		fPlayerPrompted = TRUE;
3525 	}
3526 
3527 	// if we're not prompting the player
3528 	if ( !fPlayerPrompted )
3529 	{
3530 		// and we're not at the end of our road
3531 		if (!GroupAtFinalDestination(&g))
3532 		{
3533 			if (AnyMercInGroupCantContinueMoving(g))
3534 			{
3535 				// stop: clear their strategic movement (mercpaths and waypoints)
3536 				ClearMercPathsAndWaypointsForAllInGroup(g);
3537 
3538 				// NOTE: Of course, it would be better if they continued onwards once everyone was ready to go again, in which
3539 				// case we'd want to preserve the plotted path, but since the player can mess with the squads, etc.
3540 				// in the mean-time, that just seemed to risky to try to support.  They could get into a fight and be too
3541 				// injured to move, etc.  Basically, we'd have run a complete CanCharacterMoveInStrategic(0 check on all of them.
3542 				// It's a wish list task for AM...
3543 
3544 				// stop time so player can react if group was already on the move and suddenly halts
3545 				StopTimeCompression();
3546 			}
3547 			else
3548 			{
3549 				// continue onwards: rebuild way points, initiate movement
3550 				RebuildWayPointsForGroupPath(GetGroupMercPathPtr(g), g);
3551 			}
3552 		}
3553 	}
3554 }
3555 
3556 
3557 static void HandlePlayerGroupEnteringSectorToCheckForNPCsOfNoteCallback(MessageBoxReturnValue);
3558 
3559 
HandlePlayerGroupEnteringSectorToCheckForNPCsOfNote(GROUP & g)3560 static bool HandlePlayerGroupEnteringSectorToCheckForNPCsOfNote(GROUP& g)
3561 {
3562 	Assert(g.fPlayer);
3563 
3564 	// Nobody in the group (perfectly legal with the chopper).
3565 	if (!g.pPlayerList) return false;
3566 
3567 	// Chopper doesn't stop for NPCs.
3568 	if (IsGroupTheHelicopterGroup(g)) return false;
3569 
3570 	/* If we're already in the middle of a prompt (possible with simultaneously
3571 	 * group arrivals!), don't try to prompt again. */
3572 	if (gpGroupPrompting) return false;
3573 
3574 	INT16 const x = g.ubSectorX;
3575 	INT16 const y = g.ubSectorY;
3576 	INT8  const z = g.ubSectorZ;
3577 
3578 	// Check for profiled NPCs in sector.
3579 	if (!WildernessSectorWithAllProfiledNPCsNotSpokenWith(x, y, z)) return false;
3580 
3581 	// Store the group pointer for use by the callback function.
3582 	gpGroupPrompting = &g;
3583 
3584 	// Build string for squad.
3585 	ST::string sector_name = GetSectorIDString(x, y, z, FALSE);
3586 	ST::string msg = st_format_printf(pLandMarkInSectorString, g.pPlayerList->pSoldier->bAssignment + 1, sector_name);
3587 
3588 	MessageBoxFlags const flags =
3589 		GroupAtFinalDestination(&g) ? MSG_BOX_FLAG_OK :
3590 		MSG_BOX_FLAG_CONTINUESTOP;
3591 	DoScreenIndependantMessageBox(msg, flags, HandlePlayerGroupEnteringSectorToCheckForNPCsOfNoteCallback);
3592 	// wait, we're prompting the player
3593 	return true;
3594 }
3595 
3596 
WildernessSectorWithAllProfiledNPCsNotSpokenWith(INT16 const x,INT16 const y,INT8 const z)3597 bool WildernessSectorWithAllProfiledNPCsNotSpokenWith(INT16 const x, INT16 const y, INT8 const z)
3598 {
3599 	bool found_somebody = false;
3600 
3601 	// Don't do this for underground sectors.
3602 	if (z != 0) return false;
3603 
3604 	// Skip towns/pseudo-towns (anything that shows up on the map as being special).
3605 	if (StrategicMap[CALCULATE_STRATEGIC_INDEX(x, y)].bNameId != BLANK_SECTOR) return false;
3606 
3607 	// Skip SAM sites.
3608 	if (IsThisSectorASAMSector(x, y, z)) return false;
3609 
3610 	for (const MercProfile* profile : GCM->listMercProfiles())
3611 	{
3612 		// Skip player mercs
3613 		if (profile->isPlayerMerc())       continue;
3614 
3615 		MERCPROFILESTRUCT const& p = profile->getStruct();
3616 
3617 		// Skip dead.
3618 		if (p.bMercStatus == MERC_IS_DEAD) continue;
3619 		if (p.bLife <= 0)                  continue;
3620  		// Skip vehicles.
3621 		if (profile->isVehicle()) continue;
3622 
3623 		// In this sector?
3624 		if (p.sSectorX != x || p.sSectorY != y || p.bSectorZ != z) continue;
3625 
3626 		if (p.ubLastDateSpokenTo != 0 ||
3627 				p.ubMiscFlags & (PROFILE_MISC_FLAG_RECRUITED | PROFILE_MISC_FLAG_EPCACTIVE))
3628 		{ /* Already spoke to this guy, don't prompt about this sector again,
3629 			 * regardless of status of other NPCs here. Although Hamous wanders
3630 			 * around, he never shares the same wilderness sector as other important
3631 			 * NPCs. */
3632 			return false;
3633 		}
3634 		/* We haven't talked to him yet and he's not currently recruired/escorted by
3635 		 * player. This is a guy we need to stop for. */
3636 		found_somebody = true;
3637 	}
3638 	return found_somebody;
3639 }
3640 
3641 
HandlePlayerGroupEnteringSectorToCheckForNPCsOfNoteCallback(MessageBoxReturnValue const exit_value)3642 static void HandlePlayerGroupEnteringSectorToCheckForNPCsOfNoteCallback(MessageBoxReturnValue const exit_value)
3643 {
3644 	Assert(gpGroupPrompting);
3645 	GROUP& g = *gpGroupPrompting;
3646 	gpGroupPrompting = 0;
3647 	switch (exit_value)
3648 	{
3649 		case MSG_BOX_RETURN_YES:
3650 		case MSG_BOX_RETURN_OK:
3651 			// NPCs now checked, continue moving if appropriate
3652 			PlayerGroupArrivedSafelyInSector(g, FALSE);
3653 			break;
3654 
3655 		case MSG_BOX_RETURN_NO:
3656 			// Stop here
3657 			ClearMercPathsAndWaypointsForAllInGroup(g);
3658 			ChangeSelectedMapSector(g.ubSectorX, g.ubSectorY, g.ubSectorZ);
3659 			StopTimeCompression();
3660 			break;
3661 				default:
3662 						break;
3663 	}
3664 	fMapPanelDirty        = TRUE;
3665 	fMapScreenBottomDirty = TRUE;
3666 }
3667 
3668 
DoesPlayerExistInPGroup(GROUP const & g,SOLDIERTYPE const & s)3669 bool DoesPlayerExistInPGroup(GROUP const& g, SOLDIERTYPE const& s)
3670 {
3671 	CFOR_EACH_PLAYER_IN_GROUP(curr, &g)
3672 	{
3673 		if (curr->pSoldier == &s) return true;
3674 	}
3675 	return false;
3676 }
3677 
3678 
GroupHasInTransitDeadOrPOWMercs(GROUP const & g)3679 bool GroupHasInTransitDeadOrPOWMercs(GROUP const& g)
3680 {
3681 	CFOR_EACH_PLAYER_IN_GROUP(i, &g)
3682 	{
3683 		if (!i->pSoldier) continue;
3684 		switch (i->pSoldier->bAssignment)
3685 		{
3686 			case IN_TRANSIT:
3687 			case ASSIGNMENT_POW:
3688 			case ASSIGNMENT_DEAD:
3689 				return true;
3690 		}
3691 	}
3692 	return false;
3693 }
3694 
3695