1 /**
2 * @file
3 * @brief Geoscape event implementation
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
26 #include "../../cl_shared.h"
27 #include "../../../shared/parse.h"
28 #include "../../../common/binaryexpressionparser.h"
29 #include "cp_campaign.h"
30 #include "cp_time.h"
31 #include "cp_xvi.h"
32 #include "cp_event_callbacks.h"
33 #include "save/save_triggerevents.h"
34
35 static linkedList_t* eventMails = nullptr;
36
37 /**
38 * @brief Searches all event mails for a given id
39 * @note Might also return nullptr - always check the return value
40 * @note If you want to create mails that base on a script definition but have different
41 * body messages, set createCopy to true
42 * @param[in] id The id from the script files
43 */
CL_GetEventMail(const char * id)44 eventMail_t* CL_GetEventMail (const char* id)
45 {
46 int i;
47
48 for (i = 0; i < ccs.numEventMails; i++) {
49 eventMail_t* mail = &ccs.eventMails[i];
50 if (Q_streq(mail->id, id))
51 return mail;
52 }
53
54 LIST_Foreach(eventMails, eventMail_t, listMail) {
55 if (Q_streq(listMail->id, id))
56 return listMail;
57 }
58
59 return nullptr;
60 }
61
62 /**
63 * @brief Make sure, that the linked list is freed with every new game
64 * @sa CP_ResetCampaignData
65 */
CP_FreeDynamicEventMail(void)66 void CP_FreeDynamicEventMail (void)
67 {
68 /* the pointers are not freed, this is done with the
69 * pool clear in CP_ResetCampaignData */
70 cgi->LIST_Delete(&eventMails);
71 }
72
73 /** @brief Valid event mail parameters */
74 static const value_t eventMail_vals[] = {
75 {"subject", V_TRANSLATION_STRING, offsetof(eventMail_t, subject), 0},
76 {"from", V_TRANSLATION_STRING, offsetof(eventMail_t, from), 0},
77 {"to", V_TRANSLATION_STRING, offsetof(eventMail_t, to), 0},
78 {"cc", V_TRANSLATION_STRING, offsetof(eventMail_t, cc), 0},
79 {"date", V_TRANSLATION_STRING, offsetof(eventMail_t, date), 0},
80 {"body", V_TRANSLATION_STRING, offsetof(eventMail_t, body), 0},
81 {"icon", V_HUNK_STRING, offsetof(eventMail_t, icon), 0},
82 {"model", V_HUNK_STRING, offsetof(eventMail_t, model), 0},
83 {"skipmessage", V_BOOL, offsetof(eventMail_t, skipMessage), MEMBER_SIZEOF(eventMail_t, skipMessage)},
84
85 {nullptr, V_NULL, 0, 0}
86 };
87
88 /**
89 * @sa CL_ParseScriptFirst
90 * @note write into cp_campaignPool - free on every game restart and reparse
91 */
CL_ParseEventMails(const char * name,const char ** text)92 void CL_ParseEventMails (const char* name, const char** text)
93 {
94 eventMail_t* eventMail;
95
96 if (ccs.numEventMails >= MAX_EVENTMAILS) {
97 Com_Printf("CL_ParseEventMails: mail def \"%s\" with same name found, second ignored\n", name);
98 return;
99 }
100
101 /* initialize the eventMail */
102 eventMail = &ccs.eventMails[ccs.numEventMails++];
103 OBJZERO(*eventMail);
104
105 Com_DPrintf(DEBUG_CLIENT, "...found eventMail %s\n", name);
106
107 eventMail->id = Mem_PoolStrDup(name, cp_campaignPool, 0);
108
109 Com_ParseBlock(name, text, eventMail, eventMail_vals, cp_campaignPool);
110 }
111
CP_CheckCampaignEvents(campaign_t * campaign)112 void CP_CheckCampaignEvents (campaign_t* campaign)
113 {
114 const campaignEvents_t* events = campaign->events;
115 int i;
116
117 /* no events for the current campaign */
118 if (!events)
119 return;
120
121 /* no events in that definition */
122 if (!events->numCampaignEvents)
123 return;
124
125 for (i = 0; i < events->numCampaignEvents; i++) {
126 const campaignEvent_t* event = &events->campaignEvents[i];
127 if (event->interest <= ccs.overallInterest) {
128 RS_MarkStoryLineEventResearched(event->tech);
129 }
130 }
131 }
132
133 /**
134 * Will return the campaign related events
135 * @note Also performs some sanity check
136 * @param name The events id
137 */
CP_GetEventsByID(const char * name)138 const campaignEvents_t* CP_GetEventsByID (const char* name)
139 {
140 int i;
141
142 for (i = 0; i < ccs.numCampaignEventDefinitions; i++) {
143 const campaignEvents_t* events = &ccs.campaignEvents[i];
144 if (Q_streq(events->id, name)) {
145 int j;
146 for (j = 0; j < events->numCampaignEvents; j++) {
147 const campaignEvent_t* event = &events->campaignEvents[j];
148 if (!RS_GetTechByID(event->tech))
149 Sys_Error("Illegal tech '%s' given in events '%s'", event->tech, events->id);
150 }
151 return events;
152 }
153 }
154
155 return nullptr;
156 }
157
CP_CheckTriggerEvent(const char * expression,const void * userdata)158 static int CP_CheckTriggerEvent (const char* expression, const void* userdata)
159 {
160 if (Q_strnull(expression))
161 return -1;
162
163 const char* type;
164
165 /* check that a particular installation type is built already */
166 type = Q_strstart(expression, "installation");
167 if (type != nullptr) {
168 if (strlen(type) <= 1)
169 return -1;
170 char value[MAX_VAR];
171 Q_strncpyz(value, type + 1, sizeof(value));
172 value[strlen(value) - 1] = '\0';
173 const installationType_t insType = INS_GetType(value);
174 if (INS_HasType(insType, INSTALLATION_NOT_USED))
175 return 1;
176 return 0;
177 }
178
179 /* check whether a particular ufo was detected */
180 type = Q_strstart(expression, "ufo");
181 if (type != nullptr) {
182 if (strlen(type) <= 1)
183 return -1;
184 char value[MAX_VAR];
185 Q_strncpyz(value, type + 1, sizeof(value));
186 value[strlen(value) - 1] = '\0';
187 const char* detectedUFO = static_cast<const char*>(userdata);
188 if (Q_strnull(detectedUFO))
189 return -1;
190 return Q_streq(detectedUFO, value);
191 }
192
193 /* check that the given xvi level is reached in any nation */
194 type = Q_strstart(expression, "xvi");
195 if (type != nullptr) {
196 int xvi;
197 if (sscanf(type, "[%i]", &xvi) != 1)
198 return -1;
199 int i;
200 /* check for XVI infection rate */
201 for (i = 0; i < ccs.numNations; i++) {
202 const nation_t* nation = NAT_GetNationByIDX(i);
203 const nationInfo_t* stats = NAT_GetCurrentMonthInfo(nation);
204 if (stats->xviInfection >= xvi)
205 return 1;
206 }
207 return 0;
208 }
209
210 /* check that the given tech is already researched */
211 type = Q_strstart(expression, "researched");
212 if (type != nullptr) {
213 if (strlen(type) <= 1)
214 return -1;
215 char value[MAX_VAR];
216 Q_strncpyz(value, type + 1, sizeof(value));
217 value[strlen(value) - 1] = '\0';
218 technology_t* tech = RS_GetTechByID(value);
219 if (tech == nullptr)
220 return -1;
221 if (RS_IsResearched_ptr(tech))
222 return 1;
223 return 0;
224 }
225
226 /* check for nation happiness - also see the lost conditions in the campaign */
227 type = Q_strstart(expression, "nationhappiness");
228 if (type != nullptr) {
229 int nationAmount;
230
231 if (sscanf(type, "[%i]", &nationAmount) != 1)
232 return -1;
233
234 int j, nationBelowLimit = 0;
235 for (j = 0; j < ccs.numNations; j++) {
236 const nation_t* nation = NAT_GetNationByIDX(j);
237 const nationInfo_t* stats = NAT_GetCurrentMonthInfo(nation);
238 if (stats->happiness < ccs.curCampaign->minhappiness) {
239 nationBelowLimit++;
240 if (nationBelowLimit >= nationAmount)
241 return 1;
242 }
243 }
244 return 0;
245 }
246
247 /* check that the given average xvi level is reached */
248 type = Q_strstart(expression, "averagexvi");
249 if (type != nullptr) {
250 int xvipercent;
251 if (sscanf(type, "[%i]", &xvipercent) != 1)
252 return -1;
253 if (xvipercent < 0 || xvipercent > 100)
254 return -1;
255 const int xvi = CP_GetAverageXVIRate();
256 if (xvi > ccs.curCampaign->maxAllowedXVIRateUntilLost * xvipercent / 100)
257 return 1;
258 return 0;
259 }
260
261 type = Q_strstart(expression, "difficulty");
262 if (type != nullptr) {
263 int difficulty;
264 if (sscanf(type, "[%i]", &difficulty) != 1)
265 return -1;
266 return ccs.curCampaign->difficulty == difficulty;
267 }
268
269 /* check that these days have passed in the campaign */
270 type = Q_strstart(expression, "days");
271 if (type != nullptr) {
272 int days;
273 if (sscanf(type, "[%i]", &days) != 1)
274 return -1;
275 date_t d = ccs.curCampaign->date;
276 d.day += days;
277 if (Date_IsDue(&d))
278 return 1;
279 return 0;
280 }
281
282 type = Q_strstart(expression, "alienscaptured");
283 if (type != nullptr) {
284 if (ccs.campaignStats.capturedAliens > 0)
285 return 1;
286 return 0;
287 }
288
289 type = Q_strstart(expression, "samsitearmed");
290 if (type != nullptr) {
291 if (!INS_HasType(INSTALLATION_DEFENCE))
292 return 1;
293
294 INS_ForeachOfType(installation, INSTALLATION_DEFENCE) {
295 if (installation->installationStatus == INSTALLATION_WORKING) {
296 for (int i = 0; i < installation->installationTemplate->maxBatteries; i++) {
297 const aircraftSlot_t* slot = &installation->batteries[i].slot;
298 if (slot->ammoLeft > 0)
299 return 1;
300 }
301 }
302 }
303
304 return 0;
305 }
306
307 Com_Printf("unknown expression given: '%s'\n", expression);
308
309 return -1;
310 }
311
312 /**
313 * @brief Triggers a campaign event with a special type
314 * @param[in] type the event type
315 * @param[in] userdata Any userdata that is passed to the bep checker function
316 */
CP_TriggerEvent(campaignTriggerEventType_t type,const void * userdata)317 void CP_TriggerEvent (campaignTriggerEventType_t type, const void* userdata)
318 {
319 int i;
320
321 for (i = 0; i < ccs.numCampaignTriggerEvents; i++) {
322 campaignTriggerEvent_t* event = &ccs.campaignTriggerEvents[i];
323 if (event->type != type || (!event->active && event->reactivate == nullptr))
324 continue;
325
326 if (event->active) {
327 if (!BEP_Evaluate(event->require, CP_CheckTriggerEvent, userdata))
328 continue;
329 if (Q_strvalid(event->command)) {
330 CP_CampaignTriggerFunctions(true);
331 cgi->Cmd_ExecuteString("%s", event->command);
332 CP_CampaignTriggerFunctions(false);
333 }
334
335 if (event->once) {
336 event->active = false;
337 }
338 } else {
339 event->active = BEP_Evaluate(event->reactivate, CP_CheckTriggerEvent, userdata);
340 }
341 }
342 }
343
344 /** @brief Valid event mail parameters */
345 static const value_t event_vals[] = {
346 {"type", V_INT, offsetof(campaignTriggerEvent_t, type), MEMBER_SIZEOF(campaignTriggerEvent_t, type)},
347 {"require", V_HUNK_STRING, offsetof(campaignTriggerEvent_t, require), 0},
348 {"reactivate", V_HUNK_STRING, offsetof(campaignTriggerEvent_t, reactivate), 0},
349 {"command", V_HUNK_STRING, offsetof(campaignTriggerEvent_t, command), 0},
350 {"once", V_BOOL, offsetof(campaignTriggerEvent_t, once), MEMBER_SIZEOF(campaignTriggerEvent_t, once)},
351
352 {nullptr, V_NULL, 0, 0}
353 };
354
355 #define EVENTCONSTANTS_NAMESPACE "eventTrigger::"
356 static const constListEntry_t eventConstants[] = {
357 {EVENTCONSTANTS_NAMESPACE "new_day", NEW_DAY},
358 {EVENTCONSTANTS_NAMESPACE "ufo_detection", UFO_DETECTION},
359 {EVENTCONSTANTS_NAMESPACE "captured_aliens_died", CAPTURED_ALIENS_DIED},
360 {EVENTCONSTANTS_NAMESPACE "captured_aliens", CAPTURED_ALIENS},
361 {EVENTCONSTANTS_NAMESPACE "alienbase_discovered", ALIENBASE_DISCOVERED},
362
363 {nullptr, -1}
364 };
365
CP_ParseEventTrigger(const char * name,const char ** text)366 void CP_ParseEventTrigger (const char* name, const char** text)
367 {
368 const char* errhead = "CP_ParseEventTrigger: unexpected end of file (event ";
369 const char* token;
370
371 if (ccs.numCampaignTriggerEvents >= MAX_CAMPAIGN_TRIGGER_EVENTS) {
372 Com_Printf("CP_ParseEventTrigger: max event def limit hit\n");
373 return;
374 }
375
376 token = cgi->Com_EParse(text, errhead, name);
377 if (!*text)
378 return;
379
380 if (!*text || token[0] != '{') {
381 Com_Printf("CP_ParseEventTrigger: event def '%s' without body ignored\n", name);
382 return;
383 }
384
385 cgi->Com_RegisterConstList(eventConstants);
386
387 campaignTriggerEvent_t* event = &ccs.campaignTriggerEvents[ccs.numCampaignTriggerEvents];
388 OBJZERO(*event);
389 Com_DPrintf(DEBUG_CLIENT, "...found event %s\n", name);
390 ccs.numCampaignTriggerEvents++;
391 event->active = true;
392 event->id = Mem_PoolStrDup(name, cp_campaignPool, 0);
393
394 do {
395 token = cgi->Com_EParse(text, errhead, name);
396 if (!*text)
397 break;
398 if (*token == '}')
399 break;
400 if (!Com_ParseBlockToken(name, text, event, event_vals, cp_campaignPool, token)) {
401 Com_Printf("CP_ParseEventTrigger: Ignoring unknown event value '%s'\n", token);
402 }
403 } while (*text);
404
405 cgi->Com_UnregisterConstList(eventConstants);
406 }
407
CP_TriggerEventSaveXML(xmlNode_t * p)408 bool CP_TriggerEventSaveXML (xmlNode_t* p)
409 {
410 xmlNode_t* n = cgi->XML_AddNode(p, SAVE_TRIGGEREVENTS_TRIGGEREVENTS);
411 int i;
412
413 for (i = 0; i < ccs.numCampaignTriggerEvents; i++) {
414 const campaignTriggerEvent_t* event = &ccs.campaignTriggerEvents[i];
415 if (event->active)
416 continue;
417 xmlNode_t* s = cgi->XML_AddNode(n, SAVE_TRIGGEREVENTS_TRIGGEREVENT);
418
419 cgi->XML_AddString(s, SAVE_TRIGGEREVENTS_NAME, event->id);
420 cgi->XML_AddBool(s, SAVE_TRIGGEREVENTS_STATE, event->active);
421 }
422
423 return true;
424 }
425
CP_TriggerEventLoadXML(xmlNode_t * p)426 bool CP_TriggerEventLoadXML (xmlNode_t* p)
427 {
428 xmlNode_t* n, *s;
429
430 n = cgi->XML_GetNode(p, SAVE_TRIGGEREVENTS_TRIGGEREVENTS);
431 if (!n)
432 return true;
433
434 for (s = cgi->XML_GetNode(n, SAVE_TRIGGEREVENTS_TRIGGEREVENT); s; s = cgi->XML_GetNextNode(s,n, SAVE_TRIGGEREVENTS_TRIGGEREVENT)) {
435 const char* id = cgi->XML_GetString(s, SAVE_TRIGGEREVENTS_NAME);
436 const bool state = cgi->XML_GetBool(s, SAVE_TRIGGEREVENTS_STATE, true);
437
438 int i;
439 for (i = 0; i < ccs.numCampaignTriggerEvents; i++) {
440 campaignTriggerEvent_t* event = &ccs.campaignTriggerEvents[i];
441 if (Q_streq(event->id, id)) {
442 event->active = state;
443 break;
444 }
445 }
446 }
447
448 return true;
449 }
450
451 /**
452 * @sa CL_ParseScriptFirst
453 * @note write into cp_campaignPool - free on every game restart and reparse
454 */
CL_ParseCampaignEvents(const char * name,const char ** text)455 void CL_ParseCampaignEvents (const char* name, const char** text)
456 {
457 const char* errhead = "CL_ParseCampaignEvents: unexpected end of file (events ";
458 const char* token;
459 campaignEvents_t* events;
460
461 if (ccs.numCampaignEventDefinitions >= MAX_CAMPAIGNS) {
462 Com_Printf("CL_ParseCampaignEvents: max events def limit hit\n");
463 return;
464 }
465
466 token = cgi->Com_EParse(text, errhead, name);
467 if (!*text)
468 return;
469
470 if (!*text || token[0] != '{') {
471 Com_Printf("CL_ParseCampaignEvents: events def '%s' without body ignored\n", name);
472 return;
473 }
474
475 events = &ccs.campaignEvents[ccs.numCampaignEventDefinitions];
476 OBJZERO(*events);
477 Com_DPrintf(DEBUG_CLIENT, "...found events %s\n", name);
478 events->id = Mem_PoolStrDup(name, cp_campaignPool, 0);
479 ccs.numCampaignEventDefinitions++;
480
481 do {
482 campaignEvent_t* event;
483 token = cgi->Com_EParse(text, errhead, name);
484 if (!*text)
485 break;
486 if (*token == '}')
487 break;
488
489 if (events->numCampaignEvents >= MAX_CAMPAIGNEVENTS) {
490 Com_Printf("CL_ParseCampaignEvents: max events per event definition limit hit\n");
491 return;
492 }
493
494 /* initialize the eventMail */
495 event = &events->campaignEvents[events->numCampaignEvents++];
496 OBJZERO(*event);
497
498 Mem_PoolStrDupTo(token, (char**) ((char*)event + (int)offsetof(campaignEvent_t, tech)), cp_campaignPool, 0);
499
500 token = cgi->Com_EParse(text, errhead, name);
501 if (!*text)
502 return;
503
504 cgi->Com_EParseValue(event, token, V_INT, offsetof(campaignEvent_t, interest), sizeof(int));
505
506 if (event->interest < 0)
507 Sys_Error("Illegal interest value in events definition '%s' for tech '%s'", events->id, event->tech);
508 } while (*text);
509 }
510
511 /**
512 * @brief Adds the event mail to the message stack. This message is going to be added to the savegame.
513 */
CL_EventAddMail(const char * eventMailId)514 void CL_EventAddMail (const char* eventMailId)
515 {
516 eventMail_t* eventMail = CL_GetEventMail(eventMailId);
517 if (!eventMail) {
518 Com_Printf("CL_EventAddMail: Could not find eventmail with id '%s'\n", eventMailId);
519 return;
520 }
521
522 if (eventMail->sent) {
523 return;
524 }
525
526 if (!eventMail->from || !eventMail->to || !eventMail->subject || !eventMail->body) {
527 Com_Printf("CL_EventAddMail: mail with id '%s' has incomplete data\n", eventMailId);
528 return;
529 }
530
531 if (!eventMail->date) {
532 dateLong_t date;
533 char dateBuf[MAX_VAR] = "";
534
535 CP_DateConvertLong(&ccs.date, &date);
536 Com_sprintf(dateBuf, sizeof(dateBuf), _("%i %s %02i"),
537 date.year, Date_GetMonthName(date.month - 1), date.day);
538 eventMail->date = Mem_PoolStrDup(dateBuf, cp_campaignPool, 0);
539 }
540
541 eventMail->sent = true;
542
543 if (!eventMail->skipMessage) {
544 uiMessageListNodeMessage_t* m = MS_AddNewMessage("", va(_("You've got a new mail: %s"), _(eventMail->subject)), MSG_EVENT);
545 if (m)
546 m->eventMail = eventMail;
547 else
548 Com_Printf("CL_EventAddMail: Could not add message with id: %s\n", eventMailId);
549 }
550
551 UP_OpenEventMail(eventMailId);
552 }
553
554 /**
555 * @sa UP_OpenMail_f
556 * @sa MS_AddNewMessage
557 * @sa UP_SetMailHeader
558 * @sa UP_OpenEventMail
559 */
CL_EventAddMail_f(void)560 void CL_EventAddMail_f (void)
561 {
562 if (cgi->Cmd_Argc() < 2) {
563 Com_Printf("Usage: %s <event_mail_id>\n", cgi->Cmd_Argv(0));
564 return;
565 }
566
567 CL_EventAddMail(cgi->Cmd_Argv(1));
568 }
569