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
Push — master ( 44453e...299ad6 )
by Dan
04:44
created

AbstractSmrPlayer::setUnderAttack()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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