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

Completed
Push — master ( 8b2958...09ea2b )
by Dan
31s queued 14s
created

AbstractSmrPlayer::setExperience()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 5
nop 1
dl 0
loc 16
rs 9.9666
c 0
b 0
f 0
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
9
	const TIME_FOR_FEDERAL_BOUNTY_ON_PR = 10800;
10
	const TIME_FOR_ALLIANCE_SWITCH = 0;
11
12
	const HOF_CHANGED = 1;
13
	const HOF_NEW = 2;
14
15
	protected static $CACHE_SECTOR_PLAYERS = array();
16
	protected static $CACHE_PLANET_PLAYERS = array();
17
	protected static $CACHE_ALLIANCE_PLAYERS = array();
18
	protected static $CACHE_PLAYERS = array();
19
20
	protected $db;
21
	protected $SQL;
22
23
	protected $accountID;
24
	protected $gameID;
25
	protected $playerName;
26
	protected $playerID;
27
	protected $sectorID;
28
	protected $lastSectorID;
29
	protected $newbieTurns;
30
	protected $dead;
31
	protected $npc;
32
	protected $newbieStatus;
33
	protected $newbieWarning;
34
	protected $landedOnPlanet;
35
	protected $lastActive;
36
	protected $raceID;
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 $pureRelations;
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 $accountID => &$player) {
92
				$player = self::getPlayer($accountID, $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 $accountID => $player) {
113
			if (!in_array($player->getAllianceID(), $allianceIDs)) {
114
				unset($players[$accountID]);
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('account_id')) {
132
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = [];
133
			} else {
134
				$accountID = $db->getInt('account_id');
135
				$player = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
136
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$accountID] = $player;
137
				$galaxyPlayers[$sectorID][$accountID] = $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
				$accountID = $db->getInt('account_id');
150
				$players[$accountID] = self::getPlayer($accountID, $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
				$accountID = $db->getInt('account_id');
164
				$players[$accountID] = self::getPlayer($accountID, $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
				$accountID = $db->getInt('account_id');
178
				$players[$accountID] = self::getPlayer($accountID, $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($accountID, $gameID, $forceUpdate = false, $db = null) {
186
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$accountID])) {
187
			self::$CACHE_PLAYERS[$gameID][$accountID] = new SmrPlayer($gameID, $accountID, $db);
188
		}
189
		return self::$CACHE_PLAYERS[$gameID][$accountID];
190
	}
191
192
	public static function getPlayerByPlayerID($playerID, $gameID, $forceUpdate = false) {
193
		$db = new SmrMySqlDatabase();
194
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_id = ' . $db->escapeNumber($playerID) . ' LIMIT 1');
195
		if ($db->nextRecord()) {
196
			return self::getPlayer($db->getInt('account_id'), $gameID, $forceUpdate, $db);
197
		}
198
		throw new PlayerNotFoundException('Player 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('account_id'), $gameID, $forceUpdate, $db);
206
		}
207
		throw new PlayerNotFoundException('Player Name not found.');
208
	}
209
210
	protected function __construct($gameID, $accountID, $db = null) {
211
		$this->db = new SmrMySqlDatabase();
212
		$this->SQL = 'account_id = ' . $this->db->escapeNumber($accountID) . ' 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->accountID = (int)$accountID;
224
			$this->gameID = (int)$gameID;
225
			$this->playerName = $db->getField('player_name');
226
			$this->playerID = $db->getInt('player_id');
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 accountID: ' . $accountID . ' 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($accountID, $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'),
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, account_id, ship_name)
457
			VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $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_id=' . $this->db->escapeNumber($this->getAccountID()));
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->getAccountID()); //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->getAccountID()); //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()->getFlagshipID() == $this->getAccountID();
563
	}
564
565
	public function isPresident() {
566
		return Council::getPresidentID($this->getGameID(), $this->getRaceID()) == $this->getAccountID();
567
	}
568
569
	public function isOnCouncil() {
570
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getAccountID());
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($senderID, $receiverID, $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
			(account_id,game_id,message_type_id,message_text,
639
			sender_id,send_time,expire_time,sender_delete) VALUES(' .
640
			$db->escapeNumber($receiverID) . ',' .
641
			$db->escapeNumber($gameID) . ',' .
642
			$db->escapeNumber($messageTypeID) . ',' .
643
			$db->escapeString($message) . ',' .
644
			$db->escapeNumber($senderID) . ',' .
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, account_id, message_type_id) VALUES
655
						(' . $db->escapeNumber($gameID) . ', ' . $db->escapeNumber($receiverID) . ', ' . $db->escapeNumber($messageTypeID) . ')');
656
		}
657
658
		switch ($messageTypeID) {
659
			case MSG_PLAYER:
660
				$receiverAccount = SmrAccount::getAccount($receiverID);
661
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isLoggedIn()) {
662
					require_once(get_file_loc('message.functions.inc'));
663
					$sender = getMessagePlayer($senderID, $gameID, $messageTypeID);
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
				}
677
			break;
678
		}
679
680
		return $insertID;
681
	}
682
683
	public function sendMessageToBox($boxTypeID, $message) {
684
		// send him the message
685
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
686
	}
687
688
	public function sendGlobalMessage($message, $canBeIgnored = true) {
689
		if ($canBeIgnored) {
690
			if ($this->getAccount()->isMailBanned()) {
691
				create_error('You are currently banned from sending messages');
692
			}
693
		}
694
		$this->sendMessageToBox(BOX_GLOBALS, $message);
695
696
		// send to all online player
697
		$db = new SmrMySqlDatabase();
698
		$db->query('SELECT account_id
699
					FROM active_session
700
					JOIN player USING (game_id, account_id)
701
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(TIME - SmrSession::TIME_BEFORE_EXPIRY) . '
702
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
703
						AND ignore_globals = \'FALSE\'
704
						AND account_id != ' . $db->escapeNumber($this->getAccountID()));
705
706
		while ($db->nextRecord()) {
707
			$this->sendMessage($db->getInt('account_id'), MSG_GLOBAL, $message, $canBeIgnored);
708
		}
709
		$this->sendMessage($this->getAccountID(), MSG_GLOBAL, $message, $canBeIgnored, false);
710
	}
711
712
	public function sendMessage($receiverID, $messageTypeID, $message, $canBeIgnored = true, $unread = true, $expires = false, $senderDelete = false) {
713
		//get expire time
714
		if ($canBeIgnored) {
715
			if ($this->getAccount()->isMailBanned()) {
716
				create_error('You are currently banned from sending messages');
717
			}
718
			// Don't send messages to players ignoring us
719
			$this->db->query('SELECT account_id FROM message_blacklist WHERE account_id=' . $this->db->escapeNumber($receiverID) . ' AND blacklisted_id=' . $this->db->escapeNumber($this->getAccountID()) . ' LIMIT 1');
720
			if ($this->db->nextRecord()) {
721
				return;
722
			}
723
		}
724
725
		$message = word_filter($message);
726
727
		// If expires not specified, use default based on message type
728
		if ($expires === false) {
729
			switch ($messageTypeID) {
730
				case MSG_GLOBAL: //We don't send globals to the box here or it gets done loads of times.
731
					$expires = 3600; // 1h
732
				break;
733
				case MSG_PLAYER:
734
					$expires = 86400 * 31;
735
				break;
736
				case MSG_PLANET:
737
					$expires = 86400 * 7;
738
				break;
739
				case MSG_SCOUT:
740
					$expires = 86400 * 3;
741
				break;
742
				case MSG_POLITICAL:
743
					$expires = 86400 * 31;
744
				break;
745
				case MSG_ALLIANCE:
746
					$expires = 86400 * 31;
747
				break;
748
				case MSG_ADMIN:
749
					$expires = 86400 * 365;
750
				break;
751
				case MSG_CASINO:
752
					$expires = 86400 * 31;
753
				break;
754
				default:
755
					$expires = 86400 * 7;
756
			}
757
			$expires += TIME;
758
		}
759
760
		// Do not put scout messages in the sender's sent box
761
		if ($messageTypeID == MSG_SCOUT) {
762
			$senderDelete = true;
763
		}
764
765
		// send him the message and return the message_id
766
		return self::doMessageSending($this->getAccountID(), $receiverID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
767
	}
768
769
	public function sendMessageFromOpAnnounce($receiverID, $message, $expires = false) {
770
		// get expire time if not set
771
		if ($expires === false) {
772
			$expires = TIME + 86400 * 14;
773
		}
774
		self::doMessageSending(ACCOUNT_ID_OP_ANNOUNCE, $receiverID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
775
	}
776
777
	public function sendMessageFromAllianceCommand($receiverID, $message) {
778
		$expires = TIME + 86400 * 365;
779
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_COMMAND, $receiverID, $this->getGameID(), MSG_PLAYER, $message, $expires);
780
	}
781
782
	public static function sendMessageFromPlanet($gameID, $receiverID, $message) {
783
		//get expire time
784
		$expires = TIME + 86400 * 31;
785
		// send him the message
786
		self::doMessageSending(ACCOUNT_ID_PLANET, $receiverID, $gameID, MSG_PLANET, $message, $expires);
787
	}
788
789
	public static function sendMessageFromPort($gameID, $receiverID, $message) {
790
		//get expire time
791
		$expires = TIME + 86400 * 31;
792
		// send him the message
793
		self::doMessageSending(ACCOUNT_ID_PORT, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
794
	}
795
796
	public static function sendMessageFromFedClerk($gameID, $receiverID, $message) {
797
		$expires = TIME + 86400 * 365;
798
		self::doMessageSending(ACCOUNT_ID_FED_CLERK, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
799
	}
800
801
	public static function sendMessageFromAdmin($gameID, $receiverID, $message, $expires = false) {
802
		//get expire time
803
		if ($expires === false) {
804
			$expires = TIME + 86400 * 365;
805
		}
806
		// send him the message
807
		self::doMessageSending(ACCOUNT_ID_ADMIN, $receiverID, $gameID, MSG_ADMIN, $message, $expires);
808
	}
809
810
	public static function sendMessageFromAllianceAmbassador($gameID, $receiverID, $message, $expires = false) {
811
		//get expire time
812
		if ($expires === false) {
813
			$expires = TIME + 86400 * 31;
814
		}
815
		// send him the message
816
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_AMBASSADOR, $receiverID, $gameID, MSG_ALLIANCE, $message, $expires);
817
	}
818
819
	public static function sendMessageFromCasino($gameID, $receiverID, $message, $expires = false) {
820
		//get expire time
821
		if ($expires === false) {
822
			$expires = TIME + 86400 * 7;
823
		}
824
		// send him the message
825
		self::doMessageSending(ACCOUNT_ID_CASINO, $receiverID, $gameID, MSG_CASINO, $message, $expires);
826
	}
827
828
	public static function sendMessageFromRace($raceID, $gameID, $receiverID, $message, $expires = false) {
829
		//get expire time
830
		if ($expires === false) {
831
			$expires = TIME + 86400 * 5;
832
		}
833
		// send him the message
834
		self::doMessageSending(ACCOUNT_ID_GROUP_RACES + $raceID, $receiverID, $gameID, MSG_POLITICAL, $message, $expires);
835
	}
836
837
	public function setMessagesRead($messageTypeID) {
838
		$this->db->query('DELETE FROM player_has_unread_messages
839
							WHERE '.$this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
840
	}
841
842
	public function getSafeAttackRating() {
843
		return max(0, min(8, $this->getAlignment() / 150 + 4));
844
	}
845
846
	public function hasFederalProtection() {
847
		$sector = SmrSector::getSector($this->getGameID(), $this->getSectorID());
848
		if (!$sector->offersFederalProtection()) {
849
			return false;
850
		}
851
852
		$ship = $this->getShip();
853
		if ($ship->hasIllegalGoods()) {
854
			return false;
855
		}
856
857
		if ($ship->getAttackRating() <= $this->getSafeAttackRating()) {
858
			foreach ($sector->getFedRaceIDs() as $fedRaceID) {
859
				if ($this->canBeProtectedByRace($fedRaceID)) {
860
					return true;
861
				}
862
			}
863
		}
864
865
		return false;
866
	}
867
868
	public function canBeProtectedByRace($raceID) {
869
		if (!isset($this->canFed)) {
870
			$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...
871
			$RACES = Globals::getRaces();
872
			foreach ($RACES as $raceID2 => $raceName) {
873
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
874
			}
875
			$this->db->query('SELECT race_id, allowed FROM player_can_fed
876
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(TIME));
877
			while ($this->db->nextRecord()) {
878
				$this->canFed[$this->db->getInt('race_id')] = $this->db->getBoolean('allowed');
879
			}
880
		}
881
		return $this->canFed[$raceID];
882
	}
883
884
	/**
885
	 * Returns a boolean identifying if the player can currently
886
	 * participate in battles.
887
	 */
888
	public function canFight() {
889
		return !($this->hasNewbieTurns() ||
890
		         $this->isDead() ||
891
		         $this->isLandedOnPlanet() ||
892
		         $this->hasFederalProtection());
893
	}
894
895
	public function setDead($bool) {
896
		if ($this->dead == $bool) {
897
			return;
898
		}
899
		$this->dead = $bool;
900
		$this->hasChanged = true;
901
	}
902
903
	public function getKills() {
904
		return $this->kills;
905
	}
906
907
	public function increaseKills($kills) {
908
		if ($kills < 0) {
909
			throw new Exception('Trying to increase negative kills.');
910
		}
911
		$this->setKills($this->kills + $kills);
912
	}
913
914
	public function setKills($kills) {
915
		if ($this->kills == $kills) {
916
			return;
917
		}
918
		$this->kills = $kills;
919
		$this->hasChanged = true;
920
	}
921
922
	public function getDeaths() {
923
		return $this->deaths;
924
	}
925
926
	public function increaseDeaths($deaths) {
927
		if ($deaths < 0) {
928
			throw new Exception('Trying to increase negative deaths.');
929
		}
930
		$this->setDeaths($this->getDeaths() + $deaths);
931
	}
932
933
	public function setDeaths($deaths) {
934
		if ($this->deaths == $deaths) {
935
			return;
936
		}
937
		$this->deaths = $deaths;
938
		$this->hasChanged = true;
939
	}
940
941
	public function getAssists() {
942
		return $this->assists;
943
	}
944
945
	public function increaseAssists($assists) {
946
		if ($assists < 1) {
947
			throw new Exception('Must increase by a positive number.');
948
		}
949
		$this->assists += $assists;
950
		$this->hasChanged = true;
951
	}
952
953
	public function getAlignment() {
954
		return $this->alignment;
955
	}
956
957
	public function increaseAlignment($align) {
958
		if ($align < 0) {
959
			throw new Exception('Trying to increase negative align.');
960
		}
961
		if ($align == 0) {
962
			return;
963
		}
964
		$align += $this->alignment;
965
		$this->setAlignment($align);
966
	}
967
968
	public function decreaseAlignment($align) {
969
		if ($align < 0) {
970
			throw new Exception('Trying to decrease negative align.');
971
		}
972
		if ($align == 0) {
973
			return;
974
		}
975
		$align = $this->alignment - $align;
976
		$this->setAlignment($align);
977
	}
978
979
	public function setAlignment($align) {
980
		if ($this->alignment == $align) {
981
			return;
982
		}
983
		$this->alignment = $align;
984
		$this->hasChanged = true;
985
	}
986
987
	public function getCredits() {
988
		return $this->credits;
989
	}
990
991
	public function getBank() {
992
		return $this->bank;
993
	}
994
995
	public function increaseBank($credits) {
996
		if ($credits < 0) {
997
			throw new Exception('Trying to increase negative credits.');
998
		}
999
		if ($credits == 0) {
1000
			return;
1001
		}
1002
		$credits += $this->bank;
1003
		$this->setBank($credits);
1004
	}
1005
1006
	public function decreaseBank($credits) {
1007
		if ($credits < 0) {
1008
			throw new Exception('Trying to decrease negative credits.');
1009
		}
1010
		if ($credits == 0) {
1011
			return;
1012
		}
1013
		$credits = $this->bank - $credits;
1014
		$this->setBank($credits);
1015
	}
1016
1017
	public function setBank($credits) {
1018
		if ($this->bank == $credits) {
1019
			return;
1020
		}
1021
		if ($credits < 0) {
1022
			throw new Exception('Trying to set negative credits.');
1023
		}
1024
		if ($credits > MAX_MONEY) {
1025
			throw new Exception('Trying to set more than max credits.');
1026
		}
1027
		$this->bank = $credits;
1028
		$this->hasChanged = true;
1029
	}
1030
1031
	public function getExperience() {
1032
		return $this->experience;
1033
	}
1034
1035
	/**
1036
	 * Returns the percent progress towards the next level.
1037
	 * This value is rounded because it is used primarily in HTML img widths.
1038
	 */
1039
	public function getNextLevelPercentAcquired() : int {
1040
		if ($this->getNextLevelExperience() == $this->getThisLevelExperience()) {
1041
			return 100;
1042
		}
1043
		return max(0, min(100, IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
1044
	}
1045
1046
	public function getNextLevelPercentRemaining() {
1047
		return 100 - $this->getNextLevelPercentAcquired();
1048
	}
1049
1050
	public function getNextLevelExperience() {
1051
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1052
		if (!isset($LEVELS_REQUIREMENTS[$this->getLevelID() + 1])) {
1053
			return $this->getThisLevelExperience(); //Return current level experience if on last level.
1054
		}
1055
		return $LEVELS_REQUIREMENTS[$this->getLevelID() + 1]['Requirement'];
1056
	}
1057
1058
	public function getThisLevelExperience() {
1059
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1060
		return $LEVELS_REQUIREMENTS[$this->getLevelID()]['Requirement'];
1061
	}
1062
1063
	public function setExperience($experience) {
1064
		if ($this->experience == $experience) {
1065
			return;
1066
		}
1067
		if ($experience < MIN_EXPERIENCE) {
1068
			$experience = MIN_EXPERIENCE;
1069
		}
1070
		if ($experience > MAX_EXPERIENCE) {
1071
			$experience = MAX_EXPERIENCE;
1072
		}
1073
		$this->experience = $experience;
1074
		$this->hasChanged = true;
1075
1076
		// Since exp has changed, invalidate the player level so that it can
1077
		// be recomputed next time it is queried (in case it has changed).
1078
		$this->level = null;
1079
	}
1080
1081
	public function increaseCredits($credits) {
1082
		if ($credits < 0) {
1083
			throw new Exception('Trying to increase negative credits.');
1084
		}
1085
		if ($credits == 0) {
1086
			return;
1087
		}
1088
		$credits += $this->credits;
1089
		$this->setCredits($credits);
1090
	}
1091
	public function decreaseCredits($credits) {
1092
		if ($credits < 0) {
1093
			throw new Exception('Trying to decrease negative credits.');
1094
		}
1095
		if ($credits == 0) {
1096
			return;
1097
		}
1098
		$credits = $this->credits - $credits;
1099
		$this->setCredits($credits);
1100
	}
1101
	public function setCredits($credits) {
1102
		if ($this->credits == $credits) {
1103
			return;
1104
		}
1105
		if ($credits < 0) {
1106
			throw new Exception('Trying to set negative credits.');
1107
		}
1108
		if ($credits > MAX_MONEY) {
1109
			$credits = MAX_MONEY;
1110
		}
1111
		$this->credits = $credits;
1112
		$this->hasChanged = true;
1113
	}
1114
1115
	public function increaseExperience($experience) {
1116
		if ($experience < 0) {
1117
			throw new Exception('Trying to increase negative experience.');
1118
		}
1119
		if ($experience == 0) {
1120
			return;
1121
		}
1122
		$newExperience = $this->experience + $experience;
1123
		$this->setExperience($newExperience);
1124
		$this->increaseHOF($experience, array('Experience', 'Total', 'Gain'), HOF_PUBLIC);
1125
	}
1126
	public function decreaseExperience($experience) {
1127
		if ($experience < 0) {
1128
			throw new Exception('Trying to decrease negative experience.');
1129
		}
1130
		if ($experience == 0) {
1131
			return;
1132
		}
1133
		$newExperience = $this->experience - $experience;
1134
		$this->setExperience($newExperience);
1135
		$this->increaseHOF($experience, array('Experience', 'Total', 'Loss'), HOF_PUBLIC);
1136
	}
1137
1138
	public function isLandedOnPlanet() {
1139
		return $this->landedOnPlanet;
1140
	}
1141
1142
	public function setLandedOnPlanet($bool) {
1143
		if ($this->landedOnPlanet == $bool) {
1144
			return;
1145
		}
1146
		$this->landedOnPlanet = $bool;
1147
		$this->hasChanged = true;
1148
	}
1149
1150
	/**
1151
	 * Returns the numerical level of the player (e.g. 1-50).
1152
	 */
1153
	public function getLevelID() {
1154
		// The level is cached for performance reasons unless `setExperience`
1155
		// is called and the player's experience changes.
1156
		if ($this->level === null) {
1157
			$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1158
			foreach ($LEVELS_REQUIREMENTS as $level_id => $require) {
1159
				if ($this->getExperience() >= $require['Requirement']) {
1160
					continue;
1161
				}
1162
				$this->level = $level_id - 1;
1163
				return $this->level;
1164
			}
1165
			$this->level = max(array_keys($LEVELS_REQUIREMENTS));
1166
		}
1167
		return $this->level;
1168
	}
1169
1170
	public function getLevelName() {
1171
		$level_name = Globals::getLevelRequirements()[$this->getLevelID()]['Name'];
1172
		if ($this->isPresident()) {
1173
			$level_name = '<img src="images/council_president.png" title="' . Globals::getRaceName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1174
		}
1175
		return $level_name;
1176
	}
1177
1178
	public function getMaxLevel() {
1179
		return max(array_keys(Globals::getLevelRequirements()));
1180
	}
1181
1182
	public function getPlayerID() {
1183
		return $this->playerID;
1184
	}
1185
1186
	/**
1187
	 * Returns the player name.
1188
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1189
	 */
1190
	public function getPlayerName() {
1191
		return $this->playerName;
1192
	}
1193
1194
	public function setPlayerName($name) {
1195
		$this->playerName = $name;
1196
		$this->hasChanged = true;
1197
	}
1198
1199
	/**
1200
	 * Returns the decorated player name, suitable for HTML display.
1201
	 */
1202
	public function getDisplayName($includeAlliance = false) {
1203
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1204
		$return = get_colored_text($this->getAlignment(), $name);
1205
		if ($this->isNPC()) {
1206
			$return .= ' <span class="npcColour">[NPC]</span>';
1207
		}
1208
		if ($includeAlliance) {
1209
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1210
		}
1211
		return $return;
1212
	}
1213
1214
	public function getBBLink() {
1215
			return '[player=' . $this->getPlayerID() . ']';
1216
	}
1217
1218
	public function getLinkedDisplayName($includeAlliance = true) {
1219
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1220
		if ($includeAlliance) {
1221
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1222
		}
1223
		return $return;
1224
	}
1225
1226
	/**
1227
	 * Use this method when the player is changing their own name.
1228
	 * This will flag the player as having used their free name change.
1229
	 */
1230
	public function setPlayerNameByPlayer($playerName) {
1231
		$this->setPlayerName($playerName);
1232
		$this->setNameChanged(true);
1233
	}
1234
1235
	public function isNameChanged() {
1236
		return $this->nameChanged;
1237
	}
1238
1239
	public function setNameChanged($bool) {
1240
		$this->nameChanged = $bool;
1241
		$this->hasChanged = true;
1242
	}
1243
1244
	public function isRaceChanged() : bool {
1245
		return $this->raceChanged;
1246
	}
1247
1248
	public function setRaceChanged(bool $raceChanged) : void {
1249
		$this->raceChanged = $raceChanged;
1250
		$this->hasChanged = true;
1251
	}
1252
1253
	public function canChangeRace() : bool {
1254
		return !$this->isRaceChanged() && (TIME - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1255
	}
1256
1257
	public function getRaceID() {
1258
		return $this->raceID;
1259
	}
1260
1261
	public function getRaceName() {
1262
		return Globals::getRaceName($this->getRaceID());
1263
	}
1264
1265
	public static function getColouredRaceNameOrDefault($otherRaceID, AbstractSmrPlayer $player = null, $linked = false) {
1266
		$relations = 0;
1267
		if ($player !== null) {
1268
			$relations = $player->getRelation($otherRaceID);
1269
		}
1270
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1271
	}
1272
1273
	public function getColouredRaceName($otherRaceID, $linked = false) {
1274
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1275
	}
1276
1277
	public function setRaceID($raceID) {
1278
		if ($this->raceID == $raceID) {
1279
			return;
1280
		}
1281
		$this->raceID = $raceID;
1282
		$this->hasChanged = true;
1283
	}
1284
1285
	public function isAllianceLeader($forceUpdate = false) {
1286
		return $this->getAccountID() == $this->getAlliance($forceUpdate)->getLeaderID();
1287
	}
1288
1289
	public function getAlliance($forceUpdate = false) {
1290
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1291
	}
1292
1293
	public function getAllianceID() {
1294
		return $this->allianceID;
1295
	}
1296
1297
	public function hasAlliance() {
1298
		return $this->getAllianceID() != 0;
1299
	}
1300
1301
	protected function setAllianceID($ID) {
1302
		if ($this->allianceID == $ID) {
1303
			return;
1304
		}
1305
		$this->allianceID = $ID;
1306
		if ($this->allianceID != 0) {
1307
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1308
			$this->db->query('INSERT IGNORE INTO player_joined_alliance (account_id,game_id,alliance_id,status) ' .
1309
				'VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1310
		}
1311
		$this->hasChanged = true;
1312
	}
1313
1314
	public function getAllianceBBLink() {
1315
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1316
	}
1317
1318
	public function getAllianceDisplayName($linked = false, $includeAllianceID = false) {
1319
		if ($this->hasAlliance()) {
1320
			return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1321
		} else {
1322
			return 'No Alliance';
1323
		}
1324
	}
1325
1326
	public function getAllianceRole($allianceID = false) {
1327
		if ($allianceID === false) {
1328
			$allianceID = $this->getAllianceID();
1329
		}
1330
		if (!isset($this->allianceRoles[$allianceID])) {
1331
			$this->allianceRoles[$allianceID] = 0;
1332
			$this->db->query('SELECT role_id
1333
						FROM player_has_alliance_role
1334
						WHERE ' . $this->SQL . '
1335
						AND alliance_id=' . $this->db->escapeNumber($allianceID) . '
1336
						LIMIT 1');
1337
			if ($this->db->nextRecord()) {
1338
				$this->allianceRoles[$allianceID] = $this->db->getInt('role_id');
1339
			}
1340
		}
1341
		return $this->allianceRoles[$allianceID];
1342
	}
1343
1344
	public function leaveAlliance(AbstractSmrPlayer $kickedBy = null) {
1345
		$allianceID = $this->getAllianceID();
1346
		$alliance = $this->getAlliance();
1347
		if ($kickedBy != null) {
1348
			$kickedBy->sendMessage($this->getAccountID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1349
			$this->actionTaken('PlayerKicked', array('Alliance' => $alliance, 'Player' => $kickedBy));
1350
			$kickedBy->actionTaken('KickPlayer', array('Alliance' => $alliance, 'Player' => $this));
1351
		} elseif ($this->isAllianceLeader()) {
1352
			$this->actionTaken('DisbandAlliance', array('Alliance' => $alliance));
1353
		} else {
1354
			$this->actionTaken('LeaveAlliance', array('Alliance' => $alliance));
1355
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1356
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I left your alliance!', false);
1357
			}
1358
		}
1359
1360
		$this->setAllianceID(0);
1361
		$this->db->query('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1362
1363
		if (!$this->isAllianceLeader() && $allianceID != NHA_ID) { // Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1364
			$this->setAllianceJoinable(TIME + self::TIME_FOR_ALLIANCE_SWITCH);
1365
			$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.
1366
		}
1367
	}
1368
1369
	/**
1370
	 * Join an alliance (used for both Leader and New Member roles)
1371
	 */
1372
	public function joinAlliance($allianceID) {
1373
		$this->setAllianceID($allianceID);
1374
		$alliance = $this->getAlliance();
1375
1376
		if (!$this->isAllianceLeader()) {
1377
			// Do not throw an exception if the NHL account doesn't exist.
1378
			try {
1379
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I joined your alliance!', false);
1380
			} catch (AccountNotFoundException $e) {
1381
				if ($alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1382
					throw $e;
1383
				}
1384
			}
1385
1386
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1387
		} else {
1388
			$roleID = ALLIANCE_ROLE_LEADER;
1389
		}
1390
		$this->db->query('INSERT INTO player_has_alliance_role (game_id, account_id, role_id, alliance_id) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($roleID) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
1391
1392
		$this->actionTaken('JoinAlliance', array('Alliance' => $alliance));
1393
	}
1394
1395
	public function getAllianceJoinable() {
1396
		return $this->allianceJoinable;
1397
	}
1398
1399
	private function setAllianceJoinable($time) {
1400
		if ($this->allianceJoinable == $time) {
1401
			return;
1402
		}
1403
		$this->allianceJoinable = $time;
1404
		$this->hasChanged = true;
1405
	}
1406
1407
	/**
1408
	 * Invites player with $accountID to this player's alliance.
1409
	 */
1410
	public function sendAllianceInvitation(int $accountID, string $message, int $expires) : void {
1411
		if (!$this->hasAlliance()) {
1412
			throw new Exception('Must be in an alliance to send alliance invitations');
1413
		}
1414
		// Send message to invited player
1415
		$messageID = $this->sendMessage($accountID, MSG_PLAYER, $message, false, true, $expires, true);
1416
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), $messageID, $expires);
1417
	}
1418
1419
	public function isCombatDronesKamikazeOnMines() {
1420
		return $this->combatDronesKamikazeOnMines;
1421
	}
1422
1423
	public function setCombatDronesKamikazeOnMines($bool) {
1424
		if ($this->combatDronesKamikazeOnMines == $bool) {
1425
			return;
1426
		}
1427
		$this->combatDronesKamikazeOnMines = $bool;
1428
		$this->hasChanged = true;
1429
	}
1430
1431
	protected function getPureRelationsData() {
1432
		if (!isset($this->pureRelations)) {
1433
			//get relations
1434
			$RACES = Globals::getRaces();
1435
			$this->pureRelations = array();
1436
			foreach ($RACES as $raceID => $raceName) {
1437
				$this->pureRelations[$raceID] = 0;
1438
			}
1439
			$this->db->query('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL . ' LIMIT ' . count($RACES));
1440
			while ($this->db->nextRecord()) {
1441
				$this->pureRelations[$this->db->getInt('race_id')] = $this->db->getInt('relation');
1442
			}
1443
		}
1444
	}
1445
1446
	public function getPureRelations() {
1447
		$this->getPureRelationsData();
1448
		return $this->pureRelations;
1449
	}
1450
1451
	/**
1452
	 * Get personal relations with a race
1453
	 */
1454
	public function getPureRelation($raceID) {
1455
		$rels = $this->getPureRelations();
1456
		return $rels[$raceID];
1457
	}
1458
1459
	public function getRelations() {
1460
		if (!isset($this->relations)) {
1461
			//get relations
1462
			$RACES = Globals::getRaces();
1463
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1464
			$pureRels = $this->getPureRelations(); // make sure they're initialised.
1465
			$this->relations = array();
1466
			foreach ($RACES as $raceID => $raceName) {
1467
				$this->relations[$raceID] = $pureRels[$raceID] + $raceRelations[$raceID];
1468
			}
1469
		}
1470
		return $this->relations;
1471
	}
1472
1473
	/**
1474
	 * Get total relations with a race (personal + political)
1475
	 */
1476
	public function getRelation($raceID) {
1477
		$rels = $this->getRelations();
1478
		return $rels[$raceID];
1479
	}
1480
1481
	/**
1482
	 * Increases personal relations from trading $numGoods units with the race
1483
	 * of the port given by $raceID.
1484
	 */
1485
	public function increaseRelationsByTrade($numGoods, $raceID) {
1486
		$relations = ICeil(min($numGoods, 300) / 30);
1487
		//Cap relations to a max of 1 after 500 have been reached
1488
		if ($this->getPureRelation($raceID) + $relations >= 500) {
1489
			$relations = max(1, min($relations, 500 - $this->getPureRelation($raceID)));
1490
		}
1491
		$this->increaseRelations($relations, $raceID);
1492
	}
1493
1494
	public function decreaseRelationsByTrade($numGoods, $raceID) {
1495
		$relations = ICeil(min($numGoods, 300) / 30);
1496
		$this->decreaseRelations($relations, $raceID);
1497
	}
1498
1499
	public function increaseRelations($relations, $raceID) {
1500
		if ($relations < 0) {
1501
			throw new Exception('Trying to increase negative relations.');
1502
		}
1503
		if ($relations == 0) {
1504
			return;
1505
		}
1506
		$relations += $this->getPureRelation($raceID);
1507
		$this->setRelations($relations, $raceID);
1508
	}
1509
1510
	public function decreaseRelations($relations, $raceID) {
1511
		if ($relations < 0) {
1512
			throw new Exception('Trying to decrease negative relations.');
1513
		}
1514
		if ($relations == 0) {
1515
			return;
1516
		}
1517
		$relations = $this->getPureRelation($raceID) - $relations;
1518
		$this->setRelations($relations, $raceID);
1519
	}
1520
1521
	public function setRelations($relations, $raceID) {
1522
		$this->getRelations();
1523
		if ($this->pureRelations[$raceID] == $relations) {
1524
			return;
1525
		}
1526
		if ($relations < MIN_RELATIONS) {
1527
			$relations = MIN_RELATIONS;
1528
		}
1529
		$relationsDiff = IRound($relations - $this->pureRelations[$raceID]);
1530
		$this->pureRelations[$raceID] = $relations;
1531
		$this->relations[$raceID] += $relationsDiff;
1532
		$this->db->query('REPLACE INTO player_has_relation (account_id,game_id,race_id,relation) values (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($raceID) . ',' . $this->db->escapeNumber($this->pureRelations[$raceID]) . ')');
1533
	}
1534
1535
	/**
1536
	 * Set any starting personal relations bonuses or penalties.
1537
	 */
1538
	public function giveStartingRelations() {
1539
		if ($this->getRaceID() === RACE_ALSKANT) {
1540
			// Give Alskants 250 personal relations to start.
1541
			foreach (Globals::getRaces() as $raceID => $raceInfo) {
1542
				$this->setRelations(250, $raceID);
1543
			}
1544
		}
1545
	}
1546
1547
	public function getLastNewsUpdate() {
1548
		return $this->lastNewsUpdate;
1549
	}
1550
1551
	private function setLastNewsUpdate($time) {
1552
		if ($this->lastNewsUpdate == $time) {
1553
			return;
1554
		}
1555
		$this->lastNewsUpdate = $time;
1556
		$this->hasChanged = true;
1557
	}
1558
1559
	public function updateLastNewsUpdate() {
1560
		$this->setLastNewsUpdate(TIME);
1561
	}
1562
1563
	public function getLastPort() {
1564
		return $this->lastPort;
1565
	}
1566
1567
	public function setLastPort($lastPort) {
1568
		if ($this->lastPort == $lastPort) {
1569
			return;
1570
		}
1571
		$this->lastPort = $lastPort;
1572
		$this->hasChanged = true;
1573
	}
1574
1575
	public function getPlottedCourse() {
1576
		if (!isset($this->plottedCourse)) {
1577
			// check if we have a course plotted
1578
			$this->db->query('SELECT course FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1579
1580
			if ($this->db->nextRecord()) {
1581
				// get the course back
1582
				$this->plottedCourse = unserialize($this->db->getField('course'));
1583
			} else {
1584
				$this->plottedCourse = false;
1585
			}
1586
		}
1587
1588
		// Update the plotted course if we have moved since the last query
1589
		if ($this->plottedCourse !== false && (!isset($this->plottedCourseFrom) || $this->plottedCourseFrom != $this->getSectorID())) {
1590
			$this->plottedCourseFrom = $this->getSectorID();
1591
1592
			if ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1593
				// We have walked into the next sector of the course
1594
				$this->plottedCourse->followPath();
1595
				$this->setPlottedCourse($this->plottedCourse);
1596
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1597
				// We have skipped to some later sector in the course
1598
				$this->plottedCourse->skipToSector($this->getSectorID());
1599
				$this->setPlottedCourse($this->plottedCourse);
1600
			}
1601
		}
1602
		return $this->plottedCourse;
1603
	}
1604
1605
	public function setPlottedCourse(Distance $plottedCourse) {
1606
		$hadPlottedCourse = $this->hasPlottedCourse();
1607
		$this->plottedCourse = $plottedCourse;
1608
		if ($this->plottedCourse->getTotalSectors() > 0) {
1609
			$this->db->query('REPLACE INTO player_plotted_course
1610
				(account_id, game_id, course)
1611
				VALUES(' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeBinary(serialize($this->plottedCourse)) . ')');
1612
		} elseif ($hadPlottedCourse) {
1613
			$this->deletePlottedCourse();
1614
		}
1615
	}
1616
1617
	public function hasPlottedCourse() {
1618
		return $this->getPlottedCourse() !== false;
1619
	}
1620
1621
	public function isPartOfCourse($sectorOrSectorID) {
1622
		if (!$this->hasPlottedCourse()) {
1623
			return false;
1624
		}
1625
		if ($sectorOrSectorID instanceof SmrSector) {
1626
			$sectorID = $sectorOrSectorID->getSectorID();
1627
		} else {
1628
			$sectorID = $sectorOrSectorID;
1629
		}
1630
		return $this->getPlottedCourse()->isInPath($sectorID);
1631
	}
1632
1633
	public function deletePlottedCourse() {
1634
		$this->plottedCourse = false;
1635
		$this->db->query('DELETE FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1636
	}
1637
1638
	// Computes the turn cost and max misjump between current and target sector
1639
	public function getJumpInfo(SmrSector $targetSector) {
1640
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1641
		if ($path === false) {
1642
			create_error('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1643
		}
1644
		$distance = $path->getRelativeDistance();
1645
1646
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
1647
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1648
		return array('turn_cost' => $turnCost, 'max_misjump' => $maxMisjump);
1649
	}
1650
1651
	public function __sleep() {
1652
		return array('accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName');
1653
	}
1654
1655
	public function &getStoredDestinations() {
1656
		if (!isset($this->storedDestinations)) {
1657
			$this->storedDestinations = array();
1658
			$this->db->query('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1659
			while ($this->db->nextRecord()) {
1660
				$this->storedDestinations[] = array(
1661
					'Label' => $this->db->getField('label'),
1662
					'SectorID' => $this->db->getInt('sector_id'),
1663
					'OffsetTop' => $this->db->getInt('offset_top'),
1664
					'OffsetLeft' => $this->db->getInt('offset_left')
1665
				);
1666
			}
1667
		}
1668
		return $this->storedDestinations;
1669
	}
1670
1671
	public function moveDestinationButton($sectorID, $offsetTop, $offsetLeft) {
1672
1673
		if (!is_numeric($offsetLeft) || !is_numeric($offsetTop)) {
1674
			create_error('The position of the saved sector must be numeric!.');
1675
		}
1676
		$offsetTop = round($offsetTop);
1677
		$offsetLeft = round($offsetLeft);
1678
1679
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1680
			create_error('The saved sector must be in the box!');
1681
		}
1682
1683
		$storedDestinations =& $this->getStoredDestinations();
1684
		foreach ($storedDestinations as &$sd) {
1685
			if ($sd['SectorID'] == $sectorID) {
1686
				$sd['OffsetTop'] = $offsetTop;
1687
				$sd['OffsetLeft'] = $offsetLeft;
1688
				$this->db->query('
1689
					UPDATE player_stored_sector
1690
						SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1691
					WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID)
1692
				);
1693
				return true;
1694
			}
1695
		}
1696
1697
		create_error('You do not have a saved sector for #' . $sectorID);
1698
	}
1699
1700
	public function addDestinationButton($sectorID, $label) {
1701
1702
		if (!is_numeric($sectorID) || !SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1703
			create_error('You want to add a non-existent sector?');
1704
		}
1705
1706
		// sector already stored ?
1707
		foreach ($this->getStoredDestinations() as $sd) {
1708
			if ($sd['SectorID'] == $sectorID) {
1709
				create_error('Sector already stored!');
1710
			}
1711
		}
1712
1713
		$this->storedDestinations[] = array(
1714
			'Label' => $label,
1715
			'SectorID' => (int)$sectorID,
1716
			'OffsetTop' => 1,
1717
			'OffsetLeft' => 1
1718
		);
1719
1720
		$this->db->query('
1721
			INSERT INTO player_stored_sector (account_id, game_id, sector_id, label, offset_top, offset_left)
1722
			VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($sectorID) . ',' . $this->db->escapeString($label, true) . ',1,1)'
1723
		);
1724
	}
1725
1726
	public function deleteDestinationButton($sectorID) {
1727
		if (!is_numeric($sectorID) || $sectorID < 1) {
1728
			create_error('You want to remove a non-existent sector?');
1729
		}
1730
1731
		foreach ($this->getStoredDestinations() as $key => $sd) {
1732
			if ($sd['SectorID'] == $sectorID) {
1733
				$this->db->query('
1734
					DELETE FROM player_stored_sector
1735
					WHERE ' . $this->SQL . '
1736
					AND sector_id = ' . $this->db->escapeNumber($sectorID)
1737
				);
1738
				unset($this->storedDestinations[$key]);
1739
				return true;
1740
			}
1741
		}
1742
		return false;
1743
	}
1744
1745
	public function getTickers() {
1746
		if (!isset($this->tickers)) {
1747
			$this->tickers = array();
1748
			//get ticker info
1749
			$this->db->query('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(TIME));
1750
			while ($this->db->nextRecord()) {
1751
				$this->tickers[$this->db->getField('type')] = [
1752
					'Type' => $this->db->getField('type'),
1753
					'Time' => $this->db->getInt('time'),
1754
					'Expires' => $this->db->getInt('expires'),
1755
					'Recent' => $this->db->getField('recent'),
1756
				];
1757
			}
1758
		}
1759
		return $this->tickers;
1760
	}
1761
1762
	public function hasTickers() {
1763
		return count($this->getTickers()) > 0;
1764
	}
1765
1766
	public function getTicker($tickerType) {
1767
		$tickers = $this->getTickers();
1768
		if (isset($tickers[$tickerType])) {
1769
			return $tickers[$tickerType];
1770
		}
1771
		return false;
1772
	}
1773
1774
	public function hasTicker($tickerType) {
1775
		return $this->getTicker($tickerType) !== false;
1776
	}
1777
1778
	public function &shootPlayer(AbstractSmrPlayer $targetPlayer) {
1779
		return $this->getShip()->shootPlayer($targetPlayer);
1780
	}
1781
1782
	public function &shootForces(SmrForce $forces) {
1783
		return $this->getShip()->shootForces($forces);
1784
	}
1785
1786
	public function &shootPort(SmrPort $port) {
1787
		return $this->getShip()->shootPort($port);
1788
	}
1789
1790
	public function &shootPlanet(SmrPlanet $planet, $delayed) {
1791
		return $this->getShip()->shootPlanet($planet, $delayed);
1792
	}
1793
1794
	public function &shootPlayers(array $targetPlayers) {
1795
		return $this->getShip()->shootPlayers($targetPlayers);
1796
	}
1797
1798
	public function getMilitaryPayment() {
1799
		return $this->militaryPayment;
1800
	}
1801
1802
	public function hasMilitaryPayment() {
1803
		return $this->getMilitaryPayment() > 0;
1804
	}
1805
1806
	public function setMilitaryPayment($amount) {
1807
		if ($this->militaryPayment == $amount) {
1808
			return;
1809
		}
1810
		$this->militaryPayment = $amount;
1811
		$this->hasChanged = true;
1812
	}
1813
1814
	public function increaseMilitaryPayment($amount) {
1815
		if ($amount < 0) {
1816
			throw new Exception('Trying to increase negative military payment.');
1817
		}
1818
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1819
	}
1820
1821
	public function decreaseMilitaryPayment($amount) {
1822
		if ($amount < 0) {
1823
			throw new Exception('Trying to decrease negative military payment.');
1824
		}
1825
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1826
	}
1827
1828
	protected function getBountiesData() {
1829
		if (!isset($this->bounties)) {
1830
			$this->bounties = array();
1831
			$this->db->query('SELECT * FROM bounty WHERE ' . $this->SQL);
1832
			while ($this->db->nextRecord()) {
1833
				$this->bounties[$this->db->getInt('bounty_id')] = array(
1834
							'Amount' => $this->db->getInt('amount'),
1835
							'SmrCredits' => $this->db->getInt('smr_credits'),
1836
							'Type' => $this->db->getField('type'),
1837
							'Claimer' => $this->db->getInt('claimer_id'),
1838
							'Time' => $this->db->getInt('time'),
1839
							'ID' => $this->db->getInt('bounty_id'),
1840
							'New' => false);
1841
			}
1842
		}
1843
	}
1844
1845
	// Get bounties that can be claimed by this player
1846
	// Type must be 'HQ' or 'UG'
1847
	public function getClaimableBounties($type) {
1848
		$bounties = array();
1849
		$this->db->query('SELECT * FROM bounty WHERE claimer_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND type=' . $this->db->escapeString($type));
1850
		while ($this->db->nextRecord()) {
1851
			$bounties[] = array(
1852
				'player' => SmrPlayer::getPlayer($this->db->getInt('account_id'), $this->getGameID()),
1853
				'bounty_id' => $this->db->getInt('bounty_id'),
1854
				'credits' => $this->db->getInt('amount'),
1855
				'smr_credits' => $this->db->getInt('smr_credits'),
1856
			);
1857
		}
1858
		return $bounties;
1859
	}
1860
1861
	public function getBounties() : array {
1862
		$this->getBountiesData();
1863
		return $this->bounties;
1864
	}
1865
1866
	public function hasBounties() : bool {
1867
		return count($this->getBounties()) > 0;
1868
	}
1869
1870
	protected function getBounty(int $bountyID) : array {
1871
		if (!$this->hasBounty($bountyID)) {
1872
			throw new Exception('BountyID does not exist: ' . $bountyID);
1873
		}
1874
		return $this->bounties[$bountyID];
1875
	}
1876
1877
	public function hasBounty(int $bountyID) : bool {
1878
		$bounties = $this->getBounties();
1879
		return isset($bounties[$bountyID]);
1880
	}
1881
1882
	protected function getBountyAmount(int $bountyID) : int {
1883
		$bounty = $this->getBounty($bountyID);
1884
		return $bounty['Amount'];
1885
	}
1886
1887
	protected function createBounty(string $type) : array {
1888
		$bounty = array('Amount' => 0,
1889
						'SmrCredits' => 0,
1890
						'Type' => $type,
1891
						'Claimer' => 0,
1892
						'Time' => TIME,
1893
						'ID' => $this->getNextBountyID(),
1894
						'New' => true);
1895
		$this->setBounty($bounty);
1896
		return $bounty;
1897
	}
1898
1899
	protected function getNextBountyID() : int {
1900
		$keys = array_keys($this->getBounties());
1901
		if (count($keys) > 0) {
1902
			return max($keys) + 1;
1903
		} else {
1904
			return 0;
1905
		}
1906
	}
1907
1908
	protected function setBounty(array $bounty) : void {
1909
		$this->bounties[$bounty['ID']] = $bounty;
1910
		$this->hasBountyChanged[$bounty['ID']] = true;
1911
	}
1912
1913
	protected function setBountyAmount(int $bountyID, int $amount) : void {
1914
		$bounty = $this->getBounty($bountyID);
1915
		$bounty['Amount'] = $amount;
1916
		$this->setBounty($bounty);
1917
	}
1918
1919
	public function getCurrentBounty(string $type) : array {
1920
		$bounties = $this->getBounties();
1921
		foreach ($bounties as $bounty) {
1922
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1923
				return $bounty;
1924
			}
1925
		}
1926
		return $this->createBounty($type);
1927
	}
1928
1929
	public function hasCurrentBounty(string $type) : bool {
1930
		$bounties = $this->getBounties();
1931
		foreach ($bounties as $bounty) {
1932
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1933
				return true;
1934
			}
1935
		}
1936
		return false;
1937
	}
1938
1939
	protected function getCurrentBountyAmount(string $type) : int {
1940
		$bounty = $this->getCurrentBounty($type);
1941
		return $bounty['Amount'];
1942
	}
1943
1944
	protected function setCurrentBountyAmount(string $type, int $amount) : void {
1945
		$bounty = $this->getCurrentBounty($type);
1946
		if ($bounty['Amount'] == $amount) {
1947
			return;
1948
		}
1949
		$bounty['Amount'] = $amount;
1950
		$this->setBounty($bounty);
1951
	}
1952
1953
	public function increaseCurrentBountyAmount(string $type, int $amount) : void {
1954
		if ($amount < 0) {
1955
			throw new Exception('Trying to increase negative current bounty.');
1956
		}
1957
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) + $amount);
1958
	}
1959
1960
	public function decreaseCurrentBountyAmount(string $type, int $amount) : void {
1961
		if ($amount < 0) {
1962
			throw new Exception('Trying to decrease negative current bounty.');
1963
		}
1964
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) - $amount);
1965
	}
1966
1967
	protected function getCurrentBountySmrCredits(string $type) : int {
1968
		$bounty = $this->getCurrentBounty($type);
1969
		return $bounty['SmrCredits'];
1970
	}
1971
1972
	protected function setCurrentBountySmrCredits(string $type, int $credits) : void {
1973
		$bounty = $this->getCurrentBounty($type);
1974
		if ($bounty['SmrCredits'] == $credits) {
1975
			return;
1976
		}
1977
		$bounty['SmrCredits'] = $credits;
1978
		$this->setBounty($bounty);
1979
	}
1980
1981
	public function increaseCurrentBountySmrCredits(string $type, int $credits) : void {
1982
		if ($credits < 0) {
1983
			throw new Exception('Trying to increase negative current bounty.');
1984
		}
1985
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) + $credits);
1986
	}
1987
1988
	public function decreaseCurrentBountySmrCredits(string $type, int $credits) : void {
1989
		if ($credits < 0) {
1990
			throw new Exception('Trying to decrease negative current bounty.');
1991
		}
1992
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) - $credits);
1993
	}
1994
1995
	public function setBountiesClaimable(AbstractSmrPlayer $claimer) : void {
1996
		foreach ($this->getBounties() as $bounty) {
1997
			if ($bounty['Claimer'] == 0) {
1998
				$bounty['Claimer'] = $claimer->getAccountID();
1999
				$this->setBounty($bounty);
2000
			}
2001
		}
2002
	}
2003
2004
	protected function getHOFData() {
2005
		if (!isset($this->HOF)) {
2006
			//Get Player HOF
2007
			$this->db->query('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2008
			$this->HOF = array();
2009
			while ($this->db->nextRecord()) {
2010
				$hof =& $this->HOF;
2011
				$typeList = explode(':', $this->db->getField('type'));
2012
				foreach ($typeList as $type) {
2013
					if (!isset($hof[$type])) {
2014
						$hof[$type] = array();
2015
					}
2016
					$hof =& $hof[$type];
2017
				}
2018
				$hof = $this->db->getFloat('amount');
2019
			}
2020
			self::getHOFVis();
2021
		}
2022
	}
2023
2024
	public static function getHOFVis() {
2025
		if (!isset(self::$HOFVis)) {
2026
			//Get Player HOF Vis
2027
			$db = new SmrMySqlDatabase();
2028
			$db->query('SELECT type,visibility FROM hof_visibility');
2029
			self::$HOFVis = array();
2030
			while ($db->nextRecord()) {
2031
				self::$HOFVis[$db->getField('type')] = $db->getField('visibility');
2032
			}
2033
		}
2034
	}
2035
2036
	public function getHOF(array $typeList = null) {
2037
		$this->getHOFData();
2038
		if ($typeList == null) {
2039
			return $this->HOF;
2040
		}
2041
		$hof = $this->HOF;
2042
		foreach ($typeList as $type) {
2043
			if (!isset($hof[$type])) {
2044
				return 0;
2045
			}
2046
			$hof = $hof[$type];
2047
		}
2048
		return $hof;
2049
	}
2050
2051
	public function increaseHOF($amount, array $typeList, $visibility) {
2052
		if ($amount < 0) {
2053
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2054
		}
2055
		if ($amount == 0) {
2056
			return;
2057
		}
2058
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2059
	}
2060
2061
	public function decreaseHOF($amount, array $typeList, $visibility) {
2062
		if ($amount < 0) {
2063
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2064
		}
2065
		if ($amount == 0) {
2066
			return;
2067
		}
2068
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2069
	}
2070
2071
	public function setHOF($amount, array $typeList, $visibility) {
2072
		if (is_array($this->getHOF($typeList))) {
2073
			throw new Exception('Trying to overwrite a HOF type: ' . implode(':', $typeList));
2074
		}
2075
		if ($this->isNPC()) {
2076
			// Don't store HOF for NPCs.
2077
			return;
2078
		}
2079
		if ($this->getHOF($typeList) == $amount) {
2080
			return;
2081
		}
2082
		if ($amount < 0) {
2083
			$amount = 0;
2084
		}
2085
		$this->getHOF();
2086
2087
		$hofType = implode(':', $typeList);
2088
		if (!isset(self::$HOFVis[$hofType])) {
2089
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2090
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2091
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2092
		}
2093
		self::$HOFVis[$hofType] = $visibility;
2094
2095
		$hof =& $this->HOF;
2096
		$hofChanged =& $this->hasHOFChanged;
2097
		$new = false;
2098
		foreach ($typeList as $type) {
2099
			if (!isset($hofChanged[$type])) {
2100
				$hofChanged[$type] = array();
2101
			}
2102
			if (!isset($hof[$type])) {
2103
				$hof[$type] = array();
2104
				$new = true;
2105
			}
2106
			$hof =& $hof[$type];
2107
			$hofChanged =& $hofChanged[$type];
2108
		}
2109
		if ($hofChanged == null) {
2110
			$hofChanged = self::HOF_CHANGED;
2111
			if ($new) {
2112
				$hofChanged = self::HOF_NEW;
2113
			}
2114
		}
2115
		$hof = $amount;
2116
	}
2117
2118
	public function getExperienceRank() {
2119
		return $this->computeRanking('experience', $this->getExperience());
2120
	}
2121
2122
	public function getKillsRank() {
2123
		return $this->computeRanking('kills', $this->getKills());
2124
	}
2125
2126
	public function getDeathsRank() {
2127
		return $this->computeRanking('deaths', $this->getDeaths());
2128
	}
2129
2130
	public function getAssistsRank() {
2131
		return $this->computeRanking('assists', $this->getAssists());
2132
	}
2133
2134
	private function computeRanking($dbField, $playerAmount) {
2135
		$this->db->query('SELECT count(*) FROM player
2136
			WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . '
2137
			AND (
2138
				'.$dbField . ' > ' . $this->db->escapeNumber($playerAmount) . '
2139
				OR (
2140
					'.$dbField . ' = ' . $this->db->escapeNumber($playerAmount) . '
2141
					AND player_name <= ' . $this->db->escapeString($this->getPlayerName()) . '
2142
				)
2143
			)');
2144
		$this->db->nextRecord();
2145
		$rank = $this->db->getInt('count(*)');
2146
		return $rank;
2147
	}
2148
2149
	public function killPlayer($sectorID) {
2150
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2151
		//msg taken care of in trader_att_proc.php
2152
		// forget plotted course
2153
		$this->deletePlottedCourse();
2154
2155
		$sector->diedHere($this);
2156
2157
		// if we are in an alliance we increase their deaths
2158
		if ($this->hasAlliance()) {
2159
			$this->db->query('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2160
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()) . ' LIMIT 1');
2161
		}
2162
2163
		// record death stat
2164
		$this->increaseHOF(1, array('Dying', 'Deaths'), HOF_PUBLIC);
2165
		//record cost of ship lost
2166
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2167
		// reset turns since last death
2168
		$this->setHOF(0, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2169
2170
		// 1/4 of ship value -> insurance
2171
		$newCredits = IRound($this->getShip()->getCost() / 4);
2172
		if ($newCredits < 100000) {
2173
			$newCredits = 100000;
2174
		}
2175
		$this->setCredits($newCredits);
2176
2177
		$this->setSectorID($this->getHome());
2178
		$this->increaseDeaths(1);
2179
		$this->setLandedOnPlanet(false);
2180
		$this->setDead(true);
2181
		$this->setNewbieWarning(true);
2182
		$this->getShip()->getPod($this->hasNewbieStatus());
2183
		$this->setNewbieTurns(100);
2184
	}
2185
2186
	public function &killPlayerByPlayer(AbstractSmrPlayer $killer) {
2187
		$return = array();
2188
		$msg = $this->getBBLink();
2189
2190
		if ($this->hasCustomShipName()) {
2191
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2192
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2193
		}
2194
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2195
		if ($killer->hasCustomShipName()) {
2196
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2197
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2198
		}
2199
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2200
		$this->getSector()->increaseBattles(1);
2201
		$this->db->query('INSERT INTO news (game_id,time,news_message,type,killer_id,killer_alliance,dead_id,dead_alliance) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber(TIME) . ',' . $this->db->escapeString($msg, true) . ',\'regular\',' . $this->db->escapeNumber($killer->getAccountID()) . ',' . $this->db->escapeNumber($killer->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2202
2203
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2204
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getAccountID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2205
2206
		// Dead player loses between 5% and 25% experience
2207
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2208
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2209
		$this->decreaseExperience($return['DeadExp']);
2210
2211
		// Killer gains 50% of the lost exp
2212
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
2213
		$killer->increaseExperience($return['KillerExp']);
2214
2215
		$return['KillerCredits'] = $this->getCredits();
2216
		$killer->increaseCredits($return['KillerCredits']);
2217
2218
		// The killer may change alignment
2219
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2220
		$relation = $relations[$killer->getRaceID()];
2221
2222
		$alignChangePerRelation = 0.1;
2223
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2224
			$alignChangePerRelation = 0.04;
2225
		}
2226
2227
		$return['KillerAlign'] = -$relation * $alignChangePerRelation; //Lose relations when killing a peaceful race
2228
		if ($return['KillerAlign'] > 0) {
2229
			$killer->increaseAlignment($return['KillerAlign']);
2230
		} else {
2231
			$killer->decreaseAlignment(-$return['KillerAlign']);
2232
		}
2233
		// War setting gives them military pay
2234
		if ($relation <= RELATIONS_WAR) {
2235
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2236
		}
2237
2238
		//check for federal bounty being offered for current port raiders;
2239
		$this->db->query('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(TIME - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2240
		$query = 'SELECT 1
2241
					FROM player_attacks_port
2242
					JOIN port USING(game_id, sector_id)
2243
					JOIN player USING(game_id, account_id)
2244
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2245
		$this->db->query($query);
2246
		if ($this->db->nextRecord()) {
2247
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2248
			$this->increaseCurrentBountyAmount('HQ', $bounty);
2249
		}
2250
2251
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2252
		$this->setBountiesClaimable($killer);
2253
2254
		// If the alignment difference is greater than 200 then a bounty may be set
2255
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2256
		$return['BountyGained'] = array(
2257
			'Type' => 'None',
2258
			'Amount' => 0
2259
		);
2260
		if ($alignmentDiff >= 200) {
2261
			// If the podded players alignment makes them deputy or member then set bounty
2262
			if ($this->getAlignment() >= 100) {
2263
				$return['BountyGained']['Type'] = 'HQ';
2264
			} elseif ($this->getAlignment() <= 100) {
2265
				$return['BountyGained']['Type'] = 'UG';
2266
			}
2267
2268
			if ($return['BountyGained']['Type'] != 'None') {
2269
				$return['BountyGained']['Amount'] = IFloor(pow($alignmentDiff, 2.56));
2270
				$killer->increaseCurrentBountyAmount($return['BountyGained']['Type'], $return['BountyGained']['Amount']);
2271
			}
2272
		}
2273
2274
		if ($this->isNPC()) {
2275
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'NPC', 'Experience', 'Gained'), HOF_PUBLIC);
2276
			$killer->increaseHOF($this->getExperience(), array('Killing', 'NPC', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2277
2278
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By NPCs Killed'), HOF_PUBLIC);
2279
2280
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2281
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Gain'), HOF_PUBLIC);
2282
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'NPC', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2283
2284
			if ($return['KillerAlign'] > 0) {
2285
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Gain'), HOF_PUBLIC);
2286
			} else {
2287
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Loss'), HOF_PUBLIC);
2288
			}
2289
2290
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'NPC', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2291
2292
			$killer->increaseHOF(1, array('Killing', 'NPC Kills'), HOF_PUBLIC);
2293
		} else {
2294
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'Experience', 'Gained'), HOF_PUBLIC);
2295
			$killer->increaseHOF($this->getExperience(), array('Killing', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2296
2297
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By Traders Killed'), HOF_PUBLIC);
2298
2299
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2300
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Gain'), HOF_PUBLIC);
2301
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2302
2303
			if ($return['KillerAlign'] > 0) {
2304
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'Alignment', 'Gain'), HOF_PUBLIC);
2305
			} else {
2306
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'Alignment', 'Loss'), HOF_PUBLIC);
2307
			}
2308
2309
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2310
2311
			if ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2312
				$killer->increaseHOF(1, array('Killing', 'Newbie Kills'), HOF_PUBLIC);
2313
			} else {
2314
				$killer->increaseKills(1);
2315
				$killer->increaseHOF(1, array('Killing', 'Kills'), HOF_PUBLIC);
2316
2317
				if ($killer->hasAlliance()) {
2318
					$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');
2319
				}
2320
2321
				// alliance vs. alliance stats
2322
				$this->incrementAllianceVsDeaths($killer->getAllianceID());
2323
			}
2324
		}
2325
2326
		$this->increaseHOF($return['BountyGained']['Amount'], array('Dying', 'Players', 'Money', 'Bounty Gained By Killer'), HOF_PUBLIC);
2327
		$this->increaseHOF($return['KillerExp'], array('Dying', 'Players', 'Experience', 'Gained By Killer'), HOF_PUBLIC);
2328
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2329
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Players', 'Experience', 'Lost'), HOF_PUBLIC);
2330
		$this->increaseHOF($return['KillerCredits'], array('Dying', 'Players', 'Money Lost'), HOF_PUBLIC);
2331
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Players', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2332
		$this->increaseHOF(1, array('Dying', 'Players', 'Deaths'), HOF_PUBLIC);
2333
2334
		$this->killPlayer($this->getSectorID());
2335
		return $return;
2336
	}
2337
2338
	public function &killPlayerByForces(SmrForce $forces) {
2339
		$return = array();
2340
		$owner = $forces->getOwner();
2341
		// send a message to the person who died
2342
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getAccountID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2343
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2344
2345
		$news_message = $this->getBBLink();
2346
		if ($this->hasCustomShipName()) {
2347
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2348
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2349
		}
2350
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2351
		// insert the news entry
2352
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2353
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($owner->getAccountID()) . ',' . $this->db->escapeNumber($owner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2354
2355
		// Player loses 15% experience
2356
		$expLossPercentage = .15;
2357
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
2358
		$this->decreaseExperience($return['DeadExp']);
2359
2360
		$return['LostCredits'] = $this->getCredits();
2361
2362
		// alliance vs. alliance stats
2363
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2364
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2365
2366
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2367
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Forces', 'Experience Lost'), HOF_PUBLIC);
2368
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Forces', 'Money Lost'), HOF_PUBLIC);
2369
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Forces', 'Cost Of Ships Lost'), HOF_PUBLIC);
2370
		$this->increaseHOF(1, array('Dying', 'Forces', 'Deaths'), HOF_PUBLIC);
2371
2372
		$this->killPlayer($forces->getSectorID());
2373
		return $return;
2374
	}
2375
2376
	public function &killPlayerByPort(SmrPort $port) {
2377
		$return = array();
2378
		// send a message to the person who died
2379
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2380
2381
		$news_message = $this->getBBLink();
2382
		if ($this->hasCustomShipName()) {
2383
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2384
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2385
		}
2386
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2387
		// insert the news entry
2388
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,dead_id,dead_alliance)
2389
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber(ACCOUNT_ID_PORT) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2390
2391
		// Player loses between 15% and 20% experience
2392
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2393
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2394
		$this->decreaseExperience($return['DeadExp']);
2395
2396
		$return['LostCredits'] = $this->getCredits();
2397
2398
		// alliance vs. alliance stats
2399
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2400
2401
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2402
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Ports', 'Experience Lost'), HOF_PUBLIC);
2403
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Ports', 'Money Lost'), HOF_PUBLIC);
2404
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Ports', 'Cost Of Ships Lost'), HOF_PUBLIC);
2405
		$this->increaseHOF(1, array('Dying', 'Ports', 'Deaths'), HOF_PUBLIC);
2406
2407
		$this->killPlayer($port->getSectorID());
2408
		return $return;
2409
	}
2410
2411
	public function &killPlayerByPlanet(SmrPlanet $planet) {
2412
		$return = array();
2413
		// send a message to the person who died
2414
		$planetOwner = $planet->getOwner();
2415
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getAccountID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2416
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2417
2418
		$news_message = $this->getBBLink();
2419
		if ($this->hasCustomShipName()) {
2420
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2421
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2422
		}
2423
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2424
		// insert the news entry
2425
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2426
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($planetOwner->getAccountID()) . ',' . $this->db->escapeNumber($planetOwner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2427
2428
		// Player loses between 15% and 20% experience
2429
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2430
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2431
		$this->decreaseExperience($return['DeadExp']);
2432
2433
		$return['LostCredits'] = $this->getCredits();
2434
2435
		// alliance vs. alliance stats
2436
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2437
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2438
2439
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2440
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Planets', 'Experience Lost'), HOF_PUBLIC);
2441
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Planets', 'Money Lost'), HOF_PUBLIC);
2442
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Planets', 'Cost Of Ships Lost'), HOF_PUBLIC);
2443
		$this->increaseHOF(1, array('Dying', 'Planets', 'Deaths'), HOF_PUBLIC);
2444
2445
		$this->killPlayer($planet->getSectorID());
2446
		return $return;
2447
	}
2448
2449
	public function incrementAllianceVsKills($otherID) {
2450
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2451
		$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');
2452
	}
2453
2454
	public function incrementAllianceVsDeaths($otherID) {
2455
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2456
		$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');
2457
	}
2458
2459
	public function getTurnsLevel() {
2460
		if (!$this->hasTurns()) {
2461
			return 'NONE';
2462
		}
2463
		if ($this->getTurns() <= 25) {
2464
			return 'LOW';
2465
		}
2466
		if ($this->getTurns() <= 75) {
2467
			return 'MEDIUM';
2468
		}
2469
		return 'HIGH';
2470
	}
2471
2472
	/**
2473
	 * Returns the CSS class color to use when displaying the player's turns
2474
	 */
2475
	public function getTurnsColor() {
2476
		switch ($this->getTurnsLevel()) {
2477
			case 'NONE':
2478
			case 'LOW':
2479
				return 'red';
2480
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
2481
			case 'MEDIUM':
2482
				return 'yellow';
2483
			break;
2484
			default:
2485
				return 'green';
2486
		}
2487
	}
2488
2489
	public function getTurns() {
2490
		return $this->turns;
2491
	}
2492
2493
	public function hasTurns() {
2494
		return $this->turns > 0;
2495
	}
2496
2497
	public function getMaxTurns() {
2498
		return $this->getGame()->getMaxTurns();
2499
	}
2500
2501
	public function setTurns($turns) {
2502
		if ($this->turns == $turns) {
2503
			return;
2504
		}
2505
		// Make sure turns are in range [0, MaxTurns]
2506
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2507
		$this->hasChanged = true;
2508
	}
2509
2510
	public function takeTurns($take, $takeNewbie = 0) {
2511
		if ($take < 0 || $takeNewbie < 0) {
2512
			throw new Exception('Trying to take negative turns.');
2513
		}
2514
		$take = ICeil($take);
2515
		// Only take up to as many newbie turns as we have remaining
2516
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2517
2518
		$this->setTurns($this->getTurns() - $take);
2519
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2520
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2521
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Total'), HOF_ALLIANCE);
2522
		$this->increaseHOF($takeNewbie, array('Movement', 'Turns Used', 'Newbie'), HOF_ALLIANCE);
2523
2524
		// Player has taken an action
2525
		$this->setLastActive(TIME);
2526
		$this->updateLastCPLAction();
2527
	}
2528
2529
	public function giveTurns(int $give, $giveNewbie = 0) {
2530
		if ($give < 0 || $giveNewbie < 0) {
2531
			throw new Exception('Trying to give negative turns.');
2532
		}
2533
		$this->setTurns($this->getTurns() + $give);
2534
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2535
	}
2536
2537
	/**
2538
	 * Calculate the time in seconds between the given time and when the
2539
	 * player will be at max turns.
2540
	 */
2541
	public function getTimeUntilMaxTurns($time, $forceUpdate = false) {
2542
		$timeDiff = $time - $this->getLastTurnUpdate();
2543
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2544
		$ship = $this->getShip($forceUpdate);
2545
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
2546
		// If already at max turns, return 0
2547
		return max(0, $maxTurnsTime);
2548
	}
2549
2550
	/**
2551
	 * Grant the player their starting turns.
2552
	 */
2553
	public function giveStartingTurns() {
2554
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
2555
		$this->giveTurns($startTurns);
2556
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2557
	}
2558
2559
	// Turns only update when player is active.
2560
	// Calculate turns gained between given time and the last turn update
2561
	public function getTurnsGained($time, $forceUpdate = false) : int {
2562
		$timeDiff = $time - $this->getLastTurnUpdate();
2563
		$ship = $this->getShip($forceUpdate);
2564
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
2565
		return $extraTurns;
2566
	}
2567
2568
	public function updateTurns() {
2569
		// is account validated?
2570
		if (!$this->getAccount()->isValidated()) {
2571
			return;
2572
		}
2573
2574
		// how many turns would he get right now?
2575
		$extraTurns = $this->getTurnsGained(TIME);
2576
2577
		// do we have at least one turn to give?
2578
		if ($extraTurns > 0) {
2579
			// recalc the time to avoid rounding errors
2580
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
2581
			$this->setLastTurnUpdate($newLastTurnUpdate);
2582
			$this->giveTurns($extraTurns);
2583
		}
2584
	}
2585
2586
	public function getLastTurnUpdate() {
2587
		return $this->lastTurnUpdate;
2588
	}
2589
2590
	public function setLastTurnUpdate($time) {
2591
		if ($this->lastTurnUpdate == $time) {
2592
			return;
2593
		}
2594
		$this->lastTurnUpdate = $time;
2595
		$this->hasChanged = true;
2596
	}
2597
2598
	public function getLastActive() {
2599
		return $this->lastActive;
2600
	}
2601
2602
	public function setLastActive($lastActive) {
2603
		if ($this->lastActive == $lastActive) {
2604
			return;
2605
		}
2606
		$this->lastActive = $lastActive;
2607
		$this->hasChanged = true;
2608
	}
2609
2610
	public function getLastCPLAction() {
2611
		return $this->lastCPLAction;
2612
	}
2613
2614
	public function setLastCPLAction($time) {
2615
		if ($this->lastCPLAction == $time) {
2616
			return;
2617
		}
2618
		$this->lastCPLAction = $time;
2619
		$this->hasChanged = true;
2620
	}
2621
2622
	public function updateLastCPLAction() {
2623
		$this->setLastCPLAction(TIME);
2624
	}
2625
2626
	public function setNewbieWarning($bool) {
2627
		if ($this->newbieWarning == $bool) {
2628
			return;
2629
		}
2630
		$this->newbieWarning = $bool;
2631
		$this->hasChanged = true;
2632
	}
2633
2634
	public function getNewbieWarning() {
2635
		return $this->newbieWarning;
2636
	}
2637
2638
	public function isDisplayMissions() {
2639
		return $this->displayMissions;
2640
	}
2641
2642
	public function setDisplayMissions($bool) {
2643
		if ($this->displayMissions == $bool) {
2644
			return;
2645
		}
2646
		$this->displayMissions = $bool;
2647
		$this->hasChanged = true;
2648
	}
2649
2650
	public function getMissions() {
2651
		if (!isset($this->missions)) {
2652
			$this->db->query('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2653
			$this->missions = array();
2654
			while ($this->db->nextRecord()) {
2655
				$missionID = $this->db->getInt('mission_id');
2656
				$this->missions[$missionID] = array(
2657
					'On Step' => $this->db->getInt('on_step'),
2658
					'Progress' => $this->db->getInt('progress'),
2659
					'Unread' => $this->db->getBoolean('unread'),
2660
					'Expires' => $this->db->getInt('step_fails'),
2661
					'Sector' => $this->db->getInt('mission_sector'),
2662
					'Starting Sector' => $this->db->getInt('starting_sector')
2663
				);
2664
				$this->rebuildMission($missionID);
2665
			}
2666
		}
2667
		return $this->missions;
2668
	}
2669
2670
	public function getActiveMissions() {
2671
		$missions = $this->getMissions();
2672
		foreach ($missions as $missionID => $mission) {
2673
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2674
				unset($missions[$missionID]);
2675
			}
2676
		}
2677
		return $missions;
2678
	}
2679
2680
	protected function getMission($missionID) {
2681
		$missions = $this->getMissions();
2682
		if (isset($missions[$missionID])) {
2683
			return $missions[$missionID];
2684
		}
2685
		return false;
2686
	}
2687
2688
	protected function hasMission($missionID) {
2689
		return $this->getMission($missionID) !== false;
2690
	}
2691
2692
	protected function updateMission($missionID) {
2693
		$this->getMissions();
2694
		if (isset($this->missions[$missionID])) {
2695
			$mission = $this->missions[$missionID];
2696
			$this->db->query('
2697
				UPDATE player_has_mission
2698
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2699
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2700
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2701
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2702
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2703
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2704
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1'
2705
			);
2706
			return true;
2707
		}
2708
		return false;
2709
	}
2710
2711
	private function setupMissionStep($missionID) {
2712
		$mission =& $this->missions[$missionID];
2713
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2714
			// Nothing to do if this mission is already completed
2715
			return;
2716
		}
2717
		$step = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2718
		if (isset($step['PickSector'])) {
2719
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2720
			if ($realX === false) {
2721
				throw new Exception('Invalid PickSector definition in mission: ' . $missionID);
2722
			}
2723
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2724
			if ($path === false) {
2725
				throw new Exception('Cannot find location: ' . $missionID);
2726
			}
2727
			$mission['Sector'] = $path->getEndSectorID();
2728
		}
2729
	}
2730
2731
	/**
2732
	 * Declining a mission will permanently hide it from the player
2733
	 * by adding it in its completed state.
2734
	 */
2735
	public function declineMission($missionID) {
2736
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2737
		$this->addMission($missionID, $finishedStep);
2738
	}
2739
2740
	public function addMission($missionID, $step = 0) {
2741
		$this->getMissions();
2742
2743
		if (isset($this->missions[$missionID])) {
2744
			return;
2745
		}
2746
		$sector = 0;
2747
2748
		$mission = array(
2749
			'On Step' => $step,
2750
			'Progress' => 0,
2751
			'Unread' => true,
2752
			'Expires' => (TIME + 86400),
2753
			'Sector' => $sector,
2754
			'Starting Sector' => $this->getSectorID()
2755
		);
2756
2757
		$this->missions[$missionID] =& $mission;
2758
		$this->setupMissionStep($missionID);
2759
		$this->rebuildMission($missionID);
2760
2761
		$this->db->query('
2762
			REPLACE INTO player_has_mission (game_id,account_id,mission_id,on_step,progress,unread,starting_sector,mission_sector,step_fails)
2763
			VALUES ('.$this->db->escapeNumber($this->gameID) . ',' . $this->db->escapeNumber($this->accountID) . ',' . $this->db->escapeNumber($missionID) . ',' . $this->db->escapeNumber($mission['On Step']) . ',' . $this->db->escapeNumber($mission['Progress']) . ',' . $this->db->escapeBoolean($mission['Unread']) . ',' . $this->db->escapeNumber($mission['Starting Sector']) . ',' . $this->db->escapeNumber($mission['Sector']) . ',' . $this->db->escapeNumber($mission['Expires']) . ')'
2764
		);
2765
	}
2766
2767
	private function rebuildMission($missionID) {
2768
		$mission = $this->missions[$missionID];
2769
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2770
2771
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2772
			// If we have completed this mission just use false to indicate no current task.
2773
			$currentStep = false;
2774
		} else {
2775
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2776
			$currentStep['Text'] = str_replace(array('<Race>', '<Sector>', '<Starting Sector>', '<trader>'), array($this->getRaceID(), $mission['Sector'], $mission['Starting Sector'], $this->playerName), $currentStep['Text']);
2777
			if (isset($currentStep['Task'])) {
2778
				$currentStep['Task'] = str_replace(array('<Race>', '<Sector>', '<Starting Sector>', '<trader>'), array($this->getRaceID(), $mission['Sector'], $mission['Starting Sector'], $this->playerName), $currentStep['Task']);
2779
			}
2780
			if (isset($currentStep['Level'])) {
2781
				$currentStep['Level'] = str_replace('<Player Level>', $this->getLevelID(), $currentStep['Level']);
2782
			} else {
2783
				$currentStep['Level'] = 0;
2784
			}
2785
		}
2786
		$this->missions[$missionID]['Task'] = $currentStep;
2787
	}
2788
2789
	public function deleteMission($missionID) {
2790
		$this->getMissions();
2791
		if (isset($this->missions[$missionID])) {
2792
			unset($this->missions[$missionID]);
2793
			$this->db->query('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1');
2794
			return true;
2795
		}
2796
		return false;
2797
	}
2798
2799
	public function markMissionsRead() {
2800
		$this->getMissions();
2801
		$unreadMissions = array();
2802
		foreach ($this->missions as $missionID => &$mission) {
2803
			if ($mission['Unread']) {
2804
				$unreadMissions[] = $missionID;
2805
				$mission['Unread'] = false;
2806
				$this->updateMission($missionID);
2807
			}
2808
		}
2809
		return $unreadMissions;
2810
	}
2811
2812
	public function claimMissionReward($missionID) {
2813
		$this->getMissions();
2814
		$mission =& $this->missions[$missionID];
2815
		if ($mission === false) {
2816
			throw new Exception('Unknown mission: ' . $missionID);
2817
		}
2818
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2819
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2820
		}
2821
		$mission['On Step']++;
2822
		$mission['Unread'] = true;
2823
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2824
			switch ($rewardItem) {
2825
				case 'Credits':
2826
					$this->increaseCredits($amount);
2827
				break;
2828
				case 'Experience':
2829
					$this->increaseExperience($amount);
2830
				break;
2831
			}
2832
		}
2833
		$rewardText = $mission['Task']['Rewards']['Text'];
2834
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2835
			// If we haven't finished the mission yet then 
2836
			$this->setupMissionStep($missionID);
2837
		}
2838
		$this->rebuildMission($missionID);
2839
		$this->updateMission($missionID);
2840
		return $rewardText;
2841
	}
2842
2843
	public function getAvailableMissions() {
2844
		$availableMissions = array();
2845
		foreach (MISSIONS as $missionID => $mission) {
2846
			if ($this->hasMission($missionID)) {
2847
				continue;
2848
			}
2849
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2850
			if ($realX === false) {
2851
				throw new Exception('Invalid HasX definition in mission: ' . $missionID);
2852
			}
2853
			if ($this->getSector()->hasX($realX)) {
2854
				$availableMissions[$missionID] = $mission;
2855
			}
2856
		}
2857
		return $availableMissions;
2858
	}
2859
2860
	public function actionTaken($actionID, array $values) {
2861
		if (!in_array($actionID, MISSION_ACTIONS)) {
2862
			throw new Exception('Unknown action: ' . $actionID);
2863
		}
2864
// TODO: Reenable this once tested.		if($this->getAccount()->isLoggingEnabled())
2865
			switch ($actionID) {
2866
				case 'WalkSector':
2867
					$this->getAccount()->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID(), $this->getSectorID());
2868
				break;
2869
				case 'JoinAlliance':
2870
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName(), $this->getSectorID());
2871
				break;
2872
				case 'LeaveAlliance':
2873
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName(), $this->getSectorID());
2874
				break;
2875
				case 'DisbandAlliance':
2876
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName(), $this->getSectorID());
2877
				break;
2878
				case 'KickPlayer':
2879
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName(), 0);
2880
				break;
2881
				case 'PlayerKicked':
2882
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')', 0);
2883
				break;
2884
2885
			}
2886
		$this->getMissions();
2887
		foreach ($this->missions as $missionID => &$mission) {
2888
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
2889
				if (checkMissionRequirements($values, $mission, $this) === true) {
2890
					$mission['On Step']++;
2891
					$mission['Unread'] = true;
2892
					$this->setupMissionStep($missionID);
2893
					$this->rebuildMission($missionID);
2894
					$this->updateMission($missionID);
2895
				}
2896
			}
2897
		}
2898
	}
2899
2900
	public function canSeeAny(array $otherPlayerArray) {
2901
		foreach ($otherPlayerArray as $otherPlayer) {
2902
			if ($this->canSee($otherPlayer)) {
2903
				return true;
2904
			}
2905
		}
2906
		return false;
2907
	}
2908
2909
	public function canSee(AbstractSmrPlayer $otherPlayer) {
2910
		if (!$otherPlayer->getShip()->isCloaked()) {
2911
			return true;
2912
		}
2913
		if ($this->sameAlliance($otherPlayer)) {
2914
			return true;
2915
		}
2916
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
2917
			return true;
2918
		}
2919
		return false;
2920
	}
2921
2922
	public function equals(AbstractSmrPlayer $otherPlayer = null) {
2923
		return $otherPlayer !== null && $this->getAccountID() == $otherPlayer->getAccountID() && $this->getGameID() == $otherPlayer->getGameID();
2924
	}
2925
2926
	public function sameAlliance(AbstractSmrPlayer $otherPlayer = null) {
2927
		return $this->equals($otherPlayer) || (!is_null($otherPlayer) && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
2928
	}
2929
2930
	public function sharedForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2931
		return $this->sameAlliance($otherPlayer);
2932
	}
2933
2934
	public function forceNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2935
		return $this->sameAlliance($otherPlayer);
2936
	}
2937
2938
	public function planetNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2939
		return $this->sameAlliance($otherPlayer);
2940
	}
2941
2942
	public function traderNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2943
		return $this->sameAlliance($otherPlayer);
2944
	}
2945
2946
	public function traderMAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2947
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
2948
	}
2949
2950
	public function traderAttackTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2951
		return $this->sameAlliance($otherPlayer);
2952
	}
2953
2954
	public function traderDefendTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2955
		return $this->sameAlliance($otherPlayer);
2956
	}
2957
2958
	public function traderAttackForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2959
		return $this->sameAlliance($otherPlayer);
2960
	}
2961
2962
	public function traderAttackPortAlliance(AbstractSmrPlayer $otherPlayer = null) {
2963
		return $this->sameAlliance($otherPlayer);
2964
	}
2965
2966
	public function traderAttackPlanetAlliance(AbstractSmrPlayer $otherPlayer = null) {
2967
		return $this->sameAlliance($otherPlayer);
2968
	}
2969
2970
	public function meetsAlignmentRestriction($restriction) {
2971
		if ($restriction < 0) {
2972
			return $this->getAlignment() <= $restriction;
2973
		}
2974
		if ($restriction > 0) {
2975
			return $this->getAlignment() >= $restriction;
2976
		}
2977
		return true;
2978
	}
2979
2980
	// Get an array of goods that are visible to the player
2981
	public function getVisibleGoods() {
2982
		$goods = Globals::getGoods();
2983
		$visibleGoods = array();
2984
		foreach ($goods as $key => $good) {
2985
			if ($this->meetsAlignmentRestriction($good['AlignRestriction'])) {
2986
				$visibleGoods[$key] = $good;
2987
			}
2988
		}
2989
		return $visibleGoods;
2990
	}
2991
2992
	/**
2993
	 * Will retrieve all visited sectors, use only when you are likely to check a large number of these
2994
	 */
2995
	public function hasVisitedSector($sectorID) {
2996
		if (!isset($this->visitedSectors)) {
2997
			$this->visitedSectors = array();
2998
			$this->db->query('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
2999
			while ($this->db->nextRecord()) {
3000
				$this->visitedSectors[$this->db->getInt('sector_id')] = false;
3001
			}
3002
		}
3003
		return !isset($this->visitedSectors[$sectorID]);
3004
	}
3005
3006
	public function getLeaveNewbieProtectionHREF() {
3007
		return SmrSession::getNewHREF(create_container('leave_newbie_processing.php'));
3008
	}
3009
3010
	public function getExamineTraderHREF() {
3011
		$container = create_container('skeleton.php', 'trader_examine.php');
3012
		$container['target'] = $this->getAccountID();
3013
		return SmrSession::getNewHREF($container);
3014
	}
3015
3016
	public function getAttackTraderHREF() {
3017
		return Globals::getAttackTraderHREF($this->getAccountID());
3018
	}
3019
3020
	public function getPlanetKickHREF() {
3021
		$container = create_container('planet_kick_processing.php', 'trader_attack_processing.php');
3022
		$container['account_id'] = $this->getAccountID();
3023
		return SmrSession::getNewHREF($container);
3024
	}
3025
3026
	public function getTraderSearchHREF() {
3027
		$container = create_container('skeleton.php', 'trader_search_result.php');
3028
		$container['player_id'] = $this->getPlayerID();
3029
		return SmrSession::getNewHREF($container);
3030
	}
3031
3032
	public function getAllianceRosterHREF() {
3033
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3034
	}
3035
3036
	public function getToggleWeaponHidingHREF($ajax = false) {
3037
		$container = create_container('toggle_processing.php');
3038
		$container['toggle'] = 'WeaponHiding';
3039
		$container['AJAX'] = $ajax;
3040
		return SmrSession::getNewHREF($container);
3041
	}
3042
3043
	public function isDisplayWeapons() {
3044
		return $this->displayWeapons;
3045
	}
3046
3047
	/**
3048
	 * Should weapons be displayed in the right panel?
3049
	 * This updates the player database directly because it is used with AJAX,
3050
	 * which does not acquire a sector lock.
3051
	 */
3052
	public function setDisplayWeapons($bool) {
3053
		if ($this->displayWeapons == $bool) {
3054
			return;
3055
		}
3056
		$this->displayWeapons = $bool;
3057
		$this->db->query('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3058
	}
3059
3060
	public function update() {
3061
		$this->save();
3062
	}
3063
3064
	public function save() {
3065
		if ($this->hasChanged === true) {
3066
			$this->db->query('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3067
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3068
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3069
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3070
				', turns=' . $this->db->escapeNumber($this->turns) .
3071
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3072
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3073
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3074
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3075
				', dead=' . $this->db->escapeBoolean($this->dead) .
3076
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3077
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3078
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3079
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3080
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3081
				', credits=' . $this->db->escapeNumber($this->credits) .
3082
				', experience=' . $this->db->escapeNumber($this->experience) .
3083
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3084
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3085
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3086
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3087
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3088
				', kills=' . $this->db->escapeNumber($this->kills) .
3089
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3090
				', assists=' . $this->db->escapeNumber($this->assists) .
3091
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3092
				', bank=' . $this->db->escapeNumber($this->bank) .
3093
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3094
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3095
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3096
				', group_scout_messages=' . $this->db->escapeString($this->groupScoutMessages) .
3097
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3098
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3099
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3100
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3101
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3102
				' WHERE ' . $this->SQL . ' LIMIT 1');
3103
			$this->hasChanged = false;
3104
		}
3105
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3106
			if ($bountyChanged === true) {
3107
				$bountyChanged = false;
3108
				$bounty = $this->getBounty($key);
3109
				if ($bounty['New'] === true) {
3110
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3111
						$this->db->query('INSERT INTO bounty (account_id,game_id,type,amount,smr_credits,claimer_id,time) VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeString($bounty['Type']) . ',' . $this->db->escapeNumber($bounty['Amount']) . ',' . $this->db->escapeNumber($bounty['SmrCredits']) . ',' . $this->db->escapeNumber($bounty['Claimer']) . ',' . $this->db->escapeNumber($bounty['Time']) . ')');
3112
					}
3113
				} else {
3114
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3115
						$this->db->query('UPDATE bounty
3116
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3117
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3118
							type=' . $this->db->escapeString($bounty['Type']) . ',
3119
							claimer_id=' . $this->db->escapeNumber($bounty['Claimer']) . ',
3120
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3121
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3122
					} else {
3123
						$this->db->query('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3124
					}
3125
				}
3126
			}
3127
		}
3128
		$this->saveHOF();
3129
	}
3130
3131
	public function saveHOF() {
3132
		if (count($this->hasHOFChanged) > 0) {
3133
			$this->doHOFSave($this->hasHOFChanged);
3134
			$this->hasHOFChanged = [];
3135
		}
3136
		if (!empty(self::$hasHOFVisChanged)) {
3137
			foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3138
				if ($changeType == self::HOF_NEW) {
3139
					$this->db->query('INSERT INTO hof_visibility (type, visibility) VALUES (' . $this->db->escapeString($hofType) . ',' . $this->db->escapeString(self::$HOFVis[$hofType]) . ')');
3140
				} else {
3141
					$this->db->query('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType) . ' LIMIT 1');
3142
				}
3143
				unset(self::$hasHOFVisChanged[$hofType]);
3144
			}
3145
		}
3146
	}
3147
3148
	/**
3149
	 * This should only be called by `saveHOF` (and recursively) to
3150
	 * ensure that the `hasHOFChanged` attribute is properly cleared.
3151
	 */
3152
	protected function doHOFSave(array $hasChangedList, array $typeList = array()) {
3153
		foreach ($hasChangedList as $type => $hofChanged) {
3154
			$tempTypeList = $typeList;
3155
			$tempTypeList[] = $type;
3156
			if (is_array($hofChanged)) {
3157
				$this->doHOFSave($hofChanged, $tempTypeList);
3158
			} else {
3159
				$amount = $this->getHOF($tempTypeList);
3160
				if ($hofChanged == self::HOF_NEW) {
3161
					if ($amount > 0) {
3162
						$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) . ')');
3163
					}
3164
				} elseif ($hofChanged == self::HOF_CHANGED) {
3165
					$this->db->query('UPDATE player_hof
3166
						SET amount=' . $this->db->escapeNumber($amount) . '
3167
						WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeArray($tempTypeList, false, true, ':', false) . ' LIMIT 1');
3168
				}
3169
			}
3170
		}
3171
	}
3172
3173
}
3174