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