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 — live ( a21a05...18f3f5 )
by Dan
20s queued 16s
created

AbstractSmrPlayer::getDeathsRank()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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