Scrutinizer GitHub App not installed

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

Install GitHub App

Passed
Push — master ( 44453e...299ad6 )
by Dan
04:44
created

AbstractSmrPlayer::doMessageSending()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 49
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

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

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