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