Scrutinizer GitHub App not installed

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

Install GitHub App

Completed
Push — master ( f5ba33...c2238a )
by Dan
19s queued 15s
created

AbstractSmrPlayer::doMessageSending()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 49
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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