1 /**
2  * @file
3  * @brief UFOpaedia script interpreter.
4  * @todo Split the mail code into cl_mailclient.c/h
5  * @todo Remove direct access to nodes
6  */
7 
8 /*
9 Copyright (C) 2002-2013 UFO: Alien Invasion.
10 
11 This program is free software; you can redistribute it and/or
12 modify it under the terms of the GNU General Public License
13 as published by the Free Software Foundation; either version 2
14 of the License, or (at your option) any later version.
15 
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
19 
20 See the GNU General Public License for more details.
21 
22 You should have received a copy of the GNU General Public License
23 along with this program; if not, write to the Free Software
24 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
25 
26 */
27 
28 #include "../../cl_shared.h"
29 #include "../../cl_inventory.h"
30 #include "../../ui/ui_dataids.h"
31 #include "../../ui/node/ui_node_option.h" /* OPTIONEXTRADATA */
32 #include "../../../shared/parse.h"
33 #include "cp_campaign.h"
34 #include "cp_mapfightequip.h"
35 #include "cp_time.h"
36 
37 static cvar_t* mn_uppretext;
38 static cvar_t* mn_uppreavailable;
39 
40 static pediaChapter_t* upChaptersDisplayList[MAX_PEDIACHAPTERS];
41 static int numChaptersDisplayList;
42 
43 static technology_t	*upCurrentTech;
44 static pediaChapter_t* currentChapter;
45 
46 #define MAX_UPTEXT 4096
47 static char upBuffer[MAX_UPTEXT];
48 
49 /**
50  * @note don't change the order or you have to change the if statements about mn_updisplay cvar
51  * in menu_ufopedia.ufo, too
52  */
53 enum {
54 	UFOPEDIA_CHAPTERS,
55 	UFOPEDIA_INDEX,
56 	UFOPEDIA_ARTICLE,
57 
58 	UFOPEDIA_DISPLAYEND
59 };
60 static int upDisplay = UFOPEDIA_CHAPTERS;
61 
62 /**
63  * @brief Checks If a technology/UFOpaedia-entry will be displayed in the UFOpaedia (-list).
64  * @note This does not check for different display modes (only pre-research text, what statistics, etc...). The
65  * content is mostly checked in @c UP_Article
66  * @return true if the tech gets displayed at all, otherwise false.
67  * @sa UP_Article
68  */
UP_TechGetsDisplayed(const technology_t * tech)69 static bool UP_TechGetsDisplayed (const technology_t* tech)
70 {
71 	const objDef_t* item;
72 
73 	assert(tech);
74 	/* virtual items are hidden */
75 	item = INVSH_GetItemByIDSilent(tech->provides);
76 	if (item && item->isVirtual)
77 		return false;
78 	/* Is already researched OR has collected items OR (researchable AND have description)
79 	 * AND not a logical block AND not redirected */
80 	return (RS_IsResearched_ptr(tech) || RS_Collected_(tech)
81 	 || (tech->statusResearchable && tech->preDescription.numDescriptions > 0))
82 	 && tech->type != RS_LOGIC && !tech->redirect;
83 }
84 
85 /**
86  * @brief Modify the global display var
87  * @sa UP_SetMailHeader
88  */
UP_ChangeDisplay(int newDisplay)89 static void UP_ChangeDisplay (int newDisplay)
90 {
91 	if (newDisplay < UFOPEDIA_DISPLAYEND && newDisplay >= 0)
92 		upDisplay = newDisplay;
93 	else
94 		Com_Printf("Error in UP_ChangeDisplay (%i)\n", newDisplay);
95 
96 	cgi->Cvar_SetValue("mn_uppreavailable", 0);
97 
98 	/* make sure, that we leave the mail header space */
99 	cgi->UI_ResetData(TEXT_UFOPEDIA_MAILHEADER);
100 	cgi->UI_ResetData(TEXT_UFOPEDIA_MAIL);
101 	cgi->UI_ResetData(TEXT_UFOPEDIA_REQUIREMENT);
102 	cgi->UI_ResetData(TEXT_ITEMDESCRIPTION);
103 	cgi->UI_ResetData(TEXT_UFOPEDIA);
104 
105 	switch (upDisplay) {
106 	case UFOPEDIA_CHAPTERS:
107 		currentChapter = nullptr;
108 		upCurrentTech = nullptr;
109 		cgi->Cvar_Set("mn_upmodel_top", "");
110 		cgi->Cvar_Set("mn_upmodel_bottom", "");
111 		cgi->Cvar_Set("mn_upimage_top", "base/empty");
112 		cgi->UI_ExecuteConfunc("mn_up_empty");
113 		cgi->Cvar_Set("mn_uptitle", _("UFOpaedia"));
114 		break;
115 	case UFOPEDIA_INDEX:
116 		cgi->Cvar_Set("mn_upmodel_top", "");
117 		cgi->Cvar_Set("mn_upmodel_bottom", "");
118 		cgi->Cvar_Set("mn_upimage_top", "base/empty");
119 		/* no break here */
120 	case UFOPEDIA_ARTICLE:
121 		cgi->UI_ExecuteConfunc("mn_up_article");
122 		break;
123 	}
124 	cgi->Cvar_SetValue("mn_updisplay", upDisplay);
125 }
126 
127 /**
128  * @brief Translate a aircraft statistic integer to a translated string
129  * @sa aircraftParams_t
130  * @sa AIR_AircraftMenuStatsValues
131  */
UP_AircraftStatToName(int stat)132 static const char* UP_AircraftStatToName (int stat)
133 {
134 	switch (stat) {
135 	case AIR_STATS_SPEED:
136 		return _("Cruising speed");
137 	case AIR_STATS_MAXSPEED:
138 		return _("Maximum speed");
139 	case AIR_STATS_SHIELD:
140 		return _("Armour");
141 	case AIR_STATS_ECM:
142 		return _("ECM");
143 	case AIR_STATS_DAMAGE:
144 		return _("Aircraft damage");
145 	case AIR_STATS_ACCURACY:
146 		return _("Accuracy");
147 	case AIR_STATS_FUELSIZE:
148 		return _("Fuel size");
149 	case AIR_STATS_WRANGE:
150 		return _("Weapon range");
151 	default:
152 		return _("Unknown weapon skill");
153 	}
154 }
155 
156 /**
157  * @brief Displays the tech tree dependencies in the UFOpaedia
158  * @sa UP_Article
159  * @todo Add support for "requireAND"
160  * @todo re-iterate trough logic blocks (i.e. append the tech-names it references recursively)
161  */
UP_DisplayTechTree(const technology_t * t)162 static void UP_DisplayTechTree (const technology_t* t)
163 {
164 	linkedList_t* upTechtree;
165 	const requirements_t* required;
166 
167 	required = &t->requireAND;
168 	upTechtree = nullptr;
169 
170 	if (required->numLinks <= 0)
171 		cgi->LIST_AddString(&upTechtree, _("No requirements"));
172 	else {
173 		int i;
174 		for (i = 0; i < required->numLinks; i++) {
175 			const requirement_t* req = &required->links[i];
176 			if (req->type == RS_LINK_TECH) {
177 				const technology_t* techRequired = req->link.tech;
178 				if (!techRequired)
179 					cgi->Com_Error(ERR_DROP, "Could not find the tech for '%s'", req->id);
180 
181 				/** Only display tech if it is ok to do so.
182 				 * @todo If it is one (a logic tech) we may want to re-iterate from its requirements? */
183 				if (!UP_TechGetsDisplayed(techRequired))
184 					continue;
185 
186 				cgi->LIST_AddString(&upTechtree, _(techRequired->name));
187 			}
188 		}
189 	}
190 
191 	/* and now register the buffer */
192 	cgi->Cvar_Set("mn_uprequirement", "1");
193 	cgi->UI_RegisterLinkedListText(TEXT_UFOPEDIA_REQUIREMENT, upTechtree);
194 }
195 
196 /**
197  * @brief Prints the UFOpaedia description for buildings
198  * @sa UP_Article
199  */
UP_BuildingDescription(const technology_t * t)200 static void UP_BuildingDescription (const technology_t* t)
201 {
202 	const building_t* b = B_GetBuildingTemplate(t->provides);
203 
204 	if (!b) {
205 		Com_sprintf(upBuffer, sizeof(upBuffer), _("Error - could not find building"));
206 	} else {
207 		Com_sprintf(upBuffer, sizeof(upBuffer), _("Needs:\t%s\n"), b->dependsBuilding ? _(b->dependsBuilding->name) : _("None"));
208 		Q_strcat(upBuffer, sizeof(upBuffer), ngettext("Construction time:\t%i day\n", "Construction time:\t%i days\n", b->buildTime), b->buildTime);
209 		Q_strcat(upBuffer, sizeof(upBuffer), _("Cost:\t%i c\n"), b->fixCosts);
210 		Q_strcat(upBuffer, sizeof(upBuffer), _("Running costs:\t%i c\n"), b->varCosts);
211 	}
212 
213 	cgi->Cvar_Set("mn_upmetadata", "1");
214 	cgi->UI_RegisterText(TEXT_ITEMDESCRIPTION, upBuffer);
215 	UP_DisplayTechTree(t);
216 }
217 
218 /**
219  * @brief Prints the (UFOpaedia and other) description for aircraft items
220  * @param item The object definition of the item
221  * @sa UP_Article
222  * Not only called from UFOpaedia but also from other places to display
223  * @todo Don't display things like speed for base defence items - a missile
224  * facility isn't getting slower or faster due a special weapon or ammunition
225  */
UP_AircraftItemDescription(const objDef_t * item)226 void UP_AircraftItemDescription (const objDef_t* item)
227 {
228 	static char itemText[1024];
229 	const technology_t* tech;
230 
231 	/* Set menu text node content to null. */
232 	cgi->INV_ItemDescription(nullptr);
233 	*itemText = '\0';
234 
235 	/* no valid item id given */
236 	if (!item) {
237 		cgi->Cvar_Set("mn_item", "");
238 		cgi->Cvar_Set("mn_itemname", "");
239 		cgi->Cvar_Set("mn_upmodel_top", "");
240 		cgi->UI_ResetData(TEXT_ITEMDESCRIPTION);
241 		return;
242 	}
243 
244 	tech = RS_GetTechForItem(item);
245 	/* select item */
246 	cgi->Cvar_Set("mn_item", "%s", item->id);
247 	cgi->Cvar_Set("mn_itemname", "%s", _(item->name));
248 	if (tech->mdl)
249 		cgi->Cvar_Set("mn_upmodel_top", "%s", tech->mdl);
250 	else
251 		cgi->Cvar_Set("mn_upmodel_top", "");
252 
253 	/* set description text */
254 	if (RS_IsResearched_ptr(tech)) {
255 		int i;
256 		const objDef_t* ammo = nullptr;
257 
258 		switch (item->craftitem.type) {
259 		case AC_ITEM_WEAPON:
260 				Q_strcat(itemText, sizeof(itemText), _("Weight:\t%s\n"), AII_WeightToName(AII_GetItemWeightBySize(item)));
261 				break;
262 		case AC_ITEM_BASE_MISSILE:
263 		case AC_ITEM_BASE_LASER:
264 				Q_strcat(itemText, sizeof(itemText), _("Weapon for base defence system\n"));
265 				break;
266 		case AC_ITEM_AMMO:
267 				ammo = item;
268 				break;
269 		default:
270 				break;
271 		}
272 
273 		/* check ammo of weapons */
274 		if (item->craftitem.type <= AC_ITEM_WEAPON) {
275 			for(int i = 0; i < item->numAmmos; i++)
276 				if (item->ammos[i]->isVirtual) {
277 					ammo = item->ammos[i];
278 					break;
279 				}
280 		}
281 
282 		if (ammo) {
283 			/* We display the characteristics of this ammo */
284 			Q_strcat(itemText, sizeof(itemText), _("Ammo:\t%i\n"), ammo->ammo);
285 			if (!EQUAL(ammo->craftitem.weaponDamage, 0))
286 				Q_strcat(itemText, sizeof(itemText), _("Damage:\t%i\n"), (int) ammo->craftitem.weaponDamage);
287 			Q_strcat(itemText, sizeof(itemText), _("Reloading time:\t%i\n"),  (int) ammo->craftitem.weaponDelay);
288 		}
289 		/* We write the range of the weapon */
290 		if (!EQUAL(item->craftitem.stats[AIR_STATS_WRANGE], 0))
291 			Q_strcat(itemText, sizeof(itemText), "%s:\t%i\n", UP_AircraftStatToName(AIR_STATS_WRANGE),
292 				AIR_AircraftMenuStatsValues(item->craftitem.stats[AIR_STATS_WRANGE], AIR_STATS_WRANGE));
293 
294 		/* we scan all stats except weapon range */
295 		for (i = 0; i < AIR_STATS_MAX; i++) {
296 			const char* statsName = UP_AircraftStatToName(i);
297 			if (i == AIR_STATS_WRANGE)
298 				continue;
299 			if (item->craftitem.stats[i] > 2.0f)
300 				Q_strcat(itemText, sizeof(itemText), "%s:\t+%i\n", statsName, AIR_AircraftMenuStatsValues(item->craftitem.stats[i], i));
301 			else if (item->craftitem.stats[i] < -2.0f)
302 				Q_strcat(itemText, sizeof(itemText), "%s:\t%i\n", statsName, AIR_AircraftMenuStatsValues(item->craftitem.stats[i], i));
303 			else if (item->craftitem.stats[i] > 1.0f)
304 				Q_strcat(itemText, sizeof(itemText), _("%s:\t+%i %%\n"), statsName, (int)(item->craftitem.stats[i] * 100) - 100);
305 			else if (!EQUAL(item->craftitem.stats[i], 0))
306 				Q_strcat(itemText, sizeof(itemText), _("%s:\t%i %%\n"), statsName, (int)(item->craftitem.stats[i] * 100) - 100);
307 		}
308 	} else {
309 		Q_strcat(itemText, sizeof(itemText), _("Unknown - need to research this"));
310 	}
311 
312 	cgi->Cvar_Set("mn_upmetadata", "1");
313 	cgi->UI_RegisterText(TEXT_ITEMDESCRIPTION, itemText);
314 }
315 
316 /**
317  * @brief Prints the UFOpaedia description for aircraft
318  * @note Also checks whether the aircraft tech is already researched or collected
319  * @sa BS_MarketAircraftDescription
320  * @sa UP_Article
321  */
UP_AircraftDescription(const technology_t * tech)322 void UP_AircraftDescription (const technology_t* tech)
323 {
324 	cgi->INV_ItemDescription(nullptr);
325 
326 	/* ensure that the buffer is emptied in every case */
327 	upBuffer[0] = '\0';
328 
329 	if (RS_IsResearched_ptr(tech)) {
330 		const aircraft_t* aircraft = AIR_GetAircraft(tech->provides);
331 		int i;
332 		for (i = 0; i < AIR_STATS_MAX; i++) {
333 			switch (i) {
334 			case AIR_STATS_SPEED:
335 				/* speed may be converted to km/h : multiply by pi / 180 * earth_radius */
336 				Q_strcat(upBuffer, sizeof(upBuffer), _("%s:\t%i km/h\n"), UP_AircraftStatToName(i),
337 					AIR_AircraftMenuStatsValues(aircraft->stats[i], i));
338 				break;
339 			case AIR_STATS_MAXSPEED:
340 				/* speed may be converted to km/h : multiply by pi / 180 * earth_radius */
341 				Q_strcat(upBuffer, sizeof(upBuffer), _("%s:\t%i km/h\n"), UP_AircraftStatToName(i),
342 					AIR_AircraftMenuStatsValues(aircraft->stats[i], i));
343 				break;
344 			case AIR_STATS_FUELSIZE:
345 				Q_strcat(upBuffer, sizeof(upBuffer), _("Operational range:\t%i km\n"),
346 					AIR_GetOperationRange(aircraft));
347 				break;
348 			case AIR_STATS_ACCURACY:
349 				Q_strcat(upBuffer, sizeof(upBuffer), _("%s:\t%i\n"), UP_AircraftStatToName(i),
350 					AIR_AircraftMenuStatsValues(aircraft->stats[i], i));
351 				break;
352 			default:
353 				break;
354 			}
355 		}
356 
357 		const baseCapacities_t cap = AIR_GetCapacityByAircraftWeight(aircraft);
358 		const buildingType_t buildingType = B_GetBuildingTypeByCapacity(cap);
359 		const building_t* building = B_GetBuildingTemplateByType(buildingType);
360 
361 		Q_strcat(upBuffer, sizeof(upBuffer), _("Required Hangar:\t%s\n"), _(building->name));
362 		/* @note: while MAX_ACTIVETEAM limits the number of soldiers on a craft
363 		 * there is no use to show this in case of an UFO (would be misleading): */
364 		if (!AIR_IsUFO(aircraft))
365 			Q_strcat(upBuffer, sizeof(upBuffer), _("Max. soldiers:\t%i\n"), aircraft->maxTeamSize);
366 	} else if (RS_Collected_(tech)) {
367 		/** @todo Display crippled info and pre-research text here */
368 		Com_sprintf(upBuffer, sizeof(upBuffer), _("Unknown - need to research this"));
369 	} else {
370 		Com_sprintf(upBuffer, sizeof(upBuffer), _("Unknown - need to research this"));
371 	}
372 
373 	cgi->Cvar_Set("mn_upmetadata", "1");
374 	cgi->UI_RegisterText(TEXT_ITEMDESCRIPTION, upBuffer);
375 	UP_DisplayTechTree(tech);
376 }
377 
378 /**
379  * @brief Prints the description for robots/ugvs.
380  * @param[in] ugvType What type of robot/ugv to print the description for.
381  * @sa BS_MarketClick_f
382  * @sa UP_Article
383  */
UP_UGVDescription(const ugv_t * ugvType)384 void UP_UGVDescription (const ugv_t* ugvType)
385 {
386 	static char itemText[512];
387 	const technology_t* tech;
388 
389 	assert(ugvType);
390 
391 	tech = RS_GetTechByProvided(ugvType->id);
392 	assert(tech);
393 
394 	cgi->INV_ItemDescription(nullptr);
395 
396 	/* Set name of ugv/robot */
397 	cgi->Cvar_Set("mn_itemname", "%s", _(tech->name));
398 	cgi->Cvar_Set("mn_item", "%s", tech->provides);
399 
400 	cgi->Cvar_Set("mn_upmetadata", "1");
401 	if (RS_IsResearched_ptr(tech)) {
402 		/** @todo make me shiny */
403 		Com_sprintf(itemText, sizeof(itemText), _("%s\n%s"), _(tech->name), ugvType->weapon);
404 	} else if (RS_Collected_(tech)) {
405 		/** @todo Display crippled info and pre-research text here */
406 		Com_sprintf(itemText, sizeof(itemText), _("Unknown - need to research this"));
407 	} else {
408 		Com_sprintf(itemText, sizeof(itemText), _("Unknown - need to research this"));
409 	}
410 	cgi->UI_RegisterText(TEXT_ITEMDESCRIPTION, itemText);
411 }
412 
413 /**
414  * @brief Sets the amount of unread/new mails
415  * @note This is called every campaign frame - to update ccs.numUnreadMails
416  * just set it to -1 before calling this function
417  * @sa CP_CampaignRun
418  */
UP_GetUnreadMails(void)419 int UP_GetUnreadMails (void)
420 {
421 	const uiMessageListNodeMessage_t* m = cgi->UI_MessageGetStack();
422 
423 	if (ccs.numUnreadMails != -1)
424 		return ccs.numUnreadMails;
425 
426 	ccs.numUnreadMails = 0;
427 
428 	while (m) {
429 		switch (m->type) {
430 		case MSG_RESEARCH_PROPOSAL:
431 			assert(m->pedia);
432 			if (m->pedia->mail[TECHMAIL_PRE].from && !m->pedia->mail[TECHMAIL_PRE].read)
433 				ccs.numUnreadMails++;
434 			break;
435 		case MSG_RESEARCH_FINISHED:
436 			assert(m->pedia);
437 			if (m->pedia->mail[TECHMAIL_RESEARCHED].from && RS_IsResearched_ptr(m->pedia) && !m->pedia->mail[TECHMAIL_RESEARCHED].read)
438 				ccs.numUnreadMails++;
439 			break;
440 		case MSG_NEWS:
441 			assert(m->pedia);
442 			if (m->pedia->mail[TECHMAIL_PRE].from && !m->pedia->mail[TECHMAIL_PRE].read)
443 				ccs.numUnreadMails++;
444 			if (m->pedia->mail[TECHMAIL_RESEARCHED].from && !m->pedia->mail[TECHMAIL_RESEARCHED].read)
445 				ccs.numUnreadMails++;
446 			break;
447 		case MSG_EVENT:
448 			assert(m->eventMail);
449 			if (!m->eventMail->read)
450 				ccs.numUnreadMails++;
451 			break;
452 		default:
453 			break;
454 		}
455 		m = m->next;
456 	}
457 
458 	/* use strings here */
459 	cgi->Cvar_Set("mn_upunreadmail", "%i", ccs.numUnreadMails);
460 	return ccs.numUnreadMails;
461 }
462 
463 /**
464  * @brief Binds the mail header (if needed) to the mn.menuText array.
465  * @note if there is a mail header.
466  * @param[in] tech The tech to generate a header for.
467  * @param[in] type The type of mail (research proposal or finished research)
468  * @param[in] mail The mail descriptor structure
469  * @sa UP_ChangeDisplay
470  * @sa CL_EventAddMail_f
471  * @sa CL_GetEventMail
472  */
UP_SetMailHeader(technology_t * tech,techMailType_t type,eventMail_t * mail)473 static void UP_SetMailHeader (technology_t* tech, techMailType_t type, eventMail_t* mail)
474 {
475 	static char mailHeader[8 * MAX_VAR] = ""; /* bigger as techMail_t (utf8) */
476 	char dateBuf[MAX_VAR] = "";
477 	const char* subjectType = "";
478 	const char* from, *to, *subject, *model;
479 	dateLong_t date;
480 
481 	if (mail) {
482 		from = mail->from;
483 		to = mail->to;
484 		model = mail->model;
485 		subject = mail->subject;
486 		Q_strncpyz(dateBuf, _(mail->date), sizeof(dateBuf));
487 		mail->read = true;
488 		/* reread the unread mails in UP_GetUnreadMails */
489 		ccs.numUnreadMails = -1;
490 	} else {
491 		techMail_t* mail;
492 		assert(tech);
493 		assert(type < TECHMAIL_MAX);
494 
495 		mail = &tech->mail[type];
496 		from = mail->from;
497 		to = mail->to;
498 		subject = mail->subject;
499 		model = mail->model;
500 
501 		if (mail->date) {
502 			Q_strncpyz(dateBuf, _(mail->date), sizeof(dateBuf));
503 		} else {
504 			switch (type) {
505 			case TECHMAIL_PRE:
506 				CP_DateConvertLong(&tech->preResearchedDate, &date);
507 				Com_sprintf(dateBuf, sizeof(dateBuf), _("%i %s %02i"),
508 					date.year, Date_GetMonthName(date.month - 1), date.day);
509 				break;
510 			case TECHMAIL_RESEARCHED:
511 				CP_DateConvertLong(&tech->researchedDate, &date);
512 				Com_sprintf(dateBuf, sizeof(dateBuf), _("%i %s %02i"),
513 					date.year, Date_GetMonthName(date.month - 1), date.day);
514 				break;
515 			default:
516 				cgi->Com_Error(ERR_DROP, "UP_SetMailHeader: unhandled techMailType_t %i for date.", type);
517 			}
518 		}
519 		if (from != nullptr) {
520 			if (!mail->read) {
521 				mail->read = true;
522 				/* reread the unread mails in UP_GetUnreadMails */
523 				ccs.numUnreadMails = -1;
524 			}
525 			/* only if mail and mail_pre are available */
526 			if (tech->numTechMails == TECHMAIL_MAX) {
527 				switch (type) {
528 				case TECHMAIL_PRE:
529 					subjectType = _("Proposal: ");
530 					break;
531 				case TECHMAIL_RESEARCHED:
532 					subjectType = _("Re: ");
533 					break;
534 				default:
535 					cgi->Com_Error(ERR_DROP, "UP_SetMailHeader: unhandled techMailType_t %i for subject.", type);
536 				}
537 			}
538 		} else {
539 			cgi->UI_ResetData(TEXT_UFOPEDIA_MAILHEADER);
540 			return;
541 		}
542 	}
543 	from = cgi->CL_Translate(from);
544 	to = cgi->CL_Translate(to);
545 	subject = cgi->CL_Translate(subject);
546 	Com_sprintf(mailHeader, sizeof(mailHeader), _("FROM: %s\nTO: %s\nDATE: %s"), from, to, dateBuf);
547 	cgi->Cvar_Set("mn_mail_sender_head", "%s", model ? model : "");
548 	cgi->Cvar_Set("mn_mail_from", "%s", from);
549 	cgi->Cvar_Set("mn_mail_subject", "%s%s", subjectType, _(subject));
550 	cgi->Cvar_Set("mn_mail_to", "%s", to);
551 	cgi->Cvar_Set("mn_mail_date", "%s", dateBuf);
552 	cgi->UI_RegisterText(TEXT_UFOPEDIA_MAILHEADER, mailHeader);
553 }
554 
555 /**
556  * @brief Set the ammo model to display to selected ammo (only for a reloadable weapon)
557  * @param tech technology_t pointer for the weapon's tech
558  * @sa UP_Article
559  */
UP_DrawAssociatedAmmo(const technology_t * tech)560 static void UP_DrawAssociatedAmmo (const technology_t* tech)
561 {
562 	const objDef_t* od = INVSH_GetItemByID(tech->provides);
563 	/* If this is a weapon, we display the model of the associated ammunition in the lower right */
564 	if (od->numAmmos > 0) {
565 		const technology_t* associated = RS_GetTechForItem(od->ammos[0]);
566 		cgi->Cvar_Set("mn_upmodel_bottom", "%s", associated->mdl);
567 	}
568 }
569 
570 /**
571  * @brief Display only the TEXT_UFOPEDIA part for a given technology
572  * @param[in] tech The technology_t pointer to print the UFOpaedia article for
573  * @param[in] mail The mail parameters in case we produce a mail
574  * @sa UP_Article
575  */
UP_Article(technology_t * tech,eventMail_t * mail)576 static void UP_Article (technology_t* tech, eventMail_t* mail)
577 {
578 	UP_ChangeDisplay(UFOPEDIA_ARTICLE);
579 
580 	if (tech) {
581 		if (tech->mdl)
582 			cgi->Cvar_Set("mn_upmodel_top", "%s", tech->mdl);
583 		else
584 			cgi->Cvar_Set("mn_upmodel_top", "");
585 
586 		if (tech->image)
587 			cgi->Cvar_Set("mn_upimage_top", "%s", tech->image);
588 		else
589 			cgi->Cvar_Set("mn_upimage_top", "");
590 
591 		cgi->Cvar_Set("mn_upmodel_bottom", "");
592 
593 		if (tech->type == RS_WEAPON)
594 			UP_DrawAssociatedAmmo(tech);
595 		cgi->Cvar_Set("mn_uprequirement", "");
596 		cgi->Cvar_Set("mn_upmetadata", "");
597 	}
598 
599 	cgi->UI_ResetData(TEXT_UFOPEDIA);
600 	cgi->UI_ResetData(TEXT_UFOPEDIA_REQUIREMENT);
601 
602 	if (mail) {
603 		/* event mail */
604 		cgi->Cvar_SetValue("mn_uppreavailable", 0);
605 		cgi->Cvar_SetValue("mn_updisplay", UFOPEDIA_CHAPTERS);
606 		UP_SetMailHeader(nullptr, TECHMAIL_PRE, mail);
607 		cgi->UI_RegisterText(TEXT_UFOPEDIA, _(mail->body));
608 		/* This allows us to use the index button in the UFOpaedia,
609 		 * eventMails don't have any chapter to go back to. */
610 		upDisplay = UFOPEDIA_INDEX;
611 	} else if (tech) {
612 		currentChapter = tech->upChapter;
613 		upCurrentTech = tech;
614 
615 		/* Reset itemdescription */
616 		cgi->UI_ExecuteConfunc("itemdesc_view 0 0;");
617 		if (RS_IsResearched_ptr(tech)) {
618 			int i;
619 			cgi->Cvar_Set("mn_uptitle", _("UFOpaedia: %s (complete)"), _(tech->name));
620 			/* If researched -> display research text */
621 			cgi->UI_RegisterText(TEXT_UFOPEDIA, _(RS_GetDescription(&tech->description)));
622 			if (tech->preDescription.numDescriptions > 0) {
623 				/* Display pre-research text and the buttons if a pre-research text is available. */
624 				if (mn_uppretext->integer) {
625 					cgi->UI_RegisterText(TEXT_UFOPEDIA, _(RS_GetDescription(&tech->preDescription)));
626 					UP_SetMailHeader(tech, TECHMAIL_PRE, nullptr);
627 				} else {
628 					UP_SetMailHeader(tech, TECHMAIL_RESEARCHED, nullptr);
629 				}
630 				cgi->Cvar_SetValue("mn_uppreavailable", 1);
631 			} else {
632 				/* Do not display the pre-research-text button if none is available (no need to even bother clicking there). */
633 				cgi->Cvar_SetValue("mn_uppreavailable", 0);
634 				cgi->Cvar_SetValue("mn_updisplay", UFOPEDIA_CHAPTERS);
635 				UP_SetMailHeader(tech, TECHMAIL_RESEARCHED, nullptr);
636 			}
637 
638 			switch (tech->type) {
639 			case RS_ARMOUR:
640 			case RS_WEAPON:
641 				for (i = 0; i < cgi->csi->numODs; i++) {
642 					const objDef_t* od = INVSH_GetItemByIDX(i);
643 					if (Q_streq(tech->provides, od->id)) {
644 						cgi->INV_ItemDescription(od);
645 						UP_DisplayTechTree(tech);
646 						cgi->Cvar_Set("mn_upmetadata", "1");
647 						break;
648 					}
649 				}
650 				break;
651 			case RS_TECH:
652 				UP_DisplayTechTree(tech);
653 				break;
654 			case RS_CRAFT:
655 				UP_AircraftDescription(tech);
656 				break;
657 			case RS_CRAFTITEM:
658 				UP_AircraftItemDescription(INVSH_GetItemByID(tech->provides));
659 				break;
660 			case RS_BUILDING:
661 				UP_BuildingDescription(tech);
662 				break;
663 			case RS_UGV:
664 				UP_UGVDescription(cgi->Com_GetUGVByIDSilent(tech->provides));
665 				break;
666 			default:
667 				break;
668 			}
669 		/* see also UP_TechGetsDisplayed */
670 		} else if (RS_Collected_(tech) || (tech->statusResearchable && tech->preDescription.numDescriptions > 0)) {
671 			/* This tech has something collected or has a research proposal. (i.e. pre-research text) */
672 			cgi->Cvar_Set("mn_uptitle", _("UFOpaedia: %s"), _(tech->name));
673 			/* Not researched but some items collected -> display pre-research text if available. */
674 			if (tech->preDescription.numDescriptions > 0) {
675 				cgi->UI_RegisterText(TEXT_UFOPEDIA, _(RS_GetDescription(&tech->preDescription)));
676 				UP_SetMailHeader(tech, TECHMAIL_PRE, nullptr);
677 			} else {
678 				cgi->UI_RegisterText(TEXT_UFOPEDIA, _("No pre-research description available."));
679 			}
680 		} else {
681 			cgi->Cvar_Set("mn_uptitle", _("UFOpaedia: %s"), _(tech->name));
682 			cgi->UI_ResetData(TEXT_UFOPEDIA);
683 		}
684 	} else {
685 		cgi->Com_Error(ERR_DROP, "UP_Article: No mail or tech given");
686 	}
687 }
688 
689 /**
690  * @sa CL_EventAddMail_f
691  */
UP_OpenEventMail(const char * eventMailID)692 void UP_OpenEventMail (const char* eventMailID)
693 {
694 	eventMail_t* mail;
695 	mail = CL_GetEventMail(eventMailID);
696 	if (!mail)
697 		return;
698 
699 	cgi->UI_PushWindow("mail");
700 	UP_Article(nullptr, mail);
701 }
702 
703 /**
704  * @brief Opens the mail view from everywhere with the entry given through name
705  * @param techID mail entry id (technology script id)
706  * @sa UP_FindEntry_f
707  */
UP_OpenMailWith(const char * techID)708 static void UP_OpenMailWith (const char* techID)
709 {
710 	if (!techID)
711 		return;
712 
713 	cgi->UI_PushWindow("mail");
714 	cgi->Cbuf_AddText("ufopedia %s\n", techID);
715 }
716 
717 /**
718  * @brief Opens the UFOpaedia from everywhere with the entry given through name
719  * @param techID UFOpaedia entry id (technology script id)
720  * @sa UP_FindEntry_f
721  */
UP_OpenWith(const char * techID)722 void UP_OpenWith (const char* techID)
723 {
724 	if (!techID)
725 		return;
726 
727 	cgi->UI_PushWindow("ufopedia");
728 	cgi->Cbuf_AddText("ufopedia %s\nupdate_ufopedia_layout\n", techID);
729 }
730 
731 /**
732  * @brief Opens the UFOpaedia with the entry given through name, not deleting copies
733  * @param techID UFOpaedia entry id (technology script id)
734  * @sa UP_FindEntry_f
735  */
UP_OpenCopyWith(const char * techID)736 void UP_OpenCopyWith (const char* techID)
737 {
738 	cgi->Cmd_ExecuteString("ui_push ufopedia");
739 	cgi->Cbuf_AddText("ufopedia %s\n", techID);
740 }
741 
742 
743 /**
744  * @brief Search and open the UFOpaedia with given id
745  */
UP_FindEntry_f(void)746 static void UP_FindEntry_f (void)
747 {
748 	const char* id;
749 	technology_t* tech;
750 
751 	if (cgi->Cmd_Argc() < 2) {
752 		Com_Printf("Usage: %s <id>\n", cgi->Cmd_Argv(0));
753 		return;
754 	}
755 
756 	/* what are we searching for? */
757 	id = cgi->Cmd_Argv(1);
758 
759 	/* maybe we get a call like 'ufopedia ""' */
760 	if (id[0] == '\0') {
761 		Com_Printf("UP_FindEntry_f: No UFOpaedia entry given as parameter\n");
762 		return;
763 	}
764 
765 	tech = RS_GetTechByID(id);
766 	if (!tech) {
767 		Com_DPrintf(DEBUG_CLIENT, "UP_FindEntry_f: No UFOpaedia entry found for %s\n", id);
768 		return;
769 	}
770 
771 	if (tech->redirect)
772 		tech = tech->redirect;
773 
774 	UP_Article(tech, nullptr);
775 }
776 
777 /**
778  * @brief Generate a list of options for all allowed articles of a chapter
779  * @param[in] parentChapter requested chapter
780  * @return The first option of the list, else nullptr if no articles
781  */
UP_GenerateArticlesSummary(pediaChapter_t * parentChapter)782 static uiNode_t* UP_GenerateArticlesSummary (pediaChapter_t* parentChapter)
783 {
784 	technology_t* tech = parentChapter->first;
785 	uiNode_t* first = nullptr;
786 
787 	while (tech) {
788 		if (UP_TechGetsDisplayed(tech)) {
789 			const char* id = va("@%i", tech->idx);
790 			cgi->UI_AddOption(&first, id, va("_%s", tech->name), id);
791 		}
792 		tech = tech->upNext;
793 	}
794 
795 	cgi->UI_SortOptions(&first);
796 
797 	return first;
798 }
799 
800 /**
801  * @brief Generate a tree of option for all allowed chapters and articles
802  * @note it update OPTION_UFOPEDIA
803  */
UP_GenerateSummary(void)804 static void UP_GenerateSummary (void)
805 {
806 	int i;
807 	uiNode_t* chapters = nullptr;
808 	int num = 0;
809 
810 	numChaptersDisplayList = 0;
811 
812 	for (i = 0; i < ccs.numChapters; i++) {
813 		/* hide chapters without name */
814 		pediaChapter_t* chapter = &ccs.upChapters[i];
815 		if (chapter->name == nullptr)
816 			continue;
817 
818 		/* Check if there are any researched or collected items in this chapter ... */
819 		bool researchedEntries = false;
820 		upCurrentTech = chapter->first;
821 		while (upCurrentTech) {
822 			if (UP_TechGetsDisplayed(upCurrentTech)) {
823 				researchedEntries = true;
824 				break;
825 			}
826 			upCurrentTech = upCurrentTech->upNext;
827 		}
828 
829 		/* .. and if so add them to the displaylist of chapters. */
830 		if (researchedEntries) {
831 			uiNode_t* chapterOption;
832 			if (numChaptersDisplayList >= sizeof(upChaptersDisplayList))
833 				cgi->Com_Error(ERR_DROP, "MAX_PEDIACHAPTERS hit");
834 			upChaptersDisplayList[numChaptersDisplayList++] = chapter;
835 
836 			/* chapter section*/
837 			chapterOption = cgi->UI_AddOption(&chapters, chapter->id, va("_%s", chapter->name), va("%i", num));
838 			/** @todo use a confunc */
839 			OPTIONEXTRADATA(chapterOption).icon = cgi->UI_GetSpriteByName(va("icons/ufopedia_%s", chapter->id));
840 			chapterOption->firstChild = UP_GenerateArticlesSummary(chapter);
841 
842 			num++;
843 		}
844 	}
845 
846 	cgi->UI_RegisterOption(OPTION_UFOPEDIA, chapters);
847 	cgi->Cvar_Set("mn_uptitle", _("UFOpaedia"));
848 }
849 
850 /**
851  * @brief Displays the chapters in the UFOpaedia
852  */
UP_Content_f(void)853 static void UP_Content_f (void)
854 {
855 	UP_GenerateSummary();
856 	UP_ChangeDisplay(UFOPEDIA_CHAPTERS);
857 }
858 
859 /**
860  * @brief Callback when we click on the ufopedia summary
861  * @note when we click on a chapter, param=chapterId,
862  * when we click on an article, param='@'+techIdx
863  */
UP_Click_f(void)864 static void UP_Click_f (void)
865 {
866 	if (cgi->Cmd_Argc() < 2)
867 		return;
868 
869 	/* article index starts with a @ */
870 	if (cgi->Cmd_Argv(1)[0] == '@') {
871 		const int techId = atoi(cgi->Cmd_Argv(1) + 1);
872 		technology_t* tech;
873 		assert(techId >= 0);
874 		assert(techId < ccs.numTechnologies);
875 		tech = &ccs.technologies[techId];
876 		if (tech)
877 			UP_Article(tech, nullptr);
878 		return;
879 	} else {
880 		/* Reset itemdescription */
881 		cgi->UI_ExecuteConfunc("itemdesc_view 0 0;");
882 	}
883 
884 	/* it clean up the display */
885 	UP_ChangeDisplay(UFOPEDIA_CHAPTERS);
886 }
887 
888 /**
889  * @todo The "num" value and the link-index will most probably not match.
890  */
UP_TechTreeClick_f(void)891 static void UP_TechTreeClick_f (void)
892 {
893 	int num;
894 	int i;
895 	const requirements_t* required_AND;
896 	const technology_t* techRequired;
897 
898 	if (cgi->Cmd_Argc() < 2)
899 		return;
900 	num = atoi(cgi->Cmd_Argv(1));
901 
902 	if (!upCurrentTech)
903 		return;
904 
905 	required_AND = &upCurrentTech->requireAND;
906 	if (num < 0 || num >= required_AND->numLinks)
907 		return;
908 
909 	/* skip every tech which have not been displayed in techtree */
910 	for (i = 0; i <= num; i++) {
911 		const requirement_t* r = &required_AND->links[i];
912 		if (r->type != RS_LINK_TECH && r->type != RS_LINK_TECH_NOT)
913 			num++;
914 	}
915 
916 	techRequired = required_AND->links[num].link.tech;
917 	if (!techRequired)
918 		cgi->Com_Error(ERR_DROP, "Could not find the tech for '%s'", required_AND->links[num].id);
919 
920 	/* maybe there is no UFOpaedia chapter assigned - this tech should not be opened at all */
921 	if (!techRequired->upChapter)
922 		return;
923 
924 	UP_OpenWith(techRequired->id);
925 }
926 
927 /**
928  * @brief Redraw the UFOpaedia article
929  */
UP_Update_f(void)930 static void UP_Update_f (void)
931 {
932 	if (upCurrentTech)
933 		UP_Article(upCurrentTech, nullptr);
934 }
935 
936 /**
937  * @brief Mailclient click function callback
938  * @sa UP_OpenMail_f
939  */
UP_MailClientClick_f(void)940 static void UP_MailClientClick_f (void)
941 {
942 	if (cgi->Cmd_Argc() < 2)
943 		return;
944 
945 	int cnt = -1;
946 	const int num = atoi(cgi->Cmd_Argv(1));
947 
948 	uiMessageListNodeMessage_t* m = cgi->UI_MessageGetStack();
949 	while (m) {
950 		switch (m->type) {
951 		case MSG_RESEARCH_PROPOSAL:
952 			if (!m->pedia->mail[TECHMAIL_PRE].from)
953 				break;
954 			cnt++;
955 			if (cnt == num) {
956 				cgi->Cvar_SetValue("mn_uppretext", 1);
957 				UP_OpenMailWith(m->pedia->id);
958 				return;
959 			}
960 			break;
961 		case MSG_RESEARCH_FINISHED:
962 			if (!m->pedia->mail[TECHMAIL_RESEARCHED].from)
963 				break;
964 			cnt++;
965 			if (cnt == num) {
966 				cgi->Cvar_SetValue("mn_uppretext", 0);
967 				UP_OpenMailWith(m->pedia->id);
968 				return;
969 			}
970 			break;
971 		case MSG_NEWS:
972 			if (m->pedia->mail[TECHMAIL_PRE].from || m->pedia->mail[TECHMAIL_RESEARCHED].from) {
973 				cnt++;
974 				if (cnt >= num) {
975 					UP_OpenMailWith(m->pedia->id);
976 					return;
977 				}
978 			}
979 			break;
980 		case MSG_EVENT:
981 			cnt++;
982 			if (cnt >= num) {
983 				UP_OpenEventMail(m->eventMail->id);
984 				return;
985 			}
986 			break;
987 		default:
988 			break;
989 		}
990 		m = m->next;
991 	}
992 	ccs.numUnreadMails = -1;
993 	UP_GetUnreadMails();
994 }
995 
996 /**
997  * @brief Change UFOpaedia article when clicking on the name of associated ammo or weapon
998  */
UP_ResearchedLinkClick_f(void)999 static void UP_ResearchedLinkClick_f (void)
1000 {
1001 	const objDef_t* od;
1002 
1003 	if (!upCurrentTech) /* if called from console */
1004 		return;
1005 
1006 	od = INVSH_GetItemByID(upCurrentTech->provides);
1007 	assert(od);
1008 
1009 	if (od->isAmmo()) {
1010 		const technology_t* t = RS_GetTechForItem(od->weapons[0]);
1011 		if (UP_TechGetsDisplayed(t))
1012 			UP_OpenWith(t->id);
1013 	} else if (od->weapon && od->isReloadable()) {
1014 		const technology_t* t = RS_GetTechForItem(od->ammos[0]);
1015 		if (UP_TechGetsDisplayed(t))
1016 			UP_OpenWith(t->id);
1017 	}
1018 }
1019 
1020 /**
1021  * @brief Start the mailclient
1022  * @sa UP_MailClientClick_f
1023  * @sa CP_GetUnreadMails
1024  * @sa CL_EventAddMail_f
1025  * @sa MS_AddNewMessage
1026  */
UP_OpenMail_f(void)1027 static void UP_OpenMail_f (void)
1028 {
1029 	cgi->UI_ExecuteConfunc("clear_mails");
1030 	int idx = 0;
1031 	for (const uiMessageListNodeMessage_t* m = cgi->UI_MessageGetStack(); m; m = m->next) {
1032 		dateLong_t date;
1033 		char headline[256] = "";
1034 		char dateBuf[64] = "";
1035 		const char* icon;
1036 		bool read;
1037 		switch (m->type) {
1038 		case MSG_RESEARCH_PROPOSAL: {
1039 			const techMail_t& mail = m->pedia->mail[TECHMAIL_PRE];
1040 			if (!mail.from)
1041 				continue;
1042 			CP_DateConvertLong(&m->pedia->preResearchedDate, &date);
1043 			Com_sprintf(headline, sizeof(headline), _("Proposal: %s"), _(m->pedia->mail[TECHMAIL_PRE].subject));
1044 			Com_sprintf(dateBuf, sizeof(dateBuf), _("%i %s %02i"), date.year, Date_GetMonthName(date.month - 1), date.day);
1045 			icon = mail.icon;
1046 			read = mail.read;
1047 			break;
1048 		}
1049 		case MSG_RESEARCH_FINISHED: {
1050 			const techMail_t& mail = m->pedia->mail[TECHMAIL_RESEARCHED];
1051 			if (!mail.from)
1052 				continue;
1053 			CP_DateConvertLong(&m->pedia->researchedDate, &date);
1054 			Com_sprintf(headline, sizeof(headline), _("Re: %s"), _(m->pedia->mail[TECHMAIL_PRE].subject));
1055 			Com_sprintf(dateBuf, sizeof(dateBuf), _("%i %s %02i"), date.year, Date_GetMonthName(date.month - 1), date.day);
1056 			icon = mail.icon;
1057 			read = mail.read;
1058 			break;
1059 		}
1060 		case MSG_NEWS: {
1061 			const techMail_t* mail = &m->pedia->mail[TECHMAIL_PRE];
1062 			if (mail->from) {
1063 				CP_DateConvertLong(&m->pedia->preResearchedDate, &date);
1064 			} else {
1065 				CP_DateConvertLong(&m->pedia->researchedDate, &date);
1066 				mail = &m->pedia->mail[TECHMAIL_RESEARCHED];
1067 			}
1068 			if (!mail->from)
1069 				continue;
1070 			Com_sprintf(headline, sizeof(headline), "%s", _(mail->subject));
1071 			Com_sprintf(dateBuf, sizeof(dateBuf), _("%i %s %02i"), date.year, Date_GetMonthName(date.month - 1), date.day);
1072 			icon = mail->icon;
1073 			read = mail->read;
1074 			break;
1075 		}
1076 		case MSG_EVENT: {
1077 			assert(m->eventMail);
1078 			const eventMail_t& mail = *m->eventMail;
1079 			if (!mail.from)
1080 				continue;
1081 			Com_sprintf(headline, sizeof(headline), "%s", _(mail.subject));
1082 			Com_sprintf(dateBuf, sizeof(dateBuf), "%s", mail.date);
1083 			icon = mail.icon;
1084 			read = mail.read;
1085 			break;
1086 		}
1087 		default:
1088 			continue;
1089 		}
1090 		cgi->UI_ExecuteConfunc("add_mail %i \"%s\" \"%s\" %i \"%s\"", idx++, headline, icon, read, dateBuf);
1091 	}
1092 }
1093 
1094 /**
1095  * @brief Marks all mails read in mailclient
1096  */
UP_SetAllMailsRead_f(void)1097 static void UP_SetAllMailsRead_f (void)
1098 {
1099 	const uiMessageListNodeMessage_t* m = cgi->UI_MessageGetStack();
1100 
1101 	while (m) {
1102 		switch (m->type) {
1103 		case MSG_RESEARCH_PROPOSAL:
1104 			assert(m->pedia);
1105 			m->pedia->mail[TECHMAIL_PRE].read = true;
1106 			break;
1107 		case MSG_RESEARCH_FINISHED:
1108 			assert(m->pedia);
1109 			m->pedia->mail[TECHMAIL_RESEARCHED].read = true;
1110 			break;
1111 		case MSG_NEWS:
1112 			assert(m->pedia);
1113 			m->pedia->mail[TECHMAIL_PRE].read = true;
1114 			m->pedia->mail[TECHMAIL_RESEARCHED].read = true;
1115 			break;
1116 		case MSG_EVENT:
1117 			assert(m->eventMail);
1118 			m->eventMail->read = true;
1119 			break;
1120 		default:
1121 			break;
1122 		}
1123 		m = m->next;
1124 	}
1125 
1126 	ccs.numUnreadMails = 0;
1127 	cgi->Cvar_Set("mn_upunreadmail", "%i", ccs.numUnreadMails);
1128 	UP_OpenMail_f();
1129 }
1130 
1131 /**
1132  * @sa cgi->UI_InitStartup
1133  */
UP_InitStartup(void)1134 void UP_InitStartup (void)
1135 {
1136 	/* add commands and cvars */
1137 	cgi->Cmd_AddCommand("mn_upcontent", UP_Content_f, "Shows the UFOpaedia chapters");
1138 	cgi->Cmd_AddCommand("mn_upupdate", UP_Update_f, "Redraw the current UFOpaedia article");
1139 	cgi->Cmd_AddCommand("ufopedia", UP_FindEntry_f, "Open the UFOpaedia with the given article");
1140 	cgi->Cmd_AddCommand("ufopedia_click", UP_Click_f, nullptr);
1141 	cgi->Cmd_AddCommand("mailclient_click", UP_MailClientClick_f, nullptr);
1142 	cgi->Cmd_AddCommand("mn_mail_readall", UP_SetAllMailsRead_f, "Mark all mails read");
1143 	cgi->Cmd_AddCommand("ufopedia_openmail", UP_OpenMail_f, "Start the mailclient");
1144 	cgi->Cmd_AddCommand("techtree_click", UP_TechTreeClick_f, nullptr);
1145 	cgi->Cmd_AddCommand("mn_upgotoresearchedlink", UP_ResearchedLinkClick_f, nullptr);
1146 
1147 	mn_uppretext = cgi->Cvar_Get("mn_uppretext", "0", 0, "Show the pre-research text in the UFOpaedia");
1148 	mn_uppreavailable = cgi->Cvar_Get("mn_uppreavailable", "0", 0, "True if there is a pre-research text available");
1149 	cgi->Cvar_Set("mn_uprequirement", "");
1150 	cgi->Cvar_Set("mn_upmetadata", "");
1151 }
1152 
1153 /**
1154  * @sa UI_InitStartup
1155  */
UP_Shutdown(void)1156 void UP_Shutdown (void)
1157 {
1158 	/* add commands and cvars */
1159 	cgi->Cmd_RemoveCommand("mn_upcontent");
1160 	cgi->Cmd_RemoveCommand("mn_upupdate");
1161 	cgi->Cmd_RemoveCommand("ufopedia");
1162 	cgi->Cmd_RemoveCommand("ufopedia_click");
1163 	cgi->Cmd_RemoveCommand("mailclient_click");
1164 	cgi->Cmd_RemoveCommand("mn_mail_readall");
1165 	cgi->Cmd_RemoveCommand("ufopedia_openmail");
1166 	cgi->Cmd_RemoveCommand("techtree_click");
1167 	cgi->Cmd_RemoveCommand("mn_upgotoresearchedlink");
1168 
1169 	cgi->Cvar_Delete("mn_uppretext");
1170 	cgi->Cvar_Delete("mn_uppreavailable");
1171 	cgi->Cvar_Delete("mn_uprequirement");
1172 	cgi->Cvar_Delete("mn_upmetadata");
1173 }
1174 
1175 /**
1176  * @brief Parse the UFOpaedia chapters from scripts
1177  * @param[in] name Chapter ID
1178  * @param[in] text Text for chapter ID
1179  * @sa CL_ParseFirstScript
1180  */
UP_ParseChapter(const char * name,const char ** text)1181 void UP_ParseChapter (const char* name, const char** text)
1182 {
1183 	const char* errhead = "UP_ParseChapter: unexpected end of file (names ";
1184 	const char* token;
1185 
1186 	if (ccs.numChapters >= MAX_PEDIACHAPTERS) {
1187 		cgi->Com_Error(ERR_DROP, "UP_ParseChapter: chapter def \"%s\": Too many chapter defs.\n", name);
1188 	}
1189 
1190 	pediaChapter_t chapter;
1191 	OBJZERO(chapter);
1192 	chapter.id = Mem_PoolStrDup(name, cp_campaignPool, 0);
1193 	chapter.idx = ccs.numChapters;	/* set self-link */
1194 
1195 	/* get begin block */
1196 	token = Com_Parse(text);
1197 	if (!*text || *token !='{') {
1198 		cgi->Com_Error(ERR_DROP, "UP_ParseChapter: chapter def \"%s\": '{' token expected.\n", name);
1199 	}
1200 
1201 	do {
1202 		token = Com_Parse(text);
1203 		if (!*text)
1204 			cgi->Com_Error(ERR_DROP, "UP_ParseChapter: end of file not expected \"%s\": '{' token expected.\n", name);
1205 		if (token[0] == '}')
1206 			break;
1207 
1208 		if (Q_streq(token, "name")) {
1209 			/* get the name */
1210 			token = cgi->Com_EParse(text, errhead, name);
1211 			if (!*text || *token == '}') {
1212 				cgi->Com_Error(ERR_DROP, "UP_ParseChapter: chapter def \"%s\": Name token expected.\n", name);
1213 			}
1214 			if (*token == '_')
1215 				token++;
1216 			chapter.name = Mem_PoolStrDup(token, cp_campaignPool, 0);
1217 		} else {
1218 			cgi->Com_Error(ERR_DROP, "UP_ParseChapter: chapter def \"%s\": token \"%s\" not expected.\n", name, token);
1219 		}
1220 	} while (*text);
1221 
1222 	/* add chapter to the game */
1223 	ccs.upChapters[ccs.numChapters] = chapter;
1224 	ccs.numChapters++;
1225 }
1226