1 /**
2  * @file
3  * @brief Campaign parsing code
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 "../../../shared/parse.h"
27 #include "cp_campaign.h"
28 #include "cp_rank.h"
29 #include "cp_parse.h"
30 #include "../../cl_inventory.h" /* INV_GetEquipmentDefinitionByID */
31 
32 /**
33  * @return Alien mission category
34  * @sa interestCategory_t
35  */
CP_GetAlienMissionTypeByID(const char * type)36 static interestCategory_t CP_GetAlienMissionTypeByID (const char* type)
37 {
38 	if (Q_streq(type, "recon"))
39 		return INTERESTCATEGORY_RECON;
40 	else if (Q_streq(type, "terror"))
41 		return INTERESTCATEGORY_TERROR_ATTACK;
42 	else if (Q_streq(type, "baseattack"))
43 		return INTERESTCATEGORY_BASE_ATTACK;
44 	else if (Q_streq(type, "building"))
45 		return INTERESTCATEGORY_BUILDING;
46 	else if (Q_streq(type, "supply"))
47 		return INTERESTCATEGORY_SUPPLY;
48 	else if (Q_streq(type, "xvi"))
49 		return INTERESTCATEGORY_XVI;
50 	else if (Q_streq(type, "intercept"))
51 		return INTERESTCATEGORY_INTERCEPT;
52 	else if (Q_streq(type, "harvest"))
53 		return INTERESTCATEGORY_HARVEST;
54 	else if (Q_streq(type, "alienbase"))
55 		return INTERESTCATEGORY_ALIENBASE;
56 	else if (Q_streq(type, "ufocarrier"))
57 		return INTERESTCATEGORY_UFOCARRIER;
58 	else if (Q_streq(type, "rescue"))
59 		return INTERESTCATEGORY_RESCUE;
60 	else {
61 		Com_Printf("CP_GetAlienMissionTypeByID: unknown alien mission category '%s'\n", type);
62 		return INTERESTCATEGORY_NONE;
63 	}
64 }
65 
66 static const value_t alien_group_vals[] = {
67 	{"mininterest", V_INT, offsetof(alienTeamGroup_t, minInterest), 0},
68 	{"maxinterest", V_INT, offsetof(alienTeamGroup_t, maxInterest), 0},
69 	{"minaliencount", V_INT, offsetof(alienTeamGroup_t, minAlienCount), 0},
70 	{"maxaliencount", V_INT, offsetof(alienTeamGroup_t, maxAlienCount), 0},
71 	{nullptr, V_NULL, 0, 0}
72 };
73 
74 /**
75  * @sa CL_ParseScriptFirst
76  */
CP_ParseAlienTeam(const char * name,const char ** text)77 static void CP_ParseAlienTeam (const char* name, const char** text)
78 {
79 	const char* errhead = "CP_ParseAlienTeam: unexpected end of file (alienteam ";
80 	const char* token;
81 	int i;
82 	alienTeamCategory_t* alienCategory;
83 
84 	/* get it's body */
85 	token = Com_Parse(text);
86 
87 	if (!*text || *token != '{') {
88 		Com_Printf("CP_ParseAlienTeam: alien team category \"%s\" without body ignored\n", name);
89 		return;
90 	}
91 
92 	if (ccs.numAlienCategories >= ALIENCATEGORY_MAX) {
93 		Com_Printf("CP_ParseAlienTeam: maximum number of alien team category reached (%i)\n", ALIENCATEGORY_MAX);
94 		return;
95 	}
96 
97 	/* search for category with same name */
98 	for (i = 0; i < ccs.numAlienCategories; i++)
99 		if (Q_streq(name, ccs.alienCategories[i].id))
100 			break;
101 	if (i < ccs.numAlienCategories) {
102 		Com_Printf("CP_ParseAlienTeam: alien category def \"%s\" with same name found, second ignored\n", name);
103 		return;
104 	}
105 
106 	alienCategory = &ccs.alienCategories[ccs.numAlienCategories++];
107 	Q_strncpyz(alienCategory->id, name, sizeof(alienCategory->id));
108 
109 	do {
110 		token = cgi->Com_EParse(text, errhead, name);
111 		if (!*text)
112 			break;
113 		if (*token == '}')
114 			break;
115 
116 		if (Q_streq(token, "equipment")) {
117 			linkedList_t** list = &alienCategory->equipment;
118 			if (!Com_ParseList(text, list)) {
119 				cgi->Com_Error(ERR_DROP, "CL_ParseAlienTeam: \"%s\" Error while parsing equipment list", name);
120 			}
121 		} else if (Q_streq(token, "category")) {
122 			linkedList_t* list;
123 			if (!Com_ParseList(text, &list)) {
124 				cgi->Com_Error(ERR_DROP, "CL_ParseAlienTeam: \"%s\" Error while parsing category list", name);
125 			}
126 			for (linkedList_t* element = list; element != nullptr; element = element->next) {
127 				alienCategory->missionCategories[alienCategory->numMissionCategories] = CP_GetAlienMissionTypeByID((char*)element->data);
128 				if (alienCategory->missionCategories[alienCategory->numMissionCategories] == INTERESTCATEGORY_NONE)
129 					Com_Printf("CP_ParseAlienTeam: alien team category \"%s\" is used with no mission category. It won't be used in game.\n", name);
130 				alienCategory->numMissionCategories++;
131 			}
132 			cgi->LIST_Delete(&list);
133 		} else if (Q_streq(token, "teaminterest")) {
134 			alienTeamGroup_t* group;
135 
136 			token = cgi->Com_EParse(text, errhead, name);
137 			if (!*text || *token != '{') {
138 				Com_Printf("CP_ParseAlienTeam: alien team \"%s\" has team with no opening brace\n", name);
139 				break;
140 			}
141 
142 			if (alienCategory->numAlienTeamGroups >= MAX_ALIEN_GROUP_PER_CATEGORY) {
143 				Com_Printf("CP_ParseAlienTeam: maximum number of alien team reached (%i) in category \"%s\"\n", MAX_ALIEN_GROUP_PER_CATEGORY, name);
144 				break;
145 			}
146 
147 			group = &alienCategory->alienTeamGroups[alienCategory->numAlienTeamGroups];
148 			group->idx = alienCategory->numAlienTeamGroups;
149 			group->categoryIdx = alienCategory - ccs.alienCategories;
150 			alienCategory->numAlienTeamGroups++;
151 
152 			do {
153 				token = cgi->Com_EParse(text, errhead, name);
154 
155 				if (!Com_ParseBlockToken(name, text, group, alien_group_vals, cp_campaignPool, token)) {
156 					if (!*text || *token == '}')
157 						break;
158 
159 					if (Q_streq(token, "team")) {
160 						linkedList_t* list;
161 						if (!Com_ParseList(text, &list)) {
162 							cgi->Com_Error(ERR_DROP, "CL_ParseAlienTeam: \"%s\" Error while parsing team list", name);
163 						}
164 						for (linkedList_t* element = list; element != nullptr; element = element->next) {
165 							if (group->numAlienTeams >= MAX_TEAMS_PER_MISSION)
166 								cgi->Com_Error(ERR_DROP, "CL_ParseAlienTeam: MAX_TEAMS_PER_MISSION hit");
167 							const teamDef_t* teamDef = cgi->Com_GetTeamDefinitionByID(strtok((char*)element->data, "/"));
168 							if (teamDef) {
169 								group->alienTeams[group->numAlienTeams] = teamDef;
170 								const chrTemplate_t* chrTemplate = CHRSH_GetTemplateByID(teamDef, strtok(nullptr, ""));
171 								group->alienChrTemplates[group->numAlienTeams] = chrTemplate;
172 								++group->numAlienTeams;
173 							}
174 						}
175 						cgi->LIST_Delete(&list);
176 					} else {
177 						cgi->Com_Error(ERR_DROP, "CL_ParseAlienTeam: Unknown token \"%s\"\n", token);
178 					}
179 				}
180 			} while (*text);
181 
182 			if (group->minAlienCount > group->maxAlienCount) {
183 				Com_Printf("CP_ParseAlienTeam: Minimum number of aliens is greater than maximum value! Swapped.\n");
184 				const int swap = group->minAlienCount;
185 				group->minAlienCount = group->maxAlienCount;
186 				group->maxAlienCount = swap;
187 			}
188 		} else {
189 			Com_Printf("CP_ParseAlienTeam: unknown token \"%s\" ignored (category %s)\n", token, name);
190 			continue;
191 		}
192 	} while (*text);
193 
194 	if (cgi->LIST_IsEmpty(alienCategory->equipment))
195 		Sys_Error("alien category equipment list is empty");
196 }
197 
198 /**
199  * @brief This function parses a list of items that should be set to researched = true after campaign start
200  */
CP_ParseResearchedCampaignItems(const campaign_t * campaign,const char * name,const char ** text)201 static void CP_ParseResearchedCampaignItems (const campaign_t* campaign, const char* name, const char** text)
202 {
203 	const char* errhead = "CP_ParseResearchedCampaignItems: unexpected end of file (equipment ";
204 	const char* token;
205 	int i;
206 
207 	/* Don't parse if it is not definition for current type of campaign. */
208 	if (!Q_streq(campaign->researched, name))
209 		return;
210 
211 	/* get it's body */
212 	token = Com_Parse(text);
213 
214 	if (!*text || *token != '{') {
215 		Com_Printf("CP_ParseResearchedCampaignItems: equipment def \"%s\" without body ignored (%s)\n",
216 				name, token);
217 		return;
218 	}
219 
220 	Com_DPrintf(DEBUG_CLIENT, "..campaign research list '%s'\n", name);
221 	do {
222 		token = cgi->Com_EParse(text, errhead, name);
223 		if (!*text || *token == '}')
224 			return;
225 
226 		for (i = 0; i < ccs.numTechnologies; i++) {
227 			technology_t* tech = RS_GetTechByIDX(i);
228 			assert(tech);
229 			if (Q_streq(token, tech->id)) {
230 				tech->mailSent = MAILSENT_FINISHED;
231 				tech->markResearched.markOnly[tech->markResearched.numDefinitions] = true;
232 				tech->markResearched.campaign[tech->markResearched.numDefinitions] = Mem_PoolStrDup(name, cp_campaignPool, 0);
233 				tech->markResearched.numDefinitions++;
234 				Com_DPrintf(DEBUG_CLIENT, "...tech %s\n", tech->id);
235 				break;
236 			}
237 		}
238 
239 		if (i == ccs.numTechnologies)
240 			Com_Printf("CP_ParseResearchedCampaignItems: unknown token \"%s\" ignored (tech %s)\n", token, name);
241 
242 	} while (*text);
243 }
244 
245 /**
246  * @brief This function parses a list of items that should be set to researchable = true after campaign start
247  * @param[in] campaign The campaign data structure
248  * @param[in] name Name of the techlist
249  * @param[in,out] text Script to parse
250  * @param[in] researchable Mark them researchable or not researchable
251  * @sa CP_ParseScriptFirst
252  */
CP_ParseResearchableCampaignStates(const campaign_t * campaign,const char * name,const char ** text,bool researchable)253 static void CP_ParseResearchableCampaignStates (const campaign_t* campaign, const char* name, const char** text, bool researchable)
254 {
255 	const char* errhead = "CP_ParseResearchableCampaignStates: unexpected end of file (equipment ";
256 	const char* token;
257 	int i;
258 
259 	/* get it's body */
260 	token = Com_Parse(text);
261 
262 	if (!*text || *token != '{') {
263 		Com_Printf("CP_ParseResearchableCampaignStates: equipment def \"%s\" without body ignored\n", name);
264 		return;
265 	}
266 
267 	if (!Q_streq(campaign->researched, name)) {
268 		Com_DPrintf(DEBUG_CLIENT, "..don't use '%s' as researchable list\n", name);
269 		return;
270 	}
271 
272 	Com_DPrintf(DEBUG_CLIENT, "..campaign researchable list '%s'\n", name);
273 	do {
274 		token = cgi->Com_EParse(text, errhead, name);
275 		if (!*text || *token == '}')
276 			return;
277 
278 		for (i = 0; i < ccs.numTechnologies; i++) {
279 			technology_t* tech = RS_GetTechByIDX(i);
280 			if (Q_streq(token, tech->id)) {
281 				if (researchable) {
282 					tech->mailSent = MAILSENT_PROPOSAL;
283 					RS_MarkOneResearchable(tech);
284 				} else {
285 					/** @todo Mark unresearchable */
286 				}
287 				Com_DPrintf(DEBUG_CLIENT, "...tech %s\n", tech->id);
288 				break;
289 			}
290 		}
291 
292 		if (i == ccs.numTechnologies)
293 			Com_Printf("CP_ParseResearchableCampaignStates: unknown token \"%s\" ignored (tech %s)\n", token, name);
294 
295 	} while (*text);
296 }
297 
298 /* =========================================================== */
299 
300 static const value_t salary_vals[] = {
301 	{"soldier_base", V_INT, offsetof(salary_t, base[EMPL_SOLDIER]), MEMBER_SIZEOF(salary_t, base[EMPL_SOLDIER])},
302 	{"soldier_rankbonus", V_INT, offsetof(salary_t, rankBonus[EMPL_SOLDIER]), MEMBER_SIZEOF(salary_t, rankBonus[EMPL_SOLDIER])},
303 	{"worker_base", V_INT, offsetof(salary_t, base[EMPL_WORKER]), MEMBER_SIZEOF(salary_t, base[EMPL_WORKER])},
304 	{"worker_rankbonus", V_INT, offsetof(salary_t, rankBonus[EMPL_WORKER]), MEMBER_SIZEOF(salary_t, rankBonus[EMPL_WORKER])},
305 	{"scientist_base", V_INT, offsetof(salary_t, base[EMPL_SCIENTIST]), MEMBER_SIZEOF(salary_t, base[EMPL_SCIENTIST])},
306 	{"scientist_rankbonus", V_INT, offsetof(salary_t, rankBonus[EMPL_SCIENTIST]), MEMBER_SIZEOF(salary_t, rankBonus[EMPL_SCIENTIST])},
307 	{"pilot_base", V_INT, offsetof(salary_t, base[EMPL_PILOT]), MEMBER_SIZEOF(salary_t, base[EMPL_PILOT])},
308 	{"pilot_rankbonus", V_INT, offsetof(salary_t, rankBonus[EMPL_PILOT]), MEMBER_SIZEOF(salary_t, rankBonus[EMPL_PILOT])},
309 	{"robot_base", V_INT, offsetof(salary_t, base[EMPL_ROBOT]), MEMBER_SIZEOF(salary_t, base[EMPL_ROBOT])},
310 	{"robot_rankbonus", V_INT, offsetof(salary_t, rankBonus[EMPL_ROBOT]), MEMBER_SIZEOF(salary_t, rankBonus[EMPL_ROBOT])},
311 	{"aircraft_factor", V_INT, offsetof(salary_t, aircraftFactor), MEMBER_SIZEOF(salary_t, aircraftFactor)},
312 	{"aircraft_divisor", V_INT, offsetof(salary_t, aircraftDivisor), MEMBER_SIZEOF(salary_t, aircraftDivisor)},
313 	{"base_upkeep", V_INT, offsetof(salary_t, baseUpkeep), MEMBER_SIZEOF(salary_t, baseUpkeep)},
314 	{"admin_initial", V_INT, offsetof(salary_t, adminInitial), MEMBER_SIZEOF(salary_t, adminInitial)},
315 	{"admin_soldier", V_INT, offsetof(salary_t, admin[EMPL_SOLDIER]), MEMBER_SIZEOF(salary_t, admin[EMPL_SOLDIER])},
316 	{"admin_worker", V_INT, offsetof(salary_t, admin[EMPL_WORKER]), MEMBER_SIZEOF(salary_t, admin[EMPL_WORKER])},
317 	{"admin_scientist", V_INT, offsetof(salary_t, admin[EMPL_SCIENTIST]), MEMBER_SIZEOF(salary_t, admin[EMPL_SCIENTIST])},
318 	{"admin_pilot", V_INT, offsetof(salary_t, admin[EMPL_PILOT]), MEMBER_SIZEOF(salary_t, admin[EMPL_PILOT])},
319 	{"admin_robot", V_INT, offsetof(salary_t, admin[EMPL_ROBOT]), MEMBER_SIZEOF(salary_t, admin[EMPL_ROBOT])},
320 	{"debt_interest", V_FLOAT, offsetof(salary_t, debtInterest), MEMBER_SIZEOF(salary_t, debtInterest)},
321 	{nullptr, V_NULL, 0, 0}
322 };
323 
324 /**
325  * @brief Parse the salaries from campaign definition
326  * @param[in] name Name or ID of the found character skill and ability definition
327  * @param[in] text The text of the nation node
328  * @param[out] s Pointer to the campaign salaries data structure to parse into
329  * @note Example:
330  * <code>salary {
331  *  soldier_base 3000
332  * }</code>
333  */
CP_ParseSalary(const char * name,const char ** text,salary_t * s)334 static void CP_ParseSalary (const char* name, const char** text, salary_t* s)
335 {
336 	Com_ParseBlock(name, text, s, salary_vals, cp_campaignPool);
337 }
338 
339 /* =========================================================== */
340 
341 static const value_t campaign_vals[] = {
342 	{"team", V_TEAM, offsetof(campaign_t, team), MEMBER_SIZEOF(campaign_t, team)},
343 	{"soldiers", V_INT, offsetof(campaign_t, soldiers), MEMBER_SIZEOF(campaign_t, soldiers)},
344 	{"workers", V_INT, offsetof(campaign_t, workers), MEMBER_SIZEOF(campaign_t, workers)},
345 	{"xvirate", V_INT, offsetof(campaign_t, maxAllowedXVIRateUntilLost), MEMBER_SIZEOF(campaign_t, maxAllowedXVIRateUntilLost)},
346 	{"maxdebts", V_INT, offsetof(campaign_t, negativeCreditsUntilLost), MEMBER_SIZEOF(campaign_t, negativeCreditsUntilLost)},
347 	{"minhappiness", V_FLOAT, offsetof(campaign_t, minhappiness), MEMBER_SIZEOF(campaign_t, minhappiness)},
348 	{"scientists", V_INT, offsetof(campaign_t, scientists), MEMBER_SIZEOF(campaign_t, scientists)},
349 	{"pilots", V_INT, offsetof(campaign_t, pilots), MEMBER_SIZEOF(campaign_t, pilots)},
350 	{"equipment", V_STRING, offsetof(campaign_t, equipment), 0},
351 	{"soldierequipment", V_STRING, offsetof(campaign_t, soldierEquipment), 0},
352 	{"market", V_STRING, offsetof(campaign_t, market), 0},
353 	{"asymptotic_market", V_STRING, offsetof(campaign_t, asymptoticMarket), 0},
354 	{"researched", V_STRING, offsetof(campaign_t, researched), 0},
355 	{"difficulty", V_INT, offsetof(campaign_t, difficulty), MEMBER_SIZEOF(campaign_t, difficulty)},
356 	{"map", V_STRING, offsetof(campaign_t, map), 0},
357 	{"credits", V_INT, offsetof(campaign_t, credits), MEMBER_SIZEOF(campaign_t, credits)},
358 	{"visible", V_BOOL, offsetof(campaign_t, visible), MEMBER_SIZEOF(campaign_t, visible)},
359 	{"text", V_TRANSLATION_STRING, offsetof(campaign_t, text), 0}, /* just a gettext placeholder */
360 	{"name", V_TRANSLATION_STRING, offsetof(campaign_t, name), 0},
361 	{"date", V_DATE, offsetof(campaign_t, date), 0},
362 	{"basecost", V_INT, offsetof(campaign_t, basecost), MEMBER_SIZEOF(campaign_t, basecost)},
363 	{"firstbase", V_STRING, offsetof(campaign_t, firstBaseTemplate), 0},
364 	{"researchrate", V_FLOAT, offsetof(campaign_t, researchRate), MEMBER_SIZEOF(campaign_t, researchRate)},
365 	{"producerate", V_FLOAT, offsetof(campaign_t, produceRate), MEMBER_SIZEOF(campaign_t, produceRate)},
366 	{"healingrate", V_FLOAT, offsetof(campaign_t, healingRate), MEMBER_SIZEOF(campaign_t, healingRate)},
367 	{"uforeductionrate", V_FLOAT, offsetof(campaign_t, ufoReductionRate), MEMBER_SIZEOF(campaign_t, ufoReductionRate)},
368 	{"initialinterest", V_INT, offsetof(campaign_t, initialInterest), MEMBER_SIZEOF(campaign_t, initialInterest)},
369 	{"employeerate", V_FLOAT, offsetof(campaign_t, employeeRate), MEMBER_SIZEOF(campaign_t, employeeRate)},
370 	{"alienbaseinterest", V_INT, offsetof(campaign_t, alienBaseInterest), MEMBER_SIZEOF(campaign_t, alienBaseInterest)},
371 	{nullptr, V_NULL, 0, 0}
372 };
373 
374 /**
375  * @sa CL_ParseClientData
376  */
CP_ParseCampaign(const char * name,const char ** text)377 static void CP_ParseCampaign (const char* name, const char** text)
378 {
379 	const char* errhead = "CP_ParseCampaign: unexpected end of file (campaign ";
380 	campaign_t* cp;
381 	const char* token;
382 	int i;
383 	salary_t* s;
384 	bool drop = false;
385 
386 	/* search for campaigns with same name */
387 	for (i = 0; i < ccs.numCampaigns; i++)
388 		if (Q_streq(name, ccs.campaigns[i].id))
389 			break;
390 
391 	if (i < ccs.numCampaigns) {
392 		Com_Printf("CP_ParseCampaign: campaign def \"%s\" with same name found, second ignored\n", name);
393 		return;
394 	}
395 
396 	if (ccs.numCampaigns >= MAX_CAMPAIGNS) {
397 		Com_Printf("CP_ParseCampaign: Max campaigns reached (%i)\n", MAX_CAMPAIGNS);
398 		return;
399 	}
400 
401 	/* initialize the campaign */
402 	cp = &ccs.campaigns[ccs.numCampaigns++];
403 	OBJZERO(*cp);
404 
405 	cp->idx = ccs.numCampaigns - 1;
406 	Q_strncpyz(cp->id, name, sizeof(cp->id));
407 
408 	/* some default values */
409 	cp->team = TEAM_PHALANX;
410 	Q_strncpyz(cp->researched, "researched_human", sizeof(cp->researched));
411 	cp->researchRate = 0.8f;
412 	cp->produceRate = 1.0f;
413 	cp->healingRate = 1.0f;
414 	cp->ufoReductionRate = NON_OCCURRENCE_PROBABILITY;
415 	cp->initialInterest = INITIAL_OVERALL_INTEREST;
416 	cp->employeeRate = 1.0f;
417 	cp->alienBaseInterest = 200;
418 
419 	/* get it's body */
420 	token = Com_Parse(text);
421 
422 	if (!*text || *token != '{') {
423 		Com_Printf("CP_ParseCampaign: campaign def \"%s\" without body ignored\n", name);
424 		ccs.numCampaigns--;
425 		return;
426 	}
427 
428 	/* set undefined markers */
429 	s = &cp->salaries;
430 	for (i = 0; i < MAX_EMPL; i++) {
431 		s->base[i] = -1;
432 		s->rankBonus[i] = -1;
433 		s->admin[i] = -1;
434 	}
435 	s->aircraftFactor = -1;
436 	s->aircraftDivisor = -1;
437 	s->baseUpkeep = -1;
438 	s->adminInitial = -1;
439 	s->debtInterest = -1;
440 
441 	do {
442 		token = cgi->Com_EParse(text, errhead, name);
443 		if (!*text)
444 			break;
445 		if (*token == '}')
446 			break;
447 
448 		/* check for some standard values */
449 		if (Com_ParseBlockToken(name, text, cp, campaign_vals, nullptr, token)) {
450 			continue;
451 		} else if (Q_streq(token, "salary")) {
452 			CP_ParseSalary(token, text, s);
453 		} else if (Q_streq(token, "events")) {
454 			token = cgi->Com_EParse(text, errhead, name);
455 			if (!*text)
456 				return;
457 			cp->events = CP_GetEventsByID(token);
458 		} else {
459 			Com_Printf("CP_ParseCampaign: unknown token \"%s\" ignored (campaign %s)\n", token, name);
460 			cgi->Com_EParse(text, errhead, name);
461 		}
462 	} while (*text);
463 
464 	if (cp->difficulty < -4)
465 		cp->difficulty = -4;
466 	else if (cp->difficulty > 4)
467 		cp->difficulty = 4;
468 
469 	/* checking for undefined values */
470 	for (i = 0; i < MAX_EMPL; i++) {
471 		if (s->base[i] == -1 || s->rankBonus[i] == -1 || s->admin[i] == -1) {
472 			drop = true;
473 			break;
474 		}
475 	}
476 	if (drop || s->aircraftFactor == -1 || s->aircraftDivisor == -1 || s->baseUpkeep == -1
477 	 || s->adminInitial == -1 || s->debtInterest == -1) {
478 		Com_Printf("CP_ParseCampaign: check salary definition. Campaign def \"%s\" ignored\n", name);
479 		ccs.numCampaigns--;
480 		return;
481 	}
482 }
483 
484 /** components type parsing helper */
485 struct component_type_data_t {
486 	char id[MAX_VAR];			/**< id of the campaign */
487 	char amount[MAX_VAR];			/**< placeholder for gettext stuff */
488 	char numbercrash[MAX_VAR];			/**< geoscape map */
489 };
490 /** components type values */
491 static const value_t components_type_vals[] = {
492 	{"id", V_STRING, offsetof(component_type_data_t, id), 0},
493 	{"amount", V_STRING, offsetof(component_type_data_t, amount), 0},
494 	{"numbercrash", V_STRING, offsetof(component_type_data_t, numbercrash), 0},
495 	{nullptr, V_NULL, 0, 0}
496 };
497 
498 /**
499  * @brief Parses one "components" entry in a .ufo file and writes it into the next free entry in xxxxxxxx (components_t).
500  * @param[in] name The unique id of a components_t array entry.
501  * @param[in] text the whole following text after the "components" definition.
502  * @sa CP_ParseScriptFirst
503  */
CP_ParseComponents(const char * name,const char ** text)504 static void CP_ParseComponents (const char* name, const char** text)
505 {
506 	components_t* comp;
507 	const char* errhead = "CP_ParseComponents: unexpected end of file.";
508 	const char* token;
509 
510 	/* get body */
511 	token = Com_Parse(text);
512 	if (!*text || *token != '{') {
513 		Com_Printf("CP_ParseComponents: \"%s\" components def without body ignored.\n", name);
514 		return;
515 	}
516 	if (ccs.numComponents >= MAX_ASSEMBLIES) {
517 		Com_Printf("CP_ParseComponents: too many technology entries. limit is %i.\n", MAX_ASSEMBLIES);
518 		return;
519 	}
520 
521 	/* New components-entry (next free entry in global comp-list) */
522 	comp = &ccs.components[ccs.numComponents];
523 	ccs.numComponents++;
524 
525 	OBJZERO(*comp);
526 
527 	/* name is not used */
528 
529 	do {
530 		/* get the name type */
531 		token = cgi->Com_EParse(text, errhead, name);
532 		if (!*text)
533 			break;
534 		if (*token == '}')
535 			break;
536 
537 		/* get values */
538 		if (Q_streq(token, "aircraft")) {
539 			token = cgi->Com_EParse(text, errhead, name);
540 			if (!*text)
541 				break;
542 
543 			/* set standard values */
544 			Q_strncpyz(comp->assemblyId, token, sizeof(comp->assemblyId));
545 			comp->assemblyItem = INVSH_GetItemByIDSilent(comp->assemblyId);
546 			if (comp->assemblyItem)
547 				Com_DPrintf(DEBUG_CLIENT, "CP_ParseComponents: linked item: %s with components: %s\n", token, comp->assemblyId);
548 		} else if (Q_streq(token, "item")) {
549 			/* Defines what items need to be collected for this item to be researchable. */
550 			if (comp->numItemtypes < MAX_COMP) {
551 				/* Parse block */
552 				component_type_data_t itemTokens;
553 				OBJZERO(itemTokens);
554 				if (Com_ParseBlock ("item", text, &itemTokens, components_type_vals, nullptr)) {
555 					if (itemTokens.id[0] == '\0')
556 						cgi->Com_Error(ERR_DROP, "CP_ParseComponents: \"item\" token id is missing.\n");
557 					if (itemTokens.amount[0] == '\0')
558 						cgi->Com_Error(ERR_DROP, "CP_ParseComponents: \"amount\" token id is missing.\n");
559 					if (itemTokens.numbercrash[0] == '\0')
560 						cgi->Com_Error(ERR_DROP, "CP_ParseComponents: \"numbercrash\" token id is missing.\n");
561 
562 					comp->items[comp->numItemtypes] = INVSH_GetItemByID(itemTokens.id);	/* item id -> item pointer */
563 
564 					/* Parse number of items. */
565 					comp->itemAmount[comp->numItemtypes] = atoi(itemTokens.amount);
566 					/* If itemcount needs to be scaled */
567 					if (itemTokens.numbercrash[0] == '%')
568 						comp->itemAmount2[comp->numItemtypes] = COMP_ITEMCOUNT_SCALED;
569 					else
570 						comp->itemAmount2[comp->numItemtypes] = atoi(itemTokens.numbercrash);
571 
572 					/** @todo Set item links to NONE if needed */
573 					/* comp->item_idx[comp->numItemtypes] = xxx */
574 
575 					comp->numItemtypes++;
576 				}
577 			} else {
578 				Com_Printf("CP_ParseComponents: \"%s\" Too many 'items' defined. Limit is %i - ignored.\n", name, MAX_COMP);
579 			}
580 		} else if (Q_streq(token, "time")) {
581 			/* Defines how long disassembly lasts. */
582 			token = Com_Parse(text);
583 			comp->time = atoi(token);
584 		} else {
585 			Com_Printf("CP_ParseComponents: Error in \"%s\" - unknown token: \"%s\".\n", name, token);
586 		}
587 	} while (*text);
588 
589 	if (comp->assemblyId[0] == '\0') {
590 		cgi->Com_Error(ERR_DROP, "CP_ParseComponents: component \"%s\" is not applied to any aircraft.\n", name);
591 	}
592 }
593 
594 /**
595  * @brief Returns components definition for an item.
596  * @param[in] item Item to search the components for.
597  * @return Pointer to @c components_t definition.
598  */
CP_GetComponentsByItem(const objDef_t * item)599 components_t* CP_GetComponentsByItem (const objDef_t* item)
600 {
601 	int i;
602 
603 	for (i = 0; i < ccs.numComponents; i++) {
604 		components_t* comp = &ccs.components[i];
605 		if (comp->assemblyItem == item) {
606 			Com_DPrintf(DEBUG_CLIENT, "CP_GetComponentsByItem: found components id: %s\n", comp->assemblyId);
607 			return comp;
608 		}
609 	}
610 	cgi->Com_Error(ERR_DROP, "CP_GetComponentsByItem: could not find components id for: %s", item->id);
611 }
612 
613 /**
614  * @brief Returns components definition by ID.
615  * @param[in] id assemblyId of the component definition.
616  * @return Pointer to @c components_t definition.
617  */
CP_GetComponentsByID(const char * id)618 components_t* CP_GetComponentsByID (const char* id)
619 {
620 	int i;
621 
622 	for (i = 0; i < ccs.numComponents; i++) {
623 		components_t* comp = &ccs.components[i];
624 		if (Q_streq(comp->assemblyId, id)) {
625 			return comp;
626 		}
627 	}
628 	cgi->Com_Error(ERR_DROP, "CP_GetComponentsByID: could not find components id for: %s", id);
629 }
630 
631 /**
632  * @brief Parsing campaign data
633  *
634  * first stage parses all the main data into their struct
635  * see CP_ParseScriptSecond for more details about parsing stages
636  * @sa CP_ParseCampaignData
637  * @sa CP_ParseScriptSecond
638  */
CP_ParseScriptFirst(const char * type,const char * name,const char ** text)639 static void CP_ParseScriptFirst (const char* type, const char* name, const char** text)
640 {
641 	/* check for client interpretable scripts */
642 	if (Q_streq(type, "up_chapter"))
643 		UP_ParseChapter(name, text);
644 	else if (Q_streq(type, "building"))
645 		B_ParseBuildings(name, text, false);
646 	else if (Q_streq(type, "installation"))
647 		INS_ParseInstallations(name, text);
648 	else if (Q_streq(type, "tech"))
649 		RS_ParseTechnologies(name, text);
650 	else if (Q_streq(type, "nation"))
651 		CL_ParseNations(name, text);
652 	else if (Q_streq(type, "city"))
653 		CITY_Parse(name, text);
654 	else if (Q_streq(type, "rank"))
655 		CL_ParseRanks(name, text);
656 	else if (Q_streq(type, "aircraft"))
657 		AIR_ParseAircraft(name, text, false);
658 	else if (Q_streq(type, "mail"))
659 		CL_ParseEventMails(name, text);
660 	else if (Q_streq(type, "events"))
661 		CL_ParseCampaignEvents(name, text);
662 	else if (Q_streq(type, "event"))
663 		CP_ParseEventTrigger(name, text);
664 	else if (Q_streq(type, "components"))
665 		CP_ParseComponents(name, text);
666 	else if (Q_streq(type, "alienteam"))
667 		CP_ParseAlienTeam(name, text);
668 	else if (Q_streq(type, "msgoptions"))
669 		MSO_ParseMessageSettings(name, text);
670 }
671 
672 /**
673  * @brief Parsing only for singleplayer
674  *
675  * parsed if we are no dedicated server
676  * second stage links all the parsed data from first stage
677  * example: we need a techpointer in a building - in the second stage the buildings and the
678  * techs are already parsed - so now we can link them
679  * @sa CP_ParseCampaignData
680  * @sa Com_ParseScripts
681  * @sa CL_ParseScriptFirst
682  */
CP_ParseScriptSecond(const char * type,const char * name,const char ** text)683 static void CP_ParseScriptSecond (const char* type, const char* name, const char** text)
684 {
685 	/* check for client interpretable scripts */
686 	if (Q_streq(type, "building"))
687 		B_ParseBuildings(name, text, true);
688 	else if (Q_streq(type, "aircraft"))
689 		AIR_ParseAircraft(name, text, true);
690 	else if (Q_streq(type, "basetemplate"))
691 		B_ParseBaseTemplate(name, text);
692 	else if (Q_streq(type, "campaign"))
693 		CP_ParseCampaign(name, text);
694 }
695 
696 /**
697  * @brief Parses the campaign specific data - this data can only be parsed once the campaign started
698  */
CP_ParseScriptCampaignRelated(const campaign_t * campaign,const char * type,const char * name,const char ** text)699 static void CP_ParseScriptCampaignRelated (const campaign_t* campaign, const char* type, const char* name, const char** text)
700 {
701 	if (Q_streq(type, "researched"))
702 		CP_ParseResearchedCampaignItems(campaign, name, text);
703 	else if (Q_streq(type, "researchable"))
704 		CP_ParseResearchableCampaignStates(campaign, name, text, true);
705 	else if (Q_streq(type, "notresearchable"))
706 		CP_ParseResearchableCampaignStates(campaign, name, text, false);
707 }
708 
709 /**
710  * @brief Make sure values of items after parsing are proper.
711  */
CP_ItemsSanityCheck(void)712 static bool CP_ItemsSanityCheck (void)
713 {
714 	int i;
715 	bool result = true;
716 
717 	for (i = 0; i < cgi->csi->numODs; i++) {
718 		const objDef_t* item = INVSH_GetItemByIDX(i);
719 
720 		/* Warn if item has no size set. */
721 		if (item->size <= 0 && B_ItemIsStoredInBaseStorage(item)) {
722 			result = false;
723 			Com_Printf("CP_ItemsSanityCheck: Item %s has zero size set.\n", item->id);
724 		}
725 
726 		/* Warn if no price is set. */
727 		if (item->price <= 0 && BS_IsOnMarket(item)) {
728 			result = false;
729 			Com_Printf("CP_ItemsSanityCheck: Item %s has zero price set.\n", item->id);
730 		}
731 
732 		if (item->price > 0 && !BS_IsOnMarket(item) && !PR_ItemIsProduceable(item)) {
733 			result = false;
734 			Com_Printf("CP_ItemsSanityCheck: Item %s has a price set though it is neither available on the market and production.\n", item->id);
735 		}
736 	}
737 
738 	return result;
739 }
740 
741 /** @brief struct that holds the sanity check data */
742 typedef struct {
743 	bool (*check)(void);	/**< function pointer to check function */
744 	const char* name;			/**< name of the subsystem to check */
745 } sanity_functions_t;
746 
747 /** @brief Data for sanity check of parsed script data */
748 static const sanity_functions_t sanity_functions[] = {
749 	{B_BuildingScriptSanityCheck, "buildings"},
750 	{RS_ScriptSanityCheck, "tech"},
751 	{AIR_ScriptSanityCheck, "aircraft"},
752 	{CP_ItemsSanityCheck, "items"},
753 	{NAT_ScriptSanityCheck, "nations"},
754 
755 	{nullptr, nullptr}
756 };
757 
758 /**
759  * @brief Check the parsed script values for errors after parsing every script file
760  * @sa CP_ParseCampaignData
761  */
CP_ScriptSanityCheck(void)762 void CP_ScriptSanityCheck (void)
763 {
764 	const sanity_functions_t* s;
765 
766 	Com_Printf("Sanity check for script data\n");
767 	s = sanity_functions;
768 	while (s->check) {
769 		bool status = s->check();
770 		Com_Printf("...%s %s\n", s->name, (status ? "ok" : "failed"));
771 		s++;
772 	}
773 }
774 
775 /**
776  * @brief Read the data for campaigns
777  * @sa SAV_GameLoad
778  * @sa CP_ResetCampaignData
779  */
CP_ParseCampaignData(void)780 void CP_ParseCampaignData (void)
781 {
782 	const char* type, *name, *text;
783 	int i;
784 	campaign_t* campaign;
785 
786 	/* pre-stage parsing */
787 	cgi->FS_BuildFileList("ufos/*.ufo");
788 	cgi->FS_NextScriptHeader(nullptr, nullptr, nullptr);
789 	text = nullptr;
790 
791 	while ((type = cgi->FS_NextScriptHeader("ufos/*.ufo", &name, &text)) != nullptr)
792 		CP_ParseScriptFirst(type, name, &text);
793 
794 	/* fill in IDXs for required research techs */
795 	RS_RequiredLinksAssign();
796 
797 	/* stage two parsing */
798 	cgi->FS_NextScriptHeader(nullptr, nullptr, nullptr);
799 	text = nullptr;
800 
801 	Com_DPrintf(DEBUG_CLIENT, "Second stage parsing started...\n");
802 	while ((type = cgi->FS_NextScriptHeader("ufos/*.ufo", &name, &text)) != nullptr)
803 		CP_ParseScriptSecond(type, name, &text);
804 	INS_LinkTechnologies();
805 
806 	for (i = 0; i < cgi->csi->numTeamDefs; i++) {
807 		const teamDef_t* teamDef = &cgi->csi->teamDef[i];
808 		if (!CHRSH_IsTeamDefAlien(teamDef))
809 			continue;
810 
811 		ccs.teamDefTechs[teamDef->idx] = RS_GetTechByID(teamDef->tech);
812 		if (ccs.teamDefTechs[teamDef->idx] == nullptr)
813 			cgi->Com_Error(ERR_DROP, "Could not find a tech for teamdef %s", teamDef->id);
814 	}
815 
816 	for (i = 0, campaign = ccs.campaigns; i < ccs.numCampaigns; i++, campaign++) {
817 		/* find the relevant markets */
818 		campaign->marketDef = cgi->INV_GetEquipmentDefinitionByID(campaign->market);
819 		campaign->asymptoticMarketDef = cgi->INV_GetEquipmentDefinitionByID(campaign->asymptoticMarket);
820 	}
821 
822 	Com_Printf("Campaign data loaded - size " UFO_SIZE_T " bytes\n", sizeof(ccs));
823 	Com_Printf("...techs: %i\n", ccs.numTechnologies);
824 	Com_Printf("...buildings: %i\n", ccs.numBuildingTemplates);
825 	Com_Printf("...ranks: %i\n", ccs.numRanks);
826 	Com_Printf("...nations: %i\n", ccs.numNations);
827 	Com_Printf("...cities: %i\n", ccs.numCities);
828 	Com_Printf("\n");
829 }
830 
CP_ReadCampaignData(const campaign_t * campaign)831 void CP_ReadCampaignData (const campaign_t* campaign)
832 {
833 	const char* type, *name, *text;
834 
835 	/* stage two parsing */
836 	cgi->FS_NextScriptHeader(nullptr, nullptr, nullptr);
837 	text = nullptr;
838 
839 	Com_DPrintf(DEBUG_CLIENT, "Second stage parsing started...\n");
840 	while ((type = cgi->FS_NextScriptHeader("ufos/*.ufo", &name, &text)) != nullptr)
841 		CP_ParseScriptCampaignRelated(campaign, type, name, &text);
842 
843 	/* initialise date */
844 	ccs.date = campaign->date;
845 	/* get day */
846 	while (ccs.date.sec > SECONDS_PER_DAY) {
847 		ccs.date.sec -= SECONDS_PER_DAY;
848 		ccs.date.day++;
849 	}
850 }
851