Bug Summary

File:client/cgame/campaign/cp_campaign.cpp
Location:line 264, column 2
Description:Access to field 'timesAlreadyUsed' results in a dereference of a null pointer (loaded from field 'mapDef')

Annotated Source Code

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