Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Passed
Pull Request — master (#1005)
by Dan
04:25
created

AbstractSmrPlayer::doMessageSending()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 49
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 35
nc 8
nop 8
dl 0
loc 49
rs 8.4266
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php declare(strict_types=1);
2
require_once('missions.inc.php');
3
4
// Exception thrown when a player cannot be found in the database
5
class PlayerNotFoundException extends Exception {}
6
7
abstract class AbstractSmrPlayer {
8
	use Traits\RaceID;
9
10
	const TIME_FOR_FEDERAL_BOUNTY_ON_PR = 10800;
11
	const TIME_FOR_ALLIANCE_SWITCH = 0;
12
13
	const SHIP_INSURANCE_FRACTION = 0.25; // ship value regained on death
14
15
	const HOF_CHANGED = 1;
16
	const HOF_NEW = 2;
17
18
	protected static $CACHE_SECTOR_PLAYERS = array();
19
	protected static $CACHE_PLANET_PLAYERS = array();
20
	protected static $CACHE_ALLIANCE_PLAYERS = array();
21
	protected static $CACHE_PLAYERS = array();
22
23
	protected $db;
24
	protected $SQL;
25
26
	protected $accountID;
27
	protected $gameID;
28
	protected $playerName;
29
	protected $playerID;
30
	protected $sectorID;
31
	protected $lastSectorID;
32
	protected $newbieTurns;
33
	protected $dead;
34
	protected $npc;
35
	protected $newbieStatus;
36
	protected $newbieWarning;
37
	protected $landedOnPlanet;
38
	protected $lastActive;
39
	protected $credits;
40
	protected $alignment;
41
	protected $experience;
42
	protected $level;
43
	protected $allianceID;
44
	protected $shipID;
45
	protected $kills;
46
	protected $deaths;
47
	protected $assists;
48
	protected $stats;
49
	protected $personalRelations;
50
	protected $relations;
51
	protected $militaryPayment;
52
	protected $bounties;
53
	protected $turns;
54
	protected $lastCPLAction;
55
	protected $missions;
56
57
	protected $tickers;
58
	protected $lastTurnUpdate;
59
	protected $lastNewsUpdate;
60
	protected $attackColour;
61
	protected $allianceJoinable;
62
	protected $lastPort;
63
	protected $bank;
64
	protected $zoom;
65
	protected $displayMissions;
66
	protected $displayWeapons;
67
	protected $forceDropMessages;
68
	protected $groupScoutMessages;
69
	protected $ignoreGlobals;
70
	protected $plottedCourse;
71
	protected $plottedCourseFrom;
72
	protected $nameChanged;
73
	protected bool $raceChanged;
74
	protected $combatDronesKamikazeOnMines;
75
	protected $customShipName;
76
	protected $storedDestinations;
77
	protected $canFed;
78
79
	protected $visitedSectors;
80
	protected $allianceRoles = array(
81
		0 => 0
82
	);
83
84
	protected $draftLeader;
85
	protected $gpWriter;
86
	protected $HOF;
87
	protected static $HOFVis;
88
89
	protected $hasChanged = false;
90
	protected array $hasHOFChanged = [];
91
	protected static $hasHOFVisChanged = array();
92
	protected $hasBountyChanged = array();
93
94
	public static function refreshCache() {
95
		foreach (self::$CACHE_PLAYERS as $gameID => &$gamePlayers) {
96
			foreach ($gamePlayers as $accountID => &$player) {
97
				$player = self::getPlayer($accountID, $gameID, true);
98
			}
99
		}
100
	}
101
102
	public static function clearCache() {
103
		self::$CACHE_PLAYERS = array();
104
		self::$CACHE_SECTOR_PLAYERS = array();
105
	}
106
107
	public static function savePlayers() {
108
		foreach (self::$CACHE_PLAYERS as $gamePlayers) {
109
			foreach ($gamePlayers as $player) {
110
				$player->save();
111
			}
112
		}
113
	}
114
115
	public static function getSectorPlayersByAlliances($gameID, $sectorID, array $allianceIDs, $forceUpdate = false) {
116
		$players = self::getSectorPlayers($gameID, $sectorID, $forceUpdate); // Don't use & as we do an unset
117
		foreach ($players as $accountID => $player) {
118
			if (!in_array($player->getAllianceID(), $allianceIDs)) {
119
				unset($players[$accountID]);
120
			}
121
		}
122
		return $players;
123
	}
124
125
	/**
126
	 * Returns the same players as getSectorPlayers (e.g. not on planets),
127
	 * but for an entire galaxy rather than a single sector. This is useful
128
	 * for reducing the number of queries in galaxy-wide processing.
129
	 */
130
	public static function getGalaxyPlayers($gameID, $galaxyID, $forceUpdate = false) {
131
		$db = MySqlDatabase::getInstance();
132
		$db->query('SELECT player.*, sector_id FROM sector LEFT JOIN player USING(game_id, sector_id) WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(SmrSession::getTime() - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND galaxy_id = ' . $db->escapeNumber($galaxyID));
133
		$galaxyPlayers = [];
134
		while ($db->nextRecord()) {
135
			$sectorID = $db->getInt('sector_id');
136
			if (!$db->hasField('account_id')) {
137
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = [];
138
			} else {
139
				$accountID = $db->getInt('account_id');
140
				$player = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
141
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$accountID] = $player;
142
				$galaxyPlayers[$sectorID][$accountID] = $player;
143
			}
144
		}
145
		return $galaxyPlayers;
146
	}
147
148
	public static function getSectorPlayers($gameID, $sectorID, $forceUpdate = false) {
149
		if ($forceUpdate || !isset(self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID])) {
150
			$db = MySqlDatabase::getInstance();
151
			$db->query('SELECT * FROM player WHERE sector_id = ' . $db->escapeNumber($sectorID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(SmrSession::getTime() - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
152
			$players = array();
153
			while ($db->nextRecord()) {
154
				$accountID = $db->getInt('account_id');
155
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
156
			}
157
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = $players;
158
		}
159
		return self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID];
160
	}
161
162
	public static function getPlanetPlayers($gameID, $sectorID, $forceUpdate = false) {
163
		if ($forceUpdate || !isset(self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID])) {
164
			$db = MySqlDatabase::getInstance();
165
			$db->query('SELECT * FROM player WHERE sector_id = ' . $db->escapeNumber($sectorID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(true) . ' AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
166
			$players = array();
167
			while ($db->nextRecord()) {
168
				$accountID = $db->getInt('account_id');
169
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
170
			}
171
			self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID] = $players;
172
		}
173
		return self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID];
174
	}
175
176
	public static function getAlliancePlayers($gameID, $allianceID, $forceUpdate = false) {
177
		if ($forceUpdate || !isset(self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID])) {
178
			$db = MySqlDatabase::getInstance();
179
			$db->query('SELECT * FROM player WHERE alliance_id = ' . $db->escapeNumber($allianceID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' ORDER BY experience DESC');
180
			$players = array();
181
			while ($db->nextRecord()) {
182
				$accountID = $db->getInt('account_id');
183
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
184
			}
185
			self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID] = $players;
186
		}
187
		return self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID];
188
	}
189
190
	public static function getPlayer($accountID, $gameID, $forceUpdate = false, $db = null) {
191
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$accountID])) {
192
			self::$CACHE_PLAYERS[$gameID][$accountID] = new SmrPlayer($gameID, $accountID, $db);
193
		}
194
		return self::$CACHE_PLAYERS[$gameID][$accountID];
195
	}
196
197
	public static function getPlayerByPlayerID($playerID, $gameID, $forceUpdate = false) {
198
		$db = MySqlDatabase::getInstance();
199
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_id = ' . $db->escapeNumber($playerID) . ' LIMIT 1');
200
		if ($db->nextRecord()) {
201
			return self::getPlayer($db->getInt('account_id'), $gameID, $forceUpdate, $db);
202
		}
203
		throw new PlayerNotFoundException('Player ID not found.');
204
	}
205
206
	public static function getPlayerByPlayerName($playerName, $gameID, $forceUpdate = false) {
207
		$db = MySqlDatabase::getInstance();
208
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
209
		if ($db->nextRecord()) {
210
			return self::getPlayer($db->getInt('account_id'), $gameID, $forceUpdate, $db);
211
		}
212
		throw new PlayerNotFoundException('Player Name not found.');
213
	}
214
215
	protected function __construct($gameID, $accountID, $db = null) {
216
		$this->db = MySqlDatabase::getInstance();
217
		$this->SQL = 'account_id = ' . $this->db->escapeNumber($accountID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
218
219
		if (isset($db)) {
220
			$playerExists = true;
221
		} else {
222
			$db = $this->db;
223
			$this->db->query('SELECT * FROM player WHERE ' . $this->SQL . ' LIMIT 1');
224
			$playerExists = $db->nextRecord();
225
		}
226
227
		if ($playerExists) {
228
			$this->accountID = (int)$accountID;
229
			$this->gameID = (int)$gameID;
230
			$this->playerName = $db->getField('player_name');
231
			$this->playerID = $db->getInt('player_id');
232
			$this->sectorID = $db->getInt('sector_id');
233
			$this->lastSectorID = $db->getInt('last_sector_id');
234
			$this->turns = $db->getInt('turns');
235
			$this->lastTurnUpdate = $db->getInt('last_turn_update');
236
			$this->newbieTurns = $db->getInt('newbie_turns');
237
			$this->lastNewsUpdate = $db->getInt('last_news_update');
238
			$this->attackColour = $db->getField('attack_warning');
239
			$this->dead = $db->getBoolean('dead');
240
			$this->npc = $db->getBoolean('npc');
241
			$this->newbieStatus = $db->getBoolean('newbie_status');
242
			$this->landedOnPlanet = $db->getBoolean('land_on_planet');
243
			$this->lastActive = $db->getInt('last_active');
244
			$this->lastCPLAction = $db->getInt('last_cpl_action');
245
			$this->raceID = $db->getInt('race_id');
246
			$this->credits = $db->getInt('credits');
247
			$this->experience = $db->getInt('experience');
248
			$this->alignment = $db->getInt('alignment');
249
			$this->militaryPayment = $db->getInt('military_payment');
250
			$this->allianceID = $db->getInt('alliance_id');
251
			$this->allianceJoinable = $db->getInt('alliance_join');
252
			$this->shipID = $db->getInt('ship_type_id');
253
			$this->kills = $db->getInt('kills');
254
			$this->deaths = $db->getInt('deaths');
255
			$this->assists = $db->getInt('assists');
256
			$this->lastPort = $db->getInt('last_port');
257
			$this->bank = $db->getInt('bank');
258
			$this->zoom = $db->getInt('zoom');
259
			$this->displayMissions = $db->getBoolean('display_missions');
260
			$this->displayWeapons = $db->getBoolean('display_weapons');
261
			$this->forceDropMessages = $db->getBoolean('force_drop_messages');
262
			$this->groupScoutMessages = $db->getField('group_scout_messages');
263
			$this->ignoreGlobals = $db->getBoolean('ignore_globals');
264
			$this->newbieWarning = $db->getBoolean('newbie_warning');
265
			$this->nameChanged = $db->getBoolean('name_changed');
266
			$this->raceChanged = $db->getBoolean('race_changed');
267
			$this->combatDronesKamikazeOnMines = $db->getBoolean('combat_drones_kamikaze_on_mines');
268
		} else {
269
			throw new PlayerNotFoundException('Invalid accountID: ' . $accountID . ' OR gameID:' . $gameID);
270
		}
271
	}
272
273
	/**
274
	 * Insert a new player into the database. Returns the new player object.
275
	 */
276
	public static function createPlayer($accountID, $gameID, $playerName, $raceID, $isNewbie, $npc = false) {
277
		$time = SmrSession::getTime();
278
		$db = MySqlDatabase::getInstance();
279
		$db->lockTable('player');
280
281
		// Player names must be unique within each game
282
		try {
283
			self::getPlayerByPlayerName($playerName, $gameID);
284
			$db->unlock();
285
			throw new \Smr\UserException('That player name already exists.');
286
		} catch (PlayerNotFoundException $e) {
287
			// Player name does not yet exist, we may proceed
288
		}
289
290
		// get last registered player id in that game and increase by one.
291
		$db->query('SELECT MAX(player_id) FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
292
		if ($db->nextRecord()) {
293
			$playerID = $db->getInt('MAX(player_id)') + 1;
294
		} else {
295
			$playerID = 1;
296
		}
297
298
		$startSectorID = 0; // Temporarily put player into non-existent sector
299
		$db->query('INSERT INTO player (account_id, game_id, player_id, player_name, race_id, sector_id, last_cpl_action, last_active, npc, newbie_status)
300
					VALUES(' . $db->escapeNumber($accountID) . ', ' . $db->escapeNumber($gameID) . ', ' . $db->escapeNumber($playerID) . ', ' . $db->escapeString($playerName) . ', ' . $db->escapeNumber($raceID) . ', ' . $db->escapeNumber($startSectorID) . ', ' . $db->escapeNumber($time) . ', ' . $db->escapeNumber($time) . ',' . $db->escapeBoolean($npc) . ',' . $db->escapeBoolean($isNewbie) . ')');
301
302
		$db->unlock();
303
304
		$player = SmrPlayer::getPlayer($accountID, $gameID);
305
		$player->setSectorID($player->getHome());
306
		return $player;
307
	}
308
309
	/**
310
	 * Get array of players whose info can be accessed by this player.
311
	 * Skips players who are not in the same alliance as this player.
312
	 */
313
	public function getSharingPlayers($forceUpdate = false) {
314
		$results = array($this);
315
316
		// Only return this player if not in an alliance
317
		if (!$this->hasAlliance()) {
318
			return $results;
319
		}
320
321
		// Get other players who are sharing info for this game.
322
		// NOTE: game_id=0 means that player shares info for all games.
323
		$this->db->query('SELECT from_account_id FROM account_shares_info WHERE to_account_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND (game_id=0 OR game_id=' . $this->db->escapeNumber($this->getGameID()) . ')');
324
		while ($this->db->nextRecord()) {
325
			try {
326
				$otherPlayer = SmrPlayer::getPlayer($this->db->getInt('from_account_id'),
327
				                                    $this->getGameID(), $forceUpdate);
328
			} catch (PlayerNotFoundException $e) {
329
				// Skip players that have not joined this game
330
				continue;
331
			}
332
333
			// players must be in the same alliance
334
			if ($this->sameAlliance($otherPlayer)) {
335
				$results[] = $otherPlayer;
336
			}
337
		}
338
		return $results;
339
	}
340
341
	public function getSQL() : string {
342
		return $this->SQL;
343
	}
344
345
	public function getZoom() {
346
		return $this->zoom;
347
	}
348
349
	protected function setZoom($zoom) {
350
		// Set the zoom level between [1, 9]
351
		$zoom = max(1, min(9, $zoom));
352
		if ($this->zoom == $zoom) {
353
			return;
354
		}
355
		$this->zoom = $zoom;
356
		$this->hasChanged = true;
357
	}
358
359
	public function increaseZoom($zoom) {
360
		if ($zoom < 0) {
361
			throw new Exception('Trying to increase negative zoom.');
362
		}
363
		$this->setZoom($this->getZoom() + $zoom);
364
	}
365
366
	public function decreaseZoom($zoom) {
367
		if ($zoom < 0) {
368
			throw new Exception('Trying to decrease negative zoom.');
369
		}
370
		$this->setZoom($this->getZoom() - $zoom);
371
	}
372
373
	public function getAttackColour() {
374
		return $this->attackColour;
375
	}
376
377
	public function setAttackColour($colour) {
378
		if ($this->attackColour == $colour) {
379
			return;
380
		}
381
		$this->attackColour = $colour;
382
		$this->hasChanged = true;
383
	}
384
385
	public function isIgnoreGlobals() {
386
		return $this->ignoreGlobals;
387
	}
388
389
	public function setIgnoreGlobals($bool) {
390
		if ($this->ignoreGlobals == $bool) {
391
			return;
392
		}
393
		$this->ignoreGlobals = $bool;
394
		$this->hasChanged = true;
395
	}
396
397
	public function getAccount() {
398
		return SmrAccount::getAccount($this->getAccountID());
399
	}
400
401
	public function getAccountID() {
402
		return $this->accountID;
403
	}
404
405
	public function getGameID() {
406
		return $this->gameID;
407
	}
408
409
	public function getGame() {
410
		return SmrGame::getGame($this->gameID);
411
	}
412
413
	public function getNewbieTurns() {
414
		return $this->newbieTurns;
415
	}
416
417
	public function hasNewbieTurns() {
418
		return $this->getNewbieTurns() > 0;
419
	}
420
	public function setNewbieTurns($newbieTurns) {
421
		if ($this->newbieTurns == $newbieTurns) {
422
			return;
423
		}
424
		$this->newbieTurns = $newbieTurns;
425
		$this->hasChanged = true;
426
	}
427
428
	public function getShip($forceUpdate = false) {
429
		return SmrShip::getShip($this, $forceUpdate);
430
	}
431
432
	public function getShipTypeID() {
433
		return $this->shipID;
434
	}
435
436
	/**
437
	 * Do not call directly. Use SmrShip::setShipTypeID instead.
438
	 */
439
	public function setShipTypeID($shipID) {
440
		if ($this->shipID == $shipID) {
441
			return;
442
		}
443
		$this->shipID = $shipID;
444
		$this->hasChanged = true;
445
	}
446
447
	public function hasCustomShipName() {
448
		return $this->getCustomShipName() !== false;
449
	}
450
451
	public function getCustomShipName() {
452
		if (!isset($this->customShipName)) {
453
			$this->db->query('SELECT * FROM ship_has_name WHERE ' . $this->SQL . ' LIMIT 1');
454
			if ($this->db->nextRecord()) {
455
				$this->customShipName = $this->db->getField('ship_name');
456
			} else {
457
				$this->customShipName = false;
458
			}
459
		}
460
		return $this->customShipName;
461
	}
462
463
	public function setCustomShipName(string $name) {
464
		$this->db->query('REPLACE INTO ship_has_name (game_id, account_id, ship_name)
465
			VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeString($name) . ')');
466
	}
467
468
	/**
469
	 * Get planet owned by this player.
470
	 * Returns false if this player does not own a planet.
471
	 */
472
	public function getPlanet() {
473
		$this->db->query('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_id=' . $this->db->escapeNumber($this->getAccountID()));
474
		if ($this->db->nextRecord()) {
475
			return SmrPlanet::getPlanet($this->getGameID(), $this->db->getInt('sector_id'), false, $this->db);
476
		} else {
477
			return false;
478
		}
479
	}
480
481
	public function getSectorPlanet() {
482
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
483
	}
484
485
	public function getSectorPort() {
486
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
487
	}
488
489
	public function getSectorID() {
490
		return $this->sectorID;
491
	}
492
493
	public function getSector() {
494
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
495
	}
496
497
	public function setSectorID($sectorID) {
498
		if ($this->sectorID == $sectorID) {
499
			return;
500
		}
501
502
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
503
		$port->addCachePort($this->getAccountID()); //Add port of sector we were just in, to make sure it is left totally up to date.
504
505
		$this->setLastSectorID($this->getSectorID());
506
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
507
		$this->sectorID = $sectorID;
508
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
509
		$this->hasChanged = true;
510
511
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
512
		$port->addCachePort($this->getAccountID()); //Add the port of sector we are now in.
513
	}
514
515
	public function getLastSectorID() {
516
		return $this->lastSectorID;
517
	}
518
519
	public function setLastSectorID($lastSectorID) {
520
		if ($this->lastSectorID == $lastSectorID) {
521
			return;
522
		}
523
		$this->lastSectorID = $lastSectorID;
524
		$this->hasChanged = true;
525
	}
526
527
	public function getHome() {
528
		// get his home sector
529
		$hq_id = GOVERNMENT + $this->getRaceID();
530
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
531
		if (!empty($raceHqSectors)) {
532
			// If race has multiple HQ's for some reason, use the first one
533
			return key($raceHqSectors);
534
		} else {
535
			return 1;
536
		}
537
	}
538
539
	public function isDead() {
540
		return $this->dead;
541
	}
542
543
	public function isNPC() {
544
		return $this->npc;
545
	}
546
547
	/**
548
	 * Does the player have Newbie status?
549
	 */
550
	public function hasNewbieStatus() {
551
		return $this->newbieStatus;
552
	}
553
554
	/**
555
	 * Update the player's newbie status if it has changed.
556
	 * This function queries the account, so use sparingly.
557
	 */
558
	public function updateNewbieStatus() {
559
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
560
		if ($this->newbieStatus != $accountNewbieStatus) {
561
			$this->newbieStatus = $accountNewbieStatus;
562
			$this->hasChanged = true;
563
		}
564
	}
565
566
	/**
567
	 * Has this player been designated as the alliance flagship?
568
	 */
569
	public function isFlagship() {
570
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipID() == $this->getAccountID();
571
	}
572
573
	public function isPresident() {
574
		return Council::getPresidentID($this->getGameID(), $this->getRaceID()) == $this->getAccountID();
575
	}
576
577
	public function isOnCouncil() {
578
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getAccountID());
579
	}
580
581
	public function isDraftLeader() {
582
		if (!isset($this->draftLeader)) {
583
			$this->draftLeader = false;
584
			$this->db->query('SELECT 1 FROM draft_leaders WHERE ' . $this->SQL . ' LIMIT 1');
585
			if ($this->db->nextRecord()) {
586
				$this->draftLeader = true;
587
			}
588
		}
589
		return $this->draftLeader;
590
	}
591
592
	public function getGPWriter() {
593
		if (!isset($this->gpWriter)) {
594
			$this->gpWriter = false;
595
			$this->db->query('SELECT position FROM galactic_post_writer WHERE ' . $this->SQL);
596
			if ($this->db->nextRecord()) {
597
				$this->gpWriter = $this->db->getField('position');
598
			}
599
		}
600
		return $this->gpWriter;
601
	}
602
603
	public function isGPEditor() {
604
		return $this->getGPWriter() == 'editor';
605
	}
606
607
	public function isForceDropMessages() {
608
		return $this->forceDropMessages;
609
	}
610
611
	public function setForceDropMessages($bool) {
612
		if ($this->forceDropMessages == $bool) {
613
			return;
614
		}
615
		$this->forceDropMessages = $bool;
616
		$this->hasChanged = true;
617
	}
618
619
	public function getScoutMessageGroupLimit() {
620
		if ($this->groupScoutMessages == 'ALWAYS') {
621
			return 0;
622
		} elseif ($this->groupScoutMessages == 'AUTO') {
623
			return MESSAGES_PER_PAGE;
624
		} elseif ($this->groupScoutMessages == 'NEVER') {
625
			return PHP_INT_MAX;
626
		}
627
	}
628
629
	public function getGroupScoutMessages() {
630
		return $this->groupScoutMessages;
631
	}
632
633
	public function setGroupScoutMessages($setting) {
634
		if ($this->groupScoutMessages == $setting) {
635
			return;
636
		}
637
		$this->groupScoutMessages = $setting;
638
		$this->hasChanged = true;
639
	}
640
641
	protected static function doMessageSending($senderID, $receiverID, $gameID, $messageTypeID, $message, $expires, $senderDelete = false, $unread = true) {
642
		$message = trim($message);
643
		$db = MySqlDatabase::getInstance();
644
		// send him the message
645
		$db->query('INSERT INTO message
646
			(account_id,game_id,message_type_id,message_text,
647
			sender_id,send_time,expire_time,sender_delete) VALUES(' .
648
			$db->escapeNumber($receiverID) . ',' .
649
			$db->escapeNumber($gameID) . ',' .
650
			$db->escapeNumber($messageTypeID) . ',' .
651
			$db->escapeString($message) . ',' .
652
			$db->escapeNumber($senderID) . ',' .
653
			$db->escapeNumber(SmrSession::getTime()) . ',' .
654
			$db->escapeNumber($expires) . ',' .
655
			$db->escapeBoolean($senderDelete) . ')'
656
		);
657
		// Keep track of the message_id so it can be returned
658
		$insertID = $db->getInsertID();
659
660
		if ($unread === true) {
661
			// give him the message icon
662
			$db->query('REPLACE INTO player_has_unread_messages (game_id, account_id, message_type_id) VALUES
663
						(' . $db->escapeNumber($gameID) . ', ' . $db->escapeNumber($receiverID) . ', ' . $db->escapeNumber($messageTypeID) . ')');
664
		}
665
666
		switch ($messageTypeID) {
667
			case MSG_PLAYER:
668
				$receiverAccount = SmrAccount::getAccount($receiverID);
669
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isLoggedIn()) {
670
					require_once(get_file_loc('messages.inc.php'));
671
					$sender = getMessagePlayer($senderID, $gameID, $messageTypeID);
672
					if ($sender instanceof SmrPlayer) {
673
						$sender = $sender->getDisplayName();
674
					}
675
					$mail = setupMailer();
676
					$mail->Subject = 'Message Notification';
677
					$mail->setFrom('[email protected]', 'SMR Notifications');
678
					$bbifiedMessage = 'From: ' . $sender . ' Date: ' . date($receiverAccount->getShortDateFormat() . ' ' . $receiverAccount->getShortTimeFormat(), SmrSession::getTime()) . "<br/>\r\n<br/>\r\n" . bbifyMessage($message, true);
679
					$mail->msgHTML($bbifiedMessage);
680
					$mail->AltBody = strip_tags($bbifiedMessage);
681
					$mail->addAddress($receiverAccount->getEmail(), $receiverAccount->getHofName());
682
					$mail->send();
683
					$receiverAccount->decreaseMessageNotifications($messageTypeID, 1);
684
					$receiverAccount->update();
685
				}
686
			break;
687
		}
688
689
		return $insertID;
690
	}
691
692
	public function sendMessageToBox($boxTypeID, $message) {
693
		// send him the message
694
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
695
	}
696
697
	public function sendGlobalMessage($message, $canBeIgnored = true) {
698
		if ($canBeIgnored) {
699
			if ($this->getAccount()->isMailBanned()) {
700
				create_error('You are currently banned from sending messages');
701
			}
702
		}
703
		$this->sendMessageToBox(BOX_GLOBALS, $message);
704
705
		// send to all online player
706
		$db = MySqlDatabase::getInstance();
707
		$db->query('SELECT account_id
708
					FROM active_session
709
					JOIN player USING (game_id, account_id)
710
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(SmrSession::getTime() - SmrSession::TIME_BEFORE_EXPIRY) . '
711
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
712
						AND ignore_globals = \'FALSE\'
713
						AND account_id != ' . $db->escapeNumber($this->getAccountID()));
714
715
		while ($db->nextRecord()) {
716
			$this->sendMessage($db->getInt('account_id'), MSG_GLOBAL, $message, $canBeIgnored);
717
		}
718
		$this->sendMessage($this->getAccountID(), MSG_GLOBAL, $message, $canBeIgnored, false);
719
	}
720
721
	public function sendMessage($receiverID, $messageTypeID, $message, $canBeIgnored = true, $unread = true, $expires = false, $senderDelete = false) {
722
		//get expire time
723
		if ($canBeIgnored) {
724
			if ($this->getAccount()->isMailBanned()) {
725
				create_error('You are currently banned from sending messages');
726
			}
727
			// Don't send messages to players ignoring us
728
			$this->db->query('SELECT account_id FROM message_blacklist WHERE account_id=' . $this->db->escapeNumber($receiverID) . ' AND blacklisted_id=' . $this->db->escapeNumber($this->getAccountID()) . ' LIMIT 1');
729
			if ($this->db->nextRecord()) {
730
				return;
731
			}
732
		}
733
734
		$message = word_filter($message);
735
736
		// If expires not specified, use default based on message type
737
		if ($expires === false) {
738
			switch ($messageTypeID) {
739
				case MSG_GLOBAL: //We don't send globals to the box here or it gets done loads of times.
740
					$expires = 3600; // 1h
741
				break;
742
				case MSG_PLAYER:
743
					$expires = 86400 * 31;
744
				break;
745
				case MSG_PLANET:
746
					$expires = 86400 * 7;
747
				break;
748
				case MSG_SCOUT:
749
					$expires = 86400 * 3;
750
				break;
751
				case MSG_POLITICAL:
752
					$expires = 86400 * 31;
753
				break;
754
				case MSG_ALLIANCE:
755
					$expires = 86400 * 31;
756
				break;
757
				case MSG_ADMIN:
758
					$expires = 86400 * 365;
759
				break;
760
				case MSG_CASINO:
761
					$expires = 86400 * 31;
762
				break;
763
				default:
764
					$expires = 86400 * 7;
765
			}
766
			$expires += SmrSession::getTime();
767
		}
768
769
		// Do not put scout messages in the sender's sent box
770
		if ($messageTypeID == MSG_SCOUT) {
771
			$senderDelete = true;
772
		}
773
774
		// send him the message and return the message_id
775
		return self::doMessageSending($this->getAccountID(), $receiverID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
776
	}
777
778
	public function sendMessageFromOpAnnounce($receiverID, $message, $expires = false) {
779
		// get expire time if not set
780
		if ($expires === false) {
781
			$expires = SmrSession::getTime() + 86400 * 14;
782
		}
783
		self::doMessageSending(ACCOUNT_ID_OP_ANNOUNCE, $receiverID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
784
	}
785
786
	public function sendMessageFromAllianceCommand($receiverID, $message) {
787
		$expires = SmrSession::getTime() + 86400 * 365;
788
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_COMMAND, $receiverID, $this->getGameID(), MSG_PLAYER, $message, $expires);
789
	}
790
791
	public static function sendMessageFromPlanet($gameID, $receiverID, $message) {
792
		//get expire time
793
		$expires = SmrSession::getTime() + 86400 * 31;
794
		// send him the message
795
		self::doMessageSending(ACCOUNT_ID_PLANET, $receiverID, $gameID, MSG_PLANET, $message, $expires);
796
	}
797
798
	public static function sendMessageFromPort($gameID, $receiverID, $message) {
799
		//get expire time
800
		$expires = SmrSession::getTime() + 86400 * 31;
801
		// send him the message
802
		self::doMessageSending(ACCOUNT_ID_PORT, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
803
	}
804
805
	public static function sendMessageFromFedClerk($gameID, $receiverID, $message) {
806
		$expires = SmrSession::getTime() + 86400 * 365;
807
		self::doMessageSending(ACCOUNT_ID_FED_CLERK, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
808
	}
809
810
	public static function sendMessageFromAdmin($gameID, $receiverID, $message, $expires = false) {
811
		//get expire time
812
		if ($expires === false) {
813
			$expires = SmrSession::getTime() + 86400 * 365;
814
		}
815
		// send him the message
816
		self::doMessageSending(ACCOUNT_ID_ADMIN, $receiverID, $gameID, MSG_ADMIN, $message, $expires);
817
	}
818
819
	public static function sendMessageFromAllianceAmbassador($gameID, $receiverID, $message, $expires = false) {
820
		//get expire time
821
		if ($expires === false) {
822
			$expires = SmrSession::getTime() + 86400 * 31;
823
		}
824
		// send him the message
825
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_AMBASSADOR, $receiverID, $gameID, MSG_ALLIANCE, $message, $expires);
826
	}
827
828
	public static function sendMessageFromCasino($gameID, $receiverID, $message, $expires = false) {
829
		//get expire time
830
		if ($expires === false) {
831
			$expires = SmrSession::getTime() + 86400 * 7;
832
		}
833
		// send him the message
834
		self::doMessageSending(ACCOUNT_ID_CASINO, $receiverID, $gameID, MSG_CASINO, $message, $expires);
835
	}
836
837
	public static function sendMessageFromRace($raceID, $gameID, $receiverID, $message, $expires = false) {
838
		//get expire time
839
		if ($expires === false) {
840
			$expires = SmrSession::getTime() + 86400 * 5;
841
		}
842
		// send him the message
843
		self::doMessageSending(ACCOUNT_ID_GROUP_RACES + $raceID, $receiverID, $gameID, MSG_POLITICAL, $message, $expires);
844
	}
845
846
	public function setMessagesRead($messageTypeID) {
847
		$this->db->query('DELETE FROM player_has_unread_messages
848
							WHERE '.$this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
849
	}
850
851
	public function getSafeAttackRating() {
852
		return max(0, min(8, $this->getAlignment() / 150 + 4));
853
	}
854
855
	public function hasFederalProtection() {
856
		$sector = SmrSector::getSector($this->getGameID(), $this->getSectorID());
857
		if (!$sector->offersFederalProtection()) {
858
			return false;
859
		}
860
861
		$ship = $this->getShip();
862
		if ($ship->hasIllegalGoods()) {
863
			return false;
864
		}
865
866
		if ($ship->getAttackRating() <= $this->getSafeAttackRating()) {
867
			foreach ($sector->getFedRaceIDs() as $fedRaceID) {
868
				if ($this->canBeProtectedByRace($fedRaceID)) {
869
					return true;
870
				}
871
			}
872
		}
873
874
		return false;
875
	}
876
877
	public function canBeProtectedByRace($raceID) {
878
		if (!isset($this->canFed)) {
879
			$this->canFed = array();
880
			$RACES = Globals::getRaces();
881
			foreach ($RACES as $raceID2 => $raceName) {
882
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
883
			}
884
			$this->db->query('SELECT race_id, allowed FROM player_can_fed
885
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(SmrSession::getTime()));
886
			while ($this->db->nextRecord()) {
887
				$this->canFed[$this->db->getInt('race_id')] = $this->db->getBoolean('allowed');
888
			}
889
		}
890
		return $this->canFed[$raceID];
891
	}
892
893
	/**
894
	 * Returns a boolean identifying if the player can currently
895
	 * participate in battles.
896
	 */
897
	public function canFight() {
898
		return !($this->hasNewbieTurns() ||
899
		         $this->isDead() ||
900
		         $this->isLandedOnPlanet() ||
901
		         $this->hasFederalProtection());
902
	}
903
904
	public function setDead($bool) {
905
		if ($this->dead == $bool) {
906
			return;
907
		}
908
		$this->dead = $bool;
909
		$this->hasChanged = true;
910
	}
911
912
	public function getKills() {
913
		return $this->kills;
914
	}
915
916
	public function increaseKills($kills) {
917
		if ($kills < 0) {
918
			throw new Exception('Trying to increase negative kills.');
919
		}
920
		$this->setKills($this->kills + $kills);
921
	}
922
923
	public function setKills($kills) {
924
		if ($this->kills == $kills) {
925
			return;
926
		}
927
		$this->kills = $kills;
928
		$this->hasChanged = true;
929
	}
930
931
	public function getDeaths() {
932
		return $this->deaths;
933
	}
934
935
	public function increaseDeaths($deaths) {
936
		if ($deaths < 0) {
937
			throw new Exception('Trying to increase negative deaths.');
938
		}
939
		$this->setDeaths($this->getDeaths() + $deaths);
940
	}
941
942
	public function setDeaths($deaths) {
943
		if ($this->deaths == $deaths) {
944
			return;
945
		}
946
		$this->deaths = $deaths;
947
		$this->hasChanged = true;
948
	}
949
950
	public function getAssists() {
951
		return $this->assists;
952
	}
953
954
	public function increaseAssists($assists) {
955
		if ($assists < 1) {
956
			throw new Exception('Must increase by a positive number.');
957
		}
958
		$this->assists += $assists;
959
		$this->hasChanged = true;
960
	}
961
962
	public function getAlignment() {
963
		return $this->alignment;
964
	}
965
966
	public function increaseAlignment($align) {
967
		if ($align < 0) {
968
			throw new Exception('Trying to increase negative align.');
969
		}
970
		if ($align == 0) {
971
			return;
972
		}
973
		$align += $this->alignment;
974
		$this->setAlignment($align);
975
	}
976
977
	public function decreaseAlignment($align) {
978
		if ($align < 0) {
979
			throw new Exception('Trying to decrease negative align.');
980
		}
981
		if ($align == 0) {
982
			return;
983
		}
984
		$align = $this->alignment - $align;
985
		$this->setAlignment($align);
986
	}
987
988
	public function setAlignment($align) {
989
		if ($this->alignment == $align) {
990
			return;
991
		}
992
		$this->alignment = $align;
993
		$this->hasChanged = true;
994
	}
995
996
	public function getCredits() {
997
		return $this->credits;
998
	}
999
1000
	public function getBank() {
1001
		return $this->bank;
1002
	}
1003
1004
	/**
1005
	 * Increases personal bank account up to the maximum allowed credits.
1006
	 * Returns the amount that was actually added to handle overflow.
1007
	 */
1008
	public function increaseBank(int $credits) : int {
1009
		if ($credits == 0) {
1010
			return 0;
1011
		}
1012
		if ($credits < 0) {
1013
			throw new Exception('Trying to increase negative credits.');
1014
		}
1015
		$newTotal = min($this->bank + $credits, MAX_MONEY);
1016
		$actualAdded = $newTotal - $this->bank;
1017
		$this->setBank($newTotal);
1018
		return $actualAdded;
1019
	}
1020
1021
	public function decreaseBank(int $credits) : void {
1022
		if ($credits == 0) {
1023
			return;
1024
		}
1025
		if ($credits < 0) {
1026
			throw new Exception('Trying to decrease negative credits.');
1027
		}
1028
		$newTotal = $this->bank - $credits;
1029
		$this->setBank($newTotal);
1030
	}
1031
1032
	public function setBank(int $credits) : void {
1033
		if ($this->bank == $credits) {
1034
			return;
1035
		}
1036
		if ($credits < 0) {
1037
			throw new Exception('Trying to set negative credits.');
1038
		}
1039
		if ($credits > MAX_MONEY) {
1040
			throw new Exception('Trying to set more than max credits.');
1041
		}
1042
		$this->bank = $credits;
1043
		$this->hasChanged = true;
1044
	}
1045
1046
	public function getExperience() {
1047
		return $this->experience;
1048
	}
1049
1050
	/**
1051
	 * Returns the percent progress towards the next level.
1052
	 * This value is rounded because it is used primarily in HTML img widths.
1053
	 */
1054
	public function getNextLevelPercentAcquired() : int {
1055
		if ($this->getNextLevelExperience() == $this->getThisLevelExperience()) {
1056
			return 100;
1057
		}
1058
		return max(0, min(100, IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
1059
	}
1060
1061
	public function getNextLevelPercentRemaining() {
1062
		return 100 - $this->getNextLevelPercentAcquired();
1063
	}
1064
1065
	public function getNextLevelExperience() {
1066
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1067
		if (!isset($LEVELS_REQUIREMENTS[$this->getLevelID() + 1])) {
1068
			return $this->getThisLevelExperience(); //Return current level experience if on last level.
1069
		}
1070
		return $LEVELS_REQUIREMENTS[$this->getLevelID() + 1]['Requirement'];
1071
	}
1072
1073
	public function getThisLevelExperience() {
1074
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1075
		return $LEVELS_REQUIREMENTS[$this->getLevelID()]['Requirement'];
1076
	}
1077
1078
	public function setExperience($experience) {
1079
		if ($this->experience == $experience) {
1080
			return;
1081
		}
1082
		if ($experience < MIN_EXPERIENCE) {
1083
			$experience = MIN_EXPERIENCE;
1084
		}
1085
		if ($experience > MAX_EXPERIENCE) {
1086
			$experience = MAX_EXPERIENCE;
1087
		}
1088
		$this->experience = $experience;
1089
		$this->hasChanged = true;
1090
1091
		// Since exp has changed, invalidate the player level so that it can
1092
		// be recomputed next time it is queried (in case it has changed).
1093
		$this->level = null;
1094
	}
1095
1096
	/**
1097
	 * Increases onboard credits up to the maximum allowed credits.
1098
	 * Returns the amount that was actually added to handle overflow.
1099
	 */
1100
	public function increaseCredits(int $credits) : int {
1101
		if ($credits == 0) {
1102
			return 0;
1103
		}
1104
		if ($credits < 0) {
1105
			throw new Exception('Trying to increase negative credits.');
1106
		}
1107
		$newTotal = min($this->credits + $credits, MAX_MONEY);
1108
		$actualAdded = $newTotal - $this->credits;
1109
		$this->setCredits($newTotal);
1110
		return $actualAdded;
1111
	}
1112
1113
	public function decreaseCredits(int $credits) : void {
1114
		if ($credits == 0) {
1115
			return;
1116
		}
1117
		if ($credits < 0) {
1118
			throw new Exception('Trying to decrease negative credits.');
1119
		}
1120
		$newTotal = $this->credits - $credits;
1121
		$this->setCredits($newTotal);
1122
	}
1123
1124
	public function setCredits(int $credits) : void {
1125
		if ($this->credits == $credits) {
1126
			return;
1127
		}
1128
		if ($credits < 0) {
1129
			throw new Exception('Trying to set negative credits.');
1130
		}
1131
		if ($credits > MAX_MONEY) {
1132
			throw new Exception('Trying to set more than max credits.');
1133
		}
1134
		$this->credits = $credits;
1135
		$this->hasChanged = true;
1136
	}
1137
1138
	public function increaseExperience($experience) {
1139
		if ($experience < 0) {
1140
			throw new Exception('Trying to increase negative experience.');
1141
		}
1142
		if ($experience == 0) {
1143
			return;
1144
		}
1145
		$newExperience = $this->experience + $experience;
1146
		$this->setExperience($newExperience);
1147
		$this->increaseHOF($experience, array('Experience', 'Total', 'Gain'), HOF_PUBLIC);
1148
	}
1149
	public function decreaseExperience($experience) {
1150
		if ($experience < 0) {
1151
			throw new Exception('Trying to decrease negative experience.');
1152
		}
1153
		if ($experience == 0) {
1154
			return;
1155
		}
1156
		$newExperience = $this->experience - $experience;
1157
		$this->setExperience($newExperience);
1158
		$this->increaseHOF($experience, array('Experience', 'Total', 'Loss'), HOF_PUBLIC);
1159
	}
1160
1161
	public function isLandedOnPlanet() {
1162
		return $this->landedOnPlanet;
1163
	}
1164
1165
	public function setLandedOnPlanet($bool) {
1166
		if ($this->landedOnPlanet == $bool) {
1167
			return;
1168
		}
1169
		$this->landedOnPlanet = $bool;
1170
		$this->hasChanged = true;
1171
	}
1172
1173
	/**
1174
	 * Returns the numerical level of the player (e.g. 1-50).
1175
	 */
1176
	public function getLevelID() {
1177
		// The level is cached for performance reasons unless `setExperience`
1178
		// is called and the player's experience changes.
1179
		if ($this->level === null) {
1180
			$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1181
			foreach ($LEVELS_REQUIREMENTS as $level_id => $require) {
1182
				if ($this->getExperience() >= $require['Requirement']) {
1183
					continue;
1184
				}
1185
				$this->level = $level_id - 1;
1186
				return $this->level;
1187
			}
1188
			$this->level = max(array_keys($LEVELS_REQUIREMENTS));
1189
		}
1190
		return $this->level;
1191
	}
1192
1193
	public function getLevelName() {
1194
		$level_name = Globals::getLevelRequirements()[$this->getLevelID()]['Name'];
1195
		if ($this->isPresident()) {
1196
			$level_name = '<img src="images/council_president.png" title="' . Globals::getRaceName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1197
		}
1198
		return $level_name;
1199
	}
1200
1201
	public function getMaxLevel() {
1202
		return max(array_keys(Globals::getLevelRequirements()));
1203
	}
1204
1205
	public function getPlayerID() {
1206
		return $this->playerID;
1207
	}
1208
1209
	/**
1210
	 * Returns the player name.
1211
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1212
	 */
1213
	public function getPlayerName() {
1214
		return $this->playerName;
1215
	}
1216
1217
	public function setPlayerName($name) {
1218
		$this->playerName = $name;
1219
		$this->hasChanged = true;
1220
	}
1221
1222
	/**
1223
	 * Returns the decorated player name, suitable for HTML display.
1224
	 */
1225
	public function getDisplayName($includeAlliance = false) {
1226
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1227
		$return = get_colored_text($this->getAlignment(), $name);
1228
		if ($this->isNPC()) {
1229
			$return .= ' <span class="npcColour">[NPC]</span>';
1230
		}
1231
		if ($includeAlliance) {
1232
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1233
		}
1234
		return $return;
1235
	}
1236
1237
	public function getBBLink() {
1238
			return '[player=' . $this->getPlayerID() . ']';
1239
	}
1240
1241
	public function getLinkedDisplayName($includeAlliance = true) {
1242
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1243
		if ($includeAlliance) {
1244
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1245
		}
1246
		return $return;
1247
	}
1248
1249
	/**
1250
	 * Use this method when the player is changing their own name.
1251
	 * This will flag the player as having used their free name change.
1252
	 */
1253
	public function setPlayerNameByPlayer($playerName) {
1254
		$this->setPlayerName($playerName);
1255
		$this->setNameChanged(true);
1256
	}
1257
1258
	public function isNameChanged() {
1259
		return $this->nameChanged;
1260
	}
1261
1262
	public function setNameChanged($bool) {
1263
		$this->nameChanged = $bool;
1264
		$this->hasChanged = true;
1265
	}
1266
1267
	public function isRaceChanged() : bool {
1268
		return $this->raceChanged;
1269
	}
1270
1271
	public function setRaceChanged(bool $raceChanged) : void {
1272
		$this->raceChanged = $raceChanged;
1273
		$this->hasChanged = true;
1274
	}
1275
1276
	public function canChangeRace() : bool {
1277
		return !$this->isRaceChanged() && (SmrSession::getTime() - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1278
	}
1279
1280
	public static function getColouredRaceNameOrDefault($otherRaceID, AbstractSmrPlayer $player = null, $linked = false) {
1281
		$relations = 0;
1282
		if ($player !== null) {
1283
			$relations = $player->getRelation($otherRaceID);
1284
		}
1285
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1286
	}
1287
1288
	public function getColouredRaceName($otherRaceID, $linked = false) {
1289
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1290
	}
1291
1292
	public function setRaceID($raceID) {
1293
		if ($this->raceID == $raceID) {
1294
			return;
1295
		}
1296
		$this->raceID = $raceID;
1297
		$this->hasChanged = true;
1298
	}
1299
1300
	public function isAllianceLeader($forceUpdate = false) {
1301
		return $this->getAccountID() == $this->getAlliance($forceUpdate)->getLeaderID();
1302
	}
1303
1304
	public function getAlliance($forceUpdate = false) {
1305
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1306
	}
1307
1308
	public function getAllianceID() {
1309
		return $this->allianceID;
1310
	}
1311
1312
	public function hasAlliance() {
1313
		return $this->getAllianceID() != 0;
1314
	}
1315
1316
	protected function setAllianceID($ID) {
1317
		if ($this->allianceID == $ID) {
1318
			return;
1319
		}
1320
		$this->allianceID = $ID;
1321
		if ($this->allianceID != 0) {
1322
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1323
			$this->db->query('INSERT IGNORE INTO player_joined_alliance (account_id,game_id,alliance_id,status) ' .
1324
				'VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1325
		}
1326
		$this->hasChanged = true;
1327
	}
1328
1329
	public function getAllianceBBLink() {
1330
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1331
	}
1332
1333
	public function getAllianceDisplayName($linked = false, $includeAllianceID = false) {
1334
		if ($this->hasAlliance()) {
1335
			return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1336
		} else {
1337
			return 'No Alliance';
1338
		}
1339
	}
1340
1341
	public function getAllianceRole($allianceID = false) {
1342
		if ($allianceID === false) {
1343
			$allianceID = $this->getAllianceID();
1344
		}
1345
		if (!isset($this->allianceRoles[$allianceID])) {
1346
			$this->allianceRoles[$allianceID] = 0;
1347
			$this->db->query('SELECT role_id
1348
						FROM player_has_alliance_role
1349
						WHERE ' . $this->SQL . '
1350
						AND alliance_id=' . $this->db->escapeNumber($allianceID) . '
1351
						LIMIT 1');
1352
			if ($this->db->nextRecord()) {
1353
				$this->allianceRoles[$allianceID] = $this->db->getInt('role_id');
1354
			}
1355
		}
1356
		return $this->allianceRoles[$allianceID];
1357
	}
1358
1359
	public function leaveAlliance(AbstractSmrPlayer $kickedBy = null) {
1360
		$allianceID = $this->getAllianceID();
1361
		$alliance = $this->getAlliance();
1362
		if ($kickedBy != null) {
1363
			$kickedBy->sendMessage($this->getAccountID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1364
			$this->actionTaken('PlayerKicked', array('Alliance' => $alliance, 'Player' => $kickedBy));
1365
			$kickedBy->actionTaken('KickPlayer', array('Alliance' => $alliance, 'Player' => $this));
1366
		} elseif ($this->isAllianceLeader()) {
1367
			$this->actionTaken('DisbandAlliance', array('Alliance' => $alliance));
1368
		} else {
1369
			$this->actionTaken('LeaveAlliance', array('Alliance' => $alliance));
1370
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1371
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I left your alliance!', false);
1372
			}
1373
		}
1374
1375
		if (!$this->isAllianceLeader() && $allianceID != NHA_ID) { // Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1376
			$this->setAllianceJoinable(SmrSession::getTime() + self::TIME_FOR_ALLIANCE_SWITCH);
1377
			$alliance->getLeader()->setAllianceJoinable(SmrSession::getTime() + self::TIME_FOR_ALLIANCE_SWITCH); //We set the joinable time for leader here, that way a single player alliance won't cause a player to wait before switching.
1378
		}
1379
1380
		$this->setAllianceID(0);
1381
		$this->db->query('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1382
	}
1383
1384
	/**
1385
	 * Join an alliance (used for both Leader and New Member roles)
1386
	 */
1387
	public function joinAlliance($allianceID) {
1388
		$this->setAllianceID($allianceID);
1389
		$alliance = $this->getAlliance();
1390
1391
		if (!$this->isAllianceLeader()) {
1392
			// Do not throw an exception if the NHL account doesn't exist.
1393
			try {
1394
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I joined your alliance!', false);
1395
			} catch (AccountNotFoundException $e) {
1396
				if ($alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1397
					throw $e;
1398
				}
1399
			}
1400
1401
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1402
		} else {
1403
			$roleID = ALLIANCE_ROLE_LEADER;
1404
		}
1405
		$this->db->query('INSERT INTO player_has_alliance_role (game_id, account_id, role_id, alliance_id) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($roleID) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
1406
1407
		$this->actionTaken('JoinAlliance', array('Alliance' => $alliance));
1408
	}
1409
1410
	public function getAllianceJoinable() {
1411
		return $this->allianceJoinable;
1412
	}
1413
1414
	private function setAllianceJoinable($time) {
1415
		if ($this->allianceJoinable == $time) {
1416
			return;
1417
		}
1418
		$this->allianceJoinable = $time;
1419
		$this->hasChanged = true;
1420
	}
1421
1422
	/**
1423
	 * Invites player with $accountID to this player's alliance.
1424
	 */
1425
	public function sendAllianceInvitation(int $accountID, string $message, int $expires) : void {
1426
		if (!$this->hasAlliance()) {
1427
			throw new Exception('Must be in an alliance to send alliance invitations');
1428
		}
1429
		// Send message to invited player
1430
		$messageID = $this->sendMessage($accountID, MSG_PLAYER, $message, false, true, $expires, true);
1431
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), $messageID, $expires);
0 ignored issues
show
Bug introduced by
It seems like $messageID can also be of type string; however, parameter $messageID of SmrInvitation::send() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1431
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), /** @scrutinizer ignore-type */ $messageID, $expires);
Loading history...
1432
	}
1433
1434
	public function isCombatDronesKamikazeOnMines() {
1435
		return $this->combatDronesKamikazeOnMines;
1436
	}
1437
1438
	public function setCombatDronesKamikazeOnMines($bool) {
1439
		if ($this->combatDronesKamikazeOnMines == $bool) {
1440
			return;
1441
		}
1442
		$this->combatDronesKamikazeOnMines = $bool;
1443
		$this->hasChanged = true;
1444
	}
1445
1446
	protected function getPersonalRelationsData() {
1447
		if (!isset($this->personalRelations)) {
1448
			//get relations
1449
			$RACES = Globals::getRaces();
1450
			$this->personalRelations = array();
1451
			foreach ($RACES as $raceID => $raceName) {
1452
				$this->personalRelations[$raceID] = 0;
1453
			}
1454
			$this->db->query('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL . ' LIMIT ' . count($RACES));
1455
			while ($this->db->nextRecord()) {
1456
				$this->personalRelations[$this->db->getInt('race_id')] = $this->db->getInt('relation');
1457
			}
1458
		}
1459
	}
1460
1461
	public function getPersonalRelations() {
1462
		$this->getPersonalRelationsData();
1463
		return $this->personalRelations;
1464
	}
1465
1466
	/**
1467
	 * Get personal relations with a race
1468
	 */
1469
	public function getPersonalRelation($raceID) {
1470
		$rels = $this->getPersonalRelations();
1471
		return $rels[$raceID];
1472
	}
1473
1474
	/**
1475
	 * Get total relations with all races (personal + political)
1476
	 */
1477
	public function getRelations() {
1478
		if (!isset($this->relations)) {
1479
			//get relations
1480
			$RACES = Globals::getRaces();
1481
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1482
			$personalRels = $this->getPersonalRelations(); // make sure they're initialised.
1483
			$this->relations = array();
1484
			foreach ($RACES as $raceID => $raceName) {
1485
				$this->relations[$raceID] = $personalRels[$raceID] + $raceRelations[$raceID];
1486
			}
1487
		}
1488
		return $this->relations;
1489
	}
1490
1491
	/**
1492
	 * Get total relations with a race (personal + political)
1493
	 */
1494
	public function getRelation($raceID) {
1495
		$rels = $this->getRelations();
1496
		return $rels[$raceID];
1497
	}
1498
1499
	/**
1500
	 * Increases personal relations from trading $numGoods units with the race
1501
	 * of the port given by $raceID.
1502
	 */
1503
	public function increaseRelationsByTrade($numGoods, $raceID) {
1504
		$relations = ICeil(min($numGoods, 300) / 30);
1505
		//Cap relations to a max of 1 after 500 have been reached
1506
		if ($this->getPersonalRelation($raceID) + $relations >= 500) {
1507
			$relations = max(1, min($relations, 500 - $this->getPersonalRelation($raceID)));
1508
		}
1509
		$this->increaseRelations($relations, $raceID);
1510
	}
1511
1512
	/**
1513
	 * Decreases personal relations from trading failures, e.g. rejected
1514
	 * bargaining and getting caught stealing.
1515
	 */
1516
	public function decreaseRelationsByTrade($numGoods, $raceID) {
1517
		$relations = ICeil(min($numGoods, 300) / 30);
1518
		$this->decreaseRelations($relations, $raceID);
1519
	}
1520
1521
	/**
1522
	 * Increase personal relations.
1523
	 */
1524
	public function increaseRelations($relations, $raceID) {
1525
		if ($relations < 0) {
1526
			throw new Exception('Trying to increase negative relations.');
1527
		}
1528
		if ($relations == 0) {
1529
			return;
1530
		}
1531
		$relations += $this->getPersonalRelation($raceID);
1532
		$this->setRelations($relations, $raceID);
1533
	}
1534
1535
	/**
1536
	 * Decrease personal relations.
1537
	 */
1538
	public function decreaseRelations($relations, $raceID) {
1539
		if ($relations < 0) {
1540
			throw new Exception('Trying to decrease negative relations.');
1541
		}
1542
		if ($relations == 0) {
1543
			return;
1544
		}
1545
		$relations = $this->getPersonalRelation($raceID) - $relations;
1546
		$this->setRelations($relations, $raceID);
1547
	}
1548
1549
	/**
1550
	 * Set personal relations.
1551
	 */
1552
	public function setRelations($relations, $raceID) {
1553
		$this->getRelations();
1554
		if ($this->personalRelations[$raceID] == $relations) {
1555
			return;
1556
		}
1557
		if ($relations < MIN_RELATIONS) {
1558
			$relations = MIN_RELATIONS;
1559
		}
1560
		$relationsDiff = IRound($relations - $this->personalRelations[$raceID]);
1561
		$this->personalRelations[$raceID] = $relations;
1562
		$this->relations[$raceID] += $relationsDiff;
1563
		$this->db->query('REPLACE INTO player_has_relation (account_id,game_id,race_id,relation) values (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($raceID) . ',' . $this->db->escapeNumber($this->personalRelations[$raceID]) . ')');
1564
	}
1565
1566
	/**
1567
	 * Set any starting personal relations bonuses or penalties.
1568
	 */
1569
	public function giveStartingRelations() {
1570
		if ($this->getRaceID() === RACE_ALSKANT) {
1571
			// Give Alskants bonus personal relations to start.
1572
			foreach (Globals::getRaces() as $raceID => $raceInfo) {
1573
				$this->setRelations(ALSKANT_BONUS_RELATIONS, $raceID);
1574
			}
1575
		}
1576
	}
1577
1578
	public function getLastNewsUpdate() {
1579
		return $this->lastNewsUpdate;
1580
	}
1581
1582
	private function setLastNewsUpdate($time) {
1583
		if ($this->lastNewsUpdate == $time) {
1584
			return;
1585
		}
1586
		$this->lastNewsUpdate = $time;
1587
		$this->hasChanged = true;
1588
	}
1589
1590
	public function updateLastNewsUpdate() {
1591
		$this->setLastNewsUpdate(SmrSession::getTime());
1592
	}
1593
1594
	public function getLastPort() {
1595
		return $this->lastPort;
1596
	}
1597
1598
	public function setLastPort($lastPort) {
1599
		if ($this->lastPort == $lastPort) {
1600
			return;
1601
		}
1602
		$this->lastPort = $lastPort;
1603
		$this->hasChanged = true;
1604
	}
1605
1606
	public function getPlottedCourse() {
1607
		if (!isset($this->plottedCourse)) {
1608
			// check if we have a course plotted
1609
			$this->db->query('SELECT course FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1610
1611
			if ($this->db->nextRecord()) {
1612
				// get the course back
1613
				$this->plottedCourse = unserialize($this->db->getField('course'));
1614
			} else {
1615
				$this->plottedCourse = false;
1616
			}
1617
		}
1618
1619
		// Update the plotted course if we have moved since the last query
1620
		if ($this->plottedCourse !== false && (!isset($this->plottedCourseFrom) || $this->plottedCourseFrom != $this->getSectorID())) {
1621
			$this->plottedCourseFrom = $this->getSectorID();
1622
1623
			if ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1624
				// We have walked into the next sector of the course
1625
				$this->plottedCourse->followPath();
1626
				$this->setPlottedCourse($this->plottedCourse);
1627
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1628
				// We have skipped to some later sector in the course
1629
				$this->plottedCourse->skipToSector($this->getSectorID());
1630
				$this->setPlottedCourse($this->plottedCourse);
1631
			}
1632
		}
1633
		return $this->plottedCourse;
1634
	}
1635
1636
	public function setPlottedCourse(Distance $plottedCourse) {
1637
		$hadPlottedCourse = $this->hasPlottedCourse();
1638
		$this->plottedCourse = $plottedCourse;
1639
		if ($this->plottedCourse->getTotalSectors() > 0) {
1640
			$this->db->query('REPLACE INTO player_plotted_course
1641
				(account_id, game_id, course)
1642
				VALUES(' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeBinary(serialize($this->plottedCourse)) . ')');
1643
		} elseif ($hadPlottedCourse) {
1644
			$this->deletePlottedCourse();
1645
		}
1646
	}
1647
1648
	public function hasPlottedCourse() {
1649
		return $this->getPlottedCourse() !== false;
1650
	}
1651
1652
	public function isPartOfCourse($sectorOrSectorID) {
1653
		if (!$this->hasPlottedCourse()) {
1654
			return false;
1655
		}
1656
		if ($sectorOrSectorID instanceof SmrSector) {
1657
			$sectorID = $sectorOrSectorID->getSectorID();
1658
		} else {
1659
			$sectorID = $sectorOrSectorID;
1660
		}
1661
		return $this->getPlottedCourse()->isInPath($sectorID);
1662
	}
1663
1664
	public function deletePlottedCourse() {
1665
		$this->plottedCourse = false;
1666
		$this->db->query('DELETE FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1667
	}
1668
1669
	// Computes the turn cost and max misjump between current and target sector
1670
	public function getJumpInfo(SmrSector $targetSector) {
1671
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1672
		if ($path === false) {
1673
			create_error('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1674
		}
1675
		$distance = $path->getRelativeDistance();
1676
1677
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
1678
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1679
		return array('turn_cost' => $turnCost, 'max_misjump' => $maxMisjump);
1680
	}
1681
1682
	public function __sleep() {
1683
		return array('accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName');
1684
	}
1685
1686
	public function &getStoredDestinations() {
1687
		if (!isset($this->storedDestinations)) {
1688
			$this->storedDestinations = array();
1689
			$this->db->query('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1690
			while ($this->db->nextRecord()) {
1691
				$this->storedDestinations[] = array(
1692
					'Label' => $this->db->getField('label'),
1693
					'SectorID' => $this->db->getInt('sector_id'),
1694
					'OffsetTop' => $this->db->getInt('offset_top'),
1695
					'OffsetLeft' => $this->db->getInt('offset_left')
1696
				);
1697
			}
1698
		}
1699
		return $this->storedDestinations;
1700
	}
1701
1702
	public function moveDestinationButton($sectorID, $offsetTop, $offsetLeft) {
1703
1704
		if (!is_numeric($offsetLeft) || !is_numeric($offsetTop)) {
1705
			create_error('The position of the saved sector must be numeric!.');
1706
		}
1707
		$offsetTop = round($offsetTop);
1708
		$offsetLeft = round($offsetLeft);
1709
1710
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1711
			create_error('The saved sector must be in the box!');
1712
		}
1713
1714
		$storedDestinations =& $this->getStoredDestinations();
1715
		foreach ($storedDestinations as &$sd) {
1716
			if ($sd['SectorID'] == $sectorID) {
1717
				$sd['OffsetTop'] = $offsetTop;
1718
				$sd['OffsetLeft'] = $offsetLeft;
1719
				$this->db->query('
1720
					UPDATE player_stored_sector
1721
						SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1722
					WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID)
1723
				);
1724
				return true;
1725
			}
1726
		}
1727
1728
		create_error('You do not have a saved sector for #' . $sectorID);
1729
	}
1730
1731
	public function addDestinationButton($sectorID, $label) {
1732
1733
		if (!is_numeric($sectorID) || !SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1734
			create_error('You want to add a non-existent sector?');
1735
		}
1736
1737
		// sector already stored ?
1738
		foreach ($this->getStoredDestinations() as $sd) {
1739
			if ($sd['SectorID'] == $sectorID) {
1740
				create_error('Sector already stored!');
1741
			}
1742
		}
1743
1744
		$this->storedDestinations[] = array(
1745
			'Label' => $label,
1746
			'SectorID' => (int)$sectorID,
1747
			'OffsetTop' => 1,
1748
			'OffsetLeft' => 1
1749
		);
1750
1751
		$this->db->query('
1752
			INSERT INTO player_stored_sector (account_id, game_id, sector_id, label, offset_top, offset_left)
1753
			VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($sectorID) . ',' . $this->db->escapeString($label) . ',1,1)'
1754
		);
1755
	}
1756
1757
	public function deleteDestinationButton($sectorID) {
1758
		if (!is_numeric($sectorID) || $sectorID < 1) {
1759
			create_error('You want to remove a non-existent sector?');
1760
		}
1761
1762
		foreach ($this->getStoredDestinations() as $key => $sd) {
1763
			if ($sd['SectorID'] == $sectorID) {
1764
				$this->db->query('
1765
					DELETE FROM player_stored_sector
1766
					WHERE ' . $this->SQL . '
1767
					AND sector_id = ' . $this->db->escapeNumber($sectorID)
1768
				);
1769
				unset($this->storedDestinations[$key]);
1770
				return true;
1771
			}
1772
		}
1773
		return false;
1774
	}
1775
1776
	public function getTickers() {
1777
		if (!isset($this->tickers)) {
1778
			$this->tickers = array();
1779
			//get ticker info
1780
			$this->db->query('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(SmrSession::getTime()));
1781
			while ($this->db->nextRecord()) {
1782
				$this->tickers[$this->db->getField('type')] = [
1783
					'Type' => $this->db->getField('type'),
1784
					'Time' => $this->db->getInt('time'),
1785
					'Expires' => $this->db->getInt('expires'),
1786
					'Recent' => $this->db->getField('recent'),
1787
				];
1788
			}
1789
		}
1790
		return $this->tickers;
1791
	}
1792
1793
	public function hasTickers() {
1794
		return count($this->getTickers()) > 0;
1795
	}
1796
1797
	public function getTicker($tickerType) {
1798
		$tickers = $this->getTickers();
1799
		if (isset($tickers[$tickerType])) {
1800
			return $tickers[$tickerType];
1801
		}
1802
		return false;
1803
	}
1804
1805
	public function hasTicker($tickerType) {
1806
		return $this->getTicker($tickerType) !== false;
1807
	}
1808
1809
	public function &shootForces(SmrForce $forces) {
1810
		return $this->getShip()->shootForces($forces);
1811
	}
1812
1813
	public function &shootPort(SmrPort $port) {
1814
		return $this->getShip()->shootPort($port);
1815
	}
1816
1817
	public function &shootPlanet(SmrPlanet $planet, $delayed) {
1818
		return $this->getShip()->shootPlanet($planet, $delayed);
1819
	}
1820
1821
	public function &shootPlayers(array $targetPlayers) {
1822
		return $this->getShip()->shootPlayers($targetPlayers);
1823
	}
1824
1825
	public function getMilitaryPayment() {
1826
		return $this->militaryPayment;
1827
	}
1828
1829
	public function hasMilitaryPayment() {
1830
		return $this->getMilitaryPayment() > 0;
1831
	}
1832
1833
	public function setMilitaryPayment($amount) {
1834
		if ($this->militaryPayment == $amount) {
1835
			return;
1836
		}
1837
		$this->militaryPayment = $amount;
1838
		$this->hasChanged = true;
1839
	}
1840
1841
	public function increaseMilitaryPayment($amount) {
1842
		if ($amount < 0) {
1843
			throw new Exception('Trying to increase negative military payment.');
1844
		}
1845
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1846
	}
1847
1848
	public function decreaseMilitaryPayment($amount) {
1849
		if ($amount < 0) {
1850
			throw new Exception('Trying to decrease negative military payment.');
1851
		}
1852
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1853
	}
1854
1855
	protected function getBountiesData() {
1856
		if (!isset($this->bounties)) {
1857
			$this->bounties = array();
1858
			$this->db->query('SELECT * FROM bounty WHERE ' . $this->SQL);
1859
			while ($this->db->nextRecord()) {
1860
				$this->bounties[$this->db->getInt('bounty_id')] = array(
1861
							'Amount' => $this->db->getInt('amount'),
1862
							'SmrCredits' => $this->db->getInt('smr_credits'),
1863
							'Type' => $this->db->getField('type'),
1864
							'Claimer' => $this->db->getInt('claimer_id'),
1865
							'Time' => $this->db->getInt('time'),
1866
							'ID' => $this->db->getInt('bounty_id'),
1867
							'New' => false);
1868
			}
1869
		}
1870
	}
1871
1872
	// Get bounties that can be claimed by this player
1873
	// Type must be 'HQ' or 'UG'
1874
	public function getClaimableBounties($type) {
1875
		$bounties = array();
1876
		$this->db->query('SELECT * FROM bounty WHERE claimer_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND type=' . $this->db->escapeString($type));
1877
		while ($this->db->nextRecord()) {
1878
			$bounties[] = array(
1879
				'player' => SmrPlayer::getPlayer($this->db->getInt('account_id'), $this->getGameID()),
1880
				'bounty_id' => $this->db->getInt('bounty_id'),
1881
				'credits' => $this->db->getInt('amount'),
1882
				'smr_credits' => $this->db->getInt('smr_credits'),
1883
			);
1884
		}
1885
		return $bounties;
1886
	}
1887
1888
	public function getBounties() : array {
1889
		$this->getBountiesData();
1890
		return $this->bounties;
1891
	}
1892
1893
	public function hasBounties() : bool {
1894
		return count($this->getBounties()) > 0;
1895
	}
1896
1897
	protected function getBounty(int $bountyID) : array {
1898
		if (!$this->hasBounty($bountyID)) {
1899
			throw new Exception('BountyID does not exist: ' . $bountyID);
1900
		}
1901
		return $this->bounties[$bountyID];
1902
	}
1903
1904
	public function hasBounty(int $bountyID) : bool {
1905
		$bounties = $this->getBounties();
1906
		return isset($bounties[$bountyID]);
1907
	}
1908
1909
	protected function getBountyAmount(int $bountyID) : int {
1910
		$bounty = $this->getBounty($bountyID);
1911
		return $bounty['Amount'];
1912
	}
1913
1914
	protected function createBounty(string $type) : array {
1915
		$bounty = array('Amount' => 0,
1916
						'SmrCredits' => 0,
1917
						'Type' => $type,
1918
						'Claimer' => 0,
1919
						'Time' => SmrSession::getTime(),
1920
						'ID' => $this->getNextBountyID(),
1921
						'New' => true);
1922
		$this->setBounty($bounty);
1923
		return $bounty;
1924
	}
1925
1926
	protected function getNextBountyID() : int {
1927
		$keys = array_keys($this->getBounties());
1928
		if (count($keys) > 0) {
1929
			return max($keys) + 1;
1930
		} else {
1931
			return 0;
1932
		}
1933
	}
1934
1935
	protected function setBounty(array $bounty) : void {
1936
		$this->bounties[$bounty['ID']] = $bounty;
1937
		$this->hasBountyChanged[$bounty['ID']] = true;
1938
	}
1939
1940
	protected function setBountyAmount(int $bountyID, int $amount) : void {
1941
		$bounty = $this->getBounty($bountyID);
1942
		$bounty['Amount'] = $amount;
1943
		$this->setBounty($bounty);
1944
	}
1945
1946
	public function getCurrentBounty(string $type) : array {
1947
		$bounties = $this->getBounties();
1948
		foreach ($bounties as $bounty) {
1949
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1950
				return $bounty;
1951
			}
1952
		}
1953
		return $this->createBounty($type);
1954
	}
1955
1956
	public function hasCurrentBounty(string $type) : bool {
1957
		$bounties = $this->getBounties();
1958
		foreach ($bounties as $bounty) {
1959
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1960
				return true;
1961
			}
1962
		}
1963
		return false;
1964
	}
1965
1966
	protected function getCurrentBountyAmount(string $type) : int {
1967
		$bounty = $this->getCurrentBounty($type);
1968
		return $bounty['Amount'];
1969
	}
1970
1971
	protected function setCurrentBountyAmount(string $type, int $amount) : void {
1972
		$bounty = $this->getCurrentBounty($type);
1973
		if ($bounty['Amount'] == $amount) {
1974
			return;
1975
		}
1976
		$bounty['Amount'] = $amount;
1977
		$this->setBounty($bounty);
1978
	}
1979
1980
	public function increaseCurrentBountyAmount(string $type, int $amount) : void {
1981
		if ($amount < 0) {
1982
			throw new Exception('Trying to increase negative current bounty.');
1983
		}
1984
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) + $amount);
1985
	}
1986
1987
	public function decreaseCurrentBountyAmount(string $type, int $amount) : void {
1988
		if ($amount < 0) {
1989
			throw new Exception('Trying to decrease negative current bounty.');
1990
		}
1991
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) - $amount);
1992
	}
1993
1994
	protected function getCurrentBountySmrCredits(string $type) : int {
1995
		$bounty = $this->getCurrentBounty($type);
1996
		return $bounty['SmrCredits'];
1997
	}
1998
1999
	protected function setCurrentBountySmrCredits(string $type, int $credits) : void {
2000
		$bounty = $this->getCurrentBounty($type);
2001
		if ($bounty['SmrCredits'] == $credits) {
2002
			return;
2003
		}
2004
		$bounty['SmrCredits'] = $credits;
2005
		$this->setBounty($bounty);
2006
	}
2007
2008
	public function increaseCurrentBountySmrCredits(string $type, int $credits) : void {
2009
		if ($credits < 0) {
2010
			throw new Exception('Trying to increase negative current bounty.');
2011
		}
2012
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) + $credits);
2013
	}
2014
2015
	public function decreaseCurrentBountySmrCredits(string $type, int $credits) : void {
2016
		if ($credits < 0) {
2017
			throw new Exception('Trying to decrease negative current bounty.');
2018
		}
2019
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) - $credits);
2020
	}
2021
2022
	public function setBountiesClaimable(AbstractSmrPlayer $claimer) : void {
2023
		foreach ($this->getBounties() as $bounty) {
2024
			if ($bounty['Claimer'] == 0) {
2025
				$bounty['Claimer'] = $claimer->getAccountID();
2026
				$this->setBounty($bounty);
2027
			}
2028
		}
2029
	}
2030
2031
	protected function getHOFData() {
2032
		if (!isset($this->HOF)) {
2033
			//Get Player HOF
2034
			$this->db->query('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2035
			$this->HOF = array();
2036
			while ($this->db->nextRecord()) {
2037
				$hof =& $this->HOF;
2038
				$typeList = explode(':', $this->db->getField('type'));
2039
				foreach ($typeList as $type) {
2040
					if (!isset($hof[$type])) {
2041
						$hof[$type] = array();
2042
					}
2043
					$hof =& $hof[$type];
2044
				}
2045
				$hof = $this->db->getFloat('amount');
2046
			}
2047
			self::getHOFVis();
2048
		}
2049
	}
2050
2051
	public static function getHOFVis() {
2052
		if (!isset(self::$HOFVis)) {
2053
			//Get Player HOF Vis
2054
			$db = MySqlDatabase::getInstance();
2055
			$db->query('SELECT type,visibility FROM hof_visibility');
2056
			self::$HOFVis = array();
2057
			while ($db->nextRecord()) {
2058
				self::$HOFVis[$db->getField('type')] = $db->getField('visibility');
2059
			}
2060
		}
2061
	}
2062
2063
	public function getHOF(array $typeList = null) {
2064
		$this->getHOFData();
2065
		if ($typeList == null) {
2066
			return $this->HOF;
2067
		}
2068
		$hof = $this->HOF;
2069
		foreach ($typeList as $type) {
2070
			if (!isset($hof[$type])) {
2071
				return 0;
2072
			}
2073
			$hof = $hof[$type];
2074
		}
2075
		return $hof;
2076
	}
2077
2078
	public function increaseHOF($amount, array $typeList, $visibility) {
2079
		if ($amount < 0) {
2080
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2081
		}
2082
		if ($amount == 0) {
2083
			return;
2084
		}
2085
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2086
	}
2087
2088
	public function decreaseHOF($amount, array $typeList, $visibility) {
2089
		if ($amount < 0) {
2090
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2091
		}
2092
		if ($amount == 0) {
2093
			return;
2094
		}
2095
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2096
	}
2097
2098
	public function setHOF($amount, array $typeList, $visibility) {
2099
		if (is_array($this->getHOF($typeList))) {
2100
			throw new Exception('Trying to overwrite a HOF type: ' . implode(':', $typeList));
2101
		}
2102
		if ($this->isNPC()) {
2103
			// Don't store HOF for NPCs.
2104
			return;
2105
		}
2106
		if ($this->getHOF($typeList) == $amount) {
2107
			return;
2108
		}
2109
		if ($amount < 0) {
2110
			$amount = 0;
2111
		}
2112
		$this->getHOF();
2113
2114
		$hofType = implode(':', $typeList);
2115
		if (!isset(self::$HOFVis[$hofType])) {
2116
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2117
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2118
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2119
		}
2120
		self::$HOFVis[$hofType] = $visibility;
2121
2122
		$hof =& $this->HOF;
2123
		$hofChanged =& $this->hasHOFChanged;
2124
		$new = false;
2125
		foreach ($typeList as $type) {
2126
			if (!isset($hofChanged[$type])) {
2127
				$hofChanged[$type] = array();
2128
			}
2129
			if (!isset($hof[$type])) {
2130
				$hof[$type] = array();
2131
				$new = true;
2132
			}
2133
			$hof =& $hof[$type];
2134
			$hofChanged =& $hofChanged[$type];
2135
		}
2136
		if ($hofChanged == null) {
2137
			$hofChanged = self::HOF_CHANGED;
2138
			if ($new) {
2139
				$hofChanged = self::HOF_NEW;
2140
			}
2141
		}
2142
		$hof = $amount;
2143
	}
2144
2145
	public function getExperienceRank() {
2146
		return $this->computeRanking('experience');
2147
	}
2148
2149
	public function getKillsRank() {
2150
		return $this->computeRanking('kills');
2151
	}
2152
2153
	public function getDeathsRank() {
2154
		return $this->computeRanking('deaths');
2155
	}
2156
2157
	public function getAssistsRank() {
2158
		return $this->computeRanking('assists');
2159
	}
2160
2161
	private function computeRanking(string $dbField) : int {
2162
		$this->db->query('SELECT ranking
2163
			FROM (
2164
				SELECT player_id,
2165
				ROW_NUMBER() OVER (ORDER BY ' . $dbField . ' DESC, player_name ASC) AS ranking
2166
				FROM player
2167
				WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . '
2168
			) t
2169
			WHERE player_id = ' . $this->db->escapeNumber($this->getPlayerID())
2170
		);
2171
		$this->db->requireRecord();
2172
		return $this->db->getInt('ranking');
2173
	}
2174
2175
	public function killPlayer($sectorID) {
2176
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2177
		//msg taken care of in trader_att_proc.php
2178
		// forget plotted course
2179
		$this->deletePlottedCourse();
2180
2181
		$sector->diedHere($this);
2182
2183
		// if we are in an alliance we increase their deaths
2184
		if ($this->hasAlliance()) {
2185
			$this->db->query('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2186
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()) . ' LIMIT 1');
2187
		}
2188
2189
		// record death stat
2190
		$this->increaseHOF(1, array('Dying', 'Deaths'), HOF_PUBLIC);
2191
		//record cost of ship lost
2192
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2193
		// reset turns since last death
2194
		$this->setHOF(0, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2195
2196
		// Reset credits to starting amount + ship insurance
2197
		$credits = $this->getGame()->getStartingCredits();
2198
		$credits += IRound($this->getShip()->getCost() * self::SHIP_INSURANCE_FRACTION);
2199
		$this->setCredits($credits);
2200
2201
		$this->setSectorID($this->getHome());
2202
		$this->increaseDeaths(1);
2203
		$this->setLandedOnPlanet(false);
2204
		$this->setDead(true);
2205
		$this->setNewbieWarning(true);
2206
		$this->getShip()->getPod($this->hasNewbieStatus());
2207
		$this->setNewbieTurns(NEWBIE_TURNS_ON_DEATH);
2208
	}
2209
2210
	public function &killPlayerByPlayer(AbstractSmrPlayer $killer) {
2211
		$return = array();
2212
		$msg = $this->getBBLink();
2213
2214
		if ($this->hasCustomShipName()) {
2215
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2216
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2217
		}
2218
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2219
		if ($killer->hasCustomShipName()) {
2220
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2221
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2222
		}
2223
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2224
		$this->getSector()->increaseBattles(1);
2225
		$this->db->query('INSERT INTO news (game_id,time,news_message,type,killer_id,killer_alliance,dead_id,dead_alliance) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber(SmrSession::getTime()) . ',' . $this->db->escapeString($msg) . ',\'regular\',' . $this->db->escapeNumber($killer->getAccountID()) . ',' . $this->db->escapeNumber($killer->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2226
2227
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2228
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getAccountID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2229
2230
		// Dead player loses between 5% and 25% experience
2231
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2232
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2233
		$this->decreaseExperience($return['DeadExp']);
2234
2235
		// Killer gains 50% of the lost exp
2236
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
2237
		$killer->increaseExperience($return['KillerExp']);
2238
2239
		$return['KillerCredits'] = $this->getCredits();
2240
		$killer->increaseCredits($return['KillerCredits']);
2241
2242
		// The killer may change alignment
2243
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2244
		$relation = $relations[$killer->getRaceID()];
2245
2246
		$alignChangePerRelation = 0.1;
2247
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2248
			$alignChangePerRelation = 0.04;
2249
		}
2250
2251
		$return['KillerAlign'] = -$relation * $alignChangePerRelation; //Lose relations when killing a peaceful race
2252
		if ($return['KillerAlign'] > 0) {
2253
			$killer->increaseAlignment($return['KillerAlign']);
2254
		} else {
2255
			$killer->decreaseAlignment(-$return['KillerAlign']);
2256
		}
2257
		// War setting gives them military pay
2258
		if ($relation <= RELATIONS_WAR) {
2259
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2260
		}
2261
2262
		//check for federal bounty being offered for current port raiders;
2263
		$this->db->query('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(SmrSession::getTime() - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2264
		$query = 'SELECT 1
2265
					FROM player_attacks_port
2266
					JOIN port USING(game_id, sector_id)
2267
					JOIN player USING(game_id, account_id)
2268
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2269
		$this->db->query($query);
2270
		if ($this->db->nextRecord()) {
2271
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2272
			$this->increaseCurrentBountyAmount('HQ', $bounty);
2273
		}
2274
2275
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2276
		$this->setBountiesClaimable($killer);
2277
2278
		// If the alignment difference is greater than 200 then a bounty may be set
2279
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2280
		$return['BountyGained'] = array(
2281
			'Type' => 'None',
2282
			'Amount' => 0
2283
		);
2284
		if ($alignmentDiff >= 200) {
2285
			// If the podded players alignment makes them deputy or member then set bounty
2286
			if ($this->getAlignment() >= 100) {
2287
				$return['BountyGained']['Type'] = 'HQ';
2288
			} elseif ($this->getAlignment() <= 100) {
2289
				$return['BountyGained']['Type'] = 'UG';
2290
			}
2291
2292
			if ($return['BountyGained']['Type'] != 'None') {
2293
				$return['BountyGained']['Amount'] = IFloor(pow($alignmentDiff, 2.56));
2294
				$killer->increaseCurrentBountyAmount($return['BountyGained']['Type'], $return['BountyGained']['Amount']);
2295
			}
2296
		}
2297
2298
		if ($this->isNPC()) {
2299
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'NPC', 'Experience', 'Gained'), HOF_PUBLIC);
2300
			$killer->increaseHOF($this->getExperience(), array('Killing', 'NPC', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2301
2302
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By NPCs Killed'), HOF_PUBLIC);
2303
2304
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2305
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Gain'), HOF_PUBLIC);
2306
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'NPC', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2307
2308
			if ($return['KillerAlign'] > 0) {
2309
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Gain'), HOF_PUBLIC);
2310
			} else {
2311
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Loss'), HOF_PUBLIC);
2312
			}
2313
2314
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'NPC', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2315
2316
			$killer->increaseHOF(1, array('Killing', 'NPC Kills'), HOF_PUBLIC);
2317
		} else {
2318
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'Experience', 'Gained'), HOF_PUBLIC);
2319
			$killer->increaseHOF($this->getExperience(), array('Killing', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2320
2321
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By Traders Killed'), HOF_PUBLIC);
2322
2323
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2324
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Gain'), HOF_PUBLIC);
2325
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2326
2327
			if ($return['KillerAlign'] > 0) {
2328
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'Alignment', 'Gain'), HOF_PUBLIC);
2329
			} else {
2330
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'Alignment', 'Loss'), HOF_PUBLIC);
2331
			}
2332
2333
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2334
2335
			if ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2336
				$killer->increaseHOF(1, array('Killing', 'Newbie Kills'), HOF_PUBLIC);
2337
			} else {
2338
				$killer->increaseKills(1);
2339
				$killer->increaseHOF(1, array('Killing', 'Kills'), HOF_PUBLIC);
2340
2341
				if ($killer->hasAlliance()) {
2342
					$this->db->query('UPDATE alliance SET alliance_kills=alliance_kills+1 WHERE alliance_id=' . $this->db->escapeNumber($killer->getAllianceID()) . ' AND game_id=' . $this->db->escapeNumber($killer->getGameID()) . ' LIMIT 1');
2343
				}
2344
2345
				// alliance vs. alliance stats
2346
				$this->incrementAllianceVsDeaths($killer->getAllianceID());
2347
			}
2348
		}
2349
2350
		$this->increaseHOF($return['BountyGained']['Amount'], array('Dying', 'Players', 'Money', 'Bounty Gained By Killer'), HOF_PUBLIC);
2351
		$this->increaseHOF($return['KillerExp'], array('Dying', 'Players', 'Experience', 'Gained By Killer'), HOF_PUBLIC);
2352
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2353
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Players', 'Experience', 'Lost'), HOF_PUBLIC);
2354
		$this->increaseHOF($return['KillerCredits'], array('Dying', 'Players', 'Money Lost'), HOF_PUBLIC);
2355
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Players', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2356
		$this->increaseHOF(1, array('Dying', 'Players', 'Deaths'), HOF_PUBLIC);
2357
2358
		$this->killPlayer($this->getSectorID());
2359
		return $return;
2360
	}
2361
2362
	public function &killPlayerByForces(SmrForce $forces) {
2363
		$return = array();
2364
		$owner = $forces->getOwner();
2365
		// send a message to the person who died
2366
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getAccountID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2367
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2368
2369
		$news_message = $this->getBBLink();
2370
		if ($this->hasCustomShipName()) {
2371
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2372
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2373
		}
2374
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2375
		// insert the news entry
2376
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2377
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(SmrSession::getTime()) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($owner->getAccountID()) . ',' . $this->db->escapeNumber($owner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2378
2379
		// Player loses 15% experience
2380
		$expLossPercentage = .15;
2381
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
2382
		$this->decreaseExperience($return['DeadExp']);
2383
2384
		$return['LostCredits'] = $this->getCredits();
2385
2386
		// alliance vs. alliance stats
2387
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2388
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2389
2390
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2391
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Forces', 'Experience Lost'), HOF_PUBLIC);
2392
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Forces', 'Money Lost'), HOF_PUBLIC);
2393
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Forces', 'Cost Of Ships Lost'), HOF_PUBLIC);
2394
		$this->increaseHOF(1, array('Dying', 'Forces', 'Deaths'), HOF_PUBLIC);
2395
2396
		$this->killPlayer($forces->getSectorID());
2397
		return $return;
2398
	}
2399
2400
	public function &killPlayerByPort(SmrPort $port) {
2401
		$return = array();
2402
		// send a message to the person who died
2403
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2404
2405
		$news_message = $this->getBBLink();
2406
		if ($this->hasCustomShipName()) {
2407
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2408
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2409
		}
2410
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2411
		// insert the news entry
2412
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,dead_id,dead_alliance)
2413
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(SmrSession::getTime()) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber(ACCOUNT_ID_PORT) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2414
2415
		// Player loses between 15% and 20% experience
2416
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2417
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2418
		$this->decreaseExperience($return['DeadExp']);
2419
2420
		$return['LostCredits'] = $this->getCredits();
2421
2422
		// alliance vs. alliance stats
2423
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2424
2425
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2426
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Ports', 'Experience Lost'), HOF_PUBLIC);
2427
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Ports', 'Money Lost'), HOF_PUBLIC);
2428
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Ports', 'Cost Of Ships Lost'), HOF_PUBLIC);
2429
		$this->increaseHOF(1, array('Dying', 'Ports', 'Deaths'), HOF_PUBLIC);
2430
2431
		$this->killPlayer($port->getSectorID());
2432
		return $return;
2433
	}
2434
2435
	public function &killPlayerByPlanet(SmrPlanet $planet) {
2436
		$return = array();
2437
		// send a message to the person who died
2438
		$planetOwner = $planet->getOwner();
2439
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getAccountID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2440
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2441
2442
		$news_message = $this->getBBLink();
2443
		if ($this->hasCustomShipName()) {
2444
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2445
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2446
		}
2447
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2448
		// insert the news entry
2449
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2450
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(SmrSession::getTime()) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($planetOwner->getAccountID()) . ',' . $this->db->escapeNumber($planetOwner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2451
2452
		// Player loses between 15% and 20% experience
2453
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2454
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2455
		$this->decreaseExperience($return['DeadExp']);
2456
2457
		$return['LostCredits'] = $this->getCredits();
2458
2459
		// alliance vs. alliance stats
2460
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2461
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2462
2463
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2464
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Planets', 'Experience Lost'), HOF_PUBLIC);
2465
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Planets', 'Money Lost'), HOF_PUBLIC);
2466
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Planets', 'Cost Of Ships Lost'), HOF_PUBLIC);
2467
		$this->increaseHOF(1, array('Dying', 'Planets', 'Deaths'), HOF_PUBLIC);
2468
2469
		$this->killPlayer($planet->getSectorID());
2470
		return $return;
2471
	}
2472
2473
	public function incrementAllianceVsKills($otherID) {
2474
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2475
		$this->db->query('INSERT INTO alliance_vs_alliance (game_id, alliance_id_1, alliance_id_2, kills) VALUES (' . $this->db->escapeArray($values) . ') ON DUPLICATE KEY UPDATE kills = kills + 1');
2476
	}
2477
2478
	public function incrementAllianceVsDeaths($otherID) {
2479
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2480
		$this->db->query('INSERT INTO alliance_vs_alliance (game_id, alliance_id_1, alliance_id_2, kills) VALUES (' . $this->db->escapeArray($values) . ') ON DUPLICATE KEY UPDATE kills = kills + 1');
2481
	}
2482
2483
	public function getTurnsLevel() {
2484
		if (!$this->hasTurns()) {
2485
			return 'NONE';
2486
		}
2487
		if ($this->getTurns() <= 25) {
2488
			return 'LOW';
2489
		}
2490
		if ($this->getTurns() <= 75) {
2491
			return 'MEDIUM';
2492
		}
2493
		return 'HIGH';
2494
	}
2495
2496
	/**
2497
	 * Returns the CSS class color to use when displaying the player's turns
2498
	 */
2499
	public function getTurnsColor() {
2500
		switch ($this->getTurnsLevel()) {
2501
			case 'NONE':
2502
			case 'LOW':
2503
				return 'red';
2504
			case 'MEDIUM':
2505
				return 'yellow';
2506
			default:
2507
				return 'green';
2508
		}
2509
	}
2510
2511
	public function getTurns() {
2512
		return $this->turns;
2513
	}
2514
2515
	public function hasTurns() {
2516
		return $this->turns > 0;
2517
	}
2518
2519
	public function getMaxTurns() {
2520
		return $this->getGame()->getMaxTurns();
2521
	}
2522
2523
	public function setTurns($turns) {
2524
		if ($this->turns == $turns) {
2525
			return;
2526
		}
2527
		// Make sure turns are in range [0, MaxTurns]
2528
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2529
		$this->hasChanged = true;
2530
	}
2531
2532
	public function takeTurns($take, $takeNewbie = 0) {
2533
		if ($take < 0 || $takeNewbie < 0) {
2534
			throw new Exception('Trying to take negative turns.');
2535
		}
2536
		$take = ICeil($take);
2537
		// Only take up to as many newbie turns as we have remaining
2538
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2539
2540
		$this->setTurns($this->getTurns() - $take);
2541
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2542
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2543
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Total'), HOF_ALLIANCE);
2544
		$this->increaseHOF($takeNewbie, array('Movement', 'Turns Used', 'Newbie'), HOF_ALLIANCE);
2545
2546
		// Player has taken an action
2547
		$this->setLastActive(SmrSession::getTime());
2548
		$this->updateLastCPLAction();
2549
	}
2550
2551
	public function giveTurns(int $give, $giveNewbie = 0) {
2552
		if ($give < 0 || $giveNewbie < 0) {
2553
			throw new Exception('Trying to give negative turns.');
2554
		}
2555
		$this->setTurns($this->getTurns() + $give);
2556
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2557
	}
2558
2559
	/**
2560
	 * Calculate the time in seconds between the given time and when the
2561
	 * player will be at max turns.
2562
	 */
2563
	public function getTimeUntilMaxTurns($time, $forceUpdate = false) {
2564
		$timeDiff = $time - $this->getLastTurnUpdate();
2565
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2566
		$ship = $this->getShip($forceUpdate);
2567
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
2568
		// If already at max turns, return 0
2569
		return max(0, $maxTurnsTime);
2570
	}
2571
2572
	/**
2573
	 * Grant the player their starting turns.
2574
	 */
2575
	public function giveStartingTurns() {
2576
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
2577
		$this->giveTurns($startTurns);
2578
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2579
	}
2580
2581
	// Turns only update when player is active.
2582
	// Calculate turns gained between given time and the last turn update
2583
	public function getTurnsGained($time, $forceUpdate = false) : int {
2584
		$timeDiff = $time - $this->getLastTurnUpdate();
2585
		$ship = $this->getShip($forceUpdate);
2586
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
2587
		return $extraTurns;
2588
	}
2589
2590
	public function updateTurns() {
2591
		// is account validated?
2592
		if (!$this->getAccount()->isValidated()) {
2593
			return;
2594
		}
2595
2596
		// how many turns would he get right now?
2597
		$extraTurns = $this->getTurnsGained(SmrSession::getTime());
2598
2599
		// do we have at least one turn to give?
2600
		if ($extraTurns > 0) {
2601
			// recalc the time to avoid rounding errors
2602
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
2603
			$this->setLastTurnUpdate($newLastTurnUpdate);
2604
			$this->giveTurns($extraTurns);
2605
		}
2606
	}
2607
2608
	public function getLastTurnUpdate() {
2609
		return $this->lastTurnUpdate;
2610
	}
2611
2612
	public function setLastTurnUpdate($time) {
2613
		if ($this->lastTurnUpdate == $time) {
2614
			return;
2615
		}
2616
		$this->lastTurnUpdate = $time;
2617
		$this->hasChanged = true;
2618
	}
2619
2620
	public function getLastActive() {
2621
		return $this->lastActive;
2622
	}
2623
2624
	public function setLastActive($lastActive) {
2625
		if ($this->lastActive == $lastActive) {
2626
			return;
2627
		}
2628
		$this->lastActive = $lastActive;
2629
		$this->hasChanged = true;
2630
	}
2631
2632
	public function getLastCPLAction() {
2633
		return $this->lastCPLAction;
2634
	}
2635
2636
	public function setLastCPLAction($time) {
2637
		if ($this->lastCPLAction == $time) {
2638
			return;
2639
		}
2640
		$this->lastCPLAction = $time;
2641
		$this->hasChanged = true;
2642
	}
2643
2644
	public function updateLastCPLAction() {
2645
		$this->setLastCPLAction(SmrSession::getTime());
2646
	}
2647
2648
	public function setNewbieWarning($bool) {
2649
		if ($this->newbieWarning == $bool) {
2650
			return;
2651
		}
2652
		$this->newbieWarning = $bool;
2653
		$this->hasChanged = true;
2654
	}
2655
2656
	public function getNewbieWarning() {
2657
		return $this->newbieWarning;
2658
	}
2659
2660
	public function isDisplayMissions() {
2661
		return $this->displayMissions;
2662
	}
2663
2664
	public function setDisplayMissions($bool) {
2665
		if ($this->displayMissions == $bool) {
2666
			return;
2667
		}
2668
		$this->displayMissions = $bool;
2669
		$this->hasChanged = true;
2670
	}
2671
2672
	public function getMissions() {
2673
		if (!isset($this->missions)) {
2674
			$this->db->query('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2675
			$this->missions = array();
2676
			while ($this->db->nextRecord()) {
2677
				$missionID = $this->db->getInt('mission_id');
2678
				$this->missions[$missionID] = array(
2679
					'On Step' => $this->db->getInt('on_step'),
2680
					'Progress' => $this->db->getInt('progress'),
2681
					'Unread' => $this->db->getBoolean('unread'),
2682
					'Expires' => $this->db->getInt('step_fails'),
2683
					'Sector' => $this->db->getInt('mission_sector'),
2684
					'Starting Sector' => $this->db->getInt('starting_sector')
2685
				);
2686
				$this->rebuildMission($missionID);
2687
			}
2688
		}
2689
		return $this->missions;
2690
	}
2691
2692
	public function getActiveMissions() {
2693
		$missions = $this->getMissions();
2694
		foreach ($missions as $missionID => $mission) {
2695
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2696
				unset($missions[$missionID]);
2697
			}
2698
		}
2699
		return $missions;
2700
	}
2701
2702
	protected function getMission($missionID) {
2703
		$missions = $this->getMissions();
2704
		if (isset($missions[$missionID])) {
2705
			return $missions[$missionID];
2706
		}
2707
		return false;
2708
	}
2709
2710
	protected function hasMission($missionID) {
2711
		return $this->getMission($missionID) !== false;
2712
	}
2713
2714
	protected function updateMission($missionID) {
2715
		$this->getMissions();
2716
		if (isset($this->missions[$missionID])) {
2717
			$mission = $this->missions[$missionID];
2718
			$this->db->query('
2719
				UPDATE player_has_mission
2720
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2721
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2722
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2723
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2724
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2725
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2726
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1'
2727
			);
2728
			return true;
2729
		}
2730
		return false;
2731
	}
2732
2733
	private function setupMissionStep($missionID) {
2734
		$mission =& $this->missions[$missionID];
2735
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2736
			// Nothing to do if this mission is already completed
2737
			return;
2738
		}
2739
		$step = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2740
		if (isset($step['PickSector'])) {
2741
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2742
			if ($realX === false) {
2743
				throw new Exception('Invalid PickSector definition in mission: ' . $missionID);
2744
			}
2745
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2746
			if ($path === false) {
2747
				// Abandon the mission if it cannot be completed due to a
2748
				// sector that does not exist or cannot be reached.
2749
				// (Probably shouldn't bestow this mission in the first place)
2750
				$this->deleteMission($missionID);
2751
				create_error('Cannot find a path to the destination!');
2752
			}
2753
			$mission['Sector'] = $path->getEndSectorID();
2754
		}
2755
	}
2756
2757
	/**
2758
	 * Declining a mission will permanently hide it from the player
2759
	 * by adding it in its completed state.
2760
	 */
2761
	public function declineMission($missionID) {
2762
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2763
		$this->addMission($missionID, $finishedStep);
2764
	}
2765
2766
	public function addMission($missionID, $step = 0) {
2767
		$this->getMissions();
2768
2769
		if (isset($this->missions[$missionID])) {
2770
			return;
2771
		}
2772
		$sector = 0;
2773
2774
		$mission = array(
2775
			'On Step' => $step,
2776
			'Progress' => 0,
2777
			'Unread' => true,
2778
			'Expires' => (SmrSession::getTime() + 86400),
2779
			'Sector' => $sector,
2780
			'Starting Sector' => $this->getSectorID()
2781
		);
2782
2783
		$this->missions[$missionID] =& $mission;
2784
		$this->setupMissionStep($missionID);
2785
		$this->rebuildMission($missionID);
2786
2787
		$this->db->query('
2788
			REPLACE INTO player_has_mission (game_id,account_id,mission_id,on_step,progress,unread,starting_sector,mission_sector,step_fails)
2789
			VALUES ('.$this->db->escapeNumber($this->gameID) . ',' . $this->db->escapeNumber($this->accountID) . ',' . $this->db->escapeNumber($missionID) . ',' . $this->db->escapeNumber($mission['On Step']) . ',' . $this->db->escapeNumber($mission['Progress']) . ',' . $this->db->escapeBoolean($mission['Unread']) . ',' . $this->db->escapeNumber($mission['Starting Sector']) . ',' . $this->db->escapeNumber($mission['Sector']) . ',' . $this->db->escapeNumber($mission['Expires']) . ')'
2790
		);
2791
	}
2792
2793
	private function rebuildMission($missionID) {
2794
		$mission = $this->missions[$missionID];
2795
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2796
2797
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2798
			// If we have completed this mission just use false to indicate no current task.
2799
			$currentStep = false;
2800
		} else {
2801
			$data = ['player' => $this, 'mission' => $mission];
2802
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2803
			array_walk_recursive($currentStep, 'replaceMissionTemplate', $data);
2804
		}
2805
		$this->missions[$missionID]['Task'] = $currentStep;
2806
	}
2807
2808
	public function deleteMission($missionID) {
2809
		$this->getMissions();
2810
		if (isset($this->missions[$missionID])) {
2811
			unset($this->missions[$missionID]);
2812
			$this->db->query('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1');
2813
			return true;
2814
		}
2815
		return false;
2816
	}
2817
2818
	public function markMissionsRead() {
2819
		$this->getMissions();
2820
		$unreadMissions = array();
2821
		foreach ($this->missions as $missionID => &$mission) {
2822
			if ($mission['Unread']) {
2823
				$unreadMissions[] = $missionID;
2824
				$mission['Unread'] = false;
2825
				$this->updateMission($missionID);
2826
			}
2827
		}
2828
		return $unreadMissions;
2829
	}
2830
2831
	public function claimMissionReward($missionID) {
2832
		$this->getMissions();
2833
		$mission =& $this->missions[$missionID];
2834
		if ($mission === false) {
2835
			throw new Exception('Unknown mission: ' . $missionID);
2836
		}
2837
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2838
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2839
		}
2840
		$mission['On Step']++;
2841
		$mission['Unread'] = true;
2842
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2843
			switch ($rewardItem) {
2844
				case 'Credits':
2845
					$this->increaseCredits($amount);
2846
				break;
2847
				case 'Experience':
2848
					$this->increaseExperience($amount);
2849
				break;
2850
			}
2851
		}
2852
		$rewardText = $mission['Task']['Rewards']['Text'];
2853
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2854
			// If we haven't finished the mission yet then
2855
			$this->setupMissionStep($missionID);
2856
		}
2857
		$this->rebuildMission($missionID);
2858
		$this->updateMission($missionID);
2859
		return $rewardText;
2860
	}
2861
2862
	public function getAvailableMissions() {
2863
		$availableMissions = array();
2864
		foreach (MISSIONS as $missionID => $mission) {
2865
			if ($this->hasMission($missionID)) {
2866
				continue;
2867
			}
2868
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2869
			if ($realX === false) {
2870
				throw new Exception('Invalid HasX definition in mission: ' . $missionID);
2871
			}
2872
			if ($this->getSector()->hasX($realX)) {
2873
				$availableMissions[$missionID] = $mission;
2874
			}
2875
		}
2876
		return $availableMissions;
2877
	}
2878
2879
	/**
2880
	 * Log a player action in the current sector to the admin log console.
2881
	 */
2882
	public function log(int $log_type_id, string $msg) : void {
2883
		$this->getAccount()->log($log_type_id, $msg, $this->getSectorID());
2884
	}
2885
2886
	public function actionTaken($actionID, array $values) {
2887
		if (!in_array($actionID, MISSION_ACTIONS)) {
2888
			throw new Exception('Unknown action: ' . $actionID);
2889
		}
2890
// TODO: Reenable this once tested.		if($this->getAccount()->isLoggingEnabled())
2891
			switch ($actionID) {
2892
				case 'WalkSector':
2893
					$this->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID());
2894
				break;
2895
				case 'JoinAlliance':
2896
					$this->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName());
2897
				break;
2898
				case 'LeaveAlliance':
2899
					$this->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName());
2900
				break;
2901
				case 'DisbandAlliance':
2902
					$this->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName());
2903
				break;
2904
				case 'KickPlayer':
2905
					$this->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName());
2906
				break;
2907
				case 'PlayerKicked':
2908
					$this->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')');
2909
				break;
2910
2911
			}
2912
		$this->getMissions();
2913
		foreach ($this->missions as $missionID => &$mission) {
2914
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
2915
				$requirements = $mission['Task']['Detail'];
2916
				if (checkMissionRequirements($values, $requirements) === true) {
2917
					$mission['On Step']++;
2918
					$mission['Unread'] = true;
2919
					$this->setupMissionStep($missionID);
2920
					$this->rebuildMission($missionID);
2921
					$this->updateMission($missionID);
2922
				}
2923
			}
2924
		}
2925
	}
2926
2927
	public function canSeeAny(array $otherPlayerArray) {
2928
		foreach ($otherPlayerArray as $otherPlayer) {
2929
			if ($this->canSee($otherPlayer)) {
2930
				return true;
2931
			}
2932
		}
2933
		return false;
2934
	}
2935
2936
	public function canSee(AbstractSmrPlayer $otherPlayer) {
2937
		if (!$otherPlayer->getShip()->isCloaked()) {
2938
			return true;
2939
		}
2940
		if ($this->sameAlliance($otherPlayer)) {
2941
			return true;
2942
		}
2943
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
2944
			return true;
2945
		}
2946
		return false;
2947
	}
2948
2949
	public function equals(AbstractSmrPlayer $otherPlayer = null) {
2950
		return $otherPlayer !== null && $this->getAccountID() == $otherPlayer->getAccountID() && $this->getGameID() == $otherPlayer->getGameID();
2951
	}
2952
2953
	public function sameAlliance(AbstractSmrPlayer $otherPlayer = null) {
2954
		return $this->equals($otherPlayer) || (!is_null($otherPlayer) && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
2955
	}
2956
2957
	public function sharedForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2958
		return $this->sameAlliance($otherPlayer);
2959
	}
2960
2961
	public function forceNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2962
		return $this->sameAlliance($otherPlayer);
2963
	}
2964
2965
	public function planetNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2966
		return $this->sameAlliance($otherPlayer);
2967
	}
2968
2969
	public function traderNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2970
		return $this->sameAlliance($otherPlayer);
2971
	}
2972
2973
	public function traderMAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2974
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
2975
	}
2976
2977
	public function traderAttackTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2978
		return $this->sameAlliance($otherPlayer);
2979
	}
2980
2981
	public function traderDefendTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2982
		return $this->sameAlliance($otherPlayer);
2983
	}
2984
2985
	public function traderAttackForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2986
		return $this->sameAlliance($otherPlayer);
2987
	}
2988
2989
	public function traderAttackPortAlliance(AbstractSmrPlayer $otherPlayer = null) {
2990
		return $this->sameAlliance($otherPlayer);
2991
	}
2992
2993
	public function traderAttackPlanetAlliance(AbstractSmrPlayer $otherPlayer = null) {
2994
		return $this->sameAlliance($otherPlayer);
2995
	}
2996
2997
	public function meetsAlignmentRestriction($restriction) {
2998
		if ($restriction < 0) {
2999
			return $this->getAlignment() <= $restriction;
3000
		}
3001
		if ($restriction > 0) {
3002
			return $this->getAlignment() >= $restriction;
3003
		}
3004
		return true;
3005
	}
3006
3007
	// Get an array of goods that are visible to the player
3008
	public function getVisibleGoods() {
3009
		$goods = Globals::getGoods();
3010
		$visibleGoods = array();
3011
		foreach ($goods as $key => $good) {
3012
			if ($this->meetsAlignmentRestriction($good['AlignRestriction'])) {
3013
				$visibleGoods[$key] = $good;
3014
			}
3015
		}
3016
		return $visibleGoods;
3017
	}
3018
3019
	/**
3020
	 * Will retrieve all visited sectors, use only when you are likely to check a large number of these
3021
	 */
3022
	public function hasVisitedSector($sectorID) {
3023
		if (!isset($this->visitedSectors)) {
3024
			$this->visitedSectors = array();
3025
			$this->db->query('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
3026
			while ($this->db->nextRecord()) {
3027
				$this->visitedSectors[$this->db->getInt('sector_id')] = false;
3028
			}
3029
		}
3030
		return !isset($this->visitedSectors[$sectorID]);
3031
	}
3032
3033
	public function getLeaveNewbieProtectionHREF() {
3034
		return SmrSession::getNewHREF(create_container('leave_newbie_processing.php'));
3035
	}
3036
3037
	public function getExamineTraderHREF() {
3038
		$container = create_container('skeleton.php', 'trader_examine.php');
3039
		$container['target'] = $this->getAccountID();
3040
		return SmrSession::getNewHREF($container);
3041
	}
3042
3043
	public function getAttackTraderHREF() {
3044
		return Globals::getAttackTraderHREF($this->getAccountID());
3045
	}
3046
3047
	public function getPlanetKickHREF() {
3048
		$container = create_container('planet_kick_processing.php', 'trader_attack_processing.php');
3049
		$container['account_id'] = $this->getAccountID();
3050
		return SmrSession::getNewHREF($container);
3051
	}
3052
3053
	public function getTraderSearchHREF() {
3054
		$container = create_container('skeleton.php', 'trader_search_result.php');
3055
		$container['player_id'] = $this->getPlayerID();
3056
		return SmrSession::getNewHREF($container);
3057
	}
3058
3059
	public function getAllianceRosterHREF() {
3060
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3061
	}
3062
3063
	public function getToggleWeaponHidingHREF($ajax = false) {
3064
		$container = create_container('toggle_processing.php');
3065
		$container['toggle'] = 'WeaponHiding';
3066
		$container['AJAX'] = $ajax;
3067
		return SmrSession::getNewHREF($container);
3068
	}
3069
3070
	public function isDisplayWeapons() {
3071
		return $this->displayWeapons;
3072
	}
3073
3074
	/**
3075
	 * Should weapons be displayed in the right panel?
3076
	 * This updates the player database directly because it is used with AJAX,
3077
	 * which does not acquire a sector lock.
3078
	 */
3079
	public function setDisplayWeapons($bool) {
3080
		if ($this->displayWeapons == $bool) {
3081
			return;
3082
		}
3083
		$this->displayWeapons = $bool;
3084
		$this->db->query('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3085
	}
3086
3087
	public function update() {
3088
		$this->save();
3089
	}
3090
3091
	public function save() {
3092
		if ($this->hasChanged === true) {
3093
			$this->db->query('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3094
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3095
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3096
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3097
				', turns=' . $this->db->escapeNumber($this->turns) .
3098
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3099
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3100
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3101
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3102
				', dead=' . $this->db->escapeBoolean($this->dead) .
3103
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3104
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3105
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3106
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3107
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3108
				', credits=' . $this->db->escapeNumber($this->credits) .
3109
				', experience=' . $this->db->escapeNumber($this->experience) .
3110
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3111
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3112
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3113
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3114
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3115
				', kills=' . $this->db->escapeNumber($this->kills) .
3116
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3117
				', assists=' . $this->db->escapeNumber($this->assists) .
3118
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3119
				', bank=' . $this->db->escapeNumber($this->bank) .
3120
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3121
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3122
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3123
				', group_scout_messages=' . $this->db->escapeString($this->groupScoutMessages) .
3124
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3125
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3126
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3127
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3128
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3129
				' WHERE ' . $this->SQL . ' LIMIT 1');
3130
			$this->hasChanged = false;
3131
		}
3132
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3133
			if ($bountyChanged === true) {
3134
				$bountyChanged = false;
3135
				$bounty = $this->getBounty($key);
3136
				if ($bounty['New'] === true) {
3137
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3138
						$this->db->query('INSERT INTO bounty (account_id,game_id,type,amount,smr_credits,claimer_id,time) VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeString($bounty['Type']) . ',' . $this->db->escapeNumber($bounty['Amount']) . ',' . $this->db->escapeNumber($bounty['SmrCredits']) . ',' . $this->db->escapeNumber($bounty['Claimer']) . ',' . $this->db->escapeNumber($bounty['Time']) . ')');
3139
					}
3140
				} else {
3141
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3142
						$this->db->query('UPDATE bounty
3143
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3144
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3145
							type=' . $this->db->escapeString($bounty['Type']) . ',
3146
							claimer_id=' . $this->db->escapeNumber($bounty['Claimer']) . ',
3147
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3148
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3149
					} else {
3150
						$this->db->query('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3151
					}
3152
				}
3153
			}
3154
		}
3155
		$this->saveHOF();
3156
	}
3157
3158
	public function saveHOF() {
3159
		if (count($this->hasHOFChanged) > 0) {
3160
			$this->doHOFSave($this->hasHOFChanged);
3161
			$this->hasHOFChanged = [];
3162
		}
3163
		if (!empty(self::$hasHOFVisChanged)) {
3164
			foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3165
				if ($changeType == self::HOF_NEW) {
3166
					$this->db->query('INSERT INTO hof_visibility (type, visibility) VALUES (' . $this->db->escapeString($hofType) . ',' . $this->db->escapeString(self::$HOFVis[$hofType]) . ')');
3167
				} else {
3168
					$this->db->query('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType) . ' LIMIT 1');
3169
				}
3170
				unset(self::$hasHOFVisChanged[$hofType]);
3171
			}
3172
		}
3173
	}
3174
3175
	/**
3176
	 * This should only be called by `saveHOF` (and recursively) to
3177
	 * ensure that the `hasHOFChanged` attribute is properly cleared.
3178
	 */
3179
	protected function doHOFSave(array $hasChangedList, array $typeList = array()) {
3180
		foreach ($hasChangedList as $type => $hofChanged) {
3181
			$tempTypeList = $typeList;
3182
			$tempTypeList[] = $type;
3183
			if (is_array($hofChanged)) {
3184
				$this->doHOFSave($hofChanged, $tempTypeList);
3185
			} else {
3186
				$amount = $this->getHOF($tempTypeList);
3187
				if ($hofChanged == self::HOF_NEW) {
3188
					if ($amount > 0) {
3189
						$this->db->query('INSERT INTO player_hof (account_id,game_id,type,amount) VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeArray($tempTypeList, ':', false) . ',' . $this->db->escapeNumber($amount) . ')');
3190
					}
3191
				} elseif ($hofChanged == self::HOF_CHANGED) {
3192
					$this->db->query('UPDATE player_hof
3193
						SET amount=' . $this->db->escapeNumber($amount) . '
3194
						WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeArray($tempTypeList, ':', false) . ' LIMIT 1');
3195
				}
3196
			}
3197
		}
3198
	}
3199
3200
}
3201