Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

Passed
Push — master ( ba9687...5bb441 )
by Dan
03:44
created

AbstractSmrPlayer::doMessageSending()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 49
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php declare(strict_types=1);
2
require_once('missions.inc');
3
4
// Exception thrown when a player cannot be found in the database
5
class PlayerNotFoundException extends Exception {}
6
7
abstract class AbstractSmrPlayer {
8
9
	const TIME_FOR_FEDERAL_BOUNTY_ON_PR = 10800;
10
	const TIME_FOR_ALLIANCE_SWITCH = 0;
11
12
	const HOF_CHANGED = 1;
13
	const HOF_NEW = 2;
14
15
	protected static $CACHE_SECTOR_PLAYERS = array();
16
	protected static $CACHE_PLANET_PLAYERS = array();
17
	protected static $CACHE_ALLIANCE_PLAYERS = array();
18
	protected static $CACHE_PLAYERS = array();
19
20
	protected $db;
21
	protected $SQL;
22
23
	protected $accountID;
24
	protected $gameID;
25
	protected $playerName;
26
	protected $playerID;
27
	protected $sectorID;
28
	protected $lastSectorID;
29
	protected $newbieTurns;
30
	protected $dead;
31
	protected $npc;
32
	protected $newbieStatus;
33
	protected $newbieWarning;
34
	protected $landedOnPlanet;
35
	protected $lastActive;
36
	protected $raceID;
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 $accountID => &$player) {
92
				$player = self::getPlayer($accountID, $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 $accountID => $player) {
113
			if (!in_array($player->getAllianceID(), $allianceIDs)) {
114
				unset($players[$accountID]);
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('account_id')) {
132
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = [];
133
			} else {
134
				$accountID = $db->getInt('account_id');
135
				$player = self::getPlayer($accountID, $gameID, $forceUpdate, $db);
136
				self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$accountID] = $player;
137
				$galaxyPlayers[$sectorID][$accountID] = $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
				$accountID = $db->getInt('account_id');
150
				$players[$accountID] = self::getPlayer($accountID, $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
				$accountID = $db->getInt('account_id');
164
				$players[$accountID] = self::getPlayer($accountID, $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
				$accountID = $db->getInt('account_id');
178
				$players[$accountID] = self::getPlayer($accountID, $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($accountID, $gameID, $forceUpdate = false, $db = null) {
186
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$accountID])) {
187
			self::$CACHE_PLAYERS[$gameID][$accountID] = new SmrPlayer($gameID, $accountID, $db);
188
		}
189
		return self::$CACHE_PLAYERS[$gameID][$accountID];
190
	}
191
192
	public static function getPlayerByPlayerID($playerID, $gameID, $forceUpdate = false) {
193
		$db = new SmrMySqlDatabase();
194
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_id = ' . $db->escapeNumber($playerID) . ' LIMIT 1');
195
		if ($db->nextRecord()) {
196
			return self::getPlayer($db->getInt('account_id'), $gameID, $forceUpdate, $db);
197
		}
198
		throw new PlayerNotFoundException('Player ID not found.');
199
	}
200
201
	public static function getPlayerByPlayerName($playerName, $gameID, $forceUpdate = false) {
202
		$db = new SmrMySqlDatabase();
203
		$db->query('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
204
		if ($db->nextRecord()) {
205
			return self::getPlayer($db->getInt('account_id'), $gameID, $forceUpdate, $db);
206
		}
207
		throw new PlayerNotFoundException('Player Name not found.');
208
	}
209
210
	protected function __construct($gameID, $accountID, $db = null) {
211
		$this->db = new SmrMySqlDatabase();
212
		$this->SQL = 'account_id = ' . $this->db->escapeNumber($accountID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
213
214
		if (isset($db)) {
215
			$playerExists = true;
216
		} else {
217
			$db = $this->db;
218
			$this->db->query('SELECT * FROM player WHERE ' . $this->SQL . ' LIMIT 1');
219
			$playerExists = $db->nextRecord();
220
		}
221
222
		if ($playerExists) {
223
			$this->accountID = (int)$accountID;
224
			$this->gameID = (int)$gameID;
225
			$this->playerName = $db->getField('player_name');
226
			$this->playerID = $db->getInt('player_id');
227
			$this->sectorID = $db->getInt('sector_id');
228
			$this->lastSectorID = $db->getInt('last_sector_id');
229
			$this->turns = $db->getInt('turns');
230
			$this->lastTurnUpdate = $db->getInt('last_turn_update');
231
			$this->newbieTurns = $db->getInt('newbie_turns');
232
			$this->lastNewsUpdate = $db->getInt('last_news_update');
233
			$this->attackColour = $db->getField('attack_warning');
234
			$this->dead = $db->getBoolean('dead');
235
			$this->npc = $db->getBoolean('npc');
236
			$this->newbieStatus = $db->getBoolean('newbie_status');
237
			$this->landedOnPlanet = $db->getBoolean('land_on_planet');
238
			$this->lastActive = $db->getInt('last_active');
239
			$this->lastCPLAction = $db->getInt('last_cpl_action');
240
			$this->raceID = $db->getInt('race_id');
241
			$this->credits = $db->getInt('credits');
242
			$this->experience = $db->getInt('experience');
243
			$this->alignment = $db->getInt('alignment');
244
			$this->militaryPayment = $db->getInt('military_payment');
245
			$this->allianceID = $db->getInt('alliance_id');
246
			$this->allianceJoinable = $db->getInt('alliance_join');
247
			$this->shipID = $db->getInt('ship_type_id');
248
			$this->kills = $db->getInt('kills');
249
			$this->deaths = $db->getInt('deaths');
250
			$this->assists = $db->getInt('assists');
251
			$this->lastPort = $db->getInt('last_port');
252
			$this->bank = $db->getInt('bank');
253
			$this->zoom = $db->getInt('zoom');
254
			$this->displayMissions = $db->getBoolean('display_missions');
255
			$this->displayWeapons = $db->getBoolean('display_weapons');
256
			$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...
257
			$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...
258
			$this->ignoreGlobals = $db->getBoolean('ignore_globals');
259
			$this->newbieWarning = $db->getBoolean('newbie_warning');
260
			$this->nameChanged = $db->getBoolean('name_changed');
261
			$this->raceChanged = $db->getBoolean('race_changed');
262
			$this->combatDronesKamikazeOnMines = $db->getBoolean('combat_drones_kamikaze_on_mines');
263
		} else {
264
			throw new PlayerNotFoundException('Invalid accountID: ' . $accountID . ' OR gameID:' . $gameID);
265
		}
266
	}
267
268
	/**
269
	 * Insert a new player into the database. Returns the new player object.
270
	 */
271
	public static function createPlayer($accountID, $gameID, $playerName, $raceID, $isNewbie, $npc=false) {
272
		$db = new SmrMySqlDatabase();
273
		$db->lockTable('player');
274
275
		// Player names must be unique within each game
276
		$db->query('SELECT 1 FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName) . ' LIMIT 1');
277
		if ($db->nextRecord() > 0) {
278
			$db->unlock();
279
			create_error('The player name already exists.');
280
		}
281
282
		// get last registered player id in that game and increase by one.
283
		$db->query('SELECT MAX(player_id) FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
284
		if ($db->nextRecord()) {
285
			$playerID = $db->getInt('MAX(player_id)') + 1;
286
		} else {
287
			$playerID = 1;
288
		}
289
290
		$startSectorID = 0; // Temporarily put player into non-existent sector
291
		$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)
292
					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) . ')');
293
294
		$db->unlock();
295
296
		$player = SmrPlayer::getPlayer($accountID, $gameID);
297
		$player->setSectorID($player->getHome());
298
		return $player;
299
	}
300
301
	/**
302
	 * Get array of players whose info can be accessed by this player.
303
	 * Skips players who are not in the same alliance as this player.
304
	 */
305
	public function getSharingPlayers($forceUpdate = false) {
306
		$results = array($this);
307
308
		// Only return this player if not in an alliance
309
		if (!$this->hasAlliance()) {
310
			return $results;
311
		}
312
313
		// Get other players who are sharing info for this game.
314
		// NOTE: game_id=0 means that player shares info for all games.
315
		$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()) . ')');
316
		while ($this->db->nextRecord()) {
317
			try {
318
				$otherPlayer = SmrPlayer::getPlayer($this->db->getInt('from_account_id'),
319
				                                    $this->getGameID(), $forceUpdate);
320
			} catch (PlayerNotFoundException $e) {
321
				// Skip players that have not joined this game
322
				continue;
323
			}
324
325
			// players must be in the same alliance
326
			if ($this->sameAlliance($otherPlayer)) {
327
				$results[] = $otherPlayer;
328
			}
329
		}
330
		return $results;
331
	}
332
333
	public function getSQL() : string {
334
		return $this->SQL;
335
	}
336
337
	public function getZoom() {
338
		return $this->zoom;
339
	}
340
341
	protected function setZoom($zoom) {
342
		// Set the zoom level between [1, 9]
343
		$zoom = max(1, min(9, $zoom));
344
		if ($this->zoom == $zoom) {
345
			return;
346
		}
347
		$this->zoom = $zoom;
348
		$this->hasChanged = true;
349
	}
350
351
	public function increaseZoom($zoom) {
352
		if ($zoom < 0) {
353
			throw new Exception('Trying to increase negative zoom.');
354
		}
355
		$this->setZoom($this->getZoom() + $zoom);
356
	}
357
358
	public function decreaseZoom($zoom) {
359
		if ($zoom < 0) {
360
			throw new Exception('Trying to decrease negative zoom.');
361
		}
362
		$this->setZoom($this->getZoom() - $zoom);
363
	}
364
365
	public function getAttackColour() {
366
		return $this->attackColour;
367
	}
368
369
	public function setAttackColour($colour) {
370
		if ($this->attackColour == $colour) {
371
			return;
372
		}
373
		$this->attackColour = $colour;
374
		$this->hasChanged = true;
375
	}
376
377
	public function isIgnoreGlobals() {
378
		return $this->ignoreGlobals;
379
	}
380
381
	public function setIgnoreGlobals($bool) {
382
		if ($this->ignoreGlobals == $bool) {
383
			return;
384
		}
385
		$this->ignoreGlobals = $bool;
386
		$this->hasChanged = true;
387
	}
388
389
	public function getAccount() {
390
		return SmrAccount::getAccount($this->getAccountID());
391
	}
392
393
	public function getAccountID() {
394
		return $this->accountID;
395
	}
396
397
	public function getGameID() {
398
		return $this->gameID;
399
	}
400
401
	public function getGame() {
402
		return SmrGame::getGame($this->gameID);
403
	}
404
405
	public function getNewbieTurns() {
406
		return $this->newbieTurns;
407
	}
408
409
	public function hasNewbieTurns() {
410
		return $this->getNewbieTurns() > 0;
411
	}
412
	public function setNewbieTurns($newbieTurns) {
413
		if ($this->newbieTurns == $newbieTurns) {
414
			return;
415
		}
416
		$this->newbieTurns = $newbieTurns;
417
		$this->hasChanged = true;
418
	}
419
420
	public function getShip($forceUpdate = false) {
421
		return SmrShip::getShip($this, $forceUpdate);
422
	}
423
424
	public function getShipTypeID() {
425
		return $this->shipID;
426
	}
427
428
	/**
429
	 * Do not call directly. Use SmrShip::setShipTypeID instead.
430
	 */
431
	public function setShipTypeID($shipID) {
432
		if ($this->shipID == $shipID) {
433
			return;
434
		}
435
		$this->shipID = $shipID;
436
		$this->hasChanged = true;
437
	}
438
439
	public function hasCustomShipName() {
440
		return $this->getCustomShipName() !== false;
441
	}
442
443
	public function getCustomShipName() {
444
		if (!isset($this->customShipName)) {
445
			$this->db->query('SELECT * FROM ship_has_name WHERE ' . $this->SQL . ' LIMIT 1');
446
			if ($this->db->nextRecord()) {
447
				$this->customShipName = $this->db->getField('ship_name');
448
			} else {
449
				$this->customShipName = false;
450
			}
451
		}
452
		return $this->customShipName;
453
	}
454
455
	public function setCustomShipName(string $name) {
456
		$this->db->query('REPLACE INTO ship_has_name (game_id, account_id, ship_name)
457
			VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeString($name) . ')');
458
	}
459
460
	/**
461
	 * Get planet owned by this player.
462
	 * Returns false if this player does not own a planet.
463
	 */
464
	public function getPlanet() {
465
		$this->db->query('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_id=' . $this->db->escapeNumber($this->getAccountID()));
466
		if ($this->db->nextRecord()) {
467
			return SmrPlanet::getPlanet($this->getGameID(), $this->db->getInt('sector_id'), false, $this->db);
468
		} else {
469
			return false;
470
		}
471
	}
472
473
	public function getSectorPlanet() {
474
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
475
	}
476
477
	public function getSectorPort() {
478
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
479
	}
480
481
	public function getSectorID() {
482
		return $this->sectorID;
483
	}
484
485
	public function getSector() {
486
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
487
	}
488
489
	public function setSectorID($sectorID) {
490
		if ($this->sectorID == $sectorID) {
491
			return;
492
		}
493
494
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
495
		$port->addCachePort($this->getAccountID()); //Add port of sector we were just in, to make sure it is left totally up to date.
496
497
		$this->setLastSectorID($this->getSectorID());
498
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
499
		$this->sectorID = $sectorID;
500
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
501
		$this->hasChanged = true;
502
503
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
504
		$port->addCachePort($this->getAccountID()); //Add the port of sector we are now in.
505
	}
506
507
	public function getLastSectorID() {
508
		return $this->lastSectorID;
509
	}
510
511
	public function setLastSectorID($lastSectorID) {
512
		if ($this->lastSectorID == $lastSectorID) {
513
			return;
514
		}
515
		$this->lastSectorID = $lastSectorID;
516
		$this->hasChanged = true;
517
	}
518
519
	public function getHome() {
520
		// get his home sector
521
		$hq_id = GOVERNMENT + $this->getRaceID();
522
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
523
		if (!empty($raceHqSectors)) {
524
			// If race has multiple HQ's for some reason, use the first one
525
			return key($raceHqSectors);
526
		} else {
527
			return 1;
528
		}
529
	}
530
531
	public function isDead() {
532
		return $this->dead;
533
	}
534
535
	public function isNPC() {
536
		return $this->npc;
537
	}
538
539
	/**
540
	 * Does the player have Newbie status?
541
	 */
542
	public function hasNewbieStatus() {
543
		return $this->newbieStatus;
544
	}
545
546
	/**
547
	 * Update the player's newbie status if it has changed.
548
	 * This function queries the account, so use sparingly.
549
	 */
550
	public function updateNewbieStatus() {
551
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
552
		if ($this->newbieStatus != $accountNewbieStatus) {
553
			$this->newbieStatus = $accountNewbieStatus;
554
			$this->hasChanged = true;
555
		}
556
	}
557
558
	/**
559
	 * Has this player been designated as the alliance flagship?
560
	 */
561
	public function isFlagship() {
562
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipID() == $this->getAccountID();
563
	}
564
565
	public function isPresident() {
566
		return Council::getPresidentID($this->getGameID(), $this->getRaceID()) == $this->getAccountID();
567
	}
568
569
	public function isOnCouncil() {
570
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getAccountID());
571
	}
572
573
	public function isDraftLeader() {
574
		if (!isset($this->draftLeader)) {
575
			$this->draftLeader = false;
576
			$this->db->query('SELECT 1 FROM draft_leaders WHERE ' . $this->SQL . ' LIMIT 1');
577
			if ($this->db->nextRecord()) {
578
				$this->draftLeader = true;
579
			}
580
		}
581
		return $this->draftLeader;
582
	}
583
584
	public function getGPWriter() {
585
		if (!isset($this->gpWriter)) {
586
			$this->gpWriter = false;
587
			$this->db->query('SELECT position FROM galactic_post_writer WHERE ' . $this->SQL);
588
			if ($this->db->nextRecord()) {
589
				$this->gpWriter = $this->db->getField('position');
590
			}
591
		}
592
		return $this->gpWriter;
593
	}
594
595
	public function isGPEditor() {
596
		return $this->getGPWriter() == 'editor';
597
	}
598
599
	public function isForceDropMessages() {
600
		return $this->forceDropMessages;
601
	}
602
603
	public function setForceDropMessages($bool) {
604
		if ($this->forceDropMessages == $bool) {
605
			return;
606
		}
607
		$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...
608
		$this->hasChanged = true;
609
	}
610
611
	public function getScoutMessageGroupLimit() {
612
		if ($this->groupScoutMessages == 'ALWAYS') {
613
			return 0;
614
		} elseif ($this->groupScoutMessages == 'AUTO') {
615
			return MESSAGES_PER_PAGE;
616
		} elseif ($this->groupScoutMessages == 'NEVER') {
617
			return PHP_INT_MAX;
618
		}
619
	}
620
621
	public function getGroupScoutMessages() {
622
		return $this->groupScoutMessages;
623
	}
624
625
	public function setGroupScoutMessages($setting) {
626
		if ($this->groupScoutMessages == $setting) {
627
			return;
628
		}
629
		$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...
630
		$this->hasChanged = true;
631
	}
632
633
	protected static function doMessageSending($senderID, $receiverID, $gameID, $messageTypeID, $message, $expires, $senderDelete = false, $unread = true) {
634
		$message = trim($message);
635
		$db = new SmrMySqlDatabase();
636
		// send him the message
637
		$db->query('INSERT INTO message
638
			(account_id,game_id,message_type_id,message_text,
639
			sender_id,send_time,expire_time,sender_delete) VALUES(' .
640
			$db->escapeNumber($receiverID) . ',' .
641
			$db->escapeNumber($gameID) . ',' .
642
			$db->escapeNumber($messageTypeID) . ',' .
643
			$db->escapeString($message) . ',' .
644
			$db->escapeNumber($senderID) . ',' .
645
			$db->escapeNumber(TIME) . ',' .
646
			$db->escapeNumber($expires) . ',' .
647
			$db->escapeBoolean($senderDelete) . ')'
648
		);
649
		// Keep track of the message_id so it can be returned
650
		$insertID = $db->getInsertID();
651
652
		if ($unread === true) {
653
			// give him the message icon
654
			$db->query('REPLACE INTO player_has_unread_messages (game_id, account_id, message_type_id) VALUES
655
						(' . $db->escapeNumber($gameID) . ', ' . $db->escapeNumber($receiverID) . ', ' . $db->escapeNumber($messageTypeID) . ')');
656
		}
657
658
		switch ($messageTypeID) {
659
			case MSG_PLAYER:
660
				$receiverAccount = SmrAccount::getAccount($receiverID);
661
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isLoggedIn()) {
662
					require_once(get_file_loc('message.functions.inc'));
663
					$sender = getMessagePlayer($senderID, $gameID, $messageTypeID);
664
					if ($sender instanceof SmrPlayer) {
665
						$sender = $sender->getDisplayName();
666
					}
667
					$mail = setupMailer();
668
					$mail->Subject = 'Message Notification';
669
					$mail->setFrom('[email protected]', 'SMR Notifications');
670
					$bbifiedMessage = 'From: ' . $sender . ' Date: ' . date($receiverAccount->getShortDateFormat() . ' ' . $receiverAccount->getShortTimeFormat(), TIME) . "<br/>\r\n<br/>\r\n" . bbifyMessage($message, true);
671
					$mail->msgHTML($bbifiedMessage);
672
					$mail->AltBody = strip_tags($bbifiedMessage);
673
					$mail->addAddress($receiverAccount->getEmail(), $receiverAccount->getHofName());
674
					$mail->send();
675
					$receiverAccount->decreaseMessageNotifications($messageTypeID, 1);
676
					$receiverAccount->update();
677
				}
678
			break;
679
		}
680
681
		return $insertID;
682
	}
683
684
	public function sendMessageToBox($boxTypeID, $message) {
685
		// send him the message
686
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
687
	}
688
689
	public function sendGlobalMessage($message, $canBeIgnored = true) {
690
		if ($canBeIgnored) {
691
			if ($this->getAccount()->isMailBanned()) {
692
				create_error('You are currently banned from sending messages');
693
			}
694
		}
695
		$this->sendMessageToBox(BOX_GLOBALS, $message);
696
697
		// send to all online player
698
		$db = new SmrMySqlDatabase();
699
		$db->query('SELECT account_id
700
					FROM active_session
701
					JOIN player USING (game_id, account_id)
702
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(TIME - SmrSession::TIME_BEFORE_EXPIRY) . '
703
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
704
						AND ignore_globals = \'FALSE\'
705
						AND account_id != ' . $db->escapeNumber($this->getAccountID()));
706
707
		while ($db->nextRecord()) {
708
			$this->sendMessage($db->getInt('account_id'), MSG_GLOBAL, $message, $canBeIgnored);
709
		}
710
		$this->sendMessage($this->getAccountID(), MSG_GLOBAL, $message, $canBeIgnored, false);
711
	}
712
713
	public function sendMessage($receiverID, $messageTypeID, $message, $canBeIgnored = true, $unread = true, $expires = false, $senderDelete = false) {
714
		//get expire time
715
		if ($canBeIgnored) {
716
			if ($this->getAccount()->isMailBanned()) {
717
				create_error('You are currently banned from sending messages');
718
			}
719
			// Don't send messages to players ignoring us
720
			$this->db->query('SELECT account_id FROM message_blacklist WHERE account_id=' . $this->db->escapeNumber($receiverID) . ' AND blacklisted_id=' . $this->db->escapeNumber($this->getAccountID()) . ' LIMIT 1');
721
			if ($this->db->nextRecord()) {
722
				return;
723
			}
724
		}
725
726
		$message = word_filter($message);
727
728
		// If expires not specified, use default based on message type
729
		if ($expires === false) {
730
			switch ($messageTypeID) {
731
				case MSG_GLOBAL: //We don't send globals to the box here or it gets done loads of times.
732
					$expires = 3600; // 1h
733
				break;
734
				case MSG_PLAYER:
735
					$expires = 86400 * 31;
736
				break;
737
				case MSG_PLANET:
738
					$expires = 86400 * 7;
739
				break;
740
				case MSG_SCOUT:
741
					$expires = 86400 * 3;
742
				break;
743
				case MSG_POLITICAL:
744
					$expires = 86400 * 31;
745
				break;
746
				case MSG_ALLIANCE:
747
					$expires = 86400 * 31;
748
				break;
749
				case MSG_ADMIN:
750
					$expires = 86400 * 365;
751
				break;
752
				case MSG_CASINO:
753
					$expires = 86400 * 31;
754
				break;
755
				default:
756
					$expires = 86400 * 7;
757
			}
758
			$expires += TIME;
759
		}
760
761
		// Do not put scout messages in the sender's sent box
762
		if ($messageTypeID == MSG_SCOUT) {
763
			$senderDelete = true;
764
		}
765
766
		// send him the message and return the message_id
767
		return self::doMessageSending($this->getAccountID(), $receiverID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
768
	}
769
770
	public function sendMessageFromOpAnnounce($receiverID, $message, $expires = false) {
771
		// get expire time if not set
772
		if ($expires === false) {
773
			$expires = TIME + 86400 * 14;
774
		}
775
		self::doMessageSending(ACCOUNT_ID_OP_ANNOUNCE, $receiverID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
776
	}
777
778
	public function sendMessageFromAllianceCommand($receiverID, $message) {
779
		$expires = TIME + 86400 * 365;
780
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_COMMAND, $receiverID, $this->getGameID(), MSG_PLAYER, $message, $expires);
781
	}
782
783
	public static function sendMessageFromPlanet($gameID, $receiverID, $message) {
784
		//get expire time
785
		$expires = TIME + 86400 * 31;
786
		// send him the message
787
		self::doMessageSending(ACCOUNT_ID_PLANET, $receiverID, $gameID, MSG_PLANET, $message, $expires);
788
	}
789
790
	public static function sendMessageFromPort($gameID, $receiverID, $message) {
791
		//get expire time
792
		$expires = TIME + 86400 * 31;
793
		// send him the message
794
		self::doMessageSending(ACCOUNT_ID_PORT, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
795
	}
796
797
	public static function sendMessageFromFedClerk($gameID, $receiverID, $message) {
798
		$expires = TIME + 86400 * 365;
799
		self::doMessageSending(ACCOUNT_ID_FED_CLERK, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
800
	}
801
802
	public static function sendMessageFromAdmin($gameID, $receiverID, $message, $expires = false) {
803
		//get expire time
804
		if ($expires === false) {
805
			$expires = TIME + 86400 * 365;
806
		}
807
		// send him the message
808
		self::doMessageSending(ACCOUNT_ID_ADMIN, $receiverID, $gameID, MSG_ADMIN, $message, $expires);
809
	}
810
811
	public static function sendMessageFromAllianceAmbassador($gameID, $receiverID, $message, $expires = false) {
812
		//get expire time
813
		if ($expires === false) {
814
			$expires = TIME + 86400 * 31;
815
		}
816
		// send him the message
817
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_AMBASSADOR, $receiverID, $gameID, MSG_ALLIANCE, $message, $expires);
818
	}
819
820
	public static function sendMessageFromCasino($gameID, $receiverID, $message, $expires = false) {
821
		//get expire time
822
		if ($expires === false) {
823
			$expires = TIME + 86400 * 7;
824
		}
825
		// send him the message
826
		self::doMessageSending(ACCOUNT_ID_CASINO, $receiverID, $gameID, MSG_CASINO, $message, $expires);
827
	}
828
829
	public static function sendMessageFromRace($raceID, $gameID, $receiverID, $message, $expires = false) {
830
		//get expire time
831
		if ($expires === false) {
832
			$expires = TIME + 86400 * 5;
833
		}
834
		// send him the message
835
		self::doMessageSending(ACCOUNT_ID_GROUP_RACES + $raceID, $receiverID, $gameID, MSG_POLITICAL, $message, $expires);
836
	}
837
838
	public function setMessagesRead($messageTypeID) {
839
		$this->db->query('DELETE FROM player_has_unread_messages
840
							WHERE '.$this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
841
	}
842
843
	public function getSafeAttackRating() {
844
		return max(0, min(8, $this->getAlignment() / 150 + 4));
845
	}
846
847
	public function hasFederalProtection() {
848
		$sector = SmrSector::getSector($this->getGameID(), $this->getSectorID());
849
		if (!$sector->offersFederalProtection()) {
850
			return false;
851
		}
852
853
		$ship = $this->getShip();
854
		if ($ship->hasIllegalGoods()) {
855
			return false;
856
		}
857
858
		if ($ship->getAttackRating() <= $this->getSafeAttackRating()) {
859
			foreach ($sector->getFedRaceIDs() as $fedRaceID) {
860
				if ($this->canBeProtectedByRace($fedRaceID)) {
861
					return true;
862
				}
863
			}
864
		}
865
866
		return false;
867
	}
868
869
	public function canBeProtectedByRace($raceID) {
870
		if (!isset($this->canFed)) {
871
			$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...
872
			$RACES = Globals::getRaces();
873
			foreach ($RACES as $raceID2 => $raceName) {
874
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
875
			}
876
			$this->db->query('SELECT race_id, allowed FROM player_can_fed
877
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(TIME));
878
			while ($this->db->nextRecord()) {
879
				$this->canFed[$this->db->getInt('race_id')] = $this->db->getBoolean('allowed');
880
			}
881
		}
882
		return $this->canFed[$raceID];
883
	}
884
885
	/**
886
	 * Returns a boolean identifying if the player can currently
887
	 * participate in battles.
888
	 */
889
	public function canFight() {
890
		return !($this->hasNewbieTurns() ||
891
		         $this->isDead() ||
892
		         $this->isLandedOnPlanet() ||
893
		         $this->hasFederalProtection());
894
	}
895
896
	public function setDead($bool) {
897
		if ($this->dead == $bool) {
898
			return;
899
		}
900
		$this->dead = $bool;
901
		$this->hasChanged = true;
902
	}
903
904
	public function getKills() {
905
		return $this->kills;
906
	}
907
908
	public function increaseKills($kills) {
909
		if ($kills < 0) {
910
			throw new Exception('Trying to increase negative kills.');
911
		}
912
		$this->setKills($this->kills + $kills);
913
	}
914
915
	public function setKills($kills) {
916
		if ($this->kills == $kills) {
917
			return;
918
		}
919
		$this->kills = $kills;
920
		$this->hasChanged = true;
921
	}
922
923
	public function getDeaths() {
924
		return $this->deaths;
925
	}
926
927
	public function increaseDeaths($deaths) {
928
		if ($deaths < 0) {
929
			throw new Exception('Trying to increase negative deaths.');
930
		}
931
		$this->setDeaths($this->getDeaths() + $deaths);
932
	}
933
934
	public function setDeaths($deaths) {
935
		if ($this->deaths == $deaths) {
936
			return;
937
		}
938
		$this->deaths = $deaths;
939
		$this->hasChanged = true;
940
	}
941
942
	public function getAssists() {
943
		return $this->assists;
944
	}
945
946
	public function increaseAssists($assists) {
947
		if ($assists < 1) {
948
			throw new Exception('Must increase by a positive number.');
949
		}
950
		$this->assists += $assists;
951
		$this->hasChanged = true;
952
	}
953
954
	public function getAlignment() {
955
		return $this->alignment;
956
	}
957
958
	public function increaseAlignment($align) {
959
		if ($align < 0) {
960
			throw new Exception('Trying to increase negative align.');
961
		}
962
		if ($align == 0) {
963
			return;
964
		}
965
		$align += $this->alignment;
966
		$this->setAlignment($align);
967
	}
968
969
	public function decreaseAlignment($align) {
970
		if ($align < 0) {
971
			throw new Exception('Trying to decrease negative align.');
972
		}
973
		if ($align == 0) {
974
			return;
975
		}
976
		$align = $this->alignment - $align;
977
		$this->setAlignment($align);
978
	}
979
980
	public function setAlignment($align) {
981
		if ($this->alignment == $align) {
982
			return;
983
		}
984
		$this->alignment = $align;
985
		$this->hasChanged = true;
986
	}
987
988
	public function getCredits() {
989
		return $this->credits;
990
	}
991
992
	public function getBank() {
993
		return $this->bank;
994
	}
995
996
	/**
997
	 * Increases personal bank account up to the maximum allowed credits.
998
	 * Returns the amount that was actually added to handle overflow.
999
	 */
1000
	public function increaseBank(int $credits) : int {
1001
		if ($credits == 0) {
1002
			return 0;
1003
		}
1004
		if ($credits < 0) {
1005
			throw new Exception('Trying to increase negative credits.');
1006
		}
1007
		$newTotal = min($this->bank + $credits, MAX_MONEY);
1008
		$actualAdded = $newTotal - $this->bank;
1009
		$this->setBank($newTotal);
1010
		return $actualAdded;
1011
	}
1012
1013
	public function decreaseBank(int $credits) : void {
1014
		if ($credits == 0) {
1015
			return;
1016
		}
1017
		if ($credits < 0) {
1018
			throw new Exception('Trying to decrease negative credits.');
1019
		}
1020
		$newTotal = $this->bank - $credits;
1021
		$this->setBank($newTotal);
1022
	}
1023
1024
	public function setBank(int $credits) : void {
1025
		if ($this->bank == $credits) {
1026
			return;
1027
		}
1028
		if ($credits < 0) {
1029
			throw new Exception('Trying to set negative credits.');
1030
		}
1031
		if ($credits > MAX_MONEY) {
1032
			throw new Exception('Trying to set more than max credits.');
1033
		}
1034
		$this->bank = $credits;
1035
		$this->hasChanged = true;
1036
	}
1037
1038
	public function getExperience() {
1039
		return $this->experience;
1040
	}
1041
1042
	/**
1043
	 * Returns the percent progress towards the next level.
1044
	 * This value is rounded because it is used primarily in HTML img widths.
1045
	 */
1046
	public function getNextLevelPercentAcquired() : int {
1047
		if ($this->getNextLevelExperience() == $this->getThisLevelExperience()) {
1048
			return 100;
1049
		}
1050
		return max(0, min(100, IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
1051
	}
1052
1053
	public function getNextLevelPercentRemaining() {
1054
		return 100 - $this->getNextLevelPercentAcquired();
1055
	}
1056
1057
	public function getNextLevelExperience() {
1058
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1059
		if (!isset($LEVELS_REQUIREMENTS[$this->getLevelID() + 1])) {
1060
			return $this->getThisLevelExperience(); //Return current level experience if on last level.
1061
		}
1062
		return $LEVELS_REQUIREMENTS[$this->getLevelID() + 1]['Requirement'];
1063
	}
1064
1065
	public function getThisLevelExperience() {
1066
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1067
		return $LEVELS_REQUIREMENTS[$this->getLevelID()]['Requirement'];
1068
	}
1069
1070
	public function setExperience($experience) {
1071
		if ($this->experience == $experience) {
1072
			return;
1073
		}
1074
		if ($experience < MIN_EXPERIENCE) {
1075
			$experience = MIN_EXPERIENCE;
1076
		}
1077
		if ($experience > MAX_EXPERIENCE) {
1078
			$experience = MAX_EXPERIENCE;
1079
		}
1080
		$this->experience = $experience;
1081
		$this->hasChanged = true;
1082
1083
		// Since exp has changed, invalidate the player level so that it can
1084
		// be recomputed next time it is queried (in case it has changed).
1085
		$this->level = null;
1086
	}
1087
1088
	/**
1089
	 * Increases onboard credits up to the maximum allowed credits.
1090
	 * Returns the amount that was actually added to handle overflow.
1091
	 */
1092
	public function increaseCredits(int $credits) : int {
1093
		if ($credits == 0) {
1094
			return 0;
1095
		}
1096
		if ($credits < 0) {
1097
			throw new Exception('Trying to increase negative credits.');
1098
		}
1099
		$newTotal = min($this->credits + $credits, MAX_MONEY);
1100
		$actualAdded = $newTotal - $this->credits;
1101
		$this->setCredits($newTotal);
1102
		return $actualAdded;
1103
	}
1104
1105
	public function decreaseCredits(int $credits) : void {
1106
		if ($credits == 0) {
1107
			return;
1108
		}
1109
		if ($credits < 0) {
1110
			throw new Exception('Trying to decrease negative credits.');
1111
		}
1112
		$newTotal = $this->credits - $credits;
1113
		$this->setCredits($newTotal);
1114
	}
1115
1116
	public function setCredits(int $credits) : void {
1117
		if ($this->credits == $credits) {
1118
			return;
1119
		}
1120
		if ($credits < 0) {
1121
			throw new Exception('Trying to set negative credits.');
1122
		}
1123
		if ($credits > MAX_MONEY) {
1124
			throw new Exception('Trying to set more than max credits.');
1125
		}
1126
		$this->credits = $credits;
1127
		$this->hasChanged = true;
1128
	}
1129
1130
	public function increaseExperience($experience) {
1131
		if ($experience < 0) {
1132
			throw new Exception('Trying to increase negative experience.');
1133
		}
1134
		if ($experience == 0) {
1135
			return;
1136
		}
1137
		$newExperience = $this->experience + $experience;
1138
		$this->setExperience($newExperience);
1139
		$this->increaseHOF($experience, array('Experience', 'Total', 'Gain'), HOF_PUBLIC);
1140
	}
1141
	public function decreaseExperience($experience) {
1142
		if ($experience < 0) {
1143
			throw new Exception('Trying to decrease negative experience.');
1144
		}
1145
		if ($experience == 0) {
1146
			return;
1147
		}
1148
		$newExperience = $this->experience - $experience;
1149
		$this->setExperience($newExperience);
1150
		$this->increaseHOF($experience, array('Experience', 'Total', 'Loss'), HOF_PUBLIC);
1151
	}
1152
1153
	public function isLandedOnPlanet() {
1154
		return $this->landedOnPlanet;
1155
	}
1156
1157
	public function setLandedOnPlanet($bool) {
1158
		if ($this->landedOnPlanet == $bool) {
1159
			return;
1160
		}
1161
		$this->landedOnPlanet = $bool;
1162
		$this->hasChanged = true;
1163
	}
1164
1165
	/**
1166
	 * Returns the numerical level of the player (e.g. 1-50).
1167
	 */
1168
	public function getLevelID() {
1169
		// The level is cached for performance reasons unless `setExperience`
1170
		// is called and the player's experience changes.
1171
		if ($this->level === null) {
1172
			$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1173
			foreach ($LEVELS_REQUIREMENTS as $level_id => $require) {
1174
				if ($this->getExperience() >= $require['Requirement']) {
1175
					continue;
1176
				}
1177
				$this->level = $level_id - 1;
1178
				return $this->level;
1179
			}
1180
			$this->level = max(array_keys($LEVELS_REQUIREMENTS));
1181
		}
1182
		return $this->level;
1183
	}
1184
1185
	public function getLevelName() {
1186
		$level_name = Globals::getLevelRequirements()[$this->getLevelID()]['Name'];
1187
		if ($this->isPresident()) {
1188
			$level_name = '<img src="images/council_president.png" title="' . Globals::getRaceName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1189
		}
1190
		return $level_name;
1191
	}
1192
1193
	public function getMaxLevel() {
1194
		return max(array_keys(Globals::getLevelRequirements()));
1195
	}
1196
1197
	public function getPlayerID() {
1198
		return $this->playerID;
1199
	}
1200
1201
	/**
1202
	 * Returns the player name.
1203
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1204
	 */
1205
	public function getPlayerName() {
1206
		return $this->playerName;
1207
	}
1208
1209
	public function setPlayerName($name) {
1210
		$this->playerName = $name;
1211
		$this->hasChanged = true;
1212
	}
1213
1214
	/**
1215
	 * Returns the decorated player name, suitable for HTML display.
1216
	 */
1217
	public function getDisplayName($includeAlliance = false) {
1218
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1219
		$return = get_colored_text($this->getAlignment(), $name);
1220
		if ($this->isNPC()) {
1221
			$return .= ' <span class="npcColour">[NPC]</span>';
1222
		}
1223
		if ($includeAlliance) {
1224
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1225
		}
1226
		return $return;
1227
	}
1228
1229
	public function getBBLink() {
1230
			return '[player=' . $this->getPlayerID() . ']';
1231
	}
1232
1233
	public function getLinkedDisplayName($includeAlliance = true) {
1234
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1235
		if ($includeAlliance) {
1236
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1237
		}
1238
		return $return;
1239
	}
1240
1241
	/**
1242
	 * Use this method when the player is changing their own name.
1243
	 * This will flag the player as having used their free name change.
1244
	 */
1245
	public function setPlayerNameByPlayer($playerName) {
1246
		$this->setPlayerName($playerName);
1247
		$this->setNameChanged(true);
1248
	}
1249
1250
	public function isNameChanged() {
1251
		return $this->nameChanged;
1252
	}
1253
1254
	public function setNameChanged($bool) {
1255
		$this->nameChanged = $bool;
1256
		$this->hasChanged = true;
1257
	}
1258
1259
	public function isRaceChanged() : bool {
1260
		return $this->raceChanged;
1261
	}
1262
1263
	public function setRaceChanged(bool $raceChanged) : void {
1264
		$this->raceChanged = $raceChanged;
1265
		$this->hasChanged = true;
1266
	}
1267
1268
	public function canChangeRace() : bool {
1269
		return !$this->isRaceChanged() && (TIME - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1270
	}
1271
1272
	public function getRaceID() {
1273
		return $this->raceID;
1274
	}
1275
1276
	public function getRaceName() {
1277
		return Globals::getRaceName($this->getRaceID());
1278
	}
1279
1280
	public static function getColouredRaceNameOrDefault($otherRaceID, AbstractSmrPlayer $player = null, $linked = false) {
1281
		$relations = 0;
1282
		if ($player !== null) {
1283
			$relations = $player->getRelation($otherRaceID);
1284
		}
1285
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1286
	}
1287
1288
	public function getColouredRaceName($otherRaceID, $linked = false) {
1289
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1290
	}
1291
1292
	public function setRaceID($raceID) {
1293
		if ($this->raceID == $raceID) {
1294
			return;
1295
		}
1296
		$this->raceID = $raceID;
1297
		$this->hasChanged = true;
1298
	}
1299
1300
	public function isAllianceLeader($forceUpdate = false) {
1301
		return $this->getAccountID() == $this->getAlliance($forceUpdate)->getLeaderID();
1302
	}
1303
1304
	public function getAlliance($forceUpdate = false) {
1305
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1306
	}
1307
1308
	public function getAllianceID() {
1309
		return $this->allianceID;
1310
	}
1311
1312
	public function hasAlliance() {
1313
		return $this->getAllianceID() != 0;
1314
	}
1315
1316
	protected function setAllianceID($ID) {
1317
		if ($this->allianceID == $ID) {
1318
			return;
1319
		}
1320
		$this->allianceID = $ID;
1321
		if ($this->allianceID != 0) {
1322
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1323
			$this->db->query('INSERT IGNORE INTO player_joined_alliance (account_id,game_id,alliance_id,status) ' .
1324
				'VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1325
		}
1326
		$this->hasChanged = true;
1327
	}
1328
1329
	public function getAllianceBBLink() {
1330
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1331
	}
1332
1333
	public function getAllianceDisplayName($linked = false, $includeAllianceID = false) {
1334
		if ($this->hasAlliance()) {
1335
			return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1336
		} else {
1337
			return 'No Alliance';
1338
		}
1339
	}
1340
1341
	public function getAllianceRole($allianceID = false) {
1342
		if ($allianceID === false) {
1343
			$allianceID = $this->getAllianceID();
1344
		}
1345
		if (!isset($this->allianceRoles[$allianceID])) {
1346
			$this->allianceRoles[$allianceID] = 0;
1347
			$this->db->query('SELECT role_id
1348
						FROM player_has_alliance_role
1349
						WHERE ' . $this->SQL . '
1350
						AND alliance_id=' . $this->db->escapeNumber($allianceID) . '
1351
						LIMIT 1');
1352
			if ($this->db->nextRecord()) {
1353
				$this->allianceRoles[$allianceID] = $this->db->getInt('role_id');
1354
			}
1355
		}
1356
		return $this->allianceRoles[$allianceID];
1357
	}
1358
1359
	public function leaveAlliance(AbstractSmrPlayer $kickedBy = null) {
1360
		$allianceID = $this->getAllianceID();
1361
		$alliance = $this->getAlliance();
1362
		if ($kickedBy != null) {
1363
			$kickedBy->sendMessage($this->getAccountID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1364
			$this->actionTaken('PlayerKicked', array('Alliance' => $alliance, 'Player' => $kickedBy));
1365
			$kickedBy->actionTaken('KickPlayer', array('Alliance' => $alliance, 'Player' => $this));
1366
		} elseif ($this->isAllianceLeader()) {
1367
			$this->actionTaken('DisbandAlliance', array('Alliance' => $alliance));
1368
		} else {
1369
			$this->actionTaken('LeaveAlliance', array('Alliance' => $alliance));
1370
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1371
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I left your alliance!', false);
1372
			}
1373
		}
1374
1375
		$this->setAllianceID(0);
1376
		$this->db->query('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1377
1378
		if (!$this->isAllianceLeader() && $allianceID != NHA_ID) { // Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1379
			$this->setAllianceJoinable(TIME + self::TIME_FOR_ALLIANCE_SWITCH);
1380
			$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.
1381
		}
1382
	}
1383
1384
	/**
1385
	 * Join an alliance (used for both Leader and New Member roles)
1386
	 */
1387
	public function joinAlliance($allianceID) {
1388
		$this->setAllianceID($allianceID);
1389
		$alliance = $this->getAlliance();
1390
1391
		if (!$this->isAllianceLeader()) {
1392
			// Do not throw an exception if the NHL account doesn't exist.
1393
			try {
1394
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I joined your alliance!', false);
1395
			} catch (AccountNotFoundException $e) {
1396
				if ($alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1397
					throw $e;
1398
				}
1399
			}
1400
1401
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1402
		} else {
1403
			$roleID = ALLIANCE_ROLE_LEADER;
1404
		}
1405
		$this->db->query('INSERT INTO player_has_alliance_role (game_id, account_id, role_id, alliance_id) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($roleID) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
1406
1407
		$this->actionTaken('JoinAlliance', array('Alliance' => $alliance));
1408
	}
1409
1410
	public function getAllianceJoinable() {
1411
		return $this->allianceJoinable;
1412
	}
1413
1414
	private function setAllianceJoinable($time) {
1415
		if ($this->allianceJoinable == $time) {
1416
			return;
1417
		}
1418
		$this->allianceJoinable = $time;
1419
		$this->hasChanged = true;
1420
	}
1421
1422
	/**
1423
	 * Invites player with $accountID to this player's alliance.
1424
	 */
1425
	public function sendAllianceInvitation(int $accountID, string $message, int $expires) : void {
1426
		if (!$this->hasAlliance()) {
1427
			throw new Exception('Must be in an alliance to send alliance invitations');
1428
		}
1429
		// Send message to invited player
1430
		$messageID = $this->sendMessage($accountID, MSG_PLAYER, $message, false, true, $expires, true);
1431
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), $messageID, $expires);
1432
	}
1433
1434
	public function isCombatDronesKamikazeOnMines() {
1435
		return $this->combatDronesKamikazeOnMines;
1436
	}
1437
1438
	public function setCombatDronesKamikazeOnMines($bool) {
1439
		if ($this->combatDronesKamikazeOnMines == $bool) {
1440
			return;
1441
		}
1442
		$this->combatDronesKamikazeOnMines = $bool;
1443
		$this->hasChanged = true;
1444
	}
1445
1446
	protected function getPersonalRelationsData() {
1447
		if (!isset($this->personalRelations)) {
1448
			//get relations
1449
			$RACES = Globals::getRaces();
1450
			$this->personalRelations = array();
1451
			foreach ($RACES as $raceID => $raceName) {
1452
				$this->personalRelations[$raceID] = 0;
1453
			}
1454
			$this->db->query('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL . ' LIMIT ' . count($RACES));
1455
			while ($this->db->nextRecord()) {
1456
				$this->personalRelations[$this->db->getInt('race_id')] = $this->db->getInt('relation');
1457
			}
1458
		}
1459
	}
1460
1461
	public function getPersonalRelations() {
1462
		$this->getPersonalRelationsData();
1463
		return $this->personalRelations;
1464
	}
1465
1466
	/**
1467
	 * Get personal relations with a race
1468
	 */
1469
	public function getPersonalRelation($raceID) {
1470
		$rels = $this->getPersonalRelations();
1471
		return $rels[$raceID];
1472
	}
1473
1474
	/**
1475
	 * Get total relations with all races (personal + political)
1476
	 */
1477
	public function getRelations() {
1478
		if (!isset($this->relations)) {
1479
			//get relations
1480
			$RACES = Globals::getRaces();
1481
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1482
			$personalRels = $this->getPersonalRelations(); // make sure they're initialised.
1483
			$this->relations = array();
1484
			foreach ($RACES as $raceID => $raceName) {
1485
				$this->relations[$raceID] = $personalRels[$raceID] + $raceRelations[$raceID];
1486
			}
1487
		}
1488
		return $this->relations;
1489
	}
1490
1491
	/**
1492
	 * Get total relations with a race (personal + political)
1493
	 */
1494
	public function getRelation($raceID) {
1495
		$rels = $this->getRelations();
1496
		return $rels[$raceID];
1497
	}
1498
1499
	/**
1500
	 * Increases personal relations from trading $numGoods units with the race
1501
	 * of the port given by $raceID.
1502
	 */
1503
	public function increaseRelationsByTrade($numGoods, $raceID) {
1504
		$relations = ICeil(min($numGoods, 300) / 30);
1505
		//Cap relations to a max of 1 after 500 have been reached
1506
		if ($this->getPersonalRelation($raceID) + $relations >= 500) {
1507
			$relations = max(1, min($relations, 500 - $this->getPersonalRelation($raceID)));
1508
		}
1509
		$this->increaseRelations($relations, $raceID);
1510
	}
1511
1512
	/**
1513
	 * Decreases personal relations from trading failures, e.g. rejected
1514
	 * bargaining and getting caught stealing.
1515
	 */
1516
	public function decreaseRelationsByTrade($numGoods, $raceID) {
1517
		$relations = ICeil(min($numGoods, 300) / 30);
1518
		$this->decreaseRelations($relations, $raceID);
1519
	}
1520
1521
	/**
1522
	 * Increase personal relations.
1523
	 */
1524
	public function increaseRelations($relations, $raceID) {
1525
		if ($relations < 0) {
1526
			throw new Exception('Trying to increase negative relations.');
1527
		}
1528
		if ($relations == 0) {
1529
			return;
1530
		}
1531
		$relations += $this->getPersonalRelation($raceID);
1532
		$this->setRelations($relations, $raceID);
1533
	}
1534
1535
	/**
1536
	 * Decrease personal relations.
1537
	 */
1538
	public function decreaseRelations($relations, $raceID) {
1539
		if ($relations < 0) {
1540
			throw new Exception('Trying to decrease negative relations.');
1541
		}
1542
		if ($relations == 0) {
1543
			return;
1544
		}
1545
		$relations = $this->getPersonalRelation($raceID) - $relations;
1546
		$this->setRelations($relations, $raceID);
1547
	}
1548
1549
	/**
1550
	 * Set personal relations.
1551
	 */
1552
	public function setRelations($relations, $raceID) {
1553
		$this->getRelations();
1554
		if ($this->personalRelations[$raceID] == $relations) {
1555
			return;
1556
		}
1557
		if ($relations < MIN_RELATIONS) {
1558
			$relations = MIN_RELATIONS;
1559
		}
1560
		$relationsDiff = IRound($relations - $this->personalRelations[$raceID]);
1561
		$this->personalRelations[$raceID] = $relations;
1562
		$this->relations[$raceID] += $relationsDiff;
1563
		$this->db->query('REPLACE INTO player_has_relation (account_id,game_id,race_id,relation) values (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($raceID) . ',' . $this->db->escapeNumber($this->personalRelations[$raceID]) . ')');
1564
	}
1565
1566
	/**
1567
	 * Set any starting personal relations bonuses or penalties.
1568
	 */
1569
	public function giveStartingRelations() {
1570
		if ($this->getRaceID() === RACE_ALSKANT) {
1571
			// Give Alskants 250 personal relations to start.
1572
			foreach (Globals::getRaces() as $raceID => $raceInfo) {
1573
				$this->setRelations(250, $raceID);
1574
			}
1575
		}
1576
	}
1577
1578
	public function getLastNewsUpdate() {
1579
		return $this->lastNewsUpdate;
1580
	}
1581
1582
	private function setLastNewsUpdate($time) {
1583
		if ($this->lastNewsUpdate == $time) {
1584
			return;
1585
		}
1586
		$this->lastNewsUpdate = $time;
1587
		$this->hasChanged = true;
1588
	}
1589
1590
	public function updateLastNewsUpdate() {
1591
		$this->setLastNewsUpdate(TIME);
1592
	}
1593
1594
	public function getLastPort() {
1595
		return $this->lastPort;
1596
	}
1597
1598
	public function setLastPort($lastPort) {
1599
		if ($this->lastPort == $lastPort) {
1600
			return;
1601
		}
1602
		$this->lastPort = $lastPort;
1603
		$this->hasChanged = true;
1604
	}
1605
1606
	public function getPlottedCourse() {
1607
		if (!isset($this->plottedCourse)) {
1608
			// check if we have a course plotted
1609
			$this->db->query('SELECT course FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1610
1611
			if ($this->db->nextRecord()) {
1612
				// get the course back
1613
				$this->plottedCourse = unserialize($this->db->getField('course'));
1614
			} else {
1615
				$this->plottedCourse = false;
1616
			}
1617
		}
1618
1619
		// Update the plotted course if we have moved since the last query
1620
		if ($this->plottedCourse !== false && (!isset($this->plottedCourseFrom) || $this->plottedCourseFrom != $this->getSectorID())) {
1621
			$this->plottedCourseFrom = $this->getSectorID();
1622
1623
			if ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1624
				// We have walked into the next sector of the course
1625
				$this->plottedCourse->followPath();
1626
				$this->setPlottedCourse($this->plottedCourse);
1627
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1628
				// We have skipped to some later sector in the course
1629
				$this->plottedCourse->skipToSector($this->getSectorID());
1630
				$this->setPlottedCourse($this->plottedCourse);
1631
			}
1632
		}
1633
		return $this->plottedCourse;
1634
	}
1635
1636
	public function setPlottedCourse(Distance $plottedCourse) {
1637
		$hadPlottedCourse = $this->hasPlottedCourse();
1638
		$this->plottedCourse = $plottedCourse;
1639
		if ($this->plottedCourse->getTotalSectors() > 0) {
1640
			$this->db->query('REPLACE INTO player_plotted_course
1641
				(account_id, game_id, course)
1642
				VALUES(' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeBinary(serialize($this->plottedCourse)) . ')');
1643
		} elseif ($hadPlottedCourse) {
1644
			$this->deletePlottedCourse();
1645
		}
1646
	}
1647
1648
	public function hasPlottedCourse() {
1649
		return $this->getPlottedCourse() !== false;
1650
	}
1651
1652
	public function isPartOfCourse($sectorOrSectorID) {
1653
		if (!$this->hasPlottedCourse()) {
1654
			return false;
1655
		}
1656
		if ($sectorOrSectorID instanceof SmrSector) {
1657
			$sectorID = $sectorOrSectorID->getSectorID();
1658
		} else {
1659
			$sectorID = $sectorOrSectorID;
1660
		}
1661
		return $this->getPlottedCourse()->isInPath($sectorID);
1662
	}
1663
1664
	public function deletePlottedCourse() {
1665
		$this->plottedCourse = false;
1666
		$this->db->query('DELETE FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1667
	}
1668
1669
	// Computes the turn cost and max misjump between current and target sector
1670
	public function getJumpInfo(SmrSector $targetSector) {
1671
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1672
		if ($path === false) {
1673
			create_error('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1674
		}
1675
		$distance = $path->getRelativeDistance();
1676
1677
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
1678
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1679
		return array('turn_cost' => $turnCost, 'max_misjump' => $maxMisjump);
1680
	}
1681
1682
	public function __sleep() {
1683
		return array('accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName');
1684
	}
1685
1686
	public function &getStoredDestinations() {
1687
		if (!isset($this->storedDestinations)) {
1688
			$this->storedDestinations = array();
1689
			$this->db->query('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1690
			while ($this->db->nextRecord()) {
1691
				$this->storedDestinations[] = array(
1692
					'Label' => $this->db->getField('label'),
1693
					'SectorID' => $this->db->getInt('sector_id'),
1694
					'OffsetTop' => $this->db->getInt('offset_top'),
1695
					'OffsetLeft' => $this->db->getInt('offset_left')
1696
				);
1697
			}
1698
		}
1699
		return $this->storedDestinations;
1700
	}
1701
1702
	public function moveDestinationButton($sectorID, $offsetTop, $offsetLeft) {
1703
1704
		if (!is_numeric($offsetLeft) || !is_numeric($offsetTop)) {
1705
			create_error('The position of the saved sector must be numeric!.');
1706
		}
1707
		$offsetTop = round($offsetTop);
1708
		$offsetLeft = round($offsetLeft);
1709
1710
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1711
			create_error('The saved sector must be in the box!');
1712
		}
1713
1714
		$storedDestinations =& $this->getStoredDestinations();
1715
		foreach ($storedDestinations as &$sd) {
1716
			if ($sd['SectorID'] == $sectorID) {
1717
				$sd['OffsetTop'] = $offsetTop;
1718
				$sd['OffsetLeft'] = $offsetLeft;
1719
				$this->db->query('
1720
					UPDATE player_stored_sector
1721
						SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1722
					WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID)
1723
				);
1724
				return true;
1725
			}
1726
		}
1727
1728
		create_error('You do not have a saved sector for #' . $sectorID);
1729
	}
1730
1731
	public function addDestinationButton($sectorID, $label) {
1732
1733
		if (!is_numeric($sectorID) || !SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1734
			create_error('You want to add a non-existent sector?');
1735
		}
1736
1737
		// sector already stored ?
1738
		foreach ($this->getStoredDestinations() as $sd) {
1739
			if ($sd['SectorID'] == $sectorID) {
1740
				create_error('Sector already stored!');
1741
			}
1742
		}
1743
1744
		$this->storedDestinations[] = array(
1745
			'Label' => $label,
1746
			'SectorID' => (int)$sectorID,
1747
			'OffsetTop' => 1,
1748
			'OffsetLeft' => 1
1749
		);
1750
1751
		$this->db->query('
1752
			INSERT INTO player_stored_sector (account_id, game_id, sector_id, label, offset_top, offset_left)
1753
			VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($sectorID) . ',' . $this->db->escapeString($label, true) . ',1,1)'
1754
		);
1755
	}
1756
1757
	public function deleteDestinationButton($sectorID) {
1758
		if (!is_numeric($sectorID) || $sectorID < 1) {
1759
			create_error('You want to remove a non-existent sector?');
1760
		}
1761
1762
		foreach ($this->getStoredDestinations() as $key => $sd) {
1763
			if ($sd['SectorID'] == $sectorID) {
1764
				$this->db->query('
1765
					DELETE FROM player_stored_sector
1766
					WHERE ' . $this->SQL . '
1767
					AND sector_id = ' . $this->db->escapeNumber($sectorID)
1768
				);
1769
				unset($this->storedDestinations[$key]);
1770
				return true;
1771
			}
1772
		}
1773
		return false;
1774
	}
1775
1776
	public function getTickers() {
1777
		if (!isset($this->tickers)) {
1778
			$this->tickers = array();
1779
			//get ticker info
1780
			$this->db->query('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(TIME));
1781
			while ($this->db->nextRecord()) {
1782
				$this->tickers[$this->db->getField('type')] = [
1783
					'Type' => $this->db->getField('type'),
1784
					'Time' => $this->db->getInt('time'),
1785
					'Expires' => $this->db->getInt('expires'),
1786
					'Recent' => $this->db->getField('recent'),
1787
				];
1788
			}
1789
		}
1790
		return $this->tickers;
1791
	}
1792
1793
	public function hasTickers() {
1794
		return count($this->getTickers()) > 0;
1795
	}
1796
1797
	public function getTicker($tickerType) {
1798
		$tickers = $this->getTickers();
1799
		if (isset($tickers[$tickerType])) {
1800
			return $tickers[$tickerType];
1801
		}
1802
		return false;
1803
	}
1804
1805
	public function hasTicker($tickerType) {
1806
		return $this->getTicker($tickerType) !== false;
1807
	}
1808
1809
	public function &shootPlayer(AbstractSmrPlayer $targetPlayer) {
1810
		return $this->getShip()->shootPlayer($targetPlayer);
1811
	}
1812
1813
	public function &shootForces(SmrForce $forces) {
1814
		return $this->getShip()->shootForces($forces);
1815
	}
1816
1817
	public function &shootPort(SmrPort $port) {
1818
		return $this->getShip()->shootPort($port);
1819
	}
1820
1821
	public function &shootPlanet(SmrPlanet $planet, $delayed) {
1822
		return $this->getShip()->shootPlanet($planet, $delayed);
1823
	}
1824
1825
	public function &shootPlayers(array $targetPlayers) {
1826
		return $this->getShip()->shootPlayers($targetPlayers);
1827
	}
1828
1829
	public function getMilitaryPayment() {
1830
		return $this->militaryPayment;
1831
	}
1832
1833
	public function hasMilitaryPayment() {
1834
		return $this->getMilitaryPayment() > 0;
1835
	}
1836
1837
	public function setMilitaryPayment($amount) {
1838
		if ($this->militaryPayment == $amount) {
1839
			return;
1840
		}
1841
		$this->militaryPayment = $amount;
1842
		$this->hasChanged = true;
1843
	}
1844
1845
	public function increaseMilitaryPayment($amount) {
1846
		if ($amount < 0) {
1847
			throw new Exception('Trying to increase negative military payment.');
1848
		}
1849
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1850
	}
1851
1852
	public function decreaseMilitaryPayment($amount) {
1853
		if ($amount < 0) {
1854
			throw new Exception('Trying to decrease negative military payment.');
1855
		}
1856
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1857
	}
1858
1859
	protected function getBountiesData() {
1860
		if (!isset($this->bounties)) {
1861
			$this->bounties = array();
1862
			$this->db->query('SELECT * FROM bounty WHERE ' . $this->SQL);
1863
			while ($this->db->nextRecord()) {
1864
				$this->bounties[$this->db->getInt('bounty_id')] = array(
1865
							'Amount' => $this->db->getInt('amount'),
1866
							'SmrCredits' => $this->db->getInt('smr_credits'),
1867
							'Type' => $this->db->getField('type'),
1868
							'Claimer' => $this->db->getInt('claimer_id'),
1869
							'Time' => $this->db->getInt('time'),
1870
							'ID' => $this->db->getInt('bounty_id'),
1871
							'New' => false);
1872
			}
1873
		}
1874
	}
1875
1876
	// Get bounties that can be claimed by this player
1877
	// Type must be 'HQ' or 'UG'
1878
	public function getClaimableBounties($type) {
1879
		$bounties = array();
1880
		$this->db->query('SELECT * FROM bounty WHERE claimer_id=' . $this->db->escapeNumber($this->getAccountID()) . ' AND game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND type=' . $this->db->escapeString($type));
1881
		while ($this->db->nextRecord()) {
1882
			$bounties[] = array(
1883
				'player' => SmrPlayer::getPlayer($this->db->getInt('account_id'), $this->getGameID()),
1884
				'bounty_id' => $this->db->getInt('bounty_id'),
1885
				'credits' => $this->db->getInt('amount'),
1886
				'smr_credits' => $this->db->getInt('smr_credits'),
1887
			);
1888
		}
1889
		return $bounties;
1890
	}
1891
1892
	public function getBounties() : array {
1893
		$this->getBountiesData();
1894
		return $this->bounties;
1895
	}
1896
1897
	public function hasBounties() : bool {
1898
		return count($this->getBounties()) > 0;
1899
	}
1900
1901
	protected function getBounty(int $bountyID) : array {
1902
		if (!$this->hasBounty($bountyID)) {
1903
			throw new Exception('BountyID does not exist: ' . $bountyID);
1904
		}
1905
		return $this->bounties[$bountyID];
1906
	}
1907
1908
	public function hasBounty(int $bountyID) : bool {
1909
		$bounties = $this->getBounties();
1910
		return isset($bounties[$bountyID]);
1911
	}
1912
1913
	protected function getBountyAmount(int $bountyID) : int {
1914
		$bounty = $this->getBounty($bountyID);
1915
		return $bounty['Amount'];
1916
	}
1917
1918
	protected function createBounty(string $type) : array {
1919
		$bounty = array('Amount' => 0,
1920
						'SmrCredits' => 0,
1921
						'Type' => $type,
1922
						'Claimer' => 0,
1923
						'Time' => TIME,
1924
						'ID' => $this->getNextBountyID(),
1925
						'New' => true);
1926
		$this->setBounty($bounty);
1927
		return $bounty;
1928
	}
1929
1930
	protected function getNextBountyID() : int {
1931
		$keys = array_keys($this->getBounties());
1932
		if (count($keys) > 0) {
1933
			return max($keys) + 1;
1934
		} else {
1935
			return 0;
1936
		}
1937
	}
1938
1939
	protected function setBounty(array $bounty) : void {
1940
		$this->bounties[$bounty['ID']] = $bounty;
1941
		$this->hasBountyChanged[$bounty['ID']] = true;
1942
	}
1943
1944
	protected function setBountyAmount(int $bountyID, int $amount) : void {
1945
		$bounty = $this->getBounty($bountyID);
1946
		$bounty['Amount'] = $amount;
1947
		$this->setBounty($bounty);
1948
	}
1949
1950
	public function getCurrentBounty(string $type) : array {
1951
		$bounties = $this->getBounties();
1952
		foreach ($bounties as $bounty) {
1953
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1954
				return $bounty;
1955
			}
1956
		}
1957
		return $this->createBounty($type);
1958
	}
1959
1960
	public function hasCurrentBounty(string $type) : bool {
1961
		$bounties = $this->getBounties();
1962
		foreach ($bounties as $bounty) {
1963
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1964
				return true;
1965
			}
1966
		}
1967
		return false;
1968
	}
1969
1970
	protected function getCurrentBountyAmount(string $type) : int {
1971
		$bounty = $this->getCurrentBounty($type);
1972
		return $bounty['Amount'];
1973
	}
1974
1975
	protected function setCurrentBountyAmount(string $type, int $amount) : void {
1976
		$bounty = $this->getCurrentBounty($type);
1977
		if ($bounty['Amount'] == $amount) {
1978
			return;
1979
		}
1980
		$bounty['Amount'] = $amount;
1981
		$this->setBounty($bounty);
1982
	}
1983
1984
	public function increaseCurrentBountyAmount(string $type, int $amount) : void {
1985
		if ($amount < 0) {
1986
			throw new Exception('Trying to increase negative current bounty.');
1987
		}
1988
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) + $amount);
1989
	}
1990
1991
	public function decreaseCurrentBountyAmount(string $type, int $amount) : void {
1992
		if ($amount < 0) {
1993
			throw new Exception('Trying to decrease negative current bounty.');
1994
		}
1995
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) - $amount);
1996
	}
1997
1998
	protected function getCurrentBountySmrCredits(string $type) : int {
1999
		$bounty = $this->getCurrentBounty($type);
2000
		return $bounty['SmrCredits'];
2001
	}
2002
2003
	protected function setCurrentBountySmrCredits(string $type, int $credits) : void {
2004
		$bounty = $this->getCurrentBounty($type);
2005
		if ($bounty['SmrCredits'] == $credits) {
2006
			return;
2007
		}
2008
		$bounty['SmrCredits'] = $credits;
2009
		$this->setBounty($bounty);
2010
	}
2011
2012
	public function increaseCurrentBountySmrCredits(string $type, int $credits) : void {
2013
		if ($credits < 0) {
2014
			throw new Exception('Trying to increase negative current bounty.');
2015
		}
2016
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) + $credits);
2017
	}
2018
2019
	public function decreaseCurrentBountySmrCredits(string $type, int $credits) : void {
2020
		if ($credits < 0) {
2021
			throw new Exception('Trying to decrease negative current bounty.');
2022
		}
2023
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) - $credits);
2024
	}
2025
2026
	public function setBountiesClaimable(AbstractSmrPlayer $claimer) : void {
2027
		foreach ($this->getBounties() as $bounty) {
2028
			if ($bounty['Claimer'] == 0) {
2029
				$bounty['Claimer'] = $claimer->getAccountID();
2030
				$this->setBounty($bounty);
2031
			}
2032
		}
2033
	}
2034
2035
	protected function getHOFData() {
2036
		if (!isset($this->HOF)) {
2037
			//Get Player HOF
2038
			$this->db->query('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2039
			$this->HOF = array();
2040
			while ($this->db->nextRecord()) {
2041
				$hof =& $this->HOF;
2042
				$typeList = explode(':', $this->db->getField('type'));
2043
				foreach ($typeList as $type) {
2044
					if (!isset($hof[$type])) {
2045
						$hof[$type] = array();
2046
					}
2047
					$hof =& $hof[$type];
2048
				}
2049
				$hof = $this->db->getFloat('amount');
2050
			}
2051
			self::getHOFVis();
2052
		}
2053
	}
2054
2055
	public static function getHOFVis() {
2056
		if (!isset(self::$HOFVis)) {
2057
			//Get Player HOF Vis
2058
			$db = new SmrMySqlDatabase();
2059
			$db->query('SELECT type,visibility FROM hof_visibility');
2060
			self::$HOFVis = array();
2061
			while ($db->nextRecord()) {
2062
				self::$HOFVis[$db->getField('type')] = $db->getField('visibility');
2063
			}
2064
		}
2065
	}
2066
2067
	public function getHOF(array $typeList = null) {
2068
		$this->getHOFData();
2069
		if ($typeList == null) {
2070
			return $this->HOF;
2071
		}
2072
		$hof = $this->HOF;
2073
		foreach ($typeList as $type) {
2074
			if (!isset($hof[$type])) {
2075
				return 0;
2076
			}
2077
			$hof = $hof[$type];
2078
		}
2079
		return $hof;
2080
	}
2081
2082
	public function increaseHOF($amount, array $typeList, $visibility) {
2083
		if ($amount < 0) {
2084
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2085
		}
2086
		if ($amount == 0) {
2087
			return;
2088
		}
2089
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2090
	}
2091
2092
	public function decreaseHOF($amount, array $typeList, $visibility) {
2093
		if ($amount < 0) {
2094
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2095
		}
2096
		if ($amount == 0) {
2097
			return;
2098
		}
2099
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2100
	}
2101
2102
	public function setHOF($amount, array $typeList, $visibility) {
2103
		if (is_array($this->getHOF($typeList))) {
2104
			throw new Exception('Trying to overwrite a HOF type: ' . implode(':', $typeList));
2105
		}
2106
		if ($this->isNPC()) {
2107
			// Don't store HOF for NPCs.
2108
			return;
2109
		}
2110
		if ($this->getHOF($typeList) == $amount) {
2111
			return;
2112
		}
2113
		if ($amount < 0) {
2114
			$amount = 0;
2115
		}
2116
		$this->getHOF();
2117
2118
		$hofType = implode(':', $typeList);
2119
		if (!isset(self::$HOFVis[$hofType])) {
2120
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2121
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2122
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2123
		}
2124
		self::$HOFVis[$hofType] = $visibility;
2125
2126
		$hof =& $this->HOF;
2127
		$hofChanged =& $this->hasHOFChanged;
2128
		$new = false;
2129
		foreach ($typeList as $type) {
2130
			if (!isset($hofChanged[$type])) {
2131
				$hofChanged[$type] = array();
2132
			}
2133
			if (!isset($hof[$type])) {
2134
				$hof[$type] = array();
2135
				$new = true;
2136
			}
2137
			$hof =& $hof[$type];
2138
			$hofChanged =& $hofChanged[$type];
2139
		}
2140
		if ($hofChanged == null) {
2141
			$hofChanged = self::HOF_CHANGED;
2142
			if ($new) {
2143
				$hofChanged = self::HOF_NEW;
2144
			}
2145
		}
2146
		$hof = $amount;
2147
	}
2148
2149
	public function getExperienceRank() {
2150
		return $this->computeRanking('experience', $this->getExperience());
2151
	}
2152
2153
	public function getKillsRank() {
2154
		return $this->computeRanking('kills', $this->getKills());
2155
	}
2156
2157
	public function getDeathsRank() {
2158
		return $this->computeRanking('deaths', $this->getDeaths());
2159
	}
2160
2161
	public function getAssistsRank() {
2162
		return $this->computeRanking('assists', $this->getAssists());
2163
	}
2164
2165
	private function computeRanking($dbField, $playerAmount) {
2166
		$this->db->query('SELECT count(*) FROM player
2167
			WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . '
2168
			AND (
2169
				'.$dbField . ' > ' . $this->db->escapeNumber($playerAmount) . '
2170
				OR (
2171
					'.$dbField . ' = ' . $this->db->escapeNumber($playerAmount) . '
2172
					AND player_name <= ' . $this->db->escapeString($this->getPlayerName()) . '
2173
				)
2174
			)');
2175
		$this->db->nextRecord();
2176
		$rank = $this->db->getInt('count(*)');
2177
		return $rank;
2178
	}
2179
2180
	public function killPlayer($sectorID) {
2181
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2182
		//msg taken care of in trader_att_proc.php
2183
		// forget plotted course
2184
		$this->deletePlottedCourse();
2185
2186
		$sector->diedHere($this);
2187
2188
		// if we are in an alliance we increase their deaths
2189
		if ($this->hasAlliance()) {
2190
			$this->db->query('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2191
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()) . ' LIMIT 1');
2192
		}
2193
2194
		// record death stat
2195
		$this->increaseHOF(1, array('Dying', 'Deaths'), HOF_PUBLIC);
2196
		//record cost of ship lost
2197
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2198
		// reset turns since last death
2199
		$this->setHOF(0, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2200
2201
		// 1/4 of ship value -> insurance
2202
		$newCredits = IRound($this->getShip()->getCost() / 4);
2203
		if ($newCredits < 100000) {
2204
			$newCredits = 100000;
2205
		}
2206
		$this->setCredits($newCredits);
2207
2208
		$this->setSectorID($this->getHome());
2209
		$this->increaseDeaths(1);
2210
		$this->setLandedOnPlanet(false);
2211
		$this->setDead(true);
2212
		$this->setNewbieWarning(true);
2213
		$this->getShip()->getPod($this->hasNewbieStatus());
2214
		$this->setNewbieTurns(100);
2215
	}
2216
2217
	public function &killPlayerByPlayer(AbstractSmrPlayer $killer) {
2218
		$return = array();
2219
		$msg = $this->getBBLink();
2220
2221
		if ($this->hasCustomShipName()) {
2222
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2223
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2224
		}
2225
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2226
		if ($killer->hasCustomShipName()) {
2227
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2228
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2229
		}
2230
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2231
		$this->getSector()->increaseBattles(1);
2232
		$this->db->query('INSERT INTO news (game_id,time,news_message,type,killer_id,killer_alliance,dead_id,dead_alliance) VALUES (' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber(TIME) . ',' . $this->db->escapeString($msg, true) . ',\'regular\',' . $this->db->escapeNumber($killer->getAccountID()) . ',' . $this->db->escapeNumber($killer->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2233
2234
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2235
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getAccountID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2236
2237
		// Dead player loses between 5% and 25% experience
2238
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2239
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2240
		$this->decreaseExperience($return['DeadExp']);
2241
2242
		// Killer gains 50% of the lost exp
2243
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
2244
		$killer->increaseExperience($return['KillerExp']);
2245
2246
		$return['KillerCredits'] = $this->getCredits();
2247
		$killer->increaseCredits($return['KillerCredits']);
2248
2249
		// The killer may change alignment
2250
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2251
		$relation = $relations[$killer->getRaceID()];
2252
2253
		$alignChangePerRelation = 0.1;
2254
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2255
			$alignChangePerRelation = 0.04;
2256
		}
2257
2258
		$return['KillerAlign'] = -$relation * $alignChangePerRelation; //Lose relations when killing a peaceful race
2259
		if ($return['KillerAlign'] > 0) {
2260
			$killer->increaseAlignment($return['KillerAlign']);
2261
		} else {
2262
			$killer->decreaseAlignment(-$return['KillerAlign']);
2263
		}
2264
		// War setting gives them military pay
2265
		if ($relation <= RELATIONS_WAR) {
2266
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2267
		}
2268
2269
		//check for federal bounty being offered for current port raiders;
2270
		$this->db->query('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(TIME - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2271
		$query = 'SELECT 1
2272
					FROM player_attacks_port
2273
					JOIN port USING(game_id, sector_id)
2274
					JOIN player USING(game_id, account_id)
2275
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2276
		$this->db->query($query);
2277
		if ($this->db->nextRecord()) {
2278
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2279
			$this->increaseCurrentBountyAmount('HQ', $bounty);
2280
		}
2281
2282
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2283
		$this->setBountiesClaimable($killer);
2284
2285
		// If the alignment difference is greater than 200 then a bounty may be set
2286
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2287
		$return['BountyGained'] = array(
2288
			'Type' => 'None',
2289
			'Amount' => 0
2290
		);
2291
		if ($alignmentDiff >= 200) {
2292
			// If the podded players alignment makes them deputy or member then set bounty
2293
			if ($this->getAlignment() >= 100) {
2294
				$return['BountyGained']['Type'] = 'HQ';
2295
			} elseif ($this->getAlignment() <= 100) {
2296
				$return['BountyGained']['Type'] = 'UG';
2297
			}
2298
2299
			if ($return['BountyGained']['Type'] != 'None') {
2300
				$return['BountyGained']['Amount'] = IFloor(pow($alignmentDiff, 2.56));
2301
				$killer->increaseCurrentBountyAmount($return['BountyGained']['Type'], $return['BountyGained']['Amount']);
2302
			}
2303
		}
2304
2305
		if ($this->isNPC()) {
2306
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'NPC', 'Experience', 'Gained'), HOF_PUBLIC);
2307
			$killer->increaseHOF($this->getExperience(), array('Killing', 'NPC', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2308
2309
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By NPCs Killed'), HOF_PUBLIC);
2310
2311
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2312
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Gain'), HOF_PUBLIC);
2313
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'NPC', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2314
2315
			if ($return['KillerAlign'] > 0) {
2316
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Gain'), HOF_PUBLIC);
2317
			} else {
2318
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Loss'), HOF_PUBLIC);
2319
			}
2320
2321
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'NPC', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2322
2323
			$killer->increaseHOF(1, array('Killing', 'NPC Kills'), HOF_PUBLIC);
2324
		} else {
2325
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'Experience', 'Gained'), HOF_PUBLIC);
2326
			$killer->increaseHOF($this->getExperience(), array('Killing', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2327
2328
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By Traders Killed'), HOF_PUBLIC);
2329
2330
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2331
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Gain'), HOF_PUBLIC);
2332
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2333
2334
			if ($return['KillerAlign'] > 0) {
2335
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'Alignment', 'Gain'), HOF_PUBLIC);
2336
			} else {
2337
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'Alignment', 'Loss'), HOF_PUBLIC);
2338
			}
2339
2340
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2341
2342
			if ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2343
				$killer->increaseHOF(1, array('Killing', 'Newbie Kills'), HOF_PUBLIC);
2344
			} else {
2345
				$killer->increaseKills(1);
2346
				$killer->increaseHOF(1, array('Killing', 'Kills'), HOF_PUBLIC);
2347
2348
				if ($killer->hasAlliance()) {
2349
					$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');
2350
				}
2351
2352
				// alliance vs. alliance stats
2353
				$this->incrementAllianceVsDeaths($killer->getAllianceID());
2354
			}
2355
		}
2356
2357
		$this->increaseHOF($return['BountyGained']['Amount'], array('Dying', 'Players', 'Money', 'Bounty Gained By Killer'), HOF_PUBLIC);
2358
		$this->increaseHOF($return['KillerExp'], array('Dying', 'Players', 'Experience', 'Gained By Killer'), HOF_PUBLIC);
2359
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2360
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Players', 'Experience', 'Lost'), HOF_PUBLIC);
2361
		$this->increaseHOF($return['KillerCredits'], array('Dying', 'Players', 'Money Lost'), HOF_PUBLIC);
2362
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Players', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2363
		$this->increaseHOF(1, array('Dying', 'Players', 'Deaths'), HOF_PUBLIC);
2364
2365
		$this->killPlayer($this->getSectorID());
2366
		return $return;
2367
	}
2368
2369
	public function &killPlayerByForces(SmrForce $forces) {
2370
		$return = array();
2371
		$owner = $forces->getOwner();
2372
		// send a message to the person who died
2373
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getAccountID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2374
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2375
2376
		$news_message = $this->getBBLink();
2377
		if ($this->hasCustomShipName()) {
2378
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2379
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2380
		}
2381
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2382
		// insert the news entry
2383
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2384
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($owner->getAccountID()) . ',' . $this->db->escapeNumber($owner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2385
2386
		// Player loses 15% experience
2387
		$expLossPercentage = .15;
2388
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
2389
		$this->decreaseExperience($return['DeadExp']);
2390
2391
		$return['LostCredits'] = $this->getCredits();
2392
2393
		// alliance vs. alliance stats
2394
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2395
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2396
2397
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2398
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Forces', 'Experience Lost'), HOF_PUBLIC);
2399
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Forces', 'Money Lost'), HOF_PUBLIC);
2400
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Forces', 'Cost Of Ships Lost'), HOF_PUBLIC);
2401
		$this->increaseHOF(1, array('Dying', 'Forces', 'Deaths'), HOF_PUBLIC);
2402
2403
		$this->killPlayer($forces->getSectorID());
2404
		return $return;
2405
	}
2406
2407
	public function &killPlayerByPort(SmrPort $port) {
2408
		$return = array();
2409
		// send a message to the person who died
2410
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2411
2412
		$news_message = $this->getBBLink();
2413
		if ($this->hasCustomShipName()) {
2414
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2415
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2416
		}
2417
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2418
		// insert the news entry
2419
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,dead_id,dead_alliance)
2420
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber(ACCOUNT_ID_PORT) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2421
2422
		// Player loses between 15% and 20% experience
2423
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2424
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2425
		$this->decreaseExperience($return['DeadExp']);
2426
2427
		$return['LostCredits'] = $this->getCredits();
2428
2429
		// alliance vs. alliance stats
2430
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2431
2432
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2433
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Ports', 'Experience Lost'), HOF_PUBLIC);
2434
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Ports', 'Money Lost'), HOF_PUBLIC);
2435
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Ports', 'Cost Of Ships Lost'), HOF_PUBLIC);
2436
		$this->increaseHOF(1, array('Dying', 'Ports', 'Deaths'), HOF_PUBLIC);
2437
2438
		$this->killPlayer($port->getSectorID());
2439
		return $return;
2440
	}
2441
2442
	public function &killPlayerByPlanet(SmrPlanet $planet) {
2443
		$return = array();
2444
		// send a message to the person who died
2445
		$planetOwner = $planet->getOwner();
2446
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getAccountID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2447
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2448
2449
		$news_message = $this->getBBLink();
2450
		if ($this->hasCustomShipName()) {
2451
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2452
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2453
		}
2454
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2455
		// insert the news entry
2456
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2457
						VALUES(' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber(TIME) . ', ' . $this->db->escapeString($news_message) . ',' . $this->db->escapeNumber($planetOwner->getAccountID()) . ',' . $this->db->escapeNumber($planetOwner->getAllianceID()) . ',' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ')');
2458
2459
		// Player loses between 15% and 20% experience
2460
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2461
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2462
		$this->decreaseExperience($return['DeadExp']);
2463
2464
		$return['LostCredits'] = $this->getCredits();
2465
2466
		// alliance vs. alliance stats
2467
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2468
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2469
2470
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2471
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Planets', 'Experience Lost'), HOF_PUBLIC);
2472
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Planets', 'Money Lost'), HOF_PUBLIC);
2473
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Planets', 'Cost Of Ships Lost'), HOF_PUBLIC);
2474
		$this->increaseHOF(1, array('Dying', 'Planets', 'Deaths'), HOF_PUBLIC);
2475
2476
		$this->killPlayer($planet->getSectorID());
2477
		return $return;
2478
	}
2479
2480
	public function incrementAllianceVsKills($otherID) {
2481
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2482
		$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');
2483
	}
2484
2485
	public function incrementAllianceVsDeaths($otherID) {
2486
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2487
		$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');
2488
	}
2489
2490
	public function getTurnsLevel() {
2491
		if (!$this->hasTurns()) {
2492
			return 'NONE';
2493
		}
2494
		if ($this->getTurns() <= 25) {
2495
			return 'LOW';
2496
		}
2497
		if ($this->getTurns() <= 75) {
2498
			return 'MEDIUM';
2499
		}
2500
		return 'HIGH';
2501
	}
2502
2503
	/**
2504
	 * Returns the CSS class color to use when displaying the player's turns
2505
	 */
2506
	public function getTurnsColor() {
2507
		switch ($this->getTurnsLevel()) {
2508
			case 'NONE':
2509
			case 'LOW':
2510
				return 'red';
2511
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
2512
			case 'MEDIUM':
2513
				return 'yellow';
2514
			break;
2515
			default:
2516
				return 'green';
2517
		}
2518
	}
2519
2520
	public function getTurns() {
2521
		return $this->turns;
2522
	}
2523
2524
	public function hasTurns() {
2525
		return $this->turns > 0;
2526
	}
2527
2528
	public function getMaxTurns() {
2529
		return $this->getGame()->getMaxTurns();
2530
	}
2531
2532
	public function setTurns($turns) {
2533
		if ($this->turns == $turns) {
2534
			return;
2535
		}
2536
		// Make sure turns are in range [0, MaxTurns]
2537
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2538
		$this->hasChanged = true;
2539
	}
2540
2541
	public function takeTurns($take, $takeNewbie = 0) {
2542
		if ($take < 0 || $takeNewbie < 0) {
2543
			throw new Exception('Trying to take negative turns.');
2544
		}
2545
		$take = ICeil($take);
2546
		// Only take up to as many newbie turns as we have remaining
2547
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2548
2549
		$this->setTurns($this->getTurns() - $take);
2550
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2551
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2552
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Total'), HOF_ALLIANCE);
2553
		$this->increaseHOF($takeNewbie, array('Movement', 'Turns Used', 'Newbie'), HOF_ALLIANCE);
2554
2555
		// Player has taken an action
2556
		$this->setLastActive(TIME);
2557
		$this->updateLastCPLAction();
2558
	}
2559
2560
	public function giveTurns(int $give, $giveNewbie = 0) {
2561
		if ($give < 0 || $giveNewbie < 0) {
2562
			throw new Exception('Trying to give negative turns.');
2563
		}
2564
		$this->setTurns($this->getTurns() + $give);
2565
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2566
	}
2567
2568
	/**
2569
	 * Calculate the time in seconds between the given time and when the
2570
	 * player will be at max turns.
2571
	 */
2572
	public function getTimeUntilMaxTurns($time, $forceUpdate = false) {
2573
		$timeDiff = $time - $this->getLastTurnUpdate();
2574
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2575
		$ship = $this->getShip($forceUpdate);
2576
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
2577
		// If already at max turns, return 0
2578
		return max(0, $maxTurnsTime);
2579
	}
2580
2581
	/**
2582
	 * Grant the player their starting turns.
2583
	 */
2584
	public function giveStartingTurns() {
2585
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
2586
		$this->giveTurns($startTurns);
2587
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2588
	}
2589
2590
	// Turns only update when player is active.
2591
	// Calculate turns gained between given time and the last turn update
2592
	public function getTurnsGained($time, $forceUpdate = false) : int {
2593
		$timeDiff = $time - $this->getLastTurnUpdate();
2594
		$ship = $this->getShip($forceUpdate);
2595
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
2596
		return $extraTurns;
2597
	}
2598
2599
	public function updateTurns() {
2600
		// is account validated?
2601
		if (!$this->getAccount()->isValidated()) {
2602
			return;
2603
		}
2604
2605
		// how many turns would he get right now?
2606
		$extraTurns = $this->getTurnsGained(TIME);
2607
2608
		// do we have at least one turn to give?
2609
		if ($extraTurns > 0) {
2610
			// recalc the time to avoid rounding errors
2611
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
2612
			$this->setLastTurnUpdate($newLastTurnUpdate);
2613
			$this->giveTurns($extraTurns);
2614
		}
2615
	}
2616
2617
	public function getLastTurnUpdate() {
2618
		return $this->lastTurnUpdate;
2619
	}
2620
2621
	public function setLastTurnUpdate($time) {
2622
		if ($this->lastTurnUpdate == $time) {
2623
			return;
2624
		}
2625
		$this->lastTurnUpdate = $time;
2626
		$this->hasChanged = true;
2627
	}
2628
2629
	public function getLastActive() {
2630
		return $this->lastActive;
2631
	}
2632
2633
	public function setLastActive($lastActive) {
2634
		if ($this->lastActive == $lastActive) {
2635
			return;
2636
		}
2637
		$this->lastActive = $lastActive;
2638
		$this->hasChanged = true;
2639
	}
2640
2641
	public function getLastCPLAction() {
2642
		return $this->lastCPLAction;
2643
	}
2644
2645
	public function setLastCPLAction($time) {
2646
		if ($this->lastCPLAction == $time) {
2647
			return;
2648
		}
2649
		$this->lastCPLAction = $time;
2650
		$this->hasChanged = true;
2651
	}
2652
2653
	public function updateLastCPLAction() {
2654
		$this->setLastCPLAction(TIME);
2655
	}
2656
2657
	public function setNewbieWarning($bool) {
2658
		if ($this->newbieWarning == $bool) {
2659
			return;
2660
		}
2661
		$this->newbieWarning = $bool;
2662
		$this->hasChanged = true;
2663
	}
2664
2665
	public function getNewbieWarning() {
2666
		return $this->newbieWarning;
2667
	}
2668
2669
	public function isDisplayMissions() {
2670
		return $this->displayMissions;
2671
	}
2672
2673
	public function setDisplayMissions($bool) {
2674
		if ($this->displayMissions == $bool) {
2675
			return;
2676
		}
2677
		$this->displayMissions = $bool;
2678
		$this->hasChanged = true;
2679
	}
2680
2681
	public function getMissions() {
2682
		if (!isset($this->missions)) {
2683
			$this->db->query('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2684
			$this->missions = array();
2685
			while ($this->db->nextRecord()) {
2686
				$missionID = $this->db->getInt('mission_id');
2687
				$this->missions[$missionID] = array(
2688
					'On Step' => $this->db->getInt('on_step'),
2689
					'Progress' => $this->db->getInt('progress'),
2690
					'Unread' => $this->db->getBoolean('unread'),
2691
					'Expires' => $this->db->getInt('step_fails'),
2692
					'Sector' => $this->db->getInt('mission_sector'),
2693
					'Starting Sector' => $this->db->getInt('starting_sector')
2694
				);
2695
				$this->rebuildMission($missionID);
2696
			}
2697
		}
2698
		return $this->missions;
2699
	}
2700
2701
	public function getActiveMissions() {
2702
		$missions = $this->getMissions();
2703
		foreach ($missions as $missionID => $mission) {
2704
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2705
				unset($missions[$missionID]);
2706
			}
2707
		}
2708
		return $missions;
2709
	}
2710
2711
	protected function getMission($missionID) {
2712
		$missions = $this->getMissions();
2713
		if (isset($missions[$missionID])) {
2714
			return $missions[$missionID];
2715
		}
2716
		return false;
2717
	}
2718
2719
	protected function hasMission($missionID) {
2720
		return $this->getMission($missionID) !== false;
2721
	}
2722
2723
	protected function updateMission($missionID) {
2724
		$this->getMissions();
2725
		if (isset($this->missions[$missionID])) {
2726
			$mission = $this->missions[$missionID];
2727
			$this->db->query('
2728
				UPDATE player_has_mission
2729
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2730
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2731
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2732
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2733
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2734
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2735
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1'
2736
			);
2737
			return true;
2738
		}
2739
		return false;
2740
	}
2741
2742
	private function setupMissionStep($missionID) {
2743
		$mission =& $this->missions[$missionID];
2744
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2745
			// Nothing to do if this mission is already completed
2746
			return;
2747
		}
2748
		$step = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2749
		if (isset($step['PickSector'])) {
2750
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2751
			if ($realX === false) {
2752
				throw new Exception('Invalid PickSector definition in mission: ' . $missionID);
2753
			}
2754
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2755
			if ($path === false) {
2756
				// Abandon the mission if it cannot be completed due to a
2757
				// sector that does not exist or cannot be reached.
2758
				// (Probably shouldn't bestow this mission in the first place)
2759
				$this->deleteMission($missionID);
2760
				create_error('Cannot find a path to the destination!');
2761
			}
2762
			$mission['Sector'] = $path->getEndSectorID();
2763
		}
2764
	}
2765
2766
	/**
2767
	 * Declining a mission will permanently hide it from the player
2768
	 * by adding it in its completed state.
2769
	 */
2770
	public function declineMission($missionID) {
2771
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2772
		$this->addMission($missionID, $finishedStep);
2773
	}
2774
2775
	public function addMission($missionID, $step = 0) {
2776
		$this->getMissions();
2777
2778
		if (isset($this->missions[$missionID])) {
2779
			return;
2780
		}
2781
		$sector = 0;
2782
2783
		$mission = array(
2784
			'On Step' => $step,
2785
			'Progress' => 0,
2786
			'Unread' => true,
2787
			'Expires' => (TIME + 86400),
2788
			'Sector' => $sector,
2789
			'Starting Sector' => $this->getSectorID()
2790
		);
2791
2792
		$this->missions[$missionID] =& $mission;
2793
		$this->setupMissionStep($missionID);
2794
		$this->rebuildMission($missionID);
2795
2796
		$this->db->query('
2797
			REPLACE INTO player_has_mission (game_id,account_id,mission_id,on_step,progress,unread,starting_sector,mission_sector,step_fails)
2798
			VALUES ('.$this->db->escapeNumber($this->gameID) . ',' . $this->db->escapeNumber($this->accountID) . ',' . $this->db->escapeNumber($missionID) . ',' . $this->db->escapeNumber($mission['On Step']) . ',' . $this->db->escapeNumber($mission['Progress']) . ',' . $this->db->escapeBoolean($mission['Unread']) . ',' . $this->db->escapeNumber($mission['Starting Sector']) . ',' . $this->db->escapeNumber($mission['Sector']) . ',' . $this->db->escapeNumber($mission['Expires']) . ')'
2799
		);
2800
	}
2801
2802
	private function rebuildMission($missionID) {
2803
		$mission = $this->missions[$missionID];
2804
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2805
2806
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2807
			// If we have completed this mission just use false to indicate no current task.
2808
			$currentStep = false;
2809
		} else {
2810
			$data = ['player' => $this, 'mission' => $mission];
2811
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2812
			array_walk_recursive($currentStep, 'replaceMissionTemplate', $data);
2813
		}
2814
		$this->missions[$missionID]['Task'] = $currentStep;
2815
	}
2816
2817
	public function deleteMission($missionID) {
2818
		$this->getMissions();
2819
		if (isset($this->missions[$missionID])) {
2820
			unset($this->missions[$missionID]);
2821
			$this->db->query('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1');
2822
			return true;
2823
		}
2824
		return false;
2825
	}
2826
2827
	public function markMissionsRead() {
2828
		$this->getMissions();
2829
		$unreadMissions = array();
2830
		foreach ($this->missions as $missionID => &$mission) {
2831
			if ($mission['Unread']) {
2832
				$unreadMissions[] = $missionID;
2833
				$mission['Unread'] = false;
2834
				$this->updateMission($missionID);
2835
			}
2836
		}
2837
		return $unreadMissions;
2838
	}
2839
2840
	public function claimMissionReward($missionID) {
2841
		$this->getMissions();
2842
		$mission =& $this->missions[$missionID];
2843
		if ($mission === false) {
2844
			throw new Exception('Unknown mission: ' . $missionID);
2845
		}
2846
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2847
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2848
		}
2849
		$mission['On Step']++;
2850
		$mission['Unread'] = true;
2851
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2852
			switch ($rewardItem) {
2853
				case 'Credits':
2854
					$this->increaseCredits($amount);
2855
				break;
2856
				case 'Experience':
2857
					$this->increaseExperience($amount);
2858
				break;
2859
			}
2860
		}
2861
		$rewardText = $mission['Task']['Rewards']['Text'];
2862
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2863
			// If we haven't finished the mission yet then 
2864
			$this->setupMissionStep($missionID);
2865
		}
2866
		$this->rebuildMission($missionID);
2867
		$this->updateMission($missionID);
2868
		return $rewardText;
2869
	}
2870
2871
	public function getAvailableMissions() {
2872
		$availableMissions = array();
2873
		foreach (MISSIONS as $missionID => $mission) {
2874
			if ($this->hasMission($missionID)) {
2875
				continue;
2876
			}
2877
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2878
			if ($realX === false) {
2879
				throw new Exception('Invalid HasX definition in mission: ' . $missionID);
2880
			}
2881
			if ($this->getSector()->hasX($realX)) {
2882
				$availableMissions[$missionID] = $mission;
2883
			}
2884
		}
2885
		return $availableMissions;
2886
	}
2887
2888
	public function actionTaken($actionID, array $values) {
2889
		if (!in_array($actionID, MISSION_ACTIONS)) {
2890
			throw new Exception('Unknown action: ' . $actionID);
2891
		}
2892
// TODO: Reenable this once tested.		if($this->getAccount()->isLoggingEnabled())
2893
			switch ($actionID) {
2894
				case 'WalkSector':
2895
					$this->getAccount()->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID(), $this->getSectorID());
2896
				break;
2897
				case 'JoinAlliance':
2898
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName(), $this->getSectorID());
2899
				break;
2900
				case 'LeaveAlliance':
2901
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName(), $this->getSectorID());
2902
				break;
2903
				case 'DisbandAlliance':
2904
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName(), $this->getSectorID());
2905
				break;
2906
				case 'KickPlayer':
2907
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName(), 0);
2908
				break;
2909
				case 'PlayerKicked':
2910
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')', 0);
2911
				break;
2912
2913
			}
2914
		$this->getMissions();
2915
		foreach ($this->missions as $missionID => &$mission) {
2916
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
2917
				$requirements = $mission['Task']['Detail'];
2918
				if (checkMissionRequirements($values, $requirements) === true) {
2919
					$mission['On Step']++;
2920
					$mission['Unread'] = true;
2921
					$this->setupMissionStep($missionID);
2922
					$this->rebuildMission($missionID);
2923
					$this->updateMission($missionID);
2924
				}
2925
			}
2926
		}
2927
	}
2928
2929
	public function canSeeAny(array $otherPlayerArray) {
2930
		foreach ($otherPlayerArray as $otherPlayer) {
2931
			if ($this->canSee($otherPlayer)) {
2932
				return true;
2933
			}
2934
		}
2935
		return false;
2936
	}
2937
2938
	public function canSee(AbstractSmrPlayer $otherPlayer) {
2939
		if (!$otherPlayer->getShip()->isCloaked()) {
2940
			return true;
2941
		}
2942
		if ($this->sameAlliance($otherPlayer)) {
2943
			return true;
2944
		}
2945
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
2946
			return true;
2947
		}
2948
		return false;
2949
	}
2950
2951
	public function equals(AbstractSmrPlayer $otherPlayer = null) {
2952
		return $otherPlayer !== null && $this->getAccountID() == $otherPlayer->getAccountID() && $this->getGameID() == $otherPlayer->getGameID();
2953
	}
2954
2955
	public function sameAlliance(AbstractSmrPlayer $otherPlayer = null) {
2956
		return $this->equals($otherPlayer) || (!is_null($otherPlayer) && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
2957
	}
2958
2959
	public function sharedForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2960
		return $this->sameAlliance($otherPlayer);
2961
	}
2962
2963
	public function forceNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2964
		return $this->sameAlliance($otherPlayer);
2965
	}
2966
2967
	public function planetNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2968
		return $this->sameAlliance($otherPlayer);
2969
	}
2970
2971
	public function traderNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2972
		return $this->sameAlliance($otherPlayer);
2973
	}
2974
2975
	public function traderMAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2976
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
2977
	}
2978
2979
	public function traderAttackTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2980
		return $this->sameAlliance($otherPlayer);
2981
	}
2982
2983
	public function traderDefendTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2984
		return $this->sameAlliance($otherPlayer);
2985
	}
2986
2987
	public function traderAttackForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2988
		return $this->sameAlliance($otherPlayer);
2989
	}
2990
2991
	public function traderAttackPortAlliance(AbstractSmrPlayer $otherPlayer = null) {
2992
		return $this->sameAlliance($otherPlayer);
2993
	}
2994
2995
	public function traderAttackPlanetAlliance(AbstractSmrPlayer $otherPlayer = null) {
2996
		return $this->sameAlliance($otherPlayer);
2997
	}
2998
2999
	public function meetsAlignmentRestriction($restriction) {
3000
		if ($restriction < 0) {
3001
			return $this->getAlignment() <= $restriction;
3002
		}
3003
		if ($restriction > 0) {
3004
			return $this->getAlignment() >= $restriction;
3005
		}
3006
		return true;
3007
	}
3008
3009
	// Get an array of goods that are visible to the player
3010
	public function getVisibleGoods() {
3011
		$goods = Globals::getGoods();
3012
		$visibleGoods = array();
3013
		foreach ($goods as $key => $good) {
3014
			if ($this->meetsAlignmentRestriction($good['AlignRestriction'])) {
3015
				$visibleGoods[$key] = $good;
3016
			}
3017
		}
3018
		return $visibleGoods;
3019
	}
3020
3021
	/**
3022
	 * Will retrieve all visited sectors, use only when you are likely to check a large number of these
3023
	 */
3024
	public function hasVisitedSector($sectorID) {
3025
		if (!isset($this->visitedSectors)) {
3026
			$this->visitedSectors = array();
3027
			$this->db->query('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
3028
			while ($this->db->nextRecord()) {
3029
				$this->visitedSectors[$this->db->getInt('sector_id')] = false;
3030
			}
3031
		}
3032
		return !isset($this->visitedSectors[$sectorID]);
3033
	}
3034
3035
	public function getLeaveNewbieProtectionHREF() {
3036
		return SmrSession::getNewHREF(create_container('leave_newbie_processing.php'));
3037
	}
3038
3039
	public function getExamineTraderHREF() {
3040
		$container = create_container('skeleton.php', 'trader_examine.php');
3041
		$container['target'] = $this->getAccountID();
3042
		return SmrSession::getNewHREF($container);
3043
	}
3044
3045
	public function getAttackTraderHREF() {
3046
		return Globals::getAttackTraderHREF($this->getAccountID());
3047
	}
3048
3049
	public function getPlanetKickHREF() {
3050
		$container = create_container('planet_kick_processing.php', 'trader_attack_processing.php');
3051
		$container['account_id'] = $this->getAccountID();
3052
		return SmrSession::getNewHREF($container);
3053
	}
3054
3055
	public function getTraderSearchHREF() {
3056
		$container = create_container('skeleton.php', 'trader_search_result.php');
3057
		$container['player_id'] = $this->getPlayerID();
3058
		return SmrSession::getNewHREF($container);
3059
	}
3060
3061
	public function getAllianceRosterHREF() {
3062
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3063
	}
3064
3065
	public function getToggleWeaponHidingHREF($ajax = false) {
3066
		$container = create_container('toggle_processing.php');
3067
		$container['toggle'] = 'WeaponHiding';
3068
		$container['AJAX'] = $ajax;
3069
		return SmrSession::getNewHREF($container);
3070
	}
3071
3072
	public function isDisplayWeapons() {
3073
		return $this->displayWeapons;
3074
	}
3075
3076
	/**
3077
	 * Should weapons be displayed in the right panel?
3078
	 * This updates the player database directly because it is used with AJAX,
3079
	 * which does not acquire a sector lock.
3080
	 */
3081
	public function setDisplayWeapons($bool) {
3082
		if ($this->displayWeapons == $bool) {
3083
			return;
3084
		}
3085
		$this->displayWeapons = $bool;
3086
		$this->db->query('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3087
	}
3088
3089
	public function update() {
3090
		$this->save();
3091
	}
3092
3093
	public function save() {
3094
		if ($this->hasChanged === true) {
3095
			$this->db->query('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3096
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3097
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3098
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3099
				', turns=' . $this->db->escapeNumber($this->turns) .
3100
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3101
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3102
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3103
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3104
				', dead=' . $this->db->escapeBoolean($this->dead) .
3105
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3106
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3107
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3108
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3109
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3110
				', credits=' . $this->db->escapeNumber($this->credits) .
3111
				', experience=' . $this->db->escapeNumber($this->experience) .
3112
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3113
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3114
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3115
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3116
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3117
				', kills=' . $this->db->escapeNumber($this->kills) .
3118
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3119
				', assists=' . $this->db->escapeNumber($this->assists) .
3120
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3121
				', bank=' . $this->db->escapeNumber($this->bank) .
3122
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3123
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3124
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3125
				', group_scout_messages=' . $this->db->escapeString($this->groupScoutMessages) .
3126
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3127
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3128
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3129
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3130
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3131
				' WHERE ' . $this->SQL . ' LIMIT 1');
3132
			$this->hasChanged = false;
3133
		}
3134
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3135
			if ($bountyChanged === true) {
3136
				$bountyChanged = false;
3137
				$bounty = $this->getBounty($key);
3138
				if ($bounty['New'] === true) {
3139
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3140
						$this->db->query('INSERT INTO bounty (account_id,game_id,type,amount,smr_credits,claimer_id,time) VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeString($bounty['Type']) . ',' . $this->db->escapeNumber($bounty['Amount']) . ',' . $this->db->escapeNumber($bounty['SmrCredits']) . ',' . $this->db->escapeNumber($bounty['Claimer']) . ',' . $this->db->escapeNumber($bounty['Time']) . ')');
3141
					}
3142
				} else {
3143
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3144
						$this->db->query('UPDATE bounty
3145
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3146
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3147
							type=' . $this->db->escapeString($bounty['Type']) . ',
3148
							claimer_id=' . $this->db->escapeNumber($bounty['Claimer']) . ',
3149
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3150
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3151
					} else {
3152
						$this->db->query('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3153
					}
3154
				}
3155
			}
3156
		}
3157
		$this->saveHOF();
3158
	}
3159
3160
	public function saveHOF() {
3161
		if (count($this->hasHOFChanged) > 0) {
3162
			$this->doHOFSave($this->hasHOFChanged);
3163
			$this->hasHOFChanged = [];
3164
		}
3165
		if (!empty(self::$hasHOFVisChanged)) {
3166
			foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3167
				if ($changeType == self::HOF_NEW) {
3168
					$this->db->query('INSERT INTO hof_visibility (type, visibility) VALUES (' . $this->db->escapeString($hofType) . ',' . $this->db->escapeString(self::$HOFVis[$hofType]) . ')');
3169
				} else {
3170
					$this->db->query('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType) . ' LIMIT 1');
3171
				}
3172
				unset(self::$hasHOFVisChanged[$hofType]);
3173
			}
3174
		}
3175
	}
3176
3177
	/**
3178
	 * This should only be called by `saveHOF` (and recursively) to
3179
	 * ensure that the `hasHOFChanged` attribute is properly cleared.
3180
	 */
3181
	protected function doHOFSave(array $hasChangedList, array $typeList = array()) {
3182
		foreach ($hasChangedList as $type => $hofChanged) {
3183
			$tempTypeList = $typeList;
3184
			$tempTypeList[] = $type;
3185
			if (is_array($hofChanged)) {
3186
				$this->doHOFSave($hofChanged, $tempTypeList);
3187
			} else {
3188
				$amount = $this->getHOF($tempTypeList);
3189
				if ($hofChanged == self::HOF_NEW) {
3190
					if ($amount > 0) {
3191
						$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) . ')');
3192
					}
3193
				} elseif ($hofChanged == self::HOF_CHANGED) {
3194
					$this->db->query('UPDATE player_hof
3195
						SET amount=' . $this->db->escapeNumber($amount) . '
3196
						WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeArray($tempTypeList, false, true, ':', false) . ' LIMIT 1');
3197
				}
3198
			}
3199
		}
3200
	}
3201
3202
}
3203