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

Failed Conditions
Push — main ( d9cfb9...10f5c7 )
by Dan
32s queued 21s
created

AbstractSmrPlayer::save()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 47
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 44
nc 4
nop 0
dl 0
loc 47
rs 9.216
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
require_once('missions.inc.php');
3
4
use Smr\Bounty;
5
use Smr\BountyType;
6
use Smr\Database;
7
use Smr\DatabaseRecord;
8
use Smr\DisplayNameValidator;
9
use Smr\Epoch;
10
use Smr\Exceptions\AccountNotFound;
11
use Smr\Exceptions\PlayerNotFound;
12
use Smr\Exceptions\UserError;
13
use Smr\Messages;
14
use Smr\Pages\Player\ExamineTrader;
15
use Smr\Pages\Player\NewbieLeaveProcessor;
16
use Smr\Pages\Player\Planet\KickProcessor;
17
use Smr\Pages\Player\SearchForTraderResult;
18
use Smr\Pages\Player\WeaponDisplayToggleProcessor;
19
use Smr\Path;
20
use Smr\PlayerLevel;
21
use Smr\Race;
22
use Smr\ScoutMessageGroupType;
23
use Smr\StoredDestination;
24
use Smr\TradeGood;
25
use Smr\TurnsLevel;
26
27
abstract class AbstractSmrPlayer {
28
29
	use Traits\RaceID;
30
31
	protected const TIME_FOR_FEDERAL_BOUNTY_ON_PR = 10800;
32
	protected const TIME_FOR_ALLIANCE_SWITCH = 0;
33
34
	protected const SHIP_INSURANCE_FRACTION = 0.25; // ship value regained on death
35
36
	protected const HOF_CHANGED = 1;
37
	protected const HOF_NEW = 2;
38
39
	/** @var array<int, array<int, array<int, SmrPlayer>>> */
40
	protected static array $CACHE_SECTOR_PLAYERS = [];
41
	/** @var array<int, array<int, array<int, SmrPlayer>>> */
42
	protected static array $CACHE_PLANET_PLAYERS = [];
43
	/** @var array<int, array<int, array<int, SmrPlayer>>> */
44
	protected static array $CACHE_ALLIANCE_PLAYERS = [];
45
	/** @var array<int, array<int, SmrPlayer>> */
46
	protected static array $CACHE_PLAYERS = [];
47
48
	protected Database $db;
49
	protected readonly string $SQL;
50
51
	protected string $playerName;
52
	protected int $playerID;
53
	protected int $sectorID;
54
	protected int $lastSectorID;
55
	protected int $newbieTurns;
56
	protected bool $dead;
57
	protected bool $npc = false; // initialized for legacy combat logs
58
	protected bool $newbieStatus;
59
	protected bool $newbieWarning;
60
	protected bool $landedOnPlanet;
61
	protected int $lastActive;
62
	protected int $credits;
63
	protected int $alignment;
64
	protected int $experience;
65
	protected ?PlayerLevel $level;
66
	protected int $allianceID;
67
	protected int $shipID;
68
	protected int $kills;
69
	protected int $deaths;
70
	protected int $assists;
71
	/** @var array<int, int> */
72
	protected array $personalRelations;
73
	/** @var array<int, int> */
74
	protected array $relations;
75
	protected int $militaryPayment;
76
	/** @var array<int, Bounty> */
77
	protected array $bounties;
78
	protected int $turns;
79
	protected int $lastCPLAction;
80
	/** @var array<int, array<string, mixed>> */
81
	protected array $missions;
82
83
	/** @var array<string, array<string, mixed>> */
84
	protected array $tickers;
85
	protected int $lastTurnUpdate;
86
	protected int $lastNewsUpdate;
87
	protected string $attackColour;
88
	protected int $allianceJoinable;
89
	protected int $lastPort;
90
	protected int $bank;
91
	protected int $zoom;
92
	protected bool $displayMissions;
93
	protected bool $displayWeapons;
94
	protected bool $forceDropMessages;
95
	protected ScoutMessageGroupType $scoutMessageGroupType;
96
	protected bool $ignoreGlobals;
97
	protected ?Path $plottedCourse;
98
	protected bool $nameChanged;
99
	protected bool $raceChanged;
100
	protected bool $combatDronesKamikazeOnMines;
101
	protected string|false $customShipName;
102
	/** @var array<int, StoredDestination> */
103
	protected array $storedDestinations;
104
	/** @var array<int, bool> */
105
	protected array $canFed;
106
	protected bool $underAttack;
107
108
	/** @var array<int> */
109
	protected array $unvisitedSectors;
110
	/** @var array<int, int> */
111
	protected array $allianceRoles = [
112
		0 => 0,
113
	];
114
115
	protected bool $draftLeader;
116
	protected string|false $gpWriter;
117
	/** @var array<string, float> */
118
	protected array $HOF;
119
	/** @var array<string, string> */
120
	protected static array $HOFVis;
121
122
	protected bool $hasChanged = false;
123
	/** @var array<string, int> */
124
	protected array $hasHOFChanged = [];
125
	/** @var array<string, int> */
126
	protected static array $hasHOFVisChanged = [];
127
128
	public static function clearCache(): void {
129
		self::$CACHE_PLAYERS = [];
130
		self::$CACHE_SECTOR_PLAYERS = [];
131
		self::$CACHE_PLANET_PLAYERS = [];
132
		self::$CACHE_ALLIANCE_PLAYERS = [];
133
	}
134
135
	public static function savePlayers(): void {
136
		foreach (self::$CACHE_PLAYERS as $gamePlayers) {
137
			foreach ($gamePlayers as $player) {
138
				$player->save();
139
			}
140
		}
141
	}
142
143
	/**
144
	 * @param array<int> $allianceIDs
145
	 * @return array<int, SmrPlayer>
146
	 */
147
	public static function getSectorPlayersByAlliances(int $gameID, int $sectorID, array $allianceIDs, bool $forceUpdate = false): array {
148
		$players = self::getSectorPlayers($gameID, $sectorID, $forceUpdate); // Don't use & as we do an unset
149
		foreach ($players as $accountID => $player) {
150
			if (!in_array($player->getAllianceID(), $allianceIDs)) {
151
				unset($players[$accountID]);
152
			}
153
		}
154
		return $players;
155
	}
156
157
	/**
158
	 * Returns the same players as getSectorPlayers (e.g. not on planets),
159
	 * but for an entire galaxy rather than a single sector. This is useful
160
	 * for reducing the number of queries in galaxy-wide processing.
161
	 *
162
	 * @return array<int, array<int, SmrPlayer>>
163
	 */
164
	public static function getGalaxyPlayers(int $gameID, int $galaxyID, bool $forceUpdate = false): array {
165
		$db = Database::getInstance();
166
		$dbResult = $db->read('SELECT player.* FROM player LEFT JOIN sector USING(game_id, sector_id) WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND land_on_planet = ' . $db->escapeBoolean(false) . ' AND (last_cpl_action > ' . $db->escapeNumber(Epoch::time() - TIME_BEFORE_HIDDEN) . ' OR newbie_turns = 0) AND galaxy_id = ' . $db->escapeNumber($galaxyID));
167
		$galaxyPlayers = [];
168
		foreach ($dbResult->records() as $dbRecord) {
169
			$sectorID = $dbRecord->getInt('sector_id');
170
			$accountID = $dbRecord->getInt('account_id');
171
			$player = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
172
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID][$accountID] = $player;
173
			$galaxyPlayers[$sectorID][$accountID] = $player;
174
		}
175
		return $galaxyPlayers;
176
	}
177
178
	/**
179
	 * @return array<int, SmrPlayer>
180
	 */
181
	public static function getSectorPlayers(int $gameID, int $sectorID, bool $forceUpdate = false): array {
182
		if ($forceUpdate || !isset(self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID])) {
183
			$db = Database::getInstance();
184
			$dbResult = $db->read('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(Epoch::time() - TIME_BEFORE_HIDDEN) . ' OR newbie_turns = 0) AND account_id NOT IN (' . $db->escapeArray(Globals::getHiddenPlayers()) . ') ORDER BY last_cpl_action DESC');
185
			$players = [];
186
			foreach ($dbResult->records() as $dbRecord) {
187
				$accountID = $dbRecord->getInt('account_id');
188
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
189
			}
190
			self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID] = $players;
191
		}
192
		return self::$CACHE_SECTOR_PLAYERS[$gameID][$sectorID];
193
	}
194
195
	/**
196
	 * @return array<int, SmrPlayer>
197
	 */
198
	public static function getPlanetPlayers(int $gameID, int $sectorID, bool $forceUpdate = false): array {
199
		if ($forceUpdate || !isset(self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID])) {
200
			$db = Database::getInstance();
201
			$dbResult = $db->read('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');
202
			$players = [];
203
			foreach ($dbResult->records() as $dbRecord) {
204
				$accountID = $dbRecord->getInt('account_id');
205
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
206
			}
207
			self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID] = $players;
208
		}
209
		return self::$CACHE_PLANET_PLAYERS[$gameID][$sectorID];
210
	}
211
212
	/**
213
	 * @return array<int, SmrPlayer>
214
	 */
215
	public static function getAlliancePlayers(int $gameID, int $allianceID, bool $forceUpdate = false): array {
216
		if ($forceUpdate || !isset(self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID])) {
217
			$db = Database::getInstance();
218
			$dbResult = $db->read('SELECT * FROM player WHERE alliance_id = ' . $db->escapeNumber($allianceID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' ORDER BY experience DESC');
219
			$players = [];
220
			foreach ($dbResult->records() as $dbRecord) {
221
				$accountID = $dbRecord->getInt('account_id');
222
				$players[$accountID] = self::getPlayer($accountID, $gameID, $forceUpdate, $dbRecord);
223
			}
224
			self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID] = $players;
225
		}
226
		return self::$CACHE_ALLIANCE_PLAYERS[$gameID][$allianceID];
227
	}
228
229
	public static function getPlayer(int $accountID, int $gameID, bool $forceUpdate = false, DatabaseRecord $dbRecord = null): SmrPlayer {
230
		if ($forceUpdate || !isset(self::$CACHE_PLAYERS[$gameID][$accountID])) {
231
			self::$CACHE_PLAYERS[$gameID][$accountID] = new SmrPlayer($gameID, $accountID, $dbRecord);
232
		}
233
		return self::$CACHE_PLAYERS[$gameID][$accountID];
234
	}
235
236
	public static function getPlayerByPlayerID(int $playerID, int $gameID, bool $forceUpdate = false): SmrPlayer {
237
		$db = Database::getInstance();
238
		$dbResult = $db->read('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_id = ' . $db->escapeNumber($playerID));
239
		if ($dbResult->hasRecord()) {
240
			$dbRecord = $dbResult->record();
241
			return self::getPlayer($dbRecord->getInt('account_id'), $gameID, $forceUpdate, $dbRecord);
242
		}
243
		throw new PlayerNotFound('Player ID not found.');
244
	}
245
246
	public static function getPlayerByPlayerName(string $playerName, int $gameID, bool $forceUpdate = false): SmrPlayer {
247
		$db = Database::getInstance();
248
		$dbResult = $db->read('SELECT * FROM player WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND player_name = ' . $db->escapeString($playerName));
249
		if ($dbResult->hasRecord()) {
250
			$dbRecord = $dbResult->record();
251
			return self::getPlayer($dbRecord->getInt('account_id'), $gameID, $forceUpdate, $dbRecord);
252
		}
253
		throw new PlayerNotFound('Player Name not found.');
254
	}
255
256
	protected function __construct(
257
		protected readonly int $gameID,
258
		protected readonly int $accountID,
259
		DatabaseRecord $dbRecord = null
260
	) {
261
		$this->db = Database::getInstance();
262
		$this->SQL = 'account_id = ' . $this->db->escapeNumber($accountID) . ' AND game_id = ' . $this->db->escapeNumber($gameID);
0 ignored issues
show
Bug introduced by
The property SQL is declared read-only in AbstractSmrPlayer.
Loading history...
263
264
		if ($dbRecord === null) {
265
			$dbResult = $this->db->read('SELECT * FROM player WHERE ' . $this->SQL);
266
			if ($dbResult->hasRecord()) {
267
				$dbRecord = $dbResult->record();
268
			}
269
		}
270
		if ($dbRecord === null) {
271
			throw new PlayerNotFound('Invalid accountID: ' . $accountID . ' OR gameID: ' . $gameID);
272
		}
273
274
		$this->playerName = $dbRecord->getString('player_name');
275
		$this->playerID = $dbRecord->getInt('player_id');
276
		$this->sectorID = $dbRecord->getInt('sector_id');
277
		$this->lastSectorID = $dbRecord->getInt('last_sector_id');
278
		$this->turns = $dbRecord->getInt('turns');
279
		$this->lastTurnUpdate = $dbRecord->getInt('last_turn_update');
280
		$this->newbieTurns = $dbRecord->getInt('newbie_turns');
281
		$this->lastNewsUpdate = $dbRecord->getInt('last_news_update');
282
		$this->attackColour = $dbRecord->getString('attack_warning');
283
		$this->dead = $dbRecord->getBoolean('dead');
284
		$this->npc = $dbRecord->getBoolean('npc');
285
		$this->newbieStatus = $dbRecord->getBoolean('newbie_status');
286
		$this->landedOnPlanet = $dbRecord->getBoolean('land_on_planet');
287
		$this->lastActive = $dbRecord->getInt('last_active');
288
		$this->lastCPLAction = $dbRecord->getInt('last_cpl_action');
289
		$this->raceID = $dbRecord->getInt('race_id');
290
		$this->credits = $dbRecord->getInt('credits');
291
		$this->experience = $dbRecord->getInt('experience');
292
		$this->alignment = $dbRecord->getInt('alignment');
293
		$this->militaryPayment = $dbRecord->getInt('military_payment');
294
		$this->allianceID = $dbRecord->getInt('alliance_id');
295
		$this->allianceJoinable = $dbRecord->getInt('alliance_join');
296
		$this->shipID = $dbRecord->getInt('ship_type_id');
297
		$this->kills = $dbRecord->getInt('kills');
298
		$this->deaths = $dbRecord->getInt('deaths');
299
		$this->assists = $dbRecord->getInt('assists');
300
		$this->lastPort = $dbRecord->getInt('last_port');
301
		$this->bank = $dbRecord->getInt('bank');
302
		$this->zoom = $dbRecord->getInt('zoom');
303
		$this->displayMissions = $dbRecord->getBoolean('display_missions');
304
		$this->displayWeapons = $dbRecord->getBoolean('display_weapons');
305
		$this->forceDropMessages = $dbRecord->getBoolean('force_drop_messages');
306
		$this->scoutMessageGroupType = ScoutMessageGroupType::from($dbRecord->getString('group_scout_messages'));
307
		$this->ignoreGlobals = $dbRecord->getBoolean('ignore_globals');
308
		$this->newbieWarning = $dbRecord->getBoolean('newbie_warning');
309
		$this->nameChanged = $dbRecord->getBoolean('name_changed');
310
		$this->raceChanged = $dbRecord->getBoolean('race_changed');
311
		$this->combatDronesKamikazeOnMines = $dbRecord->getBoolean('combat_drones_kamikaze_on_mines');
312
		$this->underAttack = $dbRecord->getBoolean('under_attack');
313
	}
314
315
	/**
316
	 * Insert a new player into the database. Returns the new player object.
317
	 */
318
	public static function createPlayer(int $accountID, int $gameID, string $playerName, int $raceID, bool $isNewbie, bool $npc = false): self {
319
		$time = Epoch::time();
320
		$db = Database::getInstance();
321
		$db->lockTable('player');
322
323
		// Player names must be unique within each game
324
		try {
325
			self::getPlayerByPlayerName($playerName, $gameID);
326
			$db->unlock();
327
			throw new UserError('That player name already exists.');
328
		} catch (PlayerNotFound) {
329
			// Player name does not yet exist, we may proceed
330
		}
331
332
		// Get the next available player ID (start at 1 if no players yet)
333
		$dbResult = $db->read('SELECT IFNULL(MAX(player_id), 0) AS player_id FROM player WHERE game_id = ' . $db->escapeNumber($gameID));
334
		$playerID = $dbResult->record()->getInt('player_id') + 1;
335
336
		$startSectorID = 0; // Temporarily put player into non-existent sector
337
		$db->insert('player', [
338
			'account_id' => $db->escapeNumber($accountID),
339
			'game_id' => $db->escapeNumber($gameID),
340
			'player_id' => $db->escapeNumber($playerID),
341
			'player_name' => $db->escapeString($playerName),
342
			'race_id' => $db->escapeNumber($raceID),
343
			'sector_id' => $db->escapeNumber($startSectorID),
344
			'last_cpl_action' => $db->escapeNumber($time),
345
			'last_active' => $db->escapeNumber($time),
346
			'npc' => $db->escapeBoolean($npc),
347
			'newbie_status' => $db->escapeBoolean($isNewbie),
348
		]);
349
		$db->unlock();
350
351
		$player = self::getPlayer($accountID, $gameID);
352
		$player->setSectorID($player->getHome());
353
		return $player;
354
	}
355
356
	/**
357
	 * Get array of players whose info can be accessed by this player.
358
	 * Skips players who are not in the same alliance as this player.
359
	 *
360
	 * @return array<AbstractSmrPlayer>
361
	 */
362
	public function getSharingPlayers(bool $forceUpdate = false): array {
363
		$results = [$this];
364
365
		// Only return this player if not in an alliance
366
		if (!$this->hasAlliance()) {
367
			return $results;
368
		}
369
370
		// Get other players who are sharing info for this game.
371
		// NOTE: game_id=0 means that player shares info for all games.
372
		$dbResult = $this->db->read('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()) . ')');
373
		foreach ($dbResult->records() as $dbRecord) {
374
			try {
375
				$otherPlayer = self::getPlayer($dbRecord->getInt('from_account_id'), $this->getGameID(), $forceUpdate);
376
			} catch (PlayerNotFound) {
377
				// Skip players that have not joined this game
378
				continue;
379
			}
380
381
			// players must be in the same alliance
382
			if ($this->sameAlliance($otherPlayer)) {
383
				$results[] = $otherPlayer;
384
			}
385
		}
386
		return $results;
387
	}
388
389
	public function getSQL(): string {
390
		return $this->SQL;
391
	}
392
393
	public function getZoom(): int {
394
		return $this->zoom;
395
	}
396
397
	protected function setZoom(int $zoom): void {
398
		// Set the zoom level between [1, 9]
399
		$zoom = max(1, min(9, $zoom));
400
		if ($this->zoom == $zoom) {
401
			return;
402
		}
403
		$this->zoom = $zoom;
404
		$this->hasChanged = true;
405
	}
406
407
	public function increaseZoom(int $zoom): void {
408
		if ($zoom < 0) {
409
			throw new Exception('Trying to increase negative zoom.');
410
		}
411
		$this->setZoom($this->getZoom() + $zoom);
412
	}
413
414
	public function decreaseZoom(int $zoom): void {
415
		if ($zoom < 0) {
416
			throw new Exception('Trying to decrease negative zoom.');
417
		}
418
		$this->setZoom($this->getZoom() - $zoom);
419
	}
420
421
	public function getAttackColour(): string {
422
		return $this->attackColour;
423
	}
424
425
	public function setAttackColour(string $colour): void {
426
		if ($this->attackColour == $colour) {
427
			return;
428
		}
429
		$this->attackColour = $colour;
430
		$this->hasChanged = true;
431
	}
432
433
	public function isIgnoreGlobals(): bool {
434
		return $this->ignoreGlobals;
435
	}
436
437
	public function setIgnoreGlobals(bool $bool): void {
438
		if ($this->ignoreGlobals == $bool) {
439
			return;
440
		}
441
		$this->ignoreGlobals = $bool;
442
		$this->hasChanged = true;
443
	}
444
445
	public function getAccount(): SmrAccount {
446
		return SmrAccount::getAccount($this->getAccountID());
447
	}
448
449
	public function getAccountID(): int {
450
		return $this->accountID;
451
	}
452
453
	public function getGameID(): int {
454
		return $this->gameID;
455
	}
456
457
	public function getGame(bool $forceUpdate = false): SmrGame {
458
		return SmrGame::getGame($this->gameID, $forceUpdate);
459
	}
460
461
	public function getNewbieTurns(): int {
462
		return $this->newbieTurns;
463
	}
464
465
	public function hasNewbieTurns(): bool {
466
		return $this->getNewbieTurns() > 0;
467
	}
468
469
	public function setNewbieTurns(int $newbieTurns): void {
470
		if ($this->newbieTurns == $newbieTurns) {
471
			return;
472
		}
473
		$this->newbieTurns = $newbieTurns;
474
		$this->hasChanged = true;
475
	}
476
477
	public function getShip(bool $forceUpdate = false): AbstractSmrShip {
478
		return SmrShip::getShip($this, $forceUpdate);
479
	}
480
481
	public function getShipTypeID(): int {
482
		return $this->shipID;
483
	}
484
485
	/**
486
	 * Do not call directly. Use SmrShip::setTypeID instead.
487
	 */
488
	public function setShipTypeID(int $shipID): void {
489
		if ($this->shipID == $shipID) {
490
			return;
491
		}
492
		$this->shipID = $shipID;
493
		$this->hasChanged = true;
494
	}
495
496
	/**
497
	 * @phpstan-assert-if-true !false $this->getCustomShipName()
498
	 */
499
	public function hasCustomShipName(): bool {
500
		return $this->getCustomShipName() !== false;
501
	}
502
503
	public function getCustomShipName(): string|false {
504
		if (!isset($this->customShipName)) {
505
			$dbResult = $this->db->read('SELECT * FROM ship_has_name WHERE ' . $this->SQL);
506
			if ($dbResult->hasRecord()) {
507
				$this->customShipName = $dbResult->record()->getString('ship_name');
508
			} else {
509
				$this->customShipName = false;
510
			}
511
		}
512
		return $this->customShipName;
513
	}
514
515
	public function setCustomShipName(string $name): void {
516
		$this->db->replace('ship_has_name', [
517
			'game_id' => $this->db->escapeNumber($this->getGameID()),
518
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
519
			'ship_name' => $this->db->escapeString($name),
520
		]);
521
	}
522
523
	/**
524
	 * Get planet owned by this player.
525
	 * Returns null if this player does not own a planet.
526
	 */
527
	public function getPlanet(): ?SmrPlanet {
528
		$dbResult = $this->db->read('SELECT * FROM planet WHERE game_id=' . $this->db->escapeNumber($this->getGameID()) . ' AND owner_id=' . $this->db->escapeNumber($this->getAccountID()));
529
		if ($dbResult->hasRecord()) {
530
			$dbRecord = $dbResult->record();
531
			return SmrPlanet::getPlanet($this->getGameID(), $dbRecord->getInt('sector_id'), false, $dbRecord);
532
		}
533
		return null;
534
	}
535
536
	public function getSectorPlanet(): SmrPlanet {
537
		return SmrPlanet::getPlanet($this->getGameID(), $this->getSectorID());
538
	}
539
540
	public function getSectorPort(): AbstractSmrPort {
541
		return SmrPort::getPort($this->getGameID(), $this->getSectorID());
542
	}
543
544
	public function getSectorID(): int {
545
		return $this->sectorID;
546
	}
547
548
	public function getSector(): SmrSector {
549
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
550
	}
551
552
	public function setSectorID(int $sectorID): void {
553
		if ($this->sectorID == $sectorID) {
554
			return;
555
		}
556
557
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
558
		$port->addCachePort($this->getAccountID()); //Add port of sector we were just in, to make sure it is left totally up to date.
559
560
		$this->setLastSectorID($this->getSectorID());
561
		$this->actionTaken('LeaveSector', ['SectorID' => $this->getSectorID()]);
562
		$this->sectorID = $sectorID;
563
		$this->actionTaken('EnterSector', ['SectorID' => $this->getSectorID()]);
564
		$this->hasChanged = true;
565
566
		$port = SmrPort::getPort($this->getGameID(), $this->getSectorID());
567
		$port->addCachePort($this->getAccountID()); //Add the port of sector we are now in.
568
	}
569
570
	public function getLastSectorID(): int {
571
		return $this->lastSectorID;
572
	}
573
574
	public function setLastSectorID(int $lastSectorID): void {
575
		if ($this->lastSectorID == $lastSectorID) {
576
			return;
577
		}
578
		$this->lastSectorID = $lastSectorID;
579
		$this->hasChanged = true;
580
	}
581
582
	public function getHome(): int {
583
		// Draft games may have customized home sectors
584
		if ($this->getGame()->isGameType(SmrGame::GAME_TYPE_DRAFT) && $this->hasAlliance()) {
585
			$leaderID = $this->getAlliance()->getLeaderID();
586
			$dbResult = $this->db->read('SELECT home_sector_id FROM draft_leaders WHERE account_id = ' . $this->db->escapeNumber($leaderID) . ' AND game_id = ' . $this->db->escapeNumber($this->getGameID()));
587
			if ($dbResult->hasRecord()) {
588
				return $dbResult->record()->getInt('home_sector_id');
589
			}
590
		}
591
592
		// get his home sector
593
		$hq_id = GOVERNMENT + $this->getRaceID();
594
		$raceHqSectors = SmrSector::getLocationSectors($this->getGameID(), $hq_id);
595
		if (empty($raceHqSectors)) {
596
			// No HQ, default to sector 1
597
			return 1;
598
		}
599
		// If race has multiple HQ's for some reason, use the first one
600
		return key($raceHqSectors);
601
	}
602
603
	public function isDead(): bool {
604
		return $this->dead;
605
	}
606
607
	public function isNPC(): bool {
608
		return $this->npc;
609
	}
610
611
	/**
612
	 * Does the player have Newbie status?
613
	 */
614
	public function hasNewbieStatus(): bool {
615
		return $this->newbieStatus;
616
	}
617
618
	/**
619
	 * Update the player's newbie status if it has changed.
620
	 * This function queries the account, so use sparingly.
621
	 */
622
	public function updateNewbieStatus(): void {
623
		$accountNewbieStatus = !$this->getAccount()->isVeteran();
624
		if ($this->newbieStatus != $accountNewbieStatus) {
625
			$this->newbieStatus = $accountNewbieStatus;
626
			$this->hasChanged = true;
627
		}
628
	}
629
630
	/**
631
	 * Has this player been designated as the alliance flagship?
632
	 */
633
	public function isFlagship(): bool {
634
		return $this->hasAlliance() && $this->getAlliance()->getFlagshipID() == $this->getAccountID();
635
	}
636
637
	public function isPresident(): bool {
638
		return Council::getPresidentID($this->getGameID(), $this->getRaceID()) == $this->getAccountID();
639
	}
640
641
	public function isOnCouncil(): bool {
642
		return Council::isOnCouncil($this->getGameID(), $this->getRaceID(), $this->getAccountID());
643
	}
644
645
	public function isDraftLeader(): bool {
646
		if (!isset($this->draftLeader)) {
647
			$dbResult = $this->db->read('SELECT 1 FROM draft_leaders WHERE ' . $this->SQL);
648
			$this->draftLeader = $dbResult->hasRecord();
649
		}
650
		return $this->draftLeader;
651
	}
652
653
	public function getGPWriter(): string|false {
654
		if (!isset($this->gpWriter)) {
655
			$this->gpWriter = false;
656
			$dbResult = $this->db->read('SELECT position FROM galactic_post_writer WHERE ' . $this->SQL);
657
			if ($dbResult->hasRecord()) {
658
				$this->gpWriter = $dbResult->record()->getString('position');
659
			}
660
		}
661
		return $this->gpWriter;
662
	}
663
664
	public function isGPEditor(): bool {
665
		return $this->getGPWriter() == 'editor';
666
	}
667
668
	public function isForceDropMessages(): bool {
669
		return $this->forceDropMessages;
670
	}
671
672
	public function setForceDropMessages(bool $bool): void {
673
		if ($this->forceDropMessages == $bool) {
674
			return;
675
		}
676
		$this->forceDropMessages = $bool;
677
		$this->hasChanged = true;
678
	}
679
680
	public function getScoutMessageGroupLimit(): int {
681
		return match ($this->scoutMessageGroupType) {
682
			ScoutMessageGroupType::Always => 0,
683
			ScoutMessageGroupType::Auto => MESSAGES_PER_PAGE,
684
			ScoutMessageGroupType::Never => PHP_INT_MAX,
685
		};
686
	}
687
688
	public function getScoutMessageGroupType(): ScoutMessageGroupType {
689
		return $this->scoutMessageGroupType;
690
	}
691
692
	public function setScoutMessageGroupType(ScoutMessageGroupType $setting): void {
693
		if ($this->scoutMessageGroupType === $setting) {
694
			return;
695
		}
696
		$this->scoutMessageGroupType = $setting;
697
		$this->hasChanged = true;
698
	}
699
700
	/**
701
	 * @return int Message ID
702
	 */
703
	protected static function doMessageSending(int $senderID, int $receiverID, int $gameID, int $messageTypeID, string $message, int $expires, bool $senderDelete = false, bool $unread = true): int {
704
		$message = trim($message);
705
		$db = Database::getInstance();
706
		// Keep track of the message_id so it can be returned
707
		$insertID = $db->insert('message', [
708
			'account_id' => $db->escapeNumber($receiverID),
709
			'game_id' => $db->escapeNumber($gameID),
710
			'message_type_id' => $db->escapeNumber($messageTypeID),
711
			'message_text' => $db->escapeString($message),
712
			'sender_id' => $db->escapeNumber($senderID),
713
			'send_time' => $db->escapeNumber(Epoch::time()),
714
			'expire_time' => $db->escapeNumber($expires),
715
			'sender_delete' => $db->escapeBoolean($senderDelete),
716
		]);
717
718
		if ($unread === true) {
719
			// give him the message icon
720
			$db->replace('player_has_unread_messages', [
721
				'game_id' => $db->escapeNumber($gameID),
722
				'account_id' => $db->escapeNumber($receiverID),
723
				'message_type_id' => $db->escapeNumber($messageTypeID),
724
			]);
725
		}
726
727
		switch ($messageTypeID) {
728
			case MSG_PLAYER:
729
				$receiverAccount = SmrAccount::getAccount($receiverID);
730
				if ($receiverAccount->isValidated() && $receiverAccount->isReceivingMessageNotifications($messageTypeID) && !$receiverAccount->isActive()) {
731
					$sender = Messages::getMessagePlayer($senderID, $gameID, $messageTypeID);
732
					if ($sender instanceof self) {
0 ignored issues
show
introduced by
$sender is never a sub-type of self.
Loading history...
733
						$sender = $sender->getDisplayName();
734
					}
735
					$mail = setupMailer();
0 ignored issues
show
Bug introduced by
The function setupMailer was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

735
					$mail = /** @scrutinizer ignore-call */ setupMailer();
Loading history...
736
					$mail->Subject = 'Message Notification';
737
					$mail->setFrom('[email protected]', 'SMR Notifications');
738
					$bbifiedMessage = 'From: ' . $sender . ' Date: ' . date($receiverAccount->getDateTimeFormat(), Epoch::time()) . "<br/>\r\n<br/>\r\n" . bbifyMessage($message, $gameID, true);
739
					$mail->msgHTML($bbifiedMessage);
740
					$mail->AltBody = strip_tags($bbifiedMessage);
741
					$mail->addAddress($receiverAccount->getEmail(), $receiverAccount->getHofName());
742
					$mail->send();
743
					$receiverAccount->decreaseMessageNotifications($messageTypeID, 1);
744
					$receiverAccount->update();
745
				}
746
				break;
747
		}
748
749
		return $insertID;
750
	}
751
752
	public function sendMessageToBox(int $boxTypeID, string $message): void {
753
		// send him the message
754
		SmrAccount::doMessageSendingToBox($this->getAccountID(), $boxTypeID, $message, $this->getGameID());
755
	}
756
757
	public function sendGlobalMessage(string $message, bool $canBeIgnored = true): void {
758
		if ($canBeIgnored) {
759
			if ($this->getAccount()->isMailBanned()) {
760
				throw new UserError('You are currently banned from sending messages');
761
			}
762
		}
763
		$this->sendMessageToBox(BOX_GLOBALS, $message);
764
765
		// send to all online player
766
		$db = Database::getInstance();
767
		$dbResult = $db->read('SELECT account_id
768
					FROM active_session
769
					JOIN player USING (game_id, account_id)
770
					WHERE active_session.last_accessed >= ' . $db->escapeNumber(Epoch::time() - TIME_BEFORE_INACTIVE) . '
771
						AND game_id = ' . $db->escapeNumber($this->getGameID()) . '
772
						AND ignore_globals = \'FALSE\'
773
						AND account_id != ' . $db->escapeNumber($this->getAccountID()));
774
775
		foreach ($dbResult->records() as $dbRecord) {
776
			$this->sendMessage($dbRecord->getInt('account_id'), MSG_GLOBAL, $message, $canBeIgnored);
777
		}
778
		$this->sendMessage($this->getAccountID(), MSG_GLOBAL, $message, $canBeIgnored, false);
779
	}
780
781
	/**
782
	 * @return ($canBeIgnored is true ? int|false : int) Message ID
0 ignored issues
show
Documentation Bug introduced by
The doc comment ($canBeIgnored at position 1 could not be parsed: Unknown type name '$canBeIgnored' at position 1 in ($canBeIgnored.
Loading history...
783
	 */
784
	public function sendMessage(int $receiverID, int $messageTypeID, string $message, bool $canBeIgnored = true, bool $unread = true, int $expires = null, bool $senderDelete = false): int|false {
785
		//get expire time
786
		if ($canBeIgnored) {
787
			if ($this->getAccount()->isMailBanned()) {
788
				throw new UserError('You are currently banned from sending messages');
789
			}
790
			// Don't send messages to players ignoring us
791
			$dbResult = $this->db->read('SELECT 1 FROM message_blacklist WHERE account_id=' . $this->db->escapeNumber($receiverID) . ' AND blacklisted_id=' . $this->db->escapeNumber($this->getAccountID()) . ' LIMIT 1');
792
			if ($dbResult->hasRecord()) {
793
				return false;
794
			}
795
		}
796
797
		$message = word_filter($message);
798
799
		// If expires not specified, use default based on message type
800
		if ($expires === null) {
801
			$expires = match ($messageTypeID) {
802
				MSG_GLOBAL => 3600, // 1h
803
				MSG_PLAYER => 86400 * 31, // 1 month
804
				MSG_PLANET => 86400 * 7, // 1 week
805
				MSG_SCOUT => 86400 * 3, // 3 days
806
				MSG_POLITICAL => 86400 * 31, // 1 month
807
				MSG_ALLIANCE => 86400 * 31, // 1 month
808
				MSG_ADMIN => 86400 * 365, // 1 year
809
				MSG_CASINO => 86400 * 31, // 1 month
810
				default => 86400 * 7, // 1 week
811
			};
812
			$expires += Epoch::time();
813
		}
814
815
		// Do not put scout messages in the sender's sent box
816
		if ($messageTypeID == MSG_SCOUT) {
817
			$senderDelete = true;
818
		}
819
820
		// send him the message and return the message_id
821
		return self::doMessageSending($this->getAccountID(), $receiverID, $this->getGameID(), $messageTypeID, $message, $expires, $senderDelete, $unread);
822
	}
823
824
	public function sendMessageFromOpAnnounce(int $receiverID, string $message, int $expires = null): void {
825
		// get expire time if not set
826
		if ($expires === null) {
827
			$expires = Epoch::time() + 86400 * 14;
828
		}
829
		self::doMessageSending(ACCOUNT_ID_OP_ANNOUNCE, $receiverID, $this->getGameID(), MSG_ALLIANCE, $message, $expires);
830
	}
831
832
	public function sendMessageFromAllianceCommand(int $receiverID, string $message): void {
833
		$expires = Epoch::time() + 86400 * 365;
834
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_COMMAND, $receiverID, $this->getGameID(), MSG_PLAYER, $message, $expires);
835
	}
836
837
	public static function sendMessageFromPlanet(int $gameID, int $receiverID, string $message): void {
838
		//get expire time
839
		$expires = Epoch::time() + 86400 * 31;
840
		// send him the message
841
		self::doMessageSending(ACCOUNT_ID_PLANET, $receiverID, $gameID, MSG_PLANET, $message, $expires);
842
	}
843
844
	public static function sendMessageFromPort(int $gameID, int $receiverID, string $message): void {
845
		//get expire time
846
		$expires = Epoch::time() + 86400 * 31;
847
		// send him the message
848
		self::doMessageSending(ACCOUNT_ID_PORT, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
849
	}
850
851
	public static function sendMessageFromFedClerk(int $gameID, int $receiverID, string $message): void {
852
		$expires = Epoch::time() + 86400 * 365;
853
		self::doMessageSending(ACCOUNT_ID_FED_CLERK, $receiverID, $gameID, MSG_PLAYER, $message, $expires);
854
	}
855
856
	public static function sendMessageFromAdmin(int $gameID, int $receiverID, string $message, int $expires = null): void {
857
		//get expire time
858
		if ($expires === null) {
859
			$expires = Epoch::time() + 86400 * 365;
860
		}
861
		// send him the message
862
		self::doMessageSending(ACCOUNT_ID_ADMIN, $receiverID, $gameID, MSG_ADMIN, $message, $expires);
863
	}
864
865
	public static function sendMessageFromAllianceAmbassador(int $gameID, int $receiverID, string $message, int $expires = null): void {
866
		//get expire time
867
		if ($expires === null) {
868
			$expires = Epoch::time() + 86400 * 31;
869
		}
870
		// send him the message
871
		self::doMessageSending(ACCOUNT_ID_ALLIANCE_AMBASSADOR, $receiverID, $gameID, MSG_ALLIANCE, $message, $expires);
872
	}
873
874
	public static function sendMessageFromCasino(int $gameID, int $receiverID, string $message, int $expires = null): void {
875
		//get expire time
876
		if ($expires === null) {
877
			$expires = Epoch::time() + 86400 * 7;
878
		}
879
		// send him the message
880
		self::doMessageSending(ACCOUNT_ID_CASINO, $receiverID, $gameID, MSG_CASINO, $message, $expires);
881
	}
882
883
	public static function sendMessageFromRace(int $raceID, int $gameID, int $receiverID, string $message, int $expires = null): void {
884
		//get expire time
885
		if ($expires === null) {
886
			$expires = Epoch::time() + 86400 * 5;
887
		}
888
		// send him the message
889
		self::doMessageSending(ACCOUNT_ID_GROUP_RACES + $raceID, $receiverID, $gameID, MSG_POLITICAL, $message, $expires);
890
	}
891
892
	public function setMessagesRead(int $messageTypeID): void {
893
		$this->db->write('DELETE FROM player_has_unread_messages
894
							WHERE ' . $this->SQL . ' AND message_type_id = ' . $this->db->escapeNumber($messageTypeID));
895
		$this->db->write('UPDATE message SET msg_read = ' . $this->db->escapeBoolean(true) . '
896
				WHERE message_type_id = ' . $this->db->escapeNumber($messageTypeID) . ' AND ' . $this->SQL);
897
	}
898
899
	public function getSafeAttackRating(): int {
900
		return max(0, min(8, IFloor($this->getAlignment() / 150) + 4));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

900
		return max(0, min(8, /** @scrutinizer ignore-call */ IFloor($this->getAlignment() / 150) + 4));
Loading history...
901
	}
902
903
	public function hasFederalProtection(): bool {
904
		$sector = SmrSector::getSector($this->getGameID(), $this->getSectorID());
905
		if (!$sector->offersFederalProtection()) {
906
			return false;
907
		}
908
909
		$ship = $this->getShip();
910
		if ($ship->hasIllegalGoods()) {
911
			return false;
912
		}
913
914
		if ($ship->getAttackRating() <= $this->getSafeAttackRating()) {
915
			foreach ($sector->getFedRaceIDs() as $fedRaceID) {
916
				if ($this->canBeProtectedByRace($fedRaceID)) {
917
					return true;
918
				}
919
			}
920
		}
921
922
		return false;
923
	}
924
925
	public function canBeProtectedByRace(int $raceID): bool {
926
		if (!isset($this->canFed)) {
927
			$this->canFed = [];
928
			foreach (Race::getAllIDs() as $raceID2) {
929
				$this->canFed[$raceID2] = $this->getRelation($raceID2) >= ALIGN_FED_PROTECTION;
930
			}
931
			$dbResult = $this->db->read('SELECT race_id, allowed FROM player_can_fed
932
								WHERE ' . $this->SQL . ' AND expiry > ' . $this->db->escapeNumber(Epoch::time()));
933
			foreach ($dbResult->records() as $dbRecord) {
934
				$this->canFed[$dbRecord->getInt('race_id')] = $dbRecord->getBoolean('allowed');
935
			}
936
		}
937
		return $this->canFed[$raceID];
938
	}
939
940
	/**
941
	 * Returns a boolean identifying if the player can currently
942
	 * participate in battles.
943
	 */
944
	public function canFight(): bool {
945
		return !($this->hasNewbieTurns() ||
946
		         $this->isDead() ||
947
		         $this->isLandedOnPlanet() ||
948
		         $this->hasFederalProtection());
949
	}
950
951
	public function setDead(bool $bool): void {
952
		if ($this->dead == $bool) {
953
			return;
954
		}
955
		$this->dead = $bool;
956
		$this->hasChanged = true;
957
	}
958
959
	public function getKills(): int {
960
		return $this->kills;
961
	}
962
963
	public function increaseKills(int $kills): void {
964
		if ($kills < 0) {
965
			throw new Exception('Trying to increase negative kills.');
966
		}
967
		$this->setKills($this->kills + $kills);
968
	}
969
970
	public function setKills(int $kills): void {
971
		if ($this->kills == $kills) {
972
			return;
973
		}
974
		$this->kills = $kills;
975
		$this->hasChanged = true;
976
	}
977
978
	public function getDeaths(): int {
979
		return $this->deaths;
980
	}
981
982
	public function increaseDeaths(int $deaths): void {
983
		if ($deaths < 0) {
984
			throw new Exception('Trying to increase negative deaths.');
985
		}
986
		$this->setDeaths($this->getDeaths() + $deaths);
987
	}
988
989
	public function setDeaths(int $deaths): void {
990
		if ($this->deaths == $deaths) {
991
			return;
992
		}
993
		$this->deaths = $deaths;
994
		$this->hasChanged = true;
995
	}
996
997
	public function getAssists(): int {
998
		return $this->assists;
999
	}
1000
1001
	public function increaseAssists(int $assists): void {
1002
		if ($assists < 1) {
1003
			throw new Exception('Must increase by a positive number.');
1004
		}
1005
		$this->assists += $assists;
1006
		$this->hasChanged = true;
1007
	}
1008
1009
	public function hasGoodAlignment(): bool {
1010
		return $this->alignment >= ALIGNMENT_GOOD;
1011
	}
1012
1013
	public function hasEvilAlignment(): bool {
1014
		return $this->alignment <= ALIGNMENT_EVIL;
1015
	}
1016
1017
	public function hasNeutralAlignment(): bool {
1018
		return !$this->hasGoodAlignment() && !$this->hasEvilAlignment();
1019
	}
1020
1021
	public function getAlignment(): int {
1022
		return $this->alignment;
1023
	}
1024
1025
	public function increaseAlignment(int $align): void {
1026
		if ($align < 0) {
1027
			throw new Exception('Trying to increase negative align.');
1028
		}
1029
		if ($align == 0) {
1030
			return;
1031
		}
1032
		$align += $this->alignment;
1033
		$this->setAlignment($align);
1034
	}
1035
1036
	public function decreaseAlignment(int $align): void {
1037
		if ($align < 0) {
1038
			throw new Exception('Trying to decrease negative align.');
1039
		}
1040
		if ($align == 0) {
1041
			return;
1042
		}
1043
		$align = $this->alignment - $align;
1044
		$this->setAlignment($align);
1045
	}
1046
1047
	public function setAlignment(int $align): void {
1048
		if ($this->alignment == $align) {
1049
			return;
1050
		}
1051
		$this->alignment = $align;
1052
		$this->hasChanged = true;
1053
	}
1054
1055
	public function getCredits(): int {
1056
		return $this->credits;
1057
	}
1058
1059
	public function getBank(): int {
1060
		return $this->bank;
1061
	}
1062
1063
	/**
1064
	 * Increases personal bank account up to the maximum allowed credits.
1065
	 * Returns the amount that was actually added to handle overflow.
1066
	 */
1067
	public function increaseBank(int $credits): int {
1068
		if ($credits == 0) {
1069
			return 0;
1070
		}
1071
		if ($credits < 0) {
1072
			throw new Exception('Trying to increase negative credits.');
1073
		}
1074
		$newTotal = min($this->bank + $credits, MAX_MONEY);
1075
		$actualAdded = $newTotal - $this->bank;
1076
		$this->setBank($newTotal);
1077
		return $actualAdded;
1078
	}
1079
1080
	public function decreaseBank(int $credits): void {
1081
		if ($credits == 0) {
1082
			return;
1083
		}
1084
		if ($credits < 0) {
1085
			throw new Exception('Trying to decrease negative credits.');
1086
		}
1087
		$newTotal = $this->bank - $credits;
1088
		$this->setBank($newTotal);
1089
	}
1090
1091
	public function setBank(int $credits): void {
1092
		if ($this->bank == $credits) {
1093
			return;
1094
		}
1095
		if ($credits < 0) {
1096
			throw new Exception('Trying to set negative credits.');
1097
		}
1098
		if ($credits > MAX_MONEY) {
1099
			throw new Exception('Trying to set more than max credits.');
1100
		}
1101
		$this->bank = $credits;
1102
		$this->hasChanged = true;
1103
	}
1104
1105
	public function getExperience(): int {
1106
		return $this->experience;
1107
	}
1108
1109
	/**
1110
	 * Returns the percent progress towards the next level.
1111
	 * This value is rounded because it is used primarily in HTML img widths.
1112
	 */
1113
	public function getNextLevelPercentAcquired(): int {
1114
		$currentLevelExp = $this->getLevel()->expRequired;
1115
		$nextLevelExp = $this->getLevel()->next()->expRequired;
1116
		if ($nextLevelExp == $currentLevelExp) {
1117
			return 100;
1118
		}
1119
		return max(0, min(100, IRound(($this->getExperience() - $currentLevelExp) / ($nextLevelExp - $currentLevelExp) * 100)));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1119
		return max(0, min(100, /** @scrutinizer ignore-call */ IRound(($this->getExperience() - $currentLevelExp) / ($nextLevelExp - $currentLevelExp) * 100)));
Loading history...
1120
	}
1121
1122
	public function getNextLevelPercentRemaining(): int {
1123
		return 100 - $this->getNextLevelPercentAcquired();
1124
	}
1125
1126
	public function setExperience(int $experience): void {
1127
		if ($this->experience == $experience) {
1128
			return;
1129
		}
1130
		if ($experience < MIN_EXPERIENCE) {
1131
			$experience = MIN_EXPERIENCE;
1132
		}
1133
		if ($experience > MAX_EXPERIENCE) {
1134
			$experience = MAX_EXPERIENCE;
1135
		}
1136
		$this->experience = $experience;
1137
		$this->hasChanged = true;
1138
1139
		// Since exp has changed, invalidate the player level so that it can
1140
		// be recomputed next time it is queried (in case it has changed).
1141
		$this->level = null;
1142
	}
1143
1144
	/**
1145
	 * Increases onboard credits up to the maximum allowed credits.
1146
	 * Returns the amount that was actually added to handle overflow.
1147
	 */
1148
	public function increaseCredits(int $credits): int {
1149
		if ($credits == 0) {
1150
			return 0;
1151
		}
1152
		if ($credits < 0) {
1153
			throw new Exception('Trying to increase negative credits.');
1154
		}
1155
		$newTotal = min($this->credits + $credits, MAX_MONEY);
1156
		$actualAdded = $newTotal - $this->credits;
1157
		$this->setCredits($newTotal);
1158
		return $actualAdded;
1159
	}
1160
1161
	public function decreaseCredits(int $credits): void {
1162
		if ($credits == 0) {
1163
			return;
1164
		}
1165
		if ($credits < 0) {
1166
			throw new Exception('Trying to decrease negative credits.');
1167
		}
1168
		$newTotal = $this->credits - $credits;
1169
		$this->setCredits($newTotal);
1170
	}
1171
1172
	public function setCredits(int $credits): void {
1173
		if ($this->credits == $credits) {
1174
			return;
1175
		}
1176
		if ($credits < 0) {
1177
			throw new Exception('Trying to set negative credits.');
1178
		}
1179
		if ($credits > MAX_MONEY) {
1180
			throw new Exception('Trying to set more than max credits.');
1181
		}
1182
		$this->credits = $credits;
1183
		$this->hasChanged = true;
1184
	}
1185
1186
	public function increaseExperience(int $experience): void {
1187
		if ($experience < 0) {
1188
			throw new Exception('Trying to increase negative experience.');
1189
		}
1190
		if ($experience == 0) {
1191
			return;
1192
		}
1193
		$newExperience = $this->experience + $experience;
1194
		$this->setExperience($newExperience);
1195
		$this->increaseHOF($experience, ['Experience', 'Total', 'Gain'], HOF_PUBLIC);
1196
	}
1197
1198
	public function decreaseExperience(int $experience): void {
1199
		if ($experience < 0) {
1200
			throw new Exception('Trying to decrease negative experience.');
1201
		}
1202
		if ($experience == 0) {
1203
			return;
1204
		}
1205
		$newExperience = $this->experience - $experience;
1206
		$this->setExperience($newExperience);
1207
		$this->increaseHOF($experience, ['Experience', 'Total', 'Loss'], HOF_PUBLIC);
1208
	}
1209
1210
	public function isLandedOnPlanet(): bool {
1211
		return $this->landedOnPlanet;
1212
	}
1213
1214
	public function setLandedOnPlanet(bool $bool): void {
1215
		if ($this->landedOnPlanet == $bool) {
1216
			return;
1217
		}
1218
		$this->landedOnPlanet = $bool;
1219
		$this->hasChanged = true;
1220
	}
1221
1222
	public function getLevel(): PlayerLevel {
1223
		// The level is cached for performance reasons unless `setExperience`
1224
		// is called and the player's experience changes.
1225
		if (!isset($this->level)) {
1226
			$this->level = PlayerLevel::get($this->getExperience());
1227
		}
1228
		return $this->level;
1229
	}
1230
1231
	/**
1232
	 * Returns the numerical level of the player (e.g. 1-50).
1233
	 */
1234
	public function getLevelID(): int {
1235
		return $this->getLevel()->id;
1236
	}
1237
1238
	public function getLevelName(): string {
1239
		$level_name = $this->getLevel()->name;
1240
		if ($this->isPresident()) {
1241
			$level_name = '<img src="images/council_president.png" title="' . Race::getName($this->getRaceID()) . ' President" height="12" width="16" />&nbsp;' . $level_name;
1242
		}
1243
		return $level_name;
1244
	}
1245
1246
	public function getMaxLevel(): int {
1247
		return PlayerLevel::getMax();
1248
	}
1249
1250
	public function getPlayerID(): int {
1251
		return $this->playerID;
1252
	}
1253
1254
	/**
1255
	 * Returns the player name.
1256
	 * Use getDisplayName or getLinkedDisplayName for HTML-safe versions.
1257
	 */
1258
	public function getPlayerName(): string {
1259
		return $this->playerName;
1260
	}
1261
1262
	public function setPlayerName(string $name): void {
1263
		$this->playerName = $name;
1264
		$this->hasChanged = true;
1265
	}
1266
1267
	/**
1268
	 * Returns the decorated player name, suitable for HTML display.
1269
	 */
1270
	public function getDisplayName(bool $includeAlliance = false): string {
1271
		$name = htmlentities($this->playerName) . ' (' . $this->getPlayerID() . ')';
1272
		$return = get_colored_text($this->getAlignment(), $name);
1273
		if ($this->isNPC()) {
1274
			$return .= ' <span class="npcColour">[NPC]</span>';
1275
		}
1276
		if ($includeAlliance) {
1277
			$return .= ' (' . $this->getAllianceDisplayName() . ')';
1278
		}
1279
		return $return;
1280
	}
1281
1282
	public function getBBLink(): string {
1283
			return '[player=' . $this->getPlayerID() . ']';
1284
	}
1285
1286
	public function getLinkedDisplayName(bool $includeAlliance = true): string {
1287
		$return = '<a href="' . $this->getTraderSearchHREF() . '">' . $this->getDisplayName() . '</a>';
1288
		if ($includeAlliance) {
1289
			$return .= ' (' . $this->getAllianceDisplayName(true) . ')';
1290
		}
1291
		return $return;
1292
	}
1293
1294
	/**
1295
	 * Change a player's name, with name validation.
1296
	 *
1297
	 * @throws Smr\Exceptions\UserError When the new name is not permitted.
1298
	 */
1299
	public function changePlayerName(string $name): void {
1300
		// Check if the player already has this name (case-sensitive)
1301
		if ($this->getPlayerName() == $name) {
1302
			throw new UserError('Your player already has that name!');
1303
		}
1304
1305
		// Make sure the name passes some basic character requirements
1306
		DisplayNameValidator::validate($name);
1307
1308
		// Check if name is in use by any other player.
1309
		try {
1310
			$other = self::getPlayerByPlayerName($name, $this->getGameID());
1311
			// The player_name field has case-insensitive collation, so if we
1312
			// find our own player, then it is because the new name has a
1313
			// different case (since we did a case-sensitive identity check
1314
			// above), and we allow it to be changed.
1315
			if ($this->equals($other)) {
1316
				throw new PlayerNotFound();
1317
			}
1318
			throw new UserError('That name is already being used in this game!');
1319
		} catch (PlayerNotFound) {
1320
			// Name is not in use, continue.
1321
		}
1322
1323
		$this->setPlayerName($name);
1324
	}
1325
1326
	/**
1327
	 * Use this method when the player is changing their own name.
1328
	 * This will flag the player as having used their free name change.
1329
	 */
1330
	public function changePlayerNameByPlayer(string $playerName): void {
1331
		$this->changePlayerName($playerName);
1332
		$this->setNameChanged(true);
1333
	}
1334
1335
	public function isNameChanged(): bool {
1336
		return $this->nameChanged;
1337
	}
1338
1339
	public function setNameChanged(bool $bool): void {
1340
		$this->nameChanged = $bool;
1341
		$this->hasChanged = true;
1342
	}
1343
1344
	public function isRaceChanged(): bool {
1345
		return $this->raceChanged;
1346
	}
1347
1348
	public function setRaceChanged(bool $raceChanged): void {
1349
		$this->raceChanged = $raceChanged;
1350
		$this->hasChanged = true;
1351
	}
1352
1353
	public function canChangeRace(): bool {
1354
		return !$this->isRaceChanged() && (Epoch::time() - $this->getGame()->getStartTime() < TIME_FOR_RACE_CHANGE);
1355
	}
1356
1357
	public static function getColouredRaceNameOrDefault(int $otherRaceID, self $player = null, bool $linked = false): string {
1358
		$relations = 0;
1359
		if ($player !== null) {
1360
			$relations = $player->getRelation($otherRaceID);
1361
		}
1362
		return Globals::getColouredRaceName($otherRaceID, $relations, $linked);
1363
	}
1364
1365
	public function getColouredRaceName(int $otherRaceID, bool $linked = false): string {
1366
		return self::getColouredRaceNameOrDefault($otherRaceID, $this, $linked);
1367
	}
1368
1369
	public function setRaceID(int $raceID): void {
1370
		if ($this->raceID == $raceID) {
1371
			return;
1372
		}
1373
		$this->raceID = $raceID;
1374
		$this->hasChanged = true;
1375
	}
1376
1377
	public function isAllianceLeader(bool $forceUpdate = false): bool {
1378
		return $this->getAccountID() == $this->getAlliance($forceUpdate)->getLeaderID();
1379
	}
1380
1381
	public function getAlliance(bool $forceUpdate = false): SmrAlliance {
1382
		return SmrAlliance::getAlliance($this->getAllianceID(), $this->getGameID(), $forceUpdate);
1383
	}
1384
1385
	public function getAllianceID(): int {
1386
		return $this->allianceID;
1387
	}
1388
1389
	public function hasAlliance(): bool {
1390
		return $this->getAllianceID() != 0;
1391
	}
1392
1393
	protected function setAllianceID(int $ID): void {
1394
		if ($this->allianceID == $ID) {
1395
			return;
1396
		}
1397
		$this->allianceID = $ID;
1398
		if ($this->allianceID != 0) {
1399
			$status = $this->hasNewbieStatus() ? 'NEWBIE' : 'VETERAN';
1400
			$this->db->write('INSERT IGNORE INTO player_joined_alliance (account_id,game_id,alliance_id,status) ' .
1401
				'VALUES (' . $this->db->escapeNumber($this->getAccountID()) . ',' . $this->db->escapeNumber($this->getGameID()) . ',' . $this->db->escapeNumber($this->getAllianceID()) . ',' . $this->db->escapeString($status) . ')');
1402
		}
1403
		$this->hasChanged = true;
1404
	}
1405
1406
	public function getAllianceBBLink(): string {
1407
		return $this->hasAlliance() ? $this->getAlliance()->getAllianceBBLink() : $this->getAllianceDisplayName();
1408
	}
1409
1410
	public function getAllianceDisplayName(bool $linked = false, bool $includeAllianceID = false): string {
1411
		if (!$this->hasAlliance()) {
1412
			return 'No Alliance';
1413
		}
1414
		return $this->getAlliance()->getAllianceDisplayName($linked, $includeAllianceID);
1415
	}
1416
1417
	public function getAllianceRole(int $allianceID = null): int {
1418
		if ($allianceID === null) {
1419
			$allianceID = $this->getAllianceID();
1420
		}
1421
		if (!isset($this->allianceRoles[$allianceID])) {
1422
			$this->allianceRoles[$allianceID] = 0;
1423
			$dbResult = $this->db->read('SELECT role_id
1424
						FROM player_has_alliance_role
1425
						WHERE ' . $this->SQL . '
1426
						AND alliance_id=' . $this->db->escapeNumber($allianceID));
1427
			if ($dbResult->hasRecord()) {
1428
				$this->allianceRoles[$allianceID] = $dbResult->record()->getInt('role_id');
1429
			}
1430
		}
1431
		return $this->allianceRoles[$allianceID];
1432
	}
1433
1434
	public function leaveAlliance(self $kickedBy = null): void {
1435
		$alliance = $this->getAlliance();
1436
		if ($kickedBy !== null) {
1437
			$kickedBy->sendMessage($this->getAccountID(), MSG_PLAYER, 'You were kicked out of the alliance!', false);
1438
			$this->actionTaken('PlayerKicked', ['Alliance' => $alliance, 'Player' => $kickedBy]);
1439
			$kickedBy->actionTaken('KickPlayer', ['Alliance' => $alliance, 'Player' => $this]);
1440
		} elseif ($this->isAllianceLeader()) {
1441
			$this->actionTaken('DisbandAlliance', ['Alliance' => $alliance]);
1442
		} else {
1443
			$this->actionTaken('LeaveAlliance', ['Alliance' => $alliance]);
1444
			if ($alliance->getLeaderID() != 0 && $alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1445
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I left your alliance!', false);
1446
			}
1447
		}
1448
1449
		// Don't have a delay for switching alliance after leaving NHA, or for disbanding an alliance.
1450
		if (!$this->isAllianceLeader() && !$alliance->isNHA()) {
1451
			$this->setAllianceJoinable(Epoch::time() + self::TIME_FOR_ALLIANCE_SWITCH);
1452
			$alliance->getLeader()->setAllianceJoinable(Epoch::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.
1453
		}
1454
1455
		$this->setAllianceID(0);
1456
		$this->db->write('DELETE FROM player_has_alliance_role WHERE ' . $this->SQL);
1457
1458
		// Update the alliance cache
1459
		unset(self::$CACHE_ALLIANCE_PLAYERS[$this->gameID][$alliance->getAllianceID()][$this->accountID]);
1460
	}
1461
1462
	/**
1463
	 * Join an alliance (used for both Leader and New Member roles)
1464
	 */
1465
	public function joinAlliance(int $allianceID): void {
1466
		$this->setAllianceID($allianceID);
1467
		$alliance = $this->getAlliance();
1468
1469
		if (!$this->isAllianceLeader()) {
1470
			// Do not throw an exception if the NHL account doesn't exist.
1471
			try {
1472
				$this->sendMessage($alliance->getLeaderID(), MSG_PLAYER, 'I joined your alliance!', false);
1473
			} catch (AccountNotFound $e) {
1474
				if ($alliance->getLeaderID() != ACCOUNT_ID_NHL) {
1475
					throw $e;
1476
				}
1477
			}
1478
1479
			$roleID = ALLIANCE_ROLE_NEW_MEMBER;
1480
		} else {
1481
			$roleID = ALLIANCE_ROLE_LEADER;
1482
		}
1483
		$this->db->insert('player_has_alliance_role', [
1484
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1485
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1486
			'role_id' => $this->db->escapeNumber($roleID),
1487
			'alliance_id' => $this->db->escapeNumber($this->getAllianceID()),
1488
		]);
1489
1490
		$this->actionTaken('JoinAlliance', ['Alliance' => $alliance]);
1491
	}
1492
1493
	public function getAllianceJoinable(): int {
1494
		return $this->allianceJoinable;
1495
	}
1496
1497
	private function setAllianceJoinable(int $time): void {
1498
		if ($this->allianceJoinable == $time) {
1499
			return;
1500
		}
1501
		$this->allianceJoinable = $time;
1502
		$this->hasChanged = true;
1503
	}
1504
1505
	/**
1506
	 * Invites player with $accountID to this player's alliance.
1507
	 */
1508
	public function sendAllianceInvitation(int $accountID, string $message, int $expires): void {
1509
		if (!$this->hasAlliance()) {
1510
			throw new Exception('Must be in an alliance to send alliance invitations');
1511
		}
1512
		// Send message to invited player
1513
		$messageID = $this->sendMessage($accountID, MSG_PLAYER, $message, false, true, $expires, true);
1514
		SmrInvitation::send($this->getAllianceID(), $this->getGameID(), $accountID, $this->getAccountID(), $messageID, $expires);
1515
	}
1516
1517
	public function isCombatDronesKamikazeOnMines(): bool {
1518
		return $this->combatDronesKamikazeOnMines;
1519
	}
1520
1521
	public function setCombatDronesKamikazeOnMines(bool $bool): void {
1522
		if ($this->combatDronesKamikazeOnMines == $bool) {
1523
			return;
1524
		}
1525
		$this->combatDronesKamikazeOnMines = $bool;
1526
		$this->hasChanged = true;
1527
	}
1528
1529
	protected function getPersonalRelationsData(): void {
1530
		if (!isset($this->personalRelations)) {
1531
			//get relations
1532
			$this->personalRelations = [];
1533
			foreach (Race::getAllIDs() as $raceID) {
1534
				$this->personalRelations[$raceID] = 0;
1535
			}
1536
			$dbResult = $this->db->read('SELECT race_id,relation FROM player_has_relation WHERE ' . $this->SQL);
1537
			foreach ($dbResult->records() as $dbRecord) {
1538
				$this->personalRelations[$dbRecord->getInt('race_id')] = $dbRecord->getInt('relation');
1539
			}
1540
		}
1541
	}
1542
1543
	/**
1544
	 * @return array<int, int>
1545
	 */
1546
	public function getPersonalRelations(): array {
1547
		$this->getPersonalRelationsData();
1548
		return $this->personalRelations;
1549
	}
1550
1551
	/**
1552
	 * Get personal relations with a race
1553
	 */
1554
	public function getPersonalRelation(int $raceID): int {
1555
		$rels = $this->getPersonalRelations();
1556
		return $rels[$raceID];
1557
	}
1558
1559
	/**
1560
	 * Get total relations with all races (personal + political)
1561
	 *
1562
	 * @return array<int, int>
1563
	 */
1564
	public function getRelations(): array {
1565
		if (!isset($this->relations)) {
1566
			//get relations
1567
			$raceRelations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
1568
			$personalRels = $this->getPersonalRelations(); // make sure they're initialised.
1569
			$this->relations = [];
1570
			foreach (Race::getAllIDs() as $raceID) {
1571
				$this->relations[$raceID] = $personalRels[$raceID] + $raceRelations[$raceID];
1572
			}
1573
		}
1574
		return $this->relations;
1575
	}
1576
1577
	/**
1578
	 * Get total relations with a race (personal + political)
1579
	 */
1580
	public function getRelation(int $raceID): int {
1581
		$rels = $this->getRelations();
1582
		return $rels[$raceID];
1583
	}
1584
1585
	/**
1586
	 * Increases personal relations from trading $numGoods units with the race
1587
	 * of the port given by $raceID.
1588
	 */
1589
	public function increaseRelationsByTrade(int $numGoods, int $raceID): void {
1590
		$relations = ICeil(min($numGoods, 300) / 30);
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1590
		$relations = /** @scrutinizer ignore-call */ ICeil(min($numGoods, 300) / 30);
Loading history...
1591
		//Cap relations to a max of 1 after 500 have been reached
1592
		if ($this->getPersonalRelation($raceID) + $relations >= 500) {
1593
			$relations = max(1, min($relations, 500 - $this->getPersonalRelation($raceID)));
1594
		}
1595
		$this->increaseRelations($relations, $raceID);
1596
	}
1597
1598
	/**
1599
	 * Decreases personal relations from trading failures, e.g. rejected
1600
	 * bargaining and getting caught stealing.
1601
	 */
1602
	public function decreaseRelationsByTrade(int $numGoods, int $raceID): void {
1603
		$relations = ICeil(min($numGoods, 300) / 30);
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1603
		$relations = /** @scrutinizer ignore-call */ ICeil(min($numGoods, 300) / 30);
Loading history...
1604
		$this->decreaseRelations($relations, $raceID);
1605
	}
1606
1607
	/**
1608
	 * Increase personal relations.
1609
	 */
1610
	public function increaseRelations(int $relations, int $raceID): void {
1611
		if ($relations < 0) {
1612
			throw new Exception('Trying to increase negative relations.');
1613
		}
1614
		if ($relations == 0) {
1615
			return;
1616
		}
1617
		$relations += $this->getPersonalRelation($raceID);
1618
		$this->setRelations($relations, $raceID);
1619
	}
1620
1621
	/**
1622
	 * Decrease personal relations.
1623
	 */
1624
	public function decreaseRelations(int $relations, int $raceID): void {
1625
		if ($relations < 0) {
1626
			throw new Exception('Trying to decrease negative relations.');
1627
		}
1628
		if ($relations == 0) {
1629
			return;
1630
		}
1631
		$relations = $this->getPersonalRelation($raceID) - $relations;
1632
		$this->setRelations($relations, $raceID);
1633
	}
1634
1635
	/**
1636
	 * Set personal relations.
1637
	 */
1638
	public function setRelations(int $relations, int $raceID): void {
1639
		$this->getRelations();
1640
		if ($this->personalRelations[$raceID] == $relations) {
1641
			return;
1642
		}
1643
		if ($relations < MIN_RELATIONS) {
1644
			$relations = MIN_RELATIONS;
1645
		}
1646
		$relationsDiff = IRound($relations - $this->personalRelations[$raceID]);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1646
		$relationsDiff = /** @scrutinizer ignore-call */ IRound($relations - $this->personalRelations[$raceID]);
Loading history...
1647
		$this->personalRelations[$raceID] = $relations;
1648
		$this->relations[$raceID] += $relationsDiff;
1649
		$this->db->replace('player_has_relation', [
1650
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1651
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1652
			'race_id' => $this->db->escapeNumber($raceID),
1653
			'relation' => $this->db->escapeNumber($this->personalRelations[$raceID]),
1654
		]);
1655
	}
1656
1657
	/**
1658
	 * Set any starting personal relations bonuses or penalties.
1659
	 */
1660
	public function giveStartingRelations(): void {
1661
		if ($this->getRaceID() === RACE_ALSKANT) {
1662
			// Give Alskants bonus personal relations to start.
1663
			foreach (Race::getAllIDs() as $raceID) {
1664
				$this->setRelations(ALSKANT_BONUS_RELATIONS, $raceID);
1665
			}
1666
		}
1667
	}
1668
1669
	public function getLastNewsUpdate(): int {
1670
		return $this->lastNewsUpdate;
1671
	}
1672
1673
	private function setLastNewsUpdate(int $time): void {
1674
		if ($this->lastNewsUpdate == $time) {
1675
			return;
1676
		}
1677
		$this->lastNewsUpdate = $time;
1678
		$this->hasChanged = true;
1679
	}
1680
1681
	public function updateLastNewsUpdate(): void {
1682
		$this->setLastNewsUpdate(Epoch::time());
1683
	}
1684
1685
	public function getLastPort(): int {
1686
		return $this->lastPort;
1687
	}
1688
1689
	public function setLastPort(int $lastPort): void {
1690
		if ($this->lastPort == $lastPort) {
1691
			return;
1692
		}
1693
		$this->lastPort = $lastPort;
1694
		$this->hasChanged = true;
1695
	}
1696
1697
	public function getPlottedCourse(): ?Path {
1698
		if (!isset($this->plottedCourse)) {
1699
			// check if we have a course plotted
1700
			$dbResult = $this->db->read('SELECT course FROM player_plotted_course WHERE ' . $this->SQL);
1701
1702
			if ($dbResult->hasRecord()) {
1703
				// get the course back
1704
				$this->plottedCourse = $dbResult->record()->getObject('course');
1705
			} else {
1706
				$this->plottedCourse = null;
1707
			}
1708
		}
1709
1710
		// Update the plotted course if we have moved since the last query
1711
		if ($this->plottedCourse !== null && $this->plottedCourse->getStartSectorID() != $this->getSectorID()) {
1712
			if ($this->plottedCourse->getEndSectorID() == $this->getSectorID()) {
1713
				// We have reached our destination
1714
				$this->deletePlottedCourse();
1715
			} elseif ($this->plottedCourse->getNextOnPath() == $this->getSectorID()) {
1716
				// We have walked into the next sector of the course
1717
				$this->plottedCourse->followPath();
1718
				$this->setPlottedCourse($this->plottedCourse);
1719
			} elseif ($this->plottedCourse->isInPath($this->getSectorID())) {
1720
				// We have skipped to some later sector in the course
1721
				$this->plottedCourse->skipToSector($this->getSectorID());
1722
				$this->setPlottedCourse($this->plottedCourse);
1723
			}
1724
		}
1725
		return $this->plottedCourse;
1726
	}
1727
1728
	public function setPlottedCourse(Path $plottedCourse): void {
1729
		$this->plottedCourse = $plottedCourse;
1730
		$this->db->replace('player_plotted_course', [
1731
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1732
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1733
			'course' => $this->db->escapeObject($this->plottedCourse),
1734
		]);
1735
	}
1736
1737
	public function hasPlottedCourse(): bool {
1738
		return $this->getPlottedCourse() !== null;
1739
	}
1740
1741
	public function isPartOfCourse(SmrSector $sector): bool {
1742
		return $this->getPlottedCourse()?->isInPath($sector->getSectorID()) === true;
1743
	}
1744
1745
	public function deletePlottedCourse(): void {
1746
		$this->plottedCourse = null;
1747
		$this->db->write('DELETE FROM player_plotted_course WHERE ' . $this->SQL);
1748
	}
1749
1750
	/**
1751
	 * Computes the turn cost and max misjump between current and target sector
1752
	 *
1753
	 * @return array<string, int>
1754
	 */
1755
	public function getJumpInfo(SmrSector $targetSector): array {
1756
		$path = Plotter::findDistanceToX($targetSector, $this->getSector(), true);
1757
		if ($path === false) {
0 ignored issues
show
introduced by
The condition $path === false is always true.
Loading history...
1758
			throw new UserError('Unable to plot from ' . $this->getSectorID() . ' to ' . $targetSector->getSectorID() . '.');
1759
		}
1760
		$distance = $path->getDistance();
1761
1762
		$turnCost = max(TURNS_JUMP_MINIMUM, IRound($distance * TURNS_PER_JUMP_DISTANCE));
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

1762
		$turnCost = max(TURNS_JUMP_MINIMUM, /** @scrutinizer ignore-call */ IRound($distance * TURNS_PER_JUMP_DISTANCE));
Loading history...
1763
		$maxMisjump = max(0, IRound(($distance - $turnCost) * MISJUMP_DISTANCE_DIFF_FACTOR / (1 + $this->getLevelID() * MISJUMP_LEVEL_FACTOR)));
1764
		return ['turn_cost' => $turnCost, 'max_misjump' => $maxMisjump];
1765
	}
1766
1767
	public function __sleep() {
1768
		return ['accountID', 'gameID', 'sectorID', 'alignment', 'playerID', 'playerName', 'npc'];
1769
	}
1770
1771
	/**
1772
	 * @return array<int, StoredDestination>
1773
	 */
1774
	public function getStoredDestinations(): array {
1775
		if (!isset($this->storedDestinations)) {
1776
			$this->storedDestinations = [];
1777
			$dbResult = $this->db->read('SELECT * FROM player_stored_sector WHERE ' . $this->SQL);
1778
			foreach ($dbResult->records() as $dbRecord) {
1779
				$sectorID = $dbRecord->getInt('sector_id');
1780
				$this->storedDestinations[$sectorID] = new StoredDestination(
1781
					sectorID: $sectorID,
1782
					label: $dbRecord->getString('label'),
1783
					offsetTop: $dbRecord->getInt('offset_top'),
1784
					offsetLeft: $dbRecord->getInt('offset_left'),
1785
				);
1786
			}
1787
		}
1788
		return $this->storedDestinations;
1789
	}
1790
1791
	public function moveDestinationButton(int $sectorID, int $offsetTop, int $offsetLeft): void {
1792
		$this->getStoredDestinations(); // make sure property is initialized
1793
1794
		if ($offsetLeft < 0 || $offsetLeft > 500 || $offsetTop < 0 || $offsetTop > 300) {
1795
			throw new UserError('The saved sector must be in the box!');
1796
		}
1797
1798
		if (!isset($this->storedDestinations[$sectorID])) {
1799
			throw new UserError('You do not have a saved sector for #' . $sectorID);
1800
		}
1801
1802
		// Replace destination with updated offsets
1803
		$this->storedDestinations[$sectorID] = new StoredDestination(
1804
			sectorID: $sectorID,
1805
			label: $this->storedDestinations[$sectorID]->label,
1806
			offsetTop: $offsetTop,
1807
			offsetLeft: $offsetLeft,
1808
		);
1809
		$this->db->write('
1810
			UPDATE player_stored_sector
1811
				SET offset_left = ' . $this->db->escapeNumber($offsetLeft) . ', offset_top=' . $this->db->escapeNumber($offsetTop) . '
1812
			WHERE ' . $this->SQL . ' AND sector_id = ' . $this->db->escapeNumber($sectorID));
1813
	}
1814
1815
	public function addDestinationButton(int $sectorID, string $label): void {
1816
		$this->getStoredDestinations(); // make sure property is initialized
1817
1818
		if (!SmrSector::sectorExists($this->getGameID(), $sectorID)) {
1819
			throw new UserError('You want to add a non-existent sector?');
1820
		}
1821
1822
		// sector already stored ?
1823
		if (isset($this->storedDestinations[$sectorID])) {
1824
			throw new UserError('Sector already stored!');
1825
		}
1826
1827
		$this->storedDestinations[$sectorID] = new StoredDestination(
1828
			label: $label,
1829
			sectorID: $sectorID,
1830
			offsetTop: 1,
1831
			offsetLeft: 1,
1832
		);
1833
1834
		$this->db->insert('player_stored_sector', [
1835
			'account_id' => $this->db->escapeNumber($this->getAccountID()),
1836
			'game_id' => $this->db->escapeNumber($this->getGameID()),
1837
			'sector_id' => $this->db->escapeNumber($sectorID),
1838
			'label' => $this->db->escapeString($label),
1839
			'offset_top' => 1,
1840
			'offset_left' => 1,
1841
		]);
1842
	}
1843
1844
	public function deleteDestinationButton(int $sectorID): void {
1845
		$this->getStoredDestinations(); // make sure property is initialized
1846
1847
		if (!isset($this->storedDestinations[$sectorID])) {
1848
			throw new Exception('Could not find stored destination');
1849
		}
1850
1851
		$this->db->write('
1852
			DELETE FROM player_stored_sector
1853
			WHERE ' . $this->SQL . '
1854
			AND sector_id = ' . $this->db->escapeNumber($sectorID));
1855
		unset($this->storedDestinations[$sectorID]);
1856
	}
1857
1858
	/**
1859
	 * @return array<string, array<string, mixed>>
1860
	 */
1861
	public function getTickers(): array {
1862
		if (!isset($this->tickers)) {
1863
			$this->tickers = [];
1864
			//get ticker info
1865
			$dbResult = $this->db->read('SELECT type,time,expires,recent FROM player_has_ticker WHERE ' . $this->SQL . ' AND expires > ' . $this->db->escapeNumber(Epoch::time()));
1866
			foreach ($dbResult->records() as $dbRecord) {
1867
				$this->tickers[$dbRecord->getString('type')] = [
1868
					'Type' => $dbRecord->getString('type'),
1869
					'Time' => $dbRecord->getInt('time'),
1870
					'Expires' => $dbRecord->getInt('expires'),
1871
					'Recent' => $dbRecord->getString('recent'),
1872
				];
1873
			}
1874
		}
1875
		return $this->tickers;
1876
	}
1877
1878
	public function hasTickers(): bool {
1879
		return count($this->getTickers()) > 0;
1880
	}
1881
1882
	/**
1883
	 * @return array<string, mixed>|false
1884
	 */
1885
	public function getTicker(string $tickerType): array|false {
1886
		$tickers = $this->getTickers();
1887
		if (isset($tickers[$tickerType])) {
1888
			return $tickers[$tickerType];
1889
		}
1890
		return false;
1891
	}
1892
1893
	public function hasTicker(string $tickerType): bool {
1894
		return $this->getTicker($tickerType) !== false;
1895
	}
1896
1897
	/**
1898
	 * @return array<string, mixed>
1899
	 */
1900
	public function shootForces(SmrForce $forces): array {
1901
		return $this->getShip()->shootForces($forces);
1902
	}
1903
1904
	/**
1905
	 * @return array<string, mixed>
1906
	 */
1907
	public function shootPort(SmrPort $port): array {
1908
		return $this->getShip()->shootPort($port);
1909
	}
1910
1911
	/**
1912
	 * @return array<string, mixed>
1913
	 */
1914
	public function shootPlanet(SmrPlanet $planet): array {
1915
		return $this->getShip()->shootPlanet($planet);
1916
	}
1917
1918
	/**
1919
	 * @param array<AbstractSmrPlayer> $targetPlayers
1920
	 * @return array<string, mixed>
1921
	 */
1922
	public function shootPlayers(array $targetPlayers): array {
1923
		return $this->getShip()->shootPlayers($targetPlayers);
1924
	}
1925
1926
	public function getMilitaryPayment(): int {
1927
		return $this->militaryPayment;
1928
	}
1929
1930
	public function hasMilitaryPayment(): bool {
1931
		return $this->getMilitaryPayment() > 0;
1932
	}
1933
1934
	public function setMilitaryPayment(int $amount): void {
1935
		if ($this->militaryPayment == $amount) {
1936
			return;
1937
		}
1938
		$this->militaryPayment = $amount;
1939
		$this->hasChanged = true;
1940
	}
1941
1942
	public function increaseMilitaryPayment(int $amount): void {
1943
		if ($amount < 0) {
1944
			throw new Exception('Trying to increase negative military payment.');
1945
		}
1946
		$this->setMilitaryPayment($this->getMilitaryPayment() + $amount);
1947
	}
1948
1949
	public function decreaseMilitaryPayment(int $amount): void {
1950
		if ($amount < 0) {
1951
			throw new Exception('Trying to decrease negative military payment.');
1952
		}
1953
		$this->setMilitaryPayment($this->getMilitaryPayment() - $amount);
1954
	}
1955
1956
	/**
1957
	 * Get bounties that can be claimed by this player.
1958
	 *
1959
	 * @return array<Bounty>
1960
	 */
1961
	public function getClaimableBounties(?BountyType $type = null): array {
1962
		return Bounty::getClaimableByPlayer($this, $type);
1963
	}
1964
1965
	/**
1966
	 * @return array<int, Bounty>
1967
	 */
1968
	public function getBounties(): array {
1969
		if (!isset($this->bounties)) {
1970
			$this->bounties = Bounty::getPlacedOnPlayer($this);
1971
		}
1972
		return $this->bounties;
1973
	}
1974
1975
	public function hasBounties(): bool {
1976
		return count($this->getBounties()) > 0;
1977
	}
1978
1979
	protected function createBounty(BountyType $type): Bounty {
1980
		$bounty = new Bounty(
1981
			targetID: $this->accountID,
1982
			bountyID: $this->getNextBountyID(),
1983
			gameID: $this->gameID,
1984
			type: $type,
1985
			time: Epoch::time(),
1986
		);
1987
		$this->bounties[$bounty->bountyID] = $bounty;
1988
		return $bounty;
1989
	}
1990
1991
	protected function getNextBountyID(): int {
1992
		if (!$this->hasBounties()) {
1993
			return 0;
1994
		}
1995
		return max(array_keys($this->getBounties())) + 1;
1996
	}
1997
1998
	public function getActiveBounty(BountyType $type): Bounty {
1999
		foreach ($this->getBounties() as $bounty) {
2000
			if ($bounty->isActive() && $bounty->type == $type) {
2001
				return $bounty;
2002
			}
2003
		}
2004
		return $this->createBounty($type);
2005
	}
2006
2007
	public function hasActiveBounty(BountyType $type): bool {
2008
		foreach ($this->getBounties() as $bounty) {
2009
			if ($bounty->isActive() && $bounty->type == $type) {
2010
				return true;
2011
			}
2012
		}
2013
		return false;
2014
	}
2015
2016
	/**
2017
	 * Mark all active bounties on this player as claimable by $claimer
2018
	 */
2019
	public function setBountiesClaimable(self $claimer): void {
2020
		foreach ($this->getBounties() as $bounty) {
2021
			if ($bounty->isActive()) {
2022
				$bounty->setClaimable($claimer->getAccountID());
2023
			}
2024
		}
2025
	}
2026
2027
	protected function getHOFData(): void {
2028
		if (!isset($this->HOF)) {
2029
			//Get Player HOF
2030
			$dbResult = $this->db->read('SELECT type,amount FROM player_hof WHERE ' . $this->SQL);
2031
			$this->HOF = [];
2032
			foreach ($dbResult->records() as $dbRecord) {
2033
				$this->HOF[$dbRecord->getString('type')] = $dbRecord->getFloat('amount');
2034
			}
2035
			self::getHOFVis();
2036
		}
2037
	}
2038
2039
	/**
2040
	 * @return array<string, string>
2041
	 */
2042
	public static function getHOFVis(): array {
2043
		if (!isset(self::$HOFVis)) {
2044
			//Get Player HOF Vis
2045
			$db = Database::getInstance();
2046
			$dbResult = $db->read('SELECT type,visibility FROM hof_visibility');
2047
			self::$HOFVis = [];
2048
			foreach ($dbResult->records() as $dbRecord) {
2049
				self::$HOFVis[$dbRecord->getString('type')] = $dbRecord->getString('visibility');
2050
			}
2051
			// Add non-database types
2052
			self::$HOFVis[HOF_TYPE_DONATION] = HOF_PUBLIC;
2053
			self::$HOFVis[HOF_TYPE_USER_SCORE] = HOF_PUBLIC;
2054
		}
2055
		return self::$HOFVis;
2056
	}
2057
2058
	/**
2059
	 * @param array<string> $typeList
2060
	 */
2061
	public function getHOF(array $typeList): float {
2062
		$this->getHOFData();
2063
		return $this->HOF[implode(':', $typeList)] ?? 0;
2064
	}
2065
2066
	/**
2067
	 * @param array<string> $typeList
2068
	 */
2069
	public function increaseHOF(float $amount, array $typeList, string $visibility): void {
2070
		if ($amount < 0) {
2071
			throw new Exception('Trying to increase negative HOF: ' . implode(':', $typeList));
2072
		}
2073
		if ($amount == 0) {
2074
			return;
2075
		}
2076
		$this->setHOF($this->getHOF($typeList) + $amount, $typeList, $visibility);
2077
	}
2078
2079
	/**
2080
	 * @param array<string> $typeList
2081
	 */
2082
	public function decreaseHOF(float $amount, array $typeList, string $visibility): void {
2083
		if ($amount < 0) {
2084
			throw new Exception('Trying to decrease negative HOF: ' . implode(':', $typeList));
2085
		}
2086
		if ($amount == 0) {
2087
			return;
2088
		}
2089
		$this->setHOF($this->getHOF($typeList) - $amount, $typeList, $visibility);
2090
	}
2091
2092
	/**
2093
	 * @param array<string> $typeList
2094
	 */
2095
	public function setHOF(float $amount, array $typeList, string $visibility): void {
2096
		if ($this->isNPC()) {
2097
			// Don't store HOF for NPCs.
2098
			return;
2099
		}
2100
		if ($amount < 0) {
2101
			$amount = 0;
2102
		}
2103
		if ($this->getHOF($typeList) == $amount) {
2104
			return;
2105
		}
2106
2107
		$hofType = implode(':', $typeList);
2108
		if (!isset(self::$HOFVis[$hofType])) {
2109
			self::$hasHOFVisChanged[$hofType] = self::HOF_NEW;
2110
		} elseif (self::$HOFVis[$hofType] != $visibility) {
2111
			self::$hasHOFVisChanged[$hofType] = self::HOF_CHANGED;
2112
		}
2113
		self::$HOFVis[$hofType] = $visibility;
2114
2115
		if (!isset($this->HOF[$hofType])) {
2116
			$this->hasHOFChanged[$hofType] = self::HOF_NEW;
2117
		} else {
2118
			$this->hasHOFChanged[$hofType] = self::HOF_CHANGED;
2119
		}
2120
		$this->HOF[$hofType] = $amount;
2121
	}
2122
2123
	public function isUnderAttack(): bool {
2124
		return $this->underAttack;
2125
	}
2126
2127
	public function setUnderAttack(bool $value): void {
2128
		if ($this->underAttack === $value) {
2129
			return;
2130
		}
2131
		$this->underAttack = $value;
2132
		$this->hasChanged = true;
2133
	}
2134
2135
	public function killPlayer(int $sectorID): void {
2136
		$sector = SmrSector::getSector($this->getGameID(), $sectorID);
2137
		//msg taken care of in trader_att_proc.php
2138
		// forget plotted course
2139
		$this->deletePlottedCourse();
2140
2141
		$sector->diedHere($this);
2142
2143
		// if we are in an alliance we increase their deaths
2144
		if ($this->hasAlliance()) {
2145
			$this->db->write('UPDATE alliance SET alliance_deaths = alliance_deaths + 1
2146
							WHERE game_id = ' . $this->db->escapeNumber($this->getGameID()) . ' AND alliance_id = ' . $this->db->escapeNumber($this->getAllianceID()));
2147
		}
2148
2149
		// record death stat
2150
		$this->increaseHOF(1, ['Dying', 'Deaths'], HOF_PUBLIC);
2151
		//record cost of ship lost
2152
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Money', 'Cost Of Ships Lost'], HOF_PUBLIC);
2153
		// reset turns since last death
2154
		$this->setHOF(0, ['Movement', 'Turns Used', 'Since Last Death'], HOF_ALLIANCE);
2155
2156
		// Reset credits to starting amount + ship insurance
2157
		$credits = $this->getGame()->getStartingCredits();
2158
		$credits += IRound($this->getShip()->getCost() * self::SHIP_INSURANCE_FRACTION);
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2158
		$credits += /** @scrutinizer ignore-call */ IRound($this->getShip()->getCost() * self::SHIP_INSURANCE_FRACTION);
Loading history...
2159
		$this->setCredits($credits);
2160
2161
		$this->setSectorID($this->getHome());
2162
		$this->increaseDeaths(1);
2163
		$this->setLandedOnPlanet(false);
2164
		$this->setDead(true);
2165
		$this->setNewbieWarning(true);
2166
		$this->getShip()->getPod($this->hasNewbieStatus());
2167
		$this->setNewbieTurns(NEWBIE_TURNS_ON_DEATH);
2168
		$this->setUnderAttack(false);
2169
	}
2170
2171
	/**
2172
	 * @return array<string, mixed>
2173
	 */
2174
	public function killPlayerByPlayer(self $killer): array {
2175
		$return = [];
2176
		$msg = $this->getBBLink();
2177
2178
		if ($this->hasCustomShipName()) {
2179
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2180
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2181
		}
2182
		$msg .= ' was destroyed by ' . $killer->getBBLink();
2183
		if ($killer->hasCustomShipName()) {
2184
			$named_ship = strip_tags($killer->getCustomShipName(), '<font><span><img>');
2185
			$msg .= ' flying <span class="yellow">' . $named_ship . '</span>';
2186
		}
2187
		$msg .= ' in Sector&nbsp;' . Globals::getSectorBBLink($this->getSectorID());
2188
		$this->getSector()->increaseBattles(1);
2189
		$this->db->insert('news', [
2190
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2191
			'time' => $this->db->escapeNumber(Epoch::time()),
2192
			'news_message' => $this->db->escapeString($msg),
2193
			'killer_id' => $this->db->escapeNumber($killer->getAccountID()),
2194
			'killer_alliance' => $this->db->escapeNumber($killer->getAllianceID()),
2195
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2196
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2197
		]);
2198
2199
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $killer->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2200
		self::sendMessageFromFedClerk($this->getGameID(), $killer->getAccountID(), 'You <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2201
2202
		// Dead player loses between 5% and 25% experience
2203
		$expLossPercentage = 0.15 + 0.10 * ($this->getLevelID() - $killer->getLevelID()) / $this->getMaxLevel();
2204
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2204
		$return['DeadExp'] = max(0, /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage));
Loading history...
2205
		$expBeforeDeath = $this->getExperience();
2206
		$this->decreaseExperience($return['DeadExp']);
2207
2208
		// Killer gains 50% of the lost exp
2209
		$return['KillerExp'] = max(0, ICeil(0.5 * $return['DeadExp']));
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2209
		$return['KillerExp'] = max(0, /** @scrutinizer ignore-call */ ICeil(0.5 * $return['DeadExp']));
Loading history...
2210
		$killer->increaseExperience($return['KillerExp']);
2211
2212
		$return['KillerCredits'] = $this->getCredits();
2213
		$killer->increaseCredits($return['KillerCredits']);
2214
2215
		// The killer may change alignment
2216
		$relations = Globals::getRaceRelations($this->getGameID(), $this->getRaceID());
2217
		$relation = $relations[$killer->getRaceID()];
2218
2219
		$alignChangePerRelation = 0.1;
2220
		if ($relation >= RELATIONS_PEACE || $relation <= RELATIONS_WAR) {
2221
			$alignChangePerRelation = 0.04;
2222
		}
2223
2224
		$killerAlignChange = IRound(-$relation * $alignChangePerRelation); //Lose relations when killing a peaceful race
0 ignored issues
show
Bug introduced by
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2224
		$killerAlignChange = /** @scrutinizer ignore-call */ IRound(-$relation * $alignChangePerRelation); //Lose relations when killing a peaceful race
Loading history...
2225
		if ($killerAlignChange > 0) {
2226
			$killer->increaseAlignment($killerAlignChange);
2227
		} else {
2228
			$killer->decreaseAlignment(-$killerAlignChange);
2229
		}
2230
		// War setting gives them military pay
2231
		if ($relation <= RELATIONS_WAR) {
2232
			$killer->increaseMilitaryPayment(-IFloor($relation * 100 * pow($return['KillerExp'] / 2, 0.25)));
2233
		}
2234
2235
		//check for federal bounty being offered for current port raiders;
2236
		$this->db->write('DELETE FROM player_attacks_port WHERE time < ' . $this->db->escapeNumber(Epoch::time() - self::TIME_FOR_FEDERAL_BOUNTY_ON_PR));
2237
		$query = 'SELECT 1
2238
					FROM player_attacks_port
2239
					JOIN port USING(game_id, sector_id)
2240
					JOIN player USING(game_id, account_id)
2241
					WHERE armour > 0 AND ' . $this->SQL . ' LIMIT 1';
2242
		$dbResult = $this->db->read($query);
2243
		if ($dbResult->hasRecord()) {
2244
			$bounty = IFloor(DEFEND_PORT_BOUNTY_PER_LEVEL * $this->getLevelID());
2245
			$this->getActiveBounty(BountyType::HQ)->increaseCredits($bounty);
2246
		}
2247
2248
		// Killer get marked as claimer of podded player's bounties even if they don't exist
2249
		$this->setBountiesClaimable($killer);
2250
2251
		// If the alignment difference is greater than 200 then a bounty may be set
2252
		$alignmentDiff = abs($this->getAlignment() - $killer->getAlignment());
2253
		$bountyGainedByKiller = 0;
2254
		if ($alignmentDiff >= 200) {
2255
			// If the podded players alignment makes them deputy or member then set bounty
2256
			$bountyType = match (true) {
2257
				$this->hasGoodAlignment() => BountyType::HQ,
2258
				$this->hasEvilAlignment() => BountyType::UG,
2259
				default => null,
2260
			};
2261
			if ($bountyType !== null) {
2262
				$bountyGainedByKiller = IFloor(pow($alignmentDiff, 2.56));
2263
				$killer->getActiveBounty($bountyType)->increaseCredits($bountyGainedByKiller);
2264
			}
2265
		}
2266
2267
		$killingHof = ['Killing'];
2268
		if ($this->isNPC()) {
2269
			$killingHof[] = 'NPC';
2270
		}
2271
		$killer->increaseHOF($return['KillerExp'], [...$killingHof, 'Experience', 'Gained'], HOF_PUBLIC);
2272
		$killer->increaseHOF($expBeforeDeath, [...$killingHof, 'Experience', 'Of Traders Killed'], HOF_PUBLIC);
2273
		$killer->increaseHOF($return['DeadExp'], [...$killingHof, 'Experience', 'Lost By Traders Killed'], HOF_PUBLIC);
2274
2275
		$killer->increaseHOF($return['KillerCredits'], [...$killingHof, 'Money', 'Lost By Traders Killed'], HOF_PUBLIC);
2276
		$killer->increaseHOF($return['KillerCredits'], [...$killingHof, 'Money', 'Gain'], HOF_PUBLIC);
2277
		$killer->increaseHOF($this->getShip()->getCost(), [...$killingHof, 'Money', 'Cost Of Ships Killed'], HOF_PUBLIC);
2278
		$killer->increaseHOF($bountyGainedByKiller, [...$killingHof, 'Money', 'Bounty Gained'], HOF_PUBLIC);
2279
2280
		if ($killerAlignChange > 0) {
2281
			$killer->increaseHOF($killerAlignChange, [...$killingHof, 'Alignment', 'Gain'], HOF_PUBLIC);
2282
		} else {
2283
			$killer->increaseHOF(-$killerAlignChange, [...$killingHof, 'Alignment', 'Loss'], HOF_PUBLIC);
2284
		}
2285
2286
		if ($this->isNPC()) {
2287
			$killer->increaseHOF(1, ['Killing', 'NPC Kills'], HOF_PUBLIC);
2288
		} elseif ($this->getShip()->getAttackRatingWithMaxCDs() <= MAX_ATTACK_RATING_NEWBIE && $this->hasNewbieStatus() && !$killer->hasNewbieStatus()) { //Newbie kill
2289
			$killer->increaseHOF(1, ['Killing', 'Newbie Kills'], HOF_PUBLIC);
2290
		} else {
2291
			$killer->increaseKills(1);
2292
			$killer->increaseHOF(1, ['Killing', 'Kills'], HOF_PUBLIC);
2293
2294
			if ($killer->hasAlliance()) {
2295
				$this->db->write('UPDATE alliance SET alliance_kills=alliance_kills+1 WHERE alliance_id=' . $this->db->escapeNumber($killer->getAllianceID()) . ' AND game_id=' . $this->db->escapeNumber($killer->getGameID()));
2296
			}
2297
2298
			// alliance vs. alliance stats
2299
			$this->incrementAllianceVsDeaths($killer->getAllianceID());
2300
		}
2301
2302
		$dyingHof = ['Dying', 'Players'];
2303
		if ($killer->isNPC()) {
2304
			$dyingHof[] = 'NPC';
2305
		}
2306
		$this->increaseHOF($bountyGainedByKiller, [...$dyingHof, 'Money', 'Bounty Gained By Killer'], HOF_PUBLIC);
2307
		$this->increaseHOF($return['KillerExp'], [...$dyingHof, 'Experience', 'Gained By Killer'], HOF_PUBLIC);
2308
		$this->increaseHOF($return['DeadExp'], [...$dyingHof, 'Experience', 'Lost'], HOF_PUBLIC);
2309
		$this->increaseHOF($return['KillerCredits'], [...$dyingHof, 'Money Lost'], HOF_PUBLIC);
2310
		$this->increaseHOF($this->getShip()->getCost(), [...$dyingHof, 'Money', 'Cost Of Ships Lost'], HOF_PUBLIC);
2311
		$this->increaseHOF(1, [...$dyingHof, 'Deaths'], HOF_PUBLIC);
2312
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2313
2314
		$this->killPlayer($this->getSectorID());
2315
		return $return;
2316
	}
2317
2318
	/**
2319
	 * @return array<string, mixed>
2320
	 */
2321
	public function killPlayerByForces(SmrForce $forces): array {
2322
		$return = [];
2323
		$owner = $forces->getOwner();
2324
		// send a message to the person who died
2325
		self::sendMessageFromFedClerk($this->getGameID(), $owner->getAccountID(), 'Your forces <span class="red">DESTROYED </span>' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($forces->getSectorID()));
2326
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($this->getSectorID()));
2327
2328
		$news_message = $this->getBBLink();
2329
		if ($this->hasCustomShipName()) {
2330
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2331
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2332
		}
2333
		$news_message .= ' was destroyed by ' . $owner->getBBLink() . '\'s forces in sector ' . Globals::getSectorBBLink($forces->getSectorID());
2334
		// insert the news entry
2335
		$this->db->insert('news', [
2336
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2337
			'time' => $this->db->escapeNumber(Epoch::time()),
2338
			'news_message' => $this->db->escapeString($news_message),
2339
			'killer_id' => $this->db->escapeNumber($owner->getAccountID()),
2340
			'killer_alliance' => $this->db->escapeNumber($owner->getAllianceID()),
2341
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2342
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2343
		]);
2344
2345
		// Player loses 15% experience
2346
		$expLossPercentage = .15;
2347
		$return['DeadExp'] = IFloor($this->getExperience() * $expLossPercentage);
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2347
		$return['DeadExp'] = /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage);
Loading history...
2348
		$this->decreaseExperience($return['DeadExp']);
2349
2350
		$return['LostCredits'] = $this->getCredits();
2351
2352
		// alliance vs. alliance stats
2353
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_FORCES);
2354
		$owner->incrementAllianceVsKills(ALLIANCE_VS_FORCES);
2355
2356
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2357
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Forces', 'Experience Lost'], HOF_PUBLIC);
2358
		$this->increaseHOF($return['LostCredits'], ['Dying', 'Forces', 'Money Lost'], HOF_PUBLIC);
2359
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Forces', 'Cost Of Ships Lost'], HOF_PUBLIC);
2360
		$this->increaseHOF(1, ['Dying', 'Forces', 'Deaths'], HOF_PUBLIC);
2361
2362
		$this->killPlayer($forces->getSectorID());
2363
		return $return;
2364
	}
2365
2366
	/**
2367
	 * @return array<string, mixed>
2368
	 */
2369
	public function killPlayerByPort(AbstractSmrPort $port): array {
2370
		$return = [];
2371
		// send a message to the person who died
2372
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the defenses of ' . $port->getDisplayName());
2373
2374
		$news_message = $this->getBBLink();
2375
		if ($this->hasCustomShipName()) {
2376
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2377
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2378
		}
2379
		$news_message .= ' was destroyed while invading ' . $port->getDisplayName() . '.';
2380
		// insert the news entry
2381
		$this->db->insert('news', [
2382
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2383
			'time' => $this->db->escapeNumber(Epoch::time()),
2384
			'news_message' => $this->db->escapeString($news_message),
2385
			'killer_id' => $this->db->escapeNumber(ACCOUNT_ID_PORT),
2386
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2387
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2388
		]);
2389
2390
		// Player loses between 15% and 20% experience
2391
		$expLossPercentage = .20 - .05 * ($port->getLevel() - 1) / ($port->getMaxLevel() - 1);
2392
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2392
		$return['DeadExp'] = max(0, /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage));
Loading history...
2393
		$this->decreaseExperience($return['DeadExp']);
2394
2395
		$return['LostCredits'] = $this->getCredits();
2396
2397
		// alliance vs. alliance stats
2398
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PORTS);
2399
2400
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2401
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Ports', 'Experience Lost'], HOF_PUBLIC);
2402
		$this->increaseHOF($return['LostCredits'], ['Dying', 'Ports', 'Money Lost'], HOF_PUBLIC);
2403
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Ports', 'Cost Of Ships Lost'], HOF_PUBLIC);
2404
		$this->increaseHOF(1, ['Dying', 'Ports', 'Deaths'], HOF_PUBLIC);
2405
2406
		$this->killPlayer($port->getSectorID());
2407
		return $return;
2408
	}
2409
2410
	/**
2411
	 * @return array<string, mixed>
2412
	 */
2413
	public function killPlayerByPlanet(SmrPlanet $planet): array {
2414
		$return = [];
2415
		// send a message to the person who died
2416
		$planetOwner = $planet->getOwner();
2417
		self::sendMessageFromFedClerk($this->getGameID(), $planetOwner->getAccountID(), 'Your planet <span class="red">DESTROYED</span>&nbsp;' . $this->getBBLink() . ' in sector ' . Globals::getSectorBBLink($planet->getSectorID()));
2418
		self::sendMessageFromFedClerk($this->getGameID(), $this->getAccountID(), 'You were <span class="red">DESTROYED</span> by the planetary defenses of ' . $planet->getCombatName());
2419
2420
		$news_message = $this->getBBLink();
2421
		if ($this->hasCustomShipName()) {
2422
			$named_ship = strip_tags($this->getCustomShipName(), '<font><span><img>');
2423
			$news_message .= ' flying <span class="yellow">' . $named_ship . '</span>';
2424
		}
2425
		$news_message .= ' was destroyed by ' . $planet->getCombatName() . '\'s planetary defenses in sector ' . Globals::getSectorBBLink($planet->getSectorID()) . '.';
2426
		// insert the news entry
2427
		$this->db->insert('news', [
2428
			'game_id' => $this->db->escapeNumber($this->getGameID()),
2429
			'time' => $this->db->escapeNumber(Epoch::time()),
2430
			'news_message' => $this->db->escapeString($news_message),
2431
			'killer_id' => $this->db->escapeNumber($planetOwner->getAccountID()),
2432
			'killer_alliance' => $this->db->escapeNumber($planetOwner->getAllianceID()),
2433
			'dead_id' => $this->db->escapeNumber($this->getAccountID()),
2434
			'dead_alliance' => $this->db->escapeNumber($this->getAllianceID()),
2435
		]);
2436
2437
		// Player loses between 15% and 20% experience
2438
		$expLossPercentage = .20 - .05 * $planet->getLevel() / $planet->getMaxLevel();
2439
		$return['DeadExp'] = max(0, IFloor($this->getExperience() * $expLossPercentage));
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2439
		$return['DeadExp'] = max(0, /** @scrutinizer ignore-call */ IFloor($this->getExperience() * $expLossPercentage));
Loading history...
2440
		$this->decreaseExperience($return['DeadExp']);
2441
2442
		$return['LostCredits'] = $this->getCredits();
2443
2444
		// alliance vs. alliance stats
2445
		$this->incrementAllianceVsDeaths(ALLIANCE_VS_PLANETS);
2446
		$planetOwner->incrementAllianceVsKills(ALLIANCE_VS_PLANETS);
2447
2448
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Experience', 'Lost'], HOF_PUBLIC);
2449
		$this->increaseHOF($return['DeadExp'], ['Dying', 'Planets', 'Experience Lost'], HOF_PUBLIC);
2450
		$this->increaseHOF($return['LostCredits'], ['Dying', 'Planets', 'Money Lost'], HOF_PUBLIC);
2451
		$this->increaseHOF($this->getShip()->getCost(), ['Dying', 'Planets', 'Cost Of Ships Lost'], HOF_PUBLIC);
2452
		$this->increaseHOF(1, ['Dying', 'Planets', 'Deaths'], HOF_PUBLIC);
2453
2454
		$this->killPlayer($planet->getSectorID());
2455
		return $return;
2456
	}
2457
2458
	public function incrementAllianceVsKills(int $otherID): void {
2459
		$values = [$this->getGameID(), $this->getAllianceID(), $otherID, 1];
2460
		$this->db->write('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');
2461
	}
2462
2463
	public function incrementAllianceVsDeaths(int $otherID): void {
2464
		$values = [$this->getGameID(), $otherID, $this->getAllianceID(), 1];
2465
		$this->db->write('INSERT INTO alliance_vs_alliance (game_id, alliance_id_1, alliance_id_2, kills) VALUES (' . $this->db->escapeArray($values) . ') ON DUPLICATE KEY UPDATE kills = kills + 1');
2466
	}
2467
2468
	public function getTurnsLevel(): TurnsLevel {
2469
		return match (true) {
2470
			$this->getTurns() === 0 => TurnsLevel::None,
2471
			$this->getTurns() <= 25 => TurnsLevel::Low,
2472
			$this->getTurns() <= 75 => TurnsLevel::Medium,
2473
			default => TurnsLevel::High,
2474
		};
2475
	}
2476
2477
	public function getTurns(): int {
2478
		return $this->turns;
2479
	}
2480
2481
	public function hasTurns(): bool {
2482
		return $this->turns > 0;
2483
	}
2484
2485
	public function getMaxTurns(): int {
2486
		return $this->getGame()->getMaxTurns();
2487
	}
2488
2489
	public function setTurns(int $turns): void {
2490
		if ($this->turns == $turns) {
2491
			return;
2492
		}
2493
		// Make sure turns are in range [0, MaxTurns]
2494
		$this->turns = max(0, min($turns, $this->getMaxTurns()));
2495
		$this->hasChanged = true;
2496
	}
2497
2498
	public function takeTurns(int $take, int $takeNewbie = 0): void {
2499
		if ($take < 0 || $takeNewbie < 0) {
2500
			throw new Exception('Trying to take negative turns.');
2501
		}
2502
		$take = ICeil($take);
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2502
		$take = /** @scrutinizer ignore-call */ ICeil($take);
Loading history...
2503
		// Only take up to as many newbie turns as we have remaining
2504
		$takeNewbie = min($this->getNewbieTurns(), $takeNewbie);
2505
2506
		$this->setTurns($this->getTurns() - $take);
2507
		$this->setNewbieTurns($this->getNewbieTurns() - $takeNewbie);
2508
		$this->increaseHOF($take, ['Movement', 'Turns Used', 'Since Last Death'], HOF_ALLIANCE);
2509
		$this->increaseHOF($take, ['Movement', 'Turns Used', 'Total'], HOF_ALLIANCE);
2510
		$this->increaseHOF($takeNewbie, ['Movement', 'Turns Used', 'Newbie'], HOF_ALLIANCE);
2511
2512
		// Player has taken an action
2513
		$this->setLastActive(Epoch::time());
2514
		$this->updateLastCPLAction();
2515
	}
2516
2517
	public function giveTurns(int $give, int $giveNewbie = 0): void {
2518
		if ($give < 0 || $giveNewbie < 0) {
2519
			throw new Exception('Trying to give negative turns.');
2520
		}
2521
		$this->setTurns($this->getTurns() + $give);
2522
		$this->setNewbieTurns($this->getNewbieTurns() + $giveNewbie);
2523
	}
2524
2525
	/**
2526
	 * Calculate the time in seconds between the given time and when the
2527
	 * player will be at max turns.
2528
	 */
2529
	public function getTimeUntilMaxTurns(int $time, bool $forceUpdate = false): int {
2530
		$timeDiff = $time - $this->getLastTurnUpdate();
2531
		$turnsDiff = $this->getMaxTurns() - $this->getTurns();
2532
		$ship = $this->getShip($forceUpdate);
2533
		$maxTurnsTime = ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2533
		$maxTurnsTime = /** @scrutinizer ignore-call */ ICeil(($turnsDiff * 3600 / $ship->getRealSpeed())) - $timeDiff;
Loading history...
2534
		// If already at max turns, return 0
2535
		return max(0, $maxTurnsTime);
2536
	}
2537
2538
	/**
2539
	 * Calculate the time in seconds until the next turn is awarded.
2540
	 */
2541
	public function getTimeUntilNextTurn(): int {
2542
		$secondsSinceUpdate = Epoch::time() - $this->getLastTurnUpdate();
2543
		$secondsPerTurn = 3600 / $this->getShip()->getRealSpeed();
2544
		return ICeil(fmod(abs($secondsSinceUpdate - $secondsPerTurn), $secondsPerTurn));
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2544
		return /** @scrutinizer ignore-call */ ICeil(fmod(abs($secondsSinceUpdate - $secondsPerTurn), $secondsPerTurn));
Loading history...
2545
	}
2546
2547
	/**
2548
	 * Grant the player their starting turns.
2549
	 */
2550
	public function giveStartingTurns(): void {
2551
		$startTurns = IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2551
		$startTurns = /** @scrutinizer ignore-call */ IFloor($this->getShip()->getRealSpeed() * $this->getGame()->getStartTurnHours());
Loading history...
2552
		$this->giveTurns($startTurns);
2553
		$this->setLastTurnUpdate($this->getGame()->getStartTime());
2554
	}
2555
2556
	// Turns only update when player is active.
2557
	// Calculate turns gained between given time and the last turn update
2558
	public function getTurnsGained(int $time, bool $forceUpdate = false): int {
2559
		$timeDiff = $time - $this->getLastTurnUpdate();
2560
		$ship = $this->getShip($forceUpdate);
2561
		$extraTurns = IFloor($timeDiff * $ship->getRealSpeed() / 3600);
0 ignored issues
show
Bug introduced by
The function IFloor was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2561
		$extraTurns = /** @scrutinizer ignore-call */ IFloor($timeDiff * $ship->getRealSpeed() / 3600);
Loading history...
2562
		return $extraTurns;
2563
	}
2564
2565
	public function updateTurns(): void {
2566
		// is account validated?
2567
		if (!$this->getAccount()->isValidated()) {
2568
			return;
2569
		}
2570
2571
		// how many turns would he get right now?
2572
		$extraTurns = $this->getTurnsGained(Epoch::time());
2573
2574
		// do we have at least one turn to give?
2575
		if ($extraTurns > 0) {
2576
			// recalc the time to avoid rounding errors
2577
			$newLastTurnUpdate = $this->getLastTurnUpdate() + ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
0 ignored issues
show
Bug introduced by
The function ICeil was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

2577
			$newLastTurnUpdate = $this->getLastTurnUpdate() + /** @scrutinizer ignore-call */ ICeil($extraTurns * 3600 / $this->getShip()->getRealSpeed());
Loading history...
2578
			$this->setLastTurnUpdate($newLastTurnUpdate);
2579
			$this->giveTurns($extraTurns);
2580
		}
2581
	}
2582
2583
	public function getLastTurnUpdate(): int {
2584
		return $this->lastTurnUpdate;
2585
	}
2586
2587
	public function setLastTurnUpdate(int $time): void {
2588
		if ($this->lastTurnUpdate == $time) {
2589
			return;
2590
		}
2591
		$this->lastTurnUpdate = $time;
2592
		$this->hasChanged = true;
2593
	}
2594
2595
	public function getLastActive(): int {
2596
		return $this->lastActive;
2597
	}
2598
2599
	public function setLastActive(int $lastActive): void {
2600
		if ($this->lastActive == $lastActive) {
2601
			return;
2602
		}
2603
		$this->lastActive = $lastActive;
2604
		$this->hasChanged = true;
2605
	}
2606
2607
	public function getLastCPLAction(): int {
2608
		return $this->lastCPLAction;
2609
	}
2610
2611
	public function setLastCPLAction(int $time): void {
2612
		if ($this->lastCPLAction == $time) {
2613
			return;
2614
		}
2615
		$this->lastCPLAction = $time;
2616
		$this->hasChanged = true;
2617
	}
2618
2619
	public function updateLastCPLAction(): void {
2620
		$this->setLastCPLAction(Epoch::time());
2621
	}
2622
2623
	public function setNewbieWarning(bool $bool): void {
2624
		if ($this->newbieWarning == $bool) {
2625
			return;
2626
		}
2627
		$this->newbieWarning = $bool;
2628
		$this->hasChanged = true;
2629
	}
2630
2631
	public function getNewbieWarning(): bool {
2632
		return $this->newbieWarning;
2633
	}
2634
2635
	public function isDisplayMissions(): bool {
2636
		return $this->displayMissions;
2637
	}
2638
2639
	public function setDisplayMissions(bool $bool): void {
2640
		if ($this->displayMissions == $bool) {
2641
			return;
2642
		}
2643
		$this->displayMissions = $bool;
2644
		$this->hasChanged = true;
2645
	}
2646
2647
	/**
2648
	 * @return array<int, array<string, mixed>>
2649
	 */
2650
	public function getMissions(): array {
2651
		if (!isset($this->missions)) {
2652
			$dbResult = $this->db->read('SELECT * FROM player_has_mission WHERE ' . $this->SQL);
2653
			$this->missions = [];
2654
			foreach ($dbResult->records() as $dbRecord) {
2655
				$missionID = $dbRecord->getInt('mission_id');
2656
				$this->missions[$missionID] = [
2657
					'On Step' => $dbRecord->getInt('on_step'),
2658
					'Progress' => $dbRecord->getInt('progress'),
2659
					'Unread' => $dbRecord->getBoolean('unread'),
2660
					'Expires' => $dbRecord->getInt('step_fails'),
2661
					'Sector' => $dbRecord->getInt('mission_sector'),
2662
					'Starting Sector' => $dbRecord->getInt('starting_sector'),
2663
				];
2664
				$this->rebuildMission($missionID);
2665
			}
2666
		}
2667
		return $this->missions;
2668
	}
2669
2670
	/**
2671
	 * @return array<int, array<string, mixed>>
2672
	 */
2673
	public function getActiveMissions(): array {
2674
		$missions = $this->getMissions();
2675
		foreach ($missions as $missionID => $mission) {
2676
			if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2677
				unset($missions[$missionID]);
2678
			}
2679
		}
2680
		return $missions;
2681
	}
2682
2683
	/**
2684
	 * @return array<string, mixed>|false
2685
	 */
2686
	protected function getMission(int $missionID): array|false {
2687
		$missions = $this->getMissions();
2688
		if (isset($missions[$missionID])) {
2689
			return $missions[$missionID];
2690
		}
2691
		return false;
2692
	}
2693
2694
	protected function hasMission(int $missionID): bool {
2695
		return $this->getMission($missionID) !== false;
2696
	}
2697
2698
	protected function updateMission(int $missionID): bool {
2699
		$this->getMissions();
2700
		if (isset($this->missions[$missionID])) {
2701
			$mission = $this->missions[$missionID];
2702
			$this->db->write('
2703
				UPDATE player_has_mission
2704
				SET on_step = ' . $this->db->escapeNumber($mission['On Step']) . ',
2705
					progress = ' . $this->db->escapeNumber($mission['Progress']) . ',
2706
					unread = ' . $this->db->escapeBoolean($mission['Unread']) . ',
2707
					starting_sector = ' . $this->db->escapeNumber($mission['Starting Sector']) . ',
2708
					mission_sector = ' . $this->db->escapeNumber($mission['Sector']) . ',
2709
					step_fails = ' . $this->db->escapeNumber($mission['Expires']) . '
2710
				WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID));
2711
			return true;
2712
		}
2713
		return false;
2714
	}
2715
2716
	private function setupMissionStep(int $missionID): void {
2717
		$stepID = $this->missions[$missionID]['On Step'];
2718
		if ($stepID >= count(MISSIONS[$missionID]['Steps'])) {
2719
			// Nothing to do if this mission is already completed
2720
			return;
2721
		}
2722
		$step = MISSIONS[$missionID]['Steps'][$stepID];
2723
		if (isset($step['PickSector'])) {
2724
			$realX = Plotter::getX($step['PickSector']['Type'], $step['PickSector']['X'], $this->getGameID());
2725
			$path = Plotter::findDistanceToX($realX, $this->getSector(), true, null, $this);
2726
			if ($path === false) {
0 ignored issues
show
introduced by
The condition $path === false is always true.
Loading history...
2727
				// Abandon the mission if it cannot be completed due to a
2728
				// sector that does not exist or cannot be reached.
2729
				// (Probably shouldn't bestow this mission in the first place)
2730
				$this->deleteMission($missionID);
2731
				throw new UserError('Cannot find a path to the destination!');
2732
			}
2733
			$this->missions[$missionID]['Sector'] = $path->getEndSectorID();
2734
		}
2735
	}
2736
2737
	/**
2738
	 * Declining a mission will permanently hide it from the player
2739
	 * by adding it in its completed state.
2740
	 */
2741
	public function declineMission(int $missionID): void {
2742
		$finishedStep = count(MISSIONS[$missionID]['Steps']);
2743
		$this->addMission($missionID, $finishedStep);
2744
	}
2745
2746
	public function addMission(int $missionID, int $step = 0): void {
2747
		if ($this->hasMission($missionID)) {
2748
			throw new Exception('Mission ID already added: ' . $missionID);
2749
		}
2750
2751
		$mission = [
2752
			'On Step' => $step,
2753
			'Progress' => 0,
2754
			'Unread' => true,
2755
			'Expires' => (Epoch::time() + 86400),
2756
			'Sector' => 0,
2757
			'Starting Sector' => $this->getSectorID(),
2758
		];
2759
2760
		$this->missions[$missionID] =& $mission;
2761
		$this->setupMissionStep($missionID);
2762
		$this->rebuildMission($missionID);
2763
2764
		$this->db->replace('player_has_mission', [
2765
			'game_id' => $this->db->escapeNumber($this->gameID),
2766
			'account_id' => $this->db->escapeNumber($this->accountID),
2767
			'mission_id' => $this->db->escapeNumber($missionID),
2768
			'on_step' => $this->db->escapeNumber($mission['On Step']),
2769
			'progress' => $this->db->escapeNumber($mission['Progress']),
2770
			'unread' => $this->db->escapeBoolean($mission['Unread']),
2771
			'starting_sector' => $this->db->escapeNumber($mission['Starting Sector']),
2772
			'mission_sector' => $this->db->escapeNumber($mission['Sector']),
2773
			'step_fails' => $this->db->escapeNumber($mission['Expires']),
2774
		]);
2775
	}
2776
2777
	private function rebuildMission(int $missionID): void {
2778
		$mission = $this->missions[$missionID];
2779
		$this->missions[$missionID]['Name'] = MISSIONS[$missionID]['Name'];
2780
2781
		if ($mission['On Step'] >= count(MISSIONS[$missionID]['Steps'])) {
2782
			// If we have completed this mission just use false to indicate no current task.
2783
			$currentStep = false;
2784
		} else {
2785
			$data = ['player' => $this, 'mission' => $mission];
2786
			$currentStep = MISSIONS[$missionID]['Steps'][$mission['On Step']];
2787
			array_walk_recursive($currentStep, 'replaceMissionTemplate', $data);
2788
		}
2789
		$this->missions[$missionID]['Task'] = $currentStep;
2790
	}
2791
2792
	public function deleteMission(int $missionID): void {
2793
		$this->getMissions();
2794
		if (isset($this->missions[$missionID])) {
2795
			unset($this->missions[$missionID]);
2796
			$this->db->write('DELETE FROM player_has_mission WHERE ' . $this->SQL . ' AND mission_id = ' . $this->db->escapeNumber($missionID));
2797
			return;
2798
		}
2799
		throw new Exception('Mission with ID not found: ' . $missionID);
2800
	}
2801
2802
	/**
2803
	 * @return array<int>
2804
	 */
2805
	public function markMissionsRead(): array {
2806
		$this->getMissions();
2807
		$unreadMissions = [];
2808
		foreach ($this->missions as $missionID => &$mission) {
2809
			if ($mission['Unread']) {
2810
				$unreadMissions[] = $missionID;
2811
				$mission['Unread'] = false;
2812
				$this->updateMission($missionID);
2813
			}
2814
		}
2815
		return $unreadMissions;
2816
	}
2817
2818
	public function claimMissionReward(int $missionID): string {
2819
		if (!$this->hasMission($missionID)) {
2820
			throw new Exception('Unknown mission: ' . $missionID);
2821
		}
2822
		$mission =& $this->missions[$missionID];
2823
		if ($mission['Task'] === false || $mission['Task']['Step'] != 'Claim') {
2824
			throw new Exception('Cannot claim mission: ' . $missionID . ', for step: ' . $mission['On Step']);
2825
		}
2826
		$mission['On Step']++;
2827
		$mission['Unread'] = true;
2828
		foreach ($mission['Task']['Rewards'] as $rewardItem => $amount) {
2829
			switch ($rewardItem) {
2830
				case 'Credits':
2831
					$this->increaseCredits($amount);
2832
					break;
2833
				case 'Experience':
2834
					$this->increaseExperience($amount);
2835
					break;
2836
			}
2837
		}
2838
		$rewardText = $mission['Task']['Rewards']['Text'];
2839
		if ($mission['On Step'] < count(MISSIONS[$missionID]['Steps'])) {
2840
			// If we haven't finished the mission yet then
2841
			$this->setupMissionStep($missionID);
2842
		}
2843
		$this->rebuildMission($missionID);
2844
		$this->updateMission($missionID);
2845
		return $rewardText;
2846
	}
2847
2848
	/**
2849
	 * @return array<int, array<string, mixed>>
2850
	 */
2851
	public function getAvailableMissions(): array {
2852
		$availableMissions = [];
2853
		foreach (MISSIONS as $missionID => $mission) {
2854
			if ($this->hasMission($missionID)) {
2855
				continue;
2856
			}
2857
			$realX = Plotter::getX($mission['HasX']['Type'], $mission['HasX']['X'], $this->getGameID());
2858
			if ($this->getSector()->hasX($realX)) {
2859
				$availableMissions[$missionID] = $mission;
2860
			}
2861
		}
2862
		return $availableMissions;
2863
	}
2864
2865
	/**
2866
	 * Log a player action in the current sector to the admin log console.
2867
	 */
2868
	public function log(int $log_type_id, string $msg): void {
2869
		$this->getAccount()->log($log_type_id, $msg, $this->getSectorID());
2870
	}
2871
2872
	/**
2873
	 * @param array<string, mixed> $values
2874
	 */
2875
	public function actionTaken(string $actionID, array $values): void {
2876
		if (!in_array($actionID, MISSION_ACTIONS)) {
2877
			throw new Exception('Unknown action: ' . $actionID);
2878
		}
2879
		// TODO: Reenable this once tested.     if($this->getAccount()->isLoggingEnabled())
2880
		switch ($actionID) {
2881
			case 'WalkSector':
2882
				$this->log(LOG_TYPE_MOVEMENT, 'Walks to sector: ' . $values['Sector']->getSectorID());
2883
				break;
2884
			case 'JoinAlliance':
2885
				$this->log(LOG_TYPE_ALLIANCE, 'joined alliance: ' . $values['Alliance']->getAllianceName());
2886
				break;
2887
			case 'LeaveAlliance':
2888
				$this->log(LOG_TYPE_ALLIANCE, 'left alliance: ' . $values['Alliance']->getAllianceName());
2889
				break;
2890
			case 'DisbandAlliance':
2891
				$this->log(LOG_TYPE_ALLIANCE, 'disbanded alliance ' . $values['Alliance']->getAllianceName());
2892
				break;
2893
			case 'KickPlayer':
2894
				$this->log(LOG_TYPE_ALLIANCE, 'kicked ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ') from alliance ' . $values['Alliance']->getAllianceName());
2895
				break;
2896
			case 'PlayerKicked':
2897
				$this->log(LOG_TYPE_ALLIANCE, 'was kicked from alliance ' . $values['Alliance']->getAllianceName() . ' by ' . $values['Player']->getAccount()->getLogin() . ' (' . $values['Player']->getPlayerName() . ')');
2898
				break;
2899
		}
2900
2901
		$this->getMissions();
2902
		foreach ($this->missions as $missionID => &$mission) {
2903
			if ($mission['Task'] !== false && $mission['Task']['Step'] == $actionID) {
2904
				$requirements = $mission['Task']['Detail'];
2905
				if (checkMissionRequirements($values, $requirements) === true) {
2906
					$mission['On Step']++;
2907
					$mission['Unread'] = true;
2908
					$this->setupMissionStep($missionID);
2909
					$this->rebuildMission($missionID);
2910
					$this->updateMission($missionID);
2911
				}
2912
			}
2913
		}
2914
	}
2915
2916
	/**
2917
	 * @param array<SmrPlayer> $otherPlayerArray
2918
	 */
2919
	public function canSeeAny(array $otherPlayerArray): bool {
2920
		foreach ($otherPlayerArray as $otherPlayer) {
2921
			if ($this->canSee($otherPlayer)) {
2922
				return true;
2923
			}
2924
		}
2925
		return false;
2926
	}
2927
2928
	public function canSee(self $otherPlayer): bool {
2929
		if (!$otherPlayer->getShip()->isCloaked()) {
2930
			return true;
2931
		}
2932
		if ($this->sameAlliance($otherPlayer)) {
2933
			return true;
2934
		}
2935
		if ($this->getExperience() >= $otherPlayer->getExperience()) {
2936
			return true;
2937
		}
2938
		return false;
2939
	}
2940
2941
	public function equals(self $otherPlayer = null): bool {
2942
		return $otherPlayer !== null && $this->getAccountID() == $otherPlayer->getAccountID() && $this->getGameID() == $otherPlayer->getGameID();
2943
	}
2944
2945
	public function sameAlliance(self $otherPlayer = null): bool {
2946
		return $this->equals($otherPlayer) || ($otherPlayer !== null && $this->getGameID() == $otherPlayer->getGameID() && $this->hasAlliance() && $this->getAllianceID() == $otherPlayer->getAllianceID());
2947
	}
2948
2949
	public function sharedForceAlliance(self $otherPlayer = null): bool {
2950
		return $this->sameAlliance($otherPlayer);
2951
	}
2952
2953
	public function forceNAPAlliance(self $otherPlayer = null): bool {
2954
		return $this->sameAlliance($otherPlayer);
2955
	}
2956
2957
	public function planetNAPAlliance(self $otherPlayer = null): bool {
2958
		return $this->sameAlliance($otherPlayer);
2959
	}
2960
2961
	public function traderNAPAlliance(self $otherPlayer = null): bool {
2962
		return $this->sameAlliance($otherPlayer);
2963
	}
2964
2965
	public function traderMAPAlliance(self $otherPlayer = null): bool {
2966
		return $this->traderAttackTraderAlliance($otherPlayer) && $this->traderDefendTraderAlliance($otherPlayer);
2967
	}
2968
2969
	public function traderAttackTraderAlliance(self $otherPlayer = null): bool {
2970
		return $this->sameAlliance($otherPlayer);
2971
	}
2972
2973
	public function traderDefendTraderAlliance(self $otherPlayer = null): bool {
2974
		return $this->sameAlliance($otherPlayer);
2975
	}
2976
2977
	public function traderAttackForceAlliance(self $otherPlayer = null): bool {
2978
		return $this->sameAlliance($otherPlayer);
2979
	}
2980
2981
	public function traderAttackPortAlliance(self $otherPlayer = null): bool {
2982
		return $this->sameAlliance($otherPlayer);
2983
	}
2984
2985
	public function traderAttackPlanetAlliance(self $otherPlayer = null): bool {
2986
		return $this->sameAlliance($otherPlayer);
2987
	}
2988
2989
	public function meetsAlignmentRestriction(int $restriction): bool {
2990
		if ($restriction < 0) {
2991
			return $this->getAlignment() <= $restriction;
2992
		}
2993
		if ($restriction > 0) {
2994
			return $this->getAlignment() >= $restriction;
2995
		}
2996
		return true;
2997
	}
2998
2999
	/**
3000
	 * Get an array of goods that are visible to the player
3001
	 *
3002
	 * @return array<int, TradeGood>
3003
	 */
3004
	public function getVisibleGoods(): array {
3005
		$visibleGoods = [];
3006
		foreach (TradeGood::getAll() as $goodID => $good) {
3007
			if ($this->meetsAlignmentRestriction($good->alignRestriction)) {
3008
				$visibleGoods[$goodID] = $good;
3009
			}
3010
		}
3011
		return $visibleGoods;
3012
	}
3013
3014
	/**
3015
	 * Returns an array of all unvisited sectors.
3016
	 *
3017
	 * @return array<int>
3018
	 */
3019
	public function getUnvisitedSectors(): array {
3020
		if (!isset($this->unvisitedSectors)) {
3021
			$this->unvisitedSectors = [];
3022
			// Note that this table actually has entries for the *unvisited* sectors!
3023
			$dbResult = $this->db->read('SELECT sector_id FROM player_visited_sector WHERE ' . $this->SQL);
3024
			foreach ($dbResult->records() as $dbRecord) {
3025
				$this->unvisitedSectors[] = $dbRecord->getInt('sector_id');
3026
			}
3027
		}
3028
		return $this->unvisitedSectors;
3029
	}
3030
3031
	/**
3032
	 * Check if player has visited the input sector.
3033
	 * Note that this populates the list of *all* unvisited sectors!
3034
	 */
3035
	public function hasVisitedSector(int $sectorID): bool {
3036
		return !in_array($sectorID, $this->getUnvisitedSectors());
3037
	}
3038
3039
	public function getLeaveNewbieProtectionHREF(): string {
3040
		return (new NewbieLeaveProcessor())->href();
3041
	}
3042
3043
	public function getExamineTraderHREF(): string {
3044
		$container = new ExamineTrader($this->getAccountID());
3045
		return $container->href();
3046
	}
3047
3048
	public function getAttackTraderHREF(): string {
3049
		return Globals::getAttackTraderHREF($this->getAccountID());
3050
	}
3051
3052
	public function getPlanetKickHREF(): string {
3053
		$container = new KickProcessor($this->getAccountID());
3054
		return $container->href();
3055
	}
3056
3057
	public function getTraderSearchHREF(): string {
3058
		$container = new SearchForTraderResult($this->getPlayerID());
3059
		return $container->href();
3060
	}
3061
3062
	public function getAllianceRosterHREF(): string {
3063
		return Globals::getAllianceRosterHREF($this->getAllianceID());
3064
	}
3065
3066
	public function getToggleWeaponHidingHREF(bool $ajax = false): string {
3067
		$container = new WeaponDisplayToggleProcessor();
3068
		$container->allowAjax = $ajax;
3069
		return $container->href();
3070
	}
3071
3072
	public function isDisplayWeapons(): bool {
3073
		return $this->displayWeapons;
3074
	}
3075
3076
	/**
3077
	 * Should weapons be displayed in the right panel?
3078
	 * This updates the player database directly because it is used with AJAX,
3079
	 * which does not acquire a sector lock.
3080
	 */
3081
	public function setDisplayWeapons(bool $bool): void {
3082
		if ($this->displayWeapons == $bool) {
3083
			return;
3084
		}
3085
		$this->displayWeapons = $bool;
3086
		$this->db->write('UPDATE player SET display_weapons=' . $this->db->escapeBoolean($this->displayWeapons) . ' WHERE ' . $this->SQL);
3087
	}
3088
3089
	public function update(): void {
3090
		$this->save();
3091
	}
3092
3093
	public function save(): void {
3094
		if ($this->hasChanged === true) {
3095
			$this->db->write('UPDATE player SET player_name=' . $this->db->escapeString($this->playerName) .
3096
				', player_id=' . $this->db->escapeNumber($this->playerID) .
3097
				', sector_id=' . $this->db->escapeNumber($this->sectorID) .
3098
				', last_sector_id=' . $this->db->escapeNumber($this->lastSectorID) .
3099
				', turns=' . $this->db->escapeNumber($this->turns) .
3100
				', last_turn_update=' . $this->db->escapeNumber($this->lastTurnUpdate) .
3101
				', newbie_turns=' . $this->db->escapeNumber($this->newbieTurns) .
3102
				', last_news_update=' . $this->db->escapeNumber($this->lastNewsUpdate) .
3103
				', attack_warning=' . $this->db->escapeString($this->attackColour) .
3104
				', dead=' . $this->db->escapeBoolean($this->dead) .
3105
				', newbie_status=' . $this->db->escapeBoolean($this->newbieStatus) .
3106
				', land_on_planet=' . $this->db->escapeBoolean($this->landedOnPlanet) .
3107
				', last_active=' . $this->db->escapeNumber($this->lastActive) .
3108
				', last_cpl_action=' . $this->db->escapeNumber($this->lastCPLAction) .
3109
				', race_id=' . $this->db->escapeNumber($this->raceID) .
3110
				', credits=' . $this->db->escapeNumber($this->credits) .
3111
				', experience=' . $this->db->escapeNumber($this->experience) .
3112
				', alignment=' . $this->db->escapeNumber($this->alignment) .
3113
				', military_payment=' . $this->db->escapeNumber($this->militaryPayment) .
3114
				', alliance_id=' . $this->db->escapeNumber($this->allianceID) .
3115
				', alliance_join=' . $this->db->escapeNumber($this->allianceJoinable) .
3116
				', ship_type_id=' . $this->db->escapeNumber($this->shipID) .
3117
				', kills=' . $this->db->escapeNumber($this->kills) .
3118
				', deaths=' . $this->db->escapeNumber($this->deaths) .
3119
				', assists=' . $this->db->escapeNumber($this->assists) .
3120
				', last_port=' . $this->db->escapeNumber($this->lastPort) .
3121
				', bank=' . $this->db->escapeNumber($this->bank) .
3122
				', zoom=' . $this->db->escapeNumber($this->zoom) .
3123
				', display_missions=' . $this->db->escapeBoolean($this->displayMissions) .
3124
				', force_drop_messages=' . $this->db->escapeBoolean($this->forceDropMessages) .
3125
				', group_scout_messages=' . $this->db->escapeString($this->scoutMessageGroupType->value) .
3126
				', ignore_globals=' . $this->db->escapeBoolean($this->ignoreGlobals) .
3127
				', newbie_warning = ' . $this->db->escapeBoolean($this->newbieWarning) .
3128
				', name_changed = ' . $this->db->escapeBoolean($this->nameChanged) .
3129
				', race_changed = ' . $this->db->escapeBoolean($this->raceChanged) .
3130
				', combat_drones_kamikaze_on_mines = ' . $this->db->escapeBoolean($this->combatDronesKamikazeOnMines) .
3131
				', under_attack = ' . $this->db->escapeBoolean($this->underAttack) .
3132
				' WHERE ' . $this->SQL);
3133
			$this->hasChanged = false;
3134
		}
3135
		$bounties = $this->bounties ?? []; // no need to fetch if unset
3136
		foreach ($bounties as $bounty) {
3137
			$bounty->update();
3138
		}
3139
		$this->saveHOF();
3140
	}
3141
3142
	public function saveHOF(): void {
3143
		foreach (self::$hasHOFVisChanged as $hofType => $changeType) {
3144
			if ($changeType == self::HOF_NEW) {
3145
				$this->db->insert('hof_visibility', [
3146
					'type' => $this->db->escapeString($hofType),
3147
					'visibility' => $this->db->escapeString(self::$HOFVis[$hofType]),
3148
				]);
3149
			} else {
3150
				$this->db->write('UPDATE hof_visibility SET visibility = ' . $this->db->escapeString(self::$HOFVis[$hofType]) . ' WHERE type = ' . $this->db->escapeString($hofType));
3151
			}
3152
			unset(self::$hasHOFVisChanged[$hofType]);
3153
		}
3154
3155
		foreach ($this->hasHOFChanged as $hofType => $changeType) {
3156
			$amount = $this->HOF[$hofType];
3157
			if ($changeType === self::HOF_NEW) {
3158
				if ($amount > 0) {
3159
					$this->db->insert('player_hof', [
3160
						'account_id' => $this->db->escapeNumber($this->getAccountID()),
3161
						'game_id' => $this->db->escapeNumber($this->getGameID()),
3162
						'type' => $this->db->escapeString($hofType),
3163
						'amount' => $this->db->escapeNumber($amount),
3164
					]);
3165
				}
3166
			} elseif ($changeType === self::HOF_CHANGED) {
3167
				$this->db->write('UPDATE player_hof
3168
					SET amount=' . $this->db->escapeNumber($amount) . '
3169
					WHERE ' . $this->SQL . ' AND type = ' . $this->db->escapeString($hofType));
3170
			}
3171
			unset($this->hasHOFChanged[$hofType]);
3172
		}
3173
	}
3174
3175
}
3176