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