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
04: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 getPlayerByAccountID($accountID, $gameID, $forceUpdate = false) {
193
		$db = new SmrMySqlDatabase();
194
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND account_id = ' . $db->escapeNumber($accountID));
195
		if ($db->nextRecord()) {
196
			return self::getPlayer($db->getInt('player_id'), $gameID, $forceUpdate, $db);
197
		}
198
		throw new PlayerNotFoundException('Account ID not found.');
199
	}
200
201
	public static function getPlayerByPlayerName($playerName, $gameID, $forceUpdate = false) {
202
		$db = new SmrMySqlDatabase();
203
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
204
		if ($db->nextRecord()) {
205
			return self::getPlayer($db->getInt('player_id'), $gameID, $forceUpdate, $db);
206
		}
207
		throw new PlayerNotFoundException('Player Name not found.');
208
	}
209
210
	protected function __construct($playerID, $gameID, $db = null) {
211
		$this->db = new SmrMySqlDatabase();
212
		$this->SQL = 'player_id = ' . $this->db->escapeNumber($playerID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
213
214
		if (isset($db)) {
215
			$playerExists = true;
216
		} else {
217
			$db = $this->db;
218
			$this->db->query('SELECT * FROM player WHERE ' . $this->SQL . ' LIMIT 1');
219
			$playerExists = $db->nextRecord();
220
		}
221
222
		if ($playerExists) {
223
			$this->playerID = (int)$playerID;
224
			$this->gameID = (int)$gameID;
225
			$this->accountID = $db->getInt('account_id');
226
			$this->playerName = $db->getField('player_name');
227
			$this->sectorID = $db->getInt('sector_id');
228
			$this->lastSectorID = $db->getInt('last_sector_id');
229
			$this->turns = $db->getInt('turns');
230
			$this->lastTurnUpdate = $db->getInt('last_turn_update');
231
			$this->newbieTurns = $db->getInt('newbie_turns');
232
			$this->lastNewsUpdate = $db->getInt('last_news_update');
233
			$this->attackColour = $db->getField('attack_warning');
234
			$this->dead = $db->getBoolean('dead');
235
			$this->npc = $db->getBoolean('npc');
236
			$this->newbieStatus = $db->getBoolean('newbie_status');
237
			$this->landedOnPlanet = $db->getBoolean('land_on_planet');
238
			$this->lastActive = $db->getInt('last_active');
239
			$this->lastCPLAction = $db->getInt('last_cpl_action');
240
			$this->raceID = $db->getInt('race_id');
241
			$this->credits = $db->getInt('credits');
242
			$this->experience = $db->getInt('experience');
243
			$this->alignment = $db->getInt('alignment');
244
			$this->militaryPayment = $db->getInt('military_payment');
245
			$this->allianceID = $db->getInt('alliance_id');
246
			$this->allianceJoinable = $db->getInt('alliance_join');
247
			$this->shipID = $db->getInt('ship_type_id');
248
			$this->kills = $db->getInt('kills');
249
			$this->deaths = $db->getInt('deaths');
250
			$this->assists = $db->getInt('assists');
251
			$this->lastPort = $db->getInt('last_port');
252
			$this->bank = $db->getInt('bank');
253
			$this->zoom = $db->getInt('zoom');
254
			$this->displayMissions = $db->getBoolean('display_missions');
255
			$this->displayWeapons = $db->getBoolean('display_weapons');
256
			$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...
257
			$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...
258
			$this->ignoreGlobals = $db->getBoolean('ignore_globals');
259
			$this->newbieWarning = $db->getBoolean('newbie_warning');
260
			$this->nameChanged = $db->getBoolean('name_changed');
261
			$this->raceChanged = $db->getBoolean('race_changed');
262
			$this->combatDronesKamikazeOnMines = $db->getBoolean('combat_drones_kamikaze_on_mines');
263
		} else {
264
			throw new PlayerNotFoundException('Invalid playerID: ' . $playerID . ' OR gameID:' . $gameID);
265
		}
266
	}
267
268
	/**
269
	 * Insert a new player into the database. Returns the new player object.
270
	 */
271
	public static function createPlayer($accountID, $gameID, $playerName, $raceID, $isNewbie, $npc=false) {
272
		$db = new SmrMySqlDatabase();
273
		$db->lockTable('player');
274
275
		// Player names must be unique within each game
276
		$db->query('SELECT 1 FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
277
		if ($db->nextRecord() > 0) {
278
			$db->unlock();
279
			create_error('The player name already exists.');
280
		}
281
282
		// get last registered player id in that game and increase by one.
283
		$db->query('SELECT MAX(player_id) FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
284
		if ($db->nextRecord()) {
285
			$playerID = $db->getInt('MAX(player_id)') + 1;
286
		} else {
287
			$playerID = 1;
288
		}
289
290
		$startSectorID = 0; // Temporarily put player into non-existent sector
291
		$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)
292
					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) . ')');
293
294
		$db->unlock();
295
296
		$player = SmrPlayer::getPlayer($playerID, $gameID);
297
		$player->setSectorID($player->getHome());
298
		return $player;
299
	}
300
301
	/**
302
	 * Get array of players whose info can be accessed by this player.
303
	 * Skips players who are not in the same alliance as this player.
304
	 */
305
	public function getSharingPlayers($forceUpdate = false) {
306
		$results = array($this);
307
308
		// Only return this player if not in an alliance
309
		if (!$this->hasAlliance()) {
310
			return $results;
311
		}
312
313
		// Get other players who are sharing info for this game.
314
		// NOTE: game_id=0 means that player shares info for all games.
315
		$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()) . ')');
316
		while ($this->db->nextRecord()) {
317
			try {
318
				$otherPlayer = SmrPlayer::getPlayer($this->db->getInt('from_account_id'), //TODO
319
				                                    $this->getGameID(), $forceUpdate);
320
			} catch (PlayerNotFoundException $e) {
321
				// Skip players that have not joined this game
322
				continue;
323
			}
324
325
			// players must be in the same alliance
326
			if ($this->sameAlliance($otherPlayer)) {
327
				$results[] = $otherPlayer;
328
			}
329
		}
330
		return $results;
331
	}
332
333
	public function getSQL() : string {
334
		return $this->SQL;
335
	}
336
337
	public function getZoom() {
338
		return $this->zoom;
339
	}
340
341
	protected function setZoom($zoom) {
342
		// Set the zoom level between [1, 9]
343
		$zoom = max(1, min(9, $zoom));
344
		if ($this->zoom == $zoom) {
345
			return;
346
		}
347
		$this->zoom = $zoom;
348
		$this->hasChanged = true;
349
	}
350
351
	public function increaseZoom($zoom) {
352
		if ($zoom < 0) {
353
			throw new Exception('Trying to increase negative zoom.');
354
		}
355
		$this->setZoom($this->getZoom() + $zoom);
356
	}
357
358
	public function decreaseZoom($zoom) {
359
		if ($zoom < 0) {
360
			throw new Exception('Trying to decrease negative zoom.');
361
		}
362
		$this->setZoom($this->getZoom() - $zoom);
363
	}
364
365
	public function getAttackColour() {
366
		return $this->attackColour;
367
	}
368
369
	public function setAttackColour($colour) {
370
		if ($this->attackColour == $colour) {
371
			return;
372
		}
373
		$this->attackColour = $colour;
374
		$this->hasChanged = true;
375
	}
376
377
	public function isIgnoreGlobals() {
378
		return $this->ignoreGlobals;
379
	}
380
381
	public function setIgnoreGlobals($bool) {
382
		if ($this->ignoreGlobals == $bool) {
383
			return;
384
		}
385
		$this->ignoreGlobals = $bool;
386
		$this->hasChanged = true;
387
	}
388
389
	public function getAccount() {
390
		return SmrAccount::getAccount($this->getAccountID());
391
	}
392
393
	public function getAccountID() {
394
		return $this->accountID;
395
	}
396
397
	public function getGameID() {
398
		return $this->gameID;
399
	}
400
401
	public function getGame() {
402
		return SmrGame::getGame($this->gameID);
403
	}
404
405
	public function getNewbieTurns() {
406
		return $this->newbieTurns;
407
	}
408
409
	public function hasNewbieTurns() {
410
		return $this->getNewbieTurns() > 0;
411
	}
412
	public function setNewbieTurns($newbieTurns) {
413
		if ($this->newbieTurns == $newbieTurns) {
414
			return;
415
		}
416
		$this->newbieTurns = $newbieTurns;
417
		$this->hasChanged = true;
418
	}
419
420
	public function getShip($forceUpdate = false) {
421
		return SmrShip::getShip($this, $forceUpdate);
422
	}
423
424
	public function getShipTypeID() {
425
		return $this->shipID;
426
	}
427
428
	/**
429
	 * Do not call directly. Use SmrShip::setShipTypeID instead.
430
	 */
431
	public function setShipTypeID($shipID) {
432
		if ($this->shipID == $shipID) {
433
			return;
434
		}
435
		$this->shipID = $shipID;
436
		$this->hasChanged = true;
437
	}
438
439
	public function hasCustomShipName() {
440
		return $this->getCustomShipName() !== false;
441
	}
442
443
	public function getCustomShipName() {
444
		if (!isset($this->customShipName)) {
445
			$this->db->query('SELECT * FROM ship_has_name WHERE ' . $this->SQL . ' LIMIT 1');
446
			if ($this->db->nextRecord()) {
447
				$this->customShipName = $this->db->getField('ship_name');
448
			} else {
449
				$this->customShipName = false;
450
			}
451
		}
452
		return $this->customShipName;
453
	}
454
455
	public function setCustomShipName(string $name) {
456
		$this->db->query('REPLACE INTO ship_has_name (game_id, player_id, ship_name)
457
			VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getPlayerID()) . ', ' . $this->db->escapeString($name) . ')');
458
	}
459
460
	/**
461
	 * Get planet owned by this player.
462
	 * Returns false if this player does not own a planet.
463
	 */
464
	public function getPlanet() {
465
		$this->db->query('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_player_id=' . $this->db->escapeNumber($this->getPlayerID()));
466
		if ($this->db->nextRecord()) {
467
			return SmrPlanet::getPlanet($this->getGameID(), $this->db->getInt('sector_id'), false, $this->db);
468
		} else {
469
			return false;
470
		}
471
	}
472
473
	public function getSectorPlanet() {
474
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
475
	}
476
477
	public function getSectorPort() {
478
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
479
	}
480
481
	public function getSectorID() {
482
		return $this->sectorID;
483
	}
484
485
	public function getSector() {
486
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
487
	}
488
489
	public function setSectorID($sectorID) {
490
		if ($this->sectorID == $sectorID) {
491
			return;
492
		}
493
494
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
495
		$port->addCachePort($this->getPlayerID()); //Add port of sector we were just in, to make sure it is left totally up to date.
496
497
		$this->setLastSectorID($this->getSectorID());
498
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
499
		$this->sectorID = $sectorID;
500
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
501
		$this->hasChanged = true;
502
503
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
504
		$port->addCachePort($this->getPlayerID()); //Add the port of sector we are now in.
505
	}
506
507
	public function getLastSectorID() {
508
		return $this->lastSectorID;
509
	}
510
511
	public function setLastSectorID($lastSectorID) {
512
		if ($this->lastSectorID == $lastSectorID) {
513
			return;
514
		}
515
		$this->lastSectorID = $lastSectorID;
516
		$this->hasChanged = true;
517
	}
518
519
	public function getHome() {
520
		// get his home sector
521
		$hq_id = GOVERNMENT + $this->getRaceID();
522
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
523
		if (!empty($raceHqSectors)) {
524
			// If race has multiple HQ's for some reason, use the first one
525
			return key($raceHqSectors);
526
		} else {
527
			return 1;
528
		}
529
	}
530
531
	public function isDead() {
532
		return $this->dead;
533
	}
534
535
	public function isNPC() {
536
		return $this->npc;
537
	}
538
539
	/**
540
	 * Does the player have Newbie status?
541
	 */
542
	public function hasNewbieStatus() {
543
		return $this->newbieStatus;
544
	}
545
546
	/**
547
	 * Update the player's newbie status if it has changed.
548
	 * This function queries the account, so use sparingly.
549
	 */
550
	public function updateNewbieStatus() {
551
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
552
		if ($this->newbieStatus != $accountNewbieStatus) {
553
			$this->newbieStatus = $accountNewbieStatus;
554
			$this->hasChanged = true;
555
		}
556
	}
557
558
	/**
559
	 * Has this player been designated as the alliance flagship?
560
	 */
561
	public function isFlagship() {
562
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipPlayerID() == $this->getPlayerID();
563
	}
564
565
	public function isPresident() {
566
		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

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