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