Scrutinizer GitHub App not installed

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

Install GitHub App

Failed Conditions
Push — master ( f174b5...646f17 )
by Dan
21s queued 18s
created

AbstractSmrPlayer   F

Complexity

Total Complexity 641

Size/Duplication

Total Lines 3199
Duplicated Lines 0 %

Importance

Changes 9
Bugs 2 Features 1
Metric Value
eloc 1632
c 9
b 2
f 1
dl 0
loc 3199
rs 0.8
wmc 641

295 Methods

Rating   Name   Duplication   Size   Complexity  
A setGroupScoutMessages() 0 6 2
A getGroupScoutMessages() 0 2 1
A traderNAPAlliance() 0 2 1
A setZoom() 0 8 2
A getSharingPlayers() 0 26 5
A sameAlliance() 0 2 5
A planetNAPAlliance() 0 2 1
A decreaseZoom() 0 5 2
C actionTaken() 0 36 12
A canSee() 0 11 4
A forceNAPAlliance() 0 2 1
A getAccount() 0 2 1
A getZoom() 0 2 1
A createPlayer() 0 31 3
A sendGlobalMessage() 0 22 4
A getGameID() 0 2 1
A meetsAlignmentRestriction() 0 8 3
A traderDefendTraderAlliance() 0 2 1
A canSeeAny() 0 7 3
B doMessageSending() 0 49 7
A increaseZoom() 0 5 2
A log() 0 2 1
A getAttackColour() 0 2 1
A sharedForceAlliance() 0 2 1
A traderAttackTraderAlliance() 0 2 1
A getGame() 0 2 1
A isIgnoreGlobals() 0 2 1
A setIgnoreGlobals() 0 6 2
A getNewbieTurns() 0 2 1
A traderAttackPortAlliance() 0 2 1
A getAccountID() 0 2 1
A sendMessageToBox() 0 3 1
A getSQL() 0 2 1
A equals() 0 2 3
A getVisibleGoods() 0 9 3
A setAttackColour() 0 6 2
A traderMAPAlliance() 0 2 2
A traderAttackForceAlliance() 0 2 1
A hasNewbieTurns() 0 2 1
A traderAttackPlanetAlliance() 0 2 1
A markMissionsRead() 0 11 3
A rebuildMission() 0 13 2
A refreshCache() 0 4 3
B claimMissionReward() 0 29 8
A clearCache() 0 3 1
A savePlayers() 0 4 3
A getSectorPlayersByAlliances() 0 8 3
A deleteMission() 0 8 2
A addMission() 0 24 2
A declineMission() 0 3 1
A increaseAssists() 0 6 2
A sendMessageFromAllianceAmbassador() 0 7 2
A getNextLevelPercentRemaining() 0 2 1
A sendMessageFromRace() 0 7 2
A sendMessageFromPort() 0 5 1
A getDeaths() 0 2 1
A getSafeAttackRating() 0 2 1
A sendMessageFromFedClerk() 0 3 1
A decreaseAlignment() 0 9 3
A getKills() 0 2 1
A getNextLevelPercentAcquired() 0 5 2
A getCredits() 0 2 1
B sendMessage() 0 38 6
A getAssists() 0 2 1
A getExperience() 0 2 1
A setKills() 0 6 2
A increaseAlignment() 0 9 3
A sendMessageFromCasino() 0 7 2
A setBank() 0 12 4
A setDeaths() 0 6 2
A increaseBank() 0 11 3
A setMessagesRead() 0 3 1
A getBank() 0 2 1
A sendMessageFromPlanet() 0 5 1
A getAlignment() 0 2 1
A sendMessageFromAllianceCommand() 0 3 1
A increaseKills() 0 5 2
A hasFederalProtection() 0 20 6
A canFight() 0 5 4
A sendMessageFromOpAnnounce() 0 6 2
A decreaseBank() 0 9 3
A increaseDeaths() 0 5 2
A canBeProtectedByRace() 0 14 4
A setAlignment() 0 6 2
A setDead() 0 6 2
A sendMessageFromAdmin() 0 7 2
A updateMission() 0 17 2
A getTurnsColor() 0 5 1
A getLastTurnUpdate() 0 2 1
A getActiveMissions() 0 8 3
A hasMission() 0 2 1
A giveStartingTurns() 0 4 1
A getTimeUntilMaxTurns() 0 7 1
A getCurrentBountyAmount() 0 3 1
A getKillsRank() 0 2 1
A getAvailableMissions() 0 12 4
A getMaxTurns() 0 2 1
A decreaseHOF() 0 8 3
A getMissions() 0 18 3
A giveTurns() 0 6 3
A setNewbieWarning() 0 6 2
A getBounty() 0 5 2
A killPlayer() 0 34 2
A getBounties() 0 3 1
A getHOF() 0 13 4
A getLastActive() 0 2 1
A hasBounties() 0 2 1
A setBountiesClaimable() 0 5 3
A decreaseCurrentBountySmrCredits() 0 5 2
A increaseCurrentBountyAmount() 0 5 2
A createBounty() 0 10 1
A updateLastCPLAction() 0 2 1
C setHOF() 0 45 12
A getMission() 0 6 2
A setBounty() 0 3 1
A getNextBountyID() 0 6 2
A killPlayerByForces() 0 36 2
A setTurns() 0 7 2
A getDeathsRank() 0 2 1
A incrementAllianceVsKills() 0 3 1
A killPlayerByPort() 0 33 2
A hasBounty() 0 3 1
A getExperienceRank() 0 2 1
A takeTurns() 0 17 3
A getBountyAmount() 0 3 1
A setCurrentBountyAmount() 0 7 2
A hasTurns() 0 2 1
A getTurns() 0 2 1
A setupMissionStep() 0 18 4
A isDisplayMissions() 0 2 1
A increaseCurrentBountySmrCredits() 0 5 2
A getHOFData() 0 17 5
A setCurrentBountySmrCredits() 0 7 2
A setLastActive() 0 6 2
A setLastCPLAction() 0 6 2
A setDisplayMissions() 0 6 2
A updateTurns() 0 15 3
A getLastCPLAction() 0 2 1
A decreaseCurrentBountyAmount() 0 5 2
A getTurnsGained() 0 5 1
A getAssistsRank() 0 2 1
A hasCurrentBounty() 0 8 4
A setLastTurnUpdate() 0 6 2
A getCurrentBounty() 0 8 4
A getCurrentBountySmrCredits() 0 3 1
A killPlayerByPlanet() 0 36 2
A setBountyAmount() 0 4 1
A getTurnsLevel() 0 11 4
A incrementAllianceVsDeaths() 0 3 1
A getNewbieWarning() 0 2 1
A increaseHOF() 0 8 3
A getHOFVis() 0 8 3
A getNextLevelExperience() 0 2 1
A getMaxLevel() 0 2 1
A setRelations() 0 12 3
A isOnCouncil() 0 2 1
A increaseExperience() 0 10 3
A getClaimableBounties() 0 17 2
A getPlanetPlayers() 0 12 4
A getPlayerByPlayerID() 0 8 2
A setForceDropMessages() 0 6 2
A isDisplayWeapons() 0 2 1
A getUnvisitedSectors() 0 10 3
A getLevelID() 0 15 4
A getBBLink() 0 2 1
A getExamineTraderHREF() 0 4 1
A getPlayer() 0 5 3
A joinAlliance() 0 21 4
A getNextLevel() 0 6 2
A hasMilitaryPayment() 0 2 1
A setCombatDronesKamikazeOnMines() 0 6 2
A getLastSectorID() 0 2 1
A getAllianceBBLink() 0 2 2
A setDisplayWeapons() 0 6 2
A setLastSectorID() 0 6 2
A getHome() 0 9 2
A getTickers() 0 15 3
A hasTicker() 0 2 1
A isFlagship() 0 2 2
A getLeaveNewbieProtectionHREF() 0 2 1
A isRaceChanged() 0 2 1
A setLastPort() 0 6 2
A increaseRelationsByTrade() 0 7 2
A getSector() 0 2 1
A setMilitaryPayment() 0 6 2
A getRelations() 0 12 3
A getDisplayName() 0 10 3
A computeRanking() 0 11 1
A getTraderSearchHREF() 0 4 1
A getSectorID() 0 2 1
A deletePlottedCourse() 0 3 1
A getCustomShipName() 0 10 3
A isNPC() 0 2 1
A shootPlanet() 0 2 1
A update() 0 2 1
A getSectorPlayers() 0 12 4
A getRelation() 0 3 1
A setPlottedCourse() 0 9 3
A getThisLevelExperience() 0 3 1
A getToggleWeaponHidingHREF() 0 5 1
B getPlottedCourse() 0 28 8
A shootPort() 0 2 1
A sendAllianceInvitation() 0 7 2
A getScoutMessageGroupLimit() 0 5 1
A getLastPort() 0 2 1
A setSectorID() 0 16 2
A setNewbieTurns() 0 6 2
A giveStartingRelations() 0 5 3
A getColouredRaceNameOrDefault() 0 6 2
A isPresident() 0 2 1
A hasTickers() 0 2 1
A setRaceID() 0 6 2
A __construct() 0 55 4
A getAttackTraderHREF() 0 2 1
A isPartOfCourse() 0 10 3
A updateLastNewsUpdate() 0 2 1
A getGalaxyPlayers() 0 12 2
A setAllianceJoinable() 0 6 2
A addDestinationButton() 0 23 4
A setUnderAttack() 0 6 2
A setRaceChanged() 0 3 1
A getAllianceID() 0 2 1
A getStoredDestinations() 0 14 3
A getLevelName() 0 6 2
A getAllianceRole() 0 16 4
A getAllianceDisplayName() 0 5 2
A shootForces() 0 2 1
A setShipTypeID() 0 6 2
A setAllianceID() 0 11 4
A getPlayerID() 0 2 1
A hasPlottedCourse() 0 2 1
A getLastNewsUpdate() 0 2 1
A setPlayerNameByPlayer() 0 3 1
B leaveAlliance() 0 23 7
A saveHOF() 0 13 5
A getColouredRaceName() 0 2 1
A setCustomShipName() 0 3 1
A isGPEditor() 0 2 1
A getPersonalRelations() 0 3 1
A getPlanet() 0 7 2
A getSectorPort() 0 2 1
A setCredits() 0 12 4
A hasCustomShipName() 0 2 1
A deleteDestinationButton() 0 14 3
A hasAlliance() 0 2 1
A increaseMilitaryPayment() 0 5 2
A getJumpInfo() 0 10 2
A getAlliance() 0 2 1
A setExperience() 0 16 4
A setPlayerName() 0 3 1
A getShip() 0 2 1
A doHOFSave() 0 16 6
A updateNewbieStatus() 0 5 2
A isUnderAttack() 0 2 1
F killPlayerByPlayer() 0 150 19
A isForceDropMessages() 0 2 1
A isAllianceLeader() 0 2 1
A getGPWriter() 0 9 3
A getSectorPlanet() 0 2 1
A isLandedOnPlanet() 0 2 1
A setLandedOnPlanet() 0 6 2
A getPlanetKickHREF() 0 4 1
A removeUnderAttack() 0 12 4
A getAlliancePlayers() 0 12 4
A shootPlayers() 0 2 1
A getPersonalRelation() 0 3 1
A hasVisitedSector() 0 2 1
B save() 0 66 9
A getShipTypeID() 0 2 1
A decreaseRelationsByTrade() 0 3 1
A getPersonalRelationsData() 0 11 4
A isCombatDronesKamikazeOnMines() 0 2 1
A isDead() 0 2 1
A __sleep() 0 2 1
A decreaseMilitaryPayment() 0 5 2
A getBountiesData() 0 13 3
A decreaseRelations() 0 9 3
A getPlayerName() 0 2 1
A canChangeRace() 0 2 2
A setLastNewsUpdate() 0 6 2
B moveDestinationButton() 0 21 7
A decreaseExperience() 0 10 3
A getAllianceJoinable() 0 2 1
A increaseCredits() 0 11 3
A getLinkedDisplayName() 0 6 2
A getMilitaryPayment() 0 2 1
A isNameChanged() 0 2 1
A decreaseCredits() 0 9 3
A increaseRelations() 0 9 3
A getAllianceRosterHREF() 0 2 1
A isDraftLeader() 0 6 2
A setNameChanged() 0 3 1
A getTicker() 0 6 2
A hasNewbieStatus() 0 2 1
A getPlayerByPlayerName() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractSmrPlayer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractSmrPlayer, and based on these observations, apply Extract Interface, too.

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