Scrutinizer GitHub App not installed

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

Install GitHub App

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

AbstractSmrPlayer::shootPlayer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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