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::doMessageSending()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 49
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php declare(strict_types=1);
2
require_once('missions.inc.php');
3
4
// Exception thrown when a player cannot be found in the database
5
class PlayerNotFoundException extends Exception {}
6
7
abstract class AbstractSmrPlayer {
8
	use Traits\RaceID;
9
10
	const TIME_FOR_FEDERAL_BOUNTY_ON_PR = 10800;
11
	const TIME_FOR_ALLIANCE_SWITCH = 0;
12
13
	const SHIP_INSURANCE_FRACTION = 0.25; // ship value regained on death
14
15
	const HOF_CHANGED = 1;
16
	const HOF_NEW = 2;
17
18
	protected static array $CACHE_SECTOR_PLAYERS = [];
19
	protected static array $CACHE_PLANET_PLAYERS = [];
20
	protected static array $CACHE_ALLIANCE_PLAYERS = [];
21
	protected static array $CACHE_PLAYERS = [];
22
23
	protected Smr\Database $db;
24
	protected string $SQL;
25
26
	protected int $accountID;
27
	protected int $gameID;
28
	protected string $playerName;
29
	protected int $playerID;
30
	protected int $sectorID;
31
	protected int $lastSectorID;
32
	protected int $newbieTurns;
33
	protected bool $dead;
34
	protected bool $npc = false; // initialized for legacy combat logs
35
	protected bool $newbieStatus;
36
	protected bool $newbieWarning;
37
	protected bool $landedOnPlanet;
38
	protected int $lastActive;
39
	protected int $credits;
40
	protected int $alignment;
41
	protected int $experience;
42
	protected ?int $level;
43
	protected int $allianceID;
44
	protected int $shipID;
45
	protected int $kills;
46
	protected int $deaths;
47
	protected int $assists;
48
	protected array $personalRelations;
49
	protected array $relations;
50
	protected int $militaryPayment;
51
	protected array $bounties;
52
	protected int $turns;
53
	protected int $lastCPLAction;
54
	protected array $missions;
55
56
	protected array $tickers;
57
	protected int $lastTurnUpdate;
58
	protected int $lastNewsUpdate;
59
	protected string $attackColour;
60
	protected int $allianceJoinable;
61
	protected int $lastPort;
62
	protected int $bank;
63
	protected int $zoom;
64
	protected bool $displayMissions;
65
	protected bool $displayWeapons;
66
	protected bool $forceDropMessages;
67
	protected string $groupScoutMessages;
68
	protected bool $ignoreGlobals;
69
	protected Distance|false $plottedCourse;
70
	protected int $plottedCourseFrom;
71
	protected bool $nameChanged;
72
	protected bool $raceChanged;
73
	protected bool $combatDronesKamikazeOnMines;
74
	protected string|false $customShipName;
75
	protected array $storedDestinations;
76
	protected array $canFed;
77
	protected bool $underAttack;
78
79
	protected array $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