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

Passed
Push — master ( b86122...fc41d3 )
by Dan
05:09
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 $CACHE_SECTOR_PLAYERS = array();
19
	protected static $CACHE_PLANET_PLAYERS = array();
20
	protected static $CACHE_ALLIANCE_PLAYERS = array();
21
	protected static $CACHE_PLAYERS = array();
22
23
	protected $db;
24
	protected $SQL;
25
26
	protected $accountID;
27
	protected $gameID;
28
	protected $playerName;
29
	protected $playerID;
30
	protected $sectorID;
31
	protected $lastSectorID;
32
	protected $newbieTurns;
33
	protected $dead;
34
	protected $npc;
35
	protected $newbieStatus;
36
	protected $newbieWarning;
37
	protected $landedOnPlanet;
38
	protected $lastActive;
39
	protected $credits;
40
	protected $alignment;
41
	protected $experience;
42
	protected $level;
43
	protected $allianceID;
44
	protected $shipID;
45
	protected $kills;
46
	protected $deaths;
47
	protected $assists;
48
	protected $stats;
49
	protected $personalRelations;
50
	protected $relations;
51
	protected $militaryPayment;
52
	protected $bounties;
53
	protected $turns;
54
	protected $lastCPLAction;
55
	protected $missions;
56
57
	protected $tickers;
58
	protected $lastTurnUpdate;
59
	protected $lastNewsUpdate;
60
	protected $attackColour;
61
	protected $allianceJoinable;
62
	protected $lastPort;
63
	protected $bank;
64
	protected $zoom;
65
	protected $displayMissions;
66
	protected $displayWeapons;
67
	protected $forceDropMessages;
68
	protected $groupScoutMessages;
69
	protected $ignoreGlobals;
70
	protected $plottedCourse;
71
	protected $plottedCourseFrom;
72
	protected $nameChanged;
73
	protected bool $raceChanged;
74
	protected $combatDronesKamikazeOnMines;
75
	protected $customShipName;
76
	protected $storedDestinations;
77
	protected $canFed;
78
79
	protected $visitedSectors;
80
	protected $allianceRoles = array(
81
		0 => 0
82
	);
83
84
	protected $draftLeader;
85
	protected $gpWriter;
86
	protected $HOF;
87
	protected static $HOFVis;
88
89
	protected $hasChanged = false;
90
	protected array $hasHOFChanged = [];
91
	protected static $hasHOFVisChanged = array();
92
	protected $hasBountyChanged = array();
93
94
	public static function refreshCache() {
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() {
103
		self::$CACHE_PLAYERS = array();
104
		self::$CACHE_SECTOR_PLAYERS = array();
105
	}
106
107
	public static function savePlayers() {
108
		foreach (self::$CACHE_PLAYERS as $gamePlayers) {
109
			foreach ($gamePlayers as $player) {
110
				$player->save();
111
			}
112
		}
113
	}
114
115
	public static function getSectorPlayersByAlliances($gameID, $sectorID, array $allianceIDs, $forceUpdate = false) {
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($gameID, $galaxyID, $forceUpdate = false) {
131
		$db = MySqlDatabase::getInstance();
132
		$db->query('SELECT player.*, sector_id FROM sector LEFT JOIN player USING(game_id, sector_id) WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(SmrSession::getTime() - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND galaxy_id = ' . $db->escapeNumber($galaxyID));
133
		$galaxyPlayers = [];
134
		while ($db->nextRecord()) {
135
			$sectorID = $db->getInt('sector_id');
136
			if (!$db->hasField('account_id')) {
137
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = [];
138
			} else {
139
				$accountID = $db->getInt('account_id');
140
				$player = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
141
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$accountID] = $player;
142
				$galaxyPlayers[$sectorID][$accountID] = $player;
143
			}
144
		}
145
		return $galaxyPlayers;
146
	}
147
148
	public static function getSectorPlayers($gameID, $sectorID, $forceUpdate = false) {
149
		if ($forceUpdate || !isset(self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID])) {
150
			$db = MySqlDatabase::getInstance();
151
			$db->query('SELECT * FROM player WHERE sector_id = ' . $db->escapeNumber($sectorID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(SmrSession::getTime() - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
152
			$players = array();
153
			while ($db->nextRecord()) {
154
				$accountID = $db->getInt('account_id');
155
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
156
			}
157
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = $players;
158
		}
159
		return self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID];
160
	}
161
162
	public static function getPlanetPlayers($gameID, $sectorID, $forceUpdate = false) {
163
		if ($forceUpdate || !isset(self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID])) {
164
			$db = MySqlDatabase::getInstance();
165
			$db->query('SELECT * FROM player WHERE sector_id = ' . $db->escapeNumber($sectorID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(true) . ' AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
166
			$players = array();
167
			while ($db->nextRecord()) {
168
				$accountID = $db->getInt('account_id');
169
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
170
			}
171
			self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID] = $players;
172
		}
173
		return self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID];
174
	}
175
176
	public static function getAlliancePlayers($gameID, $allianceID, $forceUpdate = false) {
177
		if ($forceUpdate || !isset(self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID])) {
178
			$db = MySqlDatabase::getInstance();
179
			$db->query('SELECT * FROM player WHERE alliance_id = ' . $db->escapeNumber($allianceID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' ORDER BY experience DESC');
180
			$players = array();
181
			while ($db->nextRecord()) {
182
				$accountID = $db->getInt('account_id');
183
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
184
			}
185
			self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID] = $players;
186
		}
187
		return self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID];
188
	}
189
190
	public static function getPlayer($accountID, $gameID, $forceUpdate = false, $db = null) {
191
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$accountID])) {
192
			self::$CACHE_PLAYERS[$gameID][$accountID] = new SmrPlayer($gameID, $accountID, $db);
193
		}
194
		return self::$CACHE_PLAYERS[$gameID][$accountID];
195
	}
196
197
	public static function getPlayerByPlayerID($playerID, $gameID, $forceUpdate = false) {
198
		$db = MySqlDatabase::getInstance();
199
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_id = ' . $db->escapeNumber($playerID) . ' LIMIT 1');
200
		if ($db->nextRecord()) {
201
			return self::getPlayer($db->getInt('account_id'), $gameID, $forceUpdate, $db);
202
		}
203
		throw new PlayerNotFoundException('Player ID not found.');
204
	}
205
206
	public static function getPlayerByPlayerName($playerName, $gameID, $forceUpdate = false) {
207
		$db = MySqlDatabase::getInstance();
208
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
209
		if ($db->nextRecord()) {
210
			return self::getPlayer($db->getInt('account_id'), $gameID, $forceUpdate, $db);
211
		}
212
		throw new PlayerNotFoundException('Player Name not found.');
213
	}
214
215
	protected function __construct($gameID, $accountID, $db = null) {
216
		$this->db = MySqlDatabase::getInstance();
217
		$this->SQL = 'account_id = ' . $this->db->escapeNumber($accountID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
218
219
		if (isset($db)) {
220
			$playerExists = true;
221
		} else {
222
			$db = $this->db;
223
			$this->db->query('SELECT * FROM player WHERE ' . $this->SQL . ' LIMIT 1');
224
			$playerExists = $db->nextRecord();
225
		}
226
227
		if ($playerExists) {
228
			$this->accountID = (int)$accountID;
229
			$this->gameID = (int)$gameID;
230
			$this->playerName = $db->getField('player_name');
231
			$this->playerID = $db->getInt('player_id');
232
			$this->sectorID = $db->getInt('sector_id');
233
			$this->lastSectorID = $db->getInt('last_sector_id');
234
			$this->turns = $db->getInt('turns');
235
			$this->lastTurnUpdate = $db->getInt('last_turn_update');
236
			$this->newbieTurns = $db->getInt('newbie_turns');
237
			$this->lastNewsUpdate = $db->getInt('last_news_update');
238
			$this->attackColour = $db->getField('attack_warning');
239
			$this->dead = $db->getBoolean('dead');
240
			$this->npc = $db->getBoolean('npc');
241
			$this->newbieStatus = $db->getBoolean('newbie_status');
242
			$this->landedOnPlanet = $db->getBoolean('land_on_planet');
243
			$this->lastActive = $db->getInt('last_active');
244
			$this->lastCPLAction = $db->getInt('last_cpl_action');
245
			$this->raceID = $db->getInt('race_id');
246
			$this->credits = $db->getInt('credits');
247
			$this->experience = $db->getInt('experience');
248
			$this->alignment = $db->getInt('alignment');
249
			$this->militaryPayment = $db->getInt('military_payment');
250
			$this->allianceID = $db->getInt('alliance_id');
251
			$this->allianceJoinable = $db->getInt('alliance_join');
252
			$this->shipID = $db->getInt('ship_type_id');
253
			$this->kills = $db->getInt('kills');
254
			$this->deaths = $db->getInt('deaths');
255
			$this->assists = $db->getInt('assists');
256
			$this->lastPort = $db->getInt('last_port');
257
			$this->bank = $db->getInt('bank');
258
			$this->zoom = $db->getInt('zoom');
259
			$this->displayMissions = $db->getBoolean('display_missions');
260
			$this->displayWeapons = $db->getBoolean('display_weapons');
261
			$this->forceDropMessages = $db->getBoolean('force_drop_messages');
262
			$this->groupScoutMessages = $db->getField('group_scout_messages');
263
			$this->ignoreGlobals = $db->getBoolean('ignore_globals');
264
			$this->newbieWarning = $db->getBoolean('newbie_warning');
265
			$this->nameChanged = $db->getBoolean('name_changed');
266
			$this->raceChanged = $db->getBoolean('race_changed');
267
			$this->combatDronesKamikazeOnMines = $db->getBoolean('combat_drones_kamikaze_on_mines');
268
		} else {
269
			throw new PlayerNotFoundException('Invalid accountID: ' . $accountID . ' OR gameID:' . $gameID);
270
		}
271
	}
272
273
	/**
274
	 * Insert a new player into the database. Returns the new player object.
275
	 */
276
	public static function createPlayer($accountID, $gameID, $playerName, $raceID, $isNewbie, $npc = false) {
277
		$time = SmrSession::getTime();
278
		$db = MySqlDatabase::getInstance();
279
		$db->lockTable('player');
280
281
		// Player names must be unique within each game
282
		try {
283
			self::getPlayerByPlayerName($playerName, $gameID);
284
			$db->unlock();
285
			throw new \Smr\UserException('That player name already exists.');
286
		} catch (PlayerNotFoundException $e) {
287
			// Player name does not yet exist, we may proceed
288
		}
289
290
		// get last registered player id in that game and increase by one.
291
		$db->query('SELECT MAX(player_id) FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
292
		if ($db->nextRecord()) {
293
			$playerID = $db->getInt('MAX(player_id)') + 1;
294
		} else {
295
			$playerID = 1;
296
		}
297
298
		$startSectorID = 0; // Temporarily put player into non-existent sector
299
		$db->query('INSERT INTO player (account_id, game_id, player_id, player_name, race_id, sector_id, last_cpl_action, last_active, npc, newbie_status)
300
					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) . ')');
301
302
		$db->unlock();
303
304
		$player = SmrPlayer::getPlayer($accountID, $gameID);
305
		$player->setSectorID($player->getHome());
306
		return $player;
307
	}
308
309
	/**
310
	 * Get array of players whose info can be accessed by this player.
311
	 * Skips players who are not in the same alliance as this player.
312
	 */
313
	public function getSharingPlayers($forceUpdate = false) {
314
		$results = array($this);
315
316
		// Only return this player if not in an alliance
317
		if (!$this->hasAlliance()) {
318
			return $results;
319
		}
320
321
		// Get other players who are sharing info for this game.
322
		// NOTE: game_id=0 means that player shares info for all games.
323
		$this->db->query('SELECT from_account_id FROM account_shares_info WHERE to_account_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND (game_id=0 OR game_id=' . $this->db->escapeNumber($this->getGameID()) . ')');
324
		while ($this->db->nextRecord()) {
325
			try {
326
				$otherPlayer = SmrPlayer::getPlayer($this->db->getInt('from_account_id'),
327
				                                    $this->getGameID(), $forceUpdate);
328
			} catch (PlayerNotFoundException $e) {
329
				// Skip players that have not joined this game
330
				continue;
331
			}
332
333
			// players must be in the same alliance
334
			if ($this->sameAlliance($otherPlayer)) {
335
				$results[] = $otherPlayer;
336
			}
337
		}
338
		return $results;
339
	}
340
341
	public function getSQL() : string {
342
		return $this->SQL;
343
	}
344
345
	public function getZoom() {
346
		return $this->zoom;
347
	}
348
349
	protected function setZoom($zoom) {
350
		// Set the zoom level between [1, 9]
351
		$zoom = max(1, min(9, $zoom));
352
		if ($this->zoom == $zoom) {
353
			return;
354
		}
355
		$this->zoom = $zoom;
356
		$this->hasChanged = true;
357
	}
358
359
	public function increaseZoom($zoom) {
360
		if ($zoom < 0) {
361
			throw new Exception('Trying to increase negative zoom.');
362
		}
363
		$this->setZoom($this->getZoom() + $zoom);
364
	}
365
366
	public function decreaseZoom($zoom) {
367
		if ($zoom < 0) {
368
			throw new Exception('Trying to decrease negative zoom.');
369
		}
370
		$this->setZoom($this->getZoom() - $zoom);
371
	}
372
373
	public function getAttackColour() {
374
		return $this->attackColour;
375
	}
376
377
	public function setAttackColour($colour) {
378
		if ($this->attackColour == $colour) {
379
			return;
380
		}
381
		$this->attackColour = $colour;
382
		$this->hasChanged = true;
383
	}
384
385
	public function isIgnoreGlobals() {
386
		return $this->ignoreGlobals;
387
	}
388
389
	public function setIgnoreGlobals($bool) {
390
		if ($this->ignoreGlobals == $bool) {
391
			return;
392
		}
393
		$this->ignoreGlobals = $bool;
394
		$this->hasChanged = true;
395
	}
396
397
	public function getAccount() {
398
		return SmrAccount::getAccount($this->getAccountID());
399
	}
400
401
	public function getAccountID() {
402
		return $this->accountID;
403
	}
404
405
	public function getGameID() {
406
		return $this->gameID;
407
	}
408
409
	public function getGame() {
410
		return SmrGame::getGame($this->gameID);
411
	}
412
413
	public function getNewbieTurns() {
414
		return $this->newbieTurns;
415
	}
416
417
	public function hasNewbieTurns() {
418
		return $this->getNewbieTurns() > 0;
419
	}
420
	public function setNewbieTurns($newbieTurns) {
421
		if ($this->newbieTurns == $newbieTurns) {
422
			return;
423
		}
424
		$this->newbieTurns = $newbieTurns;
425
		$this->hasChanged = true;
426
	}
427
428
	public function getShip($forceUpdate = false) {
429
		return SmrShip::getShip($this, $forceUpdate);
430
	}
431
432
	public function getShipTypeID() {
433
		return $this->shipID;
434
	}
435
436
	/**
437
	 * Do not call directly. Use SmrShip::setShipTypeID instead.
438
	 */
439
	public function setShipTypeID($shipID) {
440
		if ($this->shipID == $shipID) {
441
			return;
442
		}
443
		$this->shipID = $shipID;
444
		$this->hasChanged = true;
445
	}
446
447
	public function hasCustomShipName() {
448
		return $this->getCustomShipName() !== false;
449
	}
450
451
	public function getCustomShipName() {
452
		if (!isset($this->customShipName)) {
453
			$this->db->query('SELECT * FROM ship_has_name WHERE ' . $this->SQL . ' LIMIT 1');
454
			if ($this->db->nextRecord()) {
455
				$this->customShipName = $this->db->getField('ship_name');
456
			} else {
457
				$this->customShipName = false;
458
			}
459
		}
460
		return $this->customShipName;
461
	}
462
463
	public function setCustomShipName(string $name) {
464
		$this->db->query('REPLACE INTO ship_has_name (game_id, account_id, ship_name)
465
			VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeString($name) . ')');
466
	}
467
468
	/**
469
	 * Get planet owned by this player.
470
	 * Returns false if this player does not own a planet.
471
	 */
472
	public function getPlanet() {
473
		$this->db->query('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_id=' . $this->db->escapeNumber($this->getAccountID()));
474
		if ($this->db->nextRecord()) {
475
			return SmrPlanet::getPlanet($this->getGameID(), $this->db->getInt('sector_id'), false, $this->db);
476
		} else {
477
			return false;
478
		}
479
	}
480
481
	public function getSectorPlanet() {
482
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
483
	}
484
485
	public function getSectorPort() {
486
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
487
	}
488
489
	public function getSectorID() {
490
		return $this->sectorID;
491
	}
492
493
	public function getSector() {
494
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
495
	}
496
497
	public function setSectorID($sectorID) {
498
		if ($this->sectorID == $sectorID) {
499
			return;
500
		}
501
502
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
503
		$port->addCachePort($this->getAccountID()); //Add port of sector we were just in, to make sure it is left totally up to date.
504
505
		$this->setLastSectorID($this->getSectorID());
506
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
507
		$this->sectorID = $sectorID;
508
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
509
		$this->hasChanged = true;
510
511
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
512
		$port->addCachePort($this->getAccountID()); //Add the port of sector we are now in.
513
	}
514
515
	public function getLastSectorID() {
516
		return $this->lastSectorID;
517
	}
518
519
	public function setLastSectorID($lastSectorID) {
520
		if ($this->lastSectorID == $lastSectorID) {
521
			return;
522
		}
523
		$this->lastSectorID = $lastSectorID;
524
		$this->hasChanged = true;
525
	}
526
527
	public function getHome() {
528
		// get his home sector
529
		$hq_id = GOVERNMENT + $this->getRaceID();
530
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
531
		if (!empty($raceHqSectors)) {
532
			// If race has multiple HQ's for some reason, use the first one
533
			return key($raceHqSectors);
534
		} else {
535
			return 1;
536
		}
537
	}
538
539
	public function isDead() {
540
		return $this->dead;
541
	}
542
543
	public function isNPC() {
544
		return $this->npc;
545
	}
546
547
	/**
548
	 * Does the player have Newbie status?
549
	 */
550
	public function hasNewbieStatus() {
551
		return $this->newbieStatus;
552
	}
553
554
	/**
555
	 * Update the player's newbie status if it has changed.
556
	 * This function queries the account, so use sparingly.
557
	 */
558
	public function updateNewbieStatus() {
559
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
560
		if ($this->newbieStatus != $accountNewbieStatus) {
561
			$this->newbieStatus = $accountNewbieStatus;
562
			$this->hasChanged = true;
563
		}
564
	}
565
566
	/**
567
	 * Has this player been designated as the alliance flagship?
568
	 */
569
	public function isFlagship() {
570
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipID() == $this->getAccountID();
571
	}
572
573
	public function isPresident() {
574
		return Council::getPresidentID($this->getGameID(), $this->getRaceID()) == $this->getAccountID();
575
	}
576
577
	public function isOnCouncil() {
578
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getAccountID());
579
	}
580
581
	public function isDraftLeader() {
582
		if (!isset($this->draftLeader)) {
583
			$this->draftLeader = false;
584
			$this->db->query('SELECT 1 FROM draft_leaders WHERE ' . $this->SQL . ' LIMIT 1');
585
			if ($this->db->nextRecord()) {
586
				$this->draftLeader = true;
587
			}
588
		}
589
		return $this->draftLeader;
590
	}
591
592
	public function getGPWriter() {
593
		if (!isset($this->gpWriter)) {
594
			$this->gpWriter = false;
595
			$this->db->query('SELECT position FROM galactic_post_writer WHERE ' . $this->SQL);
596
			if ($this->db->nextRecord()) {
597
				$this->gpWriter = $this->db->getField('position');
598
			}
599
		}
600
		return $this->gpWriter;
601
	}
602
603
	public function isGPEditor() {
604
		return $this->getGPWriter() == 'editor';
605
	}
606
607
	public function isForceDropMessages() {
608
		return $this->forceDropMessages;
609
	}
610
611
	public function setForceDropMessages($bool) {
612
		if ($this->forceDropMessages == $bool) {
613
			return;
614
		}
615
		$this->forceDropMessages = $bool;
616
		$this->hasChanged = true;
617
	}
618
619
	public function getScoutMessageGroupLimit() {
620
		if ($this->groupScoutMessages == 'ALWAYS') {
621
			return 0;
622
		} elseif ($this->groupScoutMessages == 'AUTO') {
623
			return MESSAGES_PER_PAGE;
624
		} elseif ($this->groupScoutMessages == 'NEVER') {
625
			return PHP_INT_MAX;
626
		}
627
	}
628
629
	public function getGroupScoutMessages() {
630
		return $this->groupScoutMessages;
631
	}
632
633
	public function setGroupScoutMessages($setting) {
634
		if ($this->groupScoutMessages == $setting) {
635
			return;
636
		}
637
		$this->groupScoutMessages = $setting;
638
		$this->hasChanged = true;
639
	}
640
641
	protected static function doMessageSending($senderID, $receiverID, $gameID, $messageTypeID, $message, $expires, $senderDelete = false, $unread = true) {
642
		$message = trim($message);
643
		$db = MySqlDatabase::getInstance();
644
		// send him the message
645
		$db->query('INSERT INTO message
646
			(account_id,game_id,message_type_id,message_text,
647
			sender_id,send_time,expire_time,sender_delete) VALUES(' .
648
			$db->escapeNumber($receiverID) . ',' .
649
			$db->escapeNumber($gameID) . ',' .
650
			$db->escapeNumber($messageTypeID) . ',' .
651
			$db->escapeString($message) . ',' .
652
			$db->escapeNumber($senderID) . ',' .
653
			$db->escapeNumber(SmrSession::getTime()) . ',' .
654
			$db->escapeNumber($expires) . ',' .
655
			$db->escapeBoolean($senderDelete) . ')'
656
		);
657
		// Keep track of the message_id so it can be returned
658
		$insertID = $db->getInsertID();
659
660
		if ($unread === true) {
661
			// give him the message icon
662
			$db->query('REPLACE INTO player_has_unread_messages (game_id, account_id, message_type_id) VALUES
663
						(' . $db->escapeNumber($gameID) . ', ' . $db->escapeNumber($receiverID) . ', ' . $db->escapeNumber($messageTypeID) . ')');
664
		}
665
666
		switch ($messageTypeID) {
667
			case MSG_PLAYER:
668
				$receiverAccount = SmrAccount::getAccount($receiverID);
669
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isLoggedIn()) {
670
					require_once(get_file_loc('messages.inc.php'));
671
					$sender = getMessagePlayer($senderID, $gameID, $messageTypeID);
672
					if ($sender instanceof SmrPlayer) {
673
						$sender = $sender->getDisplayName();
674
					}
675
					$mail = setupMailer();
676
					$mail->Subject = 'Message Notification';
677
					$mail->setFrom('[email protected]', 'SMR Notifications');
678
					$bbifiedMessage = 'From: ' . $sender . ' Date: ' . date($receiverAccount->getShortDateFormat() . ' ' . $receiverAccount->getShortTimeFormat(), SmrSession::getTime()) . "<br/>\r\n<br/>\r\n" . bbifyMessage($message, true);
679
					$mail->msgHTML($bbifiedMessage);
680
					$mail->AltBody = strip_tags($bbifiedMessage);
681
					$mail->addAddress($receiverAccount->getEmail(), $receiverAccount->getHofName());
682
					$mail->send();
683
					$receiverAccount->decreaseMessageNotifications($messageTypeID, 1);
684
					$receiverAccount->update();
685
				}
686
			break;
687
		}
688
689
		return $insertID;
690
	}
691
692
	public function sendMessageToBox($boxTypeID, $message) {
693
		// send him the message
694
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
695
	}
696
697
	public function sendGlobalMessage($message, $canBeIgnored = true) {
698
		if ($canBeIgnored) {
699
			if ($this->getAccount()->isMailBanned()) {
700
				create_error('You are currently banned from sending messages');
701
			}
702
		}
703
		$this->sendMessageToBox(BOX_GLOBALS, $message);
704
705
		// send to all online player
706
		$db = MySqlDatabase::getInstance();
707
		$db->query('SELECT account_id
708
					FROM active_session
709
					JOIN player USING (game_id, account_id)
710
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(SmrSession::getTime() - SmrSession::TIME_BEFORE_EXPIRY) . '
711
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
712
						AND ignore_globals = \'FALSE\'
713
						AND account_id != ' . $db->escapeNumber($this->getAccountID()));
714
715
		while ($db->nextRecord()) {
716
			$this->sendMessage($db->getInt('account_id'), MSG_GLOBAL, $message, $canBeIgnored);
717
		}
718
		$this->sendMessage($this->getAccountID(), MSG_GLOBAL, $message, $canBeIgnored, false);
719
	}
720
721
	public function sendMessage($receiverID, $messageTypeID, $message, $canBeIgnored = true, $unread = true, $expires = false, $senderDelete = 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
			$this->db->query('SELECT account_id FROM message_blacklist WHERE account_id=' . $this->db->escapeNumber($receiverID) . ' AND blacklisted_id=' . $this->db->escapeNumber($this->getAccountID()) . ' LIMIT 1');
729
			if ($this->db->nextRecord()) {
730
				return;
731
			}
732
		}
733
734
		$message = word_filter($message);
735
736
		// If expires not specified, use default based on message type
737
		if ($expires === false) {
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 += SmrSession::getTime();
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($receiverID, $message, $expires = false) {
762
		// get expire time if not set
763
		if ($expires === false) {
764
			$expires = SmrSession::getTime() + 86400 * 14;
765
		}
766
		self::doMessageSending(ACCOUNT_ID_OP_ANNOUNCE, $receiverID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
767
	}
768
769
	public function sendMessageFromAllianceCommand($receiverID, $message) {
770
		$expires = SmrSession::getTime() + 86400 * 365;
771
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_COMMAND, $receiverID, $this->getGameID(), MSG_PLAYER, $message, $expires);
772
	}
773
774
	public static function sendMessageFromPlanet($gameID, $receiverID, $message) {
775
		//get expire time
776
		$expires = SmrSession::getTime() + 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($gameID, $receiverID, $message) {
782
		//get expire time
783
		$expires = SmrSession::getTime() + 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($gameID, $receiverID, $message) {
789
		$expires = SmrSession::getTime() + 86400 * 365;
790
		self::doMessageSending(ACCOUNT_ID_FED_CLERK, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
791
	}
792
793
	public static function sendMessageFromAdmin($gameID, $receiverID, $message, $expires = false) {
794
		//get expire time
795
		if ($expires === false) {
796
			$expires = SmrSession::getTime() + 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($gameID, $receiverID, $message, $expires = false) {
803
		//get expire time
804
		if ($expires === false) {
805
			$expires = SmrSession::getTime() + 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($gameID, $receiverID, $message, $expires = false) {
812
		//get expire time
813
		if ($expires === false) {
814
			$expires = SmrSession::getTime() + 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($raceID, $gameID, $receiverID, $message, $expires = false) {
821
		//get expire time
822
		if ($expires === false) {
823
			$expires = SmrSession::getTime() + 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($messageTypeID) {
830
		$this->db->query('DELETE FROM player_has_unread_messages
831
							WHERE '.$this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
832
	}
833
834
	public function getSafeAttackRating() {
835
		return max(0, min(8, $this->getAlignment() / 150 + 4));
836
	}
837
838
	public function hasFederalProtection() {
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($raceID) {
861
		if (!isset($this->canFed)) {
862
			$this->canFed = array();
863
			$RACES = Globals::getRaces();
864
			foreach ($RACES as $raceID2 => $raceName) {
865
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
866
			}
867
			$this->db->query('SELECT race_id, allowed FROM player_can_fed
868
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(SmrSession::getTime()));
869
			while ($this->db->nextRecord()) {
870
				$this->canFed[$this->db->getInt('race_id')] = $this->db->getBoolean('allowed');
871
			}
872
		}
873
		return $this->canFed[$raceID];
874
	}
875
876
	/**
877
	 * Returns a boolean identifying if the player can currently
878
	 * participate in battles.
879
	 */
880
	public function canFight() {
881
		return !($this->hasNewbieTurns() ||
882
		         $this->isDead() ||
883
		         $this->isLandedOnPlanet() ||
884
		         $this->hasFederalProtection());
885
	}
886
887
	public function setDead($bool) {
888
		if ($this->dead == $bool) {
889
			return;
890
		}
891
		$this->dead = $bool;
892
		$this->hasChanged = true;
893
	}
894
895
	public function getKills() {
896
		return $this->kills;
897
	}
898
899
	public function increaseKills($kills) {
900
		if ($kills < 0) {
901
			throw new Exception('Trying to increase negative kills.');
902
		}
903
		$this->setKills($this->kills + $kills);
904
	}
905
906
	public function setKills($kills) {
907
		if ($this->kills == $kills) {
908
			return;
909
		}
910
		$this->kills = $kills;
911
		$this->hasChanged = true;
912
	}
913
914
	public function getDeaths() {
915
		return $this->deaths;
916
	}
917
918
	public function increaseDeaths($deaths) {
919
		if ($deaths < 0) {
920
			throw new Exception('Trying to increase negative deaths.');
921
		}
922
		$this->setDeaths($this->getDeaths() + $deaths);
923
	}
924
925
	public function setDeaths($deaths) {
926
		if ($this->deaths == $deaths) {
927
			return;
928
		}
929
		$this->deaths = $deaths;
930
		$this->hasChanged = true;
931
	}
932
933
	public function getAssists() {
934
		return $this->assists;
935
	}
936
937
	public function increaseAssists($assists) {
938
		if ($assists < 1) {
939
			throw new Exception('Must increase by a positive number.');
940
		}
941
		$this->assists += $assists;
942
		$this->hasChanged = true;
943
	}
944
945
	public function getAlignment() {
946
		return $this->alignment;
947
	}
948
949
	public function increaseAlignment($align) {
950
		if ($align < 0) {
951
			throw new Exception('Trying to increase negative align.');
952
		}
953
		if ($align == 0) {
954
			return;
955
		}
956
		$align += $this->alignment;
957
		$this->setAlignment($align);
958
	}
959
960
	public function decreaseAlignment($align) {
961
		if ($align < 0) {
962
			throw new Exception('Trying to decrease negative align.');
963
		}
964
		if ($align == 0) {
965
			return;
966
		}
967
		$align = $this->alignment - $align;
968
		$this->setAlignment($align);
969
	}
970
971
	public function setAlignment($align) {
972
		if ($this->alignment == $align) {
973
			return;
974
		}
975
		$this->alignment = $align;
976
		$this->hasChanged = true;
977
	}
978
979
	public function getCredits() {
980
		return $this->credits;
981
	}
982
983
	public function getBank() {
984
		return $this->bank;
985
	}
986
987
	/**
988
	 * Increases personal bank account up to the maximum allowed credits.
989
	 * Returns the amount that was actually added to handle overflow.
990
	 */
991
	public function increaseBank(int $credits) : int {
992
		if ($credits == 0) {
993
			return 0;
994
		}
995
		if ($credits < 0) {
996
			throw new Exception('Trying to increase negative credits.');
997
		}
998
		$newTotal = min($this->bank + $credits, MAX_MONEY);
999
		$actualAdded = $newTotal - $this->bank;
1000
		$this->setBank($newTotal);
1001
		return $actualAdded;
1002
	}
1003
1004
	public function decreaseBank(int $credits) : void {
1005
		if ($credits == 0) {
1006
			return;
1007
		}
1008
		if ($credits < 0) {
1009
			throw new Exception('Trying to decrease negative credits.');
1010
		}
1011
		$newTotal = $this->bank - $credits;
1012
		$this->setBank($newTotal);
1013
	}
1014
1015
	public function setBank(int $credits) : void {
1016
		if ($this->bank == $credits) {
1017
			return;
1018
		}
1019
		if ($credits < 0) {
1020
			throw new Exception('Trying to set negative credits.');
1021
		}
1022
		if ($credits > MAX_MONEY) {
1023
			throw new Exception('Trying to set more than max credits.');
1024
		}
1025
		$this->bank = $credits;
1026
		$this->hasChanged = true;
1027
	}
1028
1029
	public function getExperience() {
1030
		return $this->experience;
1031
	}
1032
1033
	/**
1034
	 * Returns the percent progress towards the next level.
1035
	 * This value is rounded because it is used primarily in HTML img widths.
1036
	 */
1037
	public function getNextLevelPercentAcquired() : int {
1038
		if ($this->getNextLevelExperience() == $this->getThisLevelExperience()) {
1039
			return 100;
1040
		}
1041
		return max(0, min(100, IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
1042
	}
1043
1044
	public function getNextLevelPercentRemaining() {
1045
		return 100 - $this->getNextLevelPercentAcquired();
1046
	}
1047
1048
	public function getNextLevelExperience() {
1049
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1050
		if (!isset($LEVELS_REQUIREMENTS[$this->getLevelID() + 1])) {
1051
			return $this->getThisLevelExperience(); //Return current level experience if on last level.
1052
		}
1053
		return $LEVELS_REQUIREMENTS[$this->getLevelID() + 1]['Requirement'];
1054
	}
1055
1056
	public function getThisLevelExperience() {
1057
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1058
		return $LEVELS_REQUIREMENTS[$this->getLevelID()]['Requirement'];
1059
	}
1060
1061
	public function setExperience($experience) {
1062
		if ($this->experience == $experience) {
1063
			return;
1064
		}
1065
		if ($experience < MIN_EXPERIENCE) {
1066
			$experience = MIN_EXPERIENCE;
1067
		}
1068
		if ($experience > MAX_EXPERIENCE) {
1069
			$experience = MAX_EXPERIENCE;
1070
		}
1071
		$this->experience = $experience;
1072
		$this->hasChanged = true;
1073
1074
		// Since exp has changed, invalidate the player level so that it can
1075
		// be recomputed next time it is queried (in case it has changed).
1076
		$this->level = null;
1077
	}
1078
1079
	/**
1080
	 * Increases onboard credits up to the maximum allowed credits.
1081
	 * Returns the amount that was actually added to handle overflow.
1082
	 */
1083
	public function increaseCredits(int $credits) : int {
1084
		if ($credits == 0) {
1085
			return 0;
1086
		}
1087
		if ($credits < 0) {
1088
			throw new Exception('Trying to increase negative credits.');
1089
		}
1090
		$newTotal = min($this->credits + $credits, MAX_MONEY);
1091
		$actualAdded = $newTotal - $this->credits;
1092
		$this->setCredits($newTotal);
1093
		return $actualAdded;
1094
	}
1095
1096
	public function decreaseCredits(int $credits) : void {
1097
		if ($credits == 0) {
1098
			return;
1099
		}
1100
		if ($credits < 0) {
1101
			throw new Exception('Trying to decrease negative credits.');
1102
		}
1103
		$newTotal = $this->credits - $credits;
1104
		$this->setCredits($newTotal);
1105
	}
1106
1107
	public function setCredits(int $credits) : void {
1108
		if ($this->credits == $credits) {
1109
			return;
1110
		}
1111
		if ($credits < 0) {
1112
			throw new Exception('Trying to set negative credits.');
1113
		}
1114
		if ($credits > MAX_MONEY) {
1115
			throw new Exception('Trying to set more than max credits.');
1116
		}
1117
		$this->credits = $credits;
1118
		$this->hasChanged = true;
1119
	}
1120
1121
	public function increaseExperience($experience) {
1122
		if ($experience < 0) {
1123
			throw new Exception('Trying to increase negative experience.');
1124
		}
1125
		if ($experience == 0) {
1126
			return;
1127
		}
1128
		$newExperience = $this->experience + $experience;
1129
		$this->setExperience($newExperience);
1130
		$this->increaseHOF($experience, array('Experience', 'Total', 'Gain'), HOF_PUBLIC);
1131
	}
1132
	public function decreaseExperience($experience) {
1133
		if ($experience < 0) {
1134
			throw new Exception('Trying to decrease negative experience.');
1135
		}
1136
		if ($experience == 0) {
1137
			return;
1138
		}
1139
		$newExperience = $this->experience - $experience;
1140
		$this->setExperience($newExperience);
1141
		$this->increaseHOF($experience, array('Experience', 'Total', 'Loss'), HOF_PUBLIC);
1142
	}
1143
1144
	public function isLandedOnPlanet() {
1145
		return $this->landedOnPlanet;
1146
	}
1147
1148
	public function setLandedOnPlanet($bool) {
1149
		if ($this->landedOnPlanet == $bool) {
1150
			return;
1151
		}
1152
		$this->landedOnPlanet = $bool;
1153
		$this->hasChanged = true;
1154
	}
1155
1156
	/**
1157
	 * Returns the numerical level of the player (e.g. 1-50).
1158
	 */
1159
	public function getLevelID() {
1160
		// The level is cached for performance reasons unless `setExperience`
1161
		// is called and the player's experience changes.
1162
		if ($this->level === null) {
1163
			$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1164
			foreach ($LEVELS_REQUIREMENTS as $level_id => $require) {
1165
				if ($this->getExperience() >= $require['Requirement']) {
1166
					continue;
1167
				}
1168
				$this->level = $level_id - 1;
1169
				return $this->level;
1170
			}
1171
			$this->level = max(array_keys($LEVELS_REQUIREMENTS));
1172
		}
1173
		return $this->level;
1174
	}
1175
1176
	public function getLevelName() {
1177
		$level_name = Globals::getLevelRequirements()[$this->getLevelID()]['Name'];
1178
		if ($this->isPresident()) {
1179
			$level_name = '<img src="images/council_president.png" title="' . Globals::getRaceName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1180
		}
1181
		return $level_name;
1182
	}
1183
1184
	public function getMaxLevel() {
1185
		return max(array_keys(Globals::getLevelRequirements()));
1186
	}
1187
1188
	public function getPlayerID() {
1189
		return $this->playerID;
1190
	}
1191
1192
	/**
1193
	 * Returns the player name.
1194
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1195
	 */
1196
	public function getPlayerName() {
1197
		return $this->playerName;
1198
	}
1199
1200
	public function setPlayerName($name) {
1201
		$this->playerName = $name;
1202
		$this->hasChanged = true;
1203
	}
1204
1205
	/**
1206
	 * Returns the decorated player name, suitable for HTML display.
1207
	 */
1208
	public function getDisplayName($includeAlliance = false) {
1209
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1210
		$return = get_colored_text($this->getAlignment(), $name);
1211
		if ($this->isNPC()) {
1212
			$return .= ' <span class="npcColour">[NPC]</span>';
1213
		}
1214
		if ($includeAlliance) {
1215
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1216
		}
1217
		return $return;
1218
	}
1219
1220
	public function getBBLink() {
1221
			return '[player=' . $this->getPlayerID() . ']';
1222
	}
1223
1224
	public function getLinkedDisplayName($includeAlliance = true) {
1225
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1226
		if ($includeAlliance) {
1227
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1228
		}
1229
		return $return;
1230
	}
1231
1232
	/**
1233
	 * Use this method when the player is changing their own name.
1234
	 * This will flag the player as having used their free name change.
1235
	 */
1236
	public function setPlayerNameByPlayer($playerName) {
1237
		$this->setPlayerName($playerName);
1238
		$this->setNameChanged(true);
1239
	}
1240
1241
	public function isNameChanged() {
1242
		return $this->nameChanged;
1243
	}
1244
1245
	public function setNameChanged($bool) {
1246
		$this->nameChanged = $bool;
1247
		$this->hasChanged = true;
1248
	}
1249
1250
	public function isRaceChanged() : bool {
1251
		return $this->raceChanged;
1252
	}
1253
1254
	public function setRaceChanged(bool $raceChanged) : void {
1255
		$this->raceChanged = $raceChanged;
1256
		$this->hasChanged = true;
1257
	}
1258
1259
	public function canChangeRace() : bool {
1260
		return !$this->isRaceChanged() && (SmrSession::getTime() - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1261
	}
1262
1263
	public static function getColouredRaceNameOrDefault($otherRaceID, AbstractSmrPlayer $player = null, $linked = false) {
1264
		$relations = 0;
1265
		if ($player !== null) {
1266
			$relations = $player->getRelation($otherRaceID);
1267
		}
1268
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1269
	}
1270
1271
	public function getColouredRaceName($otherRaceID, $linked = false) {
1272
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1273
	}
1274
1275
	public function setRaceID($raceID) {
1276
		if ($this->raceID == $raceID) {
1277
			return;
1278
		}
1279
		$this->raceID = $raceID;
1280
		$this->hasChanged = true;
1281
	}
1282
1283
	public function isAllianceLeader($forceUpdate = false) {
1284
		return $this->getAccountID() == $this->getAlliance($forceUpdate)->getLeaderID();
1285
	}
1286
1287
	public function getAlliance($forceUpdate = false) {
1288
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1289
	}
1290
1291
	public function getAllianceID() {
1292
		return $this->allianceID;
1293
	}
1294
1295
	public function hasAlliance() {
1296
		return $this->getAllianceID() != 0;
1297
	}
1298
1299
	protected function setAllianceID($ID) {
1300
		if ($this->allianceID == $ID) {
1301
			return;
1302
		}
1303
		$this->allianceID = $ID;
1304
		if ($this->allianceID != 0) {
1305
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1306
			$this->db->query('INSERT IGNORE INTO player_joined_alliance (account_id,game_id,alliance_id,status) ' .
1307
				'VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1308
		}
1309
		$this->hasChanged = true;
1310
	}
1311
1312
	public function getAllianceBBLink() {
1313
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1314
	}
1315
1316
	public function getAllianceDisplayName($linked = false, $includeAllianceID = false) {
1317
		if ($this->hasAlliance()) {
1318
			return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1319
		} else {
1320
			return 'No Alliance';
1321
		}
1322
	}
1323
1324
	public function getAllianceRole($allianceID = false) {
1325
		if ($allianceID === false) {
1326
			$allianceID = $this->getAllianceID();
1327
		}
1328
		if (!isset($this->allianceRoles[$allianceID])) {
1329
			$this->allianceRoles[$allianceID] = 0;
1330
			$this->db->query('SELECT role_id
1331
						FROM player_has_alliance_role
1332
						WHERE ' . $this->SQL . '
1333
						AND alliance_id=' . $this->db->escapeNumber($allianceID) . '
1334
						LIMIT 1');
1335
			if ($this->db->nextRecord()) {
1336
				$this->allianceRoles[$allianceID] = $this->db->getInt('role_id');
1337
			}
1338
		}
1339
		return $this->allianceRoles[$allianceID];
1340
	}
1341
1342
	public function leaveAlliance(AbstractSmrPlayer $kickedBy = null) {
1343
		$allianceID = $this->getAllianceID();
1344
		$alliance = $this->getAlliance();
1345
		if ($kickedBy != null) {
1346
			$kickedBy->sendMessage($this->getAccountID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1347
			$this->actionTaken('PlayerKicked', array('Alliance' => $alliance, 'Player' => $kickedBy));
1348
			$kickedBy->actionTaken('KickPlayer', array('Alliance' => $alliance, 'Player' => $this));
1349
		} elseif ($this->isAllianceLeader()) {
1350
			$this->actionTaken('DisbandAlliance', array('Alliance' => $alliance));
1351
		} else {
1352
			$this->actionTaken('LeaveAlliance', array('Alliance' => $alliance));
1353
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1354
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I left your alliance!', false);
1355
			}
1356
		}
1357
1358
		if (!$this->isAllianceLeader() && $allianceID != NHA_ID) { // Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1359
			$this->setAllianceJoinable(SmrSession::getTime() + self::TIME_FOR_ALLIANCE_SWITCH);
1360
			$alliance->getLeader()->setAllianceJoinable(SmrSession::getTime() + 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.
1361
		}
1362
1363
		$this->setAllianceID(0);
1364
		$this->db->query('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1365
	}
1366
1367
	/**
1368
	 * Join an alliance (used for both Leader and New Member roles)
1369
	 */
1370
	public function joinAlliance($allianceID) {
1371
		$this->setAllianceID($allianceID);
1372
		$alliance = $this->getAlliance();
1373
1374
		if (!$this->isAllianceLeader()) {
1375
			// Do not throw an exception if the NHL account doesn't exist.
1376
			try {
1377
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I joined your alliance!', false);
1378
			} catch (AccountNotFoundException $e) {
1379
				if ($alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1380
					throw $e;
1381
				}
1382
			}
1383
1384
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1385
		} else {
1386
			$roleID = ALLIANCE_ROLE_LEADER;
1387
		}
1388
		$this->db->query('INSERT INTO player_has_alliance_role (game_id, account_id, role_id, alliance_id) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($roleID) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
1389
1390
		$this->actionTaken('JoinAlliance', array('Alliance' => $alliance));
1391
	}
1392
1393
	public function getAllianceJoinable() {
1394
		return $this->allianceJoinable;
1395
	}
1396
1397
	private function setAllianceJoinable($time) {
1398
		if ($this->allianceJoinable == $time) {
1399
			return;
1400
		}
1401
		$this->allianceJoinable = $time;
1402
		$this->hasChanged = true;
1403
	}
1404
1405
	/**
1406
	 * Invites player with $accountID to this player's alliance.
1407
	 */
1408
	public function sendAllianceInvitation(int $accountID, string $message, int $expires) : void {
1409
		if (!$this->hasAlliance()) {
1410
			throw new Exception('Must be in an alliance to send alliance invitations');
1411
		}
1412
		// Send message to invited player
1413
		$messageID = $this->sendMessage($accountID, MSG_PLAYER, $message, false, true, $expires, true);
1414
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), $messageID, $expires);
0 ignored issues
show
Bug introduced by
It seems like $messageID can also be of type string; however, parameter $messageID of SmrInvitation::send() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1414
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), /** @scrutinizer ignore-type */ $messageID, $expires);
Loading history...
1415
	}
1416
1417
	public function isCombatDronesKamikazeOnMines() {
1418
		return $this->combatDronesKamikazeOnMines;
1419
	}
1420
1421
	public function setCombatDronesKamikazeOnMines($bool) {
1422
		if ($this->combatDronesKamikazeOnMines == $bool) {
1423
			return;
1424
		}
1425
		$this->combatDronesKamikazeOnMines = $bool;
1426
		$this->hasChanged = true;
1427
	}
1428
1429
	protected function getPersonalRelationsData() {
1430
		if (!isset($this->personalRelations)) {
1431
			//get relations
1432
			$RACES = Globals::getRaces();
1433
			$this->personalRelations = array();
1434
			foreach ($RACES as $raceID => $raceName) {
1435
				$this->personalRelations[$raceID] = 0;
1436
			}
1437
			$this->db->query('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL . ' LIMIT ' . count($RACES));
1438
			while ($this->db->nextRecord()) {
1439
				$this->personalRelations[$this->db->getInt('race_id')] = $this->db->getInt('relation');
1440
			}
1441
		}
1442
	}
1443
1444
	public function getPersonalRelations() {
1445
		$this->getPersonalRelationsData();
1446
		return $this->personalRelations;
1447
	}
1448
1449
	/**
1450
	 * Get personal relations with a race
1451
	 */
1452
	public function getPersonalRelation($raceID) {
1453
		$rels = $this->getPersonalRelations();
1454
		return $rels[$raceID];
1455
	}
1456
1457
	/**
1458
	 * Get total relations with all races (personal + political)
1459
	 */
1460
	public function getRelations() {
1461
		if (!isset($this->relations)) {
1462
			//get relations
1463
			$RACES = Globals::getRaces();
1464
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1465
			$personalRels = $this->getPersonalRelations(); // make sure they're initialised.
1466
			$this->relations = array();
1467
			foreach ($RACES as $raceID => $raceName) {
1468
				$this->relations[$raceID] = $personalRels[$raceID] + $raceRelations[$raceID];
1469
			}
1470
		}
1471
		return $this->relations;
1472
	}
1473
1474
	/**
1475
	 * Get total relations with a race (personal + political)
1476
	 */
1477
	public function getRelation($raceID) {
1478
		$rels = $this->getRelations();
1479
		return $rels[$raceID];
1480
	}
1481
1482
	/**
1483
	 * Increases personal relations from trading $numGoods units with the race
1484
	 * of the port given by $raceID.
1485
	 */
1486
	public function increaseRelationsByTrade($numGoods, $raceID) {
1487
		$relations = ICeil(min($numGoods, 300) / 30);
1488
		//Cap relations to a max of 1 after 500 have been reached
1489
		if ($this->getPersonalRelation($raceID) + $relations >= 500) {
1490
			$relations = max(1, min($relations, 500 - $this->getPersonalRelation($raceID)));
1491
		}
1492
		$this->increaseRelations($relations, $raceID);
1493
	}
1494
1495
	/**
1496
	 * Decreases personal relations from trading failures, e.g. rejected
1497
	 * bargaining and getting caught stealing.
1498
	 */
1499
	public function decreaseRelationsByTrade($numGoods, $raceID) {
1500
		$relations = ICeil(min($numGoods, 300) / 30);
1501
		$this->decreaseRelations($relations, $raceID);
1502
	}
1503
1504
	/**
1505
	 * Increase personal relations.
1506
	 */
1507
	public function increaseRelations($relations, $raceID) {
1508
		if ($relations < 0) {
1509
			throw new Exception('Trying to increase negative relations.');
1510
		}
1511
		if ($relations == 0) {
1512
			return;
1513
		}
1514
		$relations += $this->getPersonalRelation($raceID);
1515
		$this->setRelations($relations, $raceID);
1516
	}
1517
1518
	/**
1519
	 * Decrease personal relations.
1520
	 */
1521
	public function decreaseRelations($relations, $raceID) {
1522
		if ($relations < 0) {
1523
			throw new Exception('Trying to decrease negative relations.');
1524
		}
1525
		if ($relations == 0) {
1526
			return;
1527
		}
1528
		$relations = $this->getPersonalRelation($raceID) - $relations;
1529
		$this->setRelations($relations, $raceID);
1530
	}
1531
1532
	/**
1533
	 * Set personal relations.
1534
	 */
1535
	public function setRelations($relations, $raceID) {
1536
		$this->getRelations();
1537
		if ($this->personalRelations[$raceID] == $relations) {
1538
			return;
1539
		}
1540
		if ($relations < MIN_RELATIONS) {
1541
			$relations = MIN_RELATIONS;
1542
		}
1543
		$relationsDiff = IRound($relations - $this->personalRelations[$raceID]);
1544
		$this->personalRelations[$raceID] = $relations;
1545
		$this->relations[$raceID] += $relationsDiff;
1546
		$this->db->query('REPLACE INTO player_has_relation (account_id,game_id,race_id,relation) values (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($raceID) . ',' . $this->db->escapeNumber($this->personalRelations[$raceID]) . ')');
1547
	}
1548
1549
	/**
1550
	 * Set any starting personal relations bonuses or penalties.
1551
	 */
1552
	public function giveStartingRelations() {
1553
		if ($this->getRaceID() === RACE_ALSKANT) {
1554
			// Give Alskants bonus personal relations to start.
1555
			foreach (Globals::getRaces() as $raceID => $raceInfo) {
1556
				$this->setRelations(ALSKANT_BONUS_RELATIONS, $raceID);
1557
			}
1558
		}
1559
	}
1560
1561
	public function getLastNewsUpdate() {
1562
		return $this->lastNewsUpdate;
1563
	}
1564
1565
	private function setLastNewsUpdate($time) {
1566
		if ($this->lastNewsUpdate == $time) {
1567
			return;
1568
		}
1569
		$this->lastNewsUpdate = $time;
1570
		$this->hasChanged = true;
1571
	}
1572
1573
	public function updateLastNewsUpdate() {
1574
		$this->setLastNewsUpdate(SmrSession::getTime());
1575
	}
1576
1577
	public function getLastPort() {
1578
		return $this->lastPort;
1579
	}
1580
1581
	public function setLastPort($lastPort) {
1582
		if ($this->lastPort == $lastPort) {
1583
			return;
1584
		}
1585
		$this->lastPort = $lastPort;
1586
		$this->hasChanged = true;
1587
	}
1588
1589
	public function getPlottedCourse() {
1590
		if (!isset($this->plottedCourse)) {
1591
			// check if we have a course plotted
1592
			$this->db->query('SELECT course FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1593
1594
			if ($this->db->nextRecord()) {
1595
				// get the course back
1596
				$this->plottedCourse = unserialize($this->db->getField('course'));
1597
			} else {
1598
				$this->plottedCourse = false;
1599
			}
1600
		}
1601
1602
		// Update the plotted course if we have moved since the last query
1603
		if ($this->plottedCourse !== false && (!isset($this->plottedCourseFrom) || $this->plottedCourseFrom != $this->getSectorID())) {
1604
			$this->plottedCourseFrom = $this->getSectorID();
1605
1606
			if ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1607
				// We have walked into the next sector of the course
1608
				$this->plottedCourse->followPath();
1609
				$this->setPlottedCourse($this->plottedCourse);
1610
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1611
				// We have skipped to some later sector in the course
1612
				$this->plottedCourse->skipToSector($this->getSectorID());
1613
				$this->setPlottedCourse($this->plottedCourse);
1614
			}
1615
		}
1616
		return $this->plottedCourse;
1617
	}
1618
1619
	public function setPlottedCourse(Distance $plottedCourse) {
1620
		$hadPlottedCourse = $this->hasPlottedCourse();
1621
		$this->plottedCourse = $plottedCourse;
1622
		if ($this->plottedCourse->getTotalSectors() > 0) {
1623
			$this->db->query('REPLACE INTO player_plotted_course
1624
				(account_id, game_id, course)
1625
				VALUES(' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeBinary(serialize($this->plottedCourse)) . ')');
1626
		} elseif ($hadPlottedCourse) {
1627
			$this->deletePlottedCourse();
1628
		}
1629
	}
1630
1631
	public function hasPlottedCourse() {
1632
		return $this->getPlottedCourse() !== false;
1633
	}
1634
1635
	public function isPartOfCourse($sectorOrSectorID) {
1636
		if (!$this->hasPlottedCourse()) {
1637
			return false;
1638
		}
1639
		if ($sectorOrSectorID instanceof SmrSector) {
1640
			$sectorID = $sectorOrSectorID->getSectorID();
1641
		} else {
1642
			$sectorID = $sectorOrSectorID;
1643
		}
1644
		return $this->getPlottedCourse()->isInPath($sectorID);
1645
	}
1646
1647
	public function deletePlottedCourse() {
1648
		$this->plottedCourse = false;
1649
		$this->db->query('DELETE FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1650
	}
1651
1652
	// Computes the turn cost and max misjump between current and target sector
1653
	public function getJumpInfo(SmrSector $targetSector) {
1654
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1655
		if ($path === false) {
1656
			create_error('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1657
		}
1658
		$distance = $path->getRelativeDistance();
1659
1660
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
1661
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1662
		return array('turn_cost' => $turnCost, 'max_misjump' => $maxMisjump);
1663
	}
1664
1665
	public function __sleep() {
1666
		return array('accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName');
1667
	}
1668
1669
	public function &getStoredDestinations() {
1670
		if (!isset($this->storedDestinations)) {
1671
			$this->storedDestinations = array();
1672
			$this->db->query('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1673
			while ($this->db->nextRecord()) {
1674
				$this->storedDestinations[] = array(
1675
					'Label' => $this->db->getField('label'),
1676
					'SectorID' => $this->db->getInt('sector_id'),
1677
					'OffsetTop' => $this->db->getInt('offset_top'),
1678
					'OffsetLeft' => $this->db->getInt('offset_left')
1679
				);
1680
			}
1681
		}
1682
		return $this->storedDestinations;
1683
	}
1684
1685
	public function moveDestinationButton($sectorID, $offsetTop, $offsetLeft) {
1686
1687
		if (!is_numeric($offsetLeft) || !is_numeric($offsetTop)) {
1688
			create_error('The position of the saved sector must be numeric!.');
1689
		}
1690
		$offsetTop = round($offsetTop);
1691
		$offsetLeft = round($offsetLeft);
1692
1693
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1694
			create_error('The saved sector must be in the box!');
1695
		}
1696
1697
		$storedDestinations =& $this->getStoredDestinations();
1698
		foreach ($storedDestinations as &$sd) {
1699
			if ($sd['SectorID'] == $sectorID) {
1700
				$sd['OffsetTop'] = $offsetTop;
1701
				$sd['OffsetLeft'] = $offsetLeft;
1702
				$this->db->query('
1703
					UPDATE player_stored_sector
1704
						SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1705
					WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID)
1706
				);
1707
				return true;
1708
			}
1709
		}
1710
1711
		create_error('You do not have a saved sector for #' . $sectorID);
1712
	}
1713
1714
	public function addDestinationButton($sectorID, $label) {
1715
1716
		if (!is_numeric($sectorID) || !SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1717
			create_error('You want to add a non-existent sector?');
1718
		}
1719
1720
		// sector already stored ?
1721
		foreach ($this->getStoredDestinations() as $sd) {
1722
			if ($sd['SectorID'] == $sectorID) {
1723
				create_error('Sector already stored!');
1724
			}
1725
		}
1726
1727
		$this->storedDestinations[] = array(
1728
			'Label' => $label,
1729
			'SectorID' => (int)$sectorID,
1730
			'OffsetTop' => 1,
1731
			'OffsetLeft' => 1
1732
		);
1733
1734
		$this->db->query('
1735
			INSERT INTO player_stored_sector (account_id, game_id, sector_id, label, offset_top, offset_left)
1736
			VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($sectorID) . ',' . $this->db->escapeString($label) . ',1,1)'
1737
		);
1738
	}
1739
1740
	public function deleteDestinationButton($sectorID) {
1741
		if (!is_numeric($sectorID) || $sectorID < 1) {
1742
			create_error('You want to remove a non-existent sector?');
1743
		}
1744
1745
		foreach ($this->getStoredDestinations() as $key => $sd) {
1746
			if ($sd['SectorID'] == $sectorID) {
1747
				$this->db->query('
1748
					DELETE FROM player_stored_sector
1749
					WHERE ' . $this->SQL . '
1750
					AND sector_id = ' . $this->db->escapeNumber($sectorID)
1751
				);
1752
				unset($this->storedDestinations[$key]);
1753
				return true;
1754
			}
1755
		}
1756
		return false;
1757
	}
1758
1759
	public function getTickers() {
1760
		if (!isset($this->tickers)) {
1761
			$this->tickers = array();
1762
			//get ticker info
1763
			$this->db->query('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(SmrSession::getTime()));
1764
			while ($this->db->nextRecord()) {
1765
				$this->tickers[$this->db->getField('type')] = [
1766
					'Type' => $this->db->getField('type'),
1767
					'Time' => $this->db->getInt('time'),
1768
					'Expires' => $this->db->getInt('expires'),
1769
					'Recent' => $this->db->getField('recent'),
1770
				];
1771
			}
1772
		}
1773
		return $this->tickers;
1774
	}
1775
1776
	public function hasTickers() {
1777
		return count($this->getTickers()) > 0;
1778
	}
1779
1780
	public function getTicker($tickerType) {
1781
		$tickers = $this->getTickers();
1782
		if (isset($tickers[$tickerType])) {
1783
			return $tickers[$tickerType];
1784
		}
1785
		return false;
1786
	}
1787
1788
	public function hasTicker($tickerType) {
1789
		return $this->getTicker($tickerType) !== false;
1790
	}
1791
1792
	public function shootForces(SmrForce $forces) {
1793
		return $this->getShip()->shootForces($forces);
1794
	}
1795
1796
	public function shootPort(SmrPort $port) {
1797
		return $this->getShip()->shootPort($port);
1798
	}
1799
1800
	public function shootPlanet(SmrPlanet $planet, $delayed) {
1801
		return $this->getShip()->shootPlanet($planet, $delayed);
1802
	}
1803
1804
	public function shootPlayers(array $targetPlayers) {
1805
		return $this->getShip()->shootPlayers($targetPlayers);
1806
	}
1807
1808
	public function getMilitaryPayment() {
1809
		return $this->militaryPayment;
1810
	}
1811
1812
	public function hasMilitaryPayment() {
1813
		return $this->getMilitaryPayment() > 0;
1814
	}
1815
1816
	public function setMilitaryPayment($amount) {
1817
		if ($this->militaryPayment == $amount) {
1818
			return;
1819
		}
1820
		$this->militaryPayment = $amount;
1821
		$this->hasChanged = true;
1822
	}
1823
1824
	public function increaseMilitaryPayment($amount) {
1825
		if ($amount < 0) {
1826
			throw new Exception('Trying to increase negative military payment.');
1827
		}
1828
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1829
	}
1830
1831
	public function decreaseMilitaryPayment($amount) {
1832
		if ($amount < 0) {
1833
			throw new Exception('Trying to decrease negative military payment.');
1834
		}
1835
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1836
	}
1837
1838
	protected function getBountiesData() {
1839
		if (!isset($this->bounties)) {
1840
			$this->bounties = array();
1841
			$this->db->query('SELECT * FROM bounty WHERE ' . $this->SQL);
1842
			while ($this->db->nextRecord()) {
1843
				$this->bounties[$this->db->getInt('bounty_id')] = array(
1844
							'Amount' => $this->db->getInt('amount'),
1845
							'SmrCredits' => $this->db->getInt('smr_credits'),
1846
							'Type' => $this->db->getField('type'),
1847
							'Claimer' => $this->db->getInt('claimer_id'),
1848
							'Time' => $this->db->getInt('time'),
1849
							'ID' => $this->db->getInt('bounty_id'),
1850
							'New' => false);
1851
			}
1852
		}
1853
	}
1854
1855
	// Get bounties that can be claimed by this player
1856
	// Type must be 'HQ' or 'UG'
1857
	public function getClaimableBounties($type) {
1858
		$bounties = array();
1859
		$this->db->query('SELECT * FROM bounty WHERE claimer_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND type=' . $this->db->escapeString($type));
1860
		while ($this->db->nextRecord()) {
1861
			$bounties[] = array(
1862
				'player' => SmrPlayer::getPlayer($this->db->getInt('account_id'), $this->getGameID()),
1863
				'bounty_id' => $this->db->getInt('bounty_id'),
1864
				'credits' => $this->db->getInt('amount'),
1865
				'smr_credits' => $this->db->getInt('smr_credits'),
1866
			);
1867
		}
1868
		return $bounties;
1869
	}
1870
1871
	public function getBounties() : array {
1872
		$this->getBountiesData();
1873
		return $this->bounties;
1874
	}
1875
1876
	public function hasBounties() : bool {
1877
		return count($this->getBounties()) > 0;
1878
	}
1879
1880
	protected function getBounty(int $bountyID) : array {
1881
		if (!$this->hasBounty($bountyID)) {
1882
			throw new Exception('BountyID does not exist: ' . $bountyID);
1883
		}
1884
		return $this->bounties[$bountyID];
1885
	}
1886
1887
	public function hasBounty(int $bountyID) : bool {
1888
		$bounties = $this->getBounties();
1889
		return isset($bounties[$bountyID]);
1890
	}
1891
1892
	protected function getBountyAmount(int $bountyID) : int {
1893
		$bounty = $this->getBounty($bountyID);
1894
		return $bounty['Amount'];
1895
	}
1896
1897
	protected function createBounty(string $type) : array {
1898
		$bounty = array('Amount' => 0,
1899
						'SmrCredits' => 0,
1900
						'Type' => $type,
1901
						'Claimer' => 0,
1902
						'Time' => SmrSession::getTime(),
1903
						'ID' => $this->getNextBountyID(),
1904
						'New' => true);
1905
		$this->setBounty($bounty);
1906
		return $bounty;
1907
	}
1908
1909
	protected function getNextBountyID() : int {
1910
		$keys = array_keys($this->getBounties());
1911
		if (count($keys) > 0) {
1912
			return max($keys) + 1;
1913
		} else {
1914
			return 0;
1915
		}
1916
	}
1917
1918
	protected function setBounty(array $bounty) : void {
1919
		$this->bounties[$bounty['ID']] = $bounty;
1920
		$this->hasBountyChanged[$bounty['ID']] = true;
1921
	}
1922
1923
	protected function setBountyAmount(int $bountyID, int $amount) : void {
1924
		$bounty = $this->getBounty($bountyID);
1925
		$bounty['Amount'] = $amount;
1926
		$this->setBounty($bounty);
1927
	}
1928
1929
	public function getCurrentBounty(string $type) : array {
1930
		$bounties = $this->getBounties();
1931
		foreach ($bounties as $bounty) {
1932
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1933
				return $bounty;
1934
			}
1935
		}
1936
		return $this->createBounty($type);
1937
	}
1938
1939
	public function hasCurrentBounty(string $type) : bool {
1940
		$bounties = $this->getBounties();
1941
		foreach ($bounties as $bounty) {
1942
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1943
				return true;
1944
			}
1945
		}
1946
		return false;
1947
	}
1948
1949
	protected function getCurrentBountyAmount(string $type) : int {
1950
		$bounty = $this->getCurrentBounty($type);
1951
		return $bounty['Amount'];
1952
	}
1953
1954
	protected function setCurrentBountyAmount(string $type, int $amount) : void {
1955
		$bounty = $this->getCurrentBounty($type);
1956
		if ($bounty['Amount'] == $amount) {
1957
			return;
1958
		}
1959
		$bounty['Amount'] = $amount;
1960
		$this->setBounty($bounty);
1961
	}
1962
1963
	public function increaseCurrentBountyAmount(string $type, int $amount) : void {
1964
		if ($amount < 0) {
1965
			throw new Exception('Trying to increase negative current bounty.');
1966
		}
1967
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) + $amount);
1968
	}
1969
1970
	public function decreaseCurrentBountyAmount(string $type, int $amount) : void {
1971
		if ($amount < 0) {
1972
			throw new Exception('Trying to decrease negative current bounty.');
1973
		}
1974
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) - $amount);
1975
	}
1976
1977
	protected function getCurrentBountySmrCredits(string $type) : int {
1978
		$bounty = $this->getCurrentBounty($type);
1979
		return $bounty['SmrCredits'];
1980
	}
1981
1982
	protected function setCurrentBountySmrCredits(string $type, int $credits) : void {
1983
		$bounty = $this->getCurrentBounty($type);
1984
		if ($bounty['SmrCredits'] == $credits) {
1985
			return;
1986
		}
1987
		$bounty['SmrCredits'] = $credits;
1988
		$this->setBounty($bounty);
1989
	}
1990
1991
	public function increaseCurrentBountySmrCredits(string $type, int $credits) : void {
1992
		if ($credits < 0) {
1993
			throw new Exception('Trying to increase negative current bounty.');
1994
		}
1995
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) + $credits);
1996
	}
1997
1998
	public function decreaseCurrentBountySmrCredits(string $type, int $credits) : void {
1999
		if ($credits < 0) {
2000
			throw new Exception('Trying to decrease negative current bounty.');
2001
		}
2002
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) - $credits);
2003
	}
2004
2005
	public function setBountiesClaimable(AbstractSmrPlayer $claimer) : void {
2006
		foreach ($this->getBounties() as $bounty) {
2007
			if ($bounty['Claimer'] == 0) {
2008
				$bounty['Claimer'] = $claimer->getAccountID();
2009
				$this->setBounty($bounty);
2010
			}
2011
		}
2012
	}
2013
2014
	protected function getHOFData() {
2015
		if (!isset($this->HOF)) {
2016
			//Get Player HOF
2017
			$this->db->query('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2018
			$this->HOF = array();
2019
			while ($this->db->nextRecord()) {
2020
				$hof =& $this->HOF;
2021
				$typeList = explode(':', $this->db->getField('type'));
2022
				foreach ($typeList as $type) {
2023
					if (!isset($hof[$type])) {
2024
						$hof[$type] = array();
2025
					}
2026
					$hof =& $hof[$type];
2027
				}
2028
				$hof = $this->db->getFloat('amount');
2029
			}
2030
			self::getHOFVis();
2031
		}
2032
	}
2033
2034
	public static function getHOFVis() {
2035
		if (!isset(self::$HOFVis)) {
2036
			//Get Player HOF Vis
2037
			$db = MySqlDatabase::getInstance();
2038
			$db->query('SELECT type,visibility FROM hof_visibility');
2039
			self::$HOFVis = array();
2040
			while ($db->nextRecord()) {
2041
				self::$HOFVis[$db->getField('type')] = $db->getField('visibility');
2042
			}
2043
		}
2044
	}
2045
2046
	public function getHOF(array $typeList = null) {
2047
		$this->getHOFData();
2048
		if ($typeList == null) {
2049
			return $this->HOF;
2050
		}
2051
		$hof = $this->HOF;
2052
		foreach ($typeList as $type) {
2053
			if (!isset($hof[$type])) {
2054
				return 0;
2055
			}
2056
			$hof = $hof[$type];
2057
		}
2058
		return $hof;
2059
	}
2060
2061
	public function increaseHOF($amount, array $typeList, $visibility) {
2062
		if ($amount < 0) {
2063
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2064
		}
2065
		if ($amount == 0) {
2066
			return;
2067
		}
2068
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2069
	}
2070
2071
	public function decreaseHOF($amount, array $typeList, $visibility) {
2072
		if ($amount < 0) {
2073
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2074
		}
2075
		if ($amount == 0) {
2076
			return;
2077
		}
2078
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2079
	}
2080
2081
	public function setHOF($amount, array $typeList, $visibility) {
2082
		if (is_array($this->getHOF($typeList))) {
2083
			throw new Exception('Trying to overwrite a HOF type: ' . implode(':', $typeList));
2084
		}
2085
		if ($this->isNPC()) {
2086
			// Don't store HOF for NPCs.
2087
			return;
2088
		}
2089
		if ($this->getHOF($typeList) == $amount) {
2090
			return;
2091
		}
2092
		if ($amount < 0) {
2093
			$amount = 0;
2094
		}
2095
		$this->getHOF();
2096
2097
		$hofType = implode(':', $typeList);
2098
		if (!isset(self::$HOFVis[$hofType])) {
2099
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2100
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2101
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2102
		}
2103
		self::$HOFVis[$hofType] = $visibility;
2104
2105
		$hof =& $this->HOF;
2106
		$hofChanged =& $this->hasHOFChanged;
2107
		$new = false;
2108
		foreach ($typeList as $type) {
2109
			if (!isset($hofChanged[$type])) {
2110
				$hofChanged[$type] = array();
2111
			}
2112
			if (!isset($hof[$type])) {
2113
				$hof[$type] = array();
2114
				$new = true;
2115
			}
2116
			$hof =& $hof[$type];
2117
			$hofChanged =& $hofChanged[$type];
2118
		}
2119
		if ($hofChanged == null) {
2120
			$hofChanged = self::HOF_CHANGED;
2121
			if ($new) {
2122
				$hofChanged = self::HOF_NEW;
2123
			}
2124
		}
2125
		$hof = $amount;
2126
	}
2127
2128
	public function getExperienceRank() {
2129
		return $this->computeRanking('experience');
2130
	}
2131
2132
	public function getKillsRank() {
2133
		return $this->computeRanking('kills');
2134
	}
2135
2136
	public function getDeathsRank() {
2137
		return $this->computeRanking('deaths');
2138
	}
2139
2140
	public function getAssistsRank() {
2141
		return $this->computeRanking('assists');
2142
	}
2143
2144
	private function computeRanking(string $dbField) : int {
2145
		$this->db->query('SELECT ranking
2146
			FROM (
2147
				SELECT player_id,
2148
				ROW_NUMBER() OVER (ORDER BY ' . $dbField . ' DESC, player_name ASC) AS ranking
2149
				FROM player
2150
				WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . '
2151
			) t
2152
			WHERE player_id = ' . $this->db->escapeNumber($this->getPlayerID())
2153
		);
2154
		$this->db->requireRecord();
2155
		return $this->db->getInt('ranking');
2156
	}
2157
2158
	public function killPlayer($sectorID) {
2159
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2160
		//msg taken care of in trader_att_proc.php
2161
		// forget plotted course
2162
		$this->deletePlottedCourse();
2163
2164
		$sector->diedHere($this);
2165
2166
		// if we are in an alliance we increase their deaths
2167
		if ($this->hasAlliance()) {
2168
			$this->db->query('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2169
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()) . ' LIMIT 1');
2170
		}
2171
2172
		// record death stat
2173
		$this->increaseHOF(1, array('Dying', 'Deaths'), HOF_PUBLIC);
2174
		//record cost of ship lost
2175
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2176
		// reset turns since last death
2177
		$this->setHOF(0, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2178
2179
		// Reset credits to starting amount + ship insurance
2180
		$credits = $this->getGame()->getStartingCredits();
2181
		$credits += IRound($this->getShip()->getCost() * self::SHIP_INSURANCE_FRACTION);
2182
		$this->setCredits($credits);
2183
2184
		$this->setSectorID($this->getHome());
2185
		$this->increaseDeaths(1);
2186
		$this->setLandedOnPlanet(false);
2187
		$this->setDead(true);
2188
		$this->setNewbieWarning(true);
2189
		$this->getShip()->getPod($this->hasNewbieStatus());
2190
		$this->setNewbieTurns(NEWBIE_TURNS_ON_DEATH);
2191
	}
2192
2193
	public function killPlayerByPlayer(AbstractSmrPlayer $killer) {
2194
		$return = array();
2195
		$msg = $this->getBBLink();
2196
2197
		if ($this->hasCustomShipName()) {
2198
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2199
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2200
		}
2201
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2202
		if ($killer->hasCustomShipName()) {
2203
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2204
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2205
		}
2206
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2207
		$this->getSector()->increaseBattles(1);
2208
		$this->db->query('INSERT INTO news (game_id,time,news_message,type,killer_id,killer_alliance,dead_id,dead_alliance) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber(SmrSession::getTime()) . ',' . $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()) . ')');
2209
2210
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2211
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getAccountID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2212
2213
		// Dead player loses between 5% and 25% experience
2214
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2215
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2216
		$this->decreaseExperience($return['DeadExp']);
2217
2218
		// Killer gains 50% of the lost exp
2219
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
2220
		$killer->increaseExperience($return['KillerExp']);
2221
2222
		$return['KillerCredits'] = $this->getCredits();
2223
		$killer->increaseCredits($return['KillerCredits']);
2224
2225
		// The killer may change alignment
2226
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2227
		$relation = $relations[$killer->getRaceID()];
2228
2229
		$alignChangePerRelation = 0.1;
2230
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2231
			$alignChangePerRelation = 0.04;
2232
		}
2233
2234
		$return['KillerAlign'] = -$relation * $alignChangePerRelation; //Lose relations when killing a peaceful race
2235
		if ($return['KillerAlign'] > 0) {
2236
			$killer->increaseAlignment($return['KillerAlign']);
2237
		} else {
2238
			$killer->decreaseAlignment(-$return['KillerAlign']);
2239
		}
2240
		// War setting gives them military pay
2241
		if ($relation <= RELATIONS_WAR) {
2242
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2243
		}
2244
2245
		//check for federal bounty being offered for current port raiders;
2246
		$this->db->query('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(SmrSession::getTime() - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2247
		$query = 'SELECT 1
2248
					FROM player_attacks_port
2249
					JOIN port USING(game_id, sector_id)
2250
					JOIN player USING(game_id, account_id)
2251
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2252
		$this->db->query($query);
2253
		if ($this->db->nextRecord()) {
2254
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2255
			$this->increaseCurrentBountyAmount('HQ', $bounty);
2256
		}
2257
2258
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2259
		$this->setBountiesClaimable($killer);
2260
2261
		// If the alignment difference is greater than 200 then a bounty may be set
2262
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2263
		$return['BountyGained'] = array(
2264
			'Type' => 'None',
2265
			'Amount' => 0
2266
		);
2267
		if ($alignmentDiff >= 200) {
2268
			// If the podded players alignment makes them deputy or member then set bounty
2269
			if ($this->getAlignment() >= 100) {
2270
				$return['BountyGained']['Type'] = 'HQ';
2271
			} elseif ($this->getAlignment() <= 100) {
2272
				$return['BountyGained']['Type'] = 'UG';
2273
			}
2274
2275
			if ($return['BountyGained']['Type'] != 'None') {
2276
				$return['BountyGained']['Amount'] = IFloor(pow($alignmentDiff, 2.56));
2277
				$killer->increaseCurrentBountyAmount($return['BountyGained']['Type'], $return['BountyGained']['Amount']);
2278
			}
2279
		}
2280
2281
		if ($this->isNPC()) {
2282
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'NPC', 'Experience', 'Gained'), HOF_PUBLIC);
2283
			$killer->increaseHOF($this->getExperience(), array('Killing', 'NPC', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2284
2285
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By NPCs Killed'), HOF_PUBLIC);
2286
2287
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2288
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Gain'), HOF_PUBLIC);
2289
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'NPC', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2290
2291
			if ($return['KillerAlign'] > 0) {
2292
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Gain'), HOF_PUBLIC);
2293
			} else {
2294
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Loss'), HOF_PUBLIC);
2295
			}
2296
2297
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'NPC', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2298
2299
			$killer->increaseHOF(1, array('Killing', 'NPC Kills'), HOF_PUBLIC);
2300
		} else {
2301
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'Experience', 'Gained'), HOF_PUBLIC);
2302
			$killer->increaseHOF($this->getExperience(), array('Killing', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2303
2304
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By Traders Killed'), HOF_PUBLIC);
2305
2306
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2307
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Gain'), HOF_PUBLIC);
2308
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2309
2310
			if ($return['KillerAlign'] > 0) {
2311
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'Alignment', 'Gain'), HOF_PUBLIC);
2312
			} else {
2313
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'Alignment', 'Loss'), HOF_PUBLIC);
2314
			}
2315
2316
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2317
2318
			if ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2319
				$killer->increaseHOF(1, array('Killing', 'Newbie Kills'), HOF_PUBLIC);
2320
			} else {
2321
				$killer->increaseKills(1);
2322
				$killer->increaseHOF(1, array('Killing', 'Kills'), HOF_PUBLIC);
2323
2324
				if ($killer->hasAlliance()) {
2325
					$this->db->query('UPDATE alliance SET alliance_kills=alliance_kills+1 WHERE alliance_id=' . $this->db->escapeNumber($killer->getAllianceID()) . ' AND game_id=' . $this->db->escapeNumber($killer->getGameID()) . ' LIMIT 1');
2326
				}
2327
2328
				// alliance vs. alliance stats
2329
				$this->incrementAllianceVsDeaths($killer->getAllianceID());
2330
			}
2331
		}
2332
2333
		$this->increaseHOF($return['BountyGained']['Amount'], array('Dying', 'Players', 'Money', 'Bounty Gained By Killer'), HOF_PUBLIC);
2334
		$this->increaseHOF($return['KillerExp'], array('Dying', 'Players', 'Experience', 'Gained By Killer'), HOF_PUBLIC);
2335
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2336
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Players', 'Experience', 'Lost'), HOF_PUBLIC);
2337
		$this->increaseHOF($return['KillerCredits'], array('Dying', 'Players', 'Money Lost'), HOF_PUBLIC);
2338
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Players', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2339
		$this->increaseHOF(1, array('Dying', 'Players', 'Deaths'), HOF_PUBLIC);
2340
2341
		$this->killPlayer($this->getSectorID());
2342
		return $return;
2343
	}
2344
2345
	public function killPlayerByForces(SmrForce $forces) {
2346
		$return = array();
2347
		$owner = $forces->getOwner();
2348
		// send a message to the person who died
2349
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getAccountID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2350
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2351
2352
		$news_message = $this->getBBLink();
2353
		if ($this->hasCustomShipName()) {
2354
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2355
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2356
		}
2357
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2358
		// insert the news entry
2359
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2360
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(SmrSession::getTime()) . ', ' . $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()) . ')');
2361
2362
		// Player loses 15% experience
2363
		$expLossPercentage = .15;
2364
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
2365
		$this->decreaseExperience($return['DeadExp']);
2366
2367
		$return['LostCredits'] = $this->getCredits();
2368
2369
		// alliance vs. alliance stats
2370
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2371
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2372
2373
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2374
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Forces', 'Experience Lost'), HOF_PUBLIC);
2375
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Forces', 'Money Lost'), HOF_PUBLIC);
2376
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Forces', 'Cost Of Ships Lost'), HOF_PUBLIC);
2377
		$this->increaseHOF(1, array('Dying', 'Forces', 'Deaths'), HOF_PUBLIC);
2378
2379
		$this->killPlayer($forces->getSectorID());
2380
		return $return;
2381
	}
2382
2383
	public function killPlayerByPort(SmrPort $port) {
2384
		$return = array();
2385
		// send a message to the person who died
2386
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2387
2388
		$news_message = $this->getBBLink();
2389
		if ($this->hasCustomShipName()) {
2390
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2391
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2392
		}
2393
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2394
		// insert the news entry
2395
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,dead_id,dead_alliance)
2396
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(SmrSession::getTime()) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber(ACCOUNT_ID_PORT) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2397
2398
		// Player loses between 15% and 20% experience
2399
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2400
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2401
		$this->decreaseExperience($return['DeadExp']);
2402
2403
		$return['LostCredits'] = $this->getCredits();
2404
2405
		// alliance vs. alliance stats
2406
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2407
2408
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2409
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Ports', 'Experience Lost'), HOF_PUBLIC);
2410
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Ports', 'Money Lost'), HOF_PUBLIC);
2411
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Ports', 'Cost Of Ships Lost'), HOF_PUBLIC);
2412
		$this->increaseHOF(1, array('Dying', 'Ports', 'Deaths'), HOF_PUBLIC);
2413
2414
		$this->killPlayer($port->getSectorID());
2415
		return $return;
2416
	}
2417
2418
	public function killPlayerByPlanet(SmrPlanet $planet) {
2419
		$return = array();
2420
		// send a message to the person who died
2421
		$planetOwner = $planet->getOwner();
2422
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getAccountID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2423
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2424
2425
		$news_message = $this->getBBLink();
2426
		if ($this->hasCustomShipName()) {
2427
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2428
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2429
		}
2430
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2431
		// insert the news entry
2432
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2433
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(SmrSession::getTime()) . ', ' . $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()) . ')');
2434
2435
		// Player loses between 15% and 20% experience
2436
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2437
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2438
		$this->decreaseExperience($return['DeadExp']);
2439
2440
		$return['LostCredits'] = $this->getCredits();
2441
2442
		// alliance vs. alliance stats
2443
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2444
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2445
2446
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2447
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Planets', 'Experience Lost'), HOF_PUBLIC);
2448
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Planets', 'Money Lost'), HOF_PUBLIC);
2449
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Planets', 'Cost Of Ships Lost'), HOF_PUBLIC);
2450
		$this->increaseHOF(1, array('Dying', 'Planets', 'Deaths'), HOF_PUBLIC);
2451
2452
		$this->killPlayer($planet->getSectorID());
2453
		return $return;
2454
	}
2455
2456
	public function incrementAllianceVsKills($otherID) {
2457
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2458
		$this->db->query('INSERT INTO alliance_vs_alliance (game_id, alliance_id_1, alliance_id_2, kills) VALUES (' . $this->db->escapeArray($values) . ') ON DUPLICATE KEY UPDATE kills = kills + 1');
2459
	}
2460
2461
	public function incrementAllianceVsDeaths($otherID) {
2462
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2463
		$this->db->query('INSERT INTO alliance_vs_alliance (game_id, alliance_id_1, alliance_id_2, kills) VALUES (' . $this->db->escapeArray($values) . ') ON DUPLICATE KEY UPDATE kills = kills + 1');
2464
	}
2465
2466
	public function getTurnsLevel() {
2467
		if (!$this->hasTurns()) {
2468
			return 'NONE';
2469
		}
2470
		if ($this->getTurns() <= 25) {
2471
			return 'LOW';
2472
		}
2473
		if ($this->getTurns() <= 75) {
2474
			return 'MEDIUM';
2475
		}
2476
		return 'HIGH';
2477
	}
2478
2479
	/**
2480
	 * Returns the CSS class color to use when displaying the player's turns
2481
	 */
2482
	public function getTurnsColor() {
2483
		return match($this->getTurnsLevel()) {
2484
			'NONE', 'LOW' => 'red',
2485
			'MEDIUM' => 'yellow',
2486
			'HIGH' => 'green',
2487
		};
2488
	}
2489
2490
	public function getTurns() {
2491
		return $this->turns;
2492
	}
2493
2494
	public function hasTurns() {
2495
		return $this->turns > 0;
2496
	}
2497
2498
	public function getMaxTurns() {
2499
		return $this->getGame()->getMaxTurns();
2500
	}
2501
2502
	public function setTurns($turns) {
2503
		if ($this->turns == $turns) {
2504
			return;
2505
		}
2506
		// Make sure turns are in range [0, MaxTurns]
2507
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2508
		$this->hasChanged = true;
2509
	}
2510
2511
	public function takeTurns($take, $takeNewbie = 0) {
2512
		if ($take < 0 || $takeNewbie < 0) {
2513
			throw new Exception('Trying to take negative turns.');
2514
		}
2515
		$take = ICeil($take);
2516
		// Only take up to as many newbie turns as we have remaining
2517
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2518
2519
		$this->setTurns($this->getTurns() - $take);
2520
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2521
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2522
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Total'), HOF_ALLIANCE);
2523
		$this->increaseHOF($takeNewbie, array('Movement', 'Turns Used', 'Newbie'), HOF_ALLIANCE);
2524
2525
		// Player has taken an action
2526
		$this->setLastActive(SmrSession::getTime());
2527
		$this->updateLastCPLAction();
2528
	}
2529
2530
	public function giveTurns(int $give, $giveNewbie = 0) {
2531
		if ($give < 0 || $giveNewbie < 0) {
2532
			throw new Exception('Trying to give negative turns.');
2533
		}
2534
		$this->setTurns($this->getTurns() + $give);
2535
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2536
	}
2537
2538
	/**
2539
	 * Calculate the time in seconds between the given time and when the
2540
	 * player will be at max turns.
2541
	 */
2542
	public function getTimeUntilMaxTurns($time, $forceUpdate = false) {
2543
		$timeDiff = $time - $this->getLastTurnUpdate();
2544
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2545
		$ship = $this->getShip($forceUpdate);
2546
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
2547
		// If already at max turns, return 0
2548
		return max(0, $maxTurnsTime);
2549
	}
2550
2551
	/**
2552
	 * Grant the player their starting turns.
2553
	 */
2554
	public function giveStartingTurns() {
2555
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
2556
		$this->giveTurns($startTurns);
2557
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2558
	}
2559
2560
	// Turns only update when player is active.
2561
	// Calculate turns gained between given time and the last turn update
2562
	public function getTurnsGained($time, $forceUpdate = false) : int {
2563
		$timeDiff = $time - $this->getLastTurnUpdate();
2564
		$ship = $this->getShip($forceUpdate);
2565
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
2566
		return $extraTurns;
2567
	}
2568
2569
	public function updateTurns() {
2570
		// is account validated?
2571
		if (!$this->getAccount()->isValidated()) {
2572
			return;
2573
		}
2574
2575
		// how many turns would he get right now?
2576
		$extraTurns = $this->getTurnsGained(SmrSession::getTime());
2577
2578
		// do we have at least one turn to give?
2579
		if ($extraTurns > 0) {
2580
			// recalc the time to avoid rounding errors
2581
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
2582
			$this->setLastTurnUpdate($newLastTurnUpdate);
2583
			$this->giveTurns($extraTurns);
2584
		}
2585
	}
2586
2587
	public function getLastTurnUpdate() {
2588
		return $this->lastTurnUpdate;
2589
	}
2590
2591
	public function setLastTurnUpdate($time) {
2592
		if ($this->lastTurnUpdate == $time) {
2593
			return;
2594
		}
2595
		$this->lastTurnUpdate = $time;
2596
		$this->hasChanged = true;
2597
	}
2598
2599
	public function getLastActive() {
2600
		return $this->lastActive;
2601
	}
2602
2603
	public function setLastActive($lastActive) {
2604
		if ($this->lastActive == $lastActive) {
2605
			return;
2606
		}
2607
		$this->lastActive = $lastActive;
2608
		$this->hasChanged = true;
2609
	}
2610
2611
	public function getLastCPLAction() {
2612
		return $this->lastCPLAction;
2613
	}
2614
2615
	public function setLastCPLAction($time) {
2616
		if ($this->lastCPLAction == $time) {
2617
			return;
2618
		}
2619
		$this->lastCPLAction = $time;
2620
		$this->hasChanged = true;
2621
	}
2622
2623
	public function updateLastCPLAction() {
2624
		$this->setLastCPLAction(SmrSession::getTime());
2625
	}
2626
2627
	public function setNewbieWarning($bool) {
2628
		if ($this->newbieWarning == $bool) {
2629
			return;
2630
		}
2631
		$this->newbieWarning = $bool;
2632
		$this->hasChanged = true;
2633
	}
2634
2635
	public function getNewbieWarning() {
2636
		return $this->newbieWarning;
2637
	}
2638
2639
	public function isDisplayMissions() {
2640
		return $this->displayMissions;
2641
	}
2642
2643
	public function setDisplayMissions($bool) {
2644
		if ($this->displayMissions == $bool) {
2645
			return;
2646
		}
2647
		$this->displayMissions = $bool;
2648
		$this->hasChanged = true;
2649
	}
2650
2651
	public function getMissions() {
2652
		if (!isset($this->missions)) {
2653
			$this->db->query('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2654
			$this->missions = array();
2655
			while ($this->db->nextRecord()) {
2656
				$missionID = $this->db->getInt('mission_id');
2657
				$this->missions[$missionID] = array(
2658
					'On Step' => $this->db->getInt('on_step'),
2659
					'Progress' => $this->db->getInt('progress'),
2660
					'Unread' => $this->db->getBoolean('unread'),
2661
					'Expires' => $this->db->getInt('step_fails'),
2662
					'Sector' => $this->db->getInt('mission_sector'),
2663
					'Starting Sector' => $this->db->getInt('starting_sector')
2664
				);
2665
				$this->rebuildMission($missionID);
2666
			}
2667
		}
2668
		return $this->missions;
2669
	}
2670
2671
	public function getActiveMissions() {
2672
		$missions = $this->getMissions();
2673
		foreach ($missions as $missionID => $mission) {
2674
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2675
				unset($missions[$missionID]);
2676
			}
2677
		}
2678
		return $missions;
2679
	}
2680
2681
	protected function getMission($missionID) {
2682
		$missions = $this->getMissions();
2683
		if (isset($missions[$missionID])) {
2684
			return $missions[$missionID];
2685
		}
2686
		return false;
2687
	}
2688
2689
	protected function hasMission($missionID) {
2690
		return $this->getMission($missionID) !== false;
2691
	}
2692
2693
	protected function updateMission($missionID) {
2694
		$this->getMissions();
2695
		if (isset($this->missions[$missionID])) {
2696
			$mission = $this->missions[$missionID];
2697
			$this->db->query('
2698
				UPDATE player_has_mission
2699
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2700
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2701
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2702
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2703
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2704
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2705
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1'
2706
			);
2707
			return true;
2708
		}
2709
		return false;
2710
	}
2711
2712
	private function setupMissionStep($missionID) {
2713
		$stepID = $this->missions[$missionID]['On Step'];
2714
		if ($stepID >= count(MISSIONS[$missionID]['Steps'])) {
2715
			// Nothing to do if this mission is already completed
2716
			return;
2717
		}
2718
		$step = MISSIONS[$missionID]['Steps'][$stepID];
2719
		if (isset($step['PickSector'])) {
2720
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2721
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2722
			if ($path === false) {
2723
				// Abandon the mission if it cannot be completed due to a
2724
				// sector that does not exist or cannot be reached.
2725
				// (Probably shouldn't bestow this mission in the first place)
2726
				$this->deleteMission($missionID);
2727
				create_error('Cannot find a path to the destination!');
2728
			}
2729
			$this->missions[$missionID]['Sector'] = $path->getEndSectorID();
2730
		}
2731
	}
2732
2733
	/**
2734
	 * Declining a mission will permanently hide it from the player
2735
	 * by adding it in its completed state.
2736
	 */
2737
	public function declineMission($missionID) {
2738
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2739
		$this->addMission($missionID, $finishedStep);
2740
	}
2741
2742
	public function addMission($missionID, $step = 0) {
2743
		$this->getMissions();
2744
2745
		if (isset($this->missions[$missionID])) {
2746
			return;
2747
		}
2748
		$sector = 0;
2749
2750
		$mission = array(
2751
			'On Step' => $step,
2752
			'Progress' => 0,
2753
			'Unread' => true,
2754
			'Expires' => (SmrSession::getTime() + 86400),
2755
			'Sector' => $sector,
2756
			'Starting Sector' => $this->getSectorID()
2757
		);
2758
2759
		$this->missions[$missionID] =& $mission;
2760
		$this->setupMissionStep($missionID);
2761
		$this->rebuildMission($missionID);
2762
2763
		$this->db->query('
2764
			REPLACE INTO player_has_mission (game_id,account_id,mission_id,on_step,progress,unread,starting_sector,mission_sector,step_fails)
2765
			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']) . ')'
2766
		);
2767
	}
2768
2769
	private function rebuildMission($missionID) {
2770
		$mission = $this->missions[$missionID];
2771
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2772
2773
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2774
			// If we have completed this mission just use false to indicate no current task.
2775
			$currentStep = false;
2776
		} else {
2777
			$data = ['player' => $this, 'mission' => $mission];
2778
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2779
			array_walk_recursive($currentStep, 'replaceMissionTemplate', $data);
2780
		}
2781
		$this->missions[$missionID]['Task'] = $currentStep;
2782
	}
2783
2784
	public function deleteMission($missionID) {
2785
		$this->getMissions();
2786
		if (isset($this->missions[$missionID])) {
2787
			unset($this->missions[$missionID]);
2788
			$this->db->query('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1');
2789
			return true;
2790
		}
2791
		return false;
2792
	}
2793
2794
	public function markMissionsRead() {
2795
		$this->getMissions();
2796
		$unreadMissions = array();
2797
		foreach ($this->missions as $missionID => &$mission) {
2798
			if ($mission['Unread']) {
2799
				$unreadMissions[] = $missionID;
2800
				$mission['Unread'] = false;
2801
				$this->updateMission($missionID);
2802
			}
2803
		}
2804
		return $unreadMissions;
2805
	}
2806
2807
	public function claimMissionReward($missionID) {
2808
		$this->getMissions();
2809
		$mission =& $this->missions[$missionID];
2810
		if ($mission === false) {
2811
			throw new Exception('Unknown mission: ' . $missionID);
2812
		}
2813
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2814
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2815
		}
2816
		$mission['On Step']++;
2817
		$mission['Unread'] = true;
2818
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2819
			switch ($rewardItem) {
2820
				case 'Credits':
2821
					$this->increaseCredits($amount);
2822
				break;
2823
				case 'Experience':
2824
					$this->increaseExperience($amount);
2825
				break;
2826
			}
2827
		}
2828
		$rewardText = $mission['Task']['Rewards']['Text'];
2829
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2830
			// If we haven't finished the mission yet then
2831
			$this->setupMissionStep($missionID);
2832
		}
2833
		$this->rebuildMission($missionID);
2834
		$this->updateMission($missionID);
2835
		return $rewardText;
2836
	}
2837
2838
	public function getAvailableMissions() {
2839
		$availableMissions = array();
2840
		foreach (MISSIONS as $missionID => $mission) {
2841
			if ($this->hasMission($missionID)) {
2842
				continue;
2843
			}
2844
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2845
			if ($this->getSector()->hasX($realX)) {
2846
				$availableMissions[$missionID] = $mission;
2847
			}
2848
		}
2849
		return $availableMissions;
2850
	}
2851
2852
	/**
2853
	 * Log a player action in the current sector to the admin log console.
2854
	 */
2855
	public function log(int $log_type_id, string $msg) : void {
2856
		$this->getAccount()->log($log_type_id, $msg, $this->getSectorID());
2857
	}
2858
2859
	public function actionTaken($actionID, array $values) {
2860
		if (!in_array($actionID, MISSION_ACTIONS)) {
2861
			throw new Exception('Unknown action: ' . $actionID);
2862
		}
2863
// TODO: Reenable this once tested.		if($this->getAccount()->isLoggingEnabled())
2864
			switch ($actionID) {
2865
				case 'WalkSector':
2866
					$this->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID());
2867
				break;
2868
				case 'JoinAlliance':
2869
					$this->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName());
2870
				break;
2871
				case 'LeaveAlliance':
2872
					$this->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName());
2873
				break;
2874
				case 'DisbandAlliance':
2875
					$this->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName());
2876
				break;
2877
				case 'KickPlayer':
2878
					$this->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName());
2879
				break;
2880
				case 'PlayerKicked':
2881
					$this->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')');
2882
				break;
2883
2884
			}
2885
		$this->getMissions();
2886
		foreach ($this->missions as $missionID => &$mission) {
2887
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
2888
				$requirements = $mission['Task']['Detail'];
2889
				if (checkMissionRequirements($values, $requirements) === true) {
2890
					$mission['On Step']++;
2891
					$mission['Unread'] = true;
2892
					$this->setupMissionStep($missionID);
2893
					$this->rebuildMission($missionID);
2894
					$this->updateMission($missionID);
2895
				}
2896
			}
2897
		}
2898
	}
2899
2900
	public function canSeeAny(array $otherPlayerArray) {
2901
		foreach ($otherPlayerArray as $otherPlayer) {
2902
			if ($this->canSee($otherPlayer)) {
2903
				return true;
2904
			}
2905
		}
2906
		return false;
2907
	}
2908
2909
	public function canSee(AbstractSmrPlayer $otherPlayer) {
2910
		if (!$otherPlayer->getShip()->isCloaked()) {
2911
			return true;
2912
		}
2913
		if ($this->sameAlliance($otherPlayer)) {
2914
			return true;
2915
		}
2916
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
2917
			return true;
2918
		}
2919
		return false;
2920
	}
2921
2922
	public function equals(AbstractSmrPlayer $otherPlayer = null) {
2923
		return $otherPlayer !== null && $this->getAccountID() == $otherPlayer->getAccountID() && $this->getGameID() == $otherPlayer->getGameID();
2924
	}
2925
2926
	public function sameAlliance(AbstractSmrPlayer $otherPlayer = null) {
2927
		return $this->equals($otherPlayer) || (!is_null($otherPlayer) && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
2928
	}
2929
2930
	public function sharedForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2931
		return $this->sameAlliance($otherPlayer);
2932
	}
2933
2934
	public function forceNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2935
		return $this->sameAlliance($otherPlayer);
2936
	}
2937
2938
	public function planetNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2939
		return $this->sameAlliance($otherPlayer);
2940
	}
2941
2942
	public function traderNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2943
		return $this->sameAlliance($otherPlayer);
2944
	}
2945
2946
	public function traderMAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2947
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
2948
	}
2949
2950
	public function traderAttackTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2951
		return $this->sameAlliance($otherPlayer);
2952
	}
2953
2954
	public function traderDefendTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2955
		return $this->sameAlliance($otherPlayer);
2956
	}
2957
2958
	public function traderAttackForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2959
		return $this->sameAlliance($otherPlayer);
2960
	}
2961
2962
	public function traderAttackPortAlliance(AbstractSmrPlayer $otherPlayer = null) {
2963
		return $this->sameAlliance($otherPlayer);
2964
	}
2965
2966
	public function traderAttackPlanetAlliance(AbstractSmrPlayer $otherPlayer = null) {
2967
		return $this->sameAlliance($otherPlayer);
2968
	}
2969
2970
	public function meetsAlignmentRestriction($restriction) {
2971
		if ($restriction < 0) {
2972
			return $this->getAlignment() <= $restriction;
2973
		}
2974
		if ($restriction > 0) {
2975
			return $this->getAlignment() >= $restriction;
2976
		}
2977
		return true;
2978
	}
2979
2980
	// Get an array of goods that are visible to the player
2981
	public function getVisibleGoods() {
2982
		$goods = Globals::getGoods();
2983
		$visibleGoods = array();
2984
		foreach ($goods as $key => $good) {
2985
			if ($this->meetsAlignmentRestriction($good['AlignRestriction'])) {
2986
				$visibleGoods[$key] = $good;
2987
			}
2988
		}
2989
		return $visibleGoods;
2990
	}
2991
2992
	/**
2993
	 * Will retrieve all visited sectors, use only when you are likely to check a large number of these
2994
	 */
2995
	public function hasVisitedSector($sectorID) {
2996
		if (!isset($this->visitedSectors)) {
2997
			$this->visitedSectors = array();
2998
			$this->db->query('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
2999
			while ($this->db->nextRecord()) {
3000
				$this->visitedSectors[$this->db->getInt('sector_id')] = false;
3001
			}
3002
		}
3003
		return !isset($this->visitedSectors[$sectorID]);
3004
	}
3005
3006
	public function getLeaveNewbieProtectionHREF() {
3007
		return SmrSession::getNewHREF(create_container('leave_newbie_processing.php'));
3008
	}
3009
3010
	public function getExamineTraderHREF() {
3011
		$container = create_container('skeleton.php', 'trader_examine.php');
3012
		$container['target'] = $this->getAccountID();
3013
		return SmrSession::getNewHREF($container);
3014
	}
3015
3016
	public function getAttackTraderHREF() {
3017
		return Globals::getAttackTraderHREF($this->getAccountID());
3018
	}
3019
3020
	public function getPlanetKickHREF() {
3021
		$container = create_container('planet_kick_processing.php', 'trader_attack_processing.php');
3022
		$container['account_id'] = $this->getAccountID();
3023
		return SmrSession::getNewHREF($container);
3024
	}
3025
3026
	public function getTraderSearchHREF() {
3027
		$container = create_container('skeleton.php', 'trader_search_result.php');
3028
		$container['player_id'] = $this->getPlayerID();
3029
		return SmrSession::getNewHREF($container);
3030
	}
3031
3032
	public function getAllianceRosterHREF() {
3033
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3034
	}
3035
3036
	public function getToggleWeaponHidingHREF($ajax = false) {
3037
		$container = create_container('toggle_processing.php');
3038
		$container['toggle'] = 'WeaponHiding';
3039
		$container['AJAX'] = $ajax;
3040
		return SmrSession::getNewHREF($container);
3041
	}
3042
3043
	public function isDisplayWeapons() {
3044
		return $this->displayWeapons;
3045
	}
3046
3047
	/**
3048
	 * Should weapons be displayed in the right panel?
3049
	 * This updates the player database directly because it is used with AJAX,
3050
	 * which does not acquire a sector lock.
3051
	 */
3052
	public function setDisplayWeapons($bool) {
3053
		if ($this->displayWeapons == $bool) {
3054
			return;
3055
		}
3056
		$this->displayWeapons = $bool;
3057
		$this->db->query('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3058
	}
3059
3060
	public function update() {
3061
		$this->save();
3062
	}
3063
3064
	public function save() {
3065
		if ($this->hasChanged === true) {
3066
			$this->db->query('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3067
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3068
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3069
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3070
				', turns=' . $this->db->escapeNumber($this->turns) .
3071
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3072
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3073
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3074
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3075
				', dead=' . $this->db->escapeBoolean($this->dead) .
3076
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3077
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3078
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3079
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3080
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3081
				', credits=' . $this->db->escapeNumber($this->credits) .
3082
				', experience=' . $this->db->escapeNumber($this->experience) .
3083
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3084
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3085
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3086
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3087
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3088
				', kills=' . $this->db->escapeNumber($this->kills) .
3089
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3090
				', assists=' . $this->db->escapeNumber($this->assists) .
3091
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3092
				', bank=' . $this->db->escapeNumber($this->bank) .
3093
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3094
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3095
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3096
				', group_scout_messages=' . $this->db->escapeString($this->groupScoutMessages) .
3097
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3098
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3099
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3100
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3101
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3102
				' WHERE ' . $this->SQL . ' LIMIT 1');
3103
			$this->hasChanged = false;
3104
		}
3105
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3106
			if ($bountyChanged === true) {
3107
				$bountyChanged = false;
3108
				$bounty = $this->getBounty($key);
3109
				if ($bounty['New'] === true) {
3110
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3111
						$this->db->query('INSERT INTO bounty (account_id,game_id,type,amount,smr_credits,claimer_id,time) VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeString($bounty['Type']) . ',' . $this->db->escapeNumber($bounty['Amount']) . ',' . $this->db->escapeNumber($bounty['SmrCredits']) . ',' . $this->db->escapeNumber($bounty['Claimer']) . ',' . $this->db->escapeNumber($bounty['Time']) . ')');
3112
					}
3113
				} else {
3114
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3115
						$this->db->query('UPDATE bounty
3116
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3117
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3118
							type=' . $this->db->escapeString($bounty['Type']) . ',
3119
							claimer_id=' . $this->db->escapeNumber($bounty['Claimer']) . ',
3120
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3121
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3122
					} else {
3123
						$this->db->query('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3124
					}
3125
				}
3126
			}
3127
		}
3128
		$this->saveHOF();
3129
	}
3130
3131
	public function saveHOF() {
3132
		if (count($this->hasHOFChanged) > 0) {
3133
			$this->doHOFSave($this->hasHOFChanged);
3134
			$this->hasHOFChanged = [];
3135
		}
3136
		if (!empty(self::$hasHOFVisChanged)) {
3137
			foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3138
				if ($changeType == self::HOF_NEW) {
3139
					$this->db->query('INSERT INTO hof_visibility (type, visibility) VALUES (' . $this->db->escapeString($hofType) . ',' . $this->db->escapeString(self::$HOFVis[$hofType]) . ')');
3140
				} else {
3141
					$this->db->query('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType) . ' LIMIT 1');
3142
				}
3143
				unset(self::$hasHOFVisChanged[$hofType]);
3144
			}
3145
		}
3146
	}
3147
3148
	/**
3149
	 * This should only be called by `saveHOF` (and recursively) to
3150
	 * ensure that the `hasHOFChanged` attribute is properly cleared.
3151
	 */
3152
	protected function doHOFSave(array $hasChangedList, array $typeList = array()) {
3153
		foreach ($hasChangedList as $type => $hofChanged) {
3154
			$tempTypeList = $typeList;
3155
			$tempTypeList[] = $type;
3156
			if (is_array($hofChanged)) {
3157
				$this->doHOFSave($hofChanged, $tempTypeList);
3158
			} else {
3159
				$amount = $this->getHOF($tempTypeList);
3160
				if ($hofChanged == self::HOF_NEW) {
3161
					if ($amount > 0) {
3162
						$this->db->query('INSERT INTO player_hof (account_id,game_id,type,amount) VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeArray($tempTypeList, ':', false) . ',' . $this->db->escapeNumber($amount) . ')');
3163
					}
3164
				} elseif ($hofChanged == self::HOF_CHANGED) {
3165
					$this->db->query('UPDATE player_hof
3166
						SET amount=' . $this->db->escapeNumber($amount) . '
3167
						WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeArray($tempTypeList, ':', false) . ' LIMIT 1');
3168
				}
3169
			}
3170
		}
3171
	}
3172
3173
}
3174