Scrutinizer GitHub App not installed

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

Install GitHub App

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

src/lib/Default/SmrForce.php (1 issue)

Labels
Severity
1
<?php declare(strict_types=1);
2
3
use Smr\Database;
4
use Smr\DatabaseRecord;
5
use Smr\Epoch;
6
use Smr\Pages\Player\AttackForcesProcessor;
7
use Smr\Pages\Player\ForcesDrop;
8
use Smr\Pages\Player\ForcesDropProcessor;
9
use Smr\Pages\Player\ForcesRefreshAllProcessor;
10
use Smr\Pages\Player\ForcesRefreshProcessor;
11
12
class SmrForce {
13
14
	/** @var array<int, array<int, array<int, self>>> */
15
	protected static array $CACHE_FORCES = [];
16
	/** @var array<int, array<int, array<int, self>>> */
17
	protected static array $CACHE_SECTOR_FORCES = [];
18
	/** @var array<int, array<int, bool>> */
19
	protected static array $TIDIED_UP = [];
20
21
	public const LOWEST_MAX_EXPIRE_SCOUTS_ONLY = 432000; // 5 days
22
	protected const TIME_PER_SCOUT_ONLY = 86400; // 1 = 1 day
23
	protected const TIME_PERCENT_PER_SCOUT = 0.02; // 1/50th
24
	protected const TIME_PERCENT_PER_COMBAT = 0.02; // 1/50th
25
	protected const TIME_PERCENT_PER_MINE = 0.02; // 1/50th
26
	public const REFRESH_ALL_TIME_PER_STACK = 1; // 1 second
27
28
	public const MAX_MINES = 50;
29
	public const MAX_CDS = 50;
30
	public const MAX_SDS = 5;
31
32
	protected Database $db;
33
	protected readonly string $SQL;
34
35
	protected int $combatDrones = 0;
36
	protected int $scoutDrones = 0;
37
	protected int $mines = 0;
38
	protected int $expire = 0;
39
40
	protected bool $isNew;
41
	protected bool $hasChanged = false;
42
43
	public function __sleep() {
44
		return ['ownerID', 'sectorID', 'gameID'];
45
	}
46
47
	public static function clearCache(): void {
48
		self::$CACHE_FORCES = [];
49
		self::$CACHE_SECTOR_FORCES = [];
50
	}
51
52
	public static function saveForces(): void {
53
		foreach (self::$CACHE_FORCES as $gameForces) {
54
			foreach ($gameForces as $gameSectorForces) {
55
				foreach ($gameSectorForces as $forces) {
56
					$forces->update();
57
				}
58
			}
59
		}
60
	}
61
62
	/**
63
	 * @return array<int, array<int, self>>
64
	 */
65
	public static function getGalaxyForces(int $gameID, int $galaxyID, bool $forceUpdate = false): array {
66
		$db = Database::getInstance();
67
		$dbResult = $db->read('SELECT sector_has_forces.* FROM sector_has_forces LEFT JOIN sector USING(game_id, sector_id) WHERE game_id = ' . $db->escapeNumber($gameID) . ' AND galaxy_id = ' . $db->escapeNumber($galaxyID));
68
		$galaxyForces = [];
69
		foreach ($dbResult->records() as $dbRecord) {
70
			$sectorID = $dbRecord->getInt('sector_id');
71
			$ownerID = $dbRecord->getInt('owner_id');
72
			$force = self::getForce($gameID, $sectorID, $ownerID, $forceUpdate, $dbRecord);
73
			self::$CACHE_SECTOR_FORCES[$gameID][$sectorID][$ownerID] = $force;
74
			$galaxyForces[$sectorID][$ownerID] = $force;
75
		}
76
		return $galaxyForces;
77
	}
78
79
	/**
80
	 * @return array<int, self>
81
	 */
82
	public static function getSectorForces(int $gameID, int $sectorID, bool $forceUpdate = false): array {
83
		if ($forceUpdate || !isset(self::$CACHE_SECTOR_FORCES[$gameID][$sectorID])) {
84
			self::tidyUpForces(SmrGalaxy::getGalaxyContaining($gameID, $sectorID));
85
			$db = Database::getInstance();
86
			$dbResult = $db->read('SELECT * FROM sector_has_forces WHERE sector_id = ' . $db->escapeNumber($sectorID) . ' AND game_id=' . $db->escapeNumber($gameID) . ' ORDER BY expire_time ASC');
87
			$forces = [];
88
			foreach ($dbResult->records() as $dbRecord) {
89
				$ownerID = $dbRecord->getInt('owner_id');
90
				$forces[$ownerID] = self::getForce($gameID, $sectorID, $ownerID, $forceUpdate, $dbRecord);
91
			}
92
			self::$CACHE_SECTOR_FORCES[$gameID][$sectorID] = $forces;
93
		}
94
		return self::$CACHE_SECTOR_FORCES[$gameID][$sectorID];
95
	}
96
97
	public static function getForce(int $gameID, int $sectorID, int $ownerID, bool $forceUpdate = false, DatabaseRecord $dbRecord = null): self {
98
		if ($forceUpdate || !isset(self::$CACHE_FORCES[$gameID][$sectorID][$ownerID])) {
99
			self::tidyUpForces(SmrGalaxy::getGalaxyContaining($gameID, $sectorID));
100
			$p = new self($gameID, $sectorID, $ownerID, $dbRecord);
101
			self::$CACHE_FORCES[$gameID][$sectorID][$ownerID] = $p;
102
		}
103
		return self::$CACHE_FORCES[$gameID][$sectorID][$ownerID];
104
	}
105
106
	public static function tidyUpForces(SmrGalaxy $galaxyToTidy): void {
107
		if (!isset(self::$TIDIED_UP[$galaxyToTidy->getGameID()][$galaxyToTidy->getGalaxyID()])) {
108
			self::$TIDIED_UP[$galaxyToTidy->getGameID()][$galaxyToTidy->getGalaxyID()] = true;
109
			$db = Database::getInstance();
110
			$db->write('UPDATE sector_has_forces
111
						SET refresher=0,
112
							expire_time = (refresh_at + if(combat_drones+mines=0,
113
															LEAST(' . $db->escapeNumber(self::LOWEST_MAX_EXPIRE_SCOUTS_ONLY) . ', scout_drones*' . $db->escapeNumber(self::TIME_PER_SCOUT_ONLY) . '),
114
															LEAST(' . $db->escapeNumber($galaxyToTidy->getMaxForceTime()) . ', (combat_drones*' . $db->escapeNumber(self::TIME_PERCENT_PER_COMBAT) . '+scout_drones*' . $db->escapeNumber(self::TIME_PERCENT_PER_SCOUT) . '+mines*' . $db->escapeNumber(self::TIME_PERCENT_PER_MINE) . ')*' . $db->escapeNumber($galaxyToTidy->getMaxForceTime()) . ')
115
														))
116
						WHERE game_id = ' . $db->escapeNumber($galaxyToTidy->getGameID()) . ' AND sector_id >= ' . $db->escapeNumber($galaxyToTidy->getStartSector()) . ' AND sector_id <= ' . $db->escapeNumber($galaxyToTidy->getEndSector()) . ' AND refresher != 0 AND refresh_at <= ' . $db->escapeNumber(Epoch::time()));
117
			$db->write('DELETE FROM sector_has_forces WHERE expire_time < ' . $db->escapeNumber(Epoch::time()));
118
		}
119
	}
120
121
	protected function __construct(
122
		protected readonly int $gameID,
123
		protected readonly int $sectorID,
124
		protected readonly int $ownerID,
125
		DatabaseRecord $dbRecord = null
126
	) {
127
		$this->db = Database::getInstance();
128
		$this->SQL = 'game_id = ' . $this->db->escapeNumber($gameID) . '
129
		              AND sector_id = ' . $this->db->escapeNumber($sectorID) . '
130
		              AND owner_id = ' . $this->db->escapeNumber($ownerID);
131
132
		if ($dbRecord === null) {
133
			$dbResult = $this->db->read('SELECT * FROM sector_has_forces WHERE ' . $this->SQL);
134
			if ($dbResult->hasRecord()) {
135
				$dbRecord = $dbResult->record();
136
			}
137
		}
138
		$this->isNew = $dbRecord === null;
139
140
		if (!$this->isNew) {
141
			$this->combatDrones = $dbRecord->getInt('combat_drones');
142
			$this->scoutDrones = $dbRecord->getInt('scout_drones');
143
			$this->mines = $dbRecord->getInt('mines');
144
			$this->expire = $dbRecord->getInt('expire_time');
145
		}
146
	}
147
148
	public function exists(): bool {
149
		return ($this->hasCDs() || $this->hasSDs() || $this->hasMines()) && !$this->hasExpired();
150
	}
151
152
	public function hasMaxCDs(): bool {
153
		return $this->getCDs() >= self::MAX_CDS;
154
	}
155
156
	public function hasMaxSDs(): bool {
157
		return $this->getSDs() >= self::MAX_SDS;
158
	}
159
160
	public function hasMaxMines(): bool {
161
		return $this->getMines() >= self::MAX_MINES;
162
	}
163
164
	public function hasCDs(): bool {
165
		return $this->getCDs() > 0;
166
	}
167
168
	public function hasSDs(): bool {
169
		return $this->getSDs() > 0;
170
	}
171
172
	public function hasMines(): bool {
173
		return $this->getMines() > 0;
174
	}
175
176
	public function getCDs(): int {
177
		return $this->combatDrones;
178
	}
179
180
	public function getSDs(): int {
181
		return $this->scoutDrones;
182
	}
183
184
	public function getMines(): int {
185
		return $this->mines;
186
	}
187
188
	public function addMines(int $amount): void {
189
		if ($amount < 0) {
190
			throw new Exception('Cannot add negative mines.');
191
		}
192
		$this->setMines($this->getMines() + $amount);
193
	}
194
195
	public function addCDs(int $amount): void {
196
		if ($amount < 0) {
197
			throw new Exception('Cannot add negative CDs.');
198
		}
199
		$this->setCDs($this->getCDs() + $amount);
200
	}
201
202
	public function addSDs(int $amount): void {
203
		if ($amount < 0) {
204
			throw new Exception('Cannot add negative SDs.');
205
		}
206
		$this->setSDs($this->getSDs() + $amount);
207
	}
208
209
	public function takeMines(int $amount): void {
210
		if ($amount < 0) {
211
			throw new Exception('Cannot take negative mines.');
212
		}
213
		$this->setMines($this->getMines() - $amount);
214
	}
215
216
	public function takeCDs(int $amount): void {
217
		if ($amount < 0) {
218
			throw new Exception('Cannot take negative CDs.');
219
		}
220
		$this->setCDs($this->getCDs() - $amount);
221
	}
222
223
	public function takeSDs(int $amount): void {
224
		if ($amount < 0) {
225
			throw new Exception('Cannot take negative SDs.');
226
		}
227
		$this->setSDs($this->getSDs() - $amount);
228
	}
229
230
	public function setMines(int $amount): void {
231
		if ($amount < 0) {
232
			throw new Exception('Cannot set negative mines.');
233
		}
234
		if ($amount == $this->getMines()) {
235
			return;
236
		}
237
		$this->hasChanged = true;
238
		$this->mines = $amount;
239
	}
240
241
	public function setCDs(int $amount): void {
242
		if ($amount < 0) {
243
			throw new Exception('Cannot set negative CDs.');
244
		}
245
		if ($amount == $this->getCDs()) {
246
			return;
247
		}
248
		$this->hasChanged = true;
249
		$this->combatDrones = $amount;
250
	}
251
252
	public function setSDs(int $amount): void {
253
		if ($amount < 0) {
254
			throw new Exception('Cannot set negative SDs.');
255
		}
256
		if ($amount == $this->getSDs()) {
257
			return;
258
		}
259
		$this->hasChanged = true;
260
		$this->scoutDrones = $amount;
261
	}
262
263
	public function hasExpired(): bool {
264
		return $this->expire < Epoch::time();
265
	}
266
267
	public function getExpire(): int {
268
		return $this->expire;
269
	}
270
271
	public function setExpire(int $time): void {
272
		if ($time < 0) {
273
			throw new Exception('Cannot set negative expiry.');
274
		}
275
		if ($time == $this->getExpire()) {
276
			return;
277
		}
278
		if ($time > Epoch::time() + $this->getMaxExpireTime()) {
279
			$time = Epoch::time() + $this->getMaxExpireTime();
280
		}
281
		$this->hasChanged = true;
282
		$this->expire = $time;
283
		if (!$this->isNew) {
284
			$this->update();
285
		}
286
	}
287
288
	public function updateExpire(): void {
289
		// Changed (26/10/05) - scout drones count * 2
290
		if ($this->getCDs() == 0 && $this->getMines() == 0 && $this->getSDs() > 0) {
291
			$time = self::TIME_PER_SCOUT_ONLY * $this->getSDs();
292
		} else {
293
			$time = ($this->getCDs() * self::TIME_PERCENT_PER_COMBAT + $this->getSDs() * self::TIME_PERCENT_PER_SCOUT + $this->getMines() * self::TIME_PERCENT_PER_MINE) * $this->getMaxGalaxyExpireTime();
294
		}
295
		$this->setExpire(Epoch::time() + IFloor($time));
296
	}
297
298
	public function getMaxExpireTime(): int {
299
		if ($this->hasCDs() || $this->hasMines()) {
300
			return $this->getMaxGalaxyExpireTime();
301
		}
302
		if ($this->hasSDs()) {
303
			return max(self::LOWEST_MAX_EXPIRE_SCOUTS_ONLY, $this->getMaxGalaxyExpireTime());
304
		}
305
		return 0;
306
	}
307
308
	public function getMaxGalaxyExpireTime(): int {
309
		return $this->getGalaxy()->getMaxForceTime();
310
	}
311
312
	public function getBumpTurnCost(AbstractSmrShip $ship): int {
313
		$mines = $this->getMines();
314
		if ($mines <= 1) {
315
			return 0;
316
		}
317
		if ($mines < 10) {
318
			$turns = 1;
319
		} elseif ($mines < 25) {
320
			$turns = 2;
321
		} else {
322
			$turns = 3;
323
		}
324
		if ($ship->isFederal() || $ship->hasDCS()) {
325
			$turns -= 1;
326
		}
327
		return $turns;
328
	}
329
330
	public function getAttackTurnCost(AbstractSmrShip $ship): int {
331
		if ($ship->isFederal() || $ship->hasDCS()) {
332
			return 2;
333
		}
334
		return 3;
335
	}
336
337
	public function getOwnerID(): int {
338
		return $this->ownerID;
339
	}
340
341
	public function getGameID(): int {
342
		return $this->gameID;
343
	}
344
345
	public function getSector(): SmrSector {
346
		return SmrSector::getSector($this->getGameID(), $this->getSectorID());
347
	}
348
349
	public function getSectorID(): int {
350
		return $this->sectorID;
351
	}
352
353
	public function ping(string $pingMessage, AbstractSmrPlayer $playerPinging, bool $skipCheck = false): void {
354
		if (!$this->hasSDs() && !$skipCheck) {
355
			return;
356
		}
357
		$owner = $this->getOwner();
358
		if (!$playerPinging->sameAlliance($owner)) {
359
			$playerPinging->sendMessage($owner->getAccountID(), MSG_SCOUT, $pingMessage, false);
360
		}
361
	}
362
363
	public function getGalaxy(): SmrGalaxy {
364
		return SmrGalaxy::getGalaxyContaining($this->getGameID(), $this->getSectorID());
365
	}
366
367
	public function getOwner(): AbstractSmrPlayer {
368
		return SmrPlayer::getPlayer($this->getOwnerID(), $this->getGameID());
369
	}
370
371
	public function update(): void {
372
		if (!$this->isNew) {
373
			if (!$this->exists()) {
374
				$this->db->write('DELETE FROM sector_has_forces WHERE ' . $this->SQL);
375
				$this->isNew = true;
376
			} elseif ($this->hasChanged) {
377
				$this->db->write('UPDATE sector_has_forces SET combat_drones = ' . $this->db->escapeNumber($this->combatDrones) . ', scout_drones = ' . $this->db->escapeNumber($this->scoutDrones) . ', mines = ' . $this->db->escapeNumber($this->mines) . ', expire_time = ' . $this->db->escapeNumber($this->expire) . ' WHERE ' . $this->SQL);
378
			}
379
		} elseif ($this->exists()) {
380
			$this->db->insert('sector_has_forces', [
381
				'game_id' => $this->db->escapeNumber($this->gameID),
382
				'sector_id' => $this->db->escapeNumber($this->sectorID),
383
				'owner_id' => $this->db->escapeNumber($this->ownerID),
384
				'combat_drones' => $this->db->escapeNumber($this->combatDrones),
385
				'scout_drones' => $this->db->escapeNumber($this->scoutDrones),
386
				'mines' => $this->db->escapeNumber($this->mines),
387
				'expire_time' => $this->db->escapeNumber($this->expire),
388
			]);
389
			$this->isNew = false;
390
		}
391
		// This instance is now in sync with the database
392
		$this->hasChanged = false;
393
	}
394
395
	/**
396
	 * Update the table fields associated with using Refresh All
397
	 */
398
	public function updateRefreshAll(AbstractSmrPlayer $player, int $refreshTime): void {
399
		$this->db->write('UPDATE sector_has_forces SET refresh_at=' . $this->db->escapeNumber($refreshTime) . ', refresher=' . $this->db->escapeNumber($player->getAccountID()) . ' WHERE ' . $this->SQL);
400
	}
401
402
	public function getExamineDropForcesHREF(): string {
403
		$container = new ForcesDrop($this->getOwnerID());
404
		return $container->href();
405
	}
406
407
	public function getAttackForcesHREF(): string {
408
		$container = new AttackForcesProcessor($this->getOwnerID());
409
		return $container->href();
410
	}
411
412
	public function getRefreshHREF(): string {
413
		$container = new ForcesRefreshProcessor($this->getOwnerID());
414
		return $container->href();
415
	}
416
417
	public function getDropSDHREF(): string {
418
		$container = new ForcesDropProcessor($this->getOwnerID(), dropSDs: 1);
419
		return $container->href();
420
	}
421
422
	public function getTakeSDHREF(): string {
423
		$container = new ForcesDropProcessor($this->getOwnerID(), takeSDs: 1);
424
		return $container->href();
425
	}
426
427
	public function getDropCDHREF(): string {
428
		$container = new ForcesDropProcessor($this->getOwnerID(), dropCDs: 1);
429
		return $container->href();
430
	}
431
432
	public function getTakeCDHREF(): string {
433
		$container = new ForcesDropProcessor($this->getOwnerID(), takeCDs: 1);
434
		return $container->href();
435
	}
436
437
	public function getDropMineHREF(): string {
438
		$container = new ForcesDropProcessor($this->getOwnerID(), dropMines: 1);
439
		return $container->href();
440
	}
441
442
	public function getTakeMineHREF(): string {
443
		$container = new ForcesDropProcessor($this->getOwnerID(), takeMines: 1);
444
		return $container->href();
445
	}
446
447
	public static function getRefreshAllHREF(): string {
448
		$container = new ForcesRefreshAllProcessor();
449
		return $container->href();
450
	}
451
452
	/**
453
	 * @param array<AbstractSmrPlayer> $targetPlayers
454
	 * @return array<string, mixed>
455
	 */
456
	public function shootPlayers(array $targetPlayers, bool $minesAreAttacker): array {
457
		$results = ['TotalDamage' => 0];
458
		if (!$this->exists()) {
459
			$results['DeadBeforeShot'] = true;
460
			return $results;
461
		}
462
		$results['DeadBeforeShot'] = false;
463
464
		if ($this->hasMines()) {
465
			$thisMines = new SmrMines($this->getMines());
466
			$results['Results']['Mines'] = $thisMines->shootPlayerAsForce($this, array_rand_value($targetPlayers), $minesAreAttacker);
467
			$this->setMines($thisMines->getAmount()); // kamikaze
468
			$results['TotalDamage'] += $results['Results']['Mines']['ActualDamage']['TotalDamage'];
469
		}
470
471
		if ($this->hasCDs()) {
472
			$thisCDs = new SmrCombatDrones($this->getCDs());
473
			$results['Results']['Drones'] = $thisCDs->shootPlayerAsForce($this, array_rand_value($targetPlayers));
474
			$results['TotalDamage'] += $results['Results']['Drones']['ActualDamage']['TotalDamage'];
475
		}
476
477
		if (!$minesAreAttacker) {
478
			if ($this->hasSDs()) {
479
				$thisSDs = new SmrScoutDrones($this->getSDs());
480
				$results['Results']['Scouts'] = $thisSDs->shootPlayerAsForce($this, array_rand_value($targetPlayers));
481
				$this->setSDs($thisSDs->getAmount()); // kamikaze
482
				$results['TotalDamage'] += $results['Results']['Scouts']['ActualDamage']['TotalDamage'];
483
			}
484
		}
485
486
		$results['ForcesDestroyed'] = !$this->exists();
487
		return $results;
488
	}
489
490
	/**
491
	 * @param array<string, int|bool> $damage
492
	 * @return array<string, int|bool>
493
	 */
494
	public function takeDamage(array $damage): array {
495
		$alreadyDead = !$this->exists();
496
		$minesDamage = 0;
497
		$cdDamage = 0;
498
		$sdDamage = 0;
499
		if (!$alreadyDead) {
500
			$minesDamage = $this->takeDamageToMines($damage['Armour']);
0 ignored issues
show
It seems like $damage['Armour'] can also be of type boolean; however, parameter $damage of SmrForce::takeDamageToMines() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

500
			$minesDamage = $this->takeDamageToMines(/** @scrutinizer ignore-type */ $damage['Armour']);
Loading history...
501
			if (!$this->hasMines() && ($minesDamage == 0 || $damage['Rollover'])) {
502
				$cdMaxDamage = $damage['Armour'] - $minesDamage;
503
				$cdDamage = $this->takeDamageToCDs($cdMaxDamage);
504
				if (!$this->hasCDs() && ($cdDamage == 0 || $damage['Rollover'])) {
505
					$sdMaxDamage = $damage['Armour'] - $minesDamage - $cdDamage;
506
					$sdDamage = $this->takeDamageToSDs($sdMaxDamage);
507
				}
508
			}
509
		}
510
		$return = [
511
						'KillingShot' => !$alreadyDead && !$this->exists(),
512
						'TargetAlreadyDead' => $alreadyDead,
513
						'Mines' => $minesDamage,
514
						'NumMines' => $minesDamage / MINE_ARMOUR,
515
						'HasMines' => $this->hasMines(),
516
						'CDs' => $cdDamage,
517
						'NumCDs' => $cdDamage / CD_ARMOUR,
518
						'HasCDs' => $this->hasCDs(),
519
						'SDs' => $sdDamage,
520
						'NumSDs' => $sdDamage / SD_ARMOUR,
521
						'HasSDs' => $this->hasSDs(),
522
						'TotalDamage' => $minesDamage + $cdDamage + $sdDamage,
523
		];
524
		return $return;
525
	}
526
527
	protected function takeDamageToMines(int $damage): int {
528
		$actualDamage = min($this->getMines(), IFloor($damage / MINE_ARMOUR));
529
		$this->takeMines($actualDamage);
530
		return $actualDamage * MINE_ARMOUR;
531
	}
532
533
	protected function takeDamageToCDs(int $damage): int {
534
		$actualDamage = min($this->getCDs(), IFloor($damage / CD_ARMOUR));
535
		$this->takeCDs($actualDamage);
536
		return $actualDamage * CD_ARMOUR;
537
	}
538
539
	protected function takeDamageToSDs(int $damage): int {
540
		$actualDamage = min($this->getSDs(), IFloor($damage / SD_ARMOUR));
541
		$this->takeSDs($actualDamage);
542
		return $actualDamage * SD_ARMOUR;
543
	}
544
545
	/**
546
	 * @return array<string, mixed>
547
	 */
548
	public function killForcesByPlayer(AbstractSmrPlayer $killer): array {
549
		return [];
550
	}
551
552
}
553