1 /**
2  * @file
3  * @brief cgame team management.
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.m
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 "../client.h"
27 #include "cl_game.h"
28 #include "cl_game_team.h"
29 #include "../cl_inventory.h"
30 #include "../../shared/parse.h"
31 #include "../cl_team.h"
32 #include "../ui/ui_main.h"
33 #include "../ui/ui_popup.h"
34 #include "../ui/node/ui_node_container.h"	/**< ui_inventory */
35 #include "save_team.h"
36 #include "save_character.h"
37 #include "save_inventory.h"
38 
39 #define TEAM_SAVE_FILE_VERSION 4
40 
41 static Inventory game_inventory;
42 static bool characterActive[MAX_ACTIVETEAM];
43 
44 typedef struct teamSaveFileHeader_s {
45 	uint32_t version; /**< which savegame version */
46 	uint32_t soldiercount; /** the number of soldiers stored in this savegame */
47 	char name[32]; /**< savefile name */
48 	uint32_t xmlSize; /** needed, if we store compressed */
49 } teamSaveFileHeader_t;
50 
GAME_UpdateActiveTeamList(void)51 static void GAME_UpdateActiveTeamList (void)
52 {
53 	const int teamSize = LIST_Count(chrDisplayList);
54 	OBJZERO(characterActive);
55 	for (int i = 0; i < teamSize; i++)
56 		characterActive[i] = true;
57 }
58 
GAME_AutoTeam(const char * equipmentDefinitionID,int teamMembers)59 void GAME_AutoTeam (const char* equipmentDefinitionID, int teamMembers)
60 {
61 	const equipDef_t* ed = INV_GetEquipmentDefinitionByID(equipmentDefinitionID);
62 	const char* teamDefID = GAME_GetTeamDef();
63 	cls.teamSaveSlotIndex = NO_TEAM_SLOT_LOADED;
64 
65 	GAME_GenerateTeam(teamDefID, ed, teamMembers);
66 }
67 
GAME_AutoTeam_f(void)68 void GAME_AutoTeam_f (void)
69 {
70 	if (Cmd_Argc() != 2) {
71 		Com_Printf("Usage: %s <equipment-definition>\n", Cmd_Argv(0));
72 		return;
73 	}
74 
75 	GAME_AutoTeam(Cmd_Argv(1), GAME_GetCharacterArraySize());
76 	GAME_UpdateActiveTeamList();
77 }
78 
79 /**
80  * @brief This will activate/deactivate the actor for the team
81  * @sa GAME_SaveTeamState_f
82  */
GAME_ToggleActorForTeam_f(void)83 void GAME_ToggleActorForTeam_f (void)
84 {
85 	if (Cmd_Argc() != 3) {
86 		Com_Printf("Usage: %s <num> <value>\n", Cmd_Argv(0));
87 		return;
88 	}
89 
90 	int ucn = atoi(Cmd_Argv(1));
91 	int value = atoi(Cmd_Argv(2));
92 
93 	int i = 0;
94 	bool found = false;
95 	LIST_Foreach(chrDisplayList, character_t, chrTmp) {
96 		if (ucn == chrTmp->ucn) {
97 			found = true;
98 			break;
99 		}
100 		i++;
101 	}
102 	if (!found)
103 		return;
104 
105 	characterActive[i] = (value != 0);
106 }
107 
108 /**
109  * @brief Will remove those actors that should not be used in the team
110  * @sa GAME_ToggleActorForTeam_f
111  */
GAME_SaveTeamState_f(void)112 void GAME_SaveTeamState_f (void)
113 {
114 	int i = 0;
115 	LIST_Foreach(chrDisplayList, character_t, chr) {
116 		if (!characterActive[i++])
117 			LIST_Remove(&chrDisplayList, chr);
118 	}
119 }
120 
121 /**
122  * @brief Removes a user created team
123  */
GAME_TeamDelete_f(void)124 void GAME_TeamDelete_f (void)
125 {
126 	if (Cmd_Argc() != 2) {
127 		Com_Printf("Usage: %s <filename>\n", Cmd_Argv(0));
128 		return;
129 	}
130 
131 	const char* team = Cmd_Argv(1);
132 	char buf[MAX_OSPATH];
133 	GAME_GetAbsoluteSavePath(buf, sizeof(buf));
134 	Q_strcat(buf, sizeof(buf), "%s", team);
135 	FS_RemoveFile(buf);
136 }
137 
138 /**
139  * @brief Reads the comments from team files
140  */
GAME_TeamSlotComments_f(void)141 void GAME_TeamSlotComments_f (void)
142 {
143 	UI_ExecuteConfunc("teamsaveslotsclear");
144 
145 	const char* filename;
146 	int i = 0;
147 	char relSavePath[MAX_OSPATH];
148 	GAME_GetRelativeSavePath(relSavePath, sizeof(relSavePath));
149 	char pattern[MAX_OSPATH];
150 	Q_strncpyz(pattern, relSavePath, sizeof(pattern));
151 	Q_strcat(pattern, sizeof(pattern), "*.mpt");
152 
153 	FS_BuildFileList(pattern);
154 	while ((filename = FS_NextFileFromFileList(pattern)) != nullptr) {
155 		ScopedFile f;
156 		const char* savePath = va("%s/%s", relSavePath, filename);
157 		FS_OpenFile(savePath, &f, FILE_READ);
158 		if (!f) {
159 			Com_Printf("Warning: Could not open '%s'\n", filename);
160 			continue;
161 		}
162 		teamSaveFileHeader_t header;
163 		const int clen = sizeof(header);
164 		if (FS_Read(&header, clen, &f) != clen) {
165 			Com_Printf("Warning: Could not read %i bytes from savefile\n", clen);
166 			continue;
167 		}
168 		if (LittleLong(header.version) != TEAM_SAVE_FILE_VERSION) {
169 			Com_Printf("Warning: Version mismatch in '%s'\n", filename);
170 			continue;
171 		}
172 
173 		char absSavePath[MAX_OSPATH];
174 		GAME_GetAbsoluteSavePath(absSavePath, sizeof(absSavePath));
175 		const bool uploadable = FS_FileExists("%s/%s", absSavePath, filename);
176 		UI_ExecuteConfunc("teamsaveslotadd %i \"%s\" \"%s\" %i %i", i++, filename, header.name, LittleLong(header.soldiercount), uploadable ? 1 : 0);
177 	}
178 	FS_NextFileFromFileList(nullptr);
179 }
180 
181 /**
182  * @brief Stores the wholeTeam into a xml structure
183  * @note Called by GAME_SaveTeam to store the team info
184  * @sa GAME_SendCurrentTeamSpawningInfo
185  */
GAME_SaveTeamInfo(xmlNode_t * p)186 static void GAME_SaveTeamInfo (xmlNode_t* p)
187 {
188 	LIST_Foreach(chrDisplayList, character_t, chr) {
189 		xmlNode_t* n = XML_AddNode(p, SAVE_TEAM_CHARACTER);
190 		GAME_SaveCharacter(n, chr);
191 	}
192 }
193 
194 /**
195  * @brief Loads the wholeTeam from the xml file
196  * @note Called by GAME_LoadTeam to load the team info
197  * @sa GAME_SendCurrentTeamSpawningInfo
198  */
GAME_LoadTeamInfo(xmlNode_t * p)199 static void GAME_LoadTeamInfo (xmlNode_t* p)
200 {
201 	int i;
202 	xmlNode_t* n;
203 	const size_t size = GAME_GetCharacterArraySize();
204 
205 	GAME_ResetCharacters();
206 	OBJZERO(characterActive);
207 
208 	/* header */
209 	for (i = 0, n = XML_GetNode(p, SAVE_TEAM_CHARACTER); n && i < size; i++, n = XML_GetNextNode(n, p, SAVE_TEAM_CHARACTER)) {
210 		character_t* chr = GAME_GetCharacter(i);
211 		GAME_LoadCharacter(n, chr);
212 		UI_ExecuteConfunc("team_memberadd %i \"%s\" \"%s\" %i", i, chr->name, chr->head, chr->headSkin);
213 		LIST_AddPointer(&chrDisplayList, (void*)chr);
214 	}
215 
216 	GAME_UpdateActiveTeamList();
217 }
218 
219 /**
220  * @brief Saves a team in xml format
221  */
GAME_SaveTeam(const char * filename,const char * name)222 static bool GAME_SaveTeam (const char* filename, const char* name)
223 {
224 	int requiredBufferLength;
225 	teamSaveFileHeader_t header;
226 	char dummy[2];
227 	int i;
228 	xmlNode_t* topNode, *node, *snode;
229 	equipDef_t* ed = GAME_GetEquipmentDefinition();
230 
231 	topNode = mxmlNewXML("1.0");
232 	node = XML_AddNode(topNode, SAVE_TEAM_ROOTNODE);
233 	OBJZERO(header);
234 	header.version = LittleLong(TEAM_SAVE_FILE_VERSION);
235 	header.soldiercount = LittleLong(LIST_Count(chrDisplayList));
236 	Q_strncpyz(header.name, name, sizeof(header.name));
237 
238 	snode = XML_AddNode(node, SAVE_TEAM_NODE);
239 	GAME_SaveTeamInfo(snode);
240 
241 	snode = XML_AddNode(node, SAVE_TEAM_EQUIPMENT);
242 	for (i = 0; i < csi.numODs; i++) {
243 		const objDef_t* od = INVSH_GetItemByIDX(i);
244 		if (ed->numItems[od->idx] || ed->numItemsLoose[od->idx]) {
245 			xmlNode_t* ssnode = XML_AddNode(snode, SAVE_TEAM_ITEM);
246 			XML_AddString(ssnode, SAVE_TEAM_ID, od->id);
247 			XML_AddIntValue(ssnode, SAVE_TEAM_NUM, ed->numItems[od->idx]);
248 			XML_AddIntValue(ssnode, SAVE_TEAM_NUMLOOSE, ed->numItemsLoose[od->idx]);
249 		}
250 	}
251 	requiredBufferLength = mxmlSaveString(topNode, dummy, 2, MXML_NO_CALLBACK);
252 	/* required for storing compressed */
253 	header.xmlSize = LittleLong(requiredBufferLength);
254 
255 	byte* const buf = Mem_PoolAllocTypeN(byte, requiredBufferLength + 1, cl_genericPool);
256 	if (!buf) {
257 		mxmlDelete(topNode);
258 		Com_Printf("Error: Could not allocate enough memory to save this game\n");
259 		return false;
260 	}
261 	mxmlSaveString(topNode, (char*)buf, requiredBufferLength + 1, MXML_NO_CALLBACK);
262 	mxmlDelete(topNode);
263 
264 	byte* const fbuf = Mem_PoolAllocTypeN(byte, requiredBufferLength + 1 + sizeof(header), cl_genericPool);
265 	memcpy(fbuf, &header, sizeof(header));
266 	memcpy(fbuf + sizeof(header), buf, requiredBufferLength + 1);
267 	Mem_Free(buf);
268 
269 	/* last step - write data */
270 	FS_WriteFile(fbuf, requiredBufferLength + 1 + sizeof(header), filename);
271 	Mem_Free(fbuf);
272 
273 	return true;
274 }
275 
276 /**
277  * @brief Searches a free team filename.
278  * @param[out] filename The team filename that can be used.
279  * @param[in] size The size of the @c filename buffer.
280  * @return @c true if a valid team name was found, @c false otherwise. In the latter case,
281  * don't use anything from the @c filename buffer
282  */
GAME_TeamGetFreeFilename(char * filename,size_t size)283 bool GAME_TeamGetFreeFilename (char* filename, size_t size)
284 {
285 	char buf[MAX_OSPATH];
286 	GAME_GetRelativeSavePath(buf, sizeof(buf));
287 
288 	for (int num = 0; num < 100; num++) {
289 		Com_sprintf(filename, size, "%s/team%02i.mpt", buf, num);
290 		if (FS_CheckFile("%s", filename) == -1) {
291 			return true;
292 		}
293 	}
294 	return false;
295 }
296 
297 /**
298  * @brief Stores a team in a specified teamslot
299  */
GAME_SaveTeam_f(void)300 void GAME_SaveTeam_f (void)
301 {
302 	if (Cmd_Argc() != 2) {
303 		Com_Printf("Usage: %s <name>\n", Cmd_Argv(0));
304 		return;
305 	}
306 
307 	if (LIST_IsEmpty(chrDisplayList)) {
308 		UI_Popup(_("Note"), _("Error saving team. Nothing to save yet."));
309 		return;
310 	}
311 
312 	const char* name = Cmd_Argv(1);
313 	if (Q_strnull(name))
314 		name = _("New Team");
315 
316 	char filename[MAX_OSPATH];
317 	if (!GAME_TeamGetFreeFilename(filename, sizeof(filename))) {
318 		UI_Popup(_("Note"), _("Error saving team. Too many teams!"));
319 		return;
320 	}
321 
322 	if (!GAME_SaveTeam(filename, name))
323 		UI_Popup(_("Note"), _("Error saving team. Check free disk space!"));
324 }
325 
326 /**
327  * @brief Load a team from an xml file
328  */
GAME_LoadTeam(const char * filename)329 static bool GAME_LoadTeam (const char* filename)
330 {
331 	int clen;
332 	xmlNode_t* topNode, *node, *snode, *ssnode;
333 	teamSaveFileHeader_t header;
334 	equipDef_t* ed;
335 
336 	/* open file */
337 	ScopedFile f;
338 	FS_OpenFile(filename, &f, FILE_READ);
339 	if (!f) {
340 		Com_Printf("Couldn't open file '%s'\n", filename);
341 		return false;
342 	}
343 
344 	clen = FS_FileLength(&f);
345 	byte* const cbuf = Mem_PoolAllocTypeN(byte, clen, cl_genericPool);
346 	if (FS_Read(cbuf, clen, &f) != clen) {
347 		Com_Printf("Warning: Could not read %i bytes from savefile\n", clen);
348 		Mem_Free(cbuf);
349 		return false;
350 	}
351 
352 	memcpy(&header, cbuf, sizeof(header));
353 	/* swap all int values if needed */
354 	header.version = LittleLong(header.version);
355 	header.xmlSize = LittleLong(header.xmlSize);
356 
357 	if (header.version != TEAM_SAVE_FILE_VERSION) {
358 		Com_Printf("Invalid version number\n");
359 		Mem_Free(cbuf);
360 		return false;
361 	}
362 
363 	Com_Printf("Loading team (size %d / %i)\n", clen, header.xmlSize);
364 
365 	topNode = XML_Parse((const char*)(cbuf + sizeof(header)));
366 	Mem_Free(cbuf);
367 	if (!topNode) {
368 		Com_Printf("Error: Failure in loading the xml data!");
369 		return false;
370 	}
371 
372 	node = XML_GetNode(topNode, SAVE_TEAM_ROOTNODE);
373 	if (!node) {
374 		mxmlDelete(topNode);
375 		Com_Printf("Error: Failure in loading the xml data! (node '" SAVE_TEAM_ROOTNODE "' not found)\n");
376 		return false;
377 	}
378 
379 	snode = XML_GetNode(node, SAVE_TEAM_NODE);
380 	if (!snode) {
381 		mxmlDelete(topNode);
382 		Com_Printf("Error: Failure in loading the xml data! (node '" SAVE_TEAM_NODE "' not found)\n");
383 		return false;
384 	}
385 	GAME_LoadTeamInfo(snode);
386 
387 	snode = XML_GetNode(node, SAVE_TEAM_EQUIPMENT);
388 	if (!snode) {
389 		mxmlDelete(topNode);
390 		Com_Printf("Error: Failure in loading the xml data! (node '" SAVE_TEAM_EQUIPMENT "' not found)\n");
391 		return false;
392 	}
393 
394 	ed = GAME_GetEquipmentDefinition();
395 	for (ssnode = XML_GetNode(snode, SAVE_TEAM_ITEM); ssnode; ssnode = XML_GetNextNode(ssnode, snode, SAVE_TEAM_ITEM)) {
396 		const char* objID = XML_GetString(ssnode, SAVE_TEAM_ID);
397 		const objDef_t* od = INVSH_GetItemByID(objID);
398 
399 		if (od) {
400 			ed->numItems[od->idx] = XML_GetInt(snode, SAVE_TEAM_NUM, 0);
401 			ed->numItemsLoose[od->idx] = XML_GetInt(snode, SAVE_TEAM_NUMLOOSE, 0);
402 		}
403 	}
404 
405 	Com_Printf("File '%s' loaded.\n", filename);
406 
407 	mxmlDelete(topNode);
408 
409 	return true;
410 }
411 
412 /**
413  * @brief Get the filename for the xth team in the file system
414  * @note We are only dealing with indices to identify a team, thus we have to loop
415  * the team list each time we need a filename of a team.
416  * @param[in] index The index of the team we are looking for in the filesystem
417  * @param[out] filename The filename of the team. This value is only set or valid if this function returned @c true.
418  * @param[in] filenameLength The length of the @c filename buffer.
419  * @return @c true if the filename for the given index was found, @c false otherwise (e.g. invalid index)
420  */
GAME_GetTeamFileName(unsigned int index,char * filename,size_t filenameLength)421 bool GAME_GetTeamFileName (unsigned int index, char* filename, size_t filenameLength)
422 {
423 	const char* save;
424 	/* we will loop the whole team save list, just because i don't want
425 	 * to specify the filename in the script api of this command. Otherwise
426 	 * one could upload everything with this command */
427 	char buf[MAX_OSPATH];
428 	GAME_GetRelativeSavePath(buf, sizeof(buf));
429 	char pattern[MAX_OSPATH];
430 	Com_sprintf(pattern, sizeof(pattern), "%s*.mpt", buf);
431 	const int amount = FS_BuildFileList(pattern);
432 	Com_DPrintf(DEBUG_CLIENT, "found %i entries for %s\n", amount, pattern);
433 	while ((save = FS_NextFileFromFileList(pattern)) != nullptr) {
434 		if (index == 0)
435 			break;
436 		index--;
437 	}
438 	FS_NextFileFromFileList(nullptr);
439 	if (index > 0)
440 		return false;
441 	if (save == nullptr)
442 		return false;
443 	Com_sprintf(filename, filenameLength, "%s/%s", buf, save);
444 	return true;
445 }
446 
GAME_LoadDefaultTeam(bool force)447 bool GAME_LoadDefaultTeam (bool force)
448 {
449 	if (cls.teamSaveSlotIndex == NO_TEAM_SLOT_LOADED) {
450 		if (!force)
451 			return false;
452 		cls.teamSaveSlotIndex = 0;
453 	}
454 
455 	char filename[MAX_OSPATH];
456 	if (!GAME_GetTeamFileName(cls.teamSaveSlotIndex, filename, sizeof(filename)))
457 		return false;
458 	if (GAME_LoadTeam(filename) && !GAME_IsTeamEmpty()) {
459 		return true;
460 	}
461 
462 	cls.teamSaveSlotIndex = NO_TEAM_SLOT_LOADED;
463 	return false;
464 }
465 
466 /**
467  * @brief Loads the selected teamslot
468  */
GAME_LoadTeam_f(void)469 void GAME_LoadTeam_f (void)
470 {
471 	if (Cmd_Argc() != 2) {
472 		Com_Printf("Usage: %s <slotindex>\n", Cmd_Argv(0));
473 		return;
474 	}
475 
476 	const int index = atoi(Cmd_Argv(1));
477 
478 	char filename[MAX_OSPATH];
479 	if (!GAME_GetTeamFileName(index, filename, sizeof(filename))) {
480 		Com_Printf("Could not get the file for the index: %i\n", index);
481 		return;
482 	}
483 	if (GAME_LoadTeam(filename) && !GAME_IsTeamEmpty()) {
484 		cls.teamSaveSlotIndex = index;
485 	} else {
486 		cls.teamSaveSlotIndex = NO_TEAM_SLOT_LOADED;
487 	}
488 }
489 
GAME_UpdateInventory(Inventory * inv,const equipDef_t * ed)490 static void GAME_UpdateInventory (Inventory* inv, const equipDef_t* ed)
491 {
492 	if (!LIST_IsEmpty(chrDisplayList))
493 		ui_inventory = &((character_t*)chrDisplayList->data)->inv;
494 	else
495 		ui_inventory = nullptr;
496 
497 	/* manage inventory */
498 	UI_ContainerNodeUpdateEquipment(inv, ed);
499 }
500 
501 /**
502  * @brief Get the equipment definition (from script files) for the current selected team
503  * and updates the equipment inventory containers
504  */
GAME_GetEquipment(void)505 static void GAME_GetEquipment (void)
506 {
507 	const char* equipmentName = Cvar_GetString("cl_equip");
508 	/* search equipment definition */
509 	const equipDef_t* edFromScript = INV_GetEquipmentDefinitionByID(equipmentName);
510 
511 	equipDef_t* ed = GAME_GetEquipmentDefinition();
512 	*ed = *edFromScript;
513 
514 	game_inventory.init();
515 	GAME_UpdateInventory(&game_inventory, ed);
516 }
517 
518 /**
519  * @brief Displays actor info and equipment and unused items in proper (filter) category.
520  * @note This function is called every time the team equipment screen for the team pops up.
521  */
GAME_UpdateTeamMenuParameters_f(void)522 void GAME_UpdateTeamMenuParameters_f (void)
523 {
524 	/* reset description */
525 	Cvar_Set("mn_itemname", "");
526 	Cvar_Set("mn_item", "");
527 	UI_ResetData(TEXT_STANDARD);
528 
529 	int i = 0;
530 	UI_ExecuteConfunc("team_soldierlist_clear");
531 	LIST_Foreach(chrDisplayList, character_t, chr) {
532 		UI_ExecuteConfunc("team_soldierlist_add %d %d \"%s\"", chr->ucn, characterActive[i], chr->name);
533 		i++;
534 	}
535 
536 	GAME_GetEquipment();
537 }
538 
GAME_ActorSelect_f(void)539 void GAME_ActorSelect_f (void)
540 {
541 	/* check syntax */
542 	if (Cmd_Argc() < 2) {
543 		Com_Printf("Usage: %s <num>\n", Cmd_Argv(0));
544 		return;
545 	}
546 
547 	const int ucn = atoi(Cmd_Argv(1));
548 	character_t* chr = nullptr;
549 	LIST_Foreach(chrDisplayList, character_t, chrTmp) {
550 		if (ucn == chrTmp->ucn) {
551 			chr = chrTmp;
552 			break;
553 		}
554 	}
555 	if (!chr)
556 		return;
557 
558 	/* update menu inventory */
559 	if (ui_inventory && ui_inventory != &chr->inv) {
560 		chr->inv.setContainer(CID_EQUIP, ui_inventory->getEquipContainer());
561 		/* set 'old' CID_EQUIP to nullptr */
562 		ui_inventory->resetContainer(CID_EQUIP);
563 	}
564 	ui_inventory = &chr->inv;
565 	/* set info cvars */
566 	CL_UpdateCharacterValues(chr);
567 }
568 
569 /**
570  * @brief Save one item
571  * @param[out] p XML Node structure, where we write the information to
572  * @param[in] item Pointer to the item to save
573  * @param[in] container Index of the container the item is in
574  * @param[in] x Horizontal coordinate of the item in the container
575  * @param[in] y Vertical coordinate of the item in the container
576  * @sa GAME_LoadItem
577  */
GAME_SaveItem(xmlNode_t * p,const Item * item,containerIndex_t container,int x,int y)578 static void GAME_SaveItem (xmlNode_t* p, const Item* item, containerIndex_t container, int x, int y)
579 {
580 	assert(item->def() != nullptr);
581 
582 	XML_AddString(p, SAVE_INVENTORY_CONTAINER, INVDEF(container)->name);
583 	XML_AddInt(p, SAVE_INVENTORY_X, x);
584 	XML_AddInt(p, SAVE_INVENTORY_Y, y);
585 	XML_AddIntValue(p, SAVE_INVENTORY_ROTATED, item->rotated);
586 	XML_AddString(p, SAVE_INVENTORY_WEAPONID, item->def()->id);
587 	/** @todo: is there any case when amount != 1 for soldier inventory item? */
588 	XML_AddInt(p, SAVE_INVENTORY_AMOUNT, item->getAmount());
589 	if (item->getAmmoLeft() > NONE_AMMO) {
590 		XML_AddString(p, SAVE_INVENTORY_MUNITIONID, item->ammoDef()->id);
591 		XML_AddInt(p, SAVE_INVENTORY_AMMO, item->getAmmoLeft());
592 	}
593 }
594 
595 /**
596  * @brief Save callback for savegames in XML Format
597  * @param[out] p XML Node structure, where we write the information to
598  * @param[in] inv Pointer to the inventory to save
599  * @sa GAME_SaveItem
600  * @sa GAME_LoadInventory
601  */
GAME_SaveInventory(xmlNode_t * p,const Inventory * inv)602 static void GAME_SaveInventory (xmlNode_t* p, const Inventory* inv)
603 {
604 	const Container* cont = nullptr;
605 	while ((cont = inv->getNextCont(cont, false))) {
606 		Item* item = nullptr;
607 		while ((item = cont->getNextItem(item))) {
608 			xmlNode_t* s = XML_AddNode(p, SAVE_INVENTORY_ITEM);
609 			GAME_SaveItem(s, item, cont->id, item->getX(), item->getY());
610 		}
611 	}
612 }
613 
614 /**
615  * @brief Load one item
616  * @param[in] n XML Node structure, where we write the information to
617  * @param[out] item Pointer to the item
618  * @param[out] container Index of the container the item is in
619  * @param[out] x Horizontal coordinate of the item in the container
620  * @param[out] y Vertical coordinate of the item in the container
621  * @sa GAME_SaveItem
622  */
GAME_LoadItem(xmlNode_t * n,Item * item,containerIndex_t * container,int * x,int * y)623 static void GAME_LoadItem (xmlNode_t* n, Item* item, containerIndex_t* container, int* x, int* y)
624 {
625 	const char* itemID = XML_GetString(n, SAVE_INVENTORY_WEAPONID);
626 	const char* contID = XML_GetString(n, SAVE_INVENTORY_CONTAINER);
627 	int i;
628 
629 	/* reset */
630 	OBJZERO(*item);
631 
632 	for (i = 0; i < CID_MAX; i++) {
633 		if (Q_streq(csi.ids[i].name, contID))
634 			break;
635 	}
636 	if (i >= CID_MAX) {
637 		Com_Printf("Invalid container id '%s'\n", contID);
638 	}
639 	*container = i;
640 
641 	item->setDef(INVSH_GetItemByID(itemID));
642 	*x = XML_GetInt(n, SAVE_INVENTORY_X, 0);
643 	*y = XML_GetInt(n, SAVE_INVENTORY_Y, 0);
644 	item->rotated = XML_GetInt(n, SAVE_INVENTORY_ROTATED, 0);
645 	item->setAmount(XML_GetInt(n, SAVE_INVENTORY_AMOUNT, 1));
646 	item->setAmmoLeft(XML_GetInt(n, SAVE_INVENTORY_AMMO, NONE_AMMO));
647 	if (item->getAmmoLeft() > NONE_AMMO) {
648 		itemID = XML_GetString(n, SAVE_INVENTORY_MUNITIONID);
649 		item->setAmmoDef(INVSH_GetItemByID(itemID));
650 
651 		/* reset ammo count if ammunition (item) not found */
652 		if (!item->ammoDef())
653 			item->setAmmoLeft(NONE_AMMO);
654 	/* no ammo needed - fire definitions are in the item */
655 	} else if (!item->isReloadable()) {
656 		item->setAmmoDef(item->def());
657 	}
658 }
659 
660 /**
661  * @brief Load callback for savegames in XML Format
662  * @param[in] p XML Node structure, where we load the information from
663  * @param[in,out] inv Pointer to the inventory
664  * @param[in] maxLoad The max load for this inventory.
665  * @sa GAME_SaveInventory
666  * @sa GAME_LoadItem
667  * @sa I_AddToInventory
668   */
GAME_LoadInventory(xmlNode_t * p,Inventory * inv,int maxLoad)669 static void GAME_LoadInventory (xmlNode_t* p, Inventory* inv, int maxLoad)
670 {
671 	xmlNode_t* s;
672 
673 	for (s = XML_GetNode(p, SAVE_INVENTORY_ITEM); s; s = XML_GetNextNode(s, p, SAVE_INVENTORY_ITEM)) {
674 		Item item;
675 		containerIndex_t container;
676 		int x, y;
677 
678 		GAME_LoadItem(s, &item, &container, &x, &y);
679 		if (INVDEF(container)->temp)
680 			Com_Error(ERR_DROP, "GAME_LoadInventory failed, tried to add '%s' to a temp container %i", item.def()->id, container);
681 		/* ignore the overload for now */
682 		if (!inv->canHoldItemWeight(CID_EQUIP, container, item, maxLoad))
683 			Com_Printf("GAME_LoadInventory: Item %s exceeds weight capacity\n", item.def()->id);
684 
685 		if (!cls.i.addToInventory(inv, &item, INVDEF(container), x, y, 1))
686 			Com_Printf("Could not add item '%s' to inventory\n", item.def() ? item.def()->id : "nullptr");
687 	}
688 }
689 
690 /**
691  * @brief saves a character to a given xml node
692  * @param[in] p The node to which we should save the character
693  * @param[in] chr The character we should save
694  */
GAME_SaveCharacter(xmlNode_t * p,const character_t * chr)695 bool GAME_SaveCharacter (xmlNode_t* p, const character_t* chr)
696 {
697 	xmlNode_t* sScore;
698 	xmlNode_t* sInventory;
699 	xmlNode_t* sInjuries;
700 	const chrScoreGlobal_t* score;
701 
702 	assert(chr);
703 	Com_RegisterConstList(saveCharacterConstants);
704 	/* Store the character data */
705 	XML_AddString(p, SAVE_CHARACTER_NAME, chr->name);
706 	XML_AddString(p, SAVE_CHARACTER_BODY, chr->body);
707 	XML_AddString(p, SAVE_CHARACTER_PATH, chr->path);
708 	XML_AddString(p, SAVE_CHARACTER_HEAD, chr->head);
709 	XML_AddInt(p, SAVE_CHARACTER_BDOY_SKIN, chr->bodySkin);
710 	XML_AddInt(p, SAVE_CHARACTER_HEAD_SKIN, chr->headSkin);
711 	XML_AddString(p, SAVE_CHARACTER_TEAMDEF, chr->teamDef->id);
712 	XML_AddInt(p, SAVE_CHARACTER_GENDER, chr->gender);
713 	XML_AddInt(p, SAVE_CHARACTER_UCN, chr->ucn);
714 	XML_AddInt(p, SAVE_CHARACTER_MAXHP, chr->maxHP);
715 	XML_AddInt(p, SAVE_CHARACTER_HP, chr->HP);
716 	XML_AddInt(p, SAVE_CHARACTER_STUN, chr->STUN);
717 	XML_AddInt(p, SAVE_CHARACTER_MORALE, chr->morale);
718 	XML_AddInt(p, SAVE_CHARACTER_FIELDSIZE, chr->fieldSize);
719 	XML_AddIntValue(p, SAVE_CHARACTER_STATE, chr->state);
720 
721 	const int implants = lengthof(chr->implants);
722 	xmlNode_t* sImplants = XML_AddNode(p, SAVE_CHARACTER_IMPLANTS);
723 	for (int i = 0; i < implants; i++) {
724 		const implant_t& implant = chr->implants[i];
725 		if (implant.def == nullptr)
726 			continue;
727 
728 		xmlNode_t* sImplant = XML_AddNode(sImplants, SAVE_CHARACTER_IMPLANT);
729 		XML_AddIntValue(sImplant, SAVE_CHARACTER_IMPLANT_INSTALLEDTIME, implant.installedTime);
730 		XML_AddIntValue(sImplant, SAVE_CHARACTER_IMPLANT_REMOVETIME, implant.removedTime);
731 		XML_AddString(sImplant, SAVE_CHARACTER_IMPLANT_IMPLANT, implant.def->id);
732 	}
733 
734 	sInjuries = XML_AddNode(p, SAVE_CHARACTER_INJURIES);
735 	/* Store wounds */
736 	for (int k = 0; k < chr->teamDef->bodyTemplate->numBodyParts(); ++k) {
737 		if (!chr->wounds.treatmentLevel[k])
738 			continue;
739 		xmlNode_t* sWound = XML_AddNode(sInjuries, SAVE_CHARACTER_WOUND);
740 
741 		XML_AddString(sWound, SAVE_CHARACTER_WOUNDEDPART, chr->teamDef->bodyTemplate->id(k));
742 		XML_AddInt(sWound, SAVE_CHARACTER_WOUNDSEVERITY, chr->wounds.treatmentLevel[k]);
743 	}
744 
745 	score = &chr->score;
746 
747 	sScore = XML_AddNode(p, SAVE_CHARACTER_SCORES);
748 	/* Store skills */
749 	for (int k = 0; k <= SKILL_NUM_TYPES; k++) {
750 		if (score->experience[k] || score->initialSkills[k]
751 		 || (k < SKILL_NUM_TYPES && score->skills[k])) {
752 			xmlNode_t* sSkill = XML_AddNode(sScore, SAVE_CHARACTER_SKILLS);
753 			const int initial = score->initialSkills[k];
754 			const int experience = score->experience[k];
755 
756 			XML_AddString(sSkill, SAVE_CHARACTER_SKILLTYPE, Com_GetConstVariable(SAVE_CHARACTER_SKILLTYPE_NAMESPACE, k));
757 			XML_AddIntValue(sSkill, SAVE_CHARACTER_INITSKILL, initial);
758 			XML_AddIntValue(sSkill, SAVE_CHARACTER_EXPERIENCE, experience);
759 			if (k < SKILL_NUM_TYPES) {
760 				const int skills = *(score->skills + k);
761 				const int improve = skills - initial;
762 				XML_AddIntValue(sSkill, SAVE_CHARACTER_SKILLIMPROVE, improve);
763 			}
764 		}
765 	}
766 	/* Store kills */
767 	for (int k = 0; k < KILLED_NUM_TYPES; k++) {
768 		if (score->kills[k] || score->stuns[k]) {
769 			xmlNode_t* sKill = XML_AddNode(sScore, SAVE_CHARACTER_KILLS);
770 			XML_AddString(sKill, SAVE_CHARACTER_KILLTYPE, Com_GetConstVariable(SAVE_CHARACTER_KILLTYPE_NAMESPACE, k));
771 			XML_AddIntValue(sKill, SAVE_CHARACTER_KILLED, score->kills[k]);
772 			XML_AddIntValue(sKill, SAVE_CHARACTER_STUNNED, score->stuns[k]);
773 		}
774 	}
775 	XML_AddIntValue(sScore, SAVE_CHARACTER_SCORE_ASSIGNEDMISSIONS, score->assignedMissions);
776 	XML_AddInt(sScore, SAVE_CHARACTER_SCORE_RANK, score->rank);
777 
778 	/* Store inventories */
779 	sInventory = XML_AddNode(p, SAVE_INVENTORY_INVENTORY);
780 	GAME_SaveInventory(sInventory, &chr->inv);
781 
782 	Com_UnregisterConstList(saveCharacterConstants);
783 	return true;
784 }
785 
786 /**
787  * @brief Loads a character from a given xml node.
788  * @param[in] p The node from which we should load the character.
789  * @param[in] chr Pointer to the character we should load.
790  */
GAME_LoadCharacter(xmlNode_t * p,character_t * chr)791 bool GAME_LoadCharacter (xmlNode_t* p, character_t* chr)
792 {
793 	const char* s;
794 	xmlNode_t* sScore;
795 	xmlNode_t* sSkill;
796 	xmlNode_t* sKill;
797 	xmlNode_t* sInventory;
798 	xmlNode_t* sInjuries;
799 	xmlNode_t* sWound;
800 	bool success = true;
801 
802 	/* Load the character data */
803 	Q_strncpyz(chr->name, XML_GetString(p, SAVE_CHARACTER_NAME), sizeof(chr->name));
804 	Q_strncpyz(chr->body, XML_GetString(p, SAVE_CHARACTER_BODY), sizeof(chr->body));
805 	Q_strncpyz(chr->path, XML_GetString(p, SAVE_CHARACTER_PATH), sizeof(chr->path));
806 	Q_strncpyz(chr->head, XML_GetString(p, SAVE_CHARACTER_HEAD), sizeof(chr->head));
807 
808 	const int maxSkins = CL_GetActorSkinCount() - 1;
809 	const int bodySkin = XML_GetInt(p, SAVE_CHARACTER_BDOY_SKIN, 0);
810 	chr->bodySkin = std::min(maxSkins, bodySkin);
811 	chr->headSkin = XML_GetInt(p, SAVE_CHARACTER_HEAD_SKIN, 0);
812 	chr->gender = XML_GetInt(p, SAVE_CHARACTER_GENDER, 0);
813 	chr->ucn = XML_GetInt(p, SAVE_CHARACTER_UCN, 0);
814 	chr->maxHP = XML_GetInt(p, SAVE_CHARACTER_MAXHP, 0);
815 	chr->HP = XML_GetInt(p, SAVE_CHARACTER_HP, 0);
816 	chr->STUN = XML_GetInt(p, SAVE_CHARACTER_STUN, 0);
817 	chr->morale = XML_GetInt(p, SAVE_CHARACTER_MORALE, 0);
818 	chr->fieldSize = XML_GetInt(p, SAVE_CHARACTER_FIELDSIZE, 1);
819 	chr->state = XML_GetInt(p, SAVE_CHARACTER_STATE, 0);
820 
821 	/* Team-definition */
822 	s = XML_GetString(p, SAVE_CHARACTER_TEAMDEF);
823 	chr->teamDef = Com_GetTeamDefinitionByID(s);
824 	if (!chr->teamDef)
825 		return false;
826 
827 	sInjuries = XML_GetNode(p, SAVE_CHARACTER_INJURIES);
828 	/* Load wounds */
829 	for (sWound = XML_GetNode(sInjuries, SAVE_CHARACTER_WOUND); sWound; sWound = XML_GetNextNode(sWound, sInjuries, SAVE_CHARACTER_WOUND)) {
830 		const char* bodyPartId = XML_GetString(sWound, SAVE_CHARACTER_WOUNDEDPART);
831 		short bodyPart;
832 
833 		if (bodyPartId[0] != '\0') {
834 			for (bodyPart = 0; bodyPart < chr->teamDef->bodyTemplate->numBodyParts(); ++bodyPart)
835 				if (Q_streq(chr->teamDef->bodyTemplate->id(bodyPart), bodyPartId))
836 					break;
837 		} else {
838 			/** @todo Fallback compatibility code for older saves */
839 			Com_Printf("GAME_LoadCharacter: Body part id not found while loading character wounds, must be an old save\n");
840 			bodyPart = XML_GetInt(sWound, SAVE_CHARACTER_WOUNDTYPE, NONE);
841 		}
842 		if (bodyPart < 0  || bodyPart >= chr->teamDef->bodyTemplate->numBodyParts()) {
843 			Com_Printf("GAME_LoadCharacter: Invalid body part id '%s' for %s (ucn: %i)\n", bodyPartId, chr->name, chr->ucn);
844 			return false;
845 		}
846 		chr->wounds.treatmentLevel[bodyPart] = XML_GetInt(sWound, SAVE_CHARACTER_WOUNDSEVERITY, 0);
847 	}
848 
849 	xmlNode_t* sImplants = XML_GetNode(p, SAVE_CHARACTER_IMPLANTS);
850 	/* Load implants */
851 	int implantCnt = 0;
852 	for (xmlNode_t* sImplant = XML_GetNode(sImplants, SAVE_CHARACTER_IMPLANT); sImplant; sImplant = XML_GetNextNode(sImplant, sImplants, SAVE_CHARACTER_IMPLANT)) {
853 		implant_t& implant = chr->implants[implantCnt++];
854 		implant.installedTime = XML_GetInt(sImplants, SAVE_CHARACTER_IMPLANT_INSTALLEDTIME, 0);
855 		implant.removedTime = XML_GetInt(sImplants, SAVE_CHARACTER_IMPLANT_REMOVETIME, 0);
856 		const char* implantDefID = XML_GetString(sImplants, SAVE_CHARACTER_IMPLANT_IMPLANT);
857 		implant.def = INVSH_GetImplantByID(implantDefID);
858 	}
859 
860 	Com_RegisterConstList(saveCharacterConstants);
861 
862 	sScore = XML_GetNode(p, SAVE_CHARACTER_SCORES);
863 	/* Load Skills */
864 	for (sSkill = XML_GetNode(sScore, SAVE_CHARACTER_SKILLS); sSkill; sSkill = XML_GetNextNode(sSkill, sScore, SAVE_CHARACTER_SKILLS)) {
865 		int idx;
866 		const char* type = XML_GetString(sSkill, SAVE_CHARACTER_SKILLTYPE);
867 
868 		if (!Com_GetConstIntFromNamespace(SAVE_CHARACTER_SKILLTYPE_NAMESPACE, type, &idx)) {
869 			Com_Printf("Invalid skill type '%s' for %s (ucn: %i)\n", type, chr->name, chr->ucn);
870 			success = false;
871 			break;
872 		}
873 
874 		chr->score.initialSkills[idx] = XML_GetInt(sSkill, SAVE_CHARACTER_INITSKILL, 0);
875 		chr->score.experience[idx] = XML_GetInt(sSkill, SAVE_CHARACTER_EXPERIENCE, 0);
876 		if (idx < SKILL_NUM_TYPES) {
877 			chr->score.skills[idx] = chr->score.initialSkills[idx];
878 			chr->score.skills[idx] += XML_GetInt(sSkill, SAVE_CHARACTER_SKILLIMPROVE, 0);
879 		}
880 	}
881 
882 	if (!success) {
883 		Com_UnregisterConstList(saveCharacterConstants);
884 		return false;
885 	}
886 
887 	/* Load kills */
888 	for (sKill = XML_GetNode(sScore, SAVE_CHARACTER_KILLS); sKill; sKill = XML_GetNextNode(sKill, sScore, SAVE_CHARACTER_KILLS)) {
889 		int idx;
890 		const char* type = XML_GetString(sKill, SAVE_CHARACTER_KILLTYPE);
891 
892 		if (!Com_GetConstIntFromNamespace(SAVE_CHARACTER_KILLTYPE_NAMESPACE, type, &idx)) {
893 			Com_Printf("Invalid kill type '%s' for %s (ucn: %i)\n", type, chr->name, chr->ucn);
894 			success = false;
895 			break;
896 		}
897 		chr->score.kills[idx] = XML_GetInt(sKill, SAVE_CHARACTER_KILLED, 0);
898 		chr->score.stuns[idx] = XML_GetInt(sKill, SAVE_CHARACTER_STUNNED, 0);
899 	}
900 
901 	if (!success) {
902 		Com_UnregisterConstList(saveCharacterConstants);
903 		return false;
904 	}
905 
906 	chr->score.assignedMissions = XML_GetInt(sScore, SAVE_CHARACTER_SCORE_ASSIGNEDMISSIONS, 0);
907 	chr->score.rank = XML_GetInt(sScore, SAVE_CHARACTER_SCORE_RANK, -1);
908 
909 	cls.i.destroyInventory(&chr->inv);
910 	sInventory = XML_GetNode(p, SAVE_INVENTORY_INVENTORY);
911 	GAME_LoadInventory(sInventory, &chr->inv, GAME_GetChrMaxLoad(chr));
912 
913 	Com_UnregisterConstList(saveCharacterConstants);
914 
915 	const char* body = CHRSH_CharGetBody(chr);
916 	const char* head = CHRSH_CharGetHead(chr);
917 	if (!R_ModelExists(head) || !R_ModelExists(body)) {
918 		if (!Com_GetCharacterModel(chr))
919 			return false;
920 	}
921 
922 	return true;
923 }
924