1 /**
2  * @file
3  * @brief Single player campaign control.
4  */
5 
6 /*
7 Copyright (C) 2002-2013 UFO: Alien Invasion.
8 
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License
11 as published by the Free Software Foundation; either version 2
12 of the License, or (at your option) any later version.
13 
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17 
18 See the GNU General Public License for more details.
19 
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
23 */
24 
25 #include "../../cl_shared.h"
26 #include "../../ui/ui_main.h"
27 #include "../cgame.h"
28 #include "cp_campaign.h"
29 #include "cp_capacity.h"
30 #include "cp_overlay.h"
31 #include "cp_mapfightequip.h"
32 #include "cp_hospital.h"
33 #include "cp_hospital_callbacks.h"
34 #include "cp_base_callbacks.h"
35 #include "cp_basedefence_callbacks.h"
36 #include "cp_team.h"
37 #include "cp_team_callbacks.h"
38 #include "cp_popup.h"
39 #include "cp_geoscape.h"
40 #include "cp_ufo.h"
41 #include "cp_installation_callbacks.h"
42 #include "cp_alien_interest.h"
43 #include "cp_missions.h"
44 #include "cp_mission_triggers.h"
45 #include "cp_nation.h"
46 #include "cp_statistics.h"
47 #include "cp_time.h"
48 #include "cp_xvi.h"
49 #include "cp_fightequip_callbacks.h"
50 #include "cp_produce_callbacks.h"
51 #include "cp_transfer.h"
52 #include "cp_market_callbacks.h"
53 #include "cp_research_callbacks.h"
54 #include "cp_uforecovery.h"
55 #include "save/save_campaign.h"
56 #include "cp_auto_mission.h"
57 
58 memPool_t* cp_campaignPool;		/**< reset on every game restart */
59 ccs_t ccs;
60 cvar_t* cp_campaign;
61 cvar_t* cp_start_employees;
62 cvar_t* cp_missiontest;
63 
64 typedef struct {
65 	int ucn;
66 	int HP;
67 	int STUN;
68 	int morale;
69 	woundInfo_t wounds;
70 
71 	chrScoreGlobal_t chrscore;
72 } updateCharacter_t;
73 
74 /**
75  * @brief Determines the maximum amount of XP per skill that can be gained from any one mission.
76  * @param[in] skill The skill for which to fetch the maximum amount of XP.
77  * @sa G_UpdateCharacterExperience
78  * @sa G_GetEarnedExperience
79  * @note Explanation of the values here:
80  * There is a maximum speed at which skills may rise over the course of the predicted career length of a veteran soldier.
81  * Because the increase is given as experience^0.6, that means that the maximum XP cap x per mission is given as
82  * log predictedStatGrowth / log x = 0.6
83  * log x = log predictedStatGrowth / 0.6
84  * x = 10 ^ (log predictedStatGrowth / 0.6)
85  */
CP_CharacterGetMaxExperiencePerMission(const abilityskills_t skill)86 int CP_CharacterGetMaxExperiencePerMission (const abilityskills_t skill)
87 {
88 	switch (skill) {
89 	case ABILITY_POWER:
90 		return 125;
91 	case ABILITY_SPEED:
92 		return 91;
93 	case ABILITY_ACCURACY:
94 		return 450;
95 	case ABILITY_MIND:
96 		return 450;
97 	case SKILL_CLOSE:
98 		return 680;
99 	case SKILL_HEAVY:
100 		return 680;
101 	case SKILL_ASSAULT:
102 		return 680;
103 	case SKILL_SNIPER:
104 		return 680;
105 	case SKILL_EXPLOSIVE:
106 		return 680;
107 	case SKILL_NUM_TYPES: /* This is health. */
108 		return 360;
109 	case SKILL_PILOTING:
110 	case SKILL_TARGETING:
111 	case SKILL_EVADING:
112 		return 0;
113 	default:
114 		cgi->Com_Error(ERR_DROP, "G_GetMaxExperiencePerMission: invalid skill type");
115 		return -1;
116 	}
117 }
118 
119 /**
120  * @brief Updates the character skills after a mission.
121  * @param[in,out] chr Pointer to the character that should get the skills updated.
122  */
CP_UpdateCharacterSkills(character_t * chr)123 void CP_UpdateCharacterSkills (character_t* chr)
124 {
125 	for (int i = 0; i < SKILL_NUM_TYPES; ++i)
126 		chr->score.skills[i] = std::min(MAX_SKILL, chr->score.initialSkills[i] +
127 			static_cast<int>(pow(static_cast<float>(chr->score.experience[i]) / 10, 0.6f)));
128 
129 	chr->maxHP = std::min(MAX_MAXHP, chr->score.initialSkills[SKILL_NUM_TYPES] +
130 		static_cast<int>(pow(static_cast<float>(chr->score.experience[SKILL_NUM_TYPES]) / 10, 0.6f)));
131 }
132 
133 /**
134  * @brief Transforms the battlescape values to the character
135  * @sa CP_ParseCharacterData
136  */
CP_UpdateCharacterData(linkedList_t * updateCharacters)137 void CP_UpdateCharacterData (linkedList_t* updateCharacters)
138 {
139 	LIST_Foreach(updateCharacters, updateCharacter_t, c) {
140 		Employee* employee = E_GetEmployeeFromChrUCN(c->ucn);
141 
142 		if (!employee) {
143 			Com_Printf("Warning: Could not get character with ucn: %i.\n", c->ucn);
144 			continue;
145 		}
146 
147 		character_t* chr = &employee->chr;
148 		const bool fullHP = c->HP >= chr->maxHP;
149 		chr->STUN = c->STUN;
150 		chr->morale = c->morale;
151 
152 		memcpy(chr->wounds.treatmentLevel, c->wounds.treatmentLevel, sizeof(chr->wounds.treatmentLevel));
153 		memcpy(chr->score.kills, c->chrscore.kills, sizeof(chr->score.kills));
154 		memcpy(chr->score.stuns, c->chrscore.stuns, sizeof(chr->score.stuns));
155 		chr->score.assignedMissions = c->chrscore.assignedMissions;
156 
157 		for (int i = ABILITY_POWER; i <= SKILL_NUM_TYPES; ++i) {
158 			const int maxXP = CP_CharacterGetMaxExperiencePerMission(static_cast<abilityskills_t>(i));
159 			const int gainedXP = std::min(maxXP, c->chrscore.experience[i] - chr->score.experience[i]);
160 			chr->score.experience[i] += gainedXP;
161 			cgi->Com_DPrintf(DEBUG_CLIENT, "CP_UpdateCharacterData: Soldier %s earned %d experience points in skill #%d (total experience: %d)\n",
162 						chr->name, gainedXP, i, chr->score.experience[SKILL_NUM_TYPES]);
163 		}
164 
165 		CP_UpdateCharacterSkills(chr);
166 		/* If character returned unscratched and maxHP just went up due to experience
167 		 * don't send him/her to the hospital */
168 		chr->HP = (fullHP ? chr->maxHP : std::min(c->HP, chr->maxHP));
169 	}
170 }
171 
172 /**
173  * @brief Parses the character data which was send by G_MatchSendResults using G_SendCharacterData
174  * @param[in] msg The network buffer message. If this is nullptr the character is updated, if this
175  * is not nullptr the data is stored in a temp buffer because the player can choose to retry
176  * the mission and we have to catch this situation to not update the character data in this case.
177  * @param updateCharacters A LinkedList where to store the character data. One listitem per character.
178  * @sa G_SendCharacterData
179  * @sa GAME_SendCurrentTeamSpawningInfo
180  * @sa E_Save
181  */
CP_ParseCharacterData(dbuffer * msg,linkedList_t ** updateCharacters)182 void CP_ParseCharacterData (dbuffer* msg, linkedList_t** updateCharacters)
183 {
184 	int i, j;
185 	const int num = cgi->NET_ReadByte(msg);
186 
187 	if (num < 0)
188 		cgi->Com_Error(ERR_DROP, "CP_ParseCharacterData: invalid character number found in stream (%i)\n", num);
189 
190 	for (i = 0; i < num; i++) {
191 		updateCharacter_t c;
192 		OBJZERO(c);
193 		c.ucn = NET_ReadShort(msg);
194 		c.HP = NET_ReadShort(msg);
195 		c.STUN = cgi->NET_ReadByte(msg);
196 		c.morale = cgi->NET_ReadByte(msg);
197 
198 		for (j = 0; j < BODYPART_MAXTYPE; ++j)
199 			c.wounds.treatmentLevel[j] = cgi->NET_ReadByte(msg);
200 
201 		for (j = 0; j < SKILL_NUM_TYPES + 1; j++)
202 			c.chrscore.experience[j] = NET_ReadLong(msg);
203 		for (j = 0; j < KILLED_NUM_TYPES; j++)
204 			c.chrscore.kills[j] = NET_ReadShort(msg);
205 		for (j = 0; j < KILLED_NUM_TYPES; j++)
206 			c.chrscore.stuns[j] = NET_ReadShort(msg);
207 		c.chrscore.assignedMissions = NET_ReadShort(msg);
208 		LIST_Add(updateCharacters, c);
209 	}
210 }
211 
212 /**
213  * @brief Checks whether a campaign mode game is running
214  */
CP_IsRunning(void)215 bool CP_IsRunning (void)
216 {
217 	return ccs.curCampaign != nullptr;
218 }
219 
220 /**
221  * @brief Check if a map may be selected for mission.
222  * @param[in] mission Pointer to the mission where mapDef should be added
223  * @param[in] pos position of the mission (nullptr if the position will be chosen afterwards)
224  * @param[in] md The map description data (what it is suitable for)
225  * @return false if map is not selectable
226  */
CP_MapIsSelectable(const mission_t * mission,const mapDef_t * md,const vec2_t pos)227 static bool CP_MapIsSelectable (const mission_t* mission, const mapDef_t* md, const vec2_t pos)
228 {
229 	if (md->storyRelated)
230 		return false;
231 
232 	if (!mission->ufo) {
233 		/* a mission without UFO should not use a map with UFO */
234 		if (!cgi->LIST_IsEmpty(md->ufos))
235 			return false;
236 	} else if (!cgi->LIST_IsEmpty(md->ufos)) {
237 		/* A mission with UFO should use a map with UFO
238 		 * first check that list is not empty */
239 		const ufoType_t type = mission->ufo->ufotype;
240 		const char* ufoID;
241 
242 		if (mission->crashed)
243 			ufoID = cgi->Com_UFOCrashedTypeToShortName(type);
244 		else
245 			ufoID = cgi->Com_UFOTypeToShortName(type);
246 
247 		if (!cgi->LIST_ContainsString(md->ufos, ufoID))
248 			return false;
249 	}
250 
251 	if (pos && !GEO_PositionFitsTCPNTypes(pos, md->terrains, md->cultures, md->populations, nullptr))
252 		return false;
253 
254 	return true;
255 }
256 
257 /**
258  * @brief Choose a map for given mission.
259  * @param[in,out] mission Pointer to the mission where a new map should be added
260  * @param[in] pos position of the mission (nullptr if the position will be chosen afterwards)
261  * @return false if could not set mission
262  */
CP_ChooseMap(mission_t * mission,const vec2_t pos)263 bool CP_ChooseMap (mission_t* mission, const vec2_t pos)
264 {
265 	if (mission->mapDef)
266 		return true;
267 
268 	int countMinimal = 0;	/**< Number of maps fulfilling mission conditions and appeared less often during game. */
269 	int minMapDefAppearance = -1;
270 	mapDef_t* md = nullptr;
271 	MapDef_ForeachSingleplayerCampaign(md) {
272 		/* Check if mission fulfill conditions */
273 		if (!CP_MapIsSelectable(mission, md, pos))
274 			continue;
275 
276 		if (minMapDefAppearance < 0 || md->timesAlreadyUsed < minMapDefAppearance) {
277 			minMapDefAppearance = md->timesAlreadyUsed;
278 			countMinimal = 1;
279 			continue;
280 		}
281 		if (md->timesAlreadyUsed > minMapDefAppearance)
282 			continue;
283 		countMinimal++;
284 	}
285 
286 	if (countMinimal == 0) {
287 		/* no map fulfill the conditions */
288 		if (mission->category == INTERESTCATEGORY_RESCUE) {
289 			/* default map for rescue mission is the rescue random map assembly */
290 			mission->mapDef = cgi->Com_GetMapDefinitionByID("rescue");
291 			if (!mission->mapDef)
292 				cgi->Com_Error(ERR_DROP, "Could not find mapdef: rescue");
293 			mission->mapDef->timesAlreadyUsed++;
294 			return true;
295 		}
296 		if (mission->crashed) {
297 			/* default map for crashsite mission is the crashsite random map assembly */
298 			mission->mapDef = cgi->Com_GetMapDefinitionByID("ufocrash");
299 			if (!mission->mapDef)
300 				cgi->Com_Error(ERR_DROP, "Could not find mapdef: ufocrash");
301 			mission->mapDef->timesAlreadyUsed++;
302 			return true;
303 		}
304 
305 		Com_Printf("CP_ChooseMap: Could not find map with required conditions:\n");
306 		Com_Printf("  ufo: %s -- pos: ", mission->ufo ? cgi->Com_UFOTypeToShortName(mission->ufo->ufotype) : "none");
307 		if (pos)
308 			Com_Printf("%s", MapIsWater(GEO_GetColor(pos, MAPTYPE_TERRAIN, nullptr)) ? " (in water) " : "");
309 		if (pos)
310 			Com_Printf("(%.02f, %.02f)\n", pos[0], pos[1]);
311 		else
312 			Com_Printf("none\n");
313 		return false;
314 	}
315 
316 	/* select a map randomly from the selected */
317 	int randomNum = rand() % countMinimal;
318 	md = nullptr;
319 	MapDef_ForeachSingleplayerCampaign(md) {
320 		/* Check if mission fulfill conditions */
321 		if (!CP_MapIsSelectable(mission, md, pos))
322 			continue;
323 		if (md->timesAlreadyUsed > minMapDefAppearance)
324 			continue;
325 		/* There shouldn't be mission fulfilling conditions used less time than minMissionAppearance */
326 		assert(md->timesAlreadyUsed == minMapDefAppearance);
327 
328 		if (randomNum == 0) {
329 			mission->mapDef = md;
330 			break;
331 		} else {
332 			randomNum--;
333 		}
334 	}
335 
336 	/* A mission must have been selected */
337 	mission->mapDef->timesAlreadyUsed++;
338 	if (cp_missiontest->integer)
339 		Com_Printf("Selected map '%s' (among %i possible maps)\n", mission->mapDef->id, countMinimal);
340 	else
341 		Com_DPrintf(DEBUG_CLIENT, "Selected map '%s' (among %i possible maps)\n", mission->mapDef->id, countMinimal);
342 
343 	return true;
344 }
345 
346 /**
347  * @brief Function to handle the campaign end
348  * @param[in] won If the player won the game
349  */
CP_EndCampaign(bool won)350 void CP_EndCampaign (bool won)
351 {
352 	cgi->Cmd_ExecuteString("game_save slotend \"End of game\"");
353 	cgi->Cmd_ExecuteString("game_exit");
354 
355 	if (won)
356 		cgi->UI_InitStack("endgame", nullptr);
357 	else
358 		cgi->UI_InitStack("lostgame", nullptr);
359 
360 	cgi->Com_Drop();
361 }
362 
363 /**
364  * @brief Checks whether the player has lost the campaign
365  */
CP_CheckLostCondition(const campaign_t * campaign)366 void CP_CheckLostCondition (const campaign_t* campaign)
367 {
368 	bool endCampaign = false;
369 
370 	if (cp_missiontest->integer)
371 		return;
372 
373 	if (!endCampaign && ccs.credits < -campaign->negativeCreditsUntilLost) {
374 		cgi->UI_RegisterText(TEXT_STANDARD, _("You've gone too far into debt."));
375 		endCampaign = true;
376 	}
377 
378 	/** @todo Should we make the campaign lost when a player loses all his bases?
379 	 * until he has set up a base again, the aliens might have invaded the whole
380 	 * world ;) - i mean, removing the credits check here. */
381 	if (ccs.credits < campaign->basecost - campaign->negativeCreditsUntilLost && !B_AtLeastOneExists()) {
382 		cgi->UI_RegisterText(TEXT_STANDARD, _("You've lost your bases and don't have enough money to build new ones."));
383 		endCampaign = true;
384 	}
385 
386 	if (!endCampaign) {
387 		if (CP_GetAverageXVIRate() > campaign->maxAllowedXVIRateUntilLost) {
388 			cgi->UI_RegisterText(TEXT_STANDARD, _("You have failed in your charter to protect Earth."
389 				" Our home and our people have fallen to the alien infection. Only a handful"
390 				" of people on Earth remain human, and the remaining few no longer have a"
391 				" chance to stem the tide. Your command is no more; PHALANX is no longer"
392 				" able to operate as a functioning unit. Nothing stands between the aliens"
393 				" and total victory."));
394 			endCampaign = true;
395 		} else {
396 			/* check for nation happiness */
397 			int j, nationBelowLimit = 0;
398 			for (j = 0; j < ccs.numNations; j++) {
399 				const nation_t* nation = NAT_GetNationByIDX(j);
400 				const nationInfo_t* stats = NAT_GetCurrentMonthInfo(nation);
401 				if (stats->happiness < campaign->minhappiness) {
402 					nationBelowLimit++;
403 				}
404 			}
405 			if (nationBelowLimit >= NATIONBELOWLIMITPERCENTAGE * ccs.numNations) {
406 				/* lost the game */
407 				cgi->UI_RegisterText(TEXT_STANDARD, _("Under your command, PHALANX operations have"
408 					" consistently failed to protect nations."
409 					" The UN, highly unsatisfied with your performance, has decided to remove"
410 					" you from command and subsequently disbands the PHALANX project as an"
411 					" effective task force. No further attempts at global cooperation are made."
412 					" Earth's nations each try to stand alone against the aliens, and eventually"
413 					" fall one by one."));
414 				endCampaign = true;
415 			}
416 		}
417 	}
418 
419 	if (endCampaign)
420 		CP_EndCampaign(false);
421 }
422 
423 /* Initial fraction of the population in the country where a mission has been lost / won */
424 #define XVI_LOST_START_PERCENTAGE	0.20f
425 #define XVI_WON_START_PERCENTAGE	0.05f
426 
427 /**
428  * @brief Updates each nation's happiness.
429  * Should be called at the completion or expiration of every mission.
430  * The nation where the mission took place will be most affected,
431  * surrounding nations will be less affected.
432  * @todo Scoring should eventually be expanded to include such elements as
433  * infected humans and mission objectives other than xenocide.
434  */
CP_HandleNationData(float minHappiness,mission_t * mis,const nation_t * affectedNation,const missionResults_t * results)435 void CP_HandleNationData (float minHappiness, mission_t* mis, const nation_t* affectedNation, const missionResults_t* results)
436 {
437 	int i;
438 	const float civilianSum = (float) (results->civiliansSurvived + results->civiliansKilled + results->civiliansKilledFriendlyFire);
439 	const float alienSum = (float) (results->aliensSurvived + results->aliensKilled + results->aliensStunned);
440 	float performance;
441 	float deltaHappiness = 0.0f;
442 	float happinessDivisor = 5.0f;
443 
444 	/** @todo HACK: This should be handled properly, i.e. civilians should only factor into the scoring
445 	 * if the mission objective is actually to save civilians. */
446 	if (civilianSum == 0) {
447 		Com_DPrintf(DEBUG_CLIENT, "CP_HandleNationData: Warning, civilianSum == 0, score for this mission will default to 0.\n");
448 		performance = 0.0f;
449 	} else {
450 		/* Calculate how well the mission went. */
451 		float performanceCivilian = (2 * civilianSum - results->civiliansKilled - 2
452 				* results->civiliansKilledFriendlyFire) * 3 / (2 * civilianSum) - 2;
453 		/** @todo The score for aliens is always negative or zero currently, but this
454 		 * should be dependent on the mission objective.
455 		 * In a mission that has a special objective, the amount of killed aliens should
456 		 * only serve to increase the score, not reduce the penalty. */
457 		float performanceAlien = results->aliensKilled + results->aliensStunned - alienSum;
458 		performance = performanceCivilian + performanceAlien;
459 	}
460 
461 	/* Calculate the actual happiness delta. The bigger the mission, the more potential influence. */
462 	deltaHappiness = 0.004 * civilianSum + 0.004 * alienSum;
463 
464 	/* There is a maximum base happiness delta. */
465 	if (deltaHappiness > HAPPINESS_MAX_MISSION_IMPACT)
466 		deltaHappiness = HAPPINESS_MAX_MISSION_IMPACT;
467 
468 	for (i = 0; i < ccs.numNations; i++) {
469 		nation_t* nation = NAT_GetNationByIDX(i);
470 		const nationInfo_t* stats = NAT_GetCurrentMonthInfo(nation);
471 		float happinessFactor;
472 
473 		/* update happiness. */
474 		if (nation == affectedNation)
475 			happinessFactor = deltaHappiness;
476 		else
477 			happinessFactor = deltaHappiness / happinessDivisor;
478 
479 		NAT_SetHappiness(minHappiness, nation, stats->happiness + performance * happinessFactor);
480 	}
481 }
482 
483 /**
484  * @brief Check for missions that have a timeout defined
485  */
CP_CheckMissionEnd(const campaign_t * campaign)486 static void CP_CheckMissionEnd (const campaign_t* campaign)
487 {
488 	MIS_Foreach(mission) {
489 		if (CP_CheckMissionLimitedInTime(mission) && Date_LaterThan(&ccs.date, &mission->finalDate))
490 			CP_MissionStageEnd(campaign, mission);
491 	}
492 }
493 
494 /* =========================================================== */
495 
496 /**
497  * @brief Functions that should be called with a minimum time lapse (will be called at least every DETECTION_INTERVAL)
498  * @param[in] campaign The campaign data structure
499  * @param[in] dt Elapsed game seconds since last call.
500  * @param[in] updateRadarOverlay true if radar overlay should be updated (only for drawing purpose)
501  * @sa CP_CampaignRun
502  */
CP_CampaignFunctionPeriodicCall(campaign_t * campaign,int dt,bool updateRadarOverlay)503 static void CP_CampaignFunctionPeriodicCall (campaign_t* campaign, int dt, bool updateRadarOverlay)
504 {
505 	UFO_CampaignRunUFOs(campaign, dt);
506 	AIR_CampaignRun(campaign, dt, updateRadarOverlay);
507 
508 	AIRFIGHT_CampaignRunBaseDefence(dt);
509 	AIRFIGHT_CampaignRunProjectiles(campaign, dt);
510 	CP_CheckNewMissionDetectedOnGeoscape();
511 
512 	/* Update alien interest for bases */
513 	UFO_UpdateAlienInterestForAllBasesAndInstallations();
514 
515 	/* Update how phalanx troop know alien bases */
516 	AB_UpdateStealthForAllBase();
517 
518 	UFO_CampaignCheckEvents();
519 }
520 
521 /**
522  * @brief Returns if we are currently on the Geoscape
523  * @todo This relies on scripted content. Should work other way!
524  */
CP_OnGeoscape(void)525 bool CP_OnGeoscape (void)
526 {
527 	return Q_streq("geoscape", cgi->UI_GetActiveWindowName());
528 }
529 
530 /**
531  * @brief delay between actions that must be executed independently of time scale
532  * @sa RADAR_CheckUFOSensored
533  * @sa UFO_UpdateAlienInterestForAllBasesAndInstallations
534  * @sa AB_UpdateStealthForAllBase
535  */
536 const int DETECTION_INTERVAL = (SECONDS_PER_HOUR / 2);
537 
538 /**
539  * @brief Ensure that the day always matches the seconds. If the seconds
540  * per day limit is reached, the seconds are reset and the day is increased.
541  * @param seconds The seconds to add to the campaign date
542  */
CP_AdvanceTimeBySeconds(int seconds)543 static inline void CP_AdvanceTimeBySeconds (int seconds)
544 {
545 	ccs.date.sec += seconds;
546 	while (ccs.date.sec >= SECONDS_PER_DAY) {
547 		ccs.date.sec -= SECONDS_PER_DAY;
548 		ccs.date.day++;
549 	}
550 }
551 
552 /**
553  * @return @c true if a month has passed
554  */
CP_IsBudgetDue(const dateLong_t * oldDate,const dateLong_t * date)555 static inline bool CP_IsBudgetDue (const dateLong_t* oldDate, const dateLong_t* date)
556 {
557 	if (oldDate->year < date->year) {
558 		return true;
559 	}
560 	return oldDate->month < date->month;
561 }
562 
563 /**
564  * @brief Called every frame when we are in geoscape view
565  * @note Called for node types cgi->UI_MAP and cgi->UI_3DMAP
566  * @sa NAT_HandleBudget
567  * @sa B_UpdateBaseData
568  * @sa AIR_CampaignRun
569  */
CP_CampaignRun(campaign_t * campaign,float secondsSinceLastFrame)570 void CP_CampaignRun (campaign_t* campaign, float secondsSinceLastFrame)
571 {
572 	/* advance time */
573 	ccs.frametime = secondsSinceLastFrame;
574 	ccs.timer += secondsSinceLastFrame * ccs.gameTimeScale;
575 
576 	UP_GetUnreadMails();
577 
578 	if (ccs.timer >= 1.0) {
579 		/* calculate new date */
580 		int currenthour;
581 		int currentmin;
582 		int currentsecond = ccs.date.sec;
583 		int currentday = ccs.date.day;
584 		int i;
585 		const int currentinterval = currentsecond % DETECTION_INTERVAL;
586 		int dt = DETECTION_INTERVAL - currentinterval;
587 		dateLong_t date, oldDate;
588 		const int timer = (int)floor(ccs.timer);
589 		const int checks = (currentinterval + timer) / DETECTION_INTERVAL;
590 
591 		CP_DateConvertLong(&ccs.date, &oldDate);
592 
593 		currenthour = currentsecond / SECONDS_PER_HOUR;
594 		currentmin = currentsecond / SECONDS_PER_MINUTE;
595 
596 		/* Execute every actions that needs to be independent of time speed : every DETECTION_INTERVAL
597 		 *	- Run UFOs and craft at least every DETECTION_INTERVAL. If detection occurred, break.
598 		 *	- Check if any new mission is detected
599 		 *	- Update stealth value of phalanx bases and installations ; alien bases */
600 		for (i = 0; i < checks; i++) {
601 			ccs.timer -= dt;
602 			currentsecond += dt;
603 			CP_AdvanceTimeBySeconds(dt);
604 			CP_CampaignFunctionPeriodicCall(campaign, dt, false);
605 
606 			/* if something stopped time, we must stop here the loop */
607 			if (CP_IsTimeStopped()) {
608 				ccs.timer = 0.0f;
609 				break;
610 			}
611 			dt = DETECTION_INTERVAL;
612 		}
613 
614 		dt = timer;
615 
616 		CP_AdvanceTimeBySeconds(dt);
617 		currentsecond += dt;
618 		ccs.timer -= dt;
619 
620 		/* compute minutely events  */
621 		/* (this may run multiple times if the time stepping is > 1 minute at a time) */
622 		const int newmin = currentsecond / SECONDS_PER_MINUTE;
623 		while (currentmin < newmin) {
624 			currentmin++;
625 			PR_ProductionRun();
626 			B_UpdateBaseData();
627 		}
628 
629 		/* compute hourly events  */
630 		/* (this may run multiple times if the time stepping is > 1 hour at a time) */
631 		const int newhour = currentsecond / SECONDS_PER_HOUR;
632 		while (currenthour < newhour) {
633 			currenthour++;
634 			RS_ResearchRun();
635 			UR_ProcessActive();
636 			AII_UpdateInstallationDelay();
637 			AII_RepairAircraft();
638 			TR_TransferRun();
639 			INT_IncreaseAlienInterest(campaign);
640 		}
641 
642 		/* daily events */
643 		for (i = currentday; i < ccs.date.day; i++) {
644 			/* every day */
645 			INS_UpdateInstallationData();
646 			HOS_HospitalRun();
647 			ccs.missionSpawnCallback();
648 			CP_SpreadXVI();
649 			NAT_UpdateHappinessForAllNations(campaign->minhappiness);
650 			AB_BaseSearchedByNations();
651 			CP_CampaignRunMarket(campaign);
652 			CP_CheckCampaignEvents(campaign);
653 			CP_ReduceXVIEverywhere();
654 			/* should be executed after all daily event that could
655 			 * change XVI overlay */
656 			CP_UpdateNationXVIInfection();
657 			CP_TriggerEvent(NEW_DAY);
658 		}
659 
660 		if (dt > 0) {
661 			/* check for campaign events
662 			 * aircraft and UFO already moved during radar detection (see above),
663 			 * just make them move the missing part -- if any */
664 			CP_CampaignFunctionPeriodicCall(campaign, dt, true);
665 		}
666 
667 		CP_CheckMissionEnd(campaign);
668 		CP_CheckLostCondition(campaign);
669 		/* Check if there is a base attack mission */
670 		CP_CheckBaseAttacks();
671 		/* check if any stores are full */
672 		CAP_CheckOverflow();
673 		BDEF_AutoSelectTarget();
674 
675 		CP_DateConvertLong(&ccs.date, &date);
676 		/* every new month we have to handle the budget */
677 		if (CP_IsBudgetDue(&oldDate, &date) && ccs.paid && B_AtLeastOneExists()) {
678 			NAT_BackupMonthlyData();
679 			NAT_HandleBudget(campaign);
680 			ccs.paid = false;
681 		} else if (date.day > 1)
682 			ccs.paid = true;
683 
684 		CP_UpdateXVIMapButton();
685 		/* set time cvars */
686 		CP_UpdateTime();
687 	}
688 }
689 
690 /**
691  * @brief Checks whether you have enough credits for something
692  * @param[in] costs costs to check
693  */
CP_CheckCredits(int costs)694 bool CP_CheckCredits (int costs)
695 {
696 	if (costs > ccs.credits)
697 		return false;
698 	return true;
699 }
700 
701 /**
702  * @brief Sets credits and update mn_credits cvar
703  * @param[in] credits The new credits value
704  * Checks whether credits are bigger than MAX_CREDITS
705  */
CP_UpdateCredits(int credits)706 void CP_UpdateCredits (int credits)
707 {
708 	/* credits */
709 	if (credits > MAX_CREDITS)
710 		credits = MAX_CREDITS;
711 	ccs.credits = credits;
712 	cgi->Cvar_Set("mn_credits", _("%i c"), ccs.credits);
713 }
714 
715 /**
716  * @brief Load mapDef statistics
717  * @param[in] parent XML Node structure, where we get the information from
718  */
CP_LoadMapDefStatXML(xmlNode_t * parent)719 static bool CP_LoadMapDefStatXML (xmlNode_t* parent)
720 {
721 	xmlNode_t* node;
722 
723 	for (node = cgi->XML_GetNode(parent, SAVE_CAMPAIGN_MAPDEF); node; node = cgi->XML_GetNextNode(node, parent, SAVE_CAMPAIGN_MAPDEF)) {
724 		const char* s = cgi->XML_GetString(node, SAVE_CAMPAIGN_MAPDEF_ID);
725 		mapDef_t* map;
726 
727 		if (s[0] == '\0') {
728 			Com_Printf("Warning: MapDef with no id in xml!\n");
729 			continue;
730 		}
731 		map = cgi->Com_GetMapDefinitionByID(s);
732 		if (!map) {
733 			Com_Printf("Warning: No MapDef with id '%s'!\n", s);
734 			continue;
735 		}
736 		map->timesAlreadyUsed = cgi->XML_GetInt(node, SAVE_CAMPAIGN_MAPDEF_COUNT, 0);
737 	}
738 
739 	return true;
740 }
741 
742 /**
743  * @brief Load callback for savegames in XML Format
744  * @param[in] parent XML Node structure, where we get the information from
745  */
CP_LoadXML(xmlNode_t * parent)746 bool CP_LoadXML (xmlNode_t* parent)
747 {
748 	xmlNode_t* campaignNode;
749 	xmlNode_t* mapNode;
750 	const char* name;
751 	campaign_t* campaign;
752 	xmlNode_t* mapDefStat;
753 
754 	campaignNode = cgi->XML_GetNode(parent, SAVE_CAMPAIGN_CAMPAIGN);
755 	if (!campaignNode) {
756 		Com_Printf("Did not find campaign entry in xml!\n");
757 		return false;
758 	}
759 	if (!(name = cgi->XML_GetString(campaignNode, SAVE_CAMPAIGN_ID))) {
760 		Com_Printf("couldn't locate campaign name in savegame\n");
761 		return false;
762 	}
763 
764 	campaign = CP_GetCampaign(name);
765 	if (!campaign) {
766 		Com_Printf("......campaign \"%s\" doesn't exist.\n", name);
767 		return false;
768 	}
769 
770 	CP_CampaignInit(campaign, true);
771 	/* init the map images and reset the map actions */
772 	GEO_Reset(campaign->map);
773 
774 	/* read credits */
775 	CP_UpdateCredits(cgi->XML_GetLong(campaignNode, SAVE_CAMPAIGN_CREDITS, 0));
776 	ccs.paid = cgi->XML_GetBool(campaignNode, SAVE_CAMPAIGN_PAID, false);
777 
778 	cgi->SetNextUniqueCharacterNumber(cgi->XML_GetInt(campaignNode, SAVE_CAMPAIGN_NEXTUNIQUECHARACTERNUMBER, 0));
779 
780 	cgi->XML_GetDate(campaignNode, SAVE_CAMPAIGN_DATE, &ccs.date.day, &ccs.date.sec);
781 
782 	/* read other campaign data */
783 	ccs.civiliansKilled = cgi->XML_GetInt(campaignNode, SAVE_CAMPAIGN_CIVILIANSKILLED, 0);
784 	ccs.aliensKilled = cgi->XML_GetInt(campaignNode, SAVE_CAMPAIGN_ALIENSKILLED, 0);
785 
786 	Com_DPrintf(DEBUG_CLIENT, "CP_LoadXML: Getting position\n");
787 
788 	/* read map view */
789 	mapNode = cgi->XML_GetNode(campaignNode, SAVE_CAMPAIGN_MAP);
790 	/* restore the overlay.
791 	* do not use cgi->Cvar_SetValue, because this function check if value->string are equal to skip calculation
792 	* and we never set r_geoscape_overlay->string in game: cl_geoscape_overlay won't be updated if the loaded
793 	* value is 0 (and that's a problem if you're loading a game when cl_geoscape_overlay is set to another value */
794 	cgi->Cvar_SetValue("cl_geoscape_overlay", cgi->XML_GetInt(mapNode, SAVE_CAMPAIGN_CL_GEOSCAPE_OVERLAY, 0));
795 	radarOverlayWasSet = cgi->XML_GetBool(mapNode, SAVE_CAMPAIGN_RADAROVERLAYWASSET, false);
796 	ccs.startXVI = cgi->XML_GetBool(mapNode, SAVE_CAMPAIGN_XVISTARTED, false);
797 	CP_UpdateXVIMapButton();
798 
799 	mapDefStat = cgi->XML_GetNode(campaignNode, SAVE_CAMPAIGN_MAPDEFSTAT);
800 	if (mapDefStat && !CP_LoadMapDefStatXML(mapDefStat))
801 		return false;
802 
803 	mxmlDelete(campaignNode);
804 	return true;
805 }
806 
807 /**
808  * @brief Save mapDef statistics
809  * @param[out] parent XML Node structure, where we write the information to
810  */
CP_SaveMapDefStatXML(xmlNode_t * parent)811 static bool CP_SaveMapDefStatXML (xmlNode_t* parent)
812 {
813 	const mapDef_t* md;
814 
815 	MapDef_ForeachSingleplayerCampaign(md) {
816 		if (md->timesAlreadyUsed > 0) {
817 			xmlNode_t* node = cgi->XML_AddNode(parent, SAVE_CAMPAIGN_MAPDEF);
818 			cgi->XML_AddString(node, SAVE_CAMPAIGN_MAPDEF_ID, md->id);
819 			cgi->XML_AddInt(node, SAVE_CAMPAIGN_MAPDEF_COUNT, md->timesAlreadyUsed);
820 		}
821 	}
822 
823 	return true;
824 }
825 
826 /**
827  * @brief Save callback for savegames in XML Format
828  * @param[out] parent XML Node structure, where we write the information to
829  */
CP_SaveXML(xmlNode_t * parent)830 bool CP_SaveXML (xmlNode_t* parent)
831 {
832 	xmlNode_t* campaign;
833 	xmlNode_t* map;
834 	xmlNode_t* mapDefStat;
835 
836 	campaign = cgi->XML_AddNode(parent, SAVE_CAMPAIGN_CAMPAIGN);
837 
838 	cgi->XML_AddString(campaign, SAVE_CAMPAIGN_ID, ccs.curCampaign->id);
839 	cgi->XML_AddDate(campaign, SAVE_CAMPAIGN_DATE, ccs.date.day, ccs.date.sec);
840 	cgi->XML_AddLong(campaign, SAVE_CAMPAIGN_CREDITS, ccs.credits);
841 	cgi->XML_AddShort(campaign, SAVE_CAMPAIGN_PAID, ccs.paid);
842 	cgi->XML_AddShortValue(campaign, SAVE_CAMPAIGN_NEXTUNIQUECHARACTERNUMBER, cgi->GetNextUniqueCharacterNumber());
843 
844 	cgi->XML_AddIntValue(campaign, SAVE_CAMPAIGN_CIVILIANSKILLED, ccs.civiliansKilled);
845 	cgi->XML_AddIntValue(campaign, SAVE_CAMPAIGN_ALIENSKILLED, ccs.aliensKilled);
846 
847 	/* Map and user interface */
848 	map = cgi->XML_AddNode(campaign, SAVE_CAMPAIGN_MAP);
849 	cgi->XML_AddShort(map, SAVE_CAMPAIGN_CL_GEOSCAPE_OVERLAY, cgi->Cvar_GetInteger("cl_geoscape_overlay"));
850 	cgi->XML_AddBool(map, SAVE_CAMPAIGN_RADAROVERLAYWASSET, radarOverlayWasSet);
851 	cgi->XML_AddBool(map, SAVE_CAMPAIGN_XVISTARTED, CP_IsXVIStarted());
852 
853 	mapDefStat = cgi->XML_AddNode(campaign, SAVE_CAMPAIGN_MAPDEFSTAT);
854 	if (!CP_SaveMapDefStatXML(mapDefStat))
855 		return false;
856 
857 	return true;
858 }
859 
860 /**
861  * @brief Starts a selected mission
862  * @note Checks whether a dropship is near the landing zone and whether
863  * it has a team on board
864  * @sa BATTLE_SetVars
865  */
CP_StartSelectedMission(void)866 void CP_StartSelectedMission (void)
867 {
868 	mission_t* mis;
869 	aircraft_t* aircraft = GEO_GetMissionAircraft();
870 	base_t* base;
871 	battleParam_t* battleParam = &ccs.battleParameters;
872 
873 	if (!aircraft) {
874 		Com_Printf("CP_StartSelectedMission: No mission aircraft\n");
875 		return;
876 	}
877 
878 	base = aircraft->homebase;
879 
880 	if (GEO_GetSelectedMission() == nullptr)
881 		GEO_SetSelectedMission(aircraft->mission);
882 
883 	mis = GEO_GetSelectedMission();
884 	if (!mis) {
885 		Com_Printf("CP_StartSelectedMission: No mission selected\n");
886 		return;
887 	}
888 
889 	/* Before we start, we should clear the missionResults array. */
890 	OBJZERO(ccs.missionResults);
891 
892 	/* Various sanity checks. */
893 	if (!mis->active) {
894 		Com_Printf("CP_StartSelectedMission: Dropship not near landing zone: mis->active: %i\n", mis->active);
895 		return;
896 	}
897 	if (AIR_GetTeamSize(aircraft) == 0) {
898 		Com_Printf("CP_StartSelectedMission: No team in dropship.\n");
899 		return;
900 	}
901 
902 	/* if we retry a mission we have to drop from the current game before */
903 	cgi->SV_Shutdown("Server quit.", false);
904 	cgi->CL_Disconnect();
905 
906 	CP_CreateBattleParameters(mis, battleParam, aircraft);
907 	BATTLE_SetVars(battleParam);
908 
909 	/* manage inventory */
910 	ccs.eMission = base->storage; /* copied, including arrays inside! */
911 	CP_CleanTempInventory(base);
912 	CP_CleanupAircraftTeam(aircraft, &ccs.eMission);
913 	BATTLE_Start(mis, battleParam);
914 }
915 
916 /**
917  * @brief Checks whether a soldier should be promoted
918  * @param[in] rank The rank to check for
919  * @param[in] chr The character to check a potential promotion for
920  * @todo (Zenerka 20080301) extend ranks and change calculations here.
921  */
CP_ShouldUpdateSoldierRank(const rank_t * rank,const character_t * chr)922 static bool CP_ShouldUpdateSoldierRank (const rank_t* rank, const character_t* chr)
923 {
924 	if (rank->type != EMPL_SOLDIER)
925 		return false;
926 
927 	/* mind is not yet enough */
928 	if (chr->score.skills[ABILITY_MIND] < rank->mind)
929 		return false;
930 
931 	/* not enough killed enemies yet */
932 	if (chr->score.kills[KILLED_ENEMIES] < rank->killedEnemies)
933 		return false;
934 
935 	/* too many civilians and team kills */
936 	if (chr->score.kills[KILLED_CIVILIANS] + chr->score.kills[KILLED_TEAM] > rank->killedOthers)
937 		return false;
938 
939 	return true;
940 }
941 
942 /**
943  * @brief Update employees stats after mission.
944  * @param[in] base The base where the team lives.
945  * @param[in] aircraft The aircraft used for the mission.
946  * @note Soldier promotion is being done here.
947  */
CP_UpdateCharacterStats(const base_t * base,const aircraft_t * aircraft)948 void CP_UpdateCharacterStats (const base_t* base, const aircraft_t* aircraft)
949 {
950 	assert(aircraft);
951 
952 	/* only soldiers have stats and ranks, ugvs not */
953 	E_Foreach(EMPL_SOLDIER, employee) {
954 		if (!employee->isHiredInBase(aircraft->homebase))
955 			continue;
956 		if (!AIR_IsEmployeeInAircraft(employee, aircraft))
957 			continue;
958 		character_t* chr = &employee->chr;
959 
960 		/* Remember the number of assigned mission for this character. */
961 		chr->score.assignedMissions++;
962 
963 		/** @todo use chrScore_t to determine negative influence on soldier here,
964 		 * like killing too many civilians and teammates can lead to unhire and disband
965 		 * such soldier, or maybe rank degradation. */
966 
967 		/* Check if the soldier meets the requirements for a higher rank
968 		 * and do a promotion. */
969 		if (ccs.numRanks < 2)
970 			continue;
971 
972 		for (int j = ccs.numRanks - 1; j > chr->score.rank; j--) {
973 			const rank_t* rank = CL_GetRankByIdx(j);
974 			if (!CP_ShouldUpdateSoldierRank(rank, chr))
975 				continue;
976 
977 			chr->score.rank = j;
978 			if (chr->HP > 0)
979 				Com_sprintf(cp_messageBuffer, sizeof(cp_messageBuffer), _("%s has been promoted to %s.\n"), chr->name, _(rank->name));
980 			else
981 				Com_sprintf(cp_messageBuffer, sizeof(cp_messageBuffer), _("%s has been awarded the posthumous rank of %s\nfor inspirational gallantry in the face of overwhelming odds.\n"), chr->name, _(rank->name));
982 			MS_AddNewMessage(_("Soldier promoted"), cp_messageBuffer, MSG_PROMOTION);
983 			break;
984 		}
985 	}
986 	Com_DPrintf(DEBUG_CLIENT, "CP_UpdateCharacterStats: Done\n");
987 }
988 
989 #ifdef DEBUG
990 /**
991  * @brief Debug function to show items in base storage.
992  * @note Command to call this: debug_listitem
993  */
CP_DebugShowItems_f(void)994 static void CP_DebugShowItems_f (void)
995 {
996 	int i;
997 	base_t* base;
998 
999 	if (cgi->Cmd_Argc() < 2) {
1000 		Com_Printf("Usage: %s <baseID>\n", cgi->Cmd_Argv(0));
1001 		return;
1002 	}
1003 
1004 	i = atoi(cgi->Cmd_Argv(1));
1005 	if (i >= B_GetCount()) {
1006 		Com_Printf("invalid baseID (%s)\n", cgi->Cmd_Argv(1));
1007 		return;
1008 	}
1009 	base = B_GetBaseByIDX(i);
1010 
1011 	for (i = 0; i < cgi->csi->numODs; i++) {
1012 		const objDef_t* obj = INVSH_GetItemByIDX(i);
1013 		Com_Printf("%i. %s: %i\n", i, obj->id, B_ItemInBase(obj, base));
1014 	}
1015 }
1016 
1017 /**
1018  * @brief Debug function to add certain items to base storage
1019  * @note Call this with debug_itemadd <baseIDX> <itemID> <count>
1020  * @note This function is not for antimatter
1021  * @sa CP_DebugAddAntimatter_f
1022  */
CP_DebugAddItem_f(void)1023 static void CP_DebugAddItem_f (void)
1024 {
1025 	if (cgi->Cmd_Argc() < 4) {
1026 		Com_Printf("Usage: %s <baseID> <itemid> <count>\n", cgi->Cmd_Argv(0));
1027 		return;
1028 	}
1029 
1030 	base_t* base = B_GetFoundedBaseByIDX(atoi(cgi->Cmd_Argv(1)));
1031 	const objDef_t* obj = INVSH_GetItemByID(cgi->Cmd_Argv(2));
1032 	const int count = atoi(cgi->Cmd_Argv(3));
1033 
1034 	if (!base) {
1035 		Com_Printf("Invalid base index given\n");
1036 		return;
1037 	}
1038 	if (!obj) {
1039 		/* INVSH_GetItemByIDX prints warning already */
1040 		return;
1041 	}
1042 
1043 	Com_Printf("%s %s %d\n", base->name, obj->id, count);
1044 	B_AddToStorage(base, obj, count);
1045 	if (B_ItemInBase(obj, base) > 0) {
1046 		technology_t* tech = RS_GetTechForItem(obj);
1047 		RS_MarkCollected(tech);
1048 	}
1049 }
1050 
1051 /**
1052  * @brief Debug function to add some antimatter to base container
1053  * @note Call this with debug_antimatteradd <baseIDX> <amount>
1054  * @note 0 amount will reset the container
1055  * @sa CP_DebugAddItem_f
1056  */
CP_DebugAddAntimatter_f(void)1057 static void CP_DebugAddAntimatter_f (void)
1058 {
1059 	if (cgi->Cmd_Argc() < 3) {
1060 		Com_Printf("Usage: %s <baseID> <amount>\n", cgi->Cmd_Argv(0));
1061 		return;
1062 	}
1063 
1064 	base_t* base = B_GetFoundedBaseByIDX(atoi(cgi->Cmd_Argv(1)));
1065 	const int amount = atoi(cgi->Cmd_Argv(2));
1066 
1067 	if (!base) {
1068 		Com_Printf("Invalid base index given\n");
1069 		return;
1070 	}
1071 
1072 	B_ManageAntimatter(base, amount, amount >= 0);
1073 }
1074 
1075 /**
1076  * @brief Debug function to set the credits to max
1077  */
CP_DebugFullCredits_f(void)1078 static void CP_DebugFullCredits_f (void)
1079 {
1080 	CP_UpdateCredits(MAX_CREDITS);
1081 }
1082 #endif
1083 
1084 /* ===================================================================== */
1085 
1086 /* these commands are only available in singleplayer */
1087 static const cmdList_t game_commands[] = {
1088 	{"update_base_radar_coverage", RADAR_UpdateBaseRadarCoverage_f, "Update base radar coverage"},
1089 	{"addeventmail", CL_EventAddMail_f, "Add a new mail (event trigger) - e.g. after a mission"},
1090 	{"stats_update", CP_StatsUpdate_f, nullptr},
1091 	{"game_go", CP_StartSelectedMission, nullptr},
1092 	{"game_timestop", CP_GameTimeStop, nullptr},
1093 	{"game_timeslow", CP_GameTimeSlow, nullptr},
1094 	{"game_timefast", CP_GameTimeFast, nullptr},
1095 	{"game_settimeid", CP_SetGameTime_f, nullptr},
1096 	{"map_center", GEO_CenterOnPoint_f, "Centers the geoscape view on items on the geoscape - and cycle through them"},
1097 	{"cp_start_xvi_spreading", CP_StartXVISpreading_f, "Start XVI spreading"},
1098 	{"cp_spawn_ufocarrier", CP_SpawnUFOCarrier_f, "Spawns a UFO-Carrier on the geoscape"},
1099 	{"cp_attack_ufocarrier", CP_AttackUFOCarrier_f, "Attack the UFO-Carrier"},
1100 #ifdef DEBUG
1101 	{"debug_fullcredits", CP_DebugFullCredits_f, "Debug function to give the player full credits"},
1102 	{"debug_itemadd", CP_DebugAddItem_f, "Debug function to add certain items to base storage"},
1103 	{"debug_antimatteradd", CP_DebugAddAntimatter_f, "Debug function to add some antimatter to base container"},
1104 	{"debug_listitem", CP_DebugShowItems_f, "Debug function to show all items in base storage"},
1105 #endif
1106 	{nullptr, nullptr, nullptr}
1107 };
1108 
1109 /**
1110  * @brief registers callback commands that are used by campaign
1111  * @todo callbacks should be registered on menu push
1112  * (what about sideeffects for commands that are called from different menus?)
1113  * @sa CP_AddCampaignCommands
1114  * @sa CP_RemoveCampaignCallbackCommands
1115  */
CP_AddCampaignCallbackCommands(void)1116 static void CP_AddCampaignCallbackCommands (void)
1117 {
1118 	AIM_InitCallbacks();
1119 	B_InitCallbacks();
1120 	BDEF_InitCallbacks();
1121 	BS_InitCallbacks();
1122 	CP_TEAM_InitCallbacks();
1123 	HOS_InitCallbacks();
1124 	INS_InitCallbacks();
1125 	PR_InitCallbacks();
1126 	RS_InitCallbacks();
1127 }
1128 
CP_AddCampaignCommands(void)1129 static void CP_AddCampaignCommands (void)
1130 {
1131 	const cmdList_t* commands;
1132 
1133 	for (commands = game_commands; commands->name; commands++)
1134 		cgi->Cmd_AddCommand(commands->name, commands->function, commands->description);
1135 
1136 	CP_AddCampaignCallbackCommands();
1137 }
1138 
1139 /**
1140  * @brief registers callback commands that are used by campaign
1141  * @todo callbacks should be removed on menu pop
1142  * (what about sideeffects for commands that are called from different menus?)
1143  * @sa CP_AddCampaignCommands
1144  * @sa CP_RemoveCampaignCallbackCommands
1145  */
CP_RemoveCampaignCallbackCommands(void)1146 static void CP_RemoveCampaignCallbackCommands (void)
1147 {
1148 	AIM_ShutdownCallbacks();
1149 	B_ShutdownCallbacks();
1150 	BDEF_ShutdownCallbacks();
1151 	BS_ShutdownCallbacks();
1152 	CP_TEAM_ShutdownCallbacks();
1153 	HOS_ShutdownCallbacks();
1154 	INS_ShutdownCallbacks();
1155 	PR_ShutdownCallbacks();
1156 	RS_ShutdownCallbacks();
1157 	MSO_Shutdown();
1158 	UP_Shutdown();
1159 }
1160 
CP_RemoveCampaignCommands(void)1161 static void CP_RemoveCampaignCommands (void)
1162 {
1163 	const cmdList_t* commands;
1164 
1165 	for (commands = game_commands; commands->name; commands++)
1166 		cgi->Cmd_RemoveCommand(commands->name);
1167 
1168 	CP_RemoveCampaignCallbackCommands();
1169 }
1170 
1171 /**
1172  * @brief Called at new game and load game
1173  * @param[in] load @c true if we are loading game, @c false otherwise
1174  * @param[in] campaign Pointer to campaign - it will be set to @c ccs.curCampaign here.
1175  */
CP_CampaignInit(campaign_t * campaign,bool load)1176 void CP_CampaignInit (campaign_t* campaign, bool load)
1177 {
1178 	ccs.curCampaign = campaign;
1179 
1180 	CP_ReadCampaignData(campaign);
1181 
1182 	CP_UpdateTime();
1183 
1184 	RS_InitTree(campaign, load);		/**< Initialise all data in the research tree. */
1185 
1186 	CP_AddCampaignCommands();
1187 
1188 	CP_GameTimeStop();
1189 
1190 	/* Init popup and map/geoscape */
1191 	CL_PopupInit();
1192 
1193 	CP_XVIInit();
1194 
1195 	cgi->UI_InitStack("geoscape", "campaign_main");
1196 
1197 	if (load) {
1198 		return;
1199 	}
1200 
1201 	CL_EventAddMail("prolog");
1202 
1203 	RS_MarkResearchable(nullptr, true);
1204 	BS_InitMarket(campaign);
1205 
1206 	/* create initial employees */
1207 	E_InitialEmployees(campaign);
1208 
1209 	GEO_Reset(campaign->map);
1210 
1211 	CP_UpdateCredits(campaign->credits);
1212 
1213 	/* Initialize alien interest */
1214 	INT_ResetAlienInterest();
1215 
1216 	/* Initialize XVI overlay */
1217 	CP_UpdateXVIMapButton();
1218 	CP_InitializeXVIOverlay();
1219 
1220 	/* create a base as first step */
1221 	B_SelectBase(nullptr);
1222 
1223 	/* Spawn first missions of the game */
1224 	CP_InitializeSpawningDelay();
1225 
1226 	/* now check the parsed values for errors that are not caught at parsing stage */
1227 	CP_ScriptSanityCheck();
1228 }
1229 
1230 /**
1231  * @brief Campaign closing actions
1232  */
CP_Shutdown(void)1233 void CP_Shutdown (void)
1234 {
1235 	if (CP_IsRunning()) {
1236 		int i;
1237 
1238 		AB_Shutdown();
1239 		AIR_Shutdown();
1240 		INS_Shutdown();
1241 		INT_Shutdown();
1242 		NAT_Shutdown();
1243 		MIS_Shutdown();
1244 		TR_Shutdown();
1245 		UR_Shutdown();
1246 		AM_Shutdown();
1247 		E_Shutdown();
1248 
1249 		/** @todo Where does this belong? */
1250 		for (i = 0; i < ccs.numAlienCategories; i++) {
1251 			alienTeamCategory_t* alienCat = &ccs.alienCategories[i];
1252 			cgi->LIST_Delete(&alienCat->equipment);
1253 		}
1254 
1255 		cgi->Cvar_SetValue("cl_geoscape_overlay", 0);
1256 		/* singleplayer commands are no longer available */
1257 		Com_DPrintf(DEBUG_CLIENT, "Remove game commands\n");
1258 		CP_RemoveCampaignCommands();
1259 	}
1260 
1261 	GEO_Shutdown();
1262 }
1263 
1264 /**
1265  * @brief Returns the campaign pointer from global campaign array
1266  * @param name Name of the campaign
1267  * @return campaign_t pointer to campaign with name or nullptr if not found
1268  */
CP_GetCampaign(const char * name)1269 campaign_t* CP_GetCampaign (const char* name)
1270 {
1271 	campaign_t* campaign;
1272 	int i;
1273 
1274 	for (i = 0, campaign = ccs.campaigns; i < ccs.numCampaigns; i++, campaign++)
1275 		if (Q_streq(name, campaign->id))
1276 			break;
1277 
1278 	if (i == ccs.numCampaigns) {
1279 		Com_Printf("CL_GetCampaign: Campaign \"%s\" doesn't exist.\n", name);
1280 		return nullptr;
1281 	}
1282 	return campaign;
1283 }
1284 
1285 /**
1286  * @brief Will clear most of the parsed singleplayer data
1287  * @sa initInventory
1288  * @sa CP_ParseCampaignData
1289  */
CP_ResetCampaignData(void)1290 void CP_ResetCampaignData (void)
1291 {
1292 	mapDef_t* md;
1293 
1294 	cgi->UI_MessageResetStack();
1295 
1296 	/* cleanup dynamic mails */
1297 	CP_FreeDynamicEventMail();
1298 
1299 	Mem_FreePool(cp_campaignPool);
1300 
1301 	/* called to flood the hash list - because the parse tech function
1302 	 * was maybe already called */
1303 	RS_ResetTechs();
1304 
1305 	OBJZERO(ccs);
1306 
1307 	ccs.missionSpawnCallback = CP_SpawnNewMissions;
1308 
1309 	/* Clear mapDef usage statistics */
1310 	MapDef_ForeachSingleplayerCampaign(md) {
1311 		md->timesAlreadyUsed = 0;
1312 	}
1313 }
1314 
1315 #ifdef DEBUG
1316 /**
1317  * @brief Debug function to increase the kills and test the ranks
1318  */
CP_DebugChangeCharacterStats_f(void)1319 static void CP_DebugChangeCharacterStats_f (void)
1320 {
1321 	int j;
1322 	base_t* base = B_GetCurrentSelectedBase();
1323 
1324 	if (!base)
1325 		return;
1326 
1327 	E_Foreach(EMPL_SOLDIER, employee) {
1328 		character_t* chr;
1329 
1330 		if (!employee->isHiredInBase(base))
1331 			continue;
1332 
1333 		chr = &(employee->chr);
1334 		assert(chr);
1335 
1336 		for (j = 0; j < KILLED_NUM_TYPES; j++)
1337 			chr->score.kills[j]++;
1338 	}
1339 	if (base->aircraftCurrent)
1340 		CP_UpdateCharacterStats(base, base->aircraftCurrent);
1341 }
1342 
1343 #endif /* DEBUG */
1344 
1345 /**
1346  * @brief Determines a random position on geoscape
1347  * @param[out] pos The position that will be overwritten. pos[0] is within -180, +180. pos[1] within -90, +90.
1348  * @param[in] noWater True if the position should not be on water
1349  * @sa CP_GetRandomPosOnGeoscapeWithParameters
1350  * @note The random positions should be roughly uniform thanks to the non-uniform distribution used.
1351  * @note This function always returns a value.
1352  */
CP_GetRandomPosOnGeoscape(vec2_t pos,bool noWater)1353 void CP_GetRandomPosOnGeoscape (vec2_t pos, bool noWater)
1354 {
1355 	do {
1356 		pos[0] = (frand() - 0.5f) * 360.0f;
1357 		pos[1] = asin((frand() - 0.5f) * 2.0f) * todeg;
1358 	} while (noWater && MapIsWater(GEO_GetColor(pos, MAPTYPE_TERRAIN, nullptr)));
1359 
1360 	Com_DPrintf(DEBUG_CLIENT, "CP_GetRandomPosOnGeoscape: Get random position on geoscape %.2f:%.2f\n", pos[0], pos[1]);
1361 }
1362 
1363 /**
1364  * @brief Determines a random position on geoscape that fulfills certain criteria given via parameters
1365  * @param[out] pos The position that will be overwritten with the random point fulfilling the criteria. pos[0] is within -180, +180. pos[1] within -90, +90.
1366  * @param[in] terrainTypes A linkedList_t containing a list of strings determining the acceptable terrain types (e.g. "grass") May be nullptr.
1367  * @param[in] cultureTypes A linkedList_t containing a list of strings determining the acceptable culture types (e.g. "western") May be nullptr.
1368  * @param[in] populationTypes A linkedList_t containing a list of strings determining the acceptable population types (e.g. "suburban") May be nullptr.
1369  * @param[in] nations A linkedList_t containing a list of strings determining the acceptable nations (e.g. "asia"). May be nullptr
1370  * @return true if a location was found, otherwise false
1371  * @note There may be no position fitting the parameters. The higher RASTER, the lower the probability to find a position.
1372  * @sa LIST_AddString
1373  * @sa LIST_Delete
1374  * @note When all parameters are nullptr, the algorithm assumes that it does not need to include "water" terrains when determining a random position
1375  * @note You should rather use CP_GetRandomPosOnGeoscape if there are no parameters (except water) to choose a random position
1376  */
CP_GetRandomPosOnGeoscapeWithParameters(vec2_t pos,const linkedList_t * terrainTypes,const linkedList_t * cultureTypes,const linkedList_t * populationTypes,const linkedList_t * nations)1377 bool CP_GetRandomPosOnGeoscapeWithParameters (vec2_t pos, const linkedList_t* terrainTypes, const linkedList_t* cultureTypes, const linkedList_t* populationTypes, const linkedList_t* nations)
1378 {
1379 	float x, y;
1380 	int num;
1381 	int randomNum;
1382 
1383 	/* RASTER might reduce amount of tested locations to get a better performance */
1384 	/* Number of points in latitude and longitude that will be tested. Therefore, the total number of position tried
1385 	 * will be numPoints * numPoints */
1386 	const float numPoints = 360.0 / RASTER;
1387 	/* RASTER is minimizing the amount of locations, so an offset is introduced to enable access to all locations, depending on a random factor */
1388 	const float offsetX = frand() * RASTER;
1389 	const float offsetY = -1.0 + frand() * 2.0 / numPoints;
1390 	vec2_t posT;
1391 	int hits = 0;
1392 
1393 	/* check all locations for suitability in 2 iterations */
1394 	/* prepare 1st iteration */
1395 
1396 	/* ITERATION 1 */
1397 	for (y = 0; y < numPoints; y++) {
1398 		const float posY = asin(2.0 * y / numPoints + offsetY) * todeg;	/* Use non-uniform distribution otherwise we favour the poles */
1399 		for (x = 0; x < numPoints; x++) {
1400 			const float posX = x * RASTER - 180.0 + offsetX;
1401 
1402 			Vector2Set(posT, posX, posY);
1403 
1404 			if (GEO_PositionFitsTCPNTypes(posT, terrainTypes, cultureTypes, populationTypes, nations)) {
1405 				/* the location given in pos belongs to the terrain, culture, population types and nations
1406 				 * that are acceptable, so count it */
1407 				/** @todo - cache the counted hits */
1408 				hits++;
1409 			}
1410 		}
1411 	}
1412 
1413 	/* if there have been no hits, the function failed to find a position */
1414 	if (hits == 0)
1415 		return false;
1416 
1417 	/* the 2nd iteration goes through the locations again, but does so only until a random point */
1418 	/* prepare 2nd iteration */
1419 	randomNum = num = rand() % hits;
1420 
1421 	/* ITERATION 2 */
1422 	for (y = 0; y < numPoints; y++) {
1423 		const float posY = asin(2.0 * y / numPoints + offsetY) * todeg;
1424 		for (x = 0; x < numPoints; x++) {
1425 			const float posX = x * RASTER - 180.0 + offsetX;
1426 
1427 			Vector2Set(posT,posX,posY);
1428 
1429 			if (GEO_PositionFitsTCPNTypes(posT, terrainTypes, cultureTypes, populationTypes, nations)) {
1430 				num--;
1431 
1432 				if (num < 1) {
1433 					Vector2Set(pos, posX, posY);
1434 					Com_DPrintf(DEBUG_CLIENT, "CP_GetRandomPosOnGeoscapeWithParameters: New random coords for a mission are %.0f:%.0f, chosen as #%i out of %i possible locations\n",
1435 						pos[0], pos[1], randomNum, hits);
1436 					return true;
1437 				}
1438 			}
1439 		}
1440 	}
1441 
1442 	Com_DPrintf(DEBUG_CLIENT, "CP_GetRandomPosOnGeoscapeWithParameters: New random coordinates for a mission are %.0f:%.0f, chosen as #%i out of %i possible locations\n",
1443 		pos[0], pos[1], num, hits);
1444 
1445 	/** @todo add EQUAL_EPSILON here? */
1446 	/* Make sure that position is within bounds */
1447 	assert(pos[0] >= -180);
1448 	assert(pos[0] <= 180);
1449 	assert(pos[1] >= -90);
1450 	assert(pos[1] <= 90);
1451 
1452 	return true;
1453 }
1454 
CP_GetSalaryAdministrative(const salary_t * salary)1455 int CP_GetSalaryAdministrative (const salary_t* salary)
1456 {
1457 	int i, costs;
1458 
1459 	costs = salary->adminInitial;
1460 	for (i = 0; i < MAX_EMPL; i++) {
1461 		const employeeType_t type = (employeeType_t)i;
1462 		costs += E_CountByType(type) * CP_GetSalaryAdminEmployee(salary, type);
1463 	}
1464 	return costs;
1465 }
1466 
CP_GetSalaryBaseEmployee(const salary_t * salary,employeeType_t type)1467 int CP_GetSalaryBaseEmployee (const salary_t* salary, employeeType_t type)
1468 {
1469 	return salary->base[type];
1470 }
1471 
CP_GetSalaryAdminEmployee(const salary_t * salary,employeeType_t type)1472 int CP_GetSalaryAdminEmployee (const salary_t* salary, employeeType_t type)
1473 {
1474 	return salary->admin[type];
1475 }
1476 
CP_GetSalaryRankBonusEmployee(const salary_t * salary,employeeType_t type)1477 int CP_GetSalaryRankBonusEmployee (const salary_t* salary, employeeType_t type)
1478 {
1479 	return salary->rankBonus[type];
1480 }
1481 
CP_GetSalaryUpKeepBase(const salary_t * salary,const base_t * base)1482 int CP_GetSalaryUpKeepBase (const salary_t* salary, const base_t* base)
1483 {
1484 	int cost = salary->baseUpkeep;	/* base cost */
1485 	building_t* building = nullptr;
1486 	while ((building = B_GetNextBuilding(base, building))) {
1487 		if (building->buildingStatus == B_STATUS_WORKING
1488 		 || building->buildingStatus == B_STATUS_CONSTRUCTION_FINISHED)
1489 			cost += building->varCosts;
1490 	}
1491 	return cost;
1492 }
1493 
1494 /** @todo remove me and move all the included stuff to proper places */
CP_InitStartup(void)1495 void CP_InitStartup (void)
1496 {
1497 	cp_campaignPool = Mem_CreatePool("Client: Local (per game)");
1498 
1499 	SAV_Init();
1500 
1501 	/* commands */
1502 #ifdef DEBUG
1503 	cgi->Cmd_AddCommand("debug_statsupdate", CP_DebugChangeCharacterStats_f, "Debug function to increase the kills and test the ranks");
1504 #endif
1505 
1506 	cp_missiontest = cgi->Cvar_Get("cp_missiontest", "0", CVAR_DEVELOPER, "This will never stop the time on geoscape and print information about spawned missions");
1507 
1508 	/* init subsystems */
1509 	MS_MessageInit();
1510 
1511 	MIS_InitStartup();
1512 	UP_InitStartup();
1513 	B_InitStartup();
1514 	INS_InitStartup();
1515 	RS_InitStartup();
1516 	E_InitStartup();
1517 	HOS_InitStartup();
1518 	INT_InitStartup();
1519 	AC_InitStartup();
1520 	GEO_InitStartup();
1521 	UFO_InitStartup();
1522 	TR_InitStartup();
1523 	AB_InitStartup();
1524 	AIR_InitStartup();
1525 	AIRFIGHT_InitStartup();
1526 	NAT_InitStartup();
1527 	TR_InitStartup();
1528 	STATS_InitStartup();
1529 	UR_InitStartup();
1530 	AM_InitStartup();
1531 }
1532