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