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::setGroupScoutMessages()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 6
rs 10
c 0
b 0
f 0
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