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 (#912)
by Dan
06:13
created

AbstractSmrPlayer::doMessageSending()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 49
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php declare(strict_types=1);
2
require_once('missions.inc');
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 HOF_CHANGED = 1;
14
	const HOF_NEW = 2;
15
16
	protected static $CACHE_SECTOR_PLAYERS = array();
17
	protected static $CACHE_PLANET_PLAYERS = array();
18
	protected static $CACHE_ALLIANCE_PLAYERS = array();
19
	protected static $CACHE_PLAYERS = array();
20
21
	protected $db;
22
	protected $SQL;
23
24
	protected $accountID;
25
	protected $gameID;
26
	protected $playerName;
27
	protected $playerID;
28
	protected $sectorID;
29
	protected $lastSectorID;
30
	protected $newbieTurns;
31
	protected $dead;
32
	protected $npc;
33
	protected $newbieStatus;
34
	protected $newbieWarning;
35
	protected $landedOnPlanet;
36
	protected $lastActive;
37
	protected $credits;
38
	protected $alignment;
39
	protected $experience;
40
	protected $level;
41
	protected $allianceID;
42
	protected $shipID;
43
	protected $kills;
44
	protected $deaths;
45
	protected $assists;
46
	protected $stats;
47
	protected $personalRelations;
48
	protected $relations;
49
	protected $militaryPayment;
50
	protected $bounties;
51
	protected $turns;
52
	protected $lastCPLAction;
53
	protected $missions;
54
55
	protected $tickers;
56
	protected $lastTurnUpdate;
57
	protected $lastNewsUpdate;
58
	protected $attackColour;
59
	protected $allianceJoinable;
60
	protected $lastPort;
61
	protected $bank;
62
	protected $zoom;
63
	protected $displayMissions;
64
	protected $displayWeapons;
65
	protected $ignoreGlobals;
66
	protected $plottedCourse;
67
	protected $plottedCourseFrom;
68
	protected $nameChanged;
69
	protected bool $raceChanged;
70
	protected $combatDronesKamikazeOnMines;
71
	protected $customShipName;
72
	protected $storedDestinations;
73
74
	protected $visitedSectors;
75
	protected $allianceRoles = array(
76
		0 => 0
77
	);
78
79
	protected $draftLeader;
80
	protected $gpWriter;
81
	protected $HOF;
82
	protected static $HOFVis;
83
84
	protected $hasChanged = false;
85
	protected array $hasHOFChanged = [];
86
	protected static $hasHOFVisChanged = array();
87
	protected $hasBountyChanged = array();
88
89
	public static function refreshCache() {
90
		foreach (self::$CACHE_PLAYERS as $gameID => &$gamePlayers) {
91
			foreach ($gamePlayers as $playerID => &$player) {
92
				$player = self::getPlayer($playerID, $gameID, true);
93
			}
94
		}
95
	}
96
97
	public static function clearCache() {
98
		self::$CACHE_PLAYERS = array();
99
		self::$CACHE_SECTOR_PLAYERS = array();
100
	}
101
102
	public static function savePlayers() {
103
		foreach (self::$CACHE_PLAYERS as $gamePlayers) {
104
			foreach ($gamePlayers as $player) {
105
				$player->save();
106
			}
107
		}
108
	}
109
110
	public static function getSectorPlayersByAlliances($gameID, $sectorID, array $allianceIDs, $forceUpdate = false) {
111
		$players = self::getSectorPlayers($gameID, $sectorID, $forceUpdate); // Don't use & as we do an unset
112
		foreach ($players as $playerID => $player) {
113
			if (!in_array($player->getAllianceID(), $allianceIDs)) {
114
				unset($players[$playerID]);
115
			}
116
		}
117
		return $players;
118
	}
119
120
	/**
121
	 * Returns the same players as getSectorPlayers (e.g. not on planets),
122
	 * but for an entire galaxy rather than a single sector. This is useful
123
	 * for reducing the number of queries in galaxy-wide processing.
124
	 */
125
	public static function getGalaxyPlayers($gameID, $galaxyID, $forceUpdate = false) {
126
		$db = new SmrMySqlDatabase();
127
		$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(TIME - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND galaxy_id = ' . $db->escapeNumber($galaxyID));
128
		$galaxyPlayers = [];
129
		while ($db->nextRecord()) {
130
			$sectorID = $db->getInt('sector_id');
131
			if (!$db->hasField('player_id')) {
132
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = [];
133
			} else {
134
				$playerID = $db->getInt('player_id');
135
				$player = self::getPlayer($playerID, $gameID, $forceUpdate, $db);
136
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$playerID] = $player;
137
				$galaxyPlayers[$sectorID][$playerID] = $player;
138
			}
139
		}
140
		return $galaxyPlayers;
141
	}
142
143
	public static function getSectorPlayers($gameID, $sectorID, $forceUpdate = false) {
144
		if ($forceUpdate || !isset(self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID])) {
145
			$db = new SmrMySqlDatabase();
146
			$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(TIME - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
147
			$players = array();
148
			while ($db->nextRecord()) {
149
				$playerID = $db->getInt('player_id');
150
				$players[$playerID] = self::getPlayer($playerID, $gameID, $forceUpdate, $db);
151
			}
152
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = $players;
153
		}
154
		return self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID];
155
	}
156
157
	public static function getPlanetPlayers($gameID, $sectorID, $forceUpdate = false) {
158
		if ($forceUpdate || !isset(self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID])) {
159
			$db = new SmrMySqlDatabase();
160
			$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');
161
			$players = array();
162
			while ($db->nextRecord()) {
163
				$playerID = $db->getInt('player_id');
164
				$players[$playerID] = self::getPlayer($playerID, $gameID, $forceUpdate, $db);
165
			}
166
			self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID] = $players;
167
		}
168
		return self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID];
169
	}
170
171
	public static function getAlliancePlayers($gameID, $allianceID, $forceUpdate = false) {
172
		if ($forceUpdate || !isset(self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID])) {
173
			$db = new SmrMySqlDatabase();
174
			$db->query('SELECT * FROM player WHERE alliance_id = ' . $db->escapeNumber($allianceID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' ORDER BY experience DESC');
175
			$players = array();
176
			while ($db->nextRecord()) {
177
				$playerID = $db->getInt('player_id');
178
				$players[$playerID] = self::getPlayer($playerID, $gameID, $forceUpdate, $db);
179
			}
180
			self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID] = $players;
181
		}
182
		return self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID];
183
	}
184
185
	public static function getPlayer($playerID, $gameID, $forceUpdate = false, $db = null) {
186
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$playerID])) {
187
			self::$CACHE_PLAYERS[$gameID][$playerID] = new SmrPlayer($playerID, $gameID, $db);
188
		}
189
		return self::$CACHE_PLAYERS[$gameID][$playerID];
190
	}
191
192
	public static function getPlayerByPlayerName($playerName, $gameID, $forceUpdate = false) {
193
		$db = new SmrMySqlDatabase();
194
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
195
		if ($db->nextRecord()) {
196
			return self::getPlayer($db->getInt('player_id'), $gameID, $forceUpdate, $db);
197
		}
198
		throw new PlayerNotFoundException('Player Name not found.');
199
	}
200
201
	protected function __construct($playerID, $gameID, $db = null) {
202
		$this->db = new SmrMySqlDatabase();
203
		$this->SQL = 'player_id = ' . $this->db->escapeNumber($playerID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
204
205
		if (isset($db)) {
206
			$playerExists = true;
207
		} else {
208
			$db = $this->db;
209
			$this->db->query('SELECT * FROM player WHERE ' . $this->SQL . ' LIMIT 1');
210
			$playerExists = $db->nextRecord();
211
		}
212
213
		if ($playerExists) {
214
			$this->playerID = (int)$playerID;
215
			$this->gameID = (int)$gameID;
216
			$this->accountID = $db->getInt('account_id');
217
			$this->playerName = $db->getField('player_name');
218
			$this->sectorID = $db->getInt('sector_id');
219
			$this->lastSectorID = $db->getInt('last_sector_id');
220
			$this->turns = $db->getInt('turns');
221
			$this->lastTurnUpdate = $db->getInt('last_turn_update');
222
			$this->newbieTurns = $db->getInt('newbie_turns');
223
			$this->lastNewsUpdate = $db->getInt('last_news_update');
224
			$this->attackColour = $db->getField('attack_warning');
225
			$this->dead = $db->getBoolean('dead');
226
			$this->npc = $db->getBoolean('npc');
227
			$this->newbieStatus = $db->getBoolean('newbie_status');
228
			$this->landedOnPlanet = $db->getBoolean('land_on_planet');
229
			$this->lastActive = $db->getInt('last_active');
230
			$this->lastCPLAction = $db->getInt('last_cpl_action');
231
			$this->raceID = $db->getInt('race_id');
232
			$this->credits = $db->getInt('credits');
233
			$this->experience = $db->getInt('experience');
234
			$this->alignment = $db->getInt('alignment');
235
			$this->militaryPayment = $db->getInt('military_payment');
236
			$this->allianceID = $db->getInt('alliance_id');
237
			$this->allianceJoinable = $db->getInt('alliance_join');
238
			$this->shipID = $db->getInt('ship_type_id');
239
			$this->kills = $db->getInt('kills');
240
			$this->deaths = $db->getInt('deaths');
241
			$this->assists = $db->getInt('assists');
242
			$this->lastPort = $db->getInt('last_port');
243
			$this->bank = $db->getInt('bank');
244
			$this->zoom = $db->getInt('zoom');
245
			$this->displayMissions = $db->getBoolean('display_missions');
246
			$this->displayWeapons = $db->getBoolean('display_weapons');
247
			$this->forceDropMessages = $db->getBoolean('force_drop_messages');
0 ignored issues
show
Bug Best Practice introduced by
The property forceDropMessages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
248
			$this->groupScoutMessages = $db->getField('group_scout_messages');
0 ignored issues
show
Bug Best Practice introduced by
The property groupScoutMessages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
249
			$this->ignoreGlobals = $db->getBoolean('ignore_globals');
250
			$this->newbieWarning = $db->getBoolean('newbie_warning');
251
			$this->nameChanged = $db->getBoolean('name_changed');
252
			$this->raceChanged = $db->getBoolean('race_changed');
253
			$this->combatDronesKamikazeOnMines = $db->getBoolean('combat_drones_kamikaze_on_mines');
254
		} else {
255
			throw new PlayerNotFoundException('Invalid playerID: ' . $playerID . ' OR gameID:' . $gameID);
256
		}
257
	}
258
259
	/**
260
	 * Insert a new player into the database. Returns the new player object.
261
	 */
262
	public static function createPlayer($accountID, $gameID, $playerName, $raceID, $isNewbie, $npc=false) {
263
		$db = new SmrMySqlDatabase();
264
		$db->lockTable('player');
265
266
		// Player names must be unique within each game
267
		$db->query('SELECT 1 FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
268
		if ($db->nextRecord() > 0) {
269
			$db->unlock();
270
			create_error('The player name already exists.');
271
		}
272
273
		// get last registered player id in that game and increase by one.
274
		$db->query('SELECT MAX(player_id) FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
275
		if ($db->nextRecord()) {
276
			$playerID = $db->getInt('MAX(player_id)') + 1;
277
		} else {
278
			$playerID = 1;
279
		}
280
281
		$startSectorID = 0; // Temporarily put player into non-existent sector
282
		$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)
283
					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) . ')');
284
285
		$db->unlock();
286
287
		$player = SmrPlayer::getPlayer($playerID, $gameID);
288
		$player->setSectorID($player->getHome());
289
		return $player;
290
	}
291
292
	/**
293
	 * Get array of players whose info can be accessed by this player.
294
	 * Skips players who are not in the same alliance as this player.
295
	 */
296
	public function getSharingPlayers($forceUpdate = false) {
297
		$results = array($this);
298
299
		// Only return this player if not in an alliance
300
		if (!$this->hasAlliance()) {
301
			return $results;
302
		}
303
304
		// Get other players who are sharing info for this game.
305
		// NOTE: game_id=0 means that player shares info for all games.
306
		$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()) . ')');
307
		while ($this->db->nextRecord()) {
308
			try {
309
				$otherPlayer = SmrPlayer::getPlayer($this->db->getInt('from_account_id'), //TODO
310
				                                    $this->getGameID(), $forceUpdate);
311
			} catch (PlayerNotFoundException $e) {
312
				// Skip players that have not joined this game
313
				continue;
314
			}
315
316
			// players must be in the same alliance
317
			if ($this->sameAlliance($otherPlayer)) {
318
				$results[] = $otherPlayer;
319
			}
320
		}
321
		return $results;
322
	}
323
324
	public function getSQL() : string {
325
		return $this->SQL;
326
	}
327
328
	public function getZoom() {
329
		return $this->zoom;
330
	}
331
332
	protected function setZoom($zoom) {
333
		// Set the zoom level between [1, 9]
334
		$zoom = max(1, min(9, $zoom));
335
		if ($this->zoom == $zoom) {
336
			return;
337
		}
338
		$this->zoom = $zoom;
339
		$this->hasChanged = true;
340
	}
341
342
	public function increaseZoom($zoom) {
343
		if ($zoom < 0) {
344
			throw new Exception('Trying to increase negative zoom.');
345
		}
346
		$this->setZoom($this->getZoom() + $zoom);
347
	}
348
349
	public function decreaseZoom($zoom) {
350
		if ($zoom < 0) {
351
			throw new Exception('Trying to decrease negative zoom.');
352
		}
353
		$this->setZoom($this->getZoom() - $zoom);
354
	}
355
356
	public function getAttackColour() {
357
		return $this->attackColour;
358
	}
359
360
	public function setAttackColour($colour) {
361
		if ($this->attackColour == $colour) {
362
			return;
363
		}
364
		$this->attackColour = $colour;
365
		$this->hasChanged = true;
366
	}
367
368
	public function isIgnoreGlobals() {
369
		return $this->ignoreGlobals;
370
	}
371
372
	public function setIgnoreGlobals($bool) {
373
		if ($this->ignoreGlobals == $bool) {
374
			return;
375
		}
376
		$this->ignoreGlobals = $bool;
377
		$this->hasChanged = true;
378
	}
379
380
	public function getAccount() {
381
		return SmrAccount::getAccount($this->getAccountID());
382
	}
383
384
	public function getAccountID() {
385
		return $this->accountID;
386
	}
387
388
	public function getGameID() {
389
		return $this->gameID;
390
	}
391
392
	public function getGame() {
393
		return SmrGame::getGame($this->gameID);
394
	}
395
396
	public function getNewbieTurns() {
397
		return $this->newbieTurns;
398
	}
399
400
	public function hasNewbieTurns() {
401
		return $this->getNewbieTurns() > 0;
402
	}
403
	public function setNewbieTurns($newbieTurns) {
404
		if ($this->newbieTurns == $newbieTurns) {
405
			return;
406
		}
407
		$this->newbieTurns = $newbieTurns;
408
		$this->hasChanged = true;
409
	}
410
411
	public function getShip($forceUpdate = false) {
412
		return SmrShip::getShip($this, $forceUpdate);
413
	}
414
415
	public function getShipTypeID() {
416
		return $this->shipID;
417
	}
418
419
	/**
420
	 * Do not call directly. Use SmrShip::setShipTypeID instead.
421
	 */
422
	public function setShipTypeID($shipID) {
423
		if ($this->shipID == $shipID) {
424
			return;
425
		}
426
		$this->shipID = $shipID;
427
		$this->hasChanged = true;
428
	}
429
430
	public function hasCustomShipName() {
431
		return $this->getCustomShipName() !== false;
432
	}
433
434
	public function getCustomShipName() {
435
		if (!isset($this->customShipName)) {
436
			$this->db->query('SELECT * FROM ship_has_name WHERE ' . $this->SQL . ' LIMIT 1');
437
			if ($this->db->nextRecord()) {
438
				$this->customShipName = $this->db->getField('ship_name');
439
			} else {
440
				$this->customShipName = false;
441
			}
442
		}
443
		return $this->customShipName;
444
	}
445
446
	public function setCustomShipName(string $name) {
447
		$this->db->query('REPLACE INTO ship_has_name (game_id, player_id, ship_name)
448
			VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getPlayerID()) . ', ' . $this->db->escapeString($name) . ')');
449
	}
450
451
	/**
452
	 * Get planet owned by this player.
453
	 * Returns false if this player does not own a planet.
454
	 */
455
	public function getPlanet() {
456
		$this->db->query('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_player_id=' . $this->db->escapeNumber($this->getPlayerID()));
457
		if ($this->db->nextRecord()) {
458
			return SmrPlanet::getPlanet($this->getGameID(), $this->db->getInt('sector_id'), false, $this->db);
459
		} else {
460
			return false;
461
		}
462
	}
463
464
	public function getSectorPlanet() {
465
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
466
	}
467
468
	public function getSectorPort() {
469
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
470
	}
471
472
	public function getSectorID() {
473
		return $this->sectorID;
474
	}
475
476
	public function getSector() {
477
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
478
	}
479
480
	public function setSectorID($sectorID) {
481
		if ($this->sectorID == $sectorID) {
482
			return;
483
		}
484
485
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
486
		$port->addCachePort($this->getPlayerID()); //Add port of sector we were just in, to make sure it is left totally up to date.
487
488
		$this->setLastSectorID($this->getSectorID());
489
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
490
		$this->sectorID = $sectorID;
491
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
492
		$this->hasChanged = true;
493
494
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
495
		$port->addCachePort($this->getPlayerID()); //Add the port of sector we are now in.
496
	}
497
498
	public function getLastSectorID() {
499
		return $this->lastSectorID;
500
	}
501
502
	public function setLastSectorID($lastSectorID) {
503
		if ($this->lastSectorID == $lastSectorID) {
504
			return;
505
		}
506
		$this->lastSectorID = $lastSectorID;
507
		$this->hasChanged = true;
508
	}
509
510
	public function getHome() {
511
		// get his home sector
512
		$hq_id = GOVERNMENT + $this->getRaceID();
513
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
514
		if (!empty($raceHqSectors)) {
515
			// If race has multiple HQ's for some reason, use the first one
516
			return key($raceHqSectors);
517
		} else {
518
			return 1;
519
		}
520
	}
521
522
	public function isDead() {
523
		return $this->dead;
524
	}
525
526
	public function isNPC() {
527
		return $this->npc;
528
	}
529
530
	/**
531
	 * Does the player have Newbie status?
532
	 */
533
	public function hasNewbieStatus() {
534
		return $this->newbieStatus;
535
	}
536
537
	/**
538
	 * Update the player's newbie status if it has changed.
539
	 * This function queries the account, so use sparingly.
540
	 */
541
	public function updateNewbieStatus() {
542
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
543
		if ($this->newbieStatus != $accountNewbieStatus) {
544
			$this->newbieStatus = $accountNewbieStatus;
545
			$this->hasChanged = true;
546
		}
547
	}
548
549
	/**
550
	 * Has this player been designated as the alliance flagship?
551
	 */
552
	public function isFlagship() {
553
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipPlayerID() == $this->getPlayerID();
554
	}
555
556
	public function isPresident() {
557
		return Council::getPresidentPlayerID($this->getGameID(), $this->getRaceID()) == $this->getPlayerID();
0 ignored issues
show
Bug introduced by
The method getPresidentPlayerID() does not exist on Council. Did you maybe mean getPresidentID()? ( Ignorable by Annotation )

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

557
		return Council::/** @scrutinizer ignore-call */ getPresidentPlayerID($this->getGameID(), $this->getRaceID()) == $this->getPlayerID();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
558
	}
559
560
	public function isOnCouncil() {
561
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getPlayerID());
562
	}
563
564
	public function isDraftLeader() {
565
		if (!isset($this->draftLeader)) {
566
			$this->draftLeader = false;
567
			$this->db->query('SELECT 1 FROM draft_leaders WHERE ' . $this->SQL . ' LIMIT 1');
568
			if ($this->db->nextRecord()) {
569
				$this->draftLeader = true;
570
			}
571
		}
572
		return $this->draftLeader;
573
	}
574
575
	public function getGPWriter() {
576
		if (!isset($this->gpWriter)) {
577
			$this->gpWriter = false;
578
			$this->db->query('SELECT position FROM galactic_post_writer WHERE ' . $this->SQL);
579
			if ($this->db->nextRecord()) {
580
				$this->gpWriter = $this->db->getField('position');
581
			}
582
		}
583
		return $this->gpWriter;
584
	}
585
586
	public function isGPEditor() {
587
		return $this->getGPWriter() == 'editor';
588
	}
589
590
	public function isForceDropMessages() {
591
		return $this->forceDropMessages;
592
	}
593
594
	public function setForceDropMessages($bool) {
595
		if ($this->forceDropMessages == $bool) {
596
			return;
597
		}
598
		$this->forceDropMessages = $bool;
0 ignored issues
show
Bug Best Practice introduced by
The property forceDropMessages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
599
		$this->hasChanged = true;
600
	}
601
602
	public function getScoutMessageGroupLimit() {
603
		if ($this->groupScoutMessages == 'ALWAYS') {
604
			return 0;
605
		} elseif ($this->groupScoutMessages == 'AUTO') {
606
			return MESSAGES_PER_PAGE;
607
		} elseif ($this->groupScoutMessages == 'NEVER') {
608
			return PHP_INT_MAX;
609
		}
610
	}
611
612
	public function getGroupScoutMessages() {
613
		return $this->groupScoutMessages;
614
	}
615
616
	public function setGroupScoutMessages($setting) {
617
		if ($this->groupScoutMessages == $setting) {
618
			return;
619
		}
620
		$this->groupScoutMessages = $setting;
0 ignored issues
show
Bug Best Practice introduced by
The property groupScoutMessages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
621
		$this->hasChanged = true;
622
	}
623
624
	protected static function doMessageSending($senderPlayerID, $receiverPlayerID, $gameID, $messageTypeID, $message, $expires, $senderDelete = false, $unread = true) {
625
		$message = trim($message);
626
		$db = new SmrMySqlDatabase();
627
		// send him the message
628
		$db->query('INSERT INTO message
629
			(receiver_player_id,game_id,message_type_id,message_text,
630
			sender_player_id,send_time,expire_time,sender_delete) VALUES(' .
631
			$db->escapeNumber($receiverPlayerID) . ',' .
632
			$db->escapeNumber($gameID) . ',' .
633
			$db->escapeNumber($messageTypeID) . ',' .
634
			$db->escapeString($message) . ',' .
635
			$db->escapeNumber($senderPlayerID) . ',' .
636
			$db->escapeNumber(TIME) . ',' .
637
			$db->escapeNumber($expires) . ',' .
638
			$db->escapeBoolean($senderDelete) . ')'
639
		);
640
		// Keep track of the message_id so it can be returned
641
		$insertID = $db->getInsertID();
642
643
		if ($unread === true) {
644
			// give him the message icon
645
			$db->query('REPLACE INTO player_has_unread_messages (game_id, player_id, message_type_id) VALUES
646
						(' . $db->escapeNumber($gameID) . ', ' . $db->escapeNumber($receiverPlayerID) . ', ' . $db->escapeNumber($messageTypeID) . ')');
647
		}
648
649
		switch ($messageTypeID) {
650
			case MSG_PLAYER:
651
				$receiverAccount = SmrPlayer::getPlayer($receiverPlayerID, $gameID)->getAccount();
652
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isLoggedIn()) {
653
					require_once(get_file_loc('message.functions.inc'));
654
					$sender = getMessagePlayer($senderID, $gameID, $messageTypeID);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $senderID does not exist. Did you maybe mean $sender?
Loading history...
655
					if ($sender instanceof SmrPlayer) {
656
						$sender = $sender->getDisplayName();
657
					}
658
					$mail = setupMailer();
659
					$mail->Subject = 'Message Notification';
660
					$mail->setFrom('[email protected]', 'SMR Notifications');
661
					$bbifiedMessage = 'From: ' . $sender . ' Date: ' . date($receiverAccount->getShortDateFormat() . ' ' . $receiverAccount->getShortTimeFormat(), TIME) . "<br/>\r\n<br/>\r\n" . bbifyMessage($message, true);
662
					$mail->msgHTML($bbifiedMessage);
663
					$mail->AltBody = strip_tags($bbifiedMessage);
664
					$mail->addAddress($receiverAccount->getEmail(), $receiverAccount->getHofName());
665
					$mail->send();
666
					$receiverAccount->decreaseMessageNotifications($messageTypeID, 1);
667
					$receiverAccount->update();
668
				}
669
			break;
670
		}
671
672
		return $insertID;
673
	}
674
675
	public function sendMessageToBox($boxTypeID, $message) {
676
		// send him the message
677
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
678
	}
679
680
	public function sendGlobalMessage($message, $canBeIgnored = true) {
681
		if ($canBeIgnored) {
682
			if ($this->getAccount()->isMailBanned()) {
683
				create_error('You are currently banned from sending messages');
684
			}
685
		}
686
		$this->sendMessageToBox(BOX_GLOBALS, $message);
687
688
		// send to all online player
689
		$db = new SmrMySqlDatabase();
690
		$db->query('SELECT player_id
691
					FROM active_session
692
					JOIN player USING (game_id, account_id)
693
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(TIME - SmrSession::TIME_BEFORE_EXPIRY) . '
694
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
695
						AND ignore_globals = \'FALSE\'
696
						AND player_id != ' . $db->escapeNumber($this->getPlayerID()));
697
698
		while ($db->nextRecord()) {
699
			$this->sendMessage($db->getInt('player_id'), MSG_GLOBAL, $message, $canBeIgnored);
700
		}
701
		$this->sendMessage($this->getPlayerID(), MSG_GLOBAL, $message, $canBeIgnored, false);
702
	}
703
704
	public function sendMessage($receiverPlayerID, $messageTypeID, $message, $canBeIgnored = true, $unread = true, $expires = false, $senderDelete = false) {
705
		//get expire time
706
		if ($canBeIgnored) {
707
			if ($this->getAccount()->isMailBanned()) {
708
				create_error('You are currently banned from sending messages');
709
			}
710
			// Don't send messages to players ignoring us
711
			$this->db->query('SELECT 1 FROM message_blacklist WHERE player_id=' . $this->db->escapeNumber($receiverPlayerID) . ' AND blacklisted_player_id=' . $this->db->escapeNumber($this->getPlayerID()) . ' LIMIT 1');
712
			if ($this->db->nextRecord()) {
713
				return;
714
			}
715
		}
716
717
		$message = word_filter($message);
718
719
		// If expires not specified, use default based on message type
720
		if ($expires === false) {
721
			switch ($messageTypeID) {
722
				case MSG_GLOBAL: //We don't send globals to the box here or it gets done loads of times.
723
					$expires = 3600; // 1h
724
				break;
725
				case MSG_PLAYER:
726
					$expires = 86400 * 31;
727
				break;
728
				case MSG_PLANET:
729
					$expires = 86400 * 7;
730
				break;
731
				case MSG_SCOUT:
732
					$expires = 86400 * 3;
733
				break;
734
				case MSG_POLITICAL:
735
					$expires = 86400 * 31;
736
				break;
737
				case MSG_ALLIANCE:
738
					$expires = 86400 * 31;
739
				break;
740
				case MSG_ADMIN:
741
					$expires = 86400 * 365;
742
				break;
743
				case MSG_CASINO:
744
					$expires = 86400 * 31;
745
				break;
746
				default:
747
					$expires = 86400 * 7;
748
			}
749
			$expires += TIME;
750
		}
751
752
		// Do not put scout messages in the sender's sent box
753
		if ($messageTypeID == MSG_SCOUT) {
754
			$senderDelete = true;
755
		}
756
757
		// send him the message and return the message_id
758
		return self::doMessageSending($this->getPlayerID(), $receiverPlayerID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
759
	}
760
761
	public function sendMessageFromOpAnnounce($receiverPlayerID, $message, $expires = false) {
762
		// get expire time if not set
763
		if ($expires === false) {
764
			$expires = TIME + 86400 * 14;
765
		}
766
		self::doMessageSending(PLAYER_ID_OP_ANNOUNCE, $receiverPlayerID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
767
	}
768
769
	public function sendMessageFromAllianceCommand($receiverPlayerID, $message) {
770
		$expires = TIME + 86400 * 365;
771
		self::doMessageSending(PLAYER_ID_ALLIANCE_COMMAND, $receiverPlayerID, $this->getGameID(), MSG_PLAYER, $message, $expires);
772
	}
773
774
	public static function sendMessageFromPlanet($gameID, $receiverPlayerID, $message) {
775
		//get expire time
776
		$expires = TIME + 86400 * 31;
777
		// send him the message
778
		self::doMessageSending(PLAYER_ID_PLANET, $receiverPlayerID, $gameID, MSG_PLANET, $message, $expires);
779
	}
780
781
	public static function sendMessageFromPort($gameID, $receiverPlayerID, $message) {
782
		//get expire time
783
		$expires = TIME + 86400 * 31;
784
		// send him the message
785
		self::doMessageSending(PLAYER_ID_PORT, $receiverPlayerID, $gameID, MSG_PLAYER, $message, $expires);
786
	}
787
788
	public static function sendMessageFromFedClerk($gameID, $receiverPlayerID, $message) {
789
		$expires = TIME + 86400 * 365;
790
		self::doMessageSending(PLAYER_ID_FED_CLERK, $receiverPlayerID, $gameID, MSG_PLAYER, $message, $expires);
791
	}
792
793
	public static function sendMessageFromAdmin($gameID, $receiverPlayerID, $message, $expires = false) {
794
		//get expire time
795
		if ($expires === false) {
796
			$expires = TIME + 86400 * 365;
797
		}
798
		// send him the message
799
		self::doMessageSending(PLAYER_ID_ADMIN, $receiverPlayerID, $gameID, MSG_ADMIN, $message, $expires);
800
	}
801
802
	public static function sendMessageFromAllianceAmbassador($gameID, $receiverPlayerID, $message, $expires = false) {
803
		//get expire time
804
		if ($expires === false) {
805
			$expires = TIME + 86400 * 31;
806
		}
807
		// send him the message
808
		self::doMessageSending(PLAYER_ID_ALLIANCE_AMBASSADOR, $receiverPlayerID, $gameID, MSG_ALLIANCE, $message, $expires);
809
	}
810
811
	public static function sendMessageFromCasino($gameID, $receiverPlayerID, $message, $expires = false) {
812
		//get expire time
813
		if ($expires === false) {
814
			$expires = TIME + 86400 * 7;
815
		}
816
		// send him the message
817
		self::doMessageSending(PLAYER_ID_CASINO, $receiverPlayerID, $gameID, MSG_CASINO, $message, $expires);
818
	}
819
820
	public static function sendMessageFromRace($raceID, $gameID, $receiverPlayerID, $message, $expires = false) {
821
		//get expire time
822
		if ($expires === false) {
823
			$expires = TIME + 86400 * 5;
824
		}
825
		// send him the message
826
		self::doMessageSending(PLAYER_ID_GROUP_RACES + $raceID, $receiverPlayerID, $gameID, MSG_POLITICAL, $message, $expires);
827
	}
828
829
	public function setMessagesRead($messageTypeID) {
830
		$this->db->query('DELETE FROM player_has_unread_messages
831
							WHERE '.$this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
832
	}
833
834
	public function getSafeAttackRating() {
835
		return max(0, min(8, $this->getAlignment() / 150 + 4));
836
	}
837
838
	public function hasFederalProtection() {
839
		$sector = SmrSector::getSector($this->getGameID(), $this->getSectorID());
840
		if (!$sector->offersFederalProtection()) {
841
			return false;
842
		}
843
844
		$ship = $this->getShip();
845
		if ($ship->hasIllegalGoods()) {
846
			return false;
847
		}
848
849
		if ($ship->getAttackRating() <= $this->getSafeAttackRating()) {
850
			foreach ($sector->getFedRaceIDs() as $fedRaceID) {
851
				if ($this->canBeProtectedByRace($fedRaceID)) {
852
					return true;
853
				}
854
			}
855
		}
856
857
		return false;
858
	}
859
860
	public function canBeProtectedByRace($raceID) {
861
		if (!isset($this->canFed)) {
862
			$this->canFed = array();
0 ignored issues
show
Bug Best Practice introduced by
The property canFed does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
863
			$RACES = Globals::getRaces();
864
			foreach ($RACES as $raceID2 => $raceName) {
865
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
866
			}
867
			$this->db->query('SELECT race_id, allowed FROM player_can_fed
868
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(TIME));
869
			while ($this->db->nextRecord()) {
870
				$this->canFed[$this->db->getInt('race_id')] = $this->db->getBoolean('allowed');
871
			}
872
		}
873
		return $this->canFed[$raceID];
874
	}
875
876
	/**
877
	 * Returns a boolean identifying if the player can currently
878
	 * participate in battles.
879
	 */
880
	public function canFight() {
881
		return !($this->hasNewbieTurns() ||
882
		         $this->isDead() ||
883
		         $this->isLandedOnPlanet() ||
884
		         $this->hasFederalProtection());
885
	}
886
887
	public function setDead($bool) {
888
		if ($this->dead == $bool) {
889
			return;
890
		}
891
		$this->dead = $bool;
892
		$this->hasChanged = true;
893
	}
894
895
	public function getKills() {
896
		return $this->kills;
897
	}
898
899
	public function increaseKills($kills) {
900
		if ($kills < 0) {
901
			throw new Exception('Trying to increase negative kills.');
902
		}
903
		$this->setKills($this->kills + $kills);
904
	}
905
906
	public function setKills($kills) {
907
		if ($this->kills == $kills) {
908
			return;
909
		}
910
		$this->kills = $kills;
911
		$this->hasChanged = true;
912
	}
913
914
	public function getDeaths() {
915
		return $this->deaths;
916
	}
917
918
	public function increaseDeaths($deaths) {
919
		if ($deaths < 0) {
920
			throw new Exception('Trying to increase negative deaths.');
921
		}
922
		$this->setDeaths($this->getDeaths() + $deaths);
923
	}
924
925
	public function setDeaths($deaths) {
926
		if ($this->deaths == $deaths) {
927
			return;
928
		}
929
		$this->deaths = $deaths;
930
		$this->hasChanged = true;
931
	}
932
933
	public function getAssists() {
934
		return $this->assists;
935
	}
936
937
	public function increaseAssists($assists) {
938
		if ($assists < 1) {
939
			throw new Exception('Must increase by a positive number.');
940
		}
941
		$this->assists += $assists;
942
		$this->hasChanged = true;
943
	}
944
945
	public function getAlignment() {
946
		return $this->alignment;
947
	}
948
949
	public function increaseAlignment($align) {
950
		if ($align < 0) {
951
			throw new Exception('Trying to increase negative align.');
952
		}
953
		if ($align == 0) {
954
			return;
955
		}
956
		$align += $this->alignment;
957
		$this->setAlignment($align);
958
	}
959
960
	public function decreaseAlignment($align) {
961
		if ($align < 0) {
962
			throw new Exception('Trying to decrease negative align.');
963
		}
964
		if ($align == 0) {
965
			return;
966
		}
967
		$align = $this->alignment - $align;
968
		$this->setAlignment($align);
969
	}
970
971
	public function setAlignment($align) {
972
		if ($this->alignment == $align) {
973
			return;
974
		}
975
		$this->alignment = $align;
976
		$this->hasChanged = true;
977
	}
978
979
	public function getCredits() {
980
		return $this->credits;
981
	}
982
983
	public function getBank() {
984
		return $this->bank;
985
	}
986
987
	/**
988
	 * Increases personal bank account up to the maximum allowed credits.
989
	 * Returns the amount that was actually added to handle overflow.
990
	 */
991
	public function increaseBank(int $credits) : int {
992
		if ($credits == 0) {
993
			return 0;
994
		}
995
		if ($credits < 0) {
996
			throw new Exception('Trying to increase negative credits.');
997
		}
998
		$newTotal = min($this->bank + $credits, MAX_MONEY);
999
		$actualAdded = $newTotal - $this->bank;
1000
		$this->setBank($newTotal);
1001
		return $actualAdded;
1002
	}
1003
1004
	public function decreaseBank(int $credits) : void {
1005
		if ($credits == 0) {
1006
			return;
1007
		}
1008
		if ($credits < 0) {
1009
			throw new Exception('Trying to decrease negative credits.');
1010
		}
1011
		$newTotal = $this->bank - $credits;
1012
		$this->setBank($newTotal);
1013
	}
1014
1015
	public function setBank(int $credits) : void {
1016
		if ($this->bank == $credits) {
1017
			return;
1018
		}
1019
		if ($credits < 0) {
1020
			throw new Exception('Trying to set negative credits.');
1021
		}
1022
		if ($credits > MAX_MONEY) {
1023
			throw new Exception('Trying to set more than max credits.');
1024
		}
1025
		$this->bank = $credits;
1026
		$this->hasChanged = true;
1027
	}
1028
1029
	public function getExperience() {
1030
		return $this->experience;
1031
	}
1032
1033
	/**
1034
	 * Returns the percent progress towards the next level.
1035
	 * This value is rounded because it is used primarily in HTML img widths.
1036
	 */
1037
	public function getNextLevelPercentAcquired() : int {
1038
		if ($this->getNextLevelExperience() == $this->getThisLevelExperience()) {
1039
			return 100;
1040
		}
1041
		return max(0, min(100, IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
1042
	}
1043
1044
	public function getNextLevelPercentRemaining() {
1045
		return 100 - $this->getNextLevelPercentAcquired();
1046
	}
1047
1048
	public function getNextLevelExperience() {
1049
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1050
		if (!isset($LEVELS_REQUIREMENTS[$this->getLevelID() + 1])) {
1051
			return $this->getThisLevelExperience(); //Return current level experience if on last level.
1052
		}
1053
		return $LEVELS_REQUIREMENTS[$this->getLevelID() + 1]['Requirement'];
1054
	}
1055
1056
	public function getThisLevelExperience() {
1057
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1058
		return $LEVELS_REQUIREMENTS[$this->getLevelID()]['Requirement'];
1059
	}
1060
1061
	public function setExperience($experience) {
1062
		if ($this->experience == $experience) {
1063
			return;
1064
		}
1065
		if ($experience < MIN_EXPERIENCE) {
1066
			$experience = MIN_EXPERIENCE;
1067
		}
1068
		if ($experience > MAX_EXPERIENCE) {
1069
			$experience = MAX_EXPERIENCE;
1070
		}
1071
		$this->experience = $experience;
1072
		$this->hasChanged = true;
1073
1074
		// Since exp has changed, invalidate the player level so that it can
1075
		// be recomputed next time it is queried (in case it has changed).
1076
		$this->level = null;
1077
	}
1078
1079
	/**
1080
	 * Increases onboard credits up to the maximum allowed credits.
1081
	 * Returns the amount that was actually added to handle overflow.
1082
	 */
1083
	public function increaseCredits(int $credits) : int {
1084
		if ($credits == 0) {
1085
			return 0;
1086
		}
1087
		if ($credits < 0) {
1088
			throw new Exception('Trying to increase negative credits.');
1089
		}
1090
		$newTotal = min($this->credits + $credits, MAX_MONEY);
1091
		$actualAdded = $newTotal - $this->credits;
1092
		$this->setCredits($newTotal);
1093
		return $actualAdded;
1094
	}
1095
1096
	public function decreaseCredits(int $credits) : void {
1097
		if ($credits == 0) {
1098
			return;
1099
		}
1100
		if ($credits < 0) {
1101
			throw new Exception('Trying to decrease negative credits.');
1102
		}
1103
		$newTotal = $this->credits - $credits;
1104
		$this->setCredits($newTotal);
1105
	}
1106
1107
	public function setCredits(int $credits) : void {
1108
		if ($this->credits == $credits) {
1109
			return;
1110
		}
1111
		if ($credits < 0) {
1112
			throw new Exception('Trying to set negative credits.');
1113
		}
1114
		if ($credits > MAX_MONEY) {
1115
			throw new Exception('Trying to set more than max credits.');
1116
		}
1117
		$this->credits = $credits;
1118
		$this->hasChanged = true;
1119
	}
1120
1121
	public function increaseExperience($experience) {
1122
		if ($experience < 0) {
1123
			throw new Exception('Trying to increase negative experience.');
1124
		}
1125
		if ($experience == 0) {
1126
			return;
1127
		}
1128
		$newExperience = $this->experience + $experience;
1129
		$this->setExperience($newExperience);
1130
		$this->increaseHOF($experience, array('Experience', 'Total', 'Gain'), HOF_PUBLIC);
1131
	}
1132
	public function decreaseExperience($experience) {
1133
		if ($experience < 0) {
1134
			throw new Exception('Trying to decrease negative experience.');
1135
		}
1136
		if ($experience == 0) {
1137
			return;
1138
		}
1139
		$newExperience = $this->experience - $experience;
1140
		$this->setExperience($newExperience);
1141
		$this->increaseHOF($experience, array('Experience', 'Total', 'Loss'), HOF_PUBLIC);
1142
	}
1143
1144
	public function isLandedOnPlanet() {
1145
		return $this->landedOnPlanet;
1146
	}
1147
1148
	public function setLandedOnPlanet($bool) {
1149
		if ($this->landedOnPlanet == $bool) {
1150
			return;
1151
		}
1152
		$this->landedOnPlanet = $bool;
1153
		$this->hasChanged = true;
1154
	}
1155
1156
	/**
1157
	 * Returns the numerical level of the player (e.g. 1-50).
1158
	 */
1159
	public function getLevelID() {
1160
		// The level is cached for performance reasons unless `setExperience`
1161
		// is called and the player's experience changes.
1162
		if ($this->level === null) {
1163
			$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1164
			foreach ($LEVELS_REQUIREMENTS as $level_id => $require) {
1165
				if ($this->getExperience() >= $require['Requirement']) {
1166
					continue;
1167
				}
1168
				$this->level = $level_id - 1;
1169
				return $this->level;
1170
			}
1171
			$this->level = max(array_keys($LEVELS_REQUIREMENTS));
1172
		}
1173
		return $this->level;
1174
	}
1175
1176
	public function getLevelName() {
1177
		$level_name = Globals::getLevelRequirements()[$this->getLevelID()]['Name'];
1178
		if ($this->isPresident()) {
1179
			$level_name = '<img src="images/council_president.png" title="' . Globals::getRaceName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1180
		}
1181
		return $level_name;
1182
	}
1183
1184
	public function getMaxLevel() {
1185
		return max(array_keys(Globals::getLevelRequirements()));
1186
	}
1187
1188
	public function getPlayerID() {
1189
		return $this->playerID;
1190
	}
1191
1192
	/**
1193
	 * Returns the player name.
1194
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1195
	 */
1196
	public function getPlayerName() {
1197
		return $this->playerName;
1198
	}
1199
1200
	public function setPlayerName($name) {
1201
		$this->playerName = $name;
1202
		$this->hasChanged = true;
1203
	}
1204
1205
	/**
1206
	 * Returns the decorated player name, suitable for HTML display.
1207
	 */
1208
	public function getDisplayName($includeAlliance = false) {
1209
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1210
		$return = get_colored_text($this->getAlignment(), $name);
1211
		if ($this->isNPC()) {
1212
			$return .= ' <span class="npcColour">[NPC]</span>';
1213
		}
1214
		if ($includeAlliance) {
1215
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1216
		}
1217
		return $return;
1218
	}
1219
1220
	public function getBBLink() {
1221
			return '[player=' . $this->getPlayerID() . ']';
1222
	}
1223
1224
	public function getLinkedDisplayName($includeAlliance = true) {
1225
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1226
		if ($includeAlliance) {
1227
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1228
		}
1229
		return $return;
1230
	}
1231
1232
	/**
1233
	 * Use this method when the player is changing their own name.
1234
	 * This will flag the player as having used their free name change.
1235
	 */
1236
	public function setPlayerNameByPlayer($playerName) {
1237
		$this->setPlayerName($playerName);
1238
		$this->setNameChanged(true);
1239
	}
1240
1241
	public function isNameChanged() {
1242
		return $this->nameChanged;
1243
	}
1244
1245
	public function setNameChanged($bool) {
1246
		$this->nameChanged = $bool;
1247
		$this->hasChanged = true;
1248
	}
1249
1250
	public function isRaceChanged() : bool {
1251
		return $this->raceChanged;
1252
	}
1253
1254
	public function setRaceChanged(bool $raceChanged) : void {
1255
		$this->raceChanged = $raceChanged;
1256
		$this->hasChanged = true;
1257
	}
1258
1259
	public function canChangeRace() : bool {
1260
		return !$this->isRaceChanged() && (TIME - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1261
	}
1262
1263
	public static function getColouredRaceNameOrDefault($otherRaceID, AbstractSmrPlayer $player = null, $linked = false) {
1264
		$relations = 0;
1265
		if ($player !== null) {
1266
			$relations = $player->getRelation($otherRaceID);
1267
		}
1268
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1269
	}
1270
1271
	public function getColouredRaceName($otherRaceID, $linked = false) {
1272
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1273
	}
1274
1275
	public function setRaceID($raceID) {
1276
		if ($this->raceID == $raceID) {
1277
			return;
1278
		}
1279
		$this->raceID = $raceID;
1280
		$this->hasChanged = true;
1281
	}
1282
1283
	public function isAllianceLeader($forceUpdate = false) {
1284
		return $this->getPlayerID() == $this->getAlliance($forceUpdate)->getLeaderPlayerID();
1285
	}
1286
1287
	public function getAlliance($forceUpdate = false) {
1288
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1289
	}
1290
1291
	public function getAllianceID() {
1292
		return $this->allianceID;
1293
	}
1294
1295
	public function hasAlliance() {
1296
		return $this->getAllianceID() != 0;
1297
	}
1298
1299
	protected function setAllianceID($ID) {
1300
		if ($this->allianceID == $ID) {
1301
			return;
1302
		}
1303
		$this->allianceID = $ID;
1304
		if ($this->allianceID != 0) {
1305
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1306
			$this->db->query('INSERT IGNORE INTO player_joined_alliance (player_id,game_id,alliance_id,status) ' .
1307
				'VALUES (' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1308
		}
1309
		$this->hasChanged = true;
1310
	}
1311
1312
	public function getAllianceBBLink() {
1313
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1314
	}
1315
1316
	public function getAllianceDisplayName($linked = false, $includeAllianceID = false) {
1317
		if ($this->hasAlliance()) {
1318
			return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1319
		} else {
1320
			return 'No Alliance';
1321
		}
1322
	}
1323
1324
	public function getAllianceRole($allianceID = false) {
1325
		if ($allianceID === false) {
1326
			$allianceID = $this->getAllianceID();
1327
		}
1328
		if (!isset($this->allianceRoles[$allianceID])) {
1329
			$this->allianceRoles[$allianceID] = 0;
1330
			$this->db->query('SELECT role_id
1331
						FROM player_has_alliance_role
1332
						WHERE ' . $this->SQL . '
1333
						AND alliance_id=' . $this->db->escapeNumber($allianceID) . '
1334
						LIMIT 1');
1335
			if ($this->db->nextRecord()) {
1336
				$this->allianceRoles[$allianceID] = $this->db->getInt('role_id');
1337
			}
1338
		}
1339
		return $this->allianceRoles[$allianceID];
1340
	}
1341
1342
	public function leaveAlliance(AbstractSmrPlayer $kickedBy = null) {
1343
		$allianceID = $this->getAllianceID();
1344
		$alliance = $this->getAlliance();
1345
		if ($kickedBy != null) {
1346
			$kickedBy->sendMessage($this->getPlayerID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1347
			$this->actionTaken('PlayerKicked', array('Alliance' => $alliance, 'Player' => $kickedBy));
1348
			$kickedBy->actionTaken('KickPlayer', array('Alliance' => $alliance, 'Player' => $this));
1349
		} elseif ($this->isAllianceLeader()) {
1350
			$this->actionTaken('DisbandAlliance', array('Alliance' => $alliance));
1351
		} else {
1352
			$this->actionTaken('LeaveAlliance', array('Alliance' => $alliance));
1353
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderPlayerID() != PLAYER_ID_NHL) {
1354
				$this->sendMessage($alliance->getLeaderPlayerID(), MSG_PLAYER, 'I left your alliance!', false);
1355
			}
1356
		}
1357
1358
		$this->setAllianceID(0);
1359
		$this->db->query('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1360
1361
		if (!$this->isAllianceLeader() && $allianceID != NHA_ID) { // Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1362
			$this->setAllianceJoinable(TIME + self::TIME_FOR_ALLIANCE_SWITCH);
1363
			$alliance->getLeader()->setAllianceJoinable(TIME + 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.
1364
		}
1365
	}
1366
1367
	/**
1368
	 * Join an alliance (used for both Leader and New Member roles)
1369
	 */
1370
	public function joinAlliance($allianceID) {
1371
		$this->setAllianceID($allianceID);
1372
		$alliance = $this->getAlliance();
1373
1374
		if (!$this->isAllianceLeader()) {
1375
			// Do not throw an exception if the NHL player doesn't exist.
1376
			try {
1377
				$this->sendMessage($alliance->getLeaderPlayerID(), MSG_PLAYER, 'I joined your alliance!', false);
1378
			} catch (PlayerNotFoundException $e) {
1379
				if ($alliance->getLeaderPlayerID() != PLAYER_ID_NHL) {
1380
					throw $e;
1381
				}
1382
			}
1383
1384
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1385
		} else {
1386
			$roleID = ALLIANCE_ROLE_LEADER;
1387
		}
1388
		$this->db->query('INSERT INTO player_has_alliance_role (game_id, player_id, role_id, alliance_id) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getPlayerID()) . ', ' . $this->db->escapeNumber($roleID) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
1389
1390
		$this->actionTaken('JoinAlliance', array('Alliance' => $alliance));
1391
	}
1392
1393
	public function getAllianceJoinable() {
1394
		return $this->allianceJoinable;
1395
	}
1396
1397
	private function setAllianceJoinable($time) {
1398
		if ($this->allianceJoinable == $time) {
1399
			return;
1400
		}
1401
		$this->allianceJoinable = $time;
1402
		$this->hasChanged = true;
1403
	}
1404
1405
	/**
1406
	 * Invites player with $playerID to this player's alliance.
1407
	 */
1408
	public function sendAllianceInvitation(int $invitedPlayerID, string $message, int $expires) : void {
1409
		if (!$this->hasAlliance()) {
1410
			throw new Exception('Must be in an alliance to send alliance invitations');
1411
		}
1412
		// Send message to invited player
1413
		$messageID = $this->sendMessage($invitedPlayerID, MSG_PLAYER, $message, false, true, $expires, true);
1414
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $invitedPlayerID, $this->getPlayerID(), $messageID, $expires);
1415
	}
1416
1417
	public function isCombatDronesKamikazeOnMines() {
1418
		return $this->combatDronesKamikazeOnMines;
1419
	}
1420
1421
	public function setCombatDronesKamikazeOnMines($bool) {
1422
		if ($this->combatDronesKamikazeOnMines == $bool) {
1423
			return;
1424
		}
1425
		$this->combatDronesKamikazeOnMines = $bool;
1426
		$this->hasChanged = true;
1427
	}
1428
1429
	protected function getPersonalRelationsData() {
1430
		if (!isset($this->personalRelations)) {
1431
			//get relations
1432
			$RACES = Globals::getRaces();
1433
			$this->personalRelations = array();
1434
			foreach ($RACES as $raceID => $raceName) {
1435
				$this->personalRelations[$raceID] = 0;
1436
			}
1437
			$this->db->query('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL . ' LIMIT ' . count($RACES));
1438
			while ($this->db->nextRecord()) {
1439
				$this->personalRelations[$this->db->getInt('race_id')] = $this->db->getInt('relation');
1440
			}
1441
		}
1442
	}
1443
1444
	public function getPersonalRelations() {
1445
		$this->getPersonalRelationsData();
1446
		return $this->personalRelations;
1447
	}
1448
1449
	/**
1450
	 * Get personal relations with a race
1451
	 */
1452
	public function getPersonalRelation($raceID) {
1453
		$rels = $this->getPersonalRelations();
1454
		return $rels[$raceID];
1455
	}
1456
1457
	/**
1458
	 * Get total relations with all races (personal + political)
1459
	 */
1460
	public function getRelations() {
1461
		if (!isset($this->relations)) {
1462
			//get relations
1463
			$RACES = Globals::getRaces();
1464
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1465
			$personalRels = $this->getPersonalRelations(); // make sure they're initialised.
1466
			$this->relations = array();
1467
			foreach ($RACES as $raceID => $raceName) {
1468
				$this->relations[$raceID] = $personalRels[$raceID] + $raceRelations[$raceID];
1469
			}
1470
		}
1471
		return $this->relations;
1472
	}
1473
1474
	/**
1475
	 * Get total relations with a race (personal + political)
1476
	 */
1477
	public function getRelation($raceID) {
1478
		$rels = $this->getRelations();
1479
		return $rels[$raceID];
1480
	}
1481
1482
	/**
1483
	 * Increases personal relations from trading $numGoods units with the race
1484
	 * of the port given by $raceID.
1485
	 */
1486
	public function increaseRelationsByTrade($numGoods, $raceID) {
1487
		$relations = ICeil(min($numGoods, 300) / 30);
1488
		//Cap relations to a max of 1 after 500 have been reached
1489
		if ($this->getPersonalRelation($raceID) + $relations >= 500) {
1490
			$relations = max(1, min($relations, 500 - $this->getPersonalRelation($raceID)));
1491
		}
1492
		$this->increaseRelations($relations, $raceID);
1493
	}
1494
1495
	/**
1496
	 * Decreases personal relations from trading failures, e.g. rejected
1497
	 * bargaining and getting caught stealing.
1498
	 */
1499
	public function decreaseRelationsByTrade($numGoods, $raceID) {
1500
		$relations = ICeil(min($numGoods, 300) / 30);
1501
		$this->decreaseRelations($relations, $raceID);
1502
	}
1503
1504
	/**
1505
	 * Increase personal relations.
1506
	 */
1507
	public function increaseRelations($relations, $raceID) {
1508
		if ($relations < 0) {
1509
			throw new Exception('Trying to increase negative relations.');
1510
		}
1511
		if ($relations == 0) {
1512
			return;
1513
		}
1514
		$relations += $this->getPersonalRelation($raceID);
1515
		$this->setRelations($relations, $raceID);
1516
	}
1517
1518
	/**
1519
	 * Decrease personal relations.
1520
	 */
1521
	public function decreaseRelations($relations, $raceID) {
1522
		if ($relations < 0) {
1523
			throw new Exception('Trying to decrease negative relations.');
1524
		}
1525
		if ($relations == 0) {
1526
			return;
1527
		}
1528
		$relations = $this->getPersonalRelation($raceID) - $relations;
1529
		$this->setRelations($relations, $raceID);
1530
	}
1531
1532
	/**
1533
	 * Set personal relations.
1534
	 */
1535
	public function setRelations($relations, $raceID) {
1536
		$this->getRelations();
1537
		if ($this->personalRelations[$raceID] == $relations) {
1538
			return;
1539
		}
1540
		if ($relations < MIN_RELATIONS) {
1541
			$relations = MIN_RELATIONS;
1542
		}
1543
		$relationsDiff = IRound($relations - $this->personalRelations[$raceID]);
1544
		$this->personalRelations[$raceID] = $relations;
1545
		$this->relations[$raceID] += $relationsDiff;
1546
		$this->db->query('REPLACE INTO player_has_relation (player_id,game_id,race_id,relation) values (' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($raceID) . ',' . $this->db->escapeNumber($this->personalRelations[$raceID]) . ')');
1547
	}
1548
1549
	/**
1550
	 * Set any starting personal relations bonuses or penalties.
1551
	 */
1552
	public function giveStartingRelations() {
1553
		if ($this->getRaceID() === RACE_ALSKANT) {
1554
			// Give Alskants 250 personal relations to start.
1555
			foreach (Globals::getRaces() as $raceID => $raceInfo) {
1556
				$this->setRelations(250, $raceID);
1557
			}
1558
		}
1559
	}
1560
1561
	public function getLastNewsUpdate() {
1562
		return $this->lastNewsUpdate;
1563
	}
1564
1565
	private function setLastNewsUpdate($time) {
1566
		if ($this->lastNewsUpdate == $time) {
1567
			return;
1568
		}
1569
		$this->lastNewsUpdate = $time;
1570
		$this->hasChanged = true;
1571
	}
1572
1573
	public function updateLastNewsUpdate() {
1574
		$this->setLastNewsUpdate(TIME);
1575
	}
1576
1577
	public function getLastPort() {
1578
		return $this->lastPort;
1579
	}
1580
1581
	public function setLastPort($lastPort) {
1582
		if ($this->lastPort == $lastPort) {
1583
			return;
1584
		}
1585
		$this->lastPort = $lastPort;
1586
		$this->hasChanged = true;
1587
	}
1588
1589
	public function getPlottedCourse() {
1590
		if (!isset($this->plottedCourse)) {
1591
			// check if we have a course plotted
1592
			$this->db->query('SELECT course FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1593
1594
			if ($this->db->nextRecord()) {
1595
				// get the course back
1596
				$this->plottedCourse = unserialize($this->db->getField('course'));
1597
			} else {
1598
				$this->plottedCourse = false;
1599
			}
1600
		}
1601
1602
		// Update the plotted course if we have moved since the last query
1603
		if ($this->plottedCourse !== false && (!isset($this->plottedCourseFrom) || $this->plottedCourseFrom != $this->getSectorID())) {
1604
			$this->plottedCourseFrom = $this->getSectorID();
1605
1606
			if ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1607
				// We have walked into the next sector of the course
1608
				$this->plottedCourse->followPath();
1609
				$this->setPlottedCourse($this->plottedCourse);
1610
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1611
				// We have skipped to some later sector in the course
1612
				$this->plottedCourse->skipToSector($this->getSectorID());
1613
				$this->setPlottedCourse($this->plottedCourse);
1614
			}
1615
		}
1616
		return $this->plottedCourse;
1617
	}
1618
1619
	public function setPlottedCourse(Distance $plottedCourse) {
1620
		$hadPlottedCourse = $this->hasPlottedCourse();
1621
		$this->plottedCourse = $plottedCourse;
1622
		if ($this->plottedCourse->getTotalSectors() > 0) {
1623
			$this->db->query('REPLACE INTO player_plotted_course
1624
				(player_id, game_id, course)
1625
				VALUES(' . $this->db->escapeNumber($this->getPlayerID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeBinary(serialize($this->plottedCourse)) . ')');
1626
		} elseif ($hadPlottedCourse) {
1627
			$this->deletePlottedCourse();
1628
		}
1629
	}
1630
1631
	public function hasPlottedCourse() {
1632
		return $this->getPlottedCourse() !== false;
1633
	}
1634
1635
	public function isPartOfCourse($sectorOrSectorID) {
1636
		if (!$this->hasPlottedCourse()) {
1637
			return false;
1638
		}
1639
		if ($sectorOrSectorID instanceof SmrSector) {
1640
			$sectorID = $sectorOrSectorID->getSectorID();
1641
		} else {
1642
			$sectorID = $sectorOrSectorID;
1643
		}
1644
		return $this->getPlottedCourse()->isInPath($sectorID);
1645
	}
1646
1647
	public function deletePlottedCourse() {
1648
		$this->plottedCourse = false;
1649
		$this->db->query('DELETE FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1650
	}
1651
1652
	// Computes the turn cost and max misjump between current and target sector
1653
	public function getJumpInfo(SmrSector $targetSector) {
1654
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1655
		if ($path === false) {
1656
			create_error('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1657
		}
1658
		$distance = $path->getRelativeDistance();
1659
1660
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
1661
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1662
		return array('turn_cost' => $turnCost, 'max_misjump' => $maxMisjump);
1663
	}
1664
1665
	public function __sleep() {
1666
		return array('accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName');
1667
	}
1668
1669
	public function &getStoredDestinations() {
1670
		if (!isset($this->storedDestinations)) {
1671
			$this->storedDestinations = array();
1672
			$this->db->query('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1673
			while ($this->db->nextRecord()) {
1674
				$this->storedDestinations[] = array(
1675
					'Label' => $this->db->getField('label'),
1676
					'SectorID' => $this->db->getInt('sector_id'),
1677
					'OffsetTop' => $this->db->getInt('offset_top'),
1678
					'OffsetLeft' => $this->db->getInt('offset_left')
1679
				);
1680
			}
1681
		}
1682
		return $this->storedDestinations;
1683
	}
1684
1685
	public function moveDestinationButton($sectorID, $offsetTop, $offsetLeft) {
1686
1687
		if (!is_numeric($offsetLeft) || !is_numeric($offsetTop)) {
1688
			create_error('The position of the saved sector must be numeric!.');
1689
		}
1690
		$offsetTop = round($offsetTop);
1691
		$offsetLeft = round($offsetLeft);
1692
1693
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1694
			create_error('The saved sector must be in the box!');
1695
		}
1696
1697
		$storedDestinations =& $this->getStoredDestinations();
1698
		foreach ($storedDestinations as &$sd) {
1699
			if ($sd['SectorID'] == $sectorID) {
1700
				$sd['OffsetTop'] = $offsetTop;
1701
				$sd['OffsetLeft'] = $offsetLeft;
1702
				$this->db->query('
1703
					UPDATE player_stored_sector
1704
						SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1705
					WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID)
1706
				);
1707
				return true;
1708
			}
1709
		}
1710
1711
		create_error('You do not have a saved sector for #' . $sectorID);
1712
	}
1713
1714
	public function addDestinationButton($sectorID, $label) {
1715
1716
		if (!is_numeric($sectorID) || !SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1717
			create_error('You want to add a non-existent sector?');
1718
		}
1719
1720
		// sector already stored ?
1721
		foreach ($this->getStoredDestinations() as $sd) {
1722
			if ($sd['SectorID'] == $sectorID) {
1723
				create_error('Sector already stored!');
1724
			}
1725
		}
1726
1727
		$this->storedDestinations[] = array(
1728
			'Label' => $label,
1729
			'SectorID' => (int)$sectorID,
1730
			'OffsetTop' => 1,
1731
			'OffsetLeft' => 1
1732
		);
1733
1734
		$this->db->query('
1735
			INSERT INTO player_stored_sector (player_id, game_id, sector_id, label, offset_top, offset_left)
1736
			VALUES (' . $this->db->escapeNumber($this->getPlayerID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($sectorID) . ',' . $this->db->escapeString($label, true) . ',1,1)'
1737
		);
1738
	}
1739
1740
	public function deleteDestinationButton($sectorID) {
1741
		if (!is_numeric($sectorID) || $sectorID < 1) {
1742
			create_error('You want to remove a non-existent sector?');
1743
		}
1744
1745
		foreach ($this->getStoredDestinations() as $key => $sd) {
1746
			if ($sd['SectorID'] == $sectorID) {
1747
				$this->db->query('
1748
					DELETE FROM player_stored_sector
1749
					WHERE ' . $this->SQL . '
1750
					AND sector_id = ' . $this->db->escapeNumber($sectorID)
1751
				);
1752
				unset($this->storedDestinations[$key]);
1753
				return true;
1754
			}
1755
		}
1756
		return false;
1757
	}
1758
1759
	public function getTickers() {
1760
		if (!isset($this->tickers)) {
1761
			$this->tickers = array();
1762
			//get ticker info
1763
			$this->db->query('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(TIME));
1764
			while ($this->db->nextRecord()) {
1765
				$this->tickers[$this->db->getField('type')] = [
1766
					'Type' => $this->db->getField('type'),
1767
					'Time' => $this->db->getInt('time'),
1768
					'Expires' => $this->db->getInt('expires'),
1769
					'Recent' => $this->db->getField('recent'),
1770
				];
1771
			}
1772
		}
1773
		return $this->tickers;
1774
	}
1775
1776
	public function hasTickers() {
1777
		return count($this->getTickers()) > 0;
1778
	}
1779
1780
	public function getTicker($tickerType) {
1781
		$tickers = $this->getTickers();
1782
		if (isset($tickers[$tickerType])) {
1783
			return $tickers[$tickerType];
1784
		}
1785
		return false;
1786
	}
1787
1788
	public function hasTicker($tickerType) {
1789
		return $this->getTicker($tickerType) !== false;
1790
	}
1791
1792
	public function &shootPlayer(AbstractSmrPlayer $targetPlayer) {
1793
		return $this->getShip()->shootPlayer($targetPlayer);
1794
	}
1795
1796
	public function &shootForces(SmrForce $forces) {
1797
		return $this->getShip()->shootForces($forces);
1798
	}
1799
1800
	public function &shootPort(SmrPort $port) {
1801
		return $this->getShip()->shootPort($port);
1802
	}
1803
1804
	public function &shootPlanet(SmrPlanet $planet, $delayed) {
1805
		return $this->getShip()->shootPlanet($planet, $delayed);
1806
	}
1807
1808
	public function &shootPlayers(array $targetPlayers) {
1809
		return $this->getShip()->shootPlayers($targetPlayers);
1810
	}
1811
1812
	public function getMilitaryPayment() {
1813
		return $this->militaryPayment;
1814
	}
1815
1816
	public function hasMilitaryPayment() {
1817
		return $this->getMilitaryPayment() > 0;
1818
	}
1819
1820
	public function setMilitaryPayment($amount) {
1821
		if ($this->militaryPayment == $amount) {
1822
			return;
1823
		}
1824
		$this->militaryPayment = $amount;
1825
		$this->hasChanged = true;
1826
	}
1827
1828
	public function increaseMilitaryPayment($amount) {
1829
		if ($amount < 0) {
1830
			throw new Exception('Trying to increase negative military payment.');
1831
		}
1832
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1833
	}
1834
1835
	public function decreaseMilitaryPayment($amount) {
1836
		if ($amount < 0) {
1837
			throw new Exception('Trying to decrease negative military payment.');
1838
		}
1839
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1840
	}
1841
1842
	protected function getBountiesData() {
1843
		if (!isset($this->bounties)) {
1844
			$this->bounties = array();
1845
			$this->db->query('SELECT * FROM bounty WHERE ' . $this->SQL);
1846
			while ($this->db->nextRecord()) {
1847
				$this->bounties[$this->db->getInt('bounty_id')] = array(
1848
							'Amount' => $this->db->getInt('amount'),
1849
							'SmrCredits' => $this->db->getInt('smr_credits'),
1850
							'Type' => $this->db->getField('type'),
1851
							'Claimer' => $this->db->getInt('claimer_id'),
1852
							'Time' => $this->db->getInt('time'),
1853
							'ID' => $this->db->getInt('bounty_id'),
1854
							'New' => false);
1855
			}
1856
		}
1857
	}
1858
1859
	// Get bounties that can be claimed by this player
1860
	// Type must be 'HQ' or 'UG'
1861
	public function getClaimableBounties($type) {
1862
		$bounties = array();
1863
		$this->db->query('SELECT * FROM bounty WHERE claimer_player_id=' . $this->db->escapeNumber($this->getPlayerID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND type=' . $this->db->escapeString($type));
1864
		while ($this->db->nextRecord()) {
1865
			$bounties[] = array(
1866
				'player' => SmrPlayer::getPlayer($this->db->getInt('player_id'), $this->getGameID()),
1867
				'bounty_id' => $this->db->getInt('bounty_id'),
1868
				'credits' => $this->db->getInt('amount'),
1869
				'smr_credits' => $this->db->getInt('smr_credits'),
1870
			);
1871
		}
1872
		return $bounties;
1873
	}
1874
1875
	public function getBounties() : array {
1876
		$this->getBountiesData();
1877
		return $this->bounties;
1878
	}
1879
1880
	public function hasBounties() : bool {
1881
		return count($this->getBounties()) > 0;
1882
	}
1883
1884
	protected function getBounty(int $bountyID) : array {
1885
		if (!$this->hasBounty($bountyID)) {
1886
			throw new Exception('BountyID does not exist: ' . $bountyID);
1887
		}
1888
		return $this->bounties[$bountyID];
1889
	}
1890
1891
	public function hasBounty(int $bountyID) : bool {
1892
		$bounties = $this->getBounties();
1893
		return isset($bounties[$bountyID]);
1894
	}
1895
1896
	protected function getBountyAmount(int $bountyID) : int {
1897
		$bounty = $this->getBounty($bountyID);
1898
		return $bounty['Amount'];
1899
	}
1900
1901
	protected function createBounty(string $type) : array {
1902
		$bounty = array('Amount' => 0,
1903
						'SmrCredits' => 0,
1904
						'Type' => $type,
1905
						'Claimer' => 0,
1906
						'Time' => TIME,
1907
						'ID' => $this->getNextBountyID(),
1908
						'New' => true);
1909
		$this->setBounty($bounty);
1910
		return $bounty;
1911
	}
1912
1913
	protected function getNextBountyID() : int {
1914
		$keys = array_keys($this->getBounties());
1915
		if (count($keys) > 0) {
1916
			return max($keys) + 1;
1917
		} else {
1918
			return 0;
1919
		}
1920
	}
1921
1922
	protected function setBounty(array $bounty) : void {
1923
		$this->bounties[$bounty['ID']] = $bounty;
1924
		$this->hasBountyChanged[$bounty['ID']] = true;
1925
	}
1926
1927
	protected function setBountyAmount(int $bountyID, int $amount) : void {
1928
		$bounty = $this->getBounty($bountyID);
1929
		$bounty['Amount'] = $amount;
1930
		$this->setBounty($bounty);
1931
	}
1932
1933
	public function getCurrentBounty(string $type) : array {
1934
		$bounties = $this->getBounties();
1935
		foreach ($bounties as $bounty) {
1936
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1937
				return $bounty;
1938
			}
1939
		}
1940
		return $this->createBounty($type);
1941
	}
1942
1943
	public function hasCurrentBounty(string $type) : bool {
1944
		$bounties = $this->getBounties();
1945
		foreach ($bounties as $bounty) {
1946
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1947
				return true;
1948
			}
1949
		}
1950
		return false;
1951
	}
1952
1953
	protected function getCurrentBountyAmount(string $type) : int {
1954
		$bounty = $this->getCurrentBounty($type);
1955
		return $bounty['Amount'];
1956
	}
1957
1958
	protected function setCurrentBountyAmount(string $type, int $amount) : void {
1959
		$bounty = $this->getCurrentBounty($type);
1960
		if ($bounty['Amount'] == $amount) {
1961
			return;
1962
		}
1963
		$bounty['Amount'] = $amount;
1964
		$this->setBounty($bounty);
1965
	}
1966
1967
	public function increaseCurrentBountyAmount(string $type, int $amount) : void {
1968
		if ($amount < 0) {
1969
			throw new Exception('Trying to increase negative current bounty.');
1970
		}
1971
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) + $amount);
1972
	}
1973
1974
	public function decreaseCurrentBountyAmount(string $type, int $amount) : void {
1975
		if ($amount < 0) {
1976
			throw new Exception('Trying to decrease negative current bounty.');
1977
		}
1978
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) - $amount);
1979
	}
1980
1981
	protected function getCurrentBountySmrCredits(string $type) : int {
1982
		$bounty = $this->getCurrentBounty($type);
1983
		return $bounty['SmrCredits'];
1984
	}
1985
1986
	protected function setCurrentBountySmrCredits(string $type, int $credits) : void {
1987
		$bounty = $this->getCurrentBounty($type);
1988
		if ($bounty['SmrCredits'] == $credits) {
1989
			return;
1990
		}
1991
		$bounty['SmrCredits'] = $credits;
1992
		$this->setBounty($bounty);
1993
	}
1994
1995
	public function increaseCurrentBountySmrCredits(string $type, int $credits) : void {
1996
		if ($credits < 0) {
1997
			throw new Exception('Trying to increase negative current bounty.');
1998
		}
1999
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) + $credits);
2000
	}
2001
2002
	public function decreaseCurrentBountySmrCredits(string $type, int $credits) : void {
2003
		if ($credits < 0) {
2004
			throw new Exception('Trying to decrease negative current bounty.');
2005
		}
2006
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) - $credits);
2007
	}
2008
2009
	public function setBountiesClaimable(AbstractSmrPlayer $claimer) : void {
2010
		foreach ($this->getBounties() as $bounty) {
2011
			if ($bounty['ClaimerPlayerID'] == 0) {
2012
				$bounty['ClaimerPlayerID'] = $claimer->getPlayerID();
2013
				$this->setBounty($bounty);
2014
			}
2015
		}
2016
	}
2017
2018
	protected function getHOFData() {
2019
		if (!isset($this->HOF)) {
2020
			//Get Player HOF
2021
			$this->db->query('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2022
			$this->HOF = array();
2023
			while ($this->db->nextRecord()) {
2024
				$hof =& $this->HOF;
2025
				$typeList = explode(':', $this->db->getField('type'));
2026
				foreach ($typeList as $type) {
2027
					if (!isset($hof[$type])) {
2028
						$hof[$type] = array();
2029
					}
2030
					$hof =& $hof[$type];
2031
				}
2032
				$hof = $this->db->getFloat('amount');
2033
			}
2034
			self::getHOFVis();
2035
		}
2036
	}
2037
2038
	public static function getHOFVis() {
2039
		if (!isset(self::$HOFVis)) {
2040
			//Get Player HOF Vis
2041
			$db = new SmrMySqlDatabase();
2042
			$db->query('SELECT type,visibility FROM hof_visibility');
2043
			self::$HOFVis = array();
2044
			while ($db->nextRecord()) {
2045
				self::$HOFVis[$db->getField('type')] = $db->getField('visibility');
2046
			}
2047
		}
2048
	}
2049
2050
	public function getHOF(array $typeList = null) {
2051
		$this->getHOFData();
2052
		if ($typeList == null) {
2053
			return $this->HOF;
2054
		}
2055
		$hof = $this->HOF;
2056
		foreach ($typeList as $type) {
2057
			if (!isset($hof[$type])) {
2058
				return 0;
2059
			}
2060
			$hof = $hof[$type];
2061
		}
2062
		return $hof;
2063
	}
2064
2065
	public function increaseHOF($amount, array $typeList, $visibility) {
2066
		if ($amount < 0) {
2067
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2068
		}
2069
		if ($amount == 0) {
2070
			return;
2071
		}
2072
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2073
	}
2074
2075
	public function decreaseHOF($amount, array $typeList, $visibility) {
2076
		if ($amount < 0) {
2077
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2078
		}
2079
		if ($amount == 0) {
2080
			return;
2081
		}
2082
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2083
	}
2084
2085
	public function setHOF($amount, array $typeList, $visibility) {
2086
		if (is_array($this->getHOF($typeList))) {
2087
			throw new Exception('Trying to overwrite a HOF type: ' . implode(':', $typeList));
2088
		}
2089
		if ($this->isNPC()) {
2090
			// Don't store HOF for NPCs.
2091
			return;
2092
		}
2093
		if ($this->getHOF($typeList) == $amount) {
2094
			return;
2095
		}
2096
		if ($amount < 0) {
2097
			$amount = 0;
2098
		}
2099
		$this->getHOF();
2100
2101
		$hofType = implode(':', $typeList);
2102
		if (!isset(self::$HOFVis[$hofType])) {
2103
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2104
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2105
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2106
		}
2107
		self::$HOFVis[$hofType] = $visibility;
2108
2109
		$hof =& $this->HOF;
2110
		$hofChanged =& $this->hasHOFChanged;
2111
		$new = false;
2112
		foreach ($typeList as $type) {
2113
			if (!isset($hofChanged[$type])) {
2114
				$hofChanged[$type] = array();
2115
			}
2116
			if (!isset($hof[$type])) {
2117
				$hof[$type] = array();
2118
				$new = true;
2119
			}
2120
			$hof =& $hof[$type];
2121
			$hofChanged =& $hofChanged[$type];
2122
		}
2123
		if ($hofChanged == null) {
2124
			$hofChanged = self::HOF_CHANGED;
2125
			if ($new) {
2126
				$hofChanged = self::HOF_NEW;
2127
			}
2128
		}
2129
		$hof = $amount;
2130
	}
2131
2132
	public function getExperienceRank() {
2133
		return $this->computeRanking('experience', $this->getExperience());
2134
	}
2135
2136
	public function getKillsRank() {
2137
		return $this->computeRanking('kills', $this->getKills());
2138
	}
2139
2140
	public function getDeathsRank() {
2141
		return $this->computeRanking('deaths', $this->getDeaths());
2142
	}
2143
2144
	public function getAssistsRank() {
2145
		return $this->computeRanking('assists', $this->getAssists());
2146
	}
2147
2148
	private function computeRanking($dbField, $playerAmount) {
2149
		$this->db->query('SELECT count(*) FROM player
2150
			WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . '
2151
			AND (
2152
				'.$dbField . ' > ' . $this->db->escapeNumber($playerAmount) . '
2153
				OR (
2154
					'.$dbField . ' = ' . $this->db->escapeNumber($playerAmount) . '
2155
					AND player_name <= ' . $this->db->escapeString($this->getPlayerName()) . '
2156
				)
2157
			)');
2158
		$this->db->nextRecord();
2159
		$rank = $this->db->getInt('count(*)');
2160
		return $rank;
2161
	}
2162
2163
	public function killPlayer($sectorID) {
2164
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2165
		//msg taken care of in trader_att_proc.php
2166
		// forget plotted course
2167
		$this->deletePlottedCourse();
2168
2169
		$sector->diedHere($this);
2170
2171
		// if we are in an alliance we increase their deaths
2172
		if ($this->hasAlliance()) {
2173
			$this->db->query('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2174
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()) . ' LIMIT 1');
2175
		}
2176
2177
		// record death stat
2178
		$this->increaseHOF(1, array('Dying', 'Deaths'), HOF_PUBLIC);
2179
		//record cost of ship lost
2180
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2181
		// reset turns since last death
2182
		$this->setHOF(0, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2183
2184
		// 1/4 of ship value -> insurance
2185
		$newCredits = IRound($this->getShip()->getCost() / 4);
2186
		if ($newCredits < 100000) {
2187
			$newCredits = 100000;
2188
		}
2189
		$this->setCredits($newCredits);
2190
2191
		$this->setSectorID($this->getHome());
2192
		$this->increaseDeaths(1);
2193
		$this->setLandedOnPlanet(false);
2194
		$this->setDead(true);
2195
		$this->setNewbieWarning(true);
2196
		$this->getShip()->getPod($this->hasNewbieStatus());
2197
		$this->setNewbieTurns(100);
2198
	}
2199
2200
	public function &killPlayerByPlayer(AbstractSmrPlayer $killer) {
2201
		$return = array();
2202
		$msg = $this->getBBLink();
2203
2204
		if ($this->hasCustomShipName()) {
2205
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2206
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2207
		}
2208
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2209
		if ($killer->hasCustomShipName()) {
2210
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2211
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2212
		}
2213
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2214
		$this->getSector()->increaseBattles(1);
2215
		$this->db->query('INSERT INTO news (game_id,time,news_message,type,killer_player_id,killer_alliance,dead_player_id,dead_alliance) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber(TIME) . ',' . $this->db->escapeString($msg, true) . ',\'regular\',' . $this->db->escapeNumber($killer->getPlayerID()) . ',' . $this->db->escapeNumber($killer->getAllianceID()) . ',' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2216
2217
		self::sendMessageFromFedClerk($this->getGameID(), $this->getPlayerID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2218
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getPlayerID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2219
2220
		// Dead player loses between 5% and 25% experience
2221
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2222
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2223
		$this->decreaseExperience($return['DeadExp']);
2224
2225
		// Killer gains 50% of the lost exp
2226
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
2227
		$killer->increaseExperience($return['KillerExp']);
2228
2229
		$return['KillerCredits'] = $this->getCredits();
2230
		$killer->increaseCredits($return['KillerCredits']);
2231
2232
		// The killer may change alignment
2233
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2234
		$relation = $relations[$killer->getRaceID()];
2235
2236
		$alignChangePerRelation = 0.1;
2237
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2238
			$alignChangePerRelation = 0.04;
2239
		}
2240
2241
		$return['KillerAlign'] = -$relation * $alignChangePerRelation; //Lose relations when killing a peaceful race
2242
		if ($return['KillerAlign'] > 0) {
2243
			$killer->increaseAlignment($return['KillerAlign']);
2244
		} else {
2245
			$killer->decreaseAlignment(-$return['KillerAlign']);
2246
		}
2247
		// War setting gives them military pay
2248
		if ($relation <= RELATIONS_WAR) {
2249
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2250
		}
2251
2252
		//check for federal bounty being offered for current port raiders;
2253
		$this->db->query('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(TIME - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2254
		$query = 'SELECT 1
2255
					FROM player_attacks_port
2256
					JOIN port USING(game_id, sector_id)
2257
					JOIN player USING(game_id, player_id)
2258
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2259
		$this->db->query($query);
2260
		if ($this->db->nextRecord()) {
2261
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2262
			$this->increaseCurrentBountyAmount('HQ', $bounty);
2263
		}
2264
2265
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2266
		$this->setBountiesClaimable($killer);
2267
2268
		// If the alignment difference is greater than 200 then a bounty may be set
2269
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2270
		$return['BountyGained'] = array(
2271
			'Type' => 'None',
2272
			'Amount' => 0
2273
		);
2274
		if ($alignmentDiff >= 200) {
2275
			// If the podded players alignment makes them deputy or member then set bounty
2276
			if ($this->getAlignment() >= 100) {
2277
				$return['BountyGained']['Type'] = 'HQ';
2278
			} elseif ($this->getAlignment() <= 100) {
2279
				$return['BountyGained']['Type'] = 'UG';
2280
			}
2281
2282
			if ($return['BountyGained']['Type'] != 'None') {
2283
				$return['BountyGained']['Amount'] = IFloor(pow($alignmentDiff, 2.56));
2284
				$killer->increaseCurrentBountyAmount($return['BountyGained']['Type'], $return['BountyGained']['Amount']);
2285
			}
2286
		}
2287
2288
		if ($this->isNPC()) {
2289
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'NPC', 'Experience', 'Gained'), HOF_PUBLIC);
2290
			$killer->increaseHOF($this->getExperience(), array('Killing', 'NPC', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2291
2292
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By NPCs Killed'), HOF_PUBLIC);
2293
2294
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2295
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Gain'), HOF_PUBLIC);
2296
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'NPC', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2297
2298
			if ($return['KillerAlign'] > 0) {
2299
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Gain'), HOF_PUBLIC);
2300
			} else {
2301
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Loss'), HOF_PUBLIC);
2302
			}
2303
2304
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'NPC', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2305
2306
			$killer->increaseHOF(1, array('Killing', 'NPC Kills'), HOF_PUBLIC);
2307
		} else {
2308
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'Experience', 'Gained'), HOF_PUBLIC);
2309
			$killer->increaseHOF($this->getExperience(), array('Killing', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2310
2311
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By Traders Killed'), HOF_PUBLIC);
2312
2313
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2314
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Gain'), HOF_PUBLIC);
2315
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2316
2317
			if ($return['KillerAlign'] > 0) {
2318
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'Alignment', 'Gain'), HOF_PUBLIC);
2319
			} else {
2320
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'Alignment', 'Loss'), HOF_PUBLIC);
2321
			}
2322
2323
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2324
2325
			if ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2326
				$killer->increaseHOF(1, array('Killing', 'Newbie Kills'), HOF_PUBLIC);
2327
			} else {
2328
				$killer->increaseKills(1);
2329
				$killer->increaseHOF(1, array('Killing', 'Kills'), HOF_PUBLIC);
2330
2331
				if ($killer->hasAlliance()) {
2332
					$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');
2333
				}
2334
2335
				// alliance vs. alliance stats
2336
				$this->incrementAllianceVsDeaths($killer->getAllianceID());
2337
			}
2338
		}
2339
2340
		$this->increaseHOF($return['BountyGained']['Amount'], array('Dying', 'Players', 'Money', 'Bounty Gained By Killer'), HOF_PUBLIC);
2341
		$this->increaseHOF($return['KillerExp'], array('Dying', 'Players', 'Experience', 'Gained By Killer'), HOF_PUBLIC);
2342
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2343
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Players', 'Experience', 'Lost'), HOF_PUBLIC);
2344
		$this->increaseHOF($return['KillerCredits'], array('Dying', 'Players', 'Money Lost'), HOF_PUBLIC);
2345
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Players', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2346
		$this->increaseHOF(1, array('Dying', 'Players', 'Deaths'), HOF_PUBLIC);
2347
2348
		$this->killPlayer($this->getSectorID());
2349
		return $return;
2350
	}
2351
2352
	public function &killPlayerByForces(SmrForce $forces) {
2353
		$return = array();
2354
		$owner = $forces->getOwner();
2355
		// send a message to the person who died
2356
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getPlayerID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2357
		self::sendMessageFromFedClerk($this->getGameID(), $this->getPlayerID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2358
2359
		$news_message = $this->getBBLink();
2360
		if ($this->hasCustomShipName()) {
2361
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2362
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2363
		}
2364
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2365
		// insert the news entry
2366
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_player_id,killer_alliance,dead_player_id,dead_alliance)
2367
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($owner->getPlayerID()) . ',' . $this->db->escapeNumber($owner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2368
2369
		// Player loses 15% experience
2370
		$expLossPercentage = .15;
2371
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
2372
		$this->decreaseExperience($return['DeadExp']);
2373
2374
		$return['LostCredits'] = $this->getCredits();
2375
2376
		// alliance vs. alliance stats
2377
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2378
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2379
2380
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2381
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Forces', 'Experience Lost'), HOF_PUBLIC);
2382
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Forces', 'Money Lost'), HOF_PUBLIC);
2383
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Forces', 'Cost Of Ships Lost'), HOF_PUBLIC);
2384
		$this->increaseHOF(1, array('Dying', 'Forces', 'Deaths'), HOF_PUBLIC);
2385
2386
		$this->killPlayer($forces->getSectorID());
2387
		return $return;
2388
	}
2389
2390
	public function &killPlayerByPort(SmrPort $port) {
2391
		$return = array();
2392
		// send a message to the person who died
2393
		self::sendMessageFromFedClerk($this->getGameID(), $this->getPlayerID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2394
2395
		$news_message = $this->getBBLink();
2396
		if ($this->hasCustomShipName()) {
2397
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2398
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2399
		}
2400
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2401
		// insert the news entry
2402
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_player_id,dead_player_id,dead_alliance)
2403
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber(PLAYER_ID_PORT) . ',' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2404
2405
		// Player loses between 15% and 20% experience
2406
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2407
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2408
		$this->decreaseExperience($return['DeadExp']);
2409
2410
		$return['LostCredits'] = $this->getCredits();
2411
2412
		// alliance vs. alliance stats
2413
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2414
2415
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2416
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Ports', 'Experience Lost'), HOF_PUBLIC);
2417
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Ports', 'Money Lost'), HOF_PUBLIC);
2418
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Ports', 'Cost Of Ships Lost'), HOF_PUBLIC);
2419
		$this->increaseHOF(1, array('Dying', 'Ports', 'Deaths'), HOF_PUBLIC);
2420
2421
		$this->killPlayer($port->getSectorID());
2422
		return $return;
2423
	}
2424
2425
	public function &killPlayerByPlanet(SmrPlanet $planet) {
2426
		$return = array();
2427
		// send a message to the person who died
2428
		$planetOwner = $planet->getOwner();
2429
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getPlayerID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2430
		self::sendMessageFromFedClerk($this->getGameID(), $this->getPlayerID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2431
2432
		$news_message = $this->getBBLink();
2433
		if ($this->hasCustomShipName()) {
2434
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2435
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2436
		}
2437
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2438
		// insert the news entry
2439
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_player_id,killer_alliance,dead_player_id,dead_alliance)
2440
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($planetOwner->getPlayerID()) . ',' . $this->db->escapeNumber($planetOwner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2441
2442
		// Player loses between 15% and 20% experience
2443
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2444
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2445
		$this->decreaseExperience($return['DeadExp']);
2446
2447
		$return['LostCredits'] = $this->getCredits();
2448
2449
		// alliance vs. alliance stats
2450
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2451
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2452
2453
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2454
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Planets', 'Experience Lost'), HOF_PUBLIC);
2455
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Planets', 'Money Lost'), HOF_PUBLIC);
2456
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Planets', 'Cost Of Ships Lost'), HOF_PUBLIC);
2457
		$this->increaseHOF(1, array('Dying', 'Planets', 'Deaths'), HOF_PUBLIC);
2458
2459
		$this->killPlayer($planet->getSectorID());
2460
		return $return;
2461
	}
2462
2463
	public function incrementAllianceVsKills($otherID) {
2464
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2465
		$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');
2466
	}
2467
2468
	public function incrementAllianceVsDeaths($otherID) {
2469
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2470
		$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');
2471
	}
2472
2473
	public function getTurnsLevel() {
2474
		if (!$this->hasTurns()) {
2475
			return 'NONE';
2476
		}
2477
		if ($this->getTurns() <= 25) {
2478
			return 'LOW';
2479
		}
2480
		if ($this->getTurns() <= 75) {
2481
			return 'MEDIUM';
2482
		}
2483
		return 'HIGH';
2484
	}
2485
2486
	/**
2487
	 * Returns the CSS class color to use when displaying the player's turns
2488
	 */
2489
	public function getTurnsColor() {
2490
		switch ($this->getTurnsLevel()) {
2491
			case 'NONE':
2492
			case 'LOW':
2493
				return 'red';
2494
			case 'MEDIUM':
2495
				return 'yellow';
2496
			default:
2497
				return 'green';
2498
		}
2499
	}
2500
2501
	public function getTurns() {
2502
		return $this->turns;
2503
	}
2504
2505
	public function hasTurns() {
2506
		return $this->turns > 0;
2507
	}
2508
2509
	public function getMaxTurns() {
2510
		return $this->getGame()->getMaxTurns();
2511
	}
2512
2513
	public function setTurns($turns) {
2514
		if ($this->turns == $turns) {
2515
			return;
2516
		}
2517
		// Make sure turns are in range [0, MaxTurns]
2518
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2519
		$this->hasChanged = true;
2520
	}
2521
2522
	public function takeTurns($take, $takeNewbie = 0) {
2523
		if ($take < 0 || $takeNewbie < 0) {
2524
			throw new Exception('Trying to take negative turns.');
2525
		}
2526
		$take = ICeil($take);
2527
		// Only take up to as many newbie turns as we have remaining
2528
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2529
2530
		$this->setTurns($this->getTurns() - $take);
2531
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2532
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2533
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Total'), HOF_ALLIANCE);
2534
		$this->increaseHOF($takeNewbie, array('Movement', 'Turns Used', 'Newbie'), HOF_ALLIANCE);
2535
2536
		// Player has taken an action
2537
		$this->setLastActive(TIME);
2538
		$this->updateLastCPLAction();
2539
	}
2540
2541
	public function giveTurns(int $give, $giveNewbie = 0) {
2542
		if ($give < 0 || $giveNewbie < 0) {
2543
			throw new Exception('Trying to give negative turns.');
2544
		}
2545
		$this->setTurns($this->getTurns() + $give);
2546
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2547
	}
2548
2549
	/**
2550
	 * Calculate the time in seconds between the given time and when the
2551
	 * player will be at max turns.
2552
	 */
2553
	public function getTimeUntilMaxTurns($time, $forceUpdate = false) {
2554
		$timeDiff = $time - $this->getLastTurnUpdate();
2555
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2556
		$ship = $this->getShip($forceUpdate);
2557
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
2558
		// If already at max turns, return 0
2559
		return max(0, $maxTurnsTime);
2560
	}
2561
2562
	/**
2563
	 * Grant the player their starting turns.
2564
	 */
2565
	public function giveStartingTurns() {
2566
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
2567
		$this->giveTurns($startTurns);
2568
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2569
	}
2570
2571
	// Turns only update when player is active.
2572
	// Calculate turns gained between given time and the last turn update
2573
	public function getTurnsGained($time, $forceUpdate = false) : int {
2574
		$timeDiff = $time - $this->getLastTurnUpdate();
2575
		$ship = $this->getShip($forceUpdate);
2576
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
2577
		return $extraTurns;
2578
	}
2579
2580
	public function updateTurns() {
2581
		// is account validated?
2582
		if (!$this->getAccount()->isValidated()) {
2583
			return;
2584
		}
2585
2586
		// how many turns would he get right now?
2587
		$extraTurns = $this->getTurnsGained(TIME);
2588
2589
		// do we have at least one turn to give?
2590
		if ($extraTurns > 0) {
2591
			// recalc the time to avoid rounding errors
2592
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
2593
			$this->setLastTurnUpdate($newLastTurnUpdate);
2594
			$this->giveTurns($extraTurns);
2595
		}
2596
	}
2597
2598
	public function getLastTurnUpdate() {
2599
		return $this->lastTurnUpdate;
2600
	}
2601
2602
	public function setLastTurnUpdate($time) {
2603
		if ($this->lastTurnUpdate == $time) {
2604
			return;
2605
		}
2606
		$this->lastTurnUpdate = $time;
2607
		$this->hasChanged = true;
2608
	}
2609
2610
	public function getLastActive() {
2611
		return $this->lastActive;
2612
	}
2613
2614
	public function setLastActive($lastActive) {
2615
		if ($this->lastActive == $lastActive) {
2616
			return;
2617
		}
2618
		$this->lastActive = $lastActive;
2619
		$this->hasChanged = true;
2620
	}
2621
2622
	public function getLastCPLAction() {
2623
		return $this->lastCPLAction;
2624
	}
2625
2626
	public function setLastCPLAction($time) {
2627
		if ($this->lastCPLAction == $time) {
2628
			return;
2629
		}
2630
		$this->lastCPLAction = $time;
2631
		$this->hasChanged = true;
2632
	}
2633
2634
	public function updateLastCPLAction() {
2635
		$this->setLastCPLAction(TIME);
2636
	}
2637
2638
	public function setNewbieWarning($bool) {
2639
		if ($this->newbieWarning == $bool) {
2640
			return;
2641
		}
2642
		$this->newbieWarning = $bool;
2643
		$this->hasChanged = true;
2644
	}
2645
2646
	public function getNewbieWarning() {
2647
		return $this->newbieWarning;
2648
	}
2649
2650
	public function isDisplayMissions() {
2651
		return $this->displayMissions;
2652
	}
2653
2654
	public function setDisplayMissions($bool) {
2655
		if ($this->displayMissions == $bool) {
2656
			return;
2657
		}
2658
		$this->displayMissions = $bool;
2659
		$this->hasChanged = true;
2660
	}
2661
2662
	public function getMissions() {
2663
		if (!isset($this->missions)) {
2664
			$this->db->query('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2665
			$this->missions = array();
2666
			while ($this->db->nextRecord()) {
2667
				$missionID = $this->db->getInt('mission_id');
2668
				$this->missions[$missionID] = array(
2669
					'On Step' => $this->db->getInt('on_step'),
2670
					'Progress' => $this->db->getInt('progress'),
2671
					'Unread' => $this->db->getBoolean('unread'),
2672
					'Expires' => $this->db->getInt('step_fails'),
2673
					'Sector' => $this->db->getInt('mission_sector'),
2674
					'Starting Sector' => $this->db->getInt('starting_sector')
2675
				);
2676
				$this->rebuildMission($missionID);
2677
			}
2678
		}
2679
		return $this->missions;
2680
	}
2681
2682
	public function getActiveMissions() {
2683
		$missions = $this->getMissions();
2684
		foreach ($missions as $missionID => $mission) {
2685
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2686
				unset($missions[$missionID]);
2687
			}
2688
		}
2689
		return $missions;
2690
	}
2691
2692
	protected function getMission($missionID) {
2693
		$missions = $this->getMissions();
2694
		if (isset($missions[$missionID])) {
2695
			return $missions[$missionID];
2696
		}
2697
		return false;
2698
	}
2699
2700
	protected function hasMission($missionID) {
2701
		return $this->getMission($missionID) !== false;
2702
	}
2703
2704
	protected function updateMission($missionID) {
2705
		$this->getMissions();
2706
		if (isset($this->missions[$missionID])) {
2707
			$mission = $this->missions[$missionID];
2708
			$this->db->query('
2709
				UPDATE player_has_mission
2710
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2711
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2712
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2713
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2714
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2715
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2716
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1'
2717
			);
2718
			return true;
2719
		}
2720
		return false;
2721
	}
2722
2723
	private function setupMissionStep($missionID) {
2724
		$mission =& $this->missions[$missionID];
2725
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2726
			// Nothing to do if this mission is already completed
2727
			return;
2728
		}
2729
		$step = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2730
		if (isset($step['PickSector'])) {
2731
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2732
			if ($realX === false) {
2733
				throw new Exception('Invalid PickSector definition in mission: ' . $missionID);
2734
			}
2735
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2736
			if ($path === false) {
2737
				// Abandon the mission if it cannot be completed due to a
2738
				// sector that does not exist or cannot be reached.
2739
				// (Probably shouldn't bestow this mission in the first place)
2740
				$this->deleteMission($missionID);
2741
				create_error('Cannot find a path to the destination!');
2742
			}
2743
			$mission['Sector'] = $path->getEndSectorID();
2744
		}
2745
	}
2746
2747
	/**
2748
	 * Declining a mission will permanently hide it from the player
2749
	 * by adding it in its completed state.
2750
	 */
2751
	public function declineMission($missionID) {
2752
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2753
		$this->addMission($missionID, $finishedStep);
2754
	}
2755
2756
	public function addMission($missionID, $step = 0) {
2757
		$this->getMissions();
2758
2759
		if (isset($this->missions[$missionID])) {
2760
			return;
2761
		}
2762
		$sector = 0;
2763
2764
		$mission = array(
2765
			'On Step' => $step,
2766
			'Progress' => 0,
2767
			'Unread' => true,
2768
			'Expires' => (TIME + 86400),
2769
			'Sector' => $sector,
2770
			'Starting Sector' => $this->getSectorID()
2771
		);
2772
2773
		$this->missions[$missionID] =& $mission;
2774
		$this->setupMissionStep($missionID);
2775
		$this->rebuildMission($missionID);
2776
2777
		$this->db->query('
2778
			REPLACE INTO player_has_mission (game_id,player_id,mission_id,on_step,progress,unread,starting_sector,mission_sector,step_fails)
2779
			VALUES ('.$this->db->escapeNumber($this->gameID) . ',' . $this->db->escapeNumber($this->playerID) . ',' . $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']) . ')'
2780
		);
2781
	}
2782
2783
	private function rebuildMission($missionID) {
2784
		$mission = $this->missions[$missionID];
2785
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2786
2787
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2788
			// If we have completed this mission just use false to indicate no current task.
2789
			$currentStep = false;
2790
		} else {
2791
			$data = ['player' => $this, 'mission' => $mission];
2792
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2793
			array_walk_recursive($currentStep, 'replaceMissionTemplate', $data);
2794
		}
2795
		$this->missions[$missionID]['Task'] = $currentStep;
2796
	}
2797
2798
	public function deleteMission($missionID) {
2799
		$this->getMissions();
2800
		if (isset($this->missions[$missionID])) {
2801
			unset($this->missions[$missionID]);
2802
			$this->db->query('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1');
2803
			return true;
2804
		}
2805
		return false;
2806
	}
2807
2808
	public function markMissionsRead() {
2809
		$this->getMissions();
2810
		$unreadMissions = array();
2811
		foreach ($this->missions as $missionID => &$mission) {
2812
			if ($mission['Unread']) {
2813
				$unreadMissions[] = $missionID;
2814
				$mission['Unread'] = false;
2815
				$this->updateMission($missionID);
2816
			}
2817
		}
2818
		return $unreadMissions;
2819
	}
2820
2821
	public function claimMissionReward($missionID) {
2822
		$this->getMissions();
2823
		$mission =& $this->missions[$missionID];
2824
		if ($mission === false) {
2825
			throw new Exception('Unknown mission: ' . $missionID);
2826
		}
2827
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2828
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2829
		}
2830
		$mission['On Step']++;
2831
		$mission['Unread'] = true;
2832
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2833
			switch ($rewardItem) {
2834
				case 'Credits':
2835
					$this->increaseCredits($amount);
2836
				break;
2837
				case 'Experience':
2838
					$this->increaseExperience($amount);
2839
				break;
2840
			}
2841
		}
2842
		$rewardText = $mission['Task']['Rewards']['Text'];
2843
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2844
			// If we haven't finished the mission yet then 
2845
			$this->setupMissionStep($missionID);
2846
		}
2847
		$this->rebuildMission($missionID);
2848
		$this->updateMission($missionID);
2849
		return $rewardText;
2850
	}
2851
2852
	public function getAvailableMissions() {
2853
		$availableMissions = array();
2854
		foreach (MISSIONS as $missionID => $mission) {
2855
			if ($this->hasMission($missionID)) {
2856
				continue;
2857
			}
2858
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2859
			if ($realX === false) {
2860
				throw new Exception('Invalid HasX definition in mission: ' . $missionID);
2861
			}
2862
			if ($this->getSector()->hasX($realX)) {
2863
				$availableMissions[$missionID] = $mission;
2864
			}
2865
		}
2866
		return $availableMissions;
2867
	}
2868
2869
	/**
2870
	 * Log a player action in the current sector to the admin log console.
2871
	 */
2872
	public function log(int $log_type_id, string $msg) : void {
2873
		$this->getAccount()->log($log_type_id, $msg, $this->getSectorID());
2874
	}
2875
2876
	public function actionTaken($actionID, array $values) {
2877
		if (!in_array($actionID, MISSION_ACTIONS)) {
2878
			throw new Exception('Unknown action: ' . $actionID);
2879
		}
2880
// TODO: Reenable this once tested.		if($this->getAccount()->isLoggingEnabled())
2881
			switch ($actionID) {
2882
				case 'WalkSector':
2883
					$this->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID());
2884
				break;
2885
				case 'JoinAlliance':
2886
					$this->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName());
2887
				break;
2888
				case 'LeaveAlliance':
2889
					$this->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName());
2890
				break;
2891
				case 'DisbandAlliance':
2892
					$this->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName());
2893
				break;
2894
				case 'KickPlayer':
2895
					$this->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName());
2896
				break;
2897
				case 'PlayerKicked':
2898
					$this->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')');
2899
				break;
2900
2901
			}
2902
		$this->getMissions();
2903
		foreach ($this->missions as $missionID => &$mission) {
2904
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
2905
				$requirements = $mission['Task']['Detail'];
2906
				if (checkMissionRequirements($values, $requirements) === true) {
2907
					$mission['On Step']++;
2908
					$mission['Unread'] = true;
2909
					$this->setupMissionStep($missionID);
2910
					$this->rebuildMission($missionID);
2911
					$this->updateMission($missionID);
2912
				}
2913
			}
2914
		}
2915
	}
2916
2917
	public function canSeeAny(array $otherPlayerArray) {
2918
		foreach ($otherPlayerArray as $otherPlayer) {
2919
			if ($this->canSee($otherPlayer)) {
2920
				return true;
2921
			}
2922
		}
2923
		return false;
2924
	}
2925
2926
	public function canSee(AbstractSmrPlayer $otherPlayer) {
2927
		if (!$otherPlayer->getShip()->isCloaked()) {
2928
			return true;
2929
		}
2930
		if ($this->sameAlliance($otherPlayer)) {
2931
			return true;
2932
		}
2933
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
2934
			return true;
2935
		}
2936
		return false;
2937
	}
2938
2939
	public function equals(AbstractSmrPlayer $otherPlayer = null) {
2940
		return $otherPlayer !== null && $this->getPlayerID() == $otherPlayer->getPlayerID() && $this->getGameID() == $otherPlayer->getGameID();
2941
	}
2942
2943
	public function sameAlliance(AbstractSmrPlayer $otherPlayer = null) {
2944
		return $this->equals($otherPlayer) || (!is_null($otherPlayer) && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
2945
	}
2946
2947
	public function sharedForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2948
		return $this->sameAlliance($otherPlayer);
2949
	}
2950
2951
	public function forceNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2952
		return $this->sameAlliance($otherPlayer);
2953
	}
2954
2955
	public function planetNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2956
		return $this->sameAlliance($otherPlayer);
2957
	}
2958
2959
	public function traderNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2960
		return $this->sameAlliance($otherPlayer);
2961
	}
2962
2963
	public function traderMAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2964
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
2965
	}
2966
2967
	public function traderAttackTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2968
		return $this->sameAlliance($otherPlayer);
2969
	}
2970
2971
	public function traderDefendTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2972
		return $this->sameAlliance($otherPlayer);
2973
	}
2974
2975
	public function traderAttackForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2976
		return $this->sameAlliance($otherPlayer);
2977
	}
2978
2979
	public function traderAttackPortAlliance(AbstractSmrPlayer $otherPlayer = null) {
2980
		return $this->sameAlliance($otherPlayer);
2981
	}
2982
2983
	public function traderAttackPlanetAlliance(AbstractSmrPlayer $otherPlayer = null) {
2984
		return $this->sameAlliance($otherPlayer);
2985
	}
2986
2987
	public function meetsAlignmentRestriction($restriction) {
2988
		if ($restriction < 0) {
2989
			return $this->getAlignment() <= $restriction;
2990
		}
2991
		if ($restriction > 0) {
2992
			return $this->getAlignment() >= $restriction;
2993
		}
2994
		return true;
2995
	}
2996
2997
	// Get an array of goods that are visible to the player
2998
	public function getVisibleGoods() {
2999
		$goods = Globals::getGoods();
3000
		$visibleGoods = array();
3001
		foreach ($goods as $key => $good) {
3002
			if ($this->meetsAlignmentRestriction($good['AlignRestriction'])) {
3003
				$visibleGoods[$key] = $good;
3004
			}
3005
		}
3006
		return $visibleGoods;
3007
	}
3008
3009
	/**
3010
	 * Will retrieve all visited sectors, use only when you are likely to check a large number of these
3011
	 */
3012
	public function hasVisitedSector($sectorID) {
3013
		if (!isset($this->visitedSectors)) {
3014
			$this->visitedSectors = array();
3015
			$this->db->query('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
3016
			while ($this->db->nextRecord()) {
3017
				$this->visitedSectors[$this->db->getInt('sector_id')] = false;
3018
			}
3019
		}
3020
		return !isset($this->visitedSectors[$sectorID]);
3021
	}
3022
3023
	public function getLeaveNewbieProtectionHREF() {
3024
		return SmrSession::getNewHREF(create_container('leave_newbie_processing.php'));
3025
	}
3026
3027
	public function getExamineTraderHREF() {
3028
		$container = create_container('skeleton.php', 'trader_examine.php');
3029
		$container['targetPlayerID'] = $this->getPlayerID();
3030
		return SmrSession::getNewHREF($container);
3031
	}
3032
3033
	public function getAttackTraderHREF() {
3034
		return Globals::getAttackTraderHREF($this->getPlayerID());
3035
	}
3036
3037
	public function getPlanetKickHREF() {
3038
		$container = create_container('planet_kick_processing.php', 'trader_attack_processing.php');
3039
		$container['targetPlayerID'] = $this->getPlayerID();
3040
		return SmrSession::getNewHREF($container);
3041
	}
3042
3043
	public function getTraderSearchHREF() {
3044
		$container = create_container('skeleton.php', 'trader_search_result.php');
3045
		$container['player_id'] = $this->getPlayerID();
3046
		return SmrSession::getNewHREF($container);
3047
	}
3048
3049
	public function getAllianceRosterHREF() {
3050
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3051
	}
3052
3053
	public function getToggleWeaponHidingHREF($ajax = false) {
3054
		$container = create_container('toggle_processing.php');
3055
		$container['toggle'] = 'WeaponHiding';
3056
		$container['AJAX'] = $ajax;
3057
		return SmrSession::getNewHREF($container);
3058
	}
3059
3060
	public function isDisplayWeapons() {
3061
		return $this->displayWeapons;
3062
	}
3063
3064
	/**
3065
	 * Should weapons be displayed in the right panel?
3066
	 * This updates the player database directly because it is used with AJAX,
3067
	 * which does not acquire a sector lock.
3068
	 */
3069
	public function setDisplayWeapons($bool) {
3070
		if ($this->displayWeapons == $bool) {
3071
			return;
3072
		}
3073
		$this->displayWeapons = $bool;
3074
		$this->db->query('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3075
	}
3076
3077
	public function update() {
3078
		$this->save();
3079
	}
3080
3081
	public function save() {
3082
		if ($this->hasChanged === true) {
3083
			$this->db->query('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3084
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3085
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3086
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3087
				', turns=' . $this->db->escapeNumber($this->turns) .
3088
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3089
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3090
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3091
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3092
				', dead=' . $this->db->escapeBoolean($this->dead) .
3093
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3094
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3095
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3096
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3097
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3098
				', credits=' . $this->db->escapeNumber($this->credits) .
3099
				', experience=' . $this->db->escapeNumber($this->experience) .
3100
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3101
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3102
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3103
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3104
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3105
				', kills=' . $this->db->escapeNumber($this->kills) .
3106
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3107
				', assists=' . $this->db->escapeNumber($this->assists) .
3108
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3109
				', bank=' . $this->db->escapeNumber($this->bank) .
3110
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3111
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3112
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3113
				', group_scout_messages=' . $this->db->escapeString($this->groupScoutMessages) .
3114
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3115
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3116
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3117
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3118
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3119
				' WHERE ' . $this->SQL . ' LIMIT 1');
3120
			$this->hasChanged = false;
3121
		}
3122
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3123
			if ($bountyChanged === true) {
3124
				$bountyChanged = false;
3125
				$bounty = $this->getBounty($key);
3126
				if ($bounty['New'] === true) {
3127
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3128
						$this->db->query('INSERT INTO bounty (player_id,game_id,type,amount,smr_credits,claimer_player_id,time) VALUES (' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeString($bounty['Type']) . ',' . $this->db->escapeNumber($bounty['Amount']) . ',' . $this->db->escapeNumber($bounty['SmrCredits']) . ',' . $this->db->escapeNumber($bounty['ClaimerPlayerID']) . ',' . $this->db->escapeNumber($bounty['Time']) . ')');
3129
					}
3130
				} else {
3131
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3132
						$this->db->query('UPDATE bounty
3133
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3134
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3135
							type=' . $this->db->escapeString($bounty['Type']) . ',
3136
							claimer_player_id=' . $this->db->escapeNumber($bounty['ClaimerPlayerID']) . ',
3137
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3138
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3139
					} else {
3140
						$this->db->query('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3141
					}
3142
				}
3143
			}
3144
		}
3145
		$this->saveHOF();
3146
	}
3147
3148
	public function saveHOF() {
3149
		if (count($this->hasHOFChanged) > 0) {
3150
			$this->doHOFSave($this->hasHOFChanged);
3151
			$this->hasHOFChanged = [];
3152
		}
3153
		if (!empty(self::$hasHOFVisChanged)) {
3154
			foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3155
				if ($changeType == self::HOF_NEW) {
3156
					$this->db->query('INSERT INTO hof_visibility (type, visibility) VALUES (' . $this->db->escapeString($hofType) . ',' . $this->db->escapeString(self::$HOFVis[$hofType]) . ')');
3157
				} else {
3158
					$this->db->query('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType) . ' LIMIT 1');
3159
				}
3160
				unset(self::$hasHOFVisChanged[$hofType]);
3161
			}
3162
		}
3163
	}
3164
3165
	/**
3166
	 * This should only be called by `saveHOF` (and recursively) to
3167
	 * ensure that the `hasHOFChanged` attribute is properly cleared.
3168
	 */
3169
	protected function doHOFSave(array $hasChangedList, array $typeList = array()) {
3170
		foreach ($hasChangedList as $type => $hofChanged) {
3171
			$tempTypeList = $typeList;
3172
			$tempTypeList[] = $type;
3173
			if (is_array($hofChanged)) {
3174
				$this->doHOFSave($hofChanged, $tempTypeList);
3175
			} else {
3176
				$amount = $this->getHOF($tempTypeList);
3177
				if ($hofChanged == self::HOF_NEW) {
3178
					if ($amount > 0) {
3179
						$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, true, ':', false) . ',' . $this->db->escapeNumber($amount) . ')');   //TODO
3180
					}
3181
				} elseif ($hofChanged == self::HOF_CHANGED) {
3182
					$this->db->query('UPDATE player_hof
3183
						SET amount=' . $this->db->escapeNumber($amount) . '
3184
						WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeArray($tempTypeList, false, true, ':', false) . ' LIMIT 1');
3185
				}
3186
			}
3187
		}
3188
	}
3189
3190
}
3191