1 /*
2 	This file is part of Warzone 2100.
3 	Copyright (C) 1999-2004  Eidos Interactive
4 	Copyright (C) 2005-2020  Warzone 2100 Project
5 
6 	Warzone 2100 is free software; you can redistribute it and/or modify
7 	it under the terms of the GNU General Public License as published by
8 	the Free Software Foundation; either version 2 of the License, or
9 	(at your option) any later version.
10 
11 	Warzone 2100 is distributed in the hope that it will be useful,
12 	but WITHOUT ANY WARRANTY; without even the implied warranty of
13 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 	GNU General Public License for more details.
15 
16 	You should have received a copy of the GNU General Public License
17 	along with Warzone 2100; if not, write to the Free Software
18 	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 /*
21  * MultiStat.c
22  *
23  * Alex Lee , pumpkin studios, EIDOS
24  *
25  * load / update / store multiplayer statistics for league tables etc...
26  */
27 
28 #include "lib/framework/file.h"
29 #include "lib/framework/frame.h"
30 #include "lib/framework/wzapp.h"
31 #include "lib/netplay/nettypes.h"
32 
33 #include "activity.h"
34 #include "clparse.h"
35 #include "main.h"
36 #include "mission.h" // for cheats
37 #include "multistat.h"
38 #include "urlrequest.h"
39 
40 #include <utility>
41 #include <memory>
42 #include <SQLiteCpp/SQLiteCpp.h>
43 
44 
45 // ////////////////////////////////////////////////////////////////////////////
46 // STATS STUFF
47 // ////////////////////////////////////////////////////////////////////////////
48 static PLAYERSTATS playerStats[MAX_PLAYERS];
49 
50 
51 // ////////////////////////////////////////////////////////////////////////////
52 // Get Player's stats
getMultiStats(UDWORD player)53 PLAYERSTATS const &getMultiStats(UDWORD player)
54 {
55 	return playerStats[player];
56 }
57 
NETauto(PLAYERSTATS::Autorating & ar)58 static void NETauto(PLAYERSTATS::Autorating &ar)
59 {
60 	NETauto(ar.valid);
61 	if (ar.valid)
62 	{
63 		NETauto(ar.dummy);
64 		NETauto(ar.star);
65 		NETauto(ar.medal);
66 		NETauto(ar.level);
67 		NETauto(ar.elo);
68 		NETauto(ar.autohoster);
69 	}
70 }
71 
Autorating(nlohmann::json const & json)72 PLAYERSTATS::Autorating::Autorating(nlohmann::json const &json)
73 {
74 	try {
75 		dummy = json["dummy"].get<bool>();
76 		star[0] = json["star"][0].get<uint8_t>();
77 		star[1] = json["star"][1].get<uint8_t>();
78 		star[2] = json["star"][2].get<uint8_t>();
79 		medal = json["medal"].get<uint8_t>();
80 		level = json["level"].get<uint8_t>();
81 		elo = json["elo"].get<std::string>();
82 		autohoster = json["autohoster"].get<bool>();
83 		valid = true;
84 	} catch (const std::exception &e) {
85 		debug(LOG_WARNING, "Error parsing rating JSON: %s", e.what());
86 	}
87 }
88 
lookupRatingAsync(uint32_t playerIndex)89 void lookupRatingAsync(uint32_t playerIndex)
90 {
91 	if (playerStats[playerIndex].identity.empty())
92 	{
93 		return;
94 	}
95 
96 	auto hash = playerStats[playerIndex].identity.publicHashString();
97 	if (hash.empty())
98 	{
99 		return;
100 	}
101 
102 	std::string url = autoratingUrl(hash);
103 	if (url.empty())
104 	{
105 		return;
106 	}
107 
108 	URLDataRequest req;
109 	req.url = url;
110 	debug(LOG_INFO, "Requesting \"%s\"", req.url.c_str());
111 	req.onSuccess = [playerIndex, hash](std::string const &url, HTTPResponseDetails const &response, std::shared_ptr<MemoryStruct> const &data) {
112 		wzAsyncExecOnMainThread([playerIndex, hash, url, response, data] {
113 			if (response.httpStatusCode() != 200 || !data || data->size == 0)
114 			{
115 				debug(LOG_WARNING, "Failed to retrieve data from \"%s\", got [%ld].", url.c_str(), response.httpStatusCode());
116 				return;
117 			}
118 			if (playerStats[playerIndex].identity.publicHashString() != hash)
119 			{
120 				debug(LOG_WARNING, "Got data from \"%s\", but player is already gone.", url.c_str());
121 				return;
122 			}
123 			try {
124 				playerStats[playerIndex].autorating = nlohmann::json::parse(data->memory, data->memory + data->size);
125 				if (playerStats[playerIndex].autorating.valid)
126 				{
127 					setMultiStats(playerIndex, playerStats[playerIndex], false);
128 				}
129 			}
130 			catch (const std::exception &e) {
131 				debug(LOG_WARNING, "JSON document from \"%s\" is invalid: %s", url.c_str(), e.what());
132 			}
133 			catch (...) {
134 				debug(LOG_FATAL, "Unexpected exception parsing JSON \"%s\"", url.c_str());
135 			}
136 		});
137 	};
138 	req.onFailure = [](std::string const &url, WZ_DECL_UNUSED URLRequestFailureType type, WZ_DECL_UNUSED optional<HTTPResponseDetails> transferDetails) {
139 		debug(LOG_WARNING, "Failure fetching \"%s\".", url.c_str());
140 	};
141 	req.maxDownloadSizeLimit = 4096;
142 	urlRequestData(req);
143 }
144 
145 // ////////////////////////////////////////////////////////////////////////////
146 // Set Player's stats
147 // send stats to all players when bLocal is false
setMultiStats(uint32_t playerIndex,PLAYERSTATS plStats,bool bLocal)148 bool setMultiStats(uint32_t playerIndex, PLAYERSTATS plStats, bool bLocal)
149 {
150 	if (playerIndex >= MAX_PLAYERS)
151 	{
152 		return true;
153 	}
154 
155 	// First copy over the data into our local array
156 	playerStats[playerIndex] = std::move(plStats);
157 
158 	if (!bLocal)
159 	{
160 		// Now send it to all other players
161 		NETbeginEncode(NETbroadcastQueue(), NET_PLAYER_STATS);
162 		// Send the ID of the player's stats we're updating
163 		NETuint32_t(&playerIndex);
164 
165 		NETauto(playerStats[playerIndex].autorating);
166 
167 		// Send over the actual stats
168 		NETuint32_t(&playerStats[playerIndex].played);
169 		NETuint32_t(&playerStats[playerIndex].wins);
170 		NETuint32_t(&playerStats[playerIndex].losses);
171 		NETuint32_t(&playerStats[playerIndex].totalKills);
172 		NETuint32_t(&playerStats[playerIndex].totalScore);
173 		NETuint32_t(&playerStats[playerIndex].recentKills);
174 		NETuint32_t(&playerStats[playerIndex].recentScore);
175 
176 		EcKey::Key identity;
177 		if (!playerStats[playerIndex].identity.empty())
178 		{
179 			identity = playerStats[playerIndex].identity.toBytes(EcKey::Public);
180 		}
181 		NETbytes(&identity);
182 		NETend();
183 	}
184 
185 	return true;
186 }
187 
recvMultiStats(NETQUEUE queue)188 void recvMultiStats(NETQUEUE queue)
189 {
190 	uint32_t playerIndex;
191 
192 	NETbeginDecode(queue, NET_PLAYER_STATS);
193 	// Retrieve the ID number of the player for which we need to
194 	// update the stats
195 	NETuint32_t(&playerIndex);
196 
197 	if (playerIndex >= MAX_PLAYERS)
198 	{
199 		NETend();
200 		return;
201 	}
202 
203 
204 	if (playerIndex != queue.index && queue.index != NET_HOST_ONLY)
205 	{
206 		HandleBadParam("NET_PLAYER_STATS given incorrect params.", playerIndex, queue.index);
207 		NETend();
208 		return;
209 	}
210 
211 	NETauto(playerStats[playerIndex].autorating);
212 
213 	// we don't what to update ourselves, we already know our score (FIXME: rewrite setMultiStats())
214 	if (!myResponsibility(playerIndex))
215 	{
216 		// Retrieve the actual stats
217 		NETuint32_t(&playerStats[playerIndex].played);
218 		NETuint32_t(&playerStats[playerIndex].wins);
219 		NETuint32_t(&playerStats[playerIndex].losses);
220 		NETuint32_t(&playerStats[playerIndex].totalKills);
221 		NETuint32_t(&playerStats[playerIndex].totalScore);
222 		NETuint32_t(&playerStats[playerIndex].recentKills);
223 		NETuint32_t(&playerStats[playerIndex].recentScore);
224 
225 		EcKey::Key identity;
226 		NETbytes(&identity);
227 		EcKey::Key prevIdentity;
228 		if (!playerStats[playerIndex].identity.empty())
229 		{
230 			prevIdentity = playerStats[playerIndex].identity.toBytes(EcKey::Public);
231 		}
232 		playerStats[playerIndex].identity.clear();
233 		if (!identity.empty())
234 		{
235 			playerStats[playerIndex].identity.fromBytes(identity, EcKey::Public);
236 		}
237 		if (identity != prevIdentity)
238 		{
239 			ingame.PingTimes[playerIndex] = PING_LIMIT;
240 		}
241 	}
242 	NETend();
243 
244 	if (realSelectedPlayer == 0 && !playerStats[playerIndex].autorating.valid)
245 	{
246 		lookupRatingAsync(playerIndex);
247 	}
248 }
249 
250 // ////////////////////////////////////////////////////////////////////////////
251 // Load Player Stats
252 
loadMultiStatsFile(const std::string & fileName,PLAYERSTATS * st,bool skipLoadingIdentity=false)253 static bool loadMultiStatsFile(const std::string& fileName, PLAYERSTATS *st, bool skipLoadingIdentity = false)
254 {
255 	char *pFileData = nullptr;
256 	UDWORD size = 0;
257 
258 	if (loadFile(fileName.c_str(), &pFileData, &size))
259 	{
260 		if (strncmp(pFileData, "WZ.STA.v3", 9) != 0)
261 		{
262 			free(pFileData);
263 			pFileData = nullptr;
264 			return false; // wrong version or not a stats file
265 		}
266 
267 		char identity[1001];
268 		identity[0] = '\0';
269 		if (!skipLoadingIdentity)
270 		{
271 			sscanf(pFileData, "WZ.STA.v3\n%u %u %u %u %u\n%1000[A-Za-z0-9+/=]",
272 			   &st->wins, &st->losses, &st->totalKills, &st->totalScore, &st->played, identity);
273 		}
274 		else
275 		{
276 			sscanf(pFileData, "WZ.STA.v3\n%u %u %u %u %u\n",
277 				   &st->wins, &st->losses, &st->totalKills, &st->totalScore, &st->played);
278 		}
279 		free(pFileData);
280 		if (identity[0] != '\0')
281 		{
282 			st->identity.fromBytes(base64Decode(identity), EcKey::Private);
283 		}
284 	}
285 
286 	return true;
287 }
288 
loadMultiStats(char * sPlayerName,PLAYERSTATS * st)289 bool loadMultiStats(char *sPlayerName, PLAYERSTATS *st)
290 {
291 	*st = PLAYERSTATS();  // clear in case we don't get to load
292 
293 	// Prevent an empty player name (where the first byte is a 0x0 terminating char already)
294 	if (!*sPlayerName)
295 	{
296 		strcpy(sPlayerName, _("Player"));
297 	}
298 
299 	std::string fileName = std::string(MultiPlayersPath) + sPlayerName + ".sta2";
300 
301 	debug(LOG_WZ, "loadMultiStats: %s", fileName.c_str());
302 
303 	// check player .sta2 already exists
304 	if (PHYSFS_exists(fileName.c_str()))
305 	{
306 		if (!loadMultiStatsFile(fileName, st))
307 		{
308 			return false;
309 		}
310 	}
311 	else
312 	{
313 		// one-time porting of old .sta player files to .sta2
314 		fileName = std::string(MultiPlayersPath) + sPlayerName + ".sta";
315 		if (PHYSFS_exists(fileName.c_str()))
316 		{
317 			if (!loadMultiStatsFile(fileName, st, true))
318 			{
319 				return false;
320 			}
321 		}
322 	}
323 
324 	if (st->identity.empty())
325 	{
326 		st->identity = EcKey::generate();  // Generate new identity.
327 		saveMultiStats(sPlayerName, sPlayerName, st);  // Save new identity.
328 	}
329 
330 	// reset recent scores
331 	st->recentKills = 0;
332 	st->recentScore = 0;
333 
334 	// clear any skirmish stats.
335 	for (size_t size = 0; size < MAX_PLAYERS; size++)
336 	{
337 		ingame.skScores[size][0] = 0;
338 		ingame.skScores[size][1] = 0;
339 	}
340 
341 	return true;
342 }
343 
344 // ////////////////////////////////////////////////////////////////////////////
345 // Save Player Stats
saveMultiStats(const char * sFileName,const char * sPlayerName,const PLAYERSTATS * st)346 bool saveMultiStats(const char *sFileName, const char *sPlayerName, const PLAYERSTATS *st)
347 {
348 	if (Cheated)
349 	{
350 	    return false;
351 	}
352 	char buffer[1000];
353 
354 	ssprintf(buffer, "WZ.STA.v3\n%u %u %u %u %u\n%s\n",
355 	         st->wins, st->losses, st->totalKills, st->totalScore, st->played, base64Encode(st->identity.toBytes(EcKey::Private)).c_str());
356 
357 	std::string fileName = std::string(MultiPlayersPath) + sFileName + ".sta2";
358 
359 	saveFile(fileName.c_str(), buffer, strlen(buffer));
360 
361 	return true;
362 }
363 
364 // ////////////////////////////////////////////////////////////////////////////
365 // score update functions
366 
367 // update players damage stats.
updateMultiStatsDamage(UDWORD attacker,UDWORD defender,UDWORD inflicted)368 void updateMultiStatsDamage(UDWORD attacker, UDWORD defender, UDWORD inflicted)
369 {
370 	// damaging features like skyscrapers does not count
371 	if (defender != PLAYER_FEATURE)
372 	{
373 		if (NetPlay.bComms)
374 		{
375 			// killing and getting killed by scavengers does not influence scores in MP games
376 			if (attacker != scavengerSlot() && defender != scavengerSlot())
377 			{
378 				// FIXME: Why in the world are we using two different structs for stats when we can use only one?
379 				playerStats[attacker].totalScore  += 2 * inflicted;
380 				playerStats[attacker].recentScore += 2 * inflicted;
381 				playerStats[defender].totalScore  -= inflicted;
382 				playerStats[defender].recentScore -= inflicted;
383 			}
384 		}
385 		else
386 		{
387 			ingame.skScores[attacker][0] += 2 * inflicted;  // increment skirmish players rough score.
388 			ingame.skScores[defender][0] -= inflicted;  // increment skirmish players rough score.
389 		}
390 	}
391 }
392 
393 // update games played.
updateMultiStatsGames()394 void updateMultiStatsGames()
395 {
396 	if (selectedPlayer >= MAX_PLAYERS)
397 	{
398 		return;
399 	}
400 	++playerStats[selectedPlayer].played;
401 }
402 
403 // games won
updateMultiStatsWins()404 void updateMultiStatsWins()
405 {
406 	if (selectedPlayer >= MAX_PLAYERS)
407 	{
408 		return;
409 	}
410 	++playerStats[selectedPlayer].wins;
411 }
412 
413 //games lost.
updateMultiStatsLoses()414 void updateMultiStatsLoses()
415 {
416 	if (selectedPlayer >= MAX_PLAYERS)
417 	{
418 		return;
419 	}
420 	++playerStats[selectedPlayer].losses;
421 }
422 
423 // update kills
updateMultiStatsKills(BASE_OBJECT * psKilled,UDWORD player)424 void updateMultiStatsKills(BASE_OBJECT *psKilled, UDWORD player)
425 {
426 	if (player < MAX_PLAYERS)
427 	{
428 		if (NetPlay.bComms)
429 		{
430 			// killing scavengers does not count in MP games
431 			if (psKilled != nullptr && psKilled->player != scavengerSlot())
432 			{
433 				// FIXME: Why in the world are we using two different structs for stats when we can use only one?
434 				++playerStats[player].totalKills;
435 				++playerStats[player].recentKills;
436 			}
437 		}
438 		else
439 		{
440 			ingame.skScores[player][1]++;
441 		}
442 	}
443 }
444 
445 class KnownPlayersDB {
446 public:
447 	struct PlayerInfo {
448 		int64_t local_id;
449 		std::string name;
450 		EcKey::Key pk;
451 	};
452 
453 public:
454 	// Caller is expected to handle thrown exceptions
KnownPlayersDB(const std::string & knownPlayersDBPath)455 	KnownPlayersDB(const std::string& knownPlayersDBPath)
456 	{
457 		db = std::unique_ptr<SQLite::Database>(new SQLite::Database(knownPlayersDBPath, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE));
458 		db->exec("PRAGMA journal_mode=WAL");
459 		createKnownPlayersDBTables();
460 		query_findPlayerByName = std::unique_ptr<SQLite::Statement>(new SQLite::Statement(*db, "SELECT local_id, name, pk FROM known_players WHERE name = ?"));
461 		query_insertNewKnownPlayer = std::unique_ptr<SQLite::Statement>(new SQLite::Statement(*db, "INSERT OR IGNORE INTO known_players(name, pk) VALUES(?, ?)"));
462 		query_updateKnownPlayerKey = std::unique_ptr<SQLite::Statement>(new SQLite::Statement(*db, "UPDATE known_players SET pk = ? WHERE name = ?"));
463 	}
464 
465 public:
findPlayerByName(const std::string & name)466 	optional<PlayerInfo> findPlayerByName(const std::string& name)
467 	{
468 		if (name.empty())
469 		{
470 			return nullopt;
471 		}
472 		cleanupCache();
473 		const auto i = findPlayerCache.find(name);
474 		if (i != findPlayerCache.end())
475 		{
476 			return i->second.first;
477 		}
478 		optional<PlayerInfo> result;
479 		try {
480 			query_findPlayerByName->bind(1, name);
481 			if (query_findPlayerByName->executeStep())
482 			{
483 				PlayerInfo data;
484 				data.local_id = query_findPlayerByName->getColumn(0).getInt64();
485 				data.name = query_findPlayerByName->getColumn(1).getString();
486 				std::string publicKeyb64 = query_findPlayerByName->getColumn(2).getString();
487 				data.pk = base64Decode(publicKeyb64);
488 				result = data;
489 			}
490 		}
491 		catch (const std::exception& e) {
492 			debug(LOG_ERROR, "Failure to query database for player; error: %s", e.what());
493 			result = nullopt;
494 		}
495 		try {
496 			query_findPlayerByName->reset();
497 		}
498 		catch (const std::exception& e) {
499 			debug(LOG_ERROR, "Failed to reset prepared statement; error: %s", e.what());
500 		}
501 		// add to the current in-memory cache
502 		findPlayerCache[name] = std::pair<optional<PlayerInfo>, UDWORD>(result, realTime);
503 		return result;
504 	}
505 
506 	// Note: May throw on database error!
addKnownPlayer(std::string const & name,EcKey const & key,bool overrideCurrentKey)507 	void addKnownPlayer(std::string const &name, EcKey const &key, bool overrideCurrentKey)
508 	{
509 		if (key.empty())
510 		{
511 			return;
512 		}
513 
514 		std::string publicKeyb64 = base64Encode(key.toBytes(EcKey::Public));
515 
516 		// Begin transaction
517 		SQLite::Transaction transaction(*db);
518 
519 		query_insertNewKnownPlayer->bind(1, name);
520 		query_insertNewKnownPlayer->bind(2, publicKeyb64);
521 		if (query_insertNewKnownPlayer->exec() == 0 && overrideCurrentKey)
522 		{
523 			query_updateKnownPlayerKey->bind(1, publicKeyb64);
524 			query_updateKnownPlayerKey->bind(2, name);
525 			if (query_updateKnownPlayerKey->exec() == 0)
526 			{
527 				debug(LOG_WARNING, "Failed to update known_player (%s)", name.c_str());
528 			}
529 			query_updateKnownPlayerKey->reset();
530 		}
531 		query_insertNewKnownPlayer->reset();
532 
533 		// remove from the current in-memory cache
534 		findPlayerCache.erase(name);
535 
536 		// Commit transaction
537 		transaction.commit();
538 	}
539 
540 private:
createKnownPlayersDBTables()541 	void createKnownPlayersDBTables()
542 	{
543 		SQLite::Transaction transaction(*db);
544 		if (!db->tableExists("known_players"))
545 		{
546 			db->exec("CREATE TABLE known_players (local_id INTEGER PRIMARY KEY, name TEXT UNIQUE, pk TEXT)");
547 		}
548 		transaction.commit();
549 	}
550 
cleanupCache()551 	void cleanupCache()
552 	{
553 		const UDWORD CACHE_CLEAN_INTERAL = 10 * GAME_TICKS_PER_SEC;
554 		if (realTime - lastCacheClean > CACHE_CLEAN_INTERAL)
555 		{
556 			findPlayerCache.clear();
557 			lastCacheClean = realTime;
558 		}
559 	}
560 
561 private:
562 	std::unique_ptr<SQLite::Database> db; // Must be the first-listed member variable so it is destructed last
563 	std::unique_ptr<SQLite::Statement> query_findPlayerByName;
564 	std::unique_ptr<SQLite::Statement> query_insertNewKnownPlayer;
565 	std::unique_ptr<SQLite::Statement> query_updateKnownPlayerKey;
566 	std::unordered_map<std::string, std::pair<optional<PlayerInfo>, UDWORD>> findPlayerCache;
567 	UDWORD lastCacheClean = 0;
568 };
569 
570 static std::unique_ptr<KnownPlayersDB> knownPlayersDB;
571 
initKnownPlayers()572 void initKnownPlayers()
573 {
574 	if (!knownPlayersDB)
575 	{
576 		const char *pWriteDir = PHYSFS_getWriteDir();
577 		ASSERT_OR_RETURN(, pWriteDir, "PHYSFS_getWriteDir returned null");
578 		std::string knownPlayersDBPath = std::string(pWriteDir) + "/" + "knownPlayers.db";
579 		try {
580 			knownPlayersDB = std::unique_ptr<KnownPlayersDB>(new KnownPlayersDB(knownPlayersDBPath));
581 		}
582 		catch (std::exception& e) {
583 			// error loading SQLite database
584 			debug(LOG_ERROR, "Unable to load or initialize SQLite3 database (%s); error: %s", knownPlayersDBPath.c_str(), e.what());
585 			return;
586 		}
587 	}
588 }
589 
shutdownKnownPlayers()590 void shutdownKnownPlayers()
591 {
592 	knownPlayersDB.reset();
593 }
594 
isLocallyKnownPlayer(std::string const & name,EcKey const & key)595 bool isLocallyKnownPlayer(std::string const &name, EcKey const &key)
596 {
597 	ASSERT_OR_RETURN(false, knownPlayersDB.operator bool(), "knownPlayersDB is uninitialized");
598 	if (key.empty())
599 	{
600 		return false;
601 	}
602 	auto result = knownPlayersDB->findPlayerByName(name);
603 	if (!result.has_value())
604 	{
605 		return false;
606 	}
607 	return result.value().pk == key.toBytes(EcKey::Public);
608 }
609 
addKnownPlayer(std::string const & name,EcKey const & key,bool override)610 void addKnownPlayer(std::string const &name, EcKey const &key, bool override)
611 {
612 	if (key.empty())
613 	{
614 		return;
615 	}
616 	ASSERT_OR_RETURN(, knownPlayersDB.operator bool(), "knownPlayersDB is uninitialized");
617 
618 	try {
619 		knownPlayersDB->addKnownPlayer(name, key, override);
620 	}
621 	catch (const std::exception& e) {
622 		debug(LOG_ERROR, "Failed to add known_player with error: %s", e.what());
623 	}
624 }
625 
getMultiPlayUnitsKilled(uint32_t player)626 uint32_t getMultiPlayUnitsKilled(uint32_t player)
627 {
628 	// Let's use the real score for MP games
629 	// FIXME: Why in the world are we using two different structs for stats when we can use only one?
630 	if (NetPlay.bComms)
631 	{
632 		return getMultiStats(player).recentKills;
633 	}
634 	else
635 	{
636 		// estimated kills
637 		return static_cast<uint32_t>(ingame.skScores[player][1]);
638 	}
639 }
640 
getSelectedPlayerUnitsKilled()641 uint32_t getSelectedPlayerUnitsKilled()
642 {
643 	if (ActivityManager::instance().getCurrentGameMode() != ActivitySink::GameMode::CAMPAIGN)
644 	{
645 		return getMultiPlayUnitsKilled(selectedPlayer);
646 	}
647 	else
648 	{
649 		return missionData.unitsKilled;
650 	}
651 }
652