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

Failed Conditions
Pull Request — master (#1094)
by Dan
04:47
created

AbstractSmrPlayer::getUnvisitedSectors()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 10
rs 10
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 array $CACHE_SECTOR_PLAYERS = [];
19
	protected static array $CACHE_PLANET_PLAYERS = [];
20
	protected static array $CACHE_ALLIANCE_PLAYERS = [];
21
	protected static array $CACHE_PLAYERS = [];
22
23
	protected Smr\Database $db;
24
	protected string $SQL;
25
26
	protected int $accountID;
27
	protected int $gameID;
28
	protected string $playerName;
29
	protected int $playerID;
30
	protected int $sectorID;
31
	protected int $lastSectorID;
32
	protected int $newbieTurns;
33
	protected bool $dead;
34
	protected bool $npc = false; // initialized for legacy combat logs
35
	protected bool $newbieStatus;
36
	protected bool $newbieWarning;
37
	protected bool $landedOnPlanet;
38
	protected int $lastActive;
39
	protected int $credits;
40
	protected int $alignment;
41
	protected int $experience;
42
	protected ?int $level;
43
	protected int $allianceID;
44
	protected int $shipID;
45
	protected int $kills;
46
	protected int $deaths;
47
	protected int $assists;
48
	protected array $personalRelations;
49
	protected array $relations;
50
	protected int $militaryPayment;
51
	protected array $bounties;
52
	protected int $turns;
53
	protected int $lastCPLAction;
54
	protected array $missions;
55
56
	protected array $tickers;
57
	protected int $lastTurnUpdate;
58
	protected int $lastNewsUpdate;
59
	protected string $attackColour;
60
	protected int $allianceJoinable;
61
	protected int $lastPort;
62
	protected int $bank;
63
	protected int $zoom;
64
	protected bool $displayMissions;
65
	protected bool $displayWeapons;
66
	protected bool $forceDropMessages;
67
	protected string $groupScoutMessages;
68
	protected bool $ignoreGlobals;
69
	protected Distance|false $plottedCourse;
70
	protected int $plottedCourseFrom;
71
	protected bool $nameChanged;
72
	protected bool $raceChanged;
73
	protected bool $combatDronesKamikazeOnMines;
74
	protected string|false $customShipName;
75
	protected array $storedDestinations;
76
	protected array $canFed;
77
	protected bool $underAttack;
78
79
	protected array $unvisitedSectors;
80
	protected array $allianceRoles = array(
81
		0 => 0
82
	);
83
84
	protected bool $draftLeader;
85
	protected string|false $gpWriter;
86
	protected array $HOF;
87
	protected static array $HOFVis;
88
89
	protected bool $hasChanged = false;
90
	protected array $hasHOFChanged = [];
91
	protected static array $hasHOFVisChanged = [];
92
	protected array $hasBountyChanged = [];
93
94
	public static function refreshCache() : void {
95
		foreach (self::$CACHE_PLAYERS as $gameID => &$gamePlayers) {
96
			foreach ($gamePlayers as $accountID => &$player) {
97
				$player = self::getPlayer($accountID, $gameID, true);
98
			}
99
		}
100
	}
101
102
	public static function clearCache() : void {
103
		self::$CACHE_PLAYERS = array();
104
		self::$CACHE_SECTOR_PLAYERS = array();
105
	}
106
107
	public static function savePlayers() : void {
108
		foreach (self::$CACHE_PLAYERS as $gamePlayers) {
109
			foreach ($gamePlayers as $player) {
110
				$player->save();
111
			}
112
		}
113
	}
114
115
	public static function getSectorPlayersByAlliances(int $gameID, int $sectorID, array $allianceIDs, bool $forceUpdate = false) : array {
116
		$players = self::getSectorPlayers($gameID, $sectorID, $forceUpdate); // Don't use & as we do an unset
117
		foreach ($players as $accountID => $player) {
118
			if (!in_array($player->getAllianceID(), $allianceIDs)) {
119
				unset($players[$accountID]);
120
			}
121
		}
122
		return $players;
123
	}
124
125
	/**
126
	 * Returns the same players as getSectorPlayers (e.g. not on planets),
127
	 * but for an entire galaxy rather than a single sector. This is useful
128
	 * for reducing the number of queries in galaxy-wide processing.
129
	 */
130
	public static function getGalaxyPlayers(int $gameID, int $galaxyID, bool $forceUpdate = false) : array {
131
		$db = Smr\Database::getInstance();
132
		$dbResult = $db->read('SELECT player.* FROM player LEFT JOIN sector USING(game_id, sector_id) WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(Smr\Epoch::time() - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND galaxy_id = ' . $db->escapeNumber($galaxyID));
133
		$galaxyPlayers = [];
134
		foreach ($dbResult->records() as $dbRecord) {
135
			$sectorID = $dbRecord->getInt('sector_id');
136
			$accountID = $dbRecord->getInt('account_id');
137
			$player = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
138
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$accountID] = $player;
139
			$galaxyPlayers[$sectorID][$accountID] = $player;
140
		}
141
		return $galaxyPlayers;
142
	}
143
144
	public static function getSectorPlayers(int $gameID, int $sectorID, bool $forceUpdate = false) : array {
145
		if ($forceUpdate || !isset(self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID])) {
146
			$db = Smr\Database::getInstance();
147
			$dbResult = $db->read('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(Smr\Epoch::time() - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
148
			$players = array();
149
			foreach ($dbResult->records() as $dbRecord) {
150
				$accountID = $dbRecord->getInt('account_id');
151
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
152
			}
153
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = $players;
154
		}
155
		return self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID];
156
	}
157
158
	public static function getPlanetPlayers(int $gameID, int $sectorID, bool $forceUpdate = false) : array {
159
		if ($forceUpdate || !isset(self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID])) {
160
			$db = Smr\Database::getInstance();
161
			$dbResult = $db->read('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');
162
			$players = array();
163
			foreach ($dbResult->records() as $dbRecord) {
164
				$accountID = $dbRecord->getInt('account_id');
165
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
166
			}
167
			self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID] = $players;
168
		}
169
		return self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID];
170
	}
171
172
	public static function getAlliancePlayers(int $gameID, int $allianceID, bool $forceUpdate = false) : array {
173
		if ($forceUpdate || !isset(self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID])) {
174
			$db = Smr\Database::getInstance();
175
			$dbResult = $db->read('SELECT * FROM player WHERE alliance_id = ' . $db->escapeNumber($allianceID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' ORDER BY experience DESC');
176
			$players = array();
177
			foreach ($dbResult->records() as $dbRecord) {
178
				$accountID = $dbRecord->getInt('account_id');
179
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
180
			}
181
			self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID] = $players;
182
		}
183
		return self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID];
184
	}
185
186
	public static function getPlayer(int $accountID, int $gameID, bool $forceUpdate = false, Smr\DatabaseRecord $dbRecord = null) : self {
187
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$accountID])) {
188
			self::$CACHE_PLAYERS[$gameID][$accountID] = new SmrPlayer($gameID, $accountID, $dbRecord);
189
		}
190
		return self::$CACHE_PLAYERS[$gameID][$accountID];
191
	}
192
193
	public static function getPlayerByPlayerID(int $playerID, int $gameID, bool $forceUpdate = false) : self {
194
		$db = Smr\Database::getInstance();
195
		$dbResult = $db->read('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_id = ' . $db->escapeNumber($playerID) . ' LIMIT 1');
196
		if ($dbResult->hasRecord()) {
197
			$dbRecord = $dbResult->record();
198
			return self::getPlayer($dbRecord->getInt('account_id'), $gameID, $forceUpdate, $dbRecord);
199
		}
200
		throw new PlayerNotFoundException('Player ID not found.');
201
	}
202
203
	public static function getPlayerByPlayerName(string $playerName, int $gameID, bool $forceUpdate = false) : self {
204
		$db = Smr\Database::getInstance();
205
		$dbResult = $db->read('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
206
		if ($dbResult->hasRecord()) {
207
			$dbRecord = $dbResult->record();
208
			return self::getPlayer($dbRecord->getInt('account_id'), $gameID, $forceUpdate, $dbRecord);
209
		}
210
		throw new PlayerNotFoundException('Player Name not found.');
211
	}
212
213
	protected function __construct(int $gameID, int $accountID, Smr\DatabaseRecord $dbRecord = null) {
214
		$this->db = Smr\Database::getInstance();
215
		$this->SQL = 'account_id = ' . $this->db->escapeNumber($accountID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
216
217
		if ($dbRecord === null) {
218
			$dbResult = $this->db->read('SELECT * FROM player WHERE ' . $this->SQL . ' LIMIT 1');
219
			if ($dbResult->hasRecord()) {
220
				$dbRecord = $dbResult->record();
221
			}
222
		}
223
		if ($dbRecord === null) {
224
			throw new PlayerNotFoundException('Invalid accountID: ' . $accountID . ' OR gameID:' . $gameID);
225
		}
226
227
		$this->accountID = $accountID;
228
		$this->gameID = $gameID;
229
		$this->playerName = $dbRecord->getField('player_name');
230
		$this->playerID = $dbRecord->getInt('player_id');
231
		$this->sectorID = $dbRecord->getInt('sector_id');
232
		$this->lastSectorID = $dbRecord->getInt('last_sector_id');
233
		$this->turns = $dbRecord->getInt('turns');
234
		$this->lastTurnUpdate = $dbRecord->getInt('last_turn_update');
235
		$this->newbieTurns = $dbRecord->getInt('newbie_turns');
236
		$this->lastNewsUpdate = $dbRecord->getInt('last_news_update');
237
		$this->attackColour = $dbRecord->getField('attack_warning');
238
		$this->dead = $dbRecord->getBoolean('dead');
239
		$this->npc = $dbRecord->getBoolean('npc');
240
		$this->newbieStatus = $dbRecord->getBoolean('newbie_status');
241
		$this->landedOnPlanet = $dbRecord->getBoolean('land_on_planet');
242
		$this->lastActive = $dbRecord->getInt('last_active');
243
		$this->lastCPLAction = $dbRecord->getInt('last_cpl_action');
244
		$this->raceID = $dbRecord->getInt('race_id');
245
		$this->credits = $dbRecord->getInt('credits');
246
		$this->experience = $dbRecord->getInt('experience');
247
		$this->alignment = $dbRecord->getInt('alignment');
248
		$this->militaryPayment = $dbRecord->getInt('military_payment');
249
		$this->allianceID = $dbRecord->getInt('alliance_id');
250
		$this->allianceJoinable = $dbRecord->getInt('alliance_join');
251
		$this->shipID = $dbRecord->getInt('ship_type_id');
252
		$this->kills = $dbRecord->getInt('kills');
253
		$this->deaths = $dbRecord->getInt('deaths');
254
		$this->assists = $dbRecord->getInt('assists');
255
		$this->lastPort = $dbRecord->getInt('last_port');
256
		$this->bank = $dbRecord->getInt('bank');
257
		$this->zoom = $dbRecord->getInt('zoom');
258
		$this->displayMissions = $dbRecord->getBoolean('display_missions');
259
		$this->displayWeapons = $dbRecord->getBoolean('display_weapons');
260
		$this->forceDropMessages = $dbRecord->getBoolean('force_drop_messages');
261
		$this->groupScoutMessages = $dbRecord->getField('group_scout_messages');
262
		$this->ignoreGlobals = $dbRecord->getBoolean('ignore_globals');
263
		$this->newbieWarning = $dbRecord->getBoolean('newbie_warning');
264
		$this->nameChanged = $dbRecord->getBoolean('name_changed');
265
		$this->raceChanged = $dbRecord->getBoolean('race_changed');
266
		$this->combatDronesKamikazeOnMines = $dbRecord->getBoolean('combat_drones_kamikaze_on_mines');
267
		$this->underAttack = $dbRecord->getBoolean('under_attack');
268
	}
269
270
	/**
271
	 * Insert a new player into the database. Returns the new player object.
272
	 */
273
	public static function createPlayer(int $accountID, int $gameID, string $playerName, int $raceID, bool $isNewbie, bool $npc = false) : self {
274
		$time = Smr\Epoch::time();
275
		$db = Smr\Database::getInstance();
276
		$db->lockTable('player');
277
278
		// Player names must be unique within each game
279
		try {
280
			self::getPlayerByPlayerName($playerName, $gameID);
281
			$db->unlock();
282
			throw new \Smr\UserException('That player name already exists.');
283
		} catch (PlayerNotFoundException $e) {
284
			// Player name does not yet exist, we may proceed
285
		}
286
287
		// get last registered player id in that game and increase by one.
288
		$dbResult = $db->read('SELECT MAX(player_id) FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
289
		if ($dbResult->hasRecord()) {
290
			$playerID = $dbResult->record()->getInt('MAX(player_id)') + 1;
291
		} else {
292
			$playerID = 1;
293
		}
294
295
		$startSectorID = 0; // Temporarily put player into non-existent sector
296
		$db->write('INSERT INTO player (account_id, game_id, player_id, player_name, race_id, sector_id, last_cpl_action, last_active, npc, newbie_status)
297
					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) . ')');
298
299
		$db->unlock();
300
301
		$player = SmrPlayer::getPlayer($accountID, $gameID);
302
		$player->setSectorID($player->getHome());
303
		return $player;
304
	}
305
306
	/**
307
	 * Get array of players whose info can be accessed by this player.
308
	 * Skips players who are not in the same alliance as this player.
309
	 */
310
	public function getSharingPlayers(bool $forceUpdate = false) : array {
311
		$results = array($this);
312
313
		// Only return this player if not in an alliance
314
		if (!$this->hasAlliance()) {
315
			return $results;
316
		}
317
318
		// Get other players who are sharing info for this game.
319
		// NOTE: game_id=0 means that player shares info for all games.
320
		$dbResult = $this->db->read('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()) . ')');
321
		foreach ($dbResult->records() as $dbRecord) {
322
			try {
323
				$otherPlayer = SmrPlayer::getPlayer($dbRecord->getInt('from_account_id'),
324
				                                    $this->getGameID(), $forceUpdate);
325
			} catch (PlayerNotFoundException $e) {
326
				// Skip players that have not joined this game
327
				continue;
328
			}
329
330
			// players must be in the same alliance
331
			if ($this->sameAlliance($otherPlayer)) {
332
				$results[] = $otherPlayer;
333
			}
334
		}
335
		return $results;
336
	}
337
338
	public function getSQL() : string {
339
		return $this->SQL;
340
	}
341
342
	public function getZoom() : int {
343
		return $this->zoom;
344
	}
345
346
	protected function setZoom(int $zoom) : void {
347
		// Set the zoom level between [1, 9]
348
		$zoom = max(1, min(9, $zoom));
349
		if ($this->zoom == $zoom) {
350
			return;
351
		}
352
		$this->zoom = $zoom;
353
		$this->hasChanged = true;
354
	}
355
356
	public function increaseZoom(int $zoom) : void {
357
		if ($zoom < 0) {
358
			throw new Exception('Trying to increase negative zoom.');
359
		}
360
		$this->setZoom($this->getZoom() + $zoom);
361
	}
362
363
	public function decreaseZoom(int $zoom) : void {
364
		if ($zoom < 0) {
365
			throw new Exception('Trying to decrease negative zoom.');
366
		}
367
		$this->setZoom($this->getZoom() - $zoom);
368
	}
369
370
	public function getAttackColour() : string {
371
		return $this->attackColour;
372
	}
373
374
	public function setAttackColour(string $colour) : void {
375
		if ($this->attackColour == $colour) {
376
			return;
377
		}
378
		$this->attackColour = $colour;
379
		$this->hasChanged = true;
380
	}
381
382
	public function isIgnoreGlobals() : bool {
383
		return $this->ignoreGlobals;
384
	}
385
386
	public function setIgnoreGlobals(bool $bool) : void {
387
		if ($this->ignoreGlobals == $bool) {
388
			return;
389
		}
390
		$this->ignoreGlobals = $bool;
391
		$this->hasChanged = true;
392
	}
393
394
	public function getAccount() : SmrAccount {
395
		return SmrAccount::getAccount($this->getAccountID());
396
	}
397
398
	public function getAccountID() : int {
399
		return $this->accountID;
400
	}
401
402
	public function getGameID() : int {
403
		return $this->gameID;
404
	}
405
406
	public function getGame() : SmrGame {
407
		return SmrGame::getGame($this->gameID);
408
	}
409
410
	public function getNewbieTurns() : int {
411
		return $this->newbieTurns;
412
	}
413
414
	public function hasNewbieTurns() : bool {
415
		return $this->getNewbieTurns() > 0;
416
	}
417
418
	public function setNewbieTurns(int $newbieTurns) : void {
419
		if ($this->newbieTurns == $newbieTurns) {
420
			return;
421
		}
422
		$this->newbieTurns = $newbieTurns;
423
		$this->hasChanged = true;
424
	}
425
426
	public function getShip(bool $forceUpdate = false) : AbstractSmrShip {
427
		return SmrShip::getShip($this, $forceUpdate);
428
	}
429
430
	public function getShipTypeID() : int {
431
		return $this->shipID;
432
	}
433
434
	/**
435
	 * Do not call directly. Use SmrShip::setTypeID instead.
436
	 */
437
	public function setShipTypeID(int $shipID) : void {
438
		if ($this->shipID == $shipID) {
439
			return;
440
		}
441
		$this->shipID = $shipID;
442
		$this->hasChanged = true;
443
	}
444
445
	public function hasCustomShipName() : bool {
446
		return $this->getCustomShipName() !== false;
447
	}
448
449
	public function getCustomShipName() : string|false {
450
		if (!isset($this->customShipName)) {
451
			$dbResult = $this->db->read('SELECT * FROM ship_has_name WHERE ' . $this->SQL . ' LIMIT 1');
452
			if ($dbResult->hasRecord()) {
453
				$this->customShipName = $dbResult->record()->getField('ship_name');
454
			} else {
455
				$this->customShipName = false;
456
			}
457
		}
458
		return $this->customShipName;
459
	}
460
461
	public function setCustomShipName(string $name) : void {
462
		$this->db->write('REPLACE INTO ship_has_name (game_id, account_id, ship_name)
463
			VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeString($name) . ')');
464
	}
465
466
	/**
467
	 * Get planet owned by this player.
468
	 * Returns false if this player does not own a planet.
469
	 */
470
	public function getPlanet() : SmrPlanet|false {
471
		$dbResult = $this->db->read('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_id=' . $this->db->escapeNumber($this->getAccountID()));
472
		if ($dbResult->hasRecord()) {
473
			$dbRecord = $dbResult->record();
474
			return SmrPlanet::getPlanet($this->getGameID(), $dbRecord->getInt('sector_id'), false, $dbRecord);
475
		} else {
476
			return false;
477
		}
478
	}
479
480
	public function getSectorPlanet() : SmrPlanet {
481
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
482
	}
483
484
	public function getSectorPort() : SmrPort {
485
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
486
	}
487
488
	public function getSectorID() : int {
489
		return $this->sectorID;
490
	}
491
492
	public function getSector() : SmrSector {
493
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
494
	}
495
496
	public function setSectorID(int $sectorID) : void {
497
		if ($this->sectorID == $sectorID) {
498
			return;
499
		}
500
501
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
502
		$port->addCachePort($this->getAccountID()); //Add port of sector we were just in, to make sure it is left totally up to date.
503
504
		$this->setLastSectorID($this->getSectorID());
505
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
506
		$this->sectorID = $sectorID;
507
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
508
		$this->hasChanged = true;
509
510
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
511
		$port->addCachePort($this->getAccountID()); //Add the port of sector we are now in.
512
	}
513
514
	public function getLastSectorID() : int {
515
		return $this->lastSectorID;
516
	}
517
518
	public function setLastSectorID(int $lastSectorID) : void {
519
		if ($this->lastSectorID == $lastSectorID) {
520
			return;
521
		}
522
		$this->lastSectorID = $lastSectorID;
523
		$this->hasChanged = true;
524
	}
525
526
	public function getHome() : int {
527
		// get his home sector
528
		$hq_id = GOVERNMENT + $this->getRaceID();
529
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
530
		if (!empty($raceHqSectors)) {
531
			// If race has multiple HQ's for some reason, use the first one
532
			return key($raceHqSectors);
533
		} else {
534
			return 1;
535
		}
536
	}
537
538
	public function isDead() : bool {
539
		return $this->dead;
540
	}
541
542
	public function isNPC() : bool {
543
		return $this->npc;
544
	}
545
546
	/**
547
	 * Does the player have Newbie status?
548
	 */
549
	public function hasNewbieStatus() : bool {
550
		return $this->newbieStatus;
551
	}
552
553
	/**
554
	 * Update the player's newbie status if it has changed.
555
	 * This function queries the account, so use sparingly.
556
	 */
557
	public function updateNewbieStatus() : void {
558
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
559
		if ($this->newbieStatus != $accountNewbieStatus) {
560
			$this->newbieStatus = $accountNewbieStatus;
561
			$this->hasChanged = true;
562
		}
563
	}
564
565
	/**
566
	 * Has this player been designated as the alliance flagship?
567
	 */
568
	public function isFlagship() : bool {
569
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipID() == $this->getAccountID();
570
	}
571
572
	public function isPresident() : bool {
573
		return Council::getPresidentID($this->getGameID(), $this->getRaceID()) == $this->getAccountID();
574
	}
575
576
	public function isOnCouncil() : bool {
577
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getAccountID());
578
	}
579
580
	public function isDraftLeader() : bool {
581
		if (!isset($this->draftLeader)) {
582
			$dbResult = $this->db->read('SELECT 1 FROM draft_leaders WHERE ' . $this->SQL . ' LIMIT 1');
583
			$this->draftLeader = $dbResult->hasRecord();
584
		}
585
		return $this->draftLeader;
586
	}
587
588
	public function getGPWriter() : string|false {
589
		if (!isset($this->gpWriter)) {
590
			$this->gpWriter = false;
591
			$dbResult = $this->db->read('SELECT position FROM galactic_post_writer WHERE ' . $this->SQL);
592
			if ($dbResult->hasRecord()) {
593
				$this->gpWriter = $dbResult->record()->getField('position');
594
			}
595
		}
596
		return $this->gpWriter;
597
	}
598
599
	public function isGPEditor() : bool {
600
		return $this->getGPWriter() == 'editor';
601
	}
602
603
	public function isForceDropMessages() : bool {
604
		return $this->forceDropMessages;
605
	}
606
607
	public function setForceDropMessages(bool $bool) : void {
608
		if ($this->forceDropMessages == $bool) {
609
			return;
610
		}
611
		$this->forceDropMessages = $bool;
612
		$this->hasChanged = true;
613
	}
614
615
	public function getScoutMessageGroupLimit() : int {
616
		return match($this->groupScoutMessages) {
617
			'ALWAYS' => 0,
618
			'AUTO' => MESSAGES_PER_PAGE,
619
			'NEVER' => PHP_INT_MAX,
620
		};
621
	}
622
623
	public function getGroupScoutMessages() : string {
624
		return $this->groupScoutMessages;
625
	}
626
627
	public function setGroupScoutMessages(string $setting) : void {
628
		if ($this->groupScoutMessages == $setting) {
629
			return;
630
		}
631
		$this->groupScoutMessages = $setting;
632
		$this->hasChanged = true;
633
	}
634
635
	/**
636
	 * @return int Message ID
637
	 */
638
	protected static function doMessageSending(int $senderID, int $receiverID, int $gameID, int $messageTypeID, string $message, int $expires, bool $senderDelete = false, bool $unread = true) : int {
639
		$message = trim($message);
640
		$db = Smr\Database::getInstance();
641
		// send him the message
642
		$db->write('INSERT INTO message
643
			(account_id,game_id,message_type_id,message_text,
644
			sender_id,send_time,expire_time,sender_delete) VALUES(' .
645
			$db->escapeNumber($receiverID) . ',' .
646
			$db->escapeNumber($gameID) . ',' .
647
			$db->escapeNumber($messageTypeID) . ',' .
648
			$db->escapeString($message) . ',' .
649
			$db->escapeNumber($senderID) . ',' .
650
			$db->escapeNumber(Smr\Epoch::time()) . ',' .
651
			$db->escapeNumber($expires) . ',' .
652
			$db->escapeBoolean($senderDelete) . ')'
653
		);
654
		// Keep track of the message_id so it can be returned
655
		$insertID = $db->getInsertID();
656
657
		if ($unread === true) {
658
			// give him the message icon
659
			$db->write('REPLACE INTO player_has_unread_messages (game_id, account_id, message_type_id) VALUES
660
						(' . $db->escapeNumber($gameID) . ', ' . $db->escapeNumber($receiverID) . ', ' . $db->escapeNumber($messageTypeID) . ')');
661
		}
662
663
		switch ($messageTypeID) {
664
			case MSG_PLAYER:
665
				$receiverAccount = SmrAccount::getAccount($receiverID);
666
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isLoggedIn()) {
667
					require_once(get_file_loc('messages.inc.php'));
668
					$sender = getMessagePlayer($senderID, $gameID, $messageTypeID);
669
					if ($sender instanceof SmrPlayer) {
0 ignored issues
show
introduced by
$sender is never a sub-type of SmrPlayer.
Loading history...
670
						$sender = $sender->getDisplayName();
671
					}
672
					$mail = setupMailer();
673
					$mail->Subject = 'Message Notification';
674
					$mail->setFrom('[email protected]', 'SMR Notifications');
675
					$bbifiedMessage = 'From: ' . $sender . ' Date: ' . date($receiverAccount->getDateTimeFormat(), Smr\Epoch::time()) . "<br/>\r\n<br/>\r\n" . bbifyMessage($message, true);
676
					$mail->msgHTML($bbifiedMessage);
677
					$mail->AltBody = strip_tags($bbifiedMessage);
678
					$mail->addAddress($receiverAccount->getEmail(), $receiverAccount->getHofName());
679
					$mail->send();
680
					$receiverAccount->decreaseMessageNotifications($messageTypeID, 1);
681
					$receiverAccount->update();
682
				}
683
			break;
684
		}
685
686
		return $insertID;
687
	}
688
689
	public function sendMessageToBox(int $boxTypeID, string $message) : void {
690
		// send him the message
691
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
692
	}
693
694
	public function sendGlobalMessage(string $message, bool $canBeIgnored = true) : void {
695
		if ($canBeIgnored) {
696
			if ($this->getAccount()->isMailBanned()) {
697
				create_error('You are currently banned from sending messages');
698
			}
699
		}
700
		$this->sendMessageToBox(BOX_GLOBALS, $message);
701
702
		// send to all online player
703
		$db = Smr\Database::getInstance();
704
		$dbResult = $db->read('SELECT account_id
705
					FROM active_session
706
					JOIN player USING (game_id, account_id)
707
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(Smr\Epoch::time() - Smr\Session::TIME_BEFORE_EXPIRY) . '
708
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
709
						AND ignore_globals = \'FALSE\'
710
						AND account_id != ' . $db->escapeNumber($this->getAccountID()));
711
712
		foreach ($dbResult->records() as $dbRecord) {
713
			$this->sendMessage($dbRecord->getInt('account_id'), MSG_GLOBAL, $message, $canBeIgnored);
714
		}
715
		$this->sendMessage($this->getAccountID(), MSG_GLOBAL, $message, $canBeIgnored, false);
716
	}
717
718
	/**
719
	 * @return int|false Message ID (false if not sent due to ignores)
720
	 */
721
	public function sendMessage(int $receiverID, int $messageTypeID, string $message, bool $canBeIgnored = true, bool $unread = true, int $expires = null, bool $senderDelete = false) : int|false {
722
		//get expire time
723
		if ($canBeIgnored) {
724
			if ($this->getAccount()->isMailBanned()) {
725
				create_error('You are currently banned from sending messages');
726
			}
727
			// Don't send messages to players ignoring us
728
			$dbResult = $this->db->read('SELECT 1 FROM message_blacklist WHERE account_id=' . $this->db->escapeNumber($receiverID) . ' AND blacklisted_id=' . $this->db->escapeNumber($this->getAccountID()) . ' LIMIT 1');
729
			if ($dbResult->hasRecord()) {
730
				return false;
731
			}
732
		}
733
734
		$message = word_filter($message);
735
736
		// If expires not specified, use default based on message type
737
		if ($expires === null) {
738
			$expires = match($messageTypeID) {
739
				MSG_GLOBAL => 3600, // 1h
740
				MSG_PLAYER => 86400 * 31, // 1 month
741
				MSG_PLANET => 86400 * 7, // 1 week
742
				MSG_SCOUT => 86400 * 3, // 3 days
743
				MSG_POLITICAL => 86400 * 31, // 1 month
744
				MSG_ALLIANCE => 86400 * 31, // 1 month
745
				MSG_ADMIN => 86400 * 365, // 1 year
746
				MSG_CASINO => 86400 * 31, // 1 month
747
				default => 86400 * 7, // 1 week
748
			};
749
			$expires += Smr\Epoch::time();
750
		}
751
752
		// Do not put scout messages in the sender's sent box
753
		if ($messageTypeID == MSG_SCOUT) {
754
			$senderDelete = true;
755
		}
756
757
		// send him the message and return the message_id
758
		return self::doMessageSending($this->getAccountID(), $receiverID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
759
	}
760
761
	public function sendMessageFromOpAnnounce(int $receiverID, string $message, int $expires = null) : void {
762
		// get expire time if not set
763
		if ($expires === null) {
764
			$expires = Smr\Epoch::time() + 86400 * 14;
765
		}
766
		self::doMessageSending(ACCOUNT_ID_OP_ANNOUNCE, $receiverID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
767
	}
768
769
	public function sendMessageFromAllianceCommand(int $receiverID, string $message) : void {
770
		$expires = Smr\Epoch::time() + 86400 * 365;
771
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_COMMAND, $receiverID, $this->getGameID(), MSG_PLAYER, $message, $expires);
772
	}
773
774
	public static function sendMessageFromPlanet(int $gameID, int $receiverID, string $message) : void {
775
		//get expire time
776
		$expires = Smr\Epoch::time() + 86400 * 31;
777
		// send him the message
778
		self::doMessageSending(ACCOUNT_ID_PLANET, $receiverID, $gameID, MSG_PLANET, $message, $expires);
779
	}
780
781
	public static function sendMessageFromPort(int $gameID, int $receiverID, string $message) : void {
782
		//get expire time
783
		$expires = Smr\Epoch::time() + 86400 * 31;
784
		// send him the message
785
		self::doMessageSending(ACCOUNT_ID_PORT, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
786
	}
787
788
	public static function sendMessageFromFedClerk(int $gameID, int $receiverID, string $message) : void {
789
		$expires = Smr\Epoch::time() + 86400 * 365;
790
		self::doMessageSending(ACCOUNT_ID_FED_CLERK, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
791
	}
792
793
	public static function sendMessageFromAdmin(int $gameID, int $receiverID, string $message, int $expires = null) : void {
794
		//get expire time
795
		if ($expires === null) {
796
			$expires = Smr\Epoch::time() + 86400 * 365;
797
		}
798
		// send him the message
799
		self::doMessageSending(ACCOUNT_ID_ADMIN, $receiverID, $gameID, MSG_ADMIN, $message, $expires);
800
	}
801
802
	public static function sendMessageFromAllianceAmbassador(int $gameID, int $receiverID, string $message, int $expires = null) : void {
803
		//get expire time
804
		if ($expires === null) {
805
			$expires = Smr\Epoch::time() + 86400 * 31;
806
		}
807
		// send him the message
808
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_AMBASSADOR, $receiverID, $gameID, MSG_ALLIANCE, $message, $expires);
809
	}
810
811
	public static function sendMessageFromCasino(int $gameID, int $receiverID, string $message, int $expires = null) : void {
812
		//get expire time
813
		if ($expires === null) {
814
			$expires = Smr\Epoch::time() + 86400 * 7;
815
		}
816
		// send him the message
817
		self::doMessageSending(ACCOUNT_ID_CASINO, $receiverID, $gameID, MSG_CASINO, $message, $expires);
818
	}
819
820
	public static function sendMessageFromRace(int $raceID, int $gameID, int $receiverID, string $message, int $expires = null) : void {
821
		//get expire time
822
		if ($expires === null) {
823
			$expires = Smr\Epoch::time() + 86400 * 5;
824
		}
825
		// send him the message
826
		self::doMessageSending(ACCOUNT_ID_GROUP_RACES + $raceID, $receiverID, $gameID, MSG_POLITICAL, $message, $expires);
827
	}
828
829
	public function setMessagesRead(int $messageTypeID) : void {
830
		$this->db->write('DELETE FROM player_has_unread_messages
831
							WHERE '.$this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
832
	}
833
834
	public function getSafeAttackRating() : int {
835
		return max(0, min(8, IFloor($this->getAlignment() / 150) + 4));
836
	}
837
838
	public function hasFederalProtection() : bool {
839
		$sector = SmrSector::getSector($this->getGameID(), $this->getSectorID());
840
		if (!$sector->offersFederalProtection()) {
841
			return false;
842
		}
843
844
		$ship = $this->getShip();
845
		if ($ship->hasIllegalGoods()) {
846
			return false;
847
		}
848
849
		if ($ship->getAttackRating() <= $this->getSafeAttackRating()) {
850
			foreach ($sector->getFedRaceIDs() as $fedRaceID) {
851
				if ($this->canBeProtectedByRace($fedRaceID)) {
852
					return true;
853
				}
854
			}
855
		}
856
857
		return false;
858
	}
859
860
	public function canBeProtectedByRace(int $raceID) : bool {
861
		if (!isset($this->canFed)) {
862
			$this->canFed = array();
863
			$RACES = Globals::getRaces();
864
			foreach ($RACES as $raceID2 => $raceName) {
865
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
866
			}
867
			$dbResult = $this->db->read('SELECT race_id, allowed FROM player_can_fed
868
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(Smr\Epoch::time()));
869
			foreach ($dbResult->records() as $dbRecord) {
870
				$this->canFed[$dbRecord->getInt('race_id')] = $dbRecord->getBoolean('allowed');
871
			}
872
		}
873
		return $this->canFed[$raceID];
874
	}
875
876
	/**
877
	 * Returns a boolean identifying if the player can currently
878
	 * participate in battles.
879
	 */
880
	public function canFight() : bool {
881
		return !($this->hasNewbieTurns() ||
882
		         $this->isDead() ||
883
		         $this->isLandedOnPlanet() ||
884
		         $this->hasFederalProtection());
885
	}
886
887
	public function setDead(bool $bool) : void {
888
		if ($this->dead == $bool) {
889
			return;
890
		}
891
		$this->dead = $bool;
892
		$this->hasChanged = true;
893
	}
894
895
	public function getKills() : int {
896
		return $this->kills;
897
	}
898
899
	public function increaseKills(int $kills) : void {
900
		if ($kills < 0) {
901
			throw new Exception('Trying to increase negative kills.');
902
		}
903
		$this->setKills($this->kills + $kills);
904
	}
905
906
	public function setKills(int $kills) : void {
907
		if ($this->kills == $kills) {
908
			return;
909
		}
910
		$this->kills = $kills;
911
		$this->hasChanged = true;
912
	}
913
914
	public function getDeaths() : int {
915
		return $this->deaths;
916
	}
917
918
	public function increaseDeaths(int $deaths) : void {
919
		if ($deaths < 0) {
920
			throw new Exception('Trying to increase negative deaths.');
921
		}
922
		$this->setDeaths($this->getDeaths() + $deaths);
923
	}
924
925
	public function setDeaths(int $deaths) : void {
926
		if ($this->deaths == $deaths) {
927
			return;
928
		}
929
		$this->deaths = $deaths;
930
		$this->hasChanged = true;
931
	}
932
933
	public function getAssists() : int {
934
		return $this->assists;
935
	}
936
937
	public function increaseAssists(int $assists) : void {
938
		if ($assists < 1) {
939
			throw new Exception('Must increase by a positive number.');
940
		}
941
		$this->assists += $assists;
942
		$this->hasChanged = true;
943
	}
944
945
	public function getAlignment() : int {
946
		return $this->alignment;
947
	}
948
949
	public function increaseAlignment(int $align) : void {
950
		if ($align < 0) {
951
			throw new Exception('Trying to increase negative align.');
952
		}
953
		if ($align == 0) {
954
			return;
955
		}
956
		$align += $this->alignment;
957
		$this->setAlignment($align);
958
	}
959
960
	public function decreaseAlignment(int $align) : void {
961
		if ($align < 0) {
962
			throw new Exception('Trying to decrease negative align.');
963
		}
964
		if ($align == 0) {
965
			return;
966
		}
967
		$align = $this->alignment - $align;
968
		$this->setAlignment($align);
969
	}
970
971
	public function setAlignment(int $align) : void {
972
		if ($this->alignment == $align) {
973
			return;
974
		}
975
		$this->alignment = $align;
976
		$this->hasChanged = true;
977
	}
978
979
	public function getCredits() : int {
980
		return $this->credits;
981
	}
982
983
	public function getBank() : int {
984
		return $this->bank;
985
	}
986
987
	/**
988
	 * Increases personal bank account up to the maximum allowed credits.
989
	 * Returns the amount that was actually added to handle overflow.
990
	 */
991
	public function increaseBank(int $credits) : int {
992
		if ($credits == 0) {
993
			return 0;
994
		}
995
		if ($credits < 0) {
996
			throw new Exception('Trying to increase negative credits.');
997
		}
998
		$newTotal = min($this->bank + $credits, MAX_MONEY);
999
		$actualAdded = $newTotal - $this->bank;
1000
		$this->setBank($newTotal);
1001
		return $actualAdded;
1002
	}
1003
1004
	public function decreaseBank(int $credits) : void {
1005
		if ($credits == 0) {
1006
			return;
1007
		}
1008
		if ($credits < 0) {
1009
			throw new Exception('Trying to decrease negative credits.');
1010
		}
1011
		$newTotal = $this->bank - $credits;
1012
		$this->setBank($newTotal);
1013
	}
1014
1015
	public function setBank(int $credits) : void {
1016
		if ($this->bank == $credits) {
1017
			return;
1018
		}
1019
		if ($credits < 0) {
1020
			throw new Exception('Trying to set negative credits.');
1021
		}
1022
		if ($credits > MAX_MONEY) {
1023
			throw new Exception('Trying to set more than max credits.');
1024
		}
1025
		$this->bank = $credits;
1026
		$this->hasChanged = true;
1027
	}
1028
1029
	public function getExperience() {
1030
		return $this->experience;
1031
	}
1032
1033
	/**
1034
	 * Returns the percent progress towards the next level.
1035
	 * This value is rounded because it is used primarily in HTML img widths.
1036
	 */
1037
	public function getNextLevelPercentAcquired() : int {
1038
		if ($this->getNextLevelExperience() == $this->getThisLevelExperience()) {
1039
			return 100;
1040
		}
1041
		return max(0, min(100, IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
1042
	}
1043
1044
	public function getNextLevelPercentRemaining() : int {
1045
		return 100 - $this->getNextLevelPercentAcquired();
1046
	}
1047
1048
	public function getNextLevel() : array {
1049
		$LEVELS = Globals::getLevelRequirements();
1050
		if (!isset($LEVELS[$this->getLevelID() + 1])) {
1051
			return $LEVELS[$this->getLevelID()]; //Return current level experience if on last level.
1052
		}
1053
		return $LEVELS[$this->getLevelID() + 1];
1054
	}
1055
1056
	public function getNextLevelExperience() : int {
1057
		return $this->getNextLevel()['Requirement'];
1058
	}
1059
1060
	public function getThisLevelExperience() : int {
1061
		$LEVELS = Globals::getLevelRequirements();
1062
		return $LEVELS[$this->getLevelID()]['Requirement'];
1063
	}
1064
1065
	public function setExperience(int $experience) : void {
1066
		if ($this->experience == $experience) {
1067
			return;
1068
		}
1069
		if ($experience < MIN_EXPERIENCE) {
1070
			$experience = MIN_EXPERIENCE;
1071
		}
1072
		if ($experience > MAX_EXPERIENCE) {
1073
			$experience = MAX_EXPERIENCE;
1074
		}
1075
		$this->experience = $experience;
1076
		$this->hasChanged = true;
1077
1078
		// Since exp has changed, invalidate the player level so that it can
1079
		// be recomputed next time it is queried (in case it has changed).
1080
		$this->level = null;
1081
	}
1082
1083
	/**
1084
	 * Increases onboard credits up to the maximum allowed credits.
1085
	 * Returns the amount that was actually added to handle overflow.
1086
	 */
1087
	public function increaseCredits(int $credits) : int {
1088
		if ($credits == 0) {
1089
			return 0;
1090
		}
1091
		if ($credits < 0) {
1092
			throw new Exception('Trying to increase negative credits.');
1093
		}
1094
		$newTotal = min($this->credits + $credits, MAX_MONEY);
1095
		$actualAdded = $newTotal - $this->credits;
1096
		$this->setCredits($newTotal);
1097
		return $actualAdded;
1098
	}
1099
1100
	public function decreaseCredits(int $credits) : void {
1101
		if ($credits == 0) {
1102
			return;
1103
		}
1104
		if ($credits < 0) {
1105
			throw new Exception('Trying to decrease negative credits.');
1106
		}
1107
		$newTotal = $this->credits - $credits;
1108
		$this->setCredits($newTotal);
1109
	}
1110
1111
	public function setCredits(int $credits) : void {
1112
		if ($this->credits == $credits) {
1113
			return;
1114
		}
1115
		if ($credits < 0) {
1116
			throw new Exception('Trying to set negative credits.');
1117
		}
1118
		if ($credits > MAX_MONEY) {
1119
			throw new Exception('Trying to set more than max credits.');
1120
		}
1121
		$this->credits = $credits;
1122
		$this->hasChanged = true;
1123
	}
1124
1125
	public function increaseExperience(int $experience) : void {
1126
		if ($experience < 0) {
1127
			throw new Exception('Trying to increase negative experience.');
1128
		}
1129
		if ($experience == 0) {
1130
			return;
1131
		}
1132
		$newExperience = $this->experience + $experience;
1133
		$this->setExperience($newExperience);
1134
		$this->increaseHOF($experience, array('Experience', 'Total', 'Gain'), HOF_PUBLIC);
1135
	}
1136
	public function decreaseExperience(int $experience) : void {
1137
		if ($experience < 0) {
1138
			throw new Exception('Trying to decrease negative experience.');
1139
		}
1140
		if ($experience == 0) {
1141
			return;
1142
		}
1143
		$newExperience = $this->experience - $experience;
1144
		$this->setExperience($newExperience);
1145
		$this->increaseHOF($experience, array('Experience', 'Total', 'Loss'), HOF_PUBLIC);
1146
	}
1147
1148
	public function isLandedOnPlanet() : bool {
1149
		return $this->landedOnPlanet;
1150
	}
1151
1152
	public function setLandedOnPlanet(bool $bool) : void {
1153
		if ($this->landedOnPlanet == $bool) {
1154
			return;
1155
		}
1156
		$this->landedOnPlanet = $bool;
1157
		$this->hasChanged = true;
1158
	}
1159
1160
	/**
1161
	 * Returns the numerical level of the player (e.g. 1-50).
1162
	 */
1163
	public function getLevelID() : int {
1164
		// The level is cached for performance reasons unless `setExperience`
1165
		// is called and the player's experience changes.
1166
		if (!isset($this->level)) {
1167
			$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1168
			foreach ($LEVELS_REQUIREMENTS as $level_id => $require) {
1169
				if ($this->getExperience() >= $require['Requirement']) {
1170
					continue;
1171
				}
1172
				$this->level = $level_id - 1;
1173
				return $this->level;
1174
			}
1175
			$this->level = max(array_keys($LEVELS_REQUIREMENTS));
1176
		}
1177
		return $this->level;
1178
	}
1179
1180
	public function getLevelName() : string {
1181
		$level_name = Globals::getLevelRequirements()[$this->getLevelID()]['Name'];
1182
		if ($this->isPresident()) {
1183
			$level_name = '<img src="images/council_president.png" title="' . Globals::getRaceName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1184
		}
1185
		return $level_name;
1186
	}
1187
1188
	public function getMaxLevel() : int {
1189
		return max(array_keys(Globals::getLevelRequirements()));
1190
	}
1191
1192
	public function getPlayerID() : int {
1193
		return $this->playerID;
1194
	}
1195
1196
	/**
1197
	 * Returns the player name.
1198
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1199
	 */
1200
	public function getPlayerName() : string {
1201
		return $this->playerName;
1202
	}
1203
1204
	public function setPlayerName(string $name) : void {
1205
		$this->playerName = $name;
1206
		$this->hasChanged = true;
1207
	}
1208
1209
	/**
1210
	 * Returns the decorated player name, suitable for HTML display.
1211
	 */
1212
	public function getDisplayName(bool $includeAlliance = false) : string {
1213
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1214
		$return = get_colored_text($this->getAlignment(), $name);
1215
		if ($this->isNPC()) {
1216
			$return .= ' <span class="npcColour">[NPC]</span>';
1217
		}
1218
		if ($includeAlliance) {
1219
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1220
		}
1221
		return $return;
1222
	}
1223
1224
	public function getBBLink() : string {
1225
			return '[player=' . $this->getPlayerID() . ']';
1226
	}
1227
1228
	public function getLinkedDisplayName(bool $includeAlliance = true) : string {
1229
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1230
		if ($includeAlliance) {
1231
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1232
		}
1233
		return $return;
1234
	}
1235
1236
	/**
1237
	 * Use this method when the player is changing their own name.
1238
	 * This will flag the player as having used their free name change.
1239
	 */
1240
	public function setPlayerNameByPlayer(string $playerName) : void {
1241
		$this->setPlayerName($playerName);
1242
		$this->setNameChanged(true);
1243
	}
1244
1245
	public function isNameChanged() : bool {
1246
		return $this->nameChanged;
1247
	}
1248
1249
	public function setNameChanged(bool $bool) : void {
1250
		$this->nameChanged = $bool;
1251
		$this->hasChanged = true;
1252
	}
1253
1254
	public function isRaceChanged() : bool {
1255
		return $this->raceChanged;
1256
	}
1257
1258
	public function setRaceChanged(bool $raceChanged) : void {
1259
		$this->raceChanged = $raceChanged;
1260
		$this->hasChanged = true;
1261
	}
1262
1263
	public function canChangeRace() : bool {
1264
		return !$this->isRaceChanged() && (Smr\Epoch::time() - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1265
	}
1266
1267
	public static function getColouredRaceNameOrDefault(int $otherRaceID, AbstractSmrPlayer $player = null, bool $linked = false) : string {
1268
		$relations = 0;
1269
		if ($player !== null) {
1270
			$relations = $player->getRelation($otherRaceID);
1271
		}
1272
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1273
	}
1274
1275
	public function getColouredRaceName(int $otherRaceID, bool $linked = false) : string {
1276
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1277
	}
1278
1279
	public function setRaceID(int $raceID) : void {
1280
		if ($this->raceID == $raceID) {
1281
			return;
1282
		}
1283
		$this->raceID = $raceID;
1284
		$this->hasChanged = true;
1285
	}
1286
1287
	public function isAllianceLeader(bool $forceUpdate = false) : bool {
1288
		return $this->getAccountID() == $this->getAlliance($forceUpdate)->getLeaderID();
1289
	}
1290
1291
	public function getAlliance(bool $forceUpdate = false) : SmrAlliance {
1292
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1293
	}
1294
1295
	public function getAllianceID() : int {
1296
		return $this->allianceID;
1297
	}
1298
1299
	public function hasAlliance() : bool {
1300
		return $this->getAllianceID() != 0;
1301
	}
1302
1303
	protected function setAllianceID(int $ID) : void {
1304
		if ($this->allianceID == $ID) {
1305
			return;
1306
		}
1307
		$this->allianceID = $ID;
1308
		if ($this->allianceID != 0) {
1309
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1310
			$this->db->write('INSERT IGNORE INTO player_joined_alliance (account_id,game_id,alliance_id,status) ' .
1311
				'VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1312
		}
1313
		$this->hasChanged = true;
1314
	}
1315
1316
	public function getAllianceBBLink() : string {
1317
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1318
	}
1319
1320
	public function getAllianceDisplayName(bool $linked = false, bool $includeAllianceID = false) : string {
1321
		if ($this->hasAlliance()) {
1322
			return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1323
		} else {
1324
			return 'No Alliance';
1325
		}
1326
	}
1327
1328
	public function getAllianceRole(int $allianceID = null) : int {
1329
		if ($allianceID === null) {
1330
			$allianceID = $this->getAllianceID();
1331
		}
1332
		if (!isset($this->allianceRoles[$allianceID])) {
1333
			$this->allianceRoles[$allianceID] = 0;
1334
			$dbResult = $this->db->read('SELECT role_id
1335
						FROM player_has_alliance_role
1336
						WHERE ' . $this->SQL . '
1337
						AND alliance_id=' . $this->db->escapeNumber($allianceID) . '
1338
						LIMIT 1');
1339
			if ($dbResult->hasRecord()) {
1340
				$this->allianceRoles[$allianceID] = $dbResult->record()->getInt('role_id');
1341
			}
1342
		}
1343
		return $this->allianceRoles[$allianceID];
1344
	}
1345
1346
	public function leaveAlliance(AbstractSmrPlayer $kickedBy = null) : void {
1347
		$allianceID = $this->getAllianceID();
1348
		$alliance = $this->getAlliance();
1349
		if ($kickedBy != null) {
1350
			$kickedBy->sendMessage($this->getAccountID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1351
			$this->actionTaken('PlayerKicked', array('Alliance' => $alliance, 'Player' => $kickedBy));
1352
			$kickedBy->actionTaken('KickPlayer', array('Alliance' => $alliance, 'Player' => $this));
1353
		} elseif ($this->isAllianceLeader()) {
1354
			$this->actionTaken('DisbandAlliance', array('Alliance' => $alliance));
1355
		} else {
1356
			$this->actionTaken('LeaveAlliance', array('Alliance' => $alliance));
1357
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1358
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I left your alliance!', false);
1359
			}
1360
		}
1361
1362
		if (!$this->isAllianceLeader() && $allianceID != NHA_ID) { // Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1363
			$this->setAllianceJoinable(Smr\Epoch::time() + self::TIME_FOR_ALLIANCE_SWITCH);
1364
			$alliance->getLeader()->setAllianceJoinable(Smr\Epoch::time() + self::TIME_FOR_ALLIANCE_SWITCH); //We set the joinable time for leader here, that way a single player alliance won't cause a player to wait before switching.
1365
		}
1366
1367
		$this->setAllianceID(0);
1368
		$this->db->write('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1369
	}
1370
1371
	/**
1372
	 * Join an alliance (used for both Leader and New Member roles)
1373
	 */
1374
	public function joinAlliance(int $allianceID) : void {
1375
		$this->setAllianceID($allianceID);
1376
		$alliance = $this->getAlliance();
1377
1378
		if (!$this->isAllianceLeader()) {
1379
			// Do not throw an exception if the NHL account doesn't exist.
1380
			try {
1381
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I joined your alliance!', false);
1382
			} catch (AccountNotFoundException $e) {
1383
				if ($alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1384
					throw $e;
1385
				}
1386
			}
1387
1388
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1389
		} else {
1390
			$roleID = ALLIANCE_ROLE_LEADER;
1391
		}
1392
		$this->db->write('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()) . ')');
1393
1394
		$this->actionTaken('JoinAlliance', array('Alliance' => $alliance));
1395
	}
1396
1397
	public function getAllianceJoinable() : int {
1398
		return $this->allianceJoinable;
1399
	}
1400
1401
	private function setAllianceJoinable(int $time) : void {
1402
		if ($this->allianceJoinable == $time) {
1403
			return;
1404
		}
1405
		$this->allianceJoinable = $time;
1406
		$this->hasChanged = true;
1407
	}
1408
1409
	/**
1410
	 * Invites player with $accountID to this player's alliance.
1411
	 */
1412
	public function sendAllianceInvitation(int $accountID, string $message, int $expires) : void {
1413
		if (!$this->hasAlliance()) {
1414
			throw new Exception('Must be in an alliance to send alliance invitations');
1415
		}
1416
		// Send message to invited player
1417
		$messageID = $this->sendMessage($accountID, MSG_PLAYER, $message, false, true, $expires, true);
1418
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), $messageID, $expires);
1419
	}
1420
1421
	public function isCombatDronesKamikazeOnMines() : bool {
1422
		return $this->combatDronesKamikazeOnMines;
1423
	}
1424
1425
	public function setCombatDronesKamikazeOnMines(bool $bool) : void {
1426
		if ($this->combatDronesKamikazeOnMines == $bool) {
1427
			return;
1428
		}
1429
		$this->combatDronesKamikazeOnMines = $bool;
1430
		$this->hasChanged = true;
1431
	}
1432
1433
	protected function getPersonalRelationsData() : void {
1434
		if (!isset($this->personalRelations)) {
1435
			//get relations
1436
			$RACES = Globals::getRaces();
1437
			$this->personalRelations = array();
1438
			foreach ($RACES as $raceID => $raceName) {
1439
				$this->personalRelations[$raceID] = 0;
1440
			}
1441
			$dbResult = $this->db->read('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL . ' LIMIT ' . count($RACES));
1442
			foreach ($dbResult->records() as $dbRecord) {
1443
				$this->personalRelations[$dbRecord->getInt('race_id')] = $dbRecord->getInt('relation');
1444
			}
1445
		}
1446
	}
1447
1448
	public function getPersonalRelations() : array {
1449
		$this->getPersonalRelationsData();
1450
		return $this->personalRelations;
1451
	}
1452
1453
	/**
1454
	 * Get personal relations with a race
1455
	 */
1456
	public function getPersonalRelation(int $raceID) : int {
1457
		$rels = $this->getPersonalRelations();
1458
		return $rels[$raceID];
1459
	}
1460
1461
	/**
1462
	 * Get total relations with all races (personal + political)
1463
	 */
1464
	public function getRelations() : array {
1465
		if (!isset($this->relations)) {
1466
			//get relations
1467
			$RACES = Globals::getRaces();
1468
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1469
			$personalRels = $this->getPersonalRelations(); // make sure they're initialised.
1470
			$this->relations = array();
1471
			foreach ($RACES as $raceID => $raceName) {
1472
				$this->relations[$raceID] = $personalRels[$raceID] + $raceRelations[$raceID];
1473
			}
1474
		}
1475
		return $this->relations;
1476
	}
1477
1478
	/**
1479
	 * Get total relations with a race (personal + political)
1480
	 */
1481
	public function getRelation(int $raceID) : int {
1482
		$rels = $this->getRelations();
1483
		return $rels[$raceID];
1484
	}
1485
1486
	/**
1487
	 * Increases personal relations from trading $numGoods units with the race
1488
	 * of the port given by $raceID.
1489
	 */
1490
	public function increaseRelationsByTrade(int $numGoods, int $raceID) : void {
1491
		$relations = ICeil(min($numGoods, 300) / 30);
1492
		//Cap relations to a max of 1 after 500 have been reached
1493
		if ($this->getPersonalRelation($raceID) + $relations >= 500) {
1494
			$relations = max(1, min($relations, 500 - $this->getPersonalRelation($raceID)));
1495
		}
1496
		$this->increaseRelations($relations, $raceID);
1497
	}
1498
1499
	/**
1500
	 * Decreases personal relations from trading failures, e.g. rejected
1501
	 * bargaining and getting caught stealing.
1502
	 */
1503
	public function decreaseRelationsByTrade(int $numGoods, int $raceID) : void {
1504
		$relations = ICeil(min($numGoods, 300) / 30);
1505
		$this->decreaseRelations($relations, $raceID);
1506
	}
1507
1508
	/**
1509
	 * Increase personal relations.
1510
	 */
1511
	public function increaseRelations(int $relations, int $raceID) : void {
1512
		if ($relations < 0) {
1513
			throw new Exception('Trying to increase negative relations.');
1514
		}
1515
		if ($relations == 0) {
1516
			return;
1517
		}
1518
		$relations += $this->getPersonalRelation($raceID);
1519
		$this->setRelations($relations, $raceID);
1520
	}
1521
1522
	/**
1523
	 * Decrease personal relations.
1524
	 */
1525
	public function decreaseRelations(int $relations, int $raceID) : void {
1526
		if ($relations < 0) {
1527
			throw new Exception('Trying to decrease negative relations.');
1528
		}
1529
		if ($relations == 0) {
1530
			return;
1531
		}
1532
		$relations = $this->getPersonalRelation($raceID) - $relations;
1533
		$this->setRelations($relations, $raceID);
1534
	}
1535
1536
	/**
1537
	 * Set personal relations.
1538
	 */
1539
	public function setRelations(int $relations, int $raceID) : void {
1540
		$this->getRelations();
1541
		if ($this->personalRelations[$raceID] == $relations) {
1542
			return;
1543
		}
1544
		if ($relations < MIN_RELATIONS) {
1545
			$relations = MIN_RELATIONS;
1546
		}
1547
		$relationsDiff = IRound($relations - $this->personalRelations[$raceID]);
1548
		$this->personalRelations[$raceID] = $relations;
1549
		$this->relations[$raceID] += $relationsDiff;
1550
		$this->db->write('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]) . ')');
1551
	}
1552
1553
	/**
1554
	 * Set any starting personal relations bonuses or penalties.
1555
	 */
1556
	public function giveStartingRelations() {
1557
		if ($this->getRaceID() === RACE_ALSKANT) {
1558
			// Give Alskants bonus personal relations to start.
1559
			foreach (Globals::getRaces() as $raceID => $raceInfo) {
1560
				$this->setRelations(ALSKANT_BONUS_RELATIONS, $raceID);
1561
			}
1562
		}
1563
	}
1564
1565
	public function getLastNewsUpdate() : int {
1566
		return $this->lastNewsUpdate;
1567
	}
1568
1569
	private function setLastNewsUpdate(int $time) : void {
1570
		if ($this->lastNewsUpdate == $time) {
1571
			return;
1572
		}
1573
		$this->lastNewsUpdate = $time;
1574
		$this->hasChanged = true;
1575
	}
1576
1577
	public function updateLastNewsUpdate() : void {
1578
		$this->setLastNewsUpdate(Smr\Epoch::time());
1579
	}
1580
1581
	public function getLastPort() : int {
1582
		return $this->lastPort;
1583
	}
1584
1585
	public function setLastPort(int $lastPort) : void {
1586
		if ($this->lastPort == $lastPort) {
1587
			return;
1588
		}
1589
		$this->lastPort = $lastPort;
1590
		$this->hasChanged = true;
1591
	}
1592
1593
	public function getPlottedCourse() : Distance|false {
1594
		if (!isset($this->plottedCourse)) {
1595
			// check if we have a course plotted
1596
			$dbResult = $this->db->read('SELECT course FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1597
1598
			if ($dbResult->hasRecord()) {
1599
				// get the course back
1600
				$this->plottedCourse = $dbResult->record()->getObject('course');
1601
			} else {
1602
				$this->plottedCourse = false;
1603
			}
1604
		}
1605
1606
		// Update the plotted course if we have moved since the last query
1607
		if ($this->plottedCourse !== false && (!isset($this->plottedCourseFrom) || $this->plottedCourseFrom != $this->getSectorID())) {
1608
			$this->plottedCourseFrom = $this->getSectorID();
1609
1610
			if ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1611
				// We have walked into the next sector of the course
1612
				$this->plottedCourse->followPath();
1613
				$this->setPlottedCourse($this->plottedCourse);
1614
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1615
				// We have skipped to some later sector in the course
1616
				$this->plottedCourse->skipToSector($this->getSectorID());
1617
				$this->setPlottedCourse($this->plottedCourse);
1618
			}
1619
		}
1620
		return $this->plottedCourse;
1621
	}
1622
1623
	public function setPlottedCourse(Distance $plottedCourse) : void {
1624
		$hadPlottedCourse = $this->hasPlottedCourse();
1625
		$this->plottedCourse = $plottedCourse;
1626
		if ($this->plottedCourse->getTotalSectors() > 0) {
1627
			$this->db->write('REPLACE INTO player_plotted_course
1628
				(account_id, game_id, course)
1629
				VALUES(' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeObject($this->plottedCourse) . ')');
1630
		} elseif ($hadPlottedCourse) {
1631
			$this->deletePlottedCourse();
1632
		}
1633
	}
1634
1635
	public function hasPlottedCourse() : bool {
1636
		return $this->getPlottedCourse() !== false;
1637
	}
1638
1639
	public function isPartOfCourse(SmrSector|int $sectorOrSectorID) : bool {
1640
		if (!$this->hasPlottedCourse()) {
1641
			return false;
1642
		}
1643
		if ($sectorOrSectorID instanceof SmrSector) {
1644
			$sectorID = $sectorOrSectorID->getSectorID();
1645
		} else {
1646
			$sectorID = $sectorOrSectorID;
1647
		}
1648
		return $this->getPlottedCourse()->isInPath($sectorID);
1649
	}
1650
1651
	public function deletePlottedCourse() : void {
1652
		$this->plottedCourse = false;
1653
		$this->db->write('DELETE FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1654
	}
1655
1656
	// Computes the turn cost and max misjump between current and target sector
1657
	public function getJumpInfo(SmrSector $targetSector) : array {
1658
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1659
		if ($path === false) {
1660
			create_error('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1661
		}
1662
		$distance = $path->getRelativeDistance();
1663
1664
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
1665
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1666
		return array('turn_cost' => $turnCost, 'max_misjump' => $maxMisjump);
1667
	}
1668
1669
	public function __sleep() {
1670
		return array('accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName', 'npc');
1671
	}
1672
1673
	public function &getStoredDestinations() : array {
1674
		if (!isset($this->storedDestinations)) {
1675
			$this->storedDestinations = array();
1676
			$dbResult = $this->db->read('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1677
			foreach ($dbResult->records() as $dbRecord) {
1678
				$this->storedDestinations[] = array(
1679
					'Label' => $dbRecord->getField('label'),
1680
					'SectorID' => $dbRecord->getInt('sector_id'),
1681
					'OffsetTop' => $dbRecord->getInt('offset_top'),
1682
					'OffsetLeft' => $dbRecord->getInt('offset_left')
1683
				);
1684
			}
1685
		}
1686
		return $this->storedDestinations;
1687
	}
1688
1689
	public function moveDestinationButton(int $sectorID, int $offsetTop, int $offsetLeft) : void {
1690
1691
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1692
			create_error('The saved sector must be in the box!');
1693
		}
1694
1695
		$storedDestinations =& $this->getStoredDestinations();
1696
		foreach ($storedDestinations as &$sd) {
1697
			if ($sd['SectorID'] == $sectorID) {
1698
				$sd['OffsetTop'] = $offsetTop;
1699
				$sd['OffsetLeft'] = $offsetLeft;
1700
				$this->db->write('
1701
					UPDATE player_stored_sector
1702
						SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1703
					WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID)
1704
				);
1705
				return;
1706
			}
1707
		}
1708
1709
		create_error('You do not have a saved sector for #' . $sectorID);
1710
	}
1711
1712
	public function addDestinationButton(int $sectorID, string $label) : void {
1713
1714
		if (!SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1715
			create_error('You want to add a non-existent sector?');
1716
		}
1717
1718
		// sector already stored ?
1719
		foreach ($this->getStoredDestinations() as $sd) {
1720
			if ($sd['SectorID'] == $sectorID) {
1721
				create_error('Sector already stored!');
1722
			}
1723
		}
1724
1725
		$this->storedDestinations[] = array(
1726
			'Label' => $label,
1727
			'SectorID' => (int)$sectorID,
1728
			'OffsetTop' => 1,
1729
			'OffsetLeft' => 1
1730
		);
1731
1732
		$this->db->write('
1733
			INSERT INTO player_stored_sector (account_id, game_id, sector_id, label, offset_top, offset_left)
1734
			VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($sectorID) . ',' . $this->db->escapeString($label) . ',1,1)'
1735
		);
1736
	}
1737
1738
	public function deleteDestinationButton(int $sectorID) : void {
1739
1740
		foreach ($this->getStoredDestinations() as $key => $sd) {
1741
			if ($sd['SectorID'] == $sectorID) {
1742
				$this->db->write('
1743
					DELETE FROM player_stored_sector
1744
					WHERE ' . $this->SQL . '
1745
					AND sector_id = ' . $this->db->escapeNumber($sectorID)
1746
				);
1747
				unset($this->storedDestinations[$key]);
1748
				return;
1749
			}
1750
		}
1751
		throw new Exception('Could not find stored destination');
1752
	}
1753
1754
	public function getTickers() : array {
1755
		if (!isset($this->tickers)) {
1756
			$this->tickers = array();
1757
			//get ticker info
1758
			$dbResult = $this->db->read('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(Smr\Epoch::time()));
1759
			foreach ($dbResult->records() as $dbRecord) {
1760
				$this->tickers[$dbRecord->getField('type')] = [
1761
					'Type' => $dbRecord->getField('type'),
1762
					'Time' => $dbRecord->getInt('time'),
1763
					'Expires' => $dbRecord->getInt('expires'),
1764
					'Recent' => $dbRecord->getField('recent'),
1765
				];
1766
			}
1767
		}
1768
		return $this->tickers;
1769
	}
1770
1771
	public function hasTickers() : bool {
1772
		return count($this->getTickers()) > 0;
1773
	}
1774
1775
	public function getTicker(string $tickerType) : array|false {
1776
		$tickers = $this->getTickers();
1777
		if (isset($tickers[$tickerType])) {
1778
			return $tickers[$tickerType];
1779
		}
1780
		return false;
1781
	}
1782
1783
	public function hasTicker(string $tickerType) : bool {
1784
		return $this->getTicker($tickerType) !== false;
1785
	}
1786
1787
	public function shootForces(SmrForce $forces) : array {
1788
		return $this->getShip()->shootForces($forces);
1789
	}
1790
1791
	public function shootPort(SmrPort $port) : array {
1792
		return $this->getShip()->shootPort($port);
1793
	}
1794
1795
	public function shootPlanet(SmrPlanet $planet, bool $delayed) : array {
1796
		return $this->getShip()->shootPlanet($planet, $delayed);
1797
	}
1798
1799
	public function shootPlayers(array $targetPlayers) : array {
1800
		return $this->getShip()->shootPlayers($targetPlayers);
1801
	}
1802
1803
	public function getMilitaryPayment() : int {
1804
		return $this->militaryPayment;
1805
	}
1806
1807
	public function hasMilitaryPayment() : int {
1808
		return $this->getMilitaryPayment() > 0;
1809
	}
1810
1811
	public function setMilitaryPayment(int $amount) : void {
1812
		if ($this->militaryPayment == $amount) {
1813
			return;
1814
		}
1815
		$this->militaryPayment = $amount;
1816
		$this->hasChanged = true;
1817
	}
1818
1819
	public function increaseMilitaryPayment(int $amount) : void {
1820
		if ($amount < 0) {
1821
			throw new Exception('Trying to increase negative military payment.');
1822
		}
1823
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1824
	}
1825
1826
	public function decreaseMilitaryPayment(int $amount) : void {
1827
		if ($amount < 0) {
1828
			throw new Exception('Trying to decrease negative military payment.');
1829
		}
1830
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1831
	}
1832
1833
	protected function getBountiesData() : void {
1834
		if (!isset($this->bounties)) {
1835
			$this->bounties = array();
1836
			$dbResult = $this->db->read('SELECT * FROM bounty WHERE ' . $this->SQL);
1837
			foreach ($dbResult->records() as $dbRecord) {
1838
				$this->bounties[$dbRecord->getInt('bounty_id')] = array(
1839
							'Amount' => $dbRecord->getInt('amount'),
1840
							'SmrCredits' => $dbRecord->getInt('smr_credits'),
1841
							'Type' => $dbRecord->getField('type'),
1842
							'Claimer' => $dbRecord->getInt('claimer_id'),
1843
							'Time' => $dbRecord->getInt('time'),
1844
							'ID' => $dbRecord->getInt('bounty_id'),
1845
							'New' => false);
1846
			}
1847
		}
1848
	}
1849
1850
	/**
1851
	 * Get bounties that can be claimed by this player.
1852
	 * If specified, $type must be 'HQ' or 'UG'.
1853
	 */
1854
	public function getClaimableBounties(string $type = null) : array {
1855
		$bounties = array();
1856
		$query = 'SELECT * FROM bounty WHERE claimer_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID());
1857
		$query .= match($type) {
1858
			'HQ', 'UG' => ' AND type=' . $this->db->escapeString($type),
1859
			null => '',
1860
		};
1861
		$dbResult = $this->db->read($query);
1862
		foreach ($dbResult->records() as $dbRecord) {
1863
			$bounties[] = array(
1864
				'player' => SmrPlayer::getPlayer($dbRecord->getInt('account_id'), $this->getGameID()),
1865
				'bounty_id' => $dbRecord->getInt('bounty_id'),
1866
				'credits' => $dbRecord->getInt('amount'),
1867
				'smr_credits' => $dbRecord->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' => Smr\Epoch::time(),
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() : void {
2017
		if (!isset($this->HOF)) {
2018
			//Get Player HOF
2019
			$dbResult = $this->db->read('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2020
			$this->HOF = array();
2021
			foreach ($dbResult->records() as $dbRecord) {
2022
				$hof =& $this->HOF;
2023
				$typeList = explode(':', $dbRecord->getString('type'));
2024
				foreach ($typeList as $type) {
2025
					if (!isset($hof[$type])) {
2026
						$hof[$type] = array();
2027
					}
2028
					$hof =& $hof[$type];
2029
				}
2030
				$hof = $dbRecord->getFloat('amount');
2031
			}
2032
			self::getHOFVis();
2033
		}
2034
	}
2035
2036
	public static function getHOFVis() : void {
2037
		if (!isset(self::$HOFVis)) {
2038
			//Get Player HOF Vis
2039
			$db = Smr\Database::getInstance();
2040
			$dbResult = $db->read('SELECT type,visibility FROM hof_visibility');
2041
			self::$HOFVis = array();
2042
			foreach ($dbResult->records() as $dbRecord) {
2043
				self::$HOFVis[$dbRecord->getField('type')] = $dbRecord->getField('visibility');
2044
			}
2045
		}
2046
	}
2047
2048
	public function getHOF(array $typeList = null) : array|float {
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(float $amount, array $typeList, string $visibility) : void {
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(float $amount, array $typeList, string $visibility) : void {
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(float $amount, array $typeList, string $visibility) {
2084
		if (is_array($this->getHOF($typeList))) {
0 ignored issues
show
introduced by
The condition is_array($this->getHOF($typeList)) is always true.
Loading history...
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() : int {
2131
		return $this->computeRanking('experience');
2132
	}
2133
2134
	public function getKillsRank() : int {
2135
		return $this->computeRanking('kills');
2136
	}
2137
2138
	public function getDeathsRank() : int {
2139
		return $this->computeRanking('deaths');
2140
	}
2141
2142
	public function getAssistsRank() : int {
2143
		return $this->computeRanking('assists');
2144
	}
2145
2146
	private function computeRanking(string $dbField) : int {
2147
		$dbResult = $this->db->read('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
		return $dbResult->record()->getInt('ranking');
2157
	}
2158
2159
	public function isUnderAttack() : bool {
2160
		return $this->underAttack;
2161
	}
2162
2163
	public function setUnderAttack(bool $value) : void {
2164
		if ($this->underAttack === $value) {
2165
			return;
2166
		}
2167
		$this->underAttack = $value;
2168
		$this->hasChanged = true;
2169
	}
2170
2171
	public function removeUnderAttack() : bool {
2172
		$session = Smr\Session::getInstance();
2173
		$var = $session->getCurrentVar();
2174
		if (isset($var['UnderAttack'])) {
2175
			return $var['UnderAttack'];
2176
		}
2177
		$underAttack = $this->isUnderAttack();
2178
		if ($underAttack && !USING_AJAX) {
2179
			$session->updateVar('UnderAttack', $underAttack); //Remember we are under attack for AJAX
2180
		}
2181
		$this->setUnderAttack(false);
2182
		return $underAttack;
2183
	}
2184
2185
	public function killPlayer(int $sectorID) : void {
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->write('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) : array {
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->write('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(Smr\Epoch::time()) . ',' . $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
		$killerAlignChange = IRound(-$relation * $alignChangePerRelation); //Lose relations when killing a peaceful race
2263
		if ($killerAlignChange > 0) {
2264
			$killer->increaseAlignment($killerAlignChange);
2265
		} else {
2266
			$killer->decreaseAlignment(-$killerAlignChange);
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->write('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(Smr\Epoch::time() - 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
		$dbResult = $this->db->read($query);
2281
		if ($dbResult->hasRecord()) {
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 ($killerAlignChange > 0) {
2320
				$killer->increaseHOF($killerAlignChange, array('Killing', 'NPC', 'Alignment', 'Gain'), HOF_PUBLIC);
2321
			} else {
2322
				$killer->increaseHOF(-$killerAlignChange, 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 ($killerAlignChange > 0) {
2339
				$killer->increaseHOF($killerAlignChange, array('Killing', 'Alignment', 'Gain'), HOF_PUBLIC);
2340
			} else {
2341
				$killer->increaseHOF(-$killerAlignChange, 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->write('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) : array {
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->write('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(Smr\Epoch::time()) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($owner->getAccountID()) . ',' . $this->db->escapeNumber($owner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
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) : array {
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->write('INSERT INTO news (game_id, time, news_message,killer_id,dead_id,dead_alliance)
2424
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(Smr\Epoch::time()) . ', ' . $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) : array {
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->write('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(Smr\Epoch::time()) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($planetOwner->getAccountID()) . ',' . $this->db->escapeNumber($planetOwner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
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(int $otherID) : void {
2485
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2486
		$this->db->write('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(int $otherID) : void {
2490
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2491
		$this->db->write('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() : string {
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() : string {
2511
		return match($this->getTurnsLevel()) {
2512
			'NONE', 'LOW' => 'red',
2513
			'MEDIUM' => 'yellow',
2514
			'HIGH' => 'green',
2515
		};
2516
	}
2517
2518
	public function getTurns() : int {
2519
		return $this->turns;
2520
	}
2521
2522
	public function hasTurns() : bool {
2523
		return $this->turns > 0;
2524
	}
2525
2526
	public function getMaxTurns() : int {
2527
		return $this->getGame()->getMaxTurns();
2528
	}
2529
2530
	public function setTurns(int $turns) : void {
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(int $take, int $takeNewbie = 0) : void {
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(Smr\Epoch::time());
2555
		$this->updateLastCPLAction();
2556
	}
2557
2558
	public function giveTurns(int $give, int $giveNewbie = 0) : void {
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(int $time, bool $forceUpdate = false) : int {
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() : void {
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(int $time, bool $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() : void {
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(Smr\Epoch::time());
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() : int {
2616
		return $this->lastTurnUpdate;
2617
	}
2618
2619
	public function setLastTurnUpdate(int $time) : void {
2620
		if ($this->lastTurnUpdate == $time) {
2621
			return;
2622
		}
2623
		$this->lastTurnUpdate = $time;
2624
		$this->hasChanged = true;
2625
	}
2626
2627
	public function getLastActive() : int {
2628
		return $this->lastActive;
2629
	}
2630
2631
	public function setLastActive(int $lastActive) : void {
2632
		if ($this->lastActive == $lastActive) {
2633
			return;
2634
		}
2635
		$this->lastActive = $lastActive;
2636
		$this->hasChanged = true;
2637
	}
2638
2639
	public function getLastCPLAction() : int {
2640
		return $this->lastCPLAction;
2641
	}
2642
2643
	public function setLastCPLAction(int $time) : void {
2644
		if ($this->lastCPLAction == $time) {
2645
			return;
2646
		}
2647
		$this->lastCPLAction = $time;
2648
		$this->hasChanged = true;
2649
	}
2650
2651
	public function updateLastCPLAction() : void {
2652
		$this->setLastCPLAction(Smr\Epoch::time());
2653
	}
2654
2655
	public function setNewbieWarning(bool $bool) : void {
2656
		if ($this->newbieWarning == $bool) {
2657
			return;
2658
		}
2659
		$this->newbieWarning = $bool;
2660
		$this->hasChanged = true;
2661
	}
2662
2663
	public function getNewbieWarning() : bool {
2664
		return $this->newbieWarning;
2665
	}
2666
2667
	public function isDisplayMissions() : bool {
2668
		return $this->displayMissions;
2669
	}
2670
2671
	public function setDisplayMissions(bool $bool) : void {
2672
		if ($this->displayMissions == $bool) {
2673
			return;
2674
		}
2675
		$this->displayMissions = $bool;
2676
		$this->hasChanged = true;
2677
	}
2678
2679
	public function getMissions() : array {
2680
		if (!isset($this->missions)) {
2681
			$dbResult = $this->db->read('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2682
			$this->missions = array();
2683
			foreach ($dbResult->records() as $dbRecord) {
2684
				$missionID = $dbRecord->getInt('mission_id');
2685
				$this->missions[$missionID] = array(
2686
					'On Step' => $dbRecord->getInt('on_step'),
2687
					'Progress' => $dbRecord->getInt('progress'),
2688
					'Unread' => $dbRecord->getBoolean('unread'),
2689
					'Expires' => $dbRecord->getInt('step_fails'),
2690
					'Sector' => $dbRecord->getInt('mission_sector'),
2691
					'Starting Sector' => $dbRecord->getInt('starting_sector')
2692
				);
2693
				$this->rebuildMission($missionID);
2694
			}
2695
		}
2696
		return $this->missions;
2697
	}
2698
2699
	public function getActiveMissions() : array {
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(int $missionID) : array|false {
2710
		$missions = $this->getMissions();
2711
		if (isset($missions[$missionID])) {
2712
			return $missions[$missionID];
2713
		}
2714
		return false;
2715
	}
2716
2717
	protected function hasMission(int $missionID) : bool {
2718
		return $this->getMission($missionID) !== false;
2719
	}
2720
2721
	protected function updateMission(int $missionID) : bool {
2722
		$this->getMissions();
2723
		if (isset($this->missions[$missionID])) {
2724
			$mission = $this->missions[$missionID];
2725
			$this->db->write('
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(int $missionID) : void {
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(int $missionID) : void {
2766
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2767
		$this->addMission($missionID, $finishedStep);
2768
	}
2769
2770
	public function addMission(int $missionID, int $step = 0) : void {
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' => (Smr\Epoch::time() + 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->write('
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(int $missionID) : void {
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(int $missionID) : void {
2813
		$this->getMissions();
2814
		if (isset($this->missions[$missionID])) {
2815
			unset($this->missions[$missionID]);
2816
			$this->db->write('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1');
2817
			return;
2818
		}
2819
		throw new Exception('Mission with ID not found: ' . $missionID);
2820
	}
2821
2822
	public function markMissionsRead() : array {
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(int $missionID) : string {
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() : array {
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) : bool {
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) : bool {
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) : bool {
2951
		return $otherPlayer !== null && $this->getAccountID() == $otherPlayer->getAccountID() && $this->getGameID() == $otherPlayer->getGameID();
2952
	}
2953
2954
	public function sameAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
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) : bool {
2959
		return $this->sameAlliance($otherPlayer);
2960
	}
2961
2962
	public function forceNAPAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
2963
		return $this->sameAlliance($otherPlayer);
2964
	}
2965
2966
	public function planetNAPAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
2967
		return $this->sameAlliance($otherPlayer);
2968
	}
2969
2970
	public function traderNAPAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
2971
		return $this->sameAlliance($otherPlayer);
2972
	}
2973
2974
	public function traderMAPAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
2975
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
2976
	}
2977
2978
	public function traderAttackTraderAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
2979
		return $this->sameAlliance($otherPlayer);
2980
	}
2981
2982
	public function traderDefendTraderAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
2983
		return $this->sameAlliance($otherPlayer);
2984
	}
2985
2986
	public function traderAttackForceAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
2987
		return $this->sameAlliance($otherPlayer);
2988
	}
2989
2990
	public function traderAttackPortAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
2991
		return $this->sameAlliance($otherPlayer);
2992
	}
2993
2994
	public function traderAttackPlanetAlliance(AbstractSmrPlayer $otherPlayer = null) : bool {
2995
		return $this->sameAlliance($otherPlayer);
2996
	}
2997
2998
	public function meetsAlignmentRestriction(int $restriction) : bool {
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() : array {
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
	 * Returns an array of all unvisited sectors.
3022
	 */
3023
	public function getUnvisitedSectors() : array {
3024
		if (!isset($this->unvisitedSectors)) {
3025
			$this->unvisitedSectors = array();
3026
			// Note that this table actually has entries for the *unvisited* sectors!
3027
			$dbResult = $this->db->read('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
3028
			foreach ($dbResult->records() as $dbRecord) {
3029
				$this->unvisitedSectors[] = $dbRecord->getInt('sector_id');
3030
			}
3031
		}
3032
		return $this->unvisitedSectors;
3033
	}
3034
3035
	/**
3036
	 * Check if player has visited the input sector.
3037
	 * Note that this populates the list of *all* unvisited sectors!
3038
	 */
3039
	public function hasVisitedSector(int $sectorID) : bool {
3040
		return !in_array($sectorID, $this->getUnvisitedSectors());
3041
	}
3042
3043
	public function getLeaveNewbieProtectionHREF() : string {
3044
		return Page::create('leave_newbie_processing.php')->href();
3045
	}
3046
3047
	public function getExamineTraderHREF() : string {
3048
		$container = Page::create('skeleton.php', 'trader_examine.php');
3049
		$container['target'] = $this->getAccountID();
3050
		return $container->href();
3051
	}
3052
3053
	public function getAttackTraderHREF() : string {
3054
		return Globals::getAttackTraderHREF($this->getAccountID());
3055
	}
3056
3057
	public function getPlanetKickHREF() : string {
3058
		$container = Page::create('planet_kick_processing.php', 'trader_attack_processing.php');
3059
		$container['account_id'] = $this->getAccountID();
3060
		return $container->href();
3061
	}
3062
3063
	public function getTraderSearchHREF() : string {
3064
		$container = Page::create('skeleton.php', 'trader_search_result.php');
3065
		$container['player_id'] = $this->getPlayerID();
3066
		return $container->href();
3067
	}
3068
3069
	public function getAllianceRosterHREF() : string {
3070
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3071
	}
3072
3073
	public function getToggleWeaponHidingHREF(bool $ajax = false) : string {
3074
		$container = Page::create('toggle_processing.php');
3075
		$container['toggle'] = 'WeaponHiding';
3076
		$container['AJAX'] = $ajax;
3077
		return $container->href();
3078
	}
3079
3080
	public function isDisplayWeapons() : bool {
3081
		return $this->displayWeapons;
3082
	}
3083
3084
	/**
3085
	 * Should weapons be displayed in the right panel?
3086
	 * This updates the player database directly because it is used with AJAX,
3087
	 * which does not acquire a sector lock.
3088
	 */
3089
	public function setDisplayWeapons(bool $bool) : void {
3090
		if ($this->displayWeapons == $bool) {
3091
			return;
3092
		}
3093
		$this->displayWeapons = $bool;
3094
		$this->db->write('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3095
	}
3096
3097
	public function update() : void {
3098
		$this->save();
3099
	}
3100
3101
	public function save() : void {
3102
		if ($this->hasChanged === true) {
3103
			$this->db->write('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3104
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3105
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3106
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3107
				', turns=' . $this->db->escapeNumber($this->turns) .
3108
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3109
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3110
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3111
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3112
				', dead=' . $this->db->escapeBoolean($this->dead) .
3113
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3114
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3115
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3116
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3117
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3118
				', credits=' . $this->db->escapeNumber($this->credits) .
3119
				', experience=' . $this->db->escapeNumber($this->experience) .
3120
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3121
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3122
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3123
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3124
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3125
				', kills=' . $this->db->escapeNumber($this->kills) .
3126
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3127
				', assists=' . $this->db->escapeNumber($this->assists) .
3128
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3129
				', bank=' . $this->db->escapeNumber($this->bank) .
3130
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3131
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3132
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3133
				', group_scout_messages=' . $this->db->escapeString($this->groupScoutMessages) .
3134
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3135
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3136
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3137
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3138
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3139
				', under_attack = ' . $this->db->escapeBoolean($this->underAttack) .
3140
				' WHERE ' . $this->SQL . ' LIMIT 1');
3141
			$this->hasChanged = false;
3142
		}
3143
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3144
			if ($bountyChanged === true) {
3145
				$bountyChanged = false;
3146
				$bounty = $this->getBounty($key);
3147
				if ($bounty['New'] === true) {
3148
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3149
						$this->db->write('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']) . ')');
3150
					}
3151
				} else {
3152
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3153
						$this->db->write('UPDATE bounty
3154
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3155
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3156
							type=' . $this->db->escapeString($bounty['Type']) . ',
3157
							claimer_id=' . $this->db->escapeNumber($bounty['Claimer']) . ',
3158
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3159
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3160
					} else {
3161
						$this->db->write('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3162
					}
3163
				}
3164
			}
3165
		}
3166
		$this->saveHOF();
3167
	}
3168
3169
	public function saveHOF() : void {
3170
		if (count($this->hasHOFChanged) > 0) {
3171
			$this->doHOFSave($this->hasHOFChanged);
3172
			$this->hasHOFChanged = [];
3173
		}
3174
		if (!empty(self::$hasHOFVisChanged)) {
3175
			foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3176
				if ($changeType == self::HOF_NEW) {
3177
					$this->db->write('INSERT INTO hof_visibility (type, visibility) VALUES (' . $this->db->escapeString($hofType) . ',' . $this->db->escapeString(self::$HOFVis[$hofType]) . ')');
3178
				} else {
3179
					$this->db->write('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType) . ' LIMIT 1');
3180
				}
3181
				unset(self::$hasHOFVisChanged[$hofType]);
3182
			}
3183
		}
3184
	}
3185
3186
	/**
3187
	 * This should only be called by `saveHOF` (and recursively) to
3188
	 * ensure that the `hasHOFChanged` attribute is properly cleared.
3189
	 */
3190
	protected function doHOFSave(array $hasChangedList, array $typeList = array()) {
3191
		foreach ($hasChangedList as $type => $hofChanged) {
3192
			$tempTypeList = $typeList;
3193
			$tempTypeList[] = $type;
3194
			if (is_array($hofChanged)) {
3195
				$this->doHOFSave($hofChanged, $tempTypeList);
3196
			} else {
3197
				$amount = $this->getHOF($tempTypeList);
3198
				if ($hofChanged == self::HOF_NEW) {
3199
					if ($amount > 0) {
3200
						$this->db->write('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) . ')');
3201
					}
3202
				} elseif ($hofChanged == self::HOF_CHANGED) {
3203
					$this->db->write('UPDATE player_hof
3204
						SET amount=' . $this->db->escapeNumber($amount) . '
3205
						WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeArray($tempTypeList, ':', false) . ' LIMIT 1');
3206
				}
3207
			}
3208
		}
3209
	}
3210
3211
}
3212