1 /**
2  * @file
3  * @brief In-game message settings
4  */
5 
6 /*
7 Copyright (C) 2002-2013 UFO: Alien Invasion.
8 
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License
11 as published by the Free Software Foundation; either version 2
12 of the License, or (at your option) any later version.
13 
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17 
18 See the GNU General Public License for more details.
19 
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
23 */
24 
25 #include "../../cl_shared.h"
26 #include "../../../shared/parse.h"
27 #include "cp_campaign.h"
28 #include "cp_messageoptions.h"
29 #include "cp_messageoptions_callbacks.h"
30 #include "cp_time.h"
31 #include "save/save_messageoptions.h"
32 
33 /** @brief valid notification types that may cause pause / notice */
34 char const* const nt_strings[NT_NUM_NOTIFYTYPE] = {
35 	N_("installation_installed"),
36 	N_("installation_removed"),
37 	N_("installation_replaced"),
38 	N_("aircraft_refueled"),
39 	N_("aircraft_cannotrefuel"),
40 	N_("aircraft_arrivedhome"),
41 	N_("installation_build_started"),
42 	N_("installation_build_finished"),
43 	N_("installation_destroyed"),
44 	N_("research_new_proposed"),
45 	N_("research_halted"),
46 	N_("research_completed"),
47 	N_("production_started"),
48 	N_("production_finished"),
49 	N_("production_failed"),
50 	N_("production_queue_empty"),
51 	N_("nation_happiness_changed"),
52 	N_("nation_unhappy"),
53 	N_("nation_pleased"),
54 	N_("transfer_started"),
55 	N_("transfer_completed_success"),
56 	N_("transfer_lost"),
57 	N_("transfer_aliens_defered"),
58 	N_("transfer_uforecovery_finished"),
59 	N_("ufo_spotted"),
60 	N_("ufo_signal_lost"),
61 	N_("ufo_attacking"),
62 	N_("base_attack"),
63 	N_("building_finished")
64 };
65 CASSERT(lengthof(nt_strings) == NT_NUM_NOTIFYTYPE);
66 
67 messageSettings_t messageSettings[NT_NUM_NOTIFYTYPE]; /**< array holding actual message settings for every notify type */
68 
69 /**
70  * @brief Function updates pause or notification settings.
71  * @param listIndex listIndex in menu to update via confunc
72  * @param type notification type to update
73  * @param optionType option type that should be updated
74  * @param activate flag indicating whether setting should be activated (true) or deactivated
75  * @param sendCommands flag indicating whether confunc command to update menu button should be sent
76  * @sa MSO_Toggle_f
77  * @sa MSO_Set_f
78  * @note if sendCommands is false, initialization of buttons is reactivated for next menu displaying
79  */
MSO_Set(const int listIndex,const notify_t type,const int optionType,const bool activate,const bool sendCommands)80 void MSO_Set (const int listIndex, const notify_t type, const int optionType, const bool activate, const bool sendCommands)
81 {
82 	messageSettings_t* settings = &messageSettings[type];
83 
84 	if (activate) {
85 		if ((optionType & NTMASK_PAUSE) == NTMASK_PAUSE)
86 			settings->doPause = activate;
87 		if ((optionType & NTMASK_SOUND)== NTMASK_SOUND)
88 			settings->doSound = activate;
89 		/* notification anyway*/
90 		settings->doNotify = activate;
91 	} else {
92 		if ((optionType & NTMASK_PAUSE) == NTMASK_PAUSE)
93 			settings->doPause = activate;
94 		if ((optionType & NTMASK_SOUND)== NTMASK_SOUND)
95 			settings->doSound = activate;
96 		/* disable all if notify is disabled */
97 		if (optionType == MSO_NOTIFY) {
98 			settings->doNotify = activate;
99 			settings->doPause = activate;
100 			settings->doSound = activate;
101 		}
102 	}
103 
104 	if (sendCommands)
105 		cgi->UI_ExecuteConfunc("ms_btnstate %i %i %i %i", listIndex, settings->doPause, settings->doNotify, settings->doSound);
106 	else
107 		/* ensure that message buttons will be initialized correctly if menu is shown next time */
108 		MSO_SetMenuState(MSO_MSTATE_PREPARED, false, false);
109 }
110 
111 /**
112  * @brief Parse notify type
113  * @return A NT_ number, else return -1
114  */
MSO_ParseNotifyType(const char * name)115 static int MSO_ParseNotifyType (const char* name)
116 {
117 	for (int idx = 0; idx < NT_NUM_NOTIFYTYPE; idx ++) {
118 		if (Q_streq(name, nt_strings[idx])) {
119 			return idx;
120 		}
121 	}
122 	return -1;
123 }
124 
125 /**
126  * @brief Parse option type
127  * @return A MSO value, else 0
128  */
MSO_ParseOptionType(const char * type)129 static int MSO_ParseOptionType (const char* type)
130 {
131 	if (Q_strcaseeq(type, "pause"))
132 		return MSO_PAUSE;
133 	else if (Q_strcaseeq(type, "notify"))
134 		return MSO_NOTIFY;
135 	else if (Q_strcaseeq(type, "sound"))
136 		return MSO_SOUND;
137 
138 	Com_Printf("Unrecognized optionstype during set '%s' ignored\n", type);
139 	return 0;
140 }
141 
142 /**
143  * @brief Function callback used to initialize values for messageoptions and for manual setting changes.
144  * @sa MSO_Set
145  */
MSO_Set_f(void)146 static void MSO_Set_f (void)
147 {
148 	if (cgi->Cmd_Argc() != 4) {
149 		Com_Printf("Usage: %s <messagetypename> <pause|notify|sound> <0|1>\n", cgi->Cmd_Argv(0));
150 		return;
151 	}
152 
153 	const int optionsType = MSO_ParseOptionType(cgi->Cmd_Argv(1));
154 	if (optionsType == 0)
155 		return;
156 
157 	const char* messagetype = cgi->Cmd_Argv(1);
158 	int type;
159 	for (type = 0; type < NT_NUM_NOTIFYTYPE; type++) {
160 		if (Q_streq(nt_strings[type], messagetype))
161 			break;
162 	}
163 	if (type == NT_NUM_NOTIFYTYPE) {
164 		Com_Printf("Unrecognized messagetype during set '%s' ignored\n", messagetype);
165 		return;
166 	}
167 
168 	MSO_Set(0, (notify_t)type, optionsType, atoi(cgi->Cmd_Argv(3)), false);
169 }
170 
171 /**
172  * @brief Function callback that sets all message options settings for one option type to given value.
173  * @sa MSO_Set
174  * @sa MSO_Init_f
175  */
MSO_SetAll_f(void)176 static void MSO_SetAll_f (void)
177 {
178 	if (cgi->Cmd_Argc() != 3) {
179 		Com_Printf("Usage: %s <pause|notify|sound> <0|1>\n", cgi->Cmd_Argv(0));
180 		return;
181 	}
182 
183 	const bool activate = atoi(cgi->Cmd_Argv(2));
184 	const int optionsType = MSO_ParseOptionType(cgi->Cmd_Argv(1));
185 	if (optionsType == 0)
186 		return;
187 
188 	/* update settings for chosen type */
189 	for (int type = 0; type < NT_NUM_NOTIFYTYPE; ++type) {
190 		MSO_Set(0, (notify_t)type, optionsType, activate, false);
191 	}
192 	/* reinit menu */
193 	MSO_SetMenuState(MSO_MSTATE_PREPARED, true, true);
194 }
195 
196 /**
197  * @brief Adds a new message to message stack. It uses message settings to
198  * verify whether sound should be played and whether game should stop.
199  * @param messagecategory category for notification
200  * @param[in] title Already translated message/mail title
201  * @param[in] text Already translated message/mail body
202  * @param[in] popup Show this as a popup, too?
203  * @param[in] type The message type
204  * @param[in] pedia Pointer to technology (only if needed)
205  * @return message_t pointer if message was added
206  * @sa MS_AddNewMessageSound
207  */
MSO_CheckAddNewMessage(const notify_t messagecategory,const char * title,const char * text,messageType_t type,technology_t * pedia,bool popup)208 uiMessageListNodeMessage_t* MSO_CheckAddNewMessage (const notify_t messagecategory, const char* title, const char* text, messageType_t type, technology_t* pedia, bool popup)
209 {
210 	uiMessageListNodeMessage_t* result = nullptr;
211 	const messageSettings_t* settings = &messageSettings[messagecategory];
212 
213 	if (settings->doNotify)
214 		result = MS_AddNewMessage(title, text, type, pedia, popup, settings->doSound);
215 	if (settings->doPause)
216 		CP_GameTimeStop();
217 	return result;
218 }
219 
220 /**
221  * @brief saves current notification and pause settings
222  * @sa MSO_LoadXML
223  */
MSO_SaveXML(xmlNode_t * p)224 bool MSO_SaveXML (xmlNode_t* p)
225 {
226 	int type;
227 	xmlNode_t* n = cgi->XML_AddNode(p, SAVE_MESSAGEOPTIONS_MESSAGEOPTIONS);
228 
229 	/* save positive values */
230 	for (type = 0; type < NT_NUM_NOTIFYTYPE; ++type) {
231 		messageSettings_t actualSetting = messageSettings[type];
232 		xmlNode_t* s = cgi->XML_AddNode(n, SAVE_MESSAGEOPTIONS_TYPE);
233 
234 		cgi->XML_AddString(s, SAVE_MESSAGEOPTIONS_NAME, nt_strings[type]);
235 		cgi->XML_AddBoolValue(s, SAVE_MESSAGEOPTIONS_NOTIFY, actualSetting.doNotify);
236 		cgi->XML_AddBoolValue(s, SAVE_MESSAGEOPTIONS_PAUSE, actualSetting.doPause);
237 		cgi->XML_AddBoolValue(s, SAVE_MESSAGEOPTIONS_SOUND, actualSetting.doSound);
238 	}
239 
240 	return true;
241 }
242 
243 /**
244  * @brief Restores the notification and pause settings from savegame
245  * @sa MSO_SaveXML
246  */
MSO_LoadXML(xmlNode_t * p)247 bool MSO_LoadXML (xmlNode_t* p)
248 {
249 	xmlNode_t* n, *s;
250 
251 	n = cgi->XML_GetNode(p, SAVE_MESSAGEOPTIONS_MESSAGEOPTIONS);
252 	if (!n)
253 		return false;
254 
255 	for (s = cgi->XML_GetNode(n, SAVE_MESSAGEOPTIONS_TYPE); s; s = cgi->XML_GetNextNode(s, n, SAVE_MESSAGEOPTIONS_TYPE)) {
256 		const char* messagetype = cgi->XML_GetString(s, SAVE_MESSAGEOPTIONS_NAME);
257 		int type;
258 
259 		for (type = 0; type < NT_NUM_NOTIFYTYPE; type++) {
260 			if (Q_streq(nt_strings[type], messagetype))
261 				break;
262 		}
263 
264 		/** @todo (menu) check why this message is not shown anywhere in logs*/
265 		if (type == NT_NUM_NOTIFYTYPE) {
266 			Com_Printf("Unrecognized messagetype '%s' ignored while loading\n", messagetype);
267 			continue;
268 		}
269 		MSO_Set(0, (notify_t)type, MSO_NOTIFY, cgi->XML_GetBool(s, SAVE_MESSAGEOPTIONS_NOTIFY, false), false);
270 		MSO_Set(0, (notify_t)type, MSO_PAUSE, cgi->XML_GetBool(s, SAVE_MESSAGEOPTIONS_PAUSE, false), false);
271 		MSO_Set(0, (notify_t)type, MSO_SOUND, cgi->XML_GetBool(s, SAVE_MESSAGEOPTIONS_SOUND, false), false);
272 	}
273 
274 	MSO_SetMenuState(MSO_MSTATE_REINIT, false, false);
275 	return true;
276 }
277 
278 /**
279  * @brief parses message options settings from file.
280  */
MSO_ParseOption(const char * blockName,const char ** text)281 static int MSO_ParseOption (const char* blockName, const char** text)
282 {
283 	const char* errhead = "MSO_ParseSettings: unexpected end of file (names ";
284 	const char* token;
285 
286 	/* get name list body body */
287 	token = Com_Parse(text);
288 
289 	if (!*text || *token !='{') {
290 		Com_Printf("MSO_ParseOption: settingslist \"%s\" without body ignored\n", blockName);
291 		return -1;
292 	}
293 
294 	int messageType = -1;
295 	linkedList_t* status = nullptr;
296 
297 	do {
298 		/* get the msg type*/
299 		token = cgi->Com_EParse(text, errhead, blockName);
300 		if (!*text) {
301 			Com_Printf("MSO_ParseOption: end of file not expected \"%s\"\n", blockName);
302 			return -1;
303 		}
304 		if (token[0] == '}')
305 			break;
306 
307 		if (Q_streq(token, "type")) {
308 			token = cgi->Com_EParse(text, errhead, blockName);
309 			messageType = MSO_ParseNotifyType(token);
310 		} else if (Q_streq(token, "status")) {
311 			if (status != nullptr) {
312 				Com_Printf("MSO_ParseOption: status already defined. Previous definition ignored.\n");
313 				cgi->LIST_Delete(&status);
314 			} else if (!Com_ParseList(text, &status)) {
315 				Com_Printf("MSO_ParseOption: error while parsing option status.\n");
316 				return -1;
317 			}
318 		} else {
319 			Com_Printf("MSO_ParseOption: token \"%s\" in \"%s\" not expected.\n", token, blockName);
320 			return -1;
321 		}
322 	} while (*text);
323 
324 	if (messageType == -1) {
325 		Com_Printf("MSO_ParseOption: message option type undefined.\n");
326 		return -1;
327 	}
328 
329 	for (linkedList_t* element = status; element != nullptr; element = element->next) {
330 		const char* value = (const char*)element->data;
331 		const int optionType = MSO_ParseOptionType(value);
332 		if (optionType == 0) {
333 			Com_Printf("MSO_ParseOption: message option type \"%s\" undefined.\n", value);
334 			continue;
335 		}
336 		MSO_Set(0, (notify_t)messageType, optionType, 1, false);
337 	}
338 
339 	/* reset menu state, was updated by msgoptions_set */
340 	MSO_SetMenuState(MSO_MSTATE_REINIT, false, false);
341 
342 	return messageType;
343 }
344 
345 /**
346  * @brief Parses a messagecategory script section. These categories are used to group notification types.
347  * @sa MSO_InitTextList
348  */
MSO_ParseCategory(const char * blockName,const char ** text)349 static bool MSO_ParseCategory (const char* blockName, const char** text)
350 {
351 	const char* errhead = "MSO_ParseCategory: unexpected end of file (names ";
352 	const char* token;
353 	msgCategory_t* category;
354 	msgCategoryEntry_t* categoryEntry;
355 
356 	/* get name list body body */
357 	token = Com_Parse(text);
358 
359 	if (!*text || *token != '{') {
360 		Com_Printf("MSO_ParseCategory: category without body\n");
361 		return false;
362 	}
363 
364 	/* add category */
365 	if (ccs.numMsgCategories >= MAX_MESSAGECATEGORIES) {
366 		Com_Printf("MSO_ParseCategory: too many messagecategory defs\n");
367 		return false;
368 	}
369 
370 	/* QUESTION this structure looks useless, categoryEntry is enough */
371 	category = &ccs.messageCategories[ccs.numMsgCategories];
372 
373 	OBJZERO(*category);
374 	category->idx = ccs.numMsgCategories;	/* set self-link */
375 
376 	categoryEntry = &ccs.msgCategoryEntries[ccs.numMsgCategoryEntries];
377 
378 	/* first entry is category */
379 	OBJZERO(*categoryEntry);
380 	categoryEntry->category = &ccs.messageCategories[ccs.numMsgCategories];
381 	category->last = category->first = &ccs.msgCategoryEntries[ccs.numMsgCategoryEntries];
382 	categoryEntry->previous = nullptr;
383 	categoryEntry->next = nullptr;
384 	categoryEntry->isCategory = true;
385 	ccs.numMsgCategoryEntries++;
386 
387 	do {
388 		/* get entries and add them to category */
389 		token = cgi->Com_EParse(text, errhead, blockName);
390 		if (!*text) {
391 			Com_Printf("MSO_ParseMessageSettings: end of file not expected\n");
392 			return false;
393 		}
394 		if (token[0] == '}')
395 			break;
396 
397 		if (Q_streq(token, "option")) {
398 			int optionId = MSO_ParseOption(blockName, text);
399 			if (optionId == -1) {
400 				cgi->Com_Error(ERR_DROP, "MSO_ParseMessageSettings: error while parsing option from \"%s\".\n", blockName);
401 			}
402 			/* prepare a new msgcategory entry */
403 			msgCategoryEntry_t* previous = ccs.messageCategories[ccs.numMsgCategories].last;
404 			msgCategoryEntry_t* entry = &ccs.msgCategoryEntries[ccs.numMsgCategoryEntries];
405 			OBJZERO(*entry);
406 			ccs.messageCategories[ccs.numMsgCategories].last = entry;
407 			previous->next = entry;
408 
409 			entry->category = &ccs.messageCategories[ccs.numMsgCategories];
410 			entry->previous = previous;
411 			entry->next = nullptr;
412 			entry->notifyType = nt_strings[optionId];
413 			entry->settings = &messageSettings[optionId];
414 			ccs.numMsgCategoryEntries++;
415 		} else if (Q_streq(token, "name")) {
416 			token = cgi->Com_EParse(text, errhead, blockName);
417 			if (!*text) {
418 				Com_Printf("MSO_ParseMessageSettings: end of file not expected\n");
419 				return false;
420 			}
421 			/* skip translation token */
422 			if (token[0] == '_') {
423 				token++;
424 			}
425 			category->name = Mem_PoolStrDup(token, cp_campaignPool, 0);
426 			categoryEntry->notifyType = category->name;
427 		} else {
428 			cgi->Com_Error(ERR_DROP, "MSO_ParseMessageSettings: token \"%s\" in \"%s\" not expected.\n", token, blockName);
429 		}
430 	} while (*text);
431 
432 	if (category->name == nullptr) {
433 		Com_Printf("MSO_ParseMessageSettings: category do not have name\n");
434 		return false;
435 	}
436 
437 	ccs.numMsgCategories++;
438 	MSO_SetMenuState(MSO_MSTATE_REINIT, false, false);
439 	return true;
440 }
441 
442 /**
443  * @brief parses message options settings from file.
444  */
MSO_ParseMessageSettings(const char * name,const char ** text)445 void MSO_ParseMessageSettings (const char* name, const char** text)
446 {
447 	const char* errhead = "MSO_ParseMessageSettings: unexpected end of file (names ";
448 	const char* token;
449 
450 	/* settings available, reset previous settings */
451 	OBJZERO(messageSettings);
452 
453 	/* get name list body body */
454 	token = Com_Parse(text);
455 
456 	if (!*text || token[0] != '{') {
457 		cgi->Com_Error(ERR_DROP, "MSO_ParseMessageSettings: msgoptions \"%s\" without body.\n", name);
458 		return;
459 	}
460 
461 	while (*text) {
462 		/* get entries and add them to category */
463 		token = cgi->Com_EParse(text, errhead, name);
464 		if (!*text)
465 			cgi->Com_Error(ERR_DROP, "MSO_ParseMessageSettings: end of file not expected \"%s\".\n", name);
466 		if (token[0] == '}')
467 			break;
468 
469 		if (Q_streq(token, "category")) {
470 			if (!MSO_ParseCategory(name, text)) {
471 				cgi->Com_Error(ERR_DROP, "MSO_ParseMessageSettings: error while parsing category from \"%s\".\n", name);
472 			}
473 		} else {
474 			cgi->Com_Error(ERR_DROP, "MSO_ParseMessageSettings: token \"%s\" in \"%s\" not expected.\n", token, name);
475 		}
476 	}
477 
478 }
479 
MSO_Init(void)480 void MSO_Init (void)
481 {
482 	cgi->Cmd_AddCommand("msgoptions_setall", MSO_SetAll_f, "Sets pause, notification or sound setting for all message categories");
483 	cgi->Cmd_AddCommand("msgoptions_set", MSO_Set_f, "Sets pause, notification or sound setting for a message category");
484 	MSO_InitCallbacks();
485 }
486 
MSO_Shutdown(void)487 void MSO_Shutdown (void)
488 {
489 	cgi->Cmd_RemoveCommand("msgoptions_setall");
490 	cgi->Cmd_RemoveCommand("msgoptions_set");
491 	MSO_ShutdownCallbacks();
492 }
493