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

AbstractSmrPlayer::doMessageSending()   B
last analyzed

Complexity

Conditions 7
Paths 8

Size

Total Lines 47
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 35
c 0
b 0
f 0
nc 8
nop 8
dl 0
loc 47
rs 8.4266

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php declare(strict_types=1);
2
require_once('missions.inc.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
		return 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
		return /** @scrutinizer ignore-call */ IFloor($timeDiff * $ship->getRealSpeed() / 3600);
Loading history...
2562
	}
2563
2564
	public function updateTurns(): void {
2565
		// is account validated?
2566
		if (!$this->getAccount()->isValidated()) {
2567
			return;
2568
		}
2569
2570
		// how many turns would he get right now?
2571
		$extraTurns = $this->getTurnsGained(Epoch::time());
2572
2573
		// do we have at least one turn to give?
2574
		if ($extraTurns > 0) {
2575
			// recalc the time to avoid rounding errors
2576
			$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

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