Scrutinizer GitHub App not installed

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

Install GitHub App

Completed
Push — master ( 8b2958...09ea2b )
by Dan
31s queued 14s
created

AbstractSmrPlayer::doMessageSending()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 48
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 34
nc 8
nop 8
dl 0
loc 48
rs 8.4426
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php declare(strict_types=1);
2
require_once('missions.inc');
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 $pureRelations;
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
				}
677
			break;
678
		}
679
680
		return $insertID;
681
	}
682
683
	public function sendMessageToBox($boxTypeID, $message) {
684
		// send him the message
685
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
686
	}
687
688
	public function sendGlobalMessage($message, $canBeIgnored = true) {
689
		if ($canBeIgnored) {
690
			if ($this->getAccount()->isMailBanned()) {
691
				create_error('You are currently banned from sending messages');
692
			}
693
		}
694
		$this->sendMessageToBox(BOX_GLOBALS, $message);
695
696
		// send to all online player
697
		$db = new SmrMySqlDatabase();
698
		$db->query('SELECT account_id
699
					FROM active_session
700
					JOIN player USING (game_id, account_id)
701
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(TIME - SmrSession::TIME_BEFORE_EXPIRY) . '
702
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
703
						AND ignore_globals = \'FALSE\'
704
						AND account_id != ' . $db->escapeNumber($this->getAccountID()));
705
706
		while ($db->nextRecord()) {
707
			$this->sendMessage($db->getInt('account_id'), MSG_GLOBAL, $message, $canBeIgnored);
708
		}
709
		$this->sendMessage($this->getAccountID(), MSG_GLOBAL, $message, $canBeIgnored, false);
710
	}
711
712
	public function sendMessage($receiverID, $messageTypeID, $message, $canBeIgnored = true, $unread = true, $expires = false, $senderDelete = false) {
713
		//get expire time
714
		if ($canBeIgnored) {
715
			if ($this->getAccount()->isMailBanned()) {
716
				create_error('You are currently banned from sending messages');
717
			}
718
			// Don't send messages to players ignoring us
719
			$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');
720
			if ($this->db->nextRecord()) {
721
				return;
722
			}
723
		}
724
725
		$message = word_filter($message);
726
727
		// If expires not specified, use default based on message type
728
		if ($expires === false) {
729
			switch ($messageTypeID) {
730
				case MSG_GLOBAL: //We don't send globals to the box here or it gets done loads of times.
731
					$expires = 3600; // 1h
732
				break;
733
				case MSG_PLAYER:
734
					$expires = 86400 * 31;
735
				break;
736
				case MSG_PLANET:
737
					$expires = 86400 * 7;
738
				break;
739
				case MSG_SCOUT:
740
					$expires = 86400 * 3;
741
				break;
742
				case MSG_POLITICAL:
743
					$expires = 86400 * 31;
744
				break;
745
				case MSG_ALLIANCE:
746
					$expires = 86400 * 31;
747
				break;
748
				case MSG_ADMIN:
749
					$expires = 86400 * 365;
750
				break;
751
				case MSG_CASINO:
752
					$expires = 86400 * 31;
753
				break;
754
				default:
755
					$expires = 86400 * 7;
756
			}
757
			$expires += TIME;
758
		}
759
760
		// Do not put scout messages in the sender's sent box
761
		if ($messageTypeID == MSG_SCOUT) {
762
			$senderDelete = true;
763
		}
764
765
		// send him the message and return the message_id
766
		return self::doMessageSending($this->getAccountID(), $receiverID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
767
	}
768
769
	public function sendMessageFromOpAnnounce($receiverID, $message, $expires = false) {
770
		// get expire time if not set
771
		if ($expires === false) {
772
			$expires = TIME + 86400 * 14;
773
		}
774
		self::doMessageSending(ACCOUNT_ID_OP_ANNOUNCE, $receiverID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
775
	}
776
777
	public function sendMessageFromAllianceCommand($receiverID, $message) {
778
		$expires = TIME + 86400 * 365;
779
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_COMMAND, $receiverID, $this->getGameID(), MSG_PLAYER, $message, $expires);
780
	}
781
782
	public static function sendMessageFromPlanet($gameID, $receiverID, $message) {
783
		//get expire time
784
		$expires = TIME + 86400 * 31;
785
		// send him the message
786
		self::doMessageSending(ACCOUNT_ID_PLANET, $receiverID, $gameID, MSG_PLANET, $message, $expires);
787
	}
788
789
	public static function sendMessageFromPort($gameID, $receiverID, $message) {
790
		//get expire time
791
		$expires = TIME + 86400 * 31;
792
		// send him the message
793
		self::doMessageSending(ACCOUNT_ID_PORT, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
794
	}
795
796
	public static function sendMessageFromFedClerk($gameID, $receiverID, $message) {
797
		$expires = TIME + 86400 * 365;
798
		self::doMessageSending(ACCOUNT_ID_FED_CLERK, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
799
	}
800
801
	public static function sendMessageFromAdmin($gameID, $receiverID, $message, $expires = false) {
802
		//get expire time
803
		if ($expires === false) {
804
			$expires = TIME + 86400 * 365;
805
		}
806
		// send him the message
807
		self::doMessageSending(ACCOUNT_ID_ADMIN, $receiverID, $gameID, MSG_ADMIN, $message, $expires);
808
	}
809
810
	public static function sendMessageFromAllianceAmbassador($gameID, $receiverID, $message, $expires = false) {
811
		//get expire time
812
		if ($expires === false) {
813
			$expires = TIME + 86400 * 31;
814
		}
815
		// send him the message
816
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_AMBASSADOR, $receiverID, $gameID, MSG_ALLIANCE, $message, $expires);
817
	}
818
819
	public static function sendMessageFromCasino($gameID, $receiverID, $message, $expires = false) {
820
		//get expire time
821
		if ($expires === false) {
822
			$expires = TIME + 86400 * 7;
823
		}
824
		// send him the message
825
		self::doMessageSending(ACCOUNT_ID_CASINO, $receiverID, $gameID, MSG_CASINO, $message, $expires);
826
	}
827
828
	public static function sendMessageFromRace($raceID, $gameID, $receiverID, $message, $expires = false) {
829
		//get expire time
830
		if ($expires === false) {
831
			$expires = TIME + 86400 * 5;
832
		}
833
		// send him the message
834
		self::doMessageSending(ACCOUNT_ID_GROUP_RACES + $raceID, $receiverID, $gameID, MSG_POLITICAL, $message, $expires);
835
	}
836
837
	public function setMessagesRead($messageTypeID) {
838
		$this->db->query('DELETE FROM player_has_unread_messages
839
							WHERE '.$this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
840
	}
841
842
	public function getSafeAttackRating() {
843
		return max(0, min(8, $this->getAlignment() / 150 + 4));
844
	}
845
846
	public function hasFederalProtection() {
847
		$sector = SmrSector::getSector($this->getGameID(), $this->getSectorID());
848
		if (!$sector->offersFederalProtection()) {
849
			return false;
850
		}
851
852
		$ship = $this->getShip();
853
		if ($ship->hasIllegalGoods()) {
854
			return false;
855
		}
856
857
		if ($ship->getAttackRating() <= $this->getSafeAttackRating()) {
858
			foreach ($sector->getFedRaceIDs() as $fedRaceID) {
859
				if ($this->canBeProtectedByRace($fedRaceID)) {
860
					return true;
861
				}
862
			}
863
		}
864
865
		return false;
866
	}
867
868
	public function canBeProtectedByRace($raceID) {
869
		if (!isset($this->canFed)) {
870
			$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...
871
			$RACES = Globals::getRaces();
872
			foreach ($RACES as $raceID2 => $raceName) {
873
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
874
			}
875
			$this->db->query('SELECT race_id, allowed FROM player_can_fed
876
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(TIME));
877
			while ($this->db->nextRecord()) {
878
				$this->canFed[$this->db->getInt('race_id')] = $this->db->getBoolean('allowed');
879
			}
880
		}
881
		return $this->canFed[$raceID];
882
	}
883
884
	/**
885
	 * Returns a boolean identifying if the player can currently
886
	 * participate in battles.
887
	 */
888
	public function canFight() {
889
		return !($this->hasNewbieTurns() ||
890
		         $this->isDead() ||
891
		         $this->isLandedOnPlanet() ||
892
		         $this->hasFederalProtection());
893
	}
894
895
	public function setDead($bool) {
896
		if ($this->dead == $bool) {
897
			return;
898
		}
899
		$this->dead = $bool;
900
		$this->hasChanged = true;
901
	}
902
903
	public function getKills() {
904
		return $this->kills;
905
	}
906
907
	public function increaseKills($kills) {
908
		if ($kills < 0) {
909
			throw new Exception('Trying to increase negative kills.');
910
		}
911
		$this->setKills($this->kills + $kills);
912
	}
913
914
	public function setKills($kills) {
915
		if ($this->kills == $kills) {
916
			return;
917
		}
918
		$this->kills = $kills;
919
		$this->hasChanged = true;
920
	}
921
922
	public function getDeaths() {
923
		return $this->deaths;
924
	}
925
926
	public function increaseDeaths($deaths) {
927
		if ($deaths < 0) {
928
			throw new Exception('Trying to increase negative deaths.');
929
		}
930
		$this->setDeaths($this->getDeaths() + $deaths);
931
	}
932
933
	public function setDeaths($deaths) {
934
		if ($this->deaths == $deaths) {
935
			return;
936
		}
937
		$this->deaths = $deaths;
938
		$this->hasChanged = true;
939
	}
940
941
	public function getAssists() {
942
		return $this->assists;
943
	}
944
945
	public function increaseAssists($assists) {
946
		if ($assists < 1) {
947
			throw new Exception('Must increase by a positive number.');
948
		}
949
		$this->assists += $assists;
950
		$this->hasChanged = true;
951
	}
952
953
	public function getAlignment() {
954
		return $this->alignment;
955
	}
956
957
	public function increaseAlignment($align) {
958
		if ($align < 0) {
959
			throw new Exception('Trying to increase negative align.');
960
		}
961
		if ($align == 0) {
962
			return;
963
		}
964
		$align += $this->alignment;
965
		$this->setAlignment($align);
966
	}
967
968
	public function decreaseAlignment($align) {
969
		if ($align < 0) {
970
			throw new Exception('Trying to decrease negative align.');
971
		}
972
		if ($align == 0) {
973
			return;
974
		}
975
		$align = $this->alignment - $align;
976
		$this->setAlignment($align);
977
	}
978
979
	public function setAlignment($align) {
980
		if ($this->alignment == $align) {
981
			return;
982
		}
983
		$this->alignment = $align;
984
		$this->hasChanged = true;
985
	}
986
987
	public function getCredits() {
988
		return $this->credits;
989
	}
990
991
	public function getBank() {
992
		return $this->bank;
993
	}
994
995
	public function increaseBank($credits) {
996
		if ($credits < 0) {
997
			throw new Exception('Trying to increase negative credits.');
998
		}
999
		if ($credits == 0) {
1000
			return;
1001
		}
1002
		$credits += $this->bank;
1003
		$this->setBank($credits);
1004
	}
1005
1006
	public function decreaseBank($credits) {
1007
		if ($credits < 0) {
1008
			throw new Exception('Trying to decrease negative credits.');
1009
		}
1010
		if ($credits == 0) {
1011
			return;
1012
		}
1013
		$credits = $this->bank - $credits;
1014
		$this->setBank($credits);
1015
	}
1016
1017
	public function setBank($credits) {
1018
		if ($this->bank == $credits) {
1019
			return;
1020
		}
1021
		if ($credits < 0) {
1022
			throw new Exception('Trying to set negative credits.');
1023
		}
1024
		if ($credits > MAX_MONEY) {
1025
			throw new Exception('Trying to set more than max credits.');
1026
		}
1027
		$this->bank = $credits;
1028
		$this->hasChanged = true;
1029
	}
1030
1031
	public function getExperience() {
1032
		return $this->experience;
1033
	}
1034
1035
	/**
1036
	 * Returns the percent progress towards the next level.
1037
	 * This value is rounded because it is used primarily in HTML img widths.
1038
	 */
1039
	public function getNextLevelPercentAcquired() : int {
1040
		if ($this->getNextLevelExperience() == $this->getThisLevelExperience()) {
1041
			return 100;
1042
		}
1043
		return max(0, min(100, IRound(($this->getExperience() - $this->getThisLevelExperience()) / ($this->getNextLevelExperience() - $this->getThisLevelExperience()) * 100)));
1044
	}
1045
1046
	public function getNextLevelPercentRemaining() {
1047
		return 100 - $this->getNextLevelPercentAcquired();
1048
	}
1049
1050
	public function getNextLevelExperience() {
1051
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1052
		if (!isset($LEVELS_REQUIREMENTS[$this->getLevelID() + 1])) {
1053
			return $this->getThisLevelExperience(); //Return current level experience if on last level.
1054
		}
1055
		return $LEVELS_REQUIREMENTS[$this->getLevelID() + 1]['Requirement'];
1056
	}
1057
1058
	public function getThisLevelExperience() {
1059
		$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1060
		return $LEVELS_REQUIREMENTS[$this->getLevelID()]['Requirement'];
1061
	}
1062
1063
	public function setExperience($experience) {
1064
		if ($this->experience == $experience) {
1065
			return;
1066
		}
1067
		if ($experience < MIN_EXPERIENCE) {
1068
			$experience = MIN_EXPERIENCE;
1069
		}
1070
		if ($experience > MAX_EXPERIENCE) {
1071
			$experience = MAX_EXPERIENCE;
1072
		}
1073
		$this->experience = $experience;
1074
		$this->hasChanged = true;
1075
1076
		// Since exp has changed, invalidate the player level so that it can
1077
		// be recomputed next time it is queried (in case it has changed).
1078
		$this->level = null;
1079
	}
1080
1081
	public function increaseCredits($credits) {
1082
		if ($credits < 0) {
1083
			throw new Exception('Trying to increase negative credits.');
1084
		}
1085
		if ($credits == 0) {
1086
			return;
1087
		}
1088
		$credits += $this->credits;
1089
		$this->setCredits($credits);
1090
	}
1091
	public function decreaseCredits($credits) {
1092
		if ($credits < 0) {
1093
			throw new Exception('Trying to decrease negative credits.');
1094
		}
1095
		if ($credits == 0) {
1096
			return;
1097
		}
1098
		$credits = $this->credits - $credits;
1099
		$this->setCredits($credits);
1100
	}
1101
	public function setCredits($credits) {
1102
		if ($this->credits == $credits) {
1103
			return;
1104
		}
1105
		if ($credits < 0) {
1106
			throw new Exception('Trying to set negative credits.');
1107
		}
1108
		if ($credits > MAX_MONEY) {
1109
			$credits = MAX_MONEY;
1110
		}
1111
		$this->credits = $credits;
1112
		$this->hasChanged = true;
1113
	}
1114
1115
	public function increaseExperience($experience) {
1116
		if ($experience < 0) {
1117
			throw new Exception('Trying to increase negative experience.');
1118
		}
1119
		if ($experience == 0) {
1120
			return;
1121
		}
1122
		$newExperience = $this->experience + $experience;
1123
		$this->setExperience($newExperience);
1124
		$this->increaseHOF($experience, array('Experience', 'Total', 'Gain'), HOF_PUBLIC);
1125
	}
1126
	public function decreaseExperience($experience) {
1127
		if ($experience < 0) {
1128
			throw new Exception('Trying to decrease negative experience.');
1129
		}
1130
		if ($experience == 0) {
1131
			return;
1132
		}
1133
		$newExperience = $this->experience - $experience;
1134
		$this->setExperience($newExperience);
1135
		$this->increaseHOF($experience, array('Experience', 'Total', 'Loss'), HOF_PUBLIC);
1136
	}
1137
1138
	public function isLandedOnPlanet() {
1139
		return $this->landedOnPlanet;
1140
	}
1141
1142
	public function setLandedOnPlanet($bool) {
1143
		if ($this->landedOnPlanet == $bool) {
1144
			return;
1145
		}
1146
		$this->landedOnPlanet = $bool;
1147
		$this->hasChanged = true;
1148
	}
1149
1150
	/**
1151
	 * Returns the numerical level of the player (e.g. 1-50).
1152
	 */
1153
	public function getLevelID() {
1154
		// The level is cached for performance reasons unless `setExperience`
1155
		// is called and the player's experience changes.
1156
		if ($this->level === null) {
1157
			$LEVELS_REQUIREMENTS = Globals::getLevelRequirements();
1158
			foreach ($LEVELS_REQUIREMENTS as $level_id => $require) {
1159
				if ($this->getExperience() >= $require['Requirement']) {
1160
					continue;
1161
				}
1162
				$this->level = $level_id - 1;
1163
				return $this->level;
1164
			}
1165
			$this->level = max(array_keys($LEVELS_REQUIREMENTS));
1166
		}
1167
		return $this->level;
1168
	}
1169
1170
	public function getLevelName() {
1171
		$level_name = Globals::getLevelRequirements()[$this->getLevelID()]['Name'];
1172
		if ($this->isPresident()) {
1173
			$level_name = '<img src="images/council_president.png" title="' . Globals::getRaceName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1174
		}
1175
		return $level_name;
1176
	}
1177
1178
	public function getMaxLevel() {
1179
		return max(array_keys(Globals::getLevelRequirements()));
1180
	}
1181
1182
	public function getPlayerID() {
1183
		return $this->playerID;
1184
	}
1185
1186
	/**
1187
	 * Returns the player name.
1188
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1189
	 */
1190
	public function getPlayerName() {
1191
		return $this->playerName;
1192
	}
1193
1194
	public function setPlayerName($name) {
1195
		$this->playerName = $name;
1196
		$this->hasChanged = true;
1197
	}
1198
1199
	/**
1200
	 * Returns the decorated player name, suitable for HTML display.
1201
	 */
1202
	public function getDisplayName($includeAlliance = false) {
1203
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1204
		$return = get_colored_text($this->getAlignment(), $name);
1205
		if ($this->isNPC()) {
1206
			$return .= ' <span class="npcColour">[NPC]</span>';
1207
		}
1208
		if ($includeAlliance) {
1209
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1210
		}
1211
		return $return;
1212
	}
1213
1214
	public function getBBLink() {
1215
			return '[player=' . $this->getPlayerID() . ']';
1216
	}
1217
1218
	public function getLinkedDisplayName($includeAlliance = true) {
1219
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1220
		if ($includeAlliance) {
1221
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1222
		}
1223
		return $return;
1224
	}
1225
1226
	/**
1227
	 * Use this method when the player is changing their own name.
1228
	 * This will flag the player as having used their free name change.
1229
	 */
1230
	public function setPlayerNameByPlayer($playerName) {
1231
		$this->setPlayerName($playerName);
1232
		$this->setNameChanged(true);
1233
	}
1234
1235
	public function isNameChanged() {
1236
		return $this->nameChanged;
1237
	}
1238
1239
	public function setNameChanged($bool) {
1240
		$this->nameChanged = $bool;
1241
		$this->hasChanged = true;
1242
	}
1243
1244
	public function isRaceChanged() : bool {
1245
		return $this->raceChanged;
1246
	}
1247
1248
	public function setRaceChanged(bool $raceChanged) : void {
1249
		$this->raceChanged = $raceChanged;
1250
		$this->hasChanged = true;
1251
	}
1252
1253
	public function canChangeRace() : bool {
1254
		return !$this->isRaceChanged() && (TIME - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1255
	}
1256
1257
	public function getRaceID() {
1258
		return $this->raceID;
1259
	}
1260
1261
	public function getRaceName() {
1262
		return Globals::getRaceName($this->getRaceID());
1263
	}
1264
1265
	public static function getColouredRaceNameOrDefault($otherRaceID, AbstractSmrPlayer $player = null, $linked = false) {
1266
		$relations = 0;
1267
		if ($player !== null) {
1268
			$relations = $player->getRelation($otherRaceID);
1269
		}
1270
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1271
	}
1272
1273
	public function getColouredRaceName($otherRaceID, $linked = false) {
1274
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1275
	}
1276
1277
	public function setRaceID($raceID) {
1278
		if ($this->raceID == $raceID) {
1279
			return;
1280
		}
1281
		$this->raceID = $raceID;
1282
		$this->hasChanged = true;
1283
	}
1284
1285
	public function isAllianceLeader($forceUpdate = false) {
1286
		return $this->getAccountID() == $this->getAlliance($forceUpdate)->getLeaderID();
1287
	}
1288
1289
	public function getAlliance($forceUpdate = false) {
1290
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1291
	}
1292
1293
	public function getAllianceID() {
1294
		return $this->allianceID;
1295
	}
1296
1297
	public function hasAlliance() {
1298
		return $this->getAllianceID() != 0;
1299
	}
1300
1301
	protected function setAllianceID($ID) {
1302
		if ($this->allianceID == $ID) {
1303
			return;
1304
		}
1305
		$this->allianceID = $ID;
1306
		if ($this->allianceID != 0) {
1307
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1308
			$this->db->query('INSERT IGNORE INTO player_joined_alliance (account_id,game_id,alliance_id,status) ' .
1309
				'VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1310
		}
1311
		$this->hasChanged = true;
1312
	}
1313
1314
	public function getAllianceBBLink() {
1315
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1316
	}
1317
1318
	public function getAllianceDisplayName($linked = false, $includeAllianceID = false) {
1319
		if ($this->hasAlliance()) {
1320
			return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1321
		} else {
1322
			return 'No Alliance';
1323
		}
1324
	}
1325
1326
	public function getAllianceRole($allianceID = false) {
1327
		if ($allianceID === false) {
1328
			$allianceID = $this->getAllianceID();
1329
		}
1330
		if (!isset($this->allianceRoles[$allianceID])) {
1331
			$this->allianceRoles[$allianceID] = 0;
1332
			$this->db->query('SELECT role_id
1333
						FROM player_has_alliance_role
1334
						WHERE ' . $this->SQL . '
1335
						AND alliance_id=' . $this->db->escapeNumber($allianceID) . '
1336
						LIMIT 1');
1337
			if ($this->db->nextRecord()) {
1338
				$this->allianceRoles[$allianceID] = $this->db->getInt('role_id');
1339
			}
1340
		}
1341
		return $this->allianceRoles[$allianceID];
1342
	}
1343
1344
	public function leaveAlliance(AbstractSmrPlayer $kickedBy = null) {
1345
		$allianceID = $this->getAllianceID();
1346
		$alliance = $this->getAlliance();
1347
		if ($kickedBy != null) {
1348
			$kickedBy->sendMessage($this->getAccountID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1349
			$this->actionTaken('PlayerKicked', array('Alliance' => $alliance, 'Player' => $kickedBy));
1350
			$kickedBy->actionTaken('KickPlayer', array('Alliance' => $alliance, 'Player' => $this));
1351
		} elseif ($this->isAllianceLeader()) {
1352
			$this->actionTaken('DisbandAlliance', array('Alliance' => $alliance));
1353
		} else {
1354
			$this->actionTaken('LeaveAlliance', array('Alliance' => $alliance));
1355
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1356
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I left your alliance!', false);
1357
			}
1358
		}
1359
1360
		$this->setAllianceID(0);
1361
		$this->db->query('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1362
1363
		if (!$this->isAllianceLeader() && $allianceID != NHA_ID) { // Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1364
			$this->setAllianceJoinable(TIME + self::TIME_FOR_ALLIANCE_SWITCH);
1365
			$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.
1366
		}
1367
	}
1368
1369
	/**
1370
	 * Join an alliance (used for both Leader and New Member roles)
1371
	 */
1372
	public function joinAlliance($allianceID) {
1373
		$this->setAllianceID($allianceID);
1374
		$alliance = $this->getAlliance();
1375
1376
		if (!$this->isAllianceLeader()) {
1377
			// Do not throw an exception if the NHL account doesn't exist.
1378
			try {
1379
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I joined your alliance!', false);
1380
			} catch (AccountNotFoundException $e) {
1381
				if ($alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1382
					throw $e;
1383
				}
1384
			}
1385
1386
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1387
		} else {
1388
			$roleID = ALLIANCE_ROLE_LEADER;
1389
		}
1390
		$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()) . ')');
1391
1392
		$this->actionTaken('JoinAlliance', array('Alliance' => $alliance));
1393
	}
1394
1395
	public function getAllianceJoinable() {
1396
		return $this->allianceJoinable;
1397
	}
1398
1399
	private function setAllianceJoinable($time) {
1400
		if ($this->allianceJoinable == $time) {
1401
			return;
1402
		}
1403
		$this->allianceJoinable = $time;
1404
		$this->hasChanged = true;
1405
	}
1406
1407
	/**
1408
	 * Invites player with $accountID to this player's alliance.
1409
	 */
1410
	public function sendAllianceInvitation(int $accountID, string $message, int $expires) : void {
1411
		if (!$this->hasAlliance()) {
1412
			throw new Exception('Must be in an alliance to send alliance invitations');
1413
		}
1414
		// Send message to invited player
1415
		$messageID = $this->sendMessage($accountID, MSG_PLAYER, $message, false, true, $expires, true);
1416
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), $messageID, $expires);
1417
	}
1418
1419
	public function isCombatDronesKamikazeOnMines() {
1420
		return $this->combatDronesKamikazeOnMines;
1421
	}
1422
1423
	public function setCombatDronesKamikazeOnMines($bool) {
1424
		if ($this->combatDronesKamikazeOnMines == $bool) {
1425
			return;
1426
		}
1427
		$this->combatDronesKamikazeOnMines = $bool;
1428
		$this->hasChanged = true;
1429
	}
1430
1431
	protected function getPureRelationsData() {
1432
		if (!isset($this->pureRelations)) {
1433
			//get relations
1434
			$RACES = Globals::getRaces();
1435
			$this->pureRelations = array();
1436
			foreach ($RACES as $raceID => $raceName) {
1437
				$this->pureRelations[$raceID] = 0;
1438
			}
1439
			$this->db->query('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL . ' LIMIT ' . count($RACES));
1440
			while ($this->db->nextRecord()) {
1441
				$this->pureRelations[$this->db->getInt('race_id')] = $this->db->getInt('relation');
1442
			}
1443
		}
1444
	}
1445
1446
	public function getPureRelations() {
1447
		$this->getPureRelationsData();
1448
		return $this->pureRelations;
1449
	}
1450
1451
	/**
1452
	 * Get personal relations with a race
1453
	 */
1454
	public function getPureRelation($raceID) {
1455
		$rels = $this->getPureRelations();
1456
		return $rels[$raceID];
1457
	}
1458
1459
	public function getRelations() {
1460
		if (!isset($this->relations)) {
1461
			//get relations
1462
			$RACES = Globals::getRaces();
1463
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1464
			$pureRels = $this->getPureRelations(); // make sure they're initialised.
1465
			$this->relations = array();
1466
			foreach ($RACES as $raceID => $raceName) {
1467
				$this->relations[$raceID] = $pureRels[$raceID] + $raceRelations[$raceID];
1468
			}
1469
		}
1470
		return $this->relations;
1471
	}
1472
1473
	/**
1474
	 * Get total relations with a race (personal + political)
1475
	 */
1476
	public function getRelation($raceID) {
1477
		$rels = $this->getRelations();
1478
		return $rels[$raceID];
1479
	}
1480
1481
	/**
1482
	 * Increases personal relations from trading $numGoods units with the race
1483
	 * of the port given by $raceID.
1484
	 */
1485
	public function increaseRelationsByTrade($numGoods, $raceID) {
1486
		$relations = ICeil(min($numGoods, 300) / 30);
1487
		//Cap relations to a max of 1 after 500 have been reached
1488
		if ($this->getPureRelation($raceID) + $relations >= 500) {
1489
			$relations = max(1, min($relations, 500 - $this->getPureRelation($raceID)));
1490
		}
1491
		$this->increaseRelations($relations, $raceID);
1492
	}
1493
1494
	public function decreaseRelationsByTrade($numGoods, $raceID) {
1495
		$relations = ICeil(min($numGoods, 300) / 30);
1496
		$this->decreaseRelations($relations, $raceID);
1497
	}
1498
1499
	public function increaseRelations($relations, $raceID) {
1500
		if ($relations < 0) {
1501
			throw new Exception('Trying to increase negative relations.');
1502
		}
1503
		if ($relations == 0) {
1504
			return;
1505
		}
1506
		$relations += $this->getPureRelation($raceID);
1507
		$this->setRelations($relations, $raceID);
1508
	}
1509
1510
	public function decreaseRelations($relations, $raceID) {
1511
		if ($relations < 0) {
1512
			throw new Exception('Trying to decrease negative relations.');
1513
		}
1514
		if ($relations == 0) {
1515
			return;
1516
		}
1517
		$relations = $this->getPureRelation($raceID) - $relations;
1518
		$this->setRelations($relations, $raceID);
1519
	}
1520
1521
	public function setRelations($relations, $raceID) {
1522
		$this->getRelations();
1523
		if ($this->pureRelations[$raceID] == $relations) {
1524
			return;
1525
		}
1526
		if ($relations < MIN_RELATIONS) {
1527
			$relations = MIN_RELATIONS;
1528
		}
1529
		$relationsDiff = IRound($relations - $this->pureRelations[$raceID]);
1530
		$this->pureRelations[$raceID] = $relations;
1531
		$this->relations[$raceID] += $relationsDiff;
1532
		$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->pureRelations[$raceID]) . ')');
1533
	}
1534
1535
	/**
1536
	 * Set any starting personal relations bonuses or penalties.
1537
	 */
1538
	public function giveStartingRelations() {
1539
		if ($this->getRaceID() === RACE_ALSKANT) {
1540
			// Give Alskants 250 personal relations to start.
1541
			foreach (Globals::getRaces() as $raceID => $raceInfo) {
1542
				$this->setRelations(250, $raceID);
1543
			}
1544
		}
1545
	}
1546
1547
	public function getLastNewsUpdate() {
1548
		return $this->lastNewsUpdate;
1549
	}
1550
1551
	private function setLastNewsUpdate($time) {
1552
		if ($this->lastNewsUpdate == $time) {
1553
			return;
1554
		}
1555
		$this->lastNewsUpdate = $time;
1556
		$this->hasChanged = true;
1557
	}
1558
1559
	public function updateLastNewsUpdate() {
1560
		$this->setLastNewsUpdate(TIME);
1561
	}
1562
1563
	public function getLastPort() {
1564
		return $this->lastPort;
1565
	}
1566
1567
	public function setLastPort($lastPort) {
1568
		if ($this->lastPort == $lastPort) {
1569
			return;
1570
		}
1571
		$this->lastPort = $lastPort;
1572
		$this->hasChanged = true;
1573
	}
1574
1575
	public function getPlottedCourse() {
1576
		if (!isset($this->plottedCourse)) {
1577
			// check if we have a course plotted
1578
			$this->db->query('SELECT course FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1579
1580
			if ($this->db->nextRecord()) {
1581
				// get the course back
1582
				$this->plottedCourse = unserialize($this->db->getField('course'));
1583
			} else {
1584
				$this->plottedCourse = false;
1585
			}
1586
		}
1587
1588
		// Update the plotted course if we have moved since the last query
1589
		if ($this->plottedCourse !== false && (!isset($this->plottedCourseFrom) || $this->plottedCourseFrom != $this->getSectorID())) {
1590
			$this->plottedCourseFrom = $this->getSectorID();
1591
1592
			if ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1593
				// We have walked into the next sector of the course
1594
				$this->plottedCourse->followPath();
1595
				$this->setPlottedCourse($this->plottedCourse);
1596
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1597
				// We have skipped to some later sector in the course
1598
				$this->plottedCourse->skipToSector($this->getSectorID());
1599
				$this->setPlottedCourse($this->plottedCourse);
1600
			}
1601
		}
1602
		return $this->plottedCourse;
1603
	}
1604
1605
	public function setPlottedCourse(Distance $plottedCourse) {
1606
		$hadPlottedCourse = $this->hasPlottedCourse();
1607
		$this->plottedCourse = $plottedCourse;
1608
		if ($this->plottedCourse->getTotalSectors() > 0) {
1609
			$this->db->query('REPLACE INTO player_plotted_course
1610
				(account_id, game_id, course)
1611
				VALUES(' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeBinary(serialize($this->plottedCourse)) . ')');
1612
		} elseif ($hadPlottedCourse) {
1613
			$this->deletePlottedCourse();
1614
		}
1615
	}
1616
1617
	public function hasPlottedCourse() {
1618
		return $this->getPlottedCourse() !== false;
1619
	}
1620
1621
	public function isPartOfCourse($sectorOrSectorID) {
1622
		if (!$this->hasPlottedCourse()) {
1623
			return false;
1624
		}
1625
		if ($sectorOrSectorID instanceof SmrSector) {
1626
			$sectorID = $sectorOrSectorID->getSectorID();
1627
		} else {
1628
			$sectorID = $sectorOrSectorID;
1629
		}
1630
		return $this->getPlottedCourse()->isInPath($sectorID);
1631
	}
1632
1633
	public function deletePlottedCourse() {
1634
		$this->plottedCourse = false;
1635
		$this->db->query('DELETE FROM player_plotted_course WHERE ' . $this->SQL . ' LIMIT 1');
1636
	}
1637
1638
	// Computes the turn cost and max misjump between current and target sector
1639
	public function getJumpInfo(SmrSector $targetSector) {
1640
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1641
		if ($path === false) {
1642
			create_error('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1643
		}
1644
		$distance = $path->getRelativeDistance();
1645
1646
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
1647
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1648
		return array('turn_cost' => $turnCost, 'max_misjump' => $maxMisjump);
1649
	}
1650
1651
	public function __sleep() {
1652
		return array('accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName');
1653
	}
1654
1655
	public function &getStoredDestinations() {
1656
		if (!isset($this->storedDestinations)) {
1657
			$this->storedDestinations = array();
1658
			$this->db->query('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1659
			while ($this->db->nextRecord()) {
1660
				$this->storedDestinations[] = array(
1661
					'Label' => $this->db->getField('label'),
1662
					'SectorID' => $this->db->getInt('sector_id'),
1663
					'OffsetTop' => $this->db->getInt('offset_top'),
1664
					'OffsetLeft' => $this->db->getInt('offset_left')
1665
				);
1666
			}
1667
		}
1668
		return $this->storedDestinations;
1669
	}
1670
1671
	public function moveDestinationButton($sectorID, $offsetTop, $offsetLeft) {
1672
1673
		if (!is_numeric($offsetLeft) || !is_numeric($offsetTop)) {
1674
			create_error('The position of the saved sector must be numeric!.');
1675
		}
1676
		$offsetTop = round($offsetTop);
1677
		$offsetLeft = round($offsetLeft);
1678
1679
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1680
			create_error('The saved sector must be in the box!');
1681
		}
1682
1683
		$storedDestinations =& $this->getStoredDestinations();
1684
		foreach ($storedDestinations as &$sd) {
1685
			if ($sd['SectorID'] == $sectorID) {
1686
				$sd['OffsetTop'] = $offsetTop;
1687
				$sd['OffsetLeft'] = $offsetLeft;
1688
				$this->db->query('
1689
					UPDATE player_stored_sector
1690
						SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1691
					WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID)
1692
				);
1693
				return true;
1694
			}
1695
		}
1696
1697
		create_error('You do not have a saved sector for #' . $sectorID);
1698
	}
1699
1700
	public function addDestinationButton($sectorID, $label) {
1701
1702
		if (!is_numeric($sectorID) || !SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1703
			create_error('You want to add a non-existent sector?');
1704
		}
1705
1706
		// sector already stored ?
1707
		foreach ($this->getStoredDestinations() as $sd) {
1708
			if ($sd['SectorID'] == $sectorID) {
1709
				create_error('Sector already stored!');
1710
			}
1711
		}
1712
1713
		$this->storedDestinations[] = array(
1714
			'Label' => $label,
1715
			'SectorID' => (int)$sectorID,
1716
			'OffsetTop' => 1,
1717
			'OffsetLeft' => 1
1718
		);
1719
1720
		$this->db->query('
1721
			INSERT INTO player_stored_sector (account_id, game_id, sector_id, label, offset_top, offset_left)
1722
			VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ', ' . $this->db->escapeNumber($this->getGameID()) . ', ' . $this->db->escapeNumber($sectorID) . ',' . $this->db->escapeString($label, true) . ',1,1)'
1723
		);
1724
	}
1725
1726
	public function deleteDestinationButton($sectorID) {
1727
		if (!is_numeric($sectorID) || $sectorID < 1) {
1728
			create_error('You want to remove a non-existent sector?');
1729
		}
1730
1731
		foreach ($this->getStoredDestinations() as $key => $sd) {
1732
			if ($sd['SectorID'] == $sectorID) {
1733
				$this->db->query('
1734
					DELETE FROM player_stored_sector
1735
					WHERE ' . $this->SQL . '
1736
					AND sector_id = ' . $this->db->escapeNumber($sectorID)
1737
				);
1738
				unset($this->storedDestinations[$key]);
1739
				return true;
1740
			}
1741
		}
1742
		return false;
1743
	}
1744
1745
	public function getTickers() {
1746
		if (!isset($this->tickers)) {
1747
			$this->tickers = array();
1748
			//get ticker info
1749
			$this->db->query('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(TIME));
1750
			while ($this->db->nextRecord()) {
1751
				$this->tickers[$this->db->getField('type')] = [
1752
					'Type' => $this->db->getField('type'),
1753
					'Time' => $this->db->getInt('time'),
1754
					'Expires' => $this->db->getInt('expires'),
1755
					'Recent' => $this->db->getField('recent'),
1756
				];
1757
			}
1758
		}
1759
		return $this->tickers;
1760
	}
1761
1762
	public function hasTickers() {
1763
		return count($this->getTickers()) > 0;
1764
	}
1765
1766
	public function getTicker($tickerType) {
1767
		$tickers = $this->getTickers();
1768
		if (isset($tickers[$tickerType])) {
1769
			return $tickers[$tickerType];
1770
		}
1771
		return false;
1772
	}
1773
1774
	public function hasTicker($tickerType) {
1775
		return $this->getTicker($tickerType) !== false;
1776
	}
1777
1778
	public function &shootPlayer(AbstractSmrPlayer $targetPlayer) {
1779
		return $this->getShip()->shootPlayer($targetPlayer);
1780
	}
1781
1782
	public function &shootForces(SmrForce $forces) {
1783
		return $this->getShip()->shootForces($forces);
1784
	}
1785
1786
	public function &shootPort(SmrPort $port) {
1787
		return $this->getShip()->shootPort($port);
1788
	}
1789
1790
	public function &shootPlanet(SmrPlanet $planet, $delayed) {
1791
		return $this->getShip()->shootPlanet($planet, $delayed);
1792
	}
1793
1794
	public function &shootPlayers(array $targetPlayers) {
1795
		return $this->getShip()->shootPlayers($targetPlayers);
1796
	}
1797
1798
	public function getMilitaryPayment() {
1799
		return $this->militaryPayment;
1800
	}
1801
1802
	public function hasMilitaryPayment() {
1803
		return $this->getMilitaryPayment() > 0;
1804
	}
1805
1806
	public function setMilitaryPayment($amount) {
1807
		if ($this->militaryPayment == $amount) {
1808
			return;
1809
		}
1810
		$this->militaryPayment = $amount;
1811
		$this->hasChanged = true;
1812
	}
1813
1814
	public function increaseMilitaryPayment($amount) {
1815
		if ($amount < 0) {
1816
			throw new Exception('Trying to increase negative military payment.');
1817
		}
1818
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1819
	}
1820
1821
	public function decreaseMilitaryPayment($amount) {
1822
		if ($amount < 0) {
1823
			throw new Exception('Trying to decrease negative military payment.');
1824
		}
1825
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1826
	}
1827
1828
	protected function getBountiesData() {
1829
		if (!isset($this->bounties)) {
1830
			$this->bounties = array();
1831
			$this->db->query('SELECT * FROM bounty WHERE ' . $this->SQL);
1832
			while ($this->db->nextRecord()) {
1833
				$this->bounties[$this->db->getInt('bounty_id')] = array(
1834
							'Amount' => $this->db->getInt('amount'),
1835
							'SmrCredits' => $this->db->getInt('smr_credits'),
1836
							'Type' => $this->db->getField('type'),
1837
							'Claimer' => $this->db->getInt('claimer_id'),
1838
							'Time' => $this->db->getInt('time'),
1839
							'ID' => $this->db->getInt('bounty_id'),
1840
							'New' => false);
1841
			}
1842
		}
1843
	}
1844
1845
	// Get bounties that can be claimed by this player
1846
	// Type must be 'HQ' or 'UG'
1847
	public function getClaimableBounties($type) {
1848
		$bounties = array();
1849
		$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));
1850
		while ($this->db->nextRecord()) {
1851
			$bounties[] = array(
1852
				'player' => SmrPlayer::getPlayer($this->db->getInt('account_id'), $this->getGameID()),
1853
				'bounty_id' => $this->db->getInt('bounty_id'),
1854
				'credits' => $this->db->getInt('amount'),
1855
				'smr_credits' => $this->db->getInt('smr_credits'),
1856
			);
1857
		}
1858
		return $bounties;
1859
	}
1860
1861
	public function getBounties() : array {
1862
		$this->getBountiesData();
1863
		return $this->bounties;
1864
	}
1865
1866
	public function hasBounties() : bool {
1867
		return count($this->getBounties()) > 0;
1868
	}
1869
1870
	protected function getBounty(int $bountyID) : array {
1871
		if (!$this->hasBounty($bountyID)) {
1872
			throw new Exception('BountyID does not exist: ' . $bountyID);
1873
		}
1874
		return $this->bounties[$bountyID];
1875
	}
1876
1877
	public function hasBounty(int $bountyID) : bool {
1878
		$bounties = $this->getBounties();
1879
		return isset($bounties[$bountyID]);
1880
	}
1881
1882
	protected function getBountyAmount(int $bountyID) : int {
1883
		$bounty = $this->getBounty($bountyID);
1884
		return $bounty['Amount'];
1885
	}
1886
1887
	protected function createBounty(string $type) : array {
1888
		$bounty = array('Amount' => 0,
1889
						'SmrCredits' => 0,
1890
						'Type' => $type,
1891
						'Claimer' => 0,
1892
						'Time' => TIME,
1893
						'ID' => $this->getNextBountyID(),
1894
						'New' => true);
1895
		$this->setBounty($bounty);
1896
		return $bounty;
1897
	}
1898
1899
	protected function getNextBountyID() : int {
1900
		$keys = array_keys($this->getBounties());
1901
		if (count($keys) > 0) {
1902
			return max($keys) + 1;
1903
		} else {
1904
			return 0;
1905
		}
1906
	}
1907
1908
	protected function setBounty(array $bounty) : void {
1909
		$this->bounties[$bounty['ID']] = $bounty;
1910
		$this->hasBountyChanged[$bounty['ID']] = true;
1911
	}
1912
1913
	protected function setBountyAmount(int $bountyID, int $amount) : void {
1914
		$bounty = $this->getBounty($bountyID);
1915
		$bounty['Amount'] = $amount;
1916
		$this->setBounty($bounty);
1917
	}
1918
1919
	public function getCurrentBounty(string $type) : array {
1920
		$bounties = $this->getBounties();
1921
		foreach ($bounties as $bounty) {
1922
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1923
				return $bounty;
1924
			}
1925
		}
1926
		return $this->createBounty($type);
1927
	}
1928
1929
	public function hasCurrentBounty(string $type) : bool {
1930
		$bounties = $this->getBounties();
1931
		foreach ($bounties as $bounty) {
1932
			if ($bounty['Claimer'] == 0 && $bounty['Type'] == $type) {
1933
				return true;
1934
			}
1935
		}
1936
		return false;
1937
	}
1938
1939
	protected function getCurrentBountyAmount(string $type) : int {
1940
		$bounty = $this->getCurrentBounty($type);
1941
		return $bounty['Amount'];
1942
	}
1943
1944
	protected function setCurrentBountyAmount(string $type, int $amount) : void {
1945
		$bounty = $this->getCurrentBounty($type);
1946
		if ($bounty['Amount'] == $amount) {
1947
			return;
1948
		}
1949
		$bounty['Amount'] = $amount;
1950
		$this->setBounty($bounty);
1951
	}
1952
1953
	public function increaseCurrentBountyAmount(string $type, int $amount) : void {
1954
		if ($amount < 0) {
1955
			throw new Exception('Trying to increase negative current bounty.');
1956
		}
1957
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) + $amount);
1958
	}
1959
1960
	public function decreaseCurrentBountyAmount(string $type, int $amount) : void {
1961
		if ($amount < 0) {
1962
			throw new Exception('Trying to decrease negative current bounty.');
1963
		}
1964
		$this->setCurrentBountyAmount($type, $this->getCurrentBountyAmount($type) - $amount);
1965
	}
1966
1967
	protected function getCurrentBountySmrCredits(string $type) : int {
1968
		$bounty = $this->getCurrentBounty($type);
1969
		return $bounty['SmrCredits'];
1970
	}
1971
1972
	protected function setCurrentBountySmrCredits(string $type, int $credits) : void {
1973
		$bounty = $this->getCurrentBounty($type);
1974
		if ($bounty['SmrCredits'] == $credits) {
1975
			return;
1976
		}
1977
		$bounty['SmrCredits'] = $credits;
1978
		$this->setBounty($bounty);
1979
	}
1980
1981
	public function increaseCurrentBountySmrCredits(string $type, int $credits) : void {
1982
		if ($credits < 0) {
1983
			throw new Exception('Trying to increase negative current bounty.');
1984
		}
1985
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) + $credits);
1986
	}
1987
1988
	public function decreaseCurrentBountySmrCredits(string $type, int $credits) : void {
1989
		if ($credits < 0) {
1990
			throw new Exception('Trying to decrease negative current bounty.');
1991
		}
1992
		$this->setCurrentBountySmrCredits($type, $this->getCurrentBountySmrCredits($type) - $credits);
1993
	}
1994
1995
	public function setBountiesClaimable(AbstractSmrPlayer $claimer) : void {
1996
		foreach ($this->getBounties() as $bounty) {
1997
			if ($bounty['Claimer'] == 0) {
1998
				$bounty['Claimer'] = $claimer->getAccountID();
1999
				$this->setBounty($bounty);
2000
			}
2001
		}
2002
	}
2003
2004
	protected function getHOFData() {
2005
		if (!isset($this->HOF)) {
2006
			//Get Player HOF
2007
			$this->db->query('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2008
			$this->HOF = array();
2009
			while ($this->db->nextRecord()) {
2010
				$hof =& $this->HOF;
2011
				$typeList = explode(':', $this->db->getField('type'));
2012
				foreach ($typeList as $type) {
2013
					if (!isset($hof[$type])) {
2014
						$hof[$type] = array();
2015
					}
2016
					$hof =& $hof[$type];
2017
				}
2018
				$hof = $this->db->getFloat('amount');
2019
			}
2020
			self::getHOFVis();
2021
		}
2022
	}
2023
2024
	public static function getHOFVis() {
2025
		if (!isset(self::$HOFVis)) {
2026
			//Get Player HOF Vis
2027
			$db = new SmrMySqlDatabase();
2028
			$db->query('SELECT type,visibility FROM hof_visibility');
2029
			self::$HOFVis = array();
2030
			while ($db->nextRecord()) {
2031
				self::$HOFVis[$db->getField('type')] = $db->getField('visibility');
2032
			}
2033
		}
2034
	}
2035
2036
	public function getHOF(array $typeList = null) {
2037
		$this->getHOFData();
2038
		if ($typeList == null) {
2039
			return $this->HOF;
2040
		}
2041
		$hof = $this->HOF;
2042
		foreach ($typeList as $type) {
2043
			if (!isset($hof[$type])) {
2044
				return 0;
2045
			}
2046
			$hof = $hof[$type];
2047
		}
2048
		return $hof;
2049
	}
2050
2051
	public function increaseHOF($amount, array $typeList, $visibility) {
2052
		if ($amount < 0) {
2053
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2054
		}
2055
		if ($amount == 0) {
2056
			return;
2057
		}
2058
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2059
	}
2060
2061
	public function decreaseHOF($amount, array $typeList, $visibility) {
2062
		if ($amount < 0) {
2063
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2064
		}
2065
		if ($amount == 0) {
2066
			return;
2067
		}
2068
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2069
	}
2070
2071
	public function setHOF($amount, array $typeList, $visibility) {
2072
		if (is_array($this->getHOF($typeList))) {
2073
			throw new Exception('Trying to overwrite a HOF type: ' . implode(':', $typeList));
2074
		}
2075
		if ($this->isNPC()) {
2076
			// Don't store HOF for NPCs.
2077
			return;
2078
		}
2079
		if ($this->getHOF($typeList) == $amount) {
2080
			return;
2081
		}
2082
		if ($amount < 0) {
2083
			$amount = 0;
2084
		}
2085
		$this->getHOF();
2086
2087
		$hofType = implode(':', $typeList);
2088
		if (!isset(self::$HOFVis[$hofType])) {
2089
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2090
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2091
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2092
		}
2093
		self::$HOFVis[$hofType] = $visibility;
2094
2095
		$hof =& $this->HOF;
2096
		$hofChanged =& $this->hasHOFChanged;
2097
		$new = false;
2098
		foreach ($typeList as $type) {
2099
			if (!isset($hofChanged[$type])) {
2100
				$hofChanged[$type] = array();
2101
			}
2102
			if (!isset($hof[$type])) {
2103
				$hof[$type] = array();
2104
				$new = true;
2105
			}
2106
			$hof =& $hof[$type];
2107
			$hofChanged =& $hofChanged[$type];
2108
		}
2109
		if ($hofChanged == null) {
2110
			$hofChanged = self::HOF_CHANGED;
2111
			if ($new) {
2112
				$hofChanged = self::HOF_NEW;
2113
			}
2114
		}
2115
		$hof = $amount;
2116
	}
2117
2118
	public function getExperienceRank() {
2119
		return $this->computeRanking('experience', $this->getExperience());
2120
	}
2121
2122
	public function getKillsRank() {
2123
		return $this->computeRanking('kills', $this->getKills());
2124
	}
2125
2126
	public function getDeathsRank() {
2127
		return $this->computeRanking('deaths', $this->getDeaths());
2128
	}
2129
2130
	public function getAssistsRank() {
2131
		return $this->computeRanking('assists', $this->getAssists());
2132
	}
2133
2134
	private function computeRanking($dbField, $playerAmount) {
2135
		$this->db->query('SELECT count(*) FROM player
2136
			WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . '
2137
			AND (
2138
				'.$dbField . ' > ' . $this->db->escapeNumber($playerAmount) . '
2139
				OR (
2140
					'.$dbField . ' = ' . $this->db->escapeNumber($playerAmount) . '
2141
					AND player_name <= ' . $this->db->escapeString($this->getPlayerName()) . '
2142
				)
2143
			)');
2144
		$this->db->nextRecord();
2145
		$rank = $this->db->getInt('count(*)');
2146
		return $rank;
2147
	}
2148
2149
	public function killPlayer($sectorID) {
2150
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2151
		//msg taken care of in trader_att_proc.php
2152
		// forget plotted course
2153
		$this->deletePlottedCourse();
2154
2155
		$sector->diedHere($this);
2156
2157
		// if we are in an alliance we increase their deaths
2158
		if ($this->hasAlliance()) {
2159
			$this->db->query('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2160
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()) . ' LIMIT 1');
2161
		}
2162
2163
		// record death stat
2164
		$this->increaseHOF(1, array('Dying', 'Deaths'), HOF_PUBLIC);
2165
		//record cost of ship lost
2166
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2167
		// reset turns since last death
2168
		$this->setHOF(0, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2169
2170
		// 1/4 of ship value -> insurance
2171
		$newCredits = IRound($this->getShip()->getCost() / 4);
2172
		if ($newCredits < 100000) {
2173
			$newCredits = 100000;
2174
		}
2175
		$this->setCredits($newCredits);
2176
2177
		$this->setSectorID($this->getHome());
2178
		$this->increaseDeaths(1);
2179
		$this->setLandedOnPlanet(false);
2180
		$this->setDead(true);
2181
		$this->setNewbieWarning(true);
2182
		$this->getShip()->getPod($this->hasNewbieStatus());
2183
		$this->setNewbieTurns(100);
2184
	}
2185
2186
	public function &killPlayerByPlayer(AbstractSmrPlayer $killer) {
2187
		$return = array();
2188
		$msg = $this->getBBLink();
2189
2190
		if ($this->hasCustomShipName()) {
2191
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2192
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2193
		}
2194
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2195
		if ($killer->hasCustomShipName()) {
2196
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2197
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2198
		}
2199
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2200
		$this->getSector()->increaseBattles(1);
2201
		$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()) . ')');
2202
2203
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2204
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getAccountID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2205
2206
		// Dead player loses between 5% and 25% experience
2207
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2208
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2209
		$this->decreaseExperience($return['DeadExp']);
2210
2211
		// Killer gains 50% of the lost exp
2212
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
2213
		$killer->increaseExperience($return['KillerExp']);
2214
2215
		$return['KillerCredits'] = $this->getCredits();
2216
		$killer->increaseCredits($return['KillerCredits']);
2217
2218
		// The killer may change alignment
2219
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2220
		$relation = $relations[$killer->getRaceID()];
2221
2222
		$alignChangePerRelation = 0.1;
2223
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2224
			$alignChangePerRelation = 0.04;
2225
		}
2226
2227
		$return['KillerAlign'] = -$relation * $alignChangePerRelation; //Lose relations when killing a peaceful race
2228
		if ($return['KillerAlign'] > 0) {
2229
			$killer->increaseAlignment($return['KillerAlign']);
2230
		} else {
2231
			$killer->decreaseAlignment(-$return['KillerAlign']);
2232
		}
2233
		// War setting gives them military pay
2234
		if ($relation <= RELATIONS_WAR) {
2235
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2236
		}
2237
2238
		//check for federal bounty being offered for current port raiders;
2239
		$this->db->query('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(TIME - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2240
		$query = 'SELECT 1
2241
					FROM player_attacks_port
2242
					JOIN port USING(game_id, sector_id)
2243
					JOIN player USING(game_id, account_id)
2244
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2245
		$this->db->query($query);
2246
		if ($this->db->nextRecord()) {
2247
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2248
			$this->increaseCurrentBountyAmount('HQ', $bounty);
2249
		}
2250
2251
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2252
		$this->setBountiesClaimable($killer);
2253
2254
		// If the alignment difference is greater than 200 then a bounty may be set
2255
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2256
		$return['BountyGained'] = array(
2257
			'Type' => 'None',
2258
			'Amount' => 0
2259
		);
2260
		if ($alignmentDiff >= 200) {
2261
			// If the podded players alignment makes them deputy or member then set bounty
2262
			if ($this->getAlignment() >= 100) {
2263
				$return['BountyGained']['Type'] = 'HQ';
2264
			} elseif ($this->getAlignment() <= 100) {
2265
				$return['BountyGained']['Type'] = 'UG';
2266
			}
2267
2268
			if ($return['BountyGained']['Type'] != 'None') {
2269
				$return['BountyGained']['Amount'] = IFloor(pow($alignmentDiff, 2.56));
2270
				$killer->increaseCurrentBountyAmount($return['BountyGained']['Type'], $return['BountyGained']['Amount']);
2271
			}
2272
		}
2273
2274
		if ($this->isNPC()) {
2275
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'NPC', 'Experience', 'Gained'), HOF_PUBLIC);
2276
			$killer->increaseHOF($this->getExperience(), array('Killing', 'NPC', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2277
2278
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By NPCs Killed'), HOF_PUBLIC);
2279
2280
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2281
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'NPC', 'Money', 'Gain'), HOF_PUBLIC);
2282
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'NPC', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2283
2284
			if ($return['KillerAlign'] > 0) {
2285
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Gain'), HOF_PUBLIC);
2286
			} else {
2287
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'NPC', 'Alignment', 'Loss'), HOF_PUBLIC);
2288
			}
2289
2290
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'NPC', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2291
2292
			$killer->increaseHOF(1, array('Killing', 'NPC Kills'), HOF_PUBLIC);
2293
		} else {
2294
			$killer->increaseHOF($return['KillerExp'], array('Killing', 'Experience', 'Gained'), HOF_PUBLIC);
2295
			$killer->increaseHOF($this->getExperience(), array('Killing', 'Experience', 'Of Traders Killed'), HOF_PUBLIC);
2296
2297
			$killer->increaseHOF($return['DeadExp'], array('Killing', 'Experience', 'Lost By Traders Killed'), HOF_PUBLIC);
2298
2299
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Lost By Traders Killed'), HOF_PUBLIC);
2300
			$killer->increaseHOF($return['KillerCredits'], array('Killing', 'Money', 'Gain'), HOF_PUBLIC);
2301
			$killer->increaseHOF($this->getShip()->getCost(), array('Killing', 'Money', 'Cost Of Ships Killed'), HOF_PUBLIC);
2302
2303
			if ($return['KillerAlign'] > 0) {
2304
				$killer->increaseHOF($return['KillerAlign'], array('Killing', 'Alignment', 'Gain'), HOF_PUBLIC);
2305
			} else {
2306
				$killer->increaseHOF(-$return['KillerAlign'], array('Killing', 'Alignment', 'Loss'), HOF_PUBLIC);
2307
			}
2308
2309
			$killer->increaseHOF($return['BountyGained']['Amount'], array('Killing', 'Money', 'Bounty Gained'), HOF_PUBLIC);
2310
2311
			if ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2312
				$killer->increaseHOF(1, array('Killing', 'Newbie Kills'), HOF_PUBLIC);
2313
			} else {
2314
				$killer->increaseKills(1);
2315
				$killer->increaseHOF(1, array('Killing', 'Kills'), HOF_PUBLIC);
2316
2317
				if ($killer->hasAlliance()) {
2318
					$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');
2319
				}
2320
2321
				// alliance vs. alliance stats
2322
				$this->incrementAllianceVsDeaths($killer->getAllianceID());
2323
			}
2324
		}
2325
2326
		$this->increaseHOF($return['BountyGained']['Amount'], array('Dying', 'Players', 'Money', 'Bounty Gained By Killer'), HOF_PUBLIC);
2327
		$this->increaseHOF($return['KillerExp'], array('Dying', 'Players', 'Experience', 'Gained By Killer'), HOF_PUBLIC);
2328
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2329
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Players', 'Experience', 'Lost'), HOF_PUBLIC);
2330
		$this->increaseHOF($return['KillerCredits'], array('Dying', 'Players', 'Money Lost'), HOF_PUBLIC);
2331
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Players', 'Money', 'Cost Of Ships Lost'), HOF_PUBLIC);
2332
		$this->increaseHOF(1, array('Dying', 'Players', 'Deaths'), HOF_PUBLIC);
2333
2334
		$this->killPlayer($this->getSectorID());
2335
		return $return;
2336
	}
2337
2338
	public function &killPlayerByForces(SmrForce $forces) {
2339
		$return = array();
2340
		$owner = $forces->getOwner();
2341
		// send a message to the person who died
2342
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getAccountID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2343
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2344
2345
		$news_message = $this->getBBLink();
2346
		if ($this->hasCustomShipName()) {
2347
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2348
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2349
		}
2350
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2351
		// insert the news entry
2352
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2353
						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()) . ')');
2354
2355
		// Player loses 15% experience
2356
		$expLossPercentage = .15;
2357
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
2358
		$this->decreaseExperience($return['DeadExp']);
2359
2360
		$return['LostCredits'] = $this->getCredits();
2361
2362
		// alliance vs. alliance stats
2363
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2364
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2365
2366
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2367
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Forces', 'Experience Lost'), HOF_PUBLIC);
2368
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Forces', 'Money Lost'), HOF_PUBLIC);
2369
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Forces', 'Cost Of Ships Lost'), HOF_PUBLIC);
2370
		$this->increaseHOF(1, array('Dying', 'Forces', 'Deaths'), HOF_PUBLIC);
2371
2372
		$this->killPlayer($forces->getSectorID());
2373
		return $return;
2374
	}
2375
2376
	public function &killPlayerByPort(SmrPort $port) {
2377
		$return = array();
2378
		// send a message to the person who died
2379
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2380
2381
		$news_message = $this->getBBLink();
2382
		if ($this->hasCustomShipName()) {
2383
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2384
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2385
		}
2386
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2387
		// insert the news entry
2388
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,dead_id,dead_alliance)
2389
						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()) . ')');
2390
2391
		// Player loses between 15% and 20% experience
2392
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2393
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2394
		$this->decreaseExperience($return['DeadExp']);
2395
2396
		$return['LostCredits'] = $this->getCredits();
2397
2398
		// alliance vs. alliance stats
2399
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2400
2401
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2402
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Ports', 'Experience Lost'), HOF_PUBLIC);
2403
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Ports', 'Money Lost'), HOF_PUBLIC);
2404
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Ports', 'Cost Of Ships Lost'), HOF_PUBLIC);
2405
		$this->increaseHOF(1, array('Dying', 'Ports', 'Deaths'), HOF_PUBLIC);
2406
2407
		$this->killPlayer($port->getSectorID());
2408
		return $return;
2409
	}
2410
2411
	public function &killPlayerByPlanet(SmrPlanet $planet) {
2412
		$return = array();
2413
		// send a message to the person who died
2414
		$planetOwner = $planet->getOwner();
2415
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getAccountID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2416
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2417
2418
		$news_message = $this->getBBLink();
2419
		if ($this->hasCustomShipName()) {
2420
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2421
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2422
		}
2423
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2424
		// insert the news entry
2425
		$this->db->query('INSERT INTO news (game_id, time, news_message,killer_id,killer_alliance,dead_id,dead_alliance)
2426
						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()) . ')');
2427
2428
		// Player loses between 15% and 20% experience
2429
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2430
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
2431
		$this->decreaseExperience($return['DeadExp']);
2432
2433
		$return['LostCredits'] = $this->getCredits();
2434
2435
		// alliance vs. alliance stats
2436
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2437
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2438
2439
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Experience', 'Lost'), HOF_PUBLIC);
2440
		$this->increaseHOF($return['DeadExp'], array('Dying', 'Planets', 'Experience Lost'), HOF_PUBLIC);
2441
		$this->increaseHOF($return['LostCredits'], array('Dying', 'Planets', 'Money Lost'), HOF_PUBLIC);
2442
		$this->increaseHOF($this->getShip()->getCost(), array('Dying', 'Planets', 'Cost Of Ships Lost'), HOF_PUBLIC);
2443
		$this->increaseHOF(1, array('Dying', 'Planets', 'Deaths'), HOF_PUBLIC);
2444
2445
		$this->killPlayer($planet->getSectorID());
2446
		return $return;
2447
	}
2448
2449
	public function incrementAllianceVsKills($otherID) {
2450
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2451
		$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');
2452
	}
2453
2454
	public function incrementAllianceVsDeaths($otherID) {
2455
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2456
		$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');
2457
	}
2458
2459
	public function getTurnsLevel() {
2460
		if (!$this->hasTurns()) {
2461
			return 'NONE';
2462
		}
2463
		if ($this->getTurns() <= 25) {
2464
			return 'LOW';
2465
		}
2466
		if ($this->getTurns() <= 75) {
2467
			return 'MEDIUM';
2468
		}
2469
		return 'HIGH';
2470
	}
2471
2472
	/**
2473
	 * Returns the CSS class color to use when displaying the player's turns
2474
	 */
2475
	public function getTurnsColor() {
2476
		switch ($this->getTurnsLevel()) {
2477
			case 'NONE':
2478
			case 'LOW':
2479
				return 'red';
2480
			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...
2481
			case 'MEDIUM':
2482
				return 'yellow';
2483
			break;
2484
			default:
2485
				return 'green';
2486
		}
2487
	}
2488
2489
	public function getTurns() {
2490
		return $this->turns;
2491
	}
2492
2493
	public function hasTurns() {
2494
		return $this->turns > 0;
2495
	}
2496
2497
	public function getMaxTurns() {
2498
		return $this->getGame()->getMaxTurns();
2499
	}
2500
2501
	public function setTurns($turns) {
2502
		if ($this->turns == $turns) {
2503
			return;
2504
		}
2505
		// Make sure turns are in range [0, MaxTurns]
2506
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2507
		$this->hasChanged = true;
2508
	}
2509
2510
	public function takeTurns($take, $takeNewbie = 0) {
2511
		if ($take < 0 || $takeNewbie < 0) {
2512
			throw new Exception('Trying to take negative turns.');
2513
		}
2514
		$take = ICeil($take);
2515
		// Only take up to as many newbie turns as we have remaining
2516
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2517
2518
		$this->setTurns($this->getTurns() - $take);
2519
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2520
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Since Last Death'), HOF_ALLIANCE);
2521
		$this->increaseHOF($take, array('Movement', 'Turns Used', 'Total'), HOF_ALLIANCE);
2522
		$this->increaseHOF($takeNewbie, array('Movement', 'Turns Used', 'Newbie'), HOF_ALLIANCE);
2523
2524
		// Player has taken an action
2525
		$this->setLastActive(TIME);
2526
		$this->updateLastCPLAction();
2527
	}
2528
2529
	public function giveTurns(int $give, $giveNewbie = 0) {
2530
		if ($give < 0 || $giveNewbie < 0) {
2531
			throw new Exception('Trying to give negative turns.');
2532
		}
2533
		$this->setTurns($this->getTurns() + $give);
2534
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2535
	}
2536
2537
	/**
2538
	 * Calculate the time in seconds between the given time and when the
2539
	 * player will be at max turns.
2540
	 */
2541
	public function getTimeUntilMaxTurns($time, $forceUpdate = false) {
2542
		$timeDiff = $time - $this->getLastTurnUpdate();
2543
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2544
		$ship = $this->getShip($forceUpdate);
2545
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
2546
		// If already at max turns, return 0
2547
		return max(0, $maxTurnsTime);
2548
	}
2549
2550
	/**
2551
	 * Grant the player their starting turns.
2552
	 */
2553
	public function giveStartingTurns() {
2554
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
2555
		$this->giveTurns($startTurns);
2556
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2557
	}
2558
2559
	// Turns only update when player is active.
2560
	// Calculate turns gained between given time and the last turn update
2561
	public function getTurnsGained($time, $forceUpdate = false) : int {
2562
		$timeDiff = $time - $this->getLastTurnUpdate();
2563
		$ship = $this->getShip($forceUpdate);
2564
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
2565
		return $extraTurns;
2566
	}
2567
2568
	public function updateTurns() {
2569
		// is account validated?
2570
		if (!$this->getAccount()->isValidated()) {
2571
			return;
2572
		}
2573
2574
		// how many turns would he get right now?
2575
		$extraTurns = $this->getTurnsGained(TIME);
2576
2577
		// do we have at least one turn to give?
2578
		if ($extraTurns > 0) {
2579
			// recalc the time to avoid rounding errors
2580
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
2581
			$this->setLastTurnUpdate($newLastTurnUpdate);
2582
			$this->giveTurns($extraTurns);
2583
		}
2584
	}
2585
2586
	public function getLastTurnUpdate() {
2587
		return $this->lastTurnUpdate;
2588
	}
2589
2590
	public function setLastTurnUpdate($time) {
2591
		if ($this->lastTurnUpdate == $time) {
2592
			return;
2593
		}
2594
		$this->lastTurnUpdate = $time;
2595
		$this->hasChanged = true;
2596
	}
2597
2598
	public function getLastActive() {
2599
		return $this->lastActive;
2600
	}
2601
2602
	public function setLastActive($lastActive) {
2603
		if ($this->lastActive == $lastActive) {
2604
			return;
2605
		}
2606
		$this->lastActive = $lastActive;
2607
		$this->hasChanged = true;
2608
	}
2609
2610
	public function getLastCPLAction() {
2611
		return $this->lastCPLAction;
2612
	}
2613
2614
	public function setLastCPLAction($time) {
2615
		if ($this->lastCPLAction == $time) {
2616
			return;
2617
		}
2618
		$this->lastCPLAction = $time;
2619
		$this->hasChanged = true;
2620
	}
2621
2622
	public function updateLastCPLAction() {
2623
		$this->setLastCPLAction(TIME);
2624
	}
2625
2626
	public function setNewbieWarning($bool) {
2627
		if ($this->newbieWarning == $bool) {
2628
			return;
2629
		}
2630
		$this->newbieWarning = $bool;
2631
		$this->hasChanged = true;
2632
	}
2633
2634
	public function getNewbieWarning() {
2635
		return $this->newbieWarning;
2636
	}
2637
2638
	public function isDisplayMissions() {
2639
		return $this->displayMissions;
2640
	}
2641
2642
	public function setDisplayMissions($bool) {
2643
		if ($this->displayMissions == $bool) {
2644
			return;
2645
		}
2646
		$this->displayMissions = $bool;
2647
		$this->hasChanged = true;
2648
	}
2649
2650
	public function getMissions() {
2651
		if (!isset($this->missions)) {
2652
			$this->db->query('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2653
			$this->missions = array();
2654
			while ($this->db->nextRecord()) {
2655
				$missionID = $this->db->getInt('mission_id');
2656
				$this->missions[$missionID] = array(
2657
					'On Step' => $this->db->getInt('on_step'),
2658
					'Progress' => $this->db->getInt('progress'),
2659
					'Unread' => $this->db->getBoolean('unread'),
2660
					'Expires' => $this->db->getInt('step_fails'),
2661
					'Sector' => $this->db->getInt('mission_sector'),
2662
					'Starting Sector' => $this->db->getInt('starting_sector')
2663
				);
2664
				$this->rebuildMission($missionID);
2665
			}
2666
		}
2667
		return $this->missions;
2668
	}
2669
2670
	public function getActiveMissions() {
2671
		$missions = $this->getMissions();
2672
		foreach ($missions as $missionID => $mission) {
2673
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2674
				unset($missions[$missionID]);
2675
			}
2676
		}
2677
		return $missions;
2678
	}
2679
2680
	protected function getMission($missionID) {
2681
		$missions = $this->getMissions();
2682
		if (isset($missions[$missionID])) {
2683
			return $missions[$missionID];
2684
		}
2685
		return false;
2686
	}
2687
2688
	protected function hasMission($missionID) {
2689
		return $this->getMission($missionID) !== false;
2690
	}
2691
2692
	protected function updateMission($missionID) {
2693
		$this->getMissions();
2694
		if (isset($this->missions[$missionID])) {
2695
			$mission = $this->missions[$missionID];
2696
			$this->db->query('
2697
				UPDATE player_has_mission
2698
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2699
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2700
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2701
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2702
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2703
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2704
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1'
2705
			);
2706
			return true;
2707
		}
2708
		return false;
2709
	}
2710
2711
	private function setupMissionStep($missionID) {
2712
		$mission =& $this->missions[$missionID];
2713
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2714
			// Nothing to do if this mission is already completed
2715
			return;
2716
		}
2717
		$step = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2718
		if (isset($step['PickSector'])) {
2719
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2720
			if ($realX === false) {
2721
				throw new Exception('Invalid PickSector definition in mission: ' . $missionID);
2722
			}
2723
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2724
			if ($path === false) {
2725
				throw new Exception('Cannot find location: ' . $missionID);
2726
			}
2727
			$mission['Sector'] = $path->getEndSectorID();
2728
		}
2729
	}
2730
2731
	/**
2732
	 * Declining a mission will permanently hide it from the player
2733
	 * by adding it in its completed state.
2734
	 */
2735
	public function declineMission($missionID) {
2736
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2737
		$this->addMission($missionID, $finishedStep);
2738
	}
2739
2740
	public function addMission($missionID, $step = 0) {
2741
		$this->getMissions();
2742
2743
		if (isset($this->missions[$missionID])) {
2744
			return;
2745
		}
2746
		$sector = 0;
2747
2748
		$mission = array(
2749
			'On Step' => $step,
2750
			'Progress' => 0,
2751
			'Unread' => true,
2752
			'Expires' => (TIME + 86400),
2753
			'Sector' => $sector,
2754
			'Starting Sector' => $this->getSectorID()
2755
		);
2756
2757
		$this->missions[$missionID] =& $mission;
2758
		$this->setupMissionStep($missionID);
2759
		$this->rebuildMission($missionID);
2760
2761
		$this->db->query('
2762
			REPLACE INTO player_has_mission (game_id,account_id,mission_id,on_step,progress,unread,starting_sector,mission_sector,step_fails)
2763
			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']) . ')'
2764
		);
2765
	}
2766
2767
	private function rebuildMission($missionID) {
2768
		$mission = $this->missions[$missionID];
2769
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2770
2771
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2772
			// If we have completed this mission just use false to indicate no current task.
2773
			$currentStep = false;
2774
		} else {
2775
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2776
			$currentStep['Text'] = str_replace(array('<Race>', '<Sector>', '<Starting Sector>', '<trader>'), array($this->getRaceID(), $mission['Sector'], $mission['Starting Sector'], $this->playerName), $currentStep['Text']);
2777
			if (isset($currentStep['Task'])) {
2778
				$currentStep['Task'] = str_replace(array('<Race>', '<Sector>', '<Starting Sector>', '<trader>'), array($this->getRaceID(), $mission['Sector'], $mission['Starting Sector'], $this->playerName), $currentStep['Task']);
2779
			}
2780
			if (isset($currentStep['Level'])) {
2781
				$currentStep['Level'] = str_replace('<Player Level>', $this->getLevelID(), $currentStep['Level']);
2782
			} else {
2783
				$currentStep['Level'] = 0;
2784
			}
2785
		}
2786
		$this->missions[$missionID]['Task'] = $currentStep;
2787
	}
2788
2789
	public function deleteMission($missionID) {
2790
		$this->getMissions();
2791
		if (isset($this->missions[$missionID])) {
2792
			unset($this->missions[$missionID]);
2793
			$this->db->query('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID) . ' LIMIT 1');
2794
			return true;
2795
		}
2796
		return false;
2797
	}
2798
2799
	public function markMissionsRead() {
2800
		$this->getMissions();
2801
		$unreadMissions = array();
2802
		foreach ($this->missions as $missionID => &$mission) {
2803
			if ($mission['Unread']) {
2804
				$unreadMissions[] = $missionID;
2805
				$mission['Unread'] = false;
2806
				$this->updateMission($missionID);
2807
			}
2808
		}
2809
		return $unreadMissions;
2810
	}
2811
2812
	public function claimMissionReward($missionID) {
2813
		$this->getMissions();
2814
		$mission =& $this->missions[$missionID];
2815
		if ($mission === false) {
2816
			throw new Exception('Unknown mission: ' . $missionID);
2817
		}
2818
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2819
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2820
		}
2821
		$mission['On Step']++;
2822
		$mission['Unread'] = true;
2823
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2824
			switch ($rewardItem) {
2825
				case 'Credits':
2826
					$this->increaseCredits($amount);
2827
				break;
2828
				case 'Experience':
2829
					$this->increaseExperience($amount);
2830
				break;
2831
			}
2832
		}
2833
		$rewardText = $mission['Task']['Rewards']['Text'];
2834
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2835
			// If we haven't finished the mission yet then 
2836
			$this->setupMissionStep($missionID);
2837
		}
2838
		$this->rebuildMission($missionID);
2839
		$this->updateMission($missionID);
2840
		return $rewardText;
2841
	}
2842
2843
	public function getAvailableMissions() {
2844
		$availableMissions = array();
2845
		foreach (MISSIONS as $missionID => $mission) {
2846
			if ($this->hasMission($missionID)) {
2847
				continue;
2848
			}
2849
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2850
			if ($realX === false) {
2851
				throw new Exception('Invalid HasX definition in mission: ' . $missionID);
2852
			}
2853
			if ($this->getSector()->hasX($realX)) {
2854
				$availableMissions[$missionID] = $mission;
2855
			}
2856
		}
2857
		return $availableMissions;
2858
	}
2859
2860
	public function actionTaken($actionID, array $values) {
2861
		if (!in_array($actionID, MISSION_ACTIONS)) {
2862
			throw new Exception('Unknown action: ' . $actionID);
2863
		}
2864
// TODO: Reenable this once tested.		if($this->getAccount()->isLoggingEnabled())
2865
			switch ($actionID) {
2866
				case 'WalkSector':
2867
					$this->getAccount()->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID(), $this->getSectorID());
2868
				break;
2869
				case 'JoinAlliance':
2870
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName(), $this->getSectorID());
2871
				break;
2872
				case 'LeaveAlliance':
2873
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName(), $this->getSectorID());
2874
				break;
2875
				case 'DisbandAlliance':
2876
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName(), $this->getSectorID());
2877
				break;
2878
				case 'KickPlayer':
2879
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName(), 0);
2880
				break;
2881
				case 'PlayerKicked':
2882
					$this->getAccount()->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')', 0);
2883
				break;
2884
2885
			}
2886
		$this->getMissions();
2887
		foreach ($this->missions as $missionID => &$mission) {
2888
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
2889
				if (checkMissionRequirements($values, $mission, $this) === true) {
2890
					$mission['On Step']++;
2891
					$mission['Unread'] = true;
2892
					$this->setupMissionStep($missionID);
2893
					$this->rebuildMission($missionID);
2894
					$this->updateMission($missionID);
2895
				}
2896
			}
2897
		}
2898
	}
2899
2900
	public function canSeeAny(array $otherPlayerArray) {
2901
		foreach ($otherPlayerArray as $otherPlayer) {
2902
			if ($this->canSee($otherPlayer)) {
2903
				return true;
2904
			}
2905
		}
2906
		return false;
2907
	}
2908
2909
	public function canSee(AbstractSmrPlayer $otherPlayer) {
2910
		if (!$otherPlayer->getShip()->isCloaked()) {
2911
			return true;
2912
		}
2913
		if ($this->sameAlliance($otherPlayer)) {
2914
			return true;
2915
		}
2916
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
2917
			return true;
2918
		}
2919
		return false;
2920
	}
2921
2922
	public function equals(AbstractSmrPlayer $otherPlayer = null) {
2923
		return $otherPlayer !== null && $this->getAccountID() == $otherPlayer->getAccountID() && $this->getGameID() == $otherPlayer->getGameID();
2924
	}
2925
2926
	public function sameAlliance(AbstractSmrPlayer $otherPlayer = null) {
2927
		return $this->equals($otherPlayer) || (!is_null($otherPlayer) && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
2928
	}
2929
2930
	public function sharedForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2931
		return $this->sameAlliance($otherPlayer);
2932
	}
2933
2934
	public function forceNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2935
		return $this->sameAlliance($otherPlayer);
2936
	}
2937
2938
	public function planetNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2939
		return $this->sameAlliance($otherPlayer);
2940
	}
2941
2942
	public function traderNAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2943
		return $this->sameAlliance($otherPlayer);
2944
	}
2945
2946
	public function traderMAPAlliance(AbstractSmrPlayer $otherPlayer = null) {
2947
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
2948
	}
2949
2950
	public function traderAttackTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2951
		return $this->sameAlliance($otherPlayer);
2952
	}
2953
2954
	public function traderDefendTraderAlliance(AbstractSmrPlayer $otherPlayer = null) {
2955
		return $this->sameAlliance($otherPlayer);
2956
	}
2957
2958
	public function traderAttackForceAlliance(AbstractSmrPlayer $otherPlayer = null) {
2959
		return $this->sameAlliance($otherPlayer);
2960
	}
2961
2962
	public function traderAttackPortAlliance(AbstractSmrPlayer $otherPlayer = null) {
2963
		return $this->sameAlliance($otherPlayer);
2964
	}
2965
2966
	public function traderAttackPlanetAlliance(AbstractSmrPlayer $otherPlayer = null) {
2967
		return $this->sameAlliance($otherPlayer);
2968
	}
2969
2970
	public function meetsAlignmentRestriction($restriction) {
2971
		if ($restriction < 0) {
2972
			return $this->getAlignment() <= $restriction;
2973
		}
2974
		if ($restriction > 0) {
2975
			return $this->getAlignment() >= $restriction;
2976
		}
2977
		return true;
2978
	}
2979
2980
	// Get an array of goods that are visible to the player
2981
	public function getVisibleGoods() {
2982
		$goods = Globals::getGoods();
2983
		$visibleGoods = array();
2984
		foreach ($goods as $key => $good) {
2985
			if ($this->meetsAlignmentRestriction($good['AlignRestriction'])) {
2986
				$visibleGoods[$key] = $good;
2987
			}
2988
		}
2989
		return $visibleGoods;
2990
	}
2991
2992
	/**
2993
	 * Will retrieve all visited sectors, use only when you are likely to check a large number of these
2994
	 */
2995
	public function hasVisitedSector($sectorID) {
2996
		if (!isset($this->visitedSectors)) {
2997
			$this->visitedSectors = array();
2998
			$this->db->query('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
2999
			while ($this->db->nextRecord()) {
3000
				$this->visitedSectors[$this->db->getInt('sector_id')] = false;
3001
			}
3002
		}
3003
		return !isset($this->visitedSectors[$sectorID]);
3004
	}
3005
3006
	public function getLeaveNewbieProtectionHREF() {
3007
		return SmrSession::getNewHREF(create_container('leave_newbie_processing.php'));
3008
	}
3009
3010
	public function getExamineTraderHREF() {
3011
		$container = create_container('skeleton.php', 'trader_examine.php');
3012
		$container['target'] = $this->getAccountID();
3013
		return SmrSession::getNewHREF($container);
3014
	}
3015
3016
	public function getAttackTraderHREF() {
3017
		return Globals::getAttackTraderHREF($this->getAccountID());
3018
	}
3019
3020
	public function getPlanetKickHREF() {
3021
		$container = create_container('planet_kick_processing.php', 'trader_attack_processing.php');
3022
		$container['account_id'] = $this->getAccountID();
3023
		return SmrSession::getNewHREF($container);
3024
	}
3025
3026
	public function getTraderSearchHREF() {
3027
		$container = create_container('skeleton.php', 'trader_search_result.php');
3028
		$container['player_id'] = $this->getPlayerID();
3029
		return SmrSession::getNewHREF($container);
3030
	}
3031
3032
	public function getAllianceRosterHREF() {
3033
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3034
	}
3035
3036
	public function getToggleWeaponHidingHREF($ajax = false) {
3037
		$container = create_container('toggle_processing.php');
3038
		$container['toggle'] = 'WeaponHiding';
3039
		$container['AJAX'] = $ajax;
3040
		return SmrSession::getNewHREF($container);
3041
	}
3042
3043
	public function isDisplayWeapons() {
3044
		return $this->displayWeapons;
3045
	}
3046
3047
	/**
3048
	 * Should weapons be displayed in the right panel?
3049
	 * This updates the player database directly because it is used with AJAX,
3050
	 * which does not acquire a sector lock.
3051
	 */
3052
	public function setDisplayWeapons($bool) {
3053
		if ($this->displayWeapons == $bool) {
3054
			return;
3055
		}
3056
		$this->displayWeapons = $bool;
3057
		$this->db->query('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3058
	}
3059
3060
	public function update() {
3061
		$this->save();
3062
	}
3063
3064
	public function save() {
3065
		if ($this->hasChanged === true) {
3066
			$this->db->query('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3067
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3068
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3069
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3070
				', turns=' . $this->db->escapeNumber($this->turns) .
3071
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3072
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3073
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3074
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3075
				', dead=' . $this->db->escapeBoolean($this->dead) .
3076
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3077
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3078
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3079
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3080
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3081
				', credits=' . $this->db->escapeNumber($this->credits) .
3082
				', experience=' . $this->db->escapeNumber($this->experience) .
3083
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3084
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3085
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3086
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3087
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3088
				', kills=' . $this->db->escapeNumber($this->kills) .
3089
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3090
				', assists=' . $this->db->escapeNumber($this->assists) .
3091
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3092
				', bank=' . $this->db->escapeNumber($this->bank) .
3093
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3094
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3095
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3096
				', group_scout_messages=' . $this->db->escapeString($this->groupScoutMessages) .
3097
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3098
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3099
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3100
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3101
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3102
				' WHERE ' . $this->SQL . ' LIMIT 1');
3103
			$this->hasChanged = false;
3104
		}
3105
		foreach ($this->hasBountyChanged as $key => &$bountyChanged) {
3106
			if ($bountyChanged === true) {
3107
				$bountyChanged = false;
3108
				$bounty = $this->getBounty($key);
3109
				if ($bounty['New'] === true) {
3110
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3111
						$this->db->query('INSERT INTO bounty (account_id,game_id,type,amount,smr_credits,claimer_id,time) VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeString($bounty['Type']) . ',' . $this->db->escapeNumber($bounty['Amount']) . ',' . $this->db->escapeNumber($bounty['SmrCredits']) . ',' . $this->db->escapeNumber($bounty['Claimer']) . ',' . $this->db->escapeNumber($bounty['Time']) . ')');
3112
					}
3113
				} else {
3114
					if ($bounty['Amount'] > 0 || $bounty['SmrCredits'] > 0) {
3115
						$this->db->query('UPDATE bounty
3116
							SET amount=' . $this->db->escapeNumber($bounty['Amount']) . ',
3117
							smr_credits=' . $this->db->escapeNumber($bounty['SmrCredits']) . ',
3118
							type=' . $this->db->escapeString($bounty['Type']) . ',
3119
							claimer_id=' . $this->db->escapeNumber($bounty['Claimer']) . ',
3120
							time=' . $this->db->escapeNumber($bounty['Time']) . '
3121
							WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3122
					} else {
3123
						$this->db->query('DELETE FROM bounty WHERE bounty_id=' . $this->db->escapeNumber($bounty['ID']) . ' AND ' . $this->SQL . ' LIMIT 1');
3124
					}
3125
				}
3126
			}
3127
		}
3128
		$this->saveHOF();
3129
	}
3130
3131
	public function saveHOF() {
3132
		if (count($this->hasHOFChanged) > 0) {
3133
			$this->doHOFSave($this->hasHOFChanged);
3134
			$this->hasHOFChanged = [];
3135
		}
3136
		if (!empty(self::$hasHOFVisChanged)) {
3137
			foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3138
				if ($changeType == self::HOF_NEW) {
3139
					$this->db->query('INSERT INTO hof_visibility (type, visibility) VALUES (' . $this->db->escapeString($hofType) . ',' . $this->db->escapeString(self::$HOFVis[$hofType]) . ')');
3140
				} else {
3141
					$this->db->query('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType) . ' LIMIT 1');
3142
				}
3143
				unset(self::$hasHOFVisChanged[$hofType]);
3144
			}
3145
		}
3146
	}
3147
3148
	/**
3149
	 * This should only be called by `saveHOF` (and recursively) to
3150
	 * ensure that the `hasHOFChanged` attribute is properly cleared.
3151
	 */
3152
	protected function doHOFSave(array $hasChangedList, array $typeList = array()) {
3153
		foreach ($hasChangedList as $type => $hofChanged) {
3154
			$tempTypeList = $typeList;
3155
			$tempTypeList[] = $type;
3156
			if (is_array($hofChanged)) {
3157
				$this->doHOFSave($hofChanged, $tempTypeList);
3158
			} else {
3159
				$amount = $this->getHOF($tempTypeList);
3160
				if ($hofChanged == self::HOF_NEW) {
3161
					if ($amount > 0) {
3162
						$this->db->query('INSERT INTO player_hof (account_id,game_id,type,amount) VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeArray($tempTypeList, false, true, ':', false) . ',' . $this->db->escapeNumber($amount) . ')');
3163
					}
3164
				} elseif ($hofChanged == self::HOF_CHANGED) {
3165
					$this->db->query('UPDATE player_hof
3166
						SET amount=' . $this->db->escapeNumber($amount) . '
3167
						WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeArray($tempTypeList, false, true, ':', false) . ' LIMIT 1');
3168
				}
3169
			}
3170
		}
3171
	}
3172
3173
}
3174