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
Pull Request — master (#912)
by Dan
06:13
created

AbstractSmrPlayer::rebuildMission()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 13
rs 9.9666
1
<?php declare(strict_types=1);
2
require_once('missions.inc');
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 HOF_CHANGED = 1;
14
	const HOF_NEW = 2;
15
16
	protected static $CACHE_SECTOR_PLAYERS = array();
17
	protected static $CACHE_PLANET_PLAYERS = array();
18
	protected static $CACHE_ALLIANCE_PLAYERS = array();
19
	protected static $CACHE_PLAYERS = array();
20
21
	protected $db;
22
	protected $SQL;
23
24
	protected $accountID;
25
	protected $gameID;
26
	protected $playerName;
27
	protected $playerID;
28
	protected $sectorID;
29
	protected $lastSectorID;
30
	protected $newbieTurns;
31
	protected $dead;
32
	protected $npc;
33
	protected $newbieStatus;
34
	protected $newbieWarning;
35
	protected $landedOnPlanet;
36
	protected $lastActive;
37
	protected $credits;
38
	protected $alignment;
39
	protected $experience;
40
	protected $level;
41
	protected $allianceID;
42
	protected $shipID;
43
	protected $kills;
44
	protected $deaths;
45
	protected $assists;
46
	protected $stats;
47
	protected $personalRelations;
48
	protected $relations;
49
	protected $militaryPayment;
50
	protected $bounties;
51
	protected $turns;
52
	protected $lastCPLAction;
53
	protected $missions;
54
55
	protected $tickers;
56
	protected $lastTurnUpdate;
57
	protected $lastNewsUpdate;
58
	protected $attackColour;
59
	protected $allianceJoinable;
60
	protected $lastPort;
61
	protected $bank;
62
	protected $zoom;
63
	protected $displayMissions;
64
	protected $displayWeapons;
65
	protected $ignoreGlobals;
66
	protected $plottedCourse;
67
	protected $plottedCourseFrom;
68
	protected $nameChanged;
69
	protected bool $raceChanged;
70
	protected $combatDronesKamikazeOnMines;
71
	protected $customShipName;
72
	protected $storedDestinations;
73
74
	protected $visitedSectors;
75
	protected $allianceRoles = array(
76
		0 => 0
77
	);
78
79
	protected $draftLeader;
80
	protected $gpWriter;
81
	protected $HOF;
82
	protected static $HOFVis;
83
84
	protected $hasChanged = false;
85
	protected array $hasHOFChanged = [];
86
	protected static $hasHOFVisChanged = array();
87
	protected $hasBountyChanged = array();
88
89
	public static function refreshCache() {
90
		foreach (self::$CACHE_PLAYERS as $gameID => &$gamePlayers) {
91
			foreach ($gamePlayers as $playerID => &$player) {
92
				$player = self::getPlayer($playerID, $gameID, true);
93
			}
94
		}
95
	}
96
97
	public static function clearCache() {
98
		self::$CACHE_PLAYERS = array();
99
		self::$CACHE_SECTOR_PLAYERS = array();
100
	}
101
102
	public static function savePlayers() {
103
		foreach (self::$CACHE_PLAYERS as $gamePlayers) {
104
			foreach ($gamePlayers as $player) {
105
				$player->save();
106
			}
107
		}
108
	}
109
110
	public static function getSectorPlayersByAlliances($gameID, $sectorID, array $allianceIDs, $forceUpdate = false) {
111
		$players = self::getSectorPlayers($gameID, $sectorID, $forceUpdate); // Don't use & as we do an unset
112
		foreach ($players as $playerID => $player) {
113
			if (!in_array($player->getAllianceID(), $allianceIDs)) {
114
				unset($players[$playerID]);
115
			}
116
		}
117
		return $players;
118
	}
119
120
	/**
121
	 * Returns the same players as getSectorPlayers (e.g. not on planets),
122
	 * but for an entire galaxy rather than a single sector. This is useful
123
	 * for reducing the number of queries in galaxy-wide processing.
124
	 */
125
	public static function getGalaxyPlayers($gameID, $galaxyID, $forceUpdate = false) {
126
		$db = new SmrMySqlDatabase();
127
		$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(TIME - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND galaxy_id = ' . $db->escapeNumber($galaxyID));
128
		$galaxyPlayers = [];
129
		while ($db->nextRecord()) {
130
			$sectorID = $db->getInt('sector_id');
131
			if (!$db->hasField('player_id')) {
132
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = [];
133
			} else {
134
				$playerID = $db->getInt('player_id');
135
				$player = self::getPlayer($playerID, $gameID, $forceUpdate, $db);
136
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$playerID] = $player;
137
				$galaxyPlayers[$sectorID][$playerID] = $player;
138
			}
139
		}
140
		return $galaxyPlayers;
141
	}
142
143
	public static function getSectorPlayers($gameID, $sectorID, $forceUpdate = false) {
144
		if ($forceUpdate || !isset(self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID])) {
145
			$db = new SmrMySqlDatabase();
146
			$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(TIME - TIME_BEFORE_INACTIVE) . ' OR newbie_turns = 0) AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
147
			$players = array();
148
			while ($db->nextRecord()) {
149
				$playerID = $db->getInt('player_id');
150
				$players[$playerID] = self::getPlayer($playerID, $gameID, $forceUpdate, $db);
151
			}
152
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = $players;
153
		}
154
		return self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID];
155
	}
156
157
	public static function getPlanetPlayers($gameID, $sectorID, $forceUpdate = false) {
158
		if ($forceUpdate || !isset(self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID])) {
159
			$db = new SmrMySqlDatabase();
160
			$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');
161
			$players = array();
162
			while ($db->nextRecord()) {
163
				$playerID = $db->getInt('player_id');
164
				$players[$playerID] = self::getPlayer($playerID, $gameID, $forceUpdate, $db);
165
			}
166
			self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID] = $players;
167
		}
168
		return self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID];
169
	}
170
171
	public static function getAlliancePlayers($gameID, $allianceID, $forceUpdate = false) {
172
		if ($forceUpdate || !isset(self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID])) {
173
			$db = new SmrMySqlDatabase();
174
			$db->query('SELECT * FROM player WHERE alliance_id = ' . $db->escapeNumber($allianceID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' ORDER BY experience DESC');
175
			$players = array();
176
			while ($db->nextRecord()) {
177
				$playerID = $db->getInt('player_id');
178
				$players[$playerID] = self::getPlayer($playerID, $gameID, $forceUpdate, $db);
179
			}
180
			self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID] = $players;
181
		}
182
		return self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID];
183
	}
184
185
	public static function getPlayer($playerID, $gameID, $forceUpdate = false, $db = null) {
186
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$playerID])) {
187
			self::$CACHE_PLAYERS[$gameID][$playerID] = new SmrPlayer($playerID, $gameID, $db);
188
		}
189
		return self::$CACHE_PLAYERS[$gameID][$playerID];
190
	}
191
192
	public static function getPlayerByPlayerName($playerName, $gameID, $forceUpdate = false) {
193
		$db = new SmrMySqlDatabase();
194
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
195
		if ($db->nextRecord()) {
196
			return self::getPlayer($db->getInt('player_id'), $gameID, $forceUpdate, $db);
197
		}
198
		throw new PlayerNotFoundException('Player Name not found.');
199
	}
200
201
	protected function __construct($playerID, $gameID, $db = null) {
202
		$this->db = new SmrMySqlDatabase();
203
		$this->SQL = 'player_id = ' . $this->db->escapeNumber($playerID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
204
205
		if (isset($db)) {
206
			$playerExists = true;
207
		} else {
208
			$db = $this->db;
209
			$this->db->query('SELECT * FROM player WHERE ' . $this->SQL . ' LIMIT 1');
210
			$playerExists = $db->nextRecord();
211
		}
212
213
		if ($playerExists) {
214
			$this->playerID = (int)$playerID;
215
			$this->gameID = (int)$gameID;
216
			$this->accountID = $db->getInt('account_id');
217
			$this->playerName = $db->getField('player_name');
218
			$this->sectorID = $db->getInt('sector_id');
219
			$this->lastSectorID = $db->getInt('last_sector_id');
220
			$this->turns = $db->getInt('turns');
221
			$this->lastTurnUpdate = $db->getInt('last_turn_update');
222
			$this->newbieTurns = $db->getInt('newbie_turns');
223
			$this->lastNewsUpdate = $db->getInt('last_news_update');
224
			$this->attackColour = $db->getField('attack_warning');
225
			$this->dead = $db->getBoolean('dead');
226
			$this->npc = $db->getBoolean('npc');
227
			$this->newbieStatus = $db->getBoolean('newbie_status');
228
			$this->landedOnPlanet = $db->getBoolean('land_on_planet');
229
			$this->lastActive = $db->getInt('last_active');
230
			$this->lastCPLAction = $db->getInt('last_cpl_action');
231
			$this->raceID = $db->getInt('race_id');
232
			$this->credits = $db->getInt('credits');
233
			$this->experience = $db->getInt('experience');
234
			$this->alignment = $db->getInt('alignment');
235
			$this->militaryPayment = $db->getInt('military_payment');
236
			$this->allianceID = $db->getInt('alliance_id');
237
			$this->allianceJoinable = $db->getInt('alliance_join');
238
			$this->shipID = $db->getInt('ship_type_id');
239
			$this->kills = $db->getInt('kills');
240
			$this->deaths = $db->getInt('deaths');
241
			$this->assists = $db->getInt('assists');
242
			$this->lastPort = $db->getInt('last_port');
243
			$this->bank = $db->getInt('bank');
244
			$this->zoom = $db->getInt('zoom');
245
			$this->displayMissions = $db->getBoolean('display_missions');
246
			$this->displayWeapons = $db->getBoolean('display_weapons');
247
			$this->forceDropMessages = $db->getBoolean('force_drop_messages');
0 ignored issues
show
Bug Best Practice introduced by
The property forceDropMessages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
248
			$this->groupScoutMessages = $db->getField('group_scout_messages');
0 ignored issues
show
Bug Best Practice introduced by
The property groupScoutMessages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
249
			$this->ignoreGlobals = $db->getBoolean('ignore_globals');
250
			$this->newbieWarning = $db->getBoolean('newbie_warning');
251
			$this->nameChanged = $db->getBoolean('name_changed');
252
			$this->raceChanged = $db->getBoolean('race_changed');
253
			$this->combatDronesKamikazeOnMines = $db->getBoolean('combat_drones_kamikaze_on_mines');
254
		} else {
255
			throw new PlayerNotFoundException('Invalid playerID: ' . $playerID . ' OR gameID:' . $gameID);
256
		}
257
	}
258
259
	/**
260
	 * Insert a new player into the database. Returns the new player object.
261
	 */
262
	public static function createPlayer($accountID, $gameID, $playerName, $raceID, $isNewbie, $npc=false) {
263
		$db = new SmrMySqlDatabase();
264
		$db->lockTable('player');
265
266
		// Player names must be unique within each game
267
		$db->query('SELECT 1 FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
268
		if ($db->nextRecord() > 0) {
269
			$db->unlock();
270
			create_error('The player name already exists.');
271
		}
272
273
		// get last registered player id in that game and increase by one.
274
		$db->query('SELECT MAX(player_id) FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
275
		if ($db->nextRecord()) {
276
			$playerID = $db->getInt('MAX(player_id)') + 1;
277
		} else {
278
			$playerID = 1;
279
		}
280
281
		$startSectorID = 0; // Temporarily put player into non-existent sector
282
		$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)
283
					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) . ')');
284
285
		$db->unlock();
286
287
		$player = SmrPlayer::getPlayer($playerID, $gameID);
288
		$player->setSectorID($player->getHome());
289
		return $player;
290
	}
291
292
	/**
293
	 * Get array of players whose info can be accessed by this player.
294
	 * Skips players who are not in the same alliance as this player.
295
	 */
296
	public function getSharingPlayers($forceUpdate = false) {
297
		$results = array($this);
298
299
		// Only return this player if not in an alliance
300
		if (!$this->hasAlliance()) {
301
			return $results;
302
		}
303
304
		// Get other players who are sharing info for this game.
305
		// NOTE: game_id=0 means that player shares info for all games.
306
		$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()) . ')');
307
		while ($this->db->nextRecord()) {
308
			try {
309
				$otherPlayer = SmrPlayer::getPlayer($this->db->getInt('from_account_id'), //TODO
310
				                                    $this->getGameID(), $forceUpdate);
311
			} catch (PlayerNotFoundException $e) {
312
				// Skip players that have not joined this game
313
				continue;
314
			}
315
316
			// players must be in the same alliance
317
			if ($this->sameAlliance($otherPlayer)) {
318
				$results[] = $otherPlayer;
319
			}
320
		}
321
		return $results;
322
	}
323
324
	public function getSQL() : string {
325
		return $this->SQL;
326
	}
327
328
	public function getZoom() {
329
		return $this->zoom;
330
	}
331
332
	protected function setZoom($zoom) {
333
		// Set the zoom level between [1, 9]
334
		$zoom = max(1, min(9, $zoom));
335
		if ($this->zoom == $zoom) {
336
			return;
337
		}
338
		$this->zoom = $zoom;
339
		$this->hasChanged = true;
340
	}
341
342
	public function increaseZoom($zoom) {
343
		if ($zoom < 0) {
344
			throw new Exception('Trying to increase negative zoom.');
345
		}
346
		$this->setZoom($this->getZoom() + $zoom);
347
	}
348
349
	public function decreaseZoom($zoom) {
350
		if ($zoom < 0) {
351
			throw new Exception('Trying to decrease negative zoom.');
352
		}
353
		$this->setZoom($this->getZoom() - $zoom);
354
	}
355
356
	public function getAttackColour() {
357
		return $this->attackColour;
358
	}
359
360
	public function setAttackColour($colour) {
361
		if ($this->attackColour == $colour) {
362
			return;
363
		}
364
		$this->attackColour = $colour;
365
		$this->hasChanged = true;
366
	}
367
368
	public function isIgnoreGlobals() {
369
		return $this->ignoreGlobals;
370
	}
371
372
	public function setIgnoreGlobals($bool) {
373
		if ($this->ignoreGlobals == $bool) {
374
			return;
375
		}
376
		$this->ignoreGlobals = $bool;
377
		$this->hasChanged = true;
378
	}
379
380
	public function getAccount() {
381
		return SmrAccount::getAccount($this->getAccountID());
382
	}
383
384
	public function getAccountID() {
385
		return $this->accountID;
386
	}
387
388
	public function getGameID() {
389
		return $this->gameID;
390
	}
391
392
	public function getGame() {
393
		return SmrGame::getGame($this->gameID);
394
	}
395
396
	public function getNewbieTurns() {
397
		return $this->newbieTurns;
398
	}
399
400
	public function hasNewbieTurns() {
401
		return $this->getNewbieTurns() > 0;
402
	}
403
	public function setNewbieTurns($newbieTurns) {
404
		if ($this->newbieTurns == $newbieTurns) {
405
			return;
406
		}
407
		$this->newbieTurns = $newbieTurns;
408
		$this->hasChanged = true;
409
	}
410
411
	public function getShip($forceUpdate = false) {
412
		return SmrShip::getShip($this, $forceUpdate);
413
	}
414
415
	public function getShipTypeID() {
416
		return $this->shipID;
417
	}
418
419
	/**
420
	 * Do not call directly. Use SmrShip::setShipTypeID instead.
421
	 */
422
	public function setShipTypeID($shipID) {
423
		if ($this->shipID == $shipID) {
424
			return;
425
		}
426
		$this->shipID = $shipID;
427
		$this->hasChanged = true;
428
	}
429
430
	public function hasCustomShipName() {
431
		return $this->getCustomShipName() !== false;
432
	}
433
434
	public function getCustomShipName() {
435
		if (!isset($this->customShipName)) {
436
			$this->db->query('SELECT * FROM ship_has_name WHERE ' . $this->SQL . ' LIMIT 1');
437
			if ($this->db->nextRecord()) {
438
				$this->customShipName = $this->db->getField('ship_name');
439
			} else {
440
				$this->customShipName = false;
441
			}
442
		}
443
		return $this->customShipName;
444
	}
445
446
	public function setCustomShipName(string $name) {
447
		$this->db->query('REPLACE INTO ship_has_name (game_id, player_id, ship_name)
448
			VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getPlayerID()) . ', ' . $this->db->escapeString($name) . ')');
449
	}
450
451
	/**
452
	 * Get planet owned by this player.
453
	 * Returns false if this player does not own a planet.
454
	 */
455
	public function getPlanet() {
456
		$this->db->query('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_player_id=' . $this->db->escapeNumber($this->getPlayerID()));
457
		if ($this->db->nextRecord()) {
458
			return SmrPlanet::getPlanet($this->getGameID(), $this->db->getInt('sector_id'), false, $this->db);
459
		} else {
460
			return false;
461
		}
462
	}
463
464
	public function getSectorPlanet() {
465
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
466
	}
467
468
	public function getSectorPort() {
469
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
470
	}
471
472
	public function getSectorID() {
473
		return $this->sectorID;
474
	}
475
476
	public function getSector() {
477
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
478
	}
479
480
	public function setSectorID($sectorID) {
481
		if ($this->sectorID == $sectorID) {
482
			return;
483
		}
484
485
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
486
		$port->addCachePort($this->getPlayerID()); //Add port of sector we were just in, to make sure it is left totally up to date.
487
488
		$this->setLastSectorID($this->getSectorID());
489
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
490
		$this->sectorID = $sectorID;
491
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
492
		$this->hasChanged = true;
493
494
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
495
		$port->addCachePort($this->getPlayerID()); //Add the port of sector we are now in.
496
	}
497
498
	public function getLastSectorID() {
499
		return $this->lastSectorID;
500
	}
501
502
	public function setLastSectorID($lastSectorID) {
503
		if ($this->lastSectorID == $lastSectorID) {
504
			return;
505
		}
506
		$this->lastSectorID = $lastSectorID;
507
		$this->hasChanged = true;
508
	}
509
510
	public function getHome() {
511
		// get his home sector
512
		$hq_id = GOVERNMENT + $this->getRaceID();
513
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
514
		if (!empty($raceHqSectors)) {
515
			// If race has multiple HQ's for some reason, use the first one
516
			return key($raceHqSectors);
517
		} else {
518
			return 1;
519
		}
520
	}
521
522
	public function isDead() {
523
		return $this->dead;
524
	}
525
526
	public function isNPC() {
527
		return $this->npc;
528
	}
529
530
	/**
531
	 * Does the player have Newbie status?
532
	 */
533
	public function hasNewbieStatus() {
534
		return $this->newbieStatus;
535
	}
536
537
	/**
538
	 * Update the player's newbie status if it has changed.
539
	 * This function queries the account, so use sparingly.
540
	 */
541
	public function updateNewbieStatus() {
542
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
543
		if ($this->newbieStatus != $accountNewbieStatus) {
544
			$this->newbieStatus = $accountNewbieStatus;
545
			$this->hasChanged = true;
546
		}
547
	}
548
549
	/**
550
	 * Has this player been designated as the alliance flagship?
551
	 */
552
	public function isFlagship() {
553
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipPlayerID() == $this->getPlayerID();
554
	}
555
556
	public function isPresident() {
557
		return Council::getPresidentPlayerID($this->getGameID(), $this->getRaceID()) == $this->getPlayerID();
0 ignored issues
show
Bug introduced by
The method getPresidentPlayerID() does not exist on Council. Did you maybe mean getPresidentID()? ( Ignorable by Annotation )

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

557
		return Council::/** @scrutinizer ignore-call */ getPresidentPlayerID($this->getGameID(), $this->getRaceID()) == $this->getPlayerID();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
558
	}
559
560
	public function isOnCouncil() {
561
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getPlayerID());
562
	}
563
564
	public function isDraftLeader() {
565
		if (!isset($this->draftLeader)) {
566
			$this->draftLeader = false;
567
			$this->db->query('SELECT 1 FROM draft_leaders WHERE ' . $this->SQL . ' LIMIT 1');
568
			if ($this->db->nextRecord()) {
569
				$this->draftLeader = true;
570
			}
571
		}
572
		return $this->draftLeader;
573
	}
574
575
	public function getGPWriter() {
576
		if (!isset($this->gpWriter)) {
577
			$this->gpWriter = false;
578
			$this->db->query('SELECT position FROM galactic_post_writer WHERE ' . $this->SQL);
579
			if ($this->db->nextRecord()) {
580
				$this->gpWriter = $this->db->getField('position');
581
			}
582
		}
583
		return $this->gpWriter;
584
	}
585
586
	public function isGPEditor() {
587
		return $this->getGPWriter() == 'editor';
588
	}
589
590
	public function isForceDropMessages() {
591
		return $this->forceDropMessages;
592
	}
593
594
	public function setForceDropMessages($bool) {
595
		if ($this->forceDropMessages == $bool) {
596
			return;
597
		}
598
		$this->forceDropMessages = $bool;
0 ignored issues
show
Bug Best Practice introduced by
The property forceDropMessages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
599
		$this->hasChanged = true;
600
	}
601
602
	public function getScoutMessageGroupLimit() {
603
		if ($this->groupScoutMessages == 'ALWAYS') {
604
			return 0;
605
		} elseif ($this->groupScoutMessages == 'AUTO') {
606
			return MESSAGES_PER_PAGE;
607
		} elseif ($this->groupScoutMessages == 'NEVER') {
608
			return PHP_INT_MAX;
609
		}
610
	}
611
612
	public function getGroupScoutMessages() {
613
		return $this->groupScoutMessages;
614
	}
615
616
	public function setGroupScoutMessages($setting) {
617
		if ($this->groupScoutMessages == $setting) {
618
			return;
619
		}
620
		$this->groupScoutMessages = $setting;
0 ignored issues
show
Bug Best Practice introduced by
The property groupScoutMessages does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
621
		$this->hasChanged = true;
622
	}
623
624
	protected static function doMessageSending($senderPlayerID, $receiverPlayerID, $gameID, $messageTypeID, $message, $expires, $senderDelete = false, $unread = true) {
625
		$message = trim($message);
626
		$db = new SmrMySqlDatabase();
627
		// send him the message
628
		$db->query('INSERT INTO message
629
			(receiver_player_id,game_id,message_type_id,message_text,
630
			sender_player_id,send_time,expire_time,sender_delete) VALUES(' .
631
			$db->escapeNumber($receiverPlayerID) . ',' .
632
			$db->escapeNumber($gameID) . ',' .
633
			$db->escapeNumber($messageTypeID) . ',' .
634
			$db->escapeString($message) . ',' .
635
			$db->escapeNumber($senderPlayerID) . ',' .
636
			$db->escapeNumber(TIME) . ',' .
637
			$db->escapeNumber($expires) . ',' .
638
			$db->escapeBoolean($senderDelete) . ')'
639
		);
640
		// Keep track of the message_id so it can be returned
641
		$insertID = $db->getInsertID();
642
643
		if ($unread === true) {
644
			// give him the message icon
645
			$db->query('REPLACE INTO player_has_unread_messages (game_id, player_id, message_type_id) VALUES
646
						(' . $db->escapeNumber($gameID) . ', ' . $db->escapeNumber($receiverPlayerID) . ', ' . $db->escapeNumber($messageTypeID) . ')');
647
		}
648
649
		switch ($messageTypeID) {
650
			case MSG_PLAYER:
651
				$receiverAccount = SmrPlayer::getPlayer($receiverPlayerID, $gameID)->getAccount();
652
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isLoggedIn()) {
653
					require_once(get_file_loc('message.functions.inc'));
654
					$sender = getMessagePlayer($senderID, $gameID, $messageTypeID);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $senderID does not exist. Did you maybe mean $sender?
Loading history...
655
					if ($sender instanceof SmrPlayer) {
656
						$sender = $sender->getDisplayName();
657
					}
658
					$mail = setupMailer();
659
					$mail->Subject = 'Message Notification';
660
					$mail->setFrom('[email protected]', 'SMR Notifications');
661
					$bbifiedMessage = 'From: ' . $sender . ' Date: ' . date($receiverAccount->getShortDateFormat() . ' ' . $receiverAccount->getShortTimeFormat(), TIME) . "<br/>\r\n<br/>\r\n" . bbifyMessage($message, true);
662
					$mail->msgHTML($bbifiedMessage);
663
					$mail->AltBody = strip_tags($bbifiedMessage);
664
					$mail->addAddress($receiverAccount->getEmail(), $receiverAccount->getHofName());
665
					$mail->send();
666
					$receiverAccount->decreaseMessageNotifications($messageTypeID, 1);
667
					$receiverAccount->update();
668
				}
669
			break;
670
		}
671
672
		return $insertID;
673
	}
674
675
	public function sendMessageToBox($boxTypeID, $message) {
676
		// send him the message
677
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
678
	}
679
680
	public function sendGlobalMessage($message, $canBeIgnored = true) {
681
		if ($canBeIgnored) {
682
			if ($this->getAccount()->isMailBanned()) {
683
				create_error('You are currently banned from sending messages');
684
			}
685
		}
686
		$this->sendMessageToBox(BOX_GLOBALS, $message);
687
688
		// send to all online player
689
		$db = new SmrMySqlDatabase();
690
		$db->query('SELECT player_id
691
					FROM active_session
692
					JOIN player USING (game_id, account_id)
693
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(TIME - SmrSession::TIME_BEFORE_EXPIRY) . '
694
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
695
						AND ignore_globals = \'FALSE\'
696
						AND player_id != ' . $db->escapeNumber($this->getPlayerID()));
697
698
		while ($db->nextRecord()) {
699
			$this->sendMessage($db->getInt('player_id'), MSG_GLOBAL, $message, $canBeIgnored);
700
		}
701
		$this->sendMessage($this->getPlayerID(), MSG_GLOBAL, $message, $canBeIgnored, false);
702
	}
703
704
	public function sendMessage($receiverPlayerID, $messageTypeID, $message, $canBeIgnored = true, $unread = true, $expires = false, $senderDelete = false) {
705
		//get expire time
706
		if ($canBeIgnored) {
707
			if ($this->getAccount()->isMailBanned()) {
708
				create_error('You are currently banned from sending messages');
709
			}
710
			// Don't send messages to players ignoring us
711
			$this->db->query('SELECT 1 FROM message_blacklist WHERE player_id=' . $this->db->escapeNumber($receiverPlayerID) . ' AND blacklisted_player_id=' . $this->db->escapeNumber($this->getPlayerID()) . ' LIMIT 1');
712
			if ($this->db->nextRecord()) {
713
				return;
714
			}
715
		}
716
717
		$message = word_filter($message);
718
719
		// If expires not specified, use default based on message type
720
		if ($expires === false) {
721
			switch ($messageTypeID) {
722
				case MSG_GLOBAL: //We don't send globals to the box here or it gets done loads of times.
723
					$expires = 3600; // 1h
724
				break;
725
				case MSG_PLAYER:
726
					$expires = 86400 * 31;
727
				break;
728
				case MSG_PLANET:
729
					$expires = 86400 * 7;
730
				break;
731
				case MSG_SCOUT:
732
					$expires = 86400 * 3;
733
				break;
734
				case MSG_POLITICAL:
735
					$expires = 86400 * 31;
736
				break;
737
				case MSG_ALLIANCE:
738
					$expires = 86400 * 31;
739
				break;
740
				case MSG_ADMIN:
741
					$expires = 86400 * 365;
742
				break;
743
				case MSG_CASINO:
744
					$expires = 86400 * 31;
745
				break;
746
				default:
747
					$expires = 86400 * 7;
748
			}
749
			$expires += TIME;
750
		}
751
752
		// Do not put scout messages in the sender's sent box
753
		if ($messageTypeID == MSG_SCOUT) {
754
			$senderDelete = true;
755
		}
756
757
		// send him the message and return the message_id
758
		return self::doMessageSending($this->getPlayerID(), $receiverPlayerID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
759
	}
760
761
	public function sendMessageFromOpAnnounce($receiverPlayerID, $message, $expires = false) {
762
		// get expire time if not set
763
		if ($expires === false) {
764
			$expires = TIME + 86400 * 14;
765
		}
766
		self::doMessageSending(PLAYER_ID_OP_ANNOUNCE, $receiverPlayerID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
767
	}
768
769
	public function sendMessageFromAllianceCommand($receiverPlayerID, $message) {
770
		$expires = TIME + 86400 * 365;
771
		self::doMessageSending(PLAYER_ID_ALLIANCE_COMMAND, $receiverPlayerID, $this->getGameID(), MSG_PLAYER, $message, $expires);
772
	}
773
774
	public static function sendMessageFromPlanet($gameID, $receiverPlayerID, $message) {
775
		//get expire time
776
		$expires = TIME + 86400 * 31;
777
		// send him the message
778
		self::doMessageSending(PLAYER_ID_PLANET, $receiverPlayerID, $gameID, MSG_PLANET, $message, $expires);
779
	}
780
781
	public static function sendMessageFromPort($gameID, $receiverPlayerID, $message) {
782
		//get expire time
783
		$expires = TIME + 86400 * 31;
784
		// send him the message
785
		self::doMessageSending(PLAYER_ID_PORT, $receiverPlayerID, $gameID, MSG_PLAYER, $message, $expires);
786
	}
787
788
	public static function sendMessageFromFedClerk($gameID, $receiverPlayerID, $message) {
789
		$expires = TIME + 86400 * 365;
790
		self::doMessageSending(PLAYER_ID_FED_CLERK, $receiverPlayerID, $gameID, MSG_PLAYER, $message, $expires);
791
	}
792
793
	public static function sendMessageFromAdmin($gameID, $receiverPlayerID, $message, $expires = false) {
794
		//get expire time
795
		if ($expires === false) {
796
			$expires = TIME + 86400 * 365;
797
		}
798
		// send him the message
799
		self::doMessageSending(PLAYER_ID_ADMIN, $receiverPlayerID, $gameID, MSG_ADMIN, $message, $expires);
800
	}
801
802
	public static function sendMessageFromAllianceAmbassador($gameID, $receiverPlayerID, $message, $expires = false) {
803
		//get expire time
804
		if ($expires === false) {
805
			$expires = TIME + 86400 * 31;
806
		}
807
		// send him the message
808
		self::doMessageSending(PLAYER_ID_ALLIANCE_AMBASSADOR, $receiverPlayerID, $gameID, MSG_ALLIANCE, $message, $expires);
809
	}
810
811
	public static function sendMessageFromCasino($gameID, $receiverPlayerID, $message, $expires = false) {
812
		//get expire time
813
		if ($expires === false) {
814
			$expires = TIME + 86400 * 7;
815
		}
816
		// send him the message
817
		self::doMessageSending(PLAYER_ID_CASINO, $receiverPlayerID, $gameID, MSG_CASINO, $message, $expires);
818
	}
819
820
	public static function sendMessageFromRace($raceID, $gameID, $receiverPlayerID, $message, $expires = false) {
821
		//get expire time
822
		if ($expires === false) {
823
			$expires = TIME + 86400 * 5;
824
		}
825
		// send him the message
826
		self::doMessageSending(PLAYER_ID_GROUP_RACES + $raceID, $receiverPlayerID, $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();
0 ignored issues
show
Bug Best Practice introduced by
The property canFed does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
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(TIME));
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() && (TIME - $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->getPlayerID() == $this->getAlliance($forceUpdate)->getLeaderPlayerID();
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 (player_id,game_id,alliance_id,status) ' .
1307
				'VALUES (' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $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->getPlayerID(), 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->getLeaderPlayerID() != PLAYER_ID_NHL) {
1354
				$this->sendMessage($alliance->getLeaderPlayerID(), MSG_PLAYER, 'I left your alliance!', false);
1355
			}
1356
		}
1357
1358
		$this->setAllianceID(0);
1359
		$this->db->query('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1360
1361
		if (!$this->isAllianceLeader() && $allianceID != NHA_ID) { // Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1362
			$this->setAllianceJoinable(TIME + self::TIME_FOR_ALLIANCE_SWITCH);
1363
			$alliance->getLeader()->setAllianceJoinable(TIME + self::TIME_FOR_ALLIANCE_SWITCH); //We set the joinable time for leader here, that way a single player alliance won't cause a player to wait before switching.
1364
		}
1365
	}
1366
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 player doesn't exist.
1376
			try {
1377
				$this->sendMessage($alliance->getLeaderPlayerID(), MSG_PLAYER, 'I joined your alliance!', false);
1378
			} catch (PlayerNotFoundException $e) {
1379
				if ($alliance->getLeaderPlayerID() != PLAYER_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, player_id, role_id, alliance_id) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getPlayerID()) . ', ' . $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 $playerID to this player's alliance.
1407
	 */
1408
	public function sendAllianceInvitation(int $invitedPlayerID, 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($invitedPlayerID, MSG_PLAYER, $message, false, true, $expires, true);
1414
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $invitedPlayerID, $this->getPlayerID(), $messageID, $expires);
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 (player_id,game_id,race_id,relation) values (' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $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 250 personal relations to start.
1555
			foreach (Globals::getRaces() as $raceID => $raceInfo) {
1556
				$this->setRelations(250, $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(TIME);
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
				(player_id, game_id, course)
1625
				VALUES(' . $this->db->escapeNumber($this->getPlayerID()) . ', ' . $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 (player_id, game_id, sector_id, label, offset_top, offset_left)
1736
			VALUES (' . $this->db->escapeNumber($this->getPlayerID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($sectorID) . ',' . $this->db->escapeString($label, true) . ',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(TIME));
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 &shootPlayer(AbstractSmrPlayer $targetPlayer) {
1793
		return $this->getShip()->shootPlayer($targetPlayer);
1794
	}
1795
1796
	public function &shootForces(SmrForce $forces) {
1797
		return $this->getShip()->shootForces($forces);
1798
	}
1799
1800
	public function &shootPort(SmrPort $port) {
1801
		return $this->getShip()->shootPort($port);
1802
	}
1803
1804
	public function &shootPlanet(SmrPlanet $planet, $delayed) {
1805
		return $this->getShip()->shootPlanet($planet, $delayed);
1806
	}
1807
1808
	public function &shootPlayers(array $targetPlayers) {
1809
		return $this->getShip()->shootPlayers($targetPlayers);
1810
	}
1811
1812
	public function getMilitaryPayment() {
1813
		return $this->militaryPayment;
1814
	}
1815
1816
	public function hasMilitaryPayment() {
1817
		return $this->getMilitaryPayment() > 0;
1818
	}
1819
1820
	public function setMilitaryPayment($amount) {
1821
		if ($this->militaryPayment == $amount) {
1822
			return;
1823
		}
1824
		$this->militaryPayment = $amount;
1825
		$this->hasChanged = true;
1826
	}
1827
1828
	public function increaseMilitaryPayment($amount) {
1829
		if ($amount < 0) {
1830
			throw new Exception('Trying to increase negative military payment.');
1831
		}
1832
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1833
	}
1834
1835
	public function decreaseMilitaryPayment($amount) {
1836
		if ($amount < 0) {
1837
			throw new Exception('Trying to decrease negative military payment.');
1838
		}
1839
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1840
	}
1841
1842
	protected function getBountiesData() {
1843
		if (!isset($this->bounties)) {
1844
			$this->bounties = array();
1845
			$this->db->query('SELECT * FROM bounty WHERE ' . $this->SQL);
1846
			while ($this->db->nextRecord()) {
1847
				$this->bounties[$this->db->getInt('bounty_id')] = array(
1848
							'Amount' => $this->db->getInt('amount'),
1849
							'SmrCredits' => $this->db->getInt('smr_credits'),
1850
							'Type' => $this->db->getField('type'),
1851
							'Claimer' => $this->db->getInt('claimer_id'),
1852
							'Time' => $this->db->getInt('time'),
1853
							'ID' => $this->db->getInt('bounty_id'),
1854
							'New' => false);
1855
			}
1856
		}
1857
	}
1858
1859
	// Get bounties that can be claimed by this player
1860
	// Type must be 'HQ' or 'UG'
1861
	public function getClaimableBounties($type) {
1862
		$bounties = array();
1863
		$this->db->query('SELECT * FROM bounty WHERE claimer_player_id=' . $this->db->escapeNumber($this->getPlayerID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND type=' . $this->db->escapeString($type));
1864
		while ($this->db->nextRecord()) {
1865
			$bounties[] = array(
1866
				'player' => SmrPlayer::getPlayer($this->db->getInt('player_id'), $this->getGameID()),
1867
				'bounty_id' => $this->db->getInt('bounty_id'),
1868
				'credits' => $this->db->getInt('amount'),
1869
				'smr_credits' => $this->db->getInt('smr_credits'),
1870
			);
1871
		}
1872
		return $bounties;
1873
	}
1874
1875
	public function getBounties() : array {
1876
		$this->getBountiesData();
1877
		return $this->bounties;
1878
	}
1879
1880
	public function hasBounties() : bool {
1881
		return count($this->getBounties()) > 0;
1882
	}
1883
1884
	protected function getBounty(int $bountyID) : array {
1885
		if (!$this->hasBounty($bountyID)) {
1886
			throw new Exception('BountyID does not exist: ' . $bountyID);
1887
		}
1888
		return $this->bounties[$bountyID];
1889
	}
1890
1891
	public function hasBounty(int $bountyID) : bool {
1892
		$bounties = $this->getBounties();
1893
		return isset($bounties[$bountyID]);
1894
	}
1895
1896
	protected function getBountyAmount(int $bountyID) : int {
1897
		$bounty = $this->getBounty($bountyID);
1898
		return $bounty['Amount'];
1899
	}
1900
1901
	protected function createBounty(string $type) : array {
1902
		$bounty = array('Amount' => 0,
1903
						'SmrCredits' => 0,
1904
						'Type' => $type,
1905
						'Claimer' => 0,
1906
						'Time' => TIME,
1907
						'ID' => $this->getNextBountyID(),
1908
						'New' => true);
1909
		$this->setBounty($bounty);
1910
		return $bounty;
1911
	}
1912
1913
	protected function getNextBountyID() : int {
1914
		$keys = array_keys($this->getBounties());
1915
		if (count($keys) > 0) {
1916
			return max($keys) + 1;
1917
		} else {
1918
			return 0;
1919
		}
1920
	}
1921
1922
	protected function setBounty(array $bounty) : void {
1923
		$this->bounties[$bounty['ID']] = $bounty;
1924
		$this->hasBountyChanged[$bounty['ID']] = true;
1925
	}
1926
1927
	protected function setBountyAmount(int $bountyID, int $amount) : void {
1928
		$bounty = $this->getBounty($bountyID);
1929
		$bounty['Amount'] = $amount;
1930
		$this->setBounty($bounty);
1931
	}
1932
1933
	public function getCurrentBounty(string $type) : array {
1934
		$bounties = $this->getBounties();
1935
		foreach ($bounties as $bounty) {
1936
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1937
				return $bounty;
1938
			}
1939
		}
1940
		return $this->createBounty($type);
1941
	}
1942
1943
	public function hasCurrentBounty(string $type) : bool {
1944
		$bounties = $this->getBounties();
1945
		foreach ($bounties as $bounty) {
1946
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1947
				return true;
1948
			}
1949
		}
1950
		return false;
1951
	}
1952
1953
	protected function getCurrentBountyAmount(string $type) : int {
1954
		$bounty = $this->getCurrentBounty($type);
1955
		return $bounty['Amount'];
1956
	}
1957
1958
	protected function setCurrentBountyAmount(string $type, int $amount) : void {
1959
		$bounty = $this->getCurrentBounty($type);
1960
		if ($bounty['Amount'] == $amount) {
1961
			return;
1962
		}
1963
		$bounty['Amount'] = $amount;
1964
		$this->setBounty($bounty);
1965
	}
1966
1967
	public function increaseCurrentBountyAmount(string $type, int $amount) : void {
1968
		if ($amount < 0) {
1969
			throw new Exception('Trying to increase negative current bounty.');
1970
		}
1971
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) + $amount);
1972
	}
1973
1974
	public function decreaseCurrentBountyAmount(string $type, int $amount) : void {
1975
		if ($amount < 0) {
1976
			throw new Exception('Trying to decrease negative current bounty.');
1977
		}
1978
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) - $amount);
1979
	}
1980
1981
	protected function getCurrentBountySmrCredits(string $type) : int {
1982
		$bounty = $this->getCurrentBounty($type);
1983
		return $bounty['SmrCredits'];
1984
	}
1985
1986
	protected function setCurrentBountySmrCredits(string $type, int $credits) : void {
1987
		$bounty = $this->getCurrentBounty($type);
1988
		if ($bounty['SmrCredits'] == $credits) {
1989
			return;
1990
		}
1991
		$bounty['SmrCredits'] = $credits;
1992
		$this->setBounty($bounty);
1993
	}
1994
1995
	public function increaseCurrentBountySmrCredits(string $type, int $credits) : void {
1996
		if ($credits < 0) {
1997
			throw new Exception('Trying to increase negative current bounty.');
1998
		}
1999
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) + $credits);
2000
	}
2001
2002
	public function decreaseCurrentBountySmrCredits(string $type, int $credits) : void {
2003
		if ($credits < 0) {
2004
			throw new Exception('Trying to decrease negative current bounty.');
2005
		}
2006
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) - $credits);
2007
	}
2008
2009
	public function setBountiesClaimable(AbstractSmrPlayer $claimer) : void {
2010
		foreach ($this->getBounties() as $bounty) {
2011
			if ($bounty['ClaimerPlayerID'] == 0) {
2012
				$bounty['ClaimerPlayerID'] = $claimer->getPlayerID();
2013
				$this->setBounty($bounty);
2014
			}
2015
		}
2016
	}
2017
2018
	protected function getHOFData() {
2019
		if (!isset($this->HOF)) {
2020
			//Get Player HOF
2021
			$this->db->query('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2022
			$this->HOF = array();
2023
			while ($this->db->nextRecord()) {
2024
				$hof =& $this->HOF;
2025
				$typeList = explode(':', $this->db->getField('type'));
2026
				foreach ($typeList as $type) {
2027
					if (!isset($hof[$type])) {
2028
						$hof[$type] = array();
2029
					}
2030
					$hof =& $hof[$type];
2031
				}
2032
				$hof = $this->db->getFloat('amount');
2033
			}
2034
			self::getHOFVis();
2035
		}
2036
	}
2037
2038
	public static function getHOFVis() {
2039
		if (!isset(self::$HOFVis)) {
2040
			//Get Player HOF Vis
2041
			$db = new SmrMySqlDatabase();
2042
			$db->query('SELECT type,visibility FROM hof_visibility');
2043
			self::$HOFVis = array();
2044
			while ($db->nextRecord()) {
2045
				self::$HOFVis[$db->getField('type')] = $db->getField('visibility');
2046
			}
2047
		}
2048
	}
2049
2050
	public function getHOF(array $typeList = null) {
2051
		$this->getHOFData();
2052
		if ($typeList == null) {
2053
			return $this->HOF;
2054
		}
2055
		$hof = $this->HOF;
2056
		foreach ($typeList as $type) {
2057
			if (!isset($hof[$type])) {
2058
				return 0;
2059
			}
2060
			$hof = $hof[$type];
2061
		}
2062
		return $hof;
2063
	}
2064
2065
	public function increaseHOF($amount, array $typeList, $visibility) {
2066
		if ($amount < 0) {
2067
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2068
		}
2069
		if ($amount == 0) {
2070
			return;
2071
		}
2072
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2073
	}
2074
2075
	public function decreaseHOF($amount, array $typeList, $visibility) {
2076
		if ($amount < 0) {
2077
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2078
		}
2079
		if ($amount == 0) {
2080
			return;
2081
		}
2082
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2083
	}
2084
2085
	public function setHOF($amount, array $typeList, $visibility) {
2086
		if (is_array($this->getHOF($typeList))) {
2087
			throw new Exception('Trying to overwrite a HOF type: ' . implode(':', $typeList));
2088
		}
2089
		if ($this->isNPC()) {
2090
			// Don't store HOF for NPCs.
2091
			return;
2092
		}
2093
		if ($this->getHOF($typeList) == $amount) {
2094
			return;
2095
		}
2096
		if ($amount < 0) {
2097
			$amount = 0;
2098
		}
2099
		$this->getHOF();
2100
2101
		$hofType = implode(':', $typeList);
2102
		if (!isset(self::$HOFVis[$hofType])) {
2103
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2104
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2105
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2106
		}
2107
		self::$HOFVis[$hofType] = $visibility;
2108
2109
		$hof =& $this->HOF;
2110
		$hofChanged =& $this->hasHOFChanged;
2111
		$new = false;
2112
		foreach ($typeList as $type) {
2113
			if (!isset($hofChanged[$type])) {
2114
				$hofChanged[$type] = array();
2115
			}
2116
			if (!isset($hof[$type])) {
2117
				$hof[$type] = array();
2118
				$new = true;
2119
			}
2120
			$hof =& $hof[$type];
2121
			$hofChanged =& $hofChanged[$type];
2122
		}
2123
		if ($hofChanged == null) {
2124
			$hofChanged = self::HOF_CHANGED;
2125
			if ($new) {
2126
				$hofChanged = self::HOF_NEW;
2127
			}
2128
		}
2129
		$hof = $amount;
2130
	}
2131
2132
	public function getExperienceRank() {
2133
		return $this->computeRanking('experience', $this->getExperience());
2134
	}
2135
2136
	public function getKillsRank() {
2137
		return $this->computeRanking('kills', $this->getKills());
2138
	}
2139
2140
	public function getDeathsRank() {
2141
		return $this->computeRanking('deaths', $this->getDeaths());
2142
	}
2143
2144
	public function getAssistsRank() {
2145
		return $this->computeRanking('assists', $this->getAssists());
2146
	}
2147
2148
	private function computeRanking($dbField, $playerAmount) {
2149
		$this->db->query('SELECT count(*) FROM player
2150
			WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . '
2151
			AND (
2152
				'.$dbField . ' > ' . $this->db->escapeNumber($playerAmount) . '
2153
				OR (
2154
					'.$dbField . ' = ' . $this->db->escapeNumber($playerAmount) . '
2155
					AND player_name <= ' . $this->db->escapeString($this->getPlayerName()) . '
2156
				)
2157
			)');
2158
		$this->db->nextRecord();
2159
		$rank = $this->db->getInt('count(*)');
2160
		return $rank;
2161
	}
2162
2163
	public function killPlayer($sectorID) {
2164
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2165
		//msg taken care of in trader_att_proc.php
2166
		// forget plotted course
2167
		$this->deletePlottedCourse();
2168
2169
		$sector->diedHere($this);
2170
2171
		// if we are in an alliance we increase their deaths
2172
		if ($this->hasAlliance()) {
2173
			$this->db->query('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2174
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()) . ' LIMIT 1');
2175
		}
2176
2177
		// record death stat
2178
		$this->increaseHOF(1, array('Dying', 'Deaths'), HOF_PUBLIC);
2179
		//record cost of ship lost
2180
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2181
		// reset turns since last death
2182
		$this->setHOF(0, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2183
2184
		// 1/4 of ship value -> insurance
2185
		$newCredits = IRound($this->getShip()->getCost() / 4);
2186
		if ($newCredits < 100000) {
2187
			$newCredits = 100000;
2188
		}
2189
		$this->setCredits($newCredits);
2190
2191
		$this->setSectorID($this->getHome());
2192
		$this->increaseDeaths(1);
2193
		$this->setLandedOnPlanet(false);
2194
		$this->setDead(true);
2195
		$this->setNewbieWarning(true);
2196
		$this->getShip()->getPod($this->hasNewbieStatus());
2197
		$this->setNewbieTurns(100);
2198
	}
2199
2200
	public function &killPlayerByPlayer(AbstractSmrPlayer $killer) {
2201
		$return = array();
2202
		$msg = $this->getBBLink();
2203
2204
		if ($this->hasCustomShipName()) {
2205
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2206
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2207
		}
2208
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2209
		if ($killer->hasCustomShipName()) {
2210
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2211
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2212
		}
2213
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2214
		$this->getSector()->increaseBattles(1);
2215
		$this->db->query('INSERT INTO news (game_id,time,news_message,type,killer_player_id,killer_alliance,dead_player_id,dead_alliance) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber(TIME) . ',' . $this->db->escapeString($msg, true) . ',\'regular\',' . $this->db->escapeNumber($killer->getPlayerID()) . ',' . $this->db->escapeNumber($killer->getAllianceID()) . ',' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2216
2217
		self::sendMessageFromFedClerk($this->getGameID(), $this->getPlayerID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2218
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getPlayerID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2219
2220
		// Dead player loses between 5% and 25% experience
2221
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2222
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2223
		$this->decreaseExperience($return['DeadExp']);
2224
2225
		// Killer gains 50% of the lost exp
2226
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
2227
		$killer->increaseExperience($return['KillerExp']);
2228
2229
		$return['KillerCredits'] = $this->getCredits();
2230
		$killer->increaseCredits($return['KillerCredits']);
2231
2232
		// The killer may change alignment
2233
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2234
		$relation = $relations[$killer->getRaceID()];
2235
2236
		$alignChangePerRelation = 0.1;
2237
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2238
			$alignChangePerRelation = 0.04;
2239
		}
2240
2241
		$return['KillerAlign'] = -$relation * $alignChangePerRelation; //Lose relations when killing a peaceful race
2242
		if ($return['KillerAlign'] > 0) {
2243
			$killer->increaseAlignment($return['KillerAlign']);
2244
		} else {
2245
			$killer->decreaseAlignment(-$return['KillerAlign']);
2246
		}
2247
		// War setting gives them military pay
2248
		if ($relation <= RELATIONS_WAR) {
2249
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2250
		}
2251
2252
		//check for federal bounty being offered for current port raiders;
2253
		$this->db->query('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(TIME - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2254
		$query = 'SELECT 1
2255
					FROM player_attacks_port
2256
					JOIN port USING(game_id, sector_id)
2257
					JOIN player USING(game_id, player_id)
2258
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2259
		$this->db->query($query);
2260
		if ($this->db->nextRecord()) {
2261
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2262
			$this->increaseCurrentBountyAmount('HQ', $bounty);
2263
		}
2264
2265
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2266
		$this->setBountiesClaimable($killer);
2267
2268
		// If the alignment difference is greater than 200 then a bounty may be set
2269
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2270
		$return['BountyGained'] = array(
2271
			'Type' => 'None',
2272
			'Amount' => 0
2273
		);
2274
		if ($alignmentDiff >= 200) {
2275
			// If the podded players alignment makes them deputy or member then set bounty
2276
			if ($this->getAlignment() >= 100) {
2277
				$return['BountyGained']['Type'] = 'HQ';
2278
			} elseif ($this->getAlignment() <= 100) {
2279
				$return['BountyGained']['Type'] = 'UG';
2280
			}
2281
2282
			if ($return['BountyGained']['Type'] != 'None') {
2283
				$return['BountyGained']['Amount'] = IFloor(pow($alignmentDiff, 2.56));
2284
				$killer->increaseCurrentBountyAmount($return['BountyGained']['Type'], $return['BountyGained']['Amount']);
2285
			}
2286
		}
2287
2288
		if ($this->isNPC()) {
2289
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'NPC', 'Experience', 'Gained'), HOF_PUBLIC);
2290
			$killer->increaseHOF($this->getExperience(), array('Killing', 'NPC', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2291
2292
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By NPCs Killed'), HOF_PUBLIC);
2293
2294
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2295
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Gain'), HOF_PUBLIC);
2296
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'NPC', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2297
2298
			if ($return['KillerAlign'] > 0) {
2299
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Gain'), HOF_PUBLIC);
2300
			} else {
2301
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Loss'), HOF_PUBLIC);
2302
			}
2303
2304
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'NPC', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2305
2306
			$killer->increaseHOF(1, array('Killing', 'NPC Kills'), HOF_PUBLIC);
2307
		} else {
2308
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'Experience', 'Gained'), HOF_PUBLIC);
2309
			$killer->increaseHOF($this->getExperience(), array('Killing', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2310
2311
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By Traders Killed'), HOF_PUBLIC);
2312
2313
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2314
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Gain'), HOF_PUBLIC);
2315
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2316
2317
			if ($return['KillerAlign'] > 0) {
2318
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'Alignment', 'Gain'), HOF_PUBLIC);
2319
			} else {
2320
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'Alignment', 'Loss'), HOF_PUBLIC);
2321
			}
2322
2323
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2324
2325
			if ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2326
				$killer->increaseHOF(1, array('Killing', 'Newbie Kills'), HOF_PUBLIC);
2327
			} else {
2328
				$killer->increaseKills(1);
2329
				$killer->increaseHOF(1, array('Killing', 'Kills'), HOF_PUBLIC);
2330
2331
				if ($killer->hasAlliance()) {
2332
					$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');
2333
				}
2334
2335
				// alliance vs. alliance stats
2336
				$this->incrementAllianceVsDeaths($killer->getAllianceID());
2337
			}
2338
		}
2339
2340
		$this->increaseHOF($return['BountyGained']['Amount'], array('Dying', 'Players', 'Money', 'Bounty Gained By Killer'), HOF_PUBLIC);
2341
		$this->increaseHOF($return['KillerExp'], array('Dying', 'Players', 'Experience', 'Gained By Killer'), HOF_PUBLIC);
2342
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2343
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Players', 'Experience', 'Lost'), HOF_PUBLIC);
2344
		$this->increaseHOF($return['KillerCredits'], array('Dying', 'Players', 'Money Lost'), HOF_PUBLIC);
2345
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Players', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2346
		$this->increaseHOF(1, array('Dying', 'Players', 'Deaths'), HOF_PUBLIC);
2347
2348
		$this->killPlayer($this->getSectorID());
2349
		return $return;
2350
	}
2351
2352
	public function &killPlayerByForces(SmrForce $forces) {
2353
		$return = array();
2354
		$owner = $forces->getOwner();
2355
		// send a message to the person who died
2356
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getPlayerID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2357
		self::sendMessageFromFedClerk($this->getGameID(), $this->getPlayerID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2358
2359
		$news_message = $this->getBBLink();
2360
		if ($this->hasCustomShipName()) {
2361
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2362
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2363
		}
2364
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2365
		// insert the news entry
2366
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_player_id,killer_alliance,dead_player_id,dead_alliance)
2367
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($owner->getPlayerID()) . ',' . $this->db->escapeNumber($owner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2368
2369
		// Player loses 15% experience
2370
		$expLossPercentage = .15;
2371
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
2372
		$this->decreaseExperience($return['DeadExp']);
2373
2374
		$return['LostCredits'] = $this->getCredits();
2375
2376
		// alliance vs. alliance stats
2377
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2378
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2379
2380
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2381
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Forces', 'Experience Lost'), HOF_PUBLIC);
2382
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Forces', 'Money Lost'), HOF_PUBLIC);
2383
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Forces', 'Cost Of Ships Lost'), HOF_PUBLIC);
2384
		$this->increaseHOF(1, array('Dying', 'Forces', 'Deaths'), HOF_PUBLIC);
2385
2386
		$this->killPlayer($forces->getSectorID());
2387
		return $return;
2388
	}
2389
2390
	public function &killPlayerByPort(SmrPort $port) {
2391
		$return = array();
2392
		// send a message to the person who died
2393
		self::sendMessageFromFedClerk($this->getGameID(), $this->getPlayerID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2394
2395
		$news_message = $this->getBBLink();
2396
		if ($this->hasCustomShipName()) {
2397
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2398
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2399
		}
2400
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2401
		// insert the news entry
2402
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_player_id,dead_player_id,dead_alliance)
2403
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber(PLAYER_ID_PORT) . ',' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2404
2405
		// Player loses between 15% and 20% experience
2406
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2407
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2408
		$this->decreaseExperience($return['DeadExp']);
2409
2410
		$return['LostCredits'] = $this->getCredits();
2411
2412
		// alliance vs. alliance stats
2413
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2414
2415
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2416
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Ports', 'Experience Lost'), HOF_PUBLIC);
2417
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Ports', 'Money Lost'), HOF_PUBLIC);
2418
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Ports', 'Cost Of Ships Lost'), HOF_PUBLIC);
2419
		$this->increaseHOF(1, array('Dying', 'Ports', 'Deaths'), HOF_PUBLIC);
2420
2421
		$this->killPlayer($port->getSectorID());
2422
		return $return;
2423
	}
2424
2425
	public function &killPlayerByPlanet(SmrPlanet $planet) {
2426
		$return = array();
2427
		// send a message to the person who died
2428
		$planetOwner = $planet->getOwner();
2429
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getPlayerID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2430
		self::sendMessageFromFedClerk($this->getGameID(), $this->getPlayerID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2431
2432
		$news_message = $this->getBBLink();
2433
		if ($this->hasCustomShipName()) {
2434
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2435
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2436
		}
2437
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2438
		// insert the news entry
2439
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_player_id,killer_alliance,dead_player_id,dead_alliance)
2440
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($planetOwner->getPlayerID()) . ',' . $this->db->escapeNumber($planetOwner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2441
2442
		// Player loses between 15% and 20% experience
2443
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2444
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2445
		$this->decreaseExperience($return['DeadExp']);
2446
2447
		$return['LostCredits'] = $this->getCredits();
2448
2449
		// alliance vs. alliance stats
2450
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2451
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2452
2453
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2454
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Planets', 'Experience Lost'), HOF_PUBLIC);
2455
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Planets', 'Money Lost'), HOF_PUBLIC);
2456
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Planets', 'Cost Of Ships Lost'), HOF_PUBLIC);
2457
		$this->increaseHOF(1, array('Dying', 'Planets', 'Deaths'), HOF_PUBLIC);
2458
2459
		$this->killPlayer($planet->getSectorID());
2460
		return $return;
2461
	}
2462
2463
	public function incrementAllianceVsKills($otherID) {
2464
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2465
		$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');
2466
	}
2467
2468
	public function incrementAllianceVsDeaths($otherID) {
2469
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2470
		$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');
2471
	}
2472
2473
	public function getTurnsLevel() {
2474
		if (!$this->hasTurns()) {
2475
			return 'NONE';
2476
		}
2477
		if ($this->getTurns() <= 25) {
2478
			return 'LOW';
2479
		}
2480
		if ($this->getTurns() <= 75) {
2481
			return 'MEDIUM';
2482
		}
2483
		return 'HIGH';
2484
	}
2485
2486
	/**
2487
	 * Returns the CSS class color to use when displaying the player's turns
2488
	 */
2489
	public function getTurnsColor() {
2490
		switch ($this->getTurnsLevel()) {
2491
			case 'NONE':
2492
			case 'LOW':
2493
				return 'red';
2494
			case 'MEDIUM':
2495
				return 'yellow';
2496
			default:
2497
				return 'green';
2498
		}
2499
	}
2500
2501
	public function getTurns() {
2502
		return $this->turns;
2503
	}
2504
2505
	public function hasTurns() {
2506
		return $this->turns > 0;
2507
	}
2508
2509
	public function getMaxTurns() {
2510
		return $this->getGame()->getMaxTurns();
2511
	}
2512
2513
	public function setTurns($turns) {
2514
		if ($this->turns == $turns) {
2515
			return;
2516
		}
2517
		// Make sure turns are in range [0, MaxTurns]
2518
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2519
		$this->hasChanged = true;
2520
	}
2521
2522
	public function takeTurns($take, $takeNewbie = 0) {
2523
		if ($take < 0 || $takeNewbie < 0) {
2524
			throw new Exception('Trying to take negative turns.');
2525
		}
2526
		$take = ICeil($take);
2527
		// Only take up to as many newbie turns as we have remaining
2528
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2529
2530
		$this->setTurns($this->getTurns() - $take);
2531
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2532
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2533
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Total'), HOF_ALLIANCE);
2534
		$this->increaseHOF($takeNewbie, array('Movement', 'Turns Used', 'Newbie'), HOF_ALLIANCE);
2535
2536
		// Player has taken an action
2537
		$this->setLastActive(TIME);
2538
		$this->updateLastCPLAction();
2539
	}
2540
2541
	public function giveTurns(int $give, $giveNewbie = 0) {
2542
		if ($give < 0 || $giveNewbie < 0) {
2543
			throw new Exception('Trying to give negative turns.');
2544
		}
2545
		$this->setTurns($this->getTurns() + $give);
2546
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2547
	}
2548
2549
	/**
2550
	 * Calculate the time in seconds between the given time and when the
2551
	 * player will be at max turns.
2552
	 */
2553
	public function getTimeUntilMaxTurns($time, $forceUpdate = false) {
2554
		$timeDiff = $time - $this->getLastTurnUpdate();
2555
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2556
		$ship = $this->getShip($forceUpdate);
2557
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
2558
		// If already at max turns, return 0
2559
		return max(0, $maxTurnsTime);
2560
	}
2561
2562
	/**
2563
	 * Grant the player their starting turns.
2564
	 */
2565
	public function giveStartingTurns() {
2566
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
2567
		$this->giveTurns($startTurns);
2568
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2569
	}
2570
2571
	// Turns only update when player is active.
2572
	// Calculate turns gained between given time and the last turn update
2573
	public function getTurnsGained($time, $forceUpdate = false) : int {
2574
		$timeDiff = $time - $this->getLastTurnUpdate();
2575
		$ship = $this->getShip($forceUpdate);
2576
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
2577
		return $extraTurns;
2578
	}
2579
2580
	public function updateTurns() {
2581
		// is account validated?
2582
		if (!$this->getAccount()->isValidated()) {
2583
			return;
2584
		}
2585
2586
		// how many turns would he get right now?
2587
		$extraTurns = $this->getTurnsGained(TIME);
2588
2589
		// do we have at least one turn to give?
2590
		if ($extraTurns > 0) {
2591
			// recalc the time to avoid rounding errors
2592
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
2593
			$this->setLastTurnUpdate($newLastTurnUpdate);
2594
			$this->giveTurns($extraTurns);
2595
		}
2596
	}
2597
2598
	public function getLastTurnUpdate() {
2599
		return $this->lastTurnUpdate;
2600
	}
2601
2602
	public function setLastTurnUpdate($time) {
2603
		if ($this->lastTurnUpdate == $time) {
2604
			return;
2605
		}
2606
		$this->lastTurnUpdate = $time;
2607
		$this->hasChanged = true;
2608
	}
2609
2610
	public function getLastActive() {
2611
		return $this->lastActive;
2612
	}
2613
2614
	public function setLastActive($lastActive) {
2615
		if ($this->lastActive == $lastActive) {
2616
			return;
2617
		}
2618
		$this->lastActive = $lastActive;
2619
		$this->hasChanged = true;
2620
	}
2621
2622
	public function getLastCPLAction() {
2623
		return $this->lastCPLAction;
2624
	}
2625
2626
	public function setLastCPLAction($time) {
2627
		if ($this->lastCPLAction == $time) {
2628
			return;
2629
		}
2630
		$this->lastCPLAction = $time;
2631
		$this->hasChanged = true;
2632
	}
2633
2634
	public function updateLastCPLAction() {
2635
		$this->setLastCPLAction(TIME);
2636
	}
2637
2638
	public function setNewbieWarning($bool) {
2639
		if ($this->newbieWarning == $bool) {
2640
			return;
2641
		}
2642
		$this->newbieWarning = $bool;
2643
		$this->hasChanged = true;
2644
	}
2645
2646
	public function getNewbieWarning() {
2647
		return $this->newbieWarning;
2648
	}
2649
2650
	public function isDisplayMissions() {
2651
		return $this->displayMissions;
2652
	}
2653
2654
	public function setDisplayMissions($bool) {
2655
		if ($this->displayMissions == $bool) {
2656
			return;
2657
		}
2658
		$this->displayMissions = $bool;
2659
		$this->hasChanged = true;
2660
	}
2661
2662
	public function getMissions() {
2663
		if (!isset($this->missions)) {
2664
			$this->db->query('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2665
			$this->missions = array();
2666
			while ($this->db->nextRecord()) {
2667
				$missionID = $this->db->getInt('mission_id');
2668
				$this->missions[$missionID] = array(
2669
					'On Step' => $this->db->getInt('on_step'),
2670
					'Progress' => $this->db->getInt('progress'),
2671
					'Unread' => $this->db->getBoolean('unread'),
2672
					'Expires' => $this->db->getInt('step_fails'),
2673
					'Sector' => $this->db->getInt('mission_sector'),
2674
					'Starting Sector' => $this->db->getInt('starting_sector')
2675
				);
2676
				$this->rebuildMission($missionID);
2677
			}
2678
		}
2679
		return $this->missions;
2680
	}
2681
2682
	public function getActiveMissions() {
2683
		$missions = $this->getMissions();
2684
		foreach ($missions as $missionID => $mission) {
2685
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2686
				unset($missions[$missionID]);
2687
			}
2688
		}
2689
		return $missions;
2690
	}
2691
2692
	protected function getMission($missionID) {
2693
		$missions = $this->getMissions();
2694
		if (isset($missions[$missionID])) {
2695
			return $missions[$missionID];
2696
		}
2697
		return false;
2698
	}
2699
2700
	protected function hasMission($missionID) {
2701
		return $this->getMission($missionID) !== false;
2702
	}
2703
2704
	protected function updateMission($missionID) {
2705
		$this->getMissions();
2706
		if (isset($this->missions[$missionID])) {
2707
			$mission = $this->missions[$missionID];
2708
			$this->db->query('
2709
				UPDATE player_has_mission
2710
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2711
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2712
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2713
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2714
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2715
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2716
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1'
2717
			);
2718
			return true;
2719
		}
2720
		return false;
2721
	}
2722
2723
	private function setupMissionStep($missionID) {
2724
		$mission =& $this->missions[$missionID];
2725
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2726
			// Nothing to do if this mission is already completed
2727
			return;
2728
		}
2729
		$step = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2730
		if (isset($step['PickSector'])) {
2731
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2732
			if ($realX === false) {
2733
				throw new Exception('Invalid PickSector definition in mission: ' . $missionID);
2734
			}
2735
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2736
			if ($path === false) {
2737
				// Abandon the mission if it cannot be completed due to a
2738
				// sector that does not exist or cannot be reached.
2739
				// (Probably shouldn't bestow this mission in the first place)
2740
				$this->deleteMission($missionID);
2741
				create_error('Cannot find a path to the destination!');
2742
			}
2743
			$mission['Sector'] = $path->getEndSectorID();
2744
		}
2745
	}
2746
2747
	/**
2748
	 * Declining a mission will permanently hide it from the player
2749
	 * by adding it in its completed state.
2750
	 */
2751
	public function declineMission($missionID) {
2752
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2753
		$this->addMission($missionID, $finishedStep);
2754
	}
2755
2756
	public function addMission($missionID, $step = 0) {
2757
		$this->getMissions();
2758
2759
		if (isset($this->missions[$missionID])) {
2760
			return;
2761
		}
2762
		$sector = 0;
2763
2764
		$mission = array(
2765
			'On Step' => $step,
2766
			'Progress' => 0,
2767
			'Unread' => true,
2768
			'Expires' => (TIME + 86400),
2769
			'Sector' => $sector,
2770
			'Starting Sector' => $this->getSectorID()
2771
		);
2772
2773
		$this->missions[$missionID] =& $mission;
2774
		$this->setupMissionStep($missionID);
2775
		$this->rebuildMission($missionID);
2776
2777
		$this->db->query('
2778
			REPLACE INTO player_has_mission (game_id,player_id,mission_id,on_step,progress,unread,starting_sector,mission_sector,step_fails)
2779
			VALUES ('.$this->db->escapeNumber($this->gameID) . ',' . $this->db->escapeNumber($this->playerID) . ',' . $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']) . ')'
2780
		);
2781
	}
2782
2783
	private function rebuildMission($missionID) {
2784
		$mission = $this->missions[$missionID];
2785
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2786
2787
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2788
			// If we have completed this mission just use false to indicate no current task.
2789
			$currentStep = false;
2790
		} else {
2791
			$data = ['player' => $this, 'mission' => $mission];
2792
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2793
			array_walk_recursive($currentStep, 'replaceMissionTemplate', $data);
2794
		}
2795
		$this->missions[$missionID]['Task'] = $currentStep;
2796
	}
2797
2798
	public function deleteMission($missionID) {
2799
		$this->getMissions();
2800
		if (isset($this->missions[$missionID])) {
2801
			unset($this->missions[$missionID]);
2802
			$this->db->query('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1');
2803
			return true;
2804
		}
2805
		return false;
2806
	}
2807
2808
	public function markMissionsRead() {
2809
		$this->getMissions();
2810
		$unreadMissions = array();
2811
		foreach ($this->missions as $missionID => &$mission) {
2812
			if ($mission['Unread']) {
2813
				$unreadMissions[] = $missionID;
2814
				$mission['Unread'] = false;
2815
				$this->updateMission($missionID);
2816
			}
2817
		}
2818
		return $unreadMissions;
2819
	}
2820
2821
	public function claimMissionReward($missionID) {
2822
		$this->getMissions();
2823
		$mission =& $this->missions[$missionID];
2824
		if ($mission === false) {
2825
			throw new Exception('Unknown mission: ' . $missionID);
2826
		}
2827
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2828
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2829
		}
2830
		$mission['On Step']++;
2831
		$mission['Unread'] = true;
2832
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2833
			switch ($rewardItem) {
2834
				case 'Credits':
2835
					$this->increaseCredits($amount);
2836
				break;
2837
				case 'Experience':
2838
					$this->increaseExperience($amount);
2839
				break;
2840
			}
2841
		}
2842
		$rewardText = $mission['Task']['Rewards']['Text'];
2843
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2844
			// If we haven't finished the mission yet then 
2845
			$this->setupMissionStep($missionID);
2846
		}
2847
		$this->rebuildMission($missionID);
2848
		$this->updateMission($missionID);
2849
		return $rewardText;
2850
	}
2851
2852
	public function getAvailableMissions() {
2853
		$availableMissions = array();
2854
		foreach (MISSIONS as $missionID => $mission) {
2855
			if ($this->hasMission($missionID)) {
2856
				continue;
2857
			}
2858
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2859
			if ($realX === false) {
2860
				throw new Exception('Invalid HasX definition in mission: ' . $missionID);
2861
			}
2862
			if ($this->getSector()->hasX($realX)) {
2863
				$availableMissions[$missionID] = $mission;
2864
			}
2865
		}
2866
		return $availableMissions;
2867
	}
2868
2869
	/**
2870
	 * Log a player action in the current sector to the admin log console.
2871
	 */
2872
	public function log(int $log_type_id, string $msg) : void {
2873
		$this->getAccount()->log($log_type_id, $msg, $this->getSectorID());
2874
	}
2875
2876
	public function actionTaken($actionID, array $values) {
2877
		if (!in_array($actionID, MISSION_ACTIONS)) {
2878
			throw new Exception('Unknown action: ' . $actionID);
2879
		}
2880
// TODO: Reenable this once tested.		if($this->getAccount()->isLoggingEnabled())
2881
			switch ($actionID) {
2882
				case 'WalkSector':
2883
					$this->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID());
2884
				break;
2885
				case 'JoinAlliance':
2886
					$this->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName());
2887
				break;
2888
				case 'LeaveAlliance':
2889
					$this->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName());
2890
				break;
2891
				case 'DisbandAlliance':
2892
					$this->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName());
2893
				break;
2894
				case 'KickPlayer':
2895
					$this->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName());
2896
				break;
2897
				case 'PlayerKicked':
2898
					$this->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')');
2899
				break;
2900
2901
			}
2902
		$this->getMissions();
2903
		foreach ($this->missions as $missionID => &$mission) {
2904
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
2905
				$requirements = $mission['Task']['Detail'];
2906
				if (checkMissionRequirements($values, $requirements) === true) {
2907
					$mission['On Step']++;
2908
					$mission['Unread'] = true;
2909
					$this->setupMissionStep($missionID);
2910
					$this->rebuildMission($missionID);
2911
					$this->updateMission($missionID);
2912
				}
2913
			}
2914
		}
2915
	}
2916
2917
	public function canSeeAny(array $otherPlayerArray) {
2918
		foreach ($otherPlayerArray as $otherPlayer) {
2919
			if ($this->canSee($otherPlayer)) {
2920
				return true;
2921
			}
2922
		}
2923
		return false;
2924
	}
2925
2926
	public function canSee(AbstractSmrPlayer $otherPlayer) {
2927
		if (!$otherPlayer->getShip()->isCloaked()) {
2928
			return true;
2929
		}
2930
		if ($this->sameAlliance($otherPlayer)) {
2931
			return true;
2932
		}
2933
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
2934
			return true;
2935
		}
2936
		return false;
2937
	}
2938
2939
	public function equals(AbstractSmrPlayer $otherPlayer = null) {
2940
		return $otherPlayer !== null && $this->getPlayerID() == $otherPlayer->getPlayerID() && $this->getGameID() == $otherPlayer->getGameID();
2941
	}
2942
2943
	public function sameAlliance(AbstractSmrPlayer $otherPlayer = null) {
2944
		return $this->equals($otherPlayer) || (!is_null($otherPlayer) && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
2945
	}
2946
2947
	public function sharedForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2948
		return $this->sameAlliance($otherPlayer);
2949
	}
2950
2951
	public function forceNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2952
		return $this->sameAlliance($otherPlayer);
2953
	}
2954
2955
	public function planetNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2956
		return $this->sameAlliance($otherPlayer);
2957
	}
2958
2959
	public function traderNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2960
		return $this->sameAlliance($otherPlayer);
2961
	}
2962
2963
	public function traderMAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2964
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
2965
	}
2966
2967
	public function traderAttackTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2968
		return $this->sameAlliance($otherPlayer);
2969
	}
2970
2971
	public function traderDefendTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2972
		return $this->sameAlliance($otherPlayer);
2973
	}
2974
2975
	public function traderAttackForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2976
		return $this->sameAlliance($otherPlayer);
2977
	}
2978
2979
	public function traderAttackPortAlliance(AbstractSmrPlayer $otherPlayer = null) {
2980
		return $this->sameAlliance($otherPlayer);
2981
	}
2982
2983
	public function traderAttackPlanetAlliance(AbstractSmrPlayer $otherPlayer = null) {
2984
		return $this->sameAlliance($otherPlayer);
2985
	}
2986
2987
	public function meetsAlignmentRestriction($restriction) {
2988
		if ($restriction < 0) {
2989
			return $this->getAlignment() <= $restriction;
2990
		}
2991
		if ($restriction > 0) {
2992
			return $this->getAlignment() >= $restriction;
2993
		}
2994
		return true;
2995
	}
2996
2997
	// Get an array of goods that are visible to the player
2998
	public function getVisibleGoods() {
2999
		$goods = Globals::getGoods();
3000
		$visibleGoods = array();
3001
		foreach ($goods as $key => $good) {
3002
			if ($this->meetsAlignmentRestriction($good['AlignRestriction'])) {
3003
				$visibleGoods[$key] = $good;
3004
			}
3005
		}
3006
		return $visibleGoods;
3007
	}
3008
3009
	/**
3010
	 * Will retrieve all visited sectors, use only when you are likely to check a large number of these
3011
	 */
3012
	public function hasVisitedSector($sectorID) {
3013
		if (!isset($this->visitedSectors)) {
3014
			$this->visitedSectors = array();
3015
			$this->db->query('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
3016
			while ($this->db->nextRecord()) {
3017
				$this->visitedSectors[$this->db->getInt('sector_id')] = false;
3018
			}
3019
		}
3020
		return !isset($this->visitedSectors[$sectorID]);
3021
	}
3022
3023
	public function getLeaveNewbieProtectionHREF() {
3024
		return SmrSession::getNewHREF(create_container('leave_newbie_processing.php'));
3025
	}
3026
3027
	public function getExamineTraderHREF() {
3028
		$container = create_container('skeleton.php', 'trader_examine.php');
3029
		$container['targetPlayerID'] = $this->getPlayerID();
3030
		return SmrSession::getNewHREF($container);
3031
	}
3032
3033
	public function getAttackTraderHREF() {
3034
		return Globals::getAttackTraderHREF($this->getPlayerID());
3035
	}
3036
3037
	public function getPlanetKickHREF() {
3038
		$container = create_container('planet_kick_processing.php', 'trader_attack_processing.php');
3039
		$container['targetPlayerID'] = $this->getPlayerID();
3040
		return SmrSession::getNewHREF($container);
3041
	}
3042
3043
	public function getTraderSearchHREF() {
3044
		$container = create_container('skeleton.php', 'trader_search_result.php');
3045
		$container['player_id'] = $this->getPlayerID();
3046
		return SmrSession::getNewHREF($container);
3047
	}
3048
3049
	public function getAllianceRosterHREF() {
3050
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3051
	}
3052
3053
	public function getToggleWeaponHidingHREF($ajax = false) {
3054
		$container = create_container('toggle_processing.php');
3055
		$container['toggle'] = 'WeaponHiding';
3056
		$container['AJAX'] = $ajax;
3057
		return SmrSession::getNewHREF($container);
3058
	}
3059
3060
	public function isDisplayWeapons() {
3061
		return $this->displayWeapons;
3062
	}
3063
3064
	/**
3065
	 * Should weapons be displayed in the right panel?
3066
	 * This updates the player database directly because it is used with AJAX,
3067
	 * which does not acquire a sector lock.
3068
	 */
3069
	public function setDisplayWeapons($bool) {
3070
		if ($this->displayWeapons == $bool) {
3071
			return;
3072
		}
3073
		$this->displayWeapons = $bool;
3074
		$this->db->query('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3075
	}
3076
3077
	public function update() {
3078
		$this->save();
3079
	}
3080
3081
	public function save() {
3082
		if ($this->hasChanged === true) {
3083
			$this->db->query('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3084
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3085
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3086
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3087
				', turns=' . $this->db->escapeNumber($this->turns) .
3088
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3089
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3090
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3091
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3092
				', dead=' . $this->db->escapeBoolean($this->dead) .
3093
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3094
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3095
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3096
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3097
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3098
				', credits=' . $this->db->escapeNumber($this->credits) .
3099
				', experience=' . $this->db->escapeNumber($this->experience) .
3100
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3101
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3102
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3103
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3104
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3105
				', kills=' . $this->db->escapeNumber($this->kills) .
3106
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3107
				', assists=' . $this->db->escapeNumber($this->assists) .
3108
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3109
				', bank=' . $this->db->escapeNumber($this->bank) .
3110
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3111
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3112
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3113
				', group_scout_messages=' . $this->db->escapeString($this->groupScoutMessages) .
3114
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3115
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3116
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3117
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3118
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3119
				' WHERE ' . $this->SQL . ' LIMIT 1');
3120
			$this->hasChanged = false;
3121
		}
3122
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3123
			if ($bountyChanged === true) {
3124
				$bountyChanged = false;
3125
				$bounty = $this->getBounty($key);
3126
				if ($bounty['New'] === true) {
3127
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3128
						$this->db->query('INSERT INTO bounty (player_id,game_id,type,amount,smr_credits,claimer_player_id,time) VALUES (' . $this->db->escapeNumber($this->getPlayerID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeString($bounty['Type']) . ',' . $this->db->escapeNumber($bounty['Amount']) . ',' . $this->db->escapeNumber($bounty['SmrCredits']) . ',' . $this->db->escapeNumber($bounty['ClaimerPlayerID']) . ',' . $this->db->escapeNumber($bounty['Time']) . ')');
3129
					}
3130
				} else {
3131
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3132
						$this->db->query('UPDATE bounty
3133
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3134
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3135
							type=' . $this->db->escapeString($bounty['Type']) . ',
3136
							claimer_player_id=' . $this->db->escapeNumber($bounty['ClaimerPlayerID']) . ',
3137
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3138
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3139
					} else {
3140
						$this->db->query('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3141
					}
3142
				}
3143
			}
3144
		}
3145
		$this->saveHOF();
3146
	}
3147
3148
	public function saveHOF() {
3149
		if (count($this->hasHOFChanged) > 0) {
3150
			$this->doHOFSave($this->hasHOFChanged);
3151
			$this->hasHOFChanged = [];
3152
		}
3153
		if (!empty(self::$hasHOFVisChanged)) {
3154
			foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3155
				if ($changeType == self::HOF_NEW) {
3156
					$this->db->query('INSERT INTO hof_visibility (type, visibility) VALUES (' . $this->db->escapeString($hofType) . ',' . $this->db->escapeString(self::$HOFVis[$hofType]) . ')');
3157
				} else {
3158
					$this->db->query('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType) . ' LIMIT 1');
3159
				}
3160
				unset(self::$hasHOFVisChanged[$hofType]);
3161
			}
3162
		}
3163
	}
3164
3165
	/**
3166
	 * This should only be called by `saveHOF` (and recursively) to
3167
	 * ensure that the `hasHOFChanged` attribute is properly cleared.
3168
	 */
3169
	protected function doHOFSave(array $hasChangedList, array $typeList = array()) {
3170
		foreach ($hasChangedList as $type => $hofChanged) {
3171
			$tempTypeList = $typeList;
3172
			$tempTypeList[] = $type;
3173
			if (is_array($hofChanged)) {
3174
				$this->doHOFSave($hofChanged, $tempTypeList);
3175
			} else {
3176
				$amount = $this->getHOF($tempTypeList);
3177
				if ($hofChanged == self::HOF_NEW) {
3178
					if ($amount > 0) {
3179
						$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, true, ':', false) . ',' . $this->db->escapeNumber($amount) . ')');   //TODO
3180
					}
3181
				} elseif ($hofChanged == self::HOF_CHANGED) {
3182
					$this->db->query('UPDATE player_hof
3183
						SET amount=' . $this->db->escapeNumber($amount) . '
3184
						WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeArray($tempTypeList, false, true, ':', false) . ' LIMIT 1');
3185
				}
3186
			}
3187
		}
3188
	}
3189
3190
}
3191