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

Issues (412)

src/lib/Default/smr.inc.php (3 issues)

1
<?php declare(strict_types=1);
2
3
use Smr\Chess\ChessGame;
4
use Smr\Container\DiContainer;
5
use Smr\Database;
6
use Smr\Epoch;
7
use Smr\Exceptions\UserError;
8
use Smr\Messages;
9
use Smr\Page\Page;
10
use Smr\Pages\Account\AlbumEdit;
11
use Smr\Pages\Account\BugReport;
12
use Smr\Pages\Account\ChangelogView;
13
use Smr\Pages\Account\ChatJoin;
14
use Smr\Pages\Account\ContactForm;
15
use Smr\Pages\Account\Donation;
16
use Smr\Pages\Account\ErrorDisplay;
17
use Smr\Pages\Account\GameLeaveProcessor;
18
use Smr\Pages\Account\GamePlay;
19
use Smr\Pages\Account\HallOfFameAll;
20
use Smr\Pages\Account\LogoffProcessor;
21
use Smr\Pages\Account\Preferences;
22
use Smr\Pages\Admin\AdminTools;
23
use Smr\Pages\Player\AllianceInviteAcceptProcessor;
24
use Smr\Pages\Player\AllianceMotd;
25
use Smr\Pages\Player\AllianceRoster;
26
use Smr\Pages\Player\CargoDump;
27
use Smr\Pages\Player\CombatLogList;
28
use Smr\Pages\Player\CombatLogViewerVerifyProcessor;
29
use Smr\Pages\Player\CurrentSector;
30
use Smr\Pages\Player\DeathProcessor;
31
use Smr\Pages\Player\ForcesDrop;
32
use Smr\Pages\Player\ForcesDropProcessor;
33
use Smr\Pages\Player\ForcesList;
34
use Smr\Pages\Player\GalacticPost\CurrentEditionProcessor;
35
use Smr\Pages\Player\HardwareConfigure;
36
use Smr\Pages\Player\MessageView;
37
use Smr\Pages\Player\NewbieWarningProcessor;
38
use Smr\Pages\Player\NewsReadCurrent;
39
use Smr\Pages\Player\Rankings\PlayerExperience;
40
use Smr\Pages\Player\SearchForTrader;
41
use Smr\Pages\Player\SearchForTraderResult;
42
use Smr\Pages\Player\WeaponReorder;
43
use Smr\Race;
44
use Smr\SectorLock;
45
use Smr\Session;
46
use Smr\Template;
47
use Smr\VoteLink;
48
use Smr\VoteSite;
49
50
function parseBoolean(mixed $check): bool {
51
	// Only negative strings are not implicitly converted to the correct bool
52
	if (is_string($check) && (strcasecmp($check, 'NO') == 0 || strcasecmp($check, 'FALSE') == 0)) {
53
		return false;
54
	}
55
	return (bool)$check;
56
}
57
58
function linkCombatLog(int $logID): string {
59
	$container = new CombatLogViewerVerifyProcessor($logID);
60
	return '<a href="' . $container->href() . '"><img src="images/notify.gif" width="14" height="11" border="0" title="View the combat log" /></a>';
61
}
62
63
/**
64
 * Converts a BBCode tag into some other text depending on the tag and value.
65
 * This is called in two stages: first with action BBCODE_CHECK (where the
66
 * returned value must be a boolean) and second, if the first check passes,
67
 * with action BBCODE_OUTPUT.
68
 *
69
 * @param array<string, string> $tagParams
70
 */
71
function smrBBCode(\Nbbc\BBCode $bbParser, int $action, string $tagName, string $default, array $tagParams, string $tagContent): bool|string {
0 ignored issues
show
The parameter $bbParser is not used and could be removed. ( Ignorable by Annotation )

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

71
function smrBBCode(/** @scrutinizer ignore-unused */ \Nbbc\BBCode $bbParser, int $action, string $tagName, string $default, array $tagParams, string $tagContent): bool|string {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
72
	global $overrideGameID, $disableBBLinks;
73
	$session = Session::getInstance();
74
	try {
75
		switch ($tagName) {
76
			case 'combatlog':
77
				if ($action == \Nbbc\BBCode::BBCODE_CHECK) {
78
					return is_numeric($default);
79
				}
80
				$logID = (int)$default;
81
				return linkCombatLog($logID);
82
83
			case 'player':
84
				if ($action == \Nbbc\BBCode::BBCODE_CHECK) {
85
					return is_numeric($default);
86
				}
87
				$playerID = (int)$default;
88
				$bbPlayer = SmrPlayer::getPlayerByPlayerID($playerID, $overrideGameID);
89
				$showAlliance = isset($tagParams['showalliance']) ? parseBoolean($tagParams['showalliance']) : false;
90
				if ($disableBBLinks === false && $overrideGameID == $session->getGameID()) {
91
					return $bbPlayer->getLinkedDisplayName($showAlliance);
92
				}
93
				return $bbPlayer->getDisplayName($showAlliance);
94
95
			case 'alliance':
96
				if ($action == \Nbbc\BBCode::BBCODE_CHECK) {
97
					return is_numeric($default);
98
				}
99
				$allianceID = (int)$default;
100
				$alliance = SmrAlliance::getAlliance($allianceID, $overrideGameID);
101
				if ($disableBBLinks === false && $overrideGameID == $session->getGameID()) {
102
					if ($session->hasGame() && $alliance->getAllianceID() == $session->getPlayer()->getAllianceID()) {
103
						$container = new AllianceMotd($alliance->getAllianceID());
104
					} else {
105
						$container = new AllianceRoster($alliance->getAllianceID());
106
					}
107
					return create_link($container, $alliance->getAllianceDisplayName());
108
				}
109
				return $alliance->getAllianceDisplayName();
110
111
			case 'race':
112
				$raceNameID = $default;
113
				foreach (Race::getAllNames() as $raceID => $raceName) {
114
					if ((is_numeric($raceNameID) && $raceNameID == $raceID)
115
						|| $raceNameID == $raceName) {
116
						if ($action == \Nbbc\BBCode::BBCODE_CHECK) {
117
							return true;
118
						}
119
						$linked = $disableBBLinks === false && $overrideGameID == $session->getGameID();
120
						$player = $session->hasGame() ? $session->getPlayer() : null;
121
						return AbstractSmrPlayer::getColouredRaceNameOrDefault($raceID, $player, $linked);
122
					}
123
				}
124
				break;
125
126
			case 'servertimetouser':
127
				if ($action == \Nbbc\BBCode::BBCODE_CHECK) {
128
					return true;
129
				}
130
				$time = strtotime($default);
131
				if ($time !== false) {
132
					$time += $session->getAccount()->getOffset() * 3600;
133
					return date($session->getAccount()->getDateTimeFormat(), $time);
134
				}
135
				break;
136
137
			case 'chess':
138
				if ($action == \Nbbc\BBCode::BBCODE_CHECK) {
139
					return is_numeric($default);
140
				}
141
				$chessGameID = (int)$default;
142
				$chessGame = ChessGame::getChessGame($chessGameID);
143
				return '<a href="' . $chessGame->getPlayGameHREF() . '">chess game (' . $chessGame->getChessGameID() . ')</a>';
144
145
			case 'sector':
146
				if ($action == \Nbbc\BBCode::BBCODE_CHECK) {
147
					return is_numeric($default);
148
				}
149
150
				$sectorID = (int)$default;
151
				$sectorTag = '<span class="sectorColour">#' . $sectorID . '</span>';
152
153
				if ($disableBBLinks === false
154
					&& $session->hasGame()
155
					&& $session->getGameID() == $overrideGameID
156
					&& SmrSector::sectorExists($overrideGameID, $sectorID)) {
157
					return '<a href="' . Globals::getPlotCourseHREF($session->getPlayer()->getSectorID(), $sectorID) . '">' . $sectorTag . '</a>';
158
				}
159
				return $sectorTag;
160
161
			case 'join_alliance':
162
				if ($action == \Nbbc\BBCode::BBCODE_CHECK) {
163
					return is_numeric($default);
164
				}
165
				$allianceID = (int)$default;
166
				$alliance = SmrAlliance::getAlliance($allianceID, $overrideGameID);
167
				$container = new AllianceInviteAcceptProcessor($allianceID);
168
				return '<div class="buttonA"><a class="buttonA" href="' . $container->href() . '">Join ' . $alliance->getAllianceDisplayName() . '</a></div>';
169
		}
170
	} catch (Throwable) {
171
		// If there's an error, we will silently display the original text
172
	}
173
	if ($action == \Nbbc\BBCode::BBCODE_CHECK) {
174
		return false;
175
	}
176
	return htmlspecialchars($tagParams['_tag']) . $tagContent . htmlspecialchars($tagParams['_endtag']);
177
}
178
179
function inify(string $text): string {
180
	return str_replace(',', '', html_entity_decode($text));
181
}
182
183
function bbifyMessage(string $message, int $gameID = null, bool $noLinks = false): string {
184
	static $bbParser;
185
	if (!isset($bbParser)) {
186
		$bbParser = new \Nbbc\BBCode();
187
		$bbParser->setEnableSmileys(false);
188
		$bbParser->removeRule('wiki');
189
		$bbParser->removeRule('img');
190
		$bbParser->setURLTarget('_blank');
191
		$bbParser->setURLTargetable('override');
192
		$bbParser->setEscapeContent(false); // don't escape HTML, needed for News etc.
193
		$smrRule = [
194
				'mode' => \Nbbc\BBCode::BBCODE_MODE_CALLBACK,
195
				'method' => 'smrBBCode',
196
				'class' => 'link',
197
				'allow_in' => ['listitem', 'block', 'columns', 'inline'],
198
				'end_tag' => \Nbbc\BBCode::BBCODE_PROHIBIT,
199
				'content' => \Nbbc\BBCode::BBCODE_PROHIBIT,
200
			];
201
		$bbParser->addRule('combatlog', $smrRule);
202
		$bbParser->addRule('player', $smrRule);
203
		$bbParser->addRule('alliance', $smrRule);
204
		$bbParser->addRule('race', $smrRule);
205
		$bbParser->addRule('servertimetouser', $smrRule);
206
		$bbParser->addRule('chess', $smrRule);
207
		$bbParser->addRule('sector', $smrRule);
208
		$bbParser->addRule('join_alliance', $smrRule);
209
	}
210
211
	global $overrideGameID;
212
	if ($gameID === null) {
213
		$overrideGameID = Session::getInstance()->getGameID();
214
	} else {
215
		$overrideGameID = $gameID;
216
	}
217
218
	global $disableBBLinks;
219
	$disableBBLinks = $noLinks;
220
221
	if (str_contains($message, '[')) { //We have BBCode so let's do a full parse.
222
		$message = $bbParser->parse($message);
223
	} else { //Otherwise just convert newlines
224
		$message = nl2br($message, true);
225
	}
226
	return $message;
227
}
228
229
function create_error(string $message): never {
230
	throw new UserError($message);
231
}
232
233
function handleUserError(string $message): never {
234
	if ($_SERVER['SCRIPT_NAME'] !== LOADER_URI) {
235
		header('Location: /error.php?msg=' . urlencode($message));
236
		exit;
237
	}
238
239
	// If we're throwing an error, we don't care what data was stored in the
240
	// Template from the original page.
241
	DiContainer::getContainer()->reset(Template::class);
242
243
	$session = Session::getInstance();
244
	if ($session->hasGame()) {
245
		$errorMsg = '<span class="red bold">ERROR: </span>' . $message;
246
		$container = new CurrentSector(errorMessage: $errorMsg);
247
	} else {
248
		$container = new ErrorDisplay(message: $message);
249
	}
250
251
	if ($session->ajax) {
252
		// To avoid the page just not refreshing when an error is encountered
253
		// during ajax updates, use javascript to auto-redirect to the
254
		// appropriate error page.
255
		$errorHREF = $container->href();
256
		// json_encode the HREF as a safety precaution
257
		$template = Template::getInstance();
258
		$template->addJavascriptForAjax('EVAL', 'location.href = ' . json_encode($errorHREF));
259
	}
260
	$container->go();
261
}
262
263
function create_link(Page|string $container, string $text, string $class = null): string {
264
	return '<a' . ($class === null ? '' : ' class="' . $class . '"') . ' href="' . (is_string($container) ? $container : $container->href()) . '">' . $text . '</a>';
265
}
266
267
function create_submit_link(Page $container, string $text): string {
268
	return '<a href="' . $container->href() . '" class="submitStyle">' . $text . '</a>';
269
}
270
271
function get_colored_text_range(float $value, float $maxValue, string $text = null, float $minValue = 0): string {
272
	if ($text === null) {
273
		$text = number_format($value);
274
	}
275
	if ($maxValue - $minValue == 0) {
276
		return $text;
277
	}
278
	$normalisedValue = IRound(510 * max(0, min($maxValue, $value) - $minValue) / ($maxValue - $minValue)) - 255;
0 ignored issues
show
The function IRound was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

278
	$normalisedValue = /** @scrutinizer ignore-call */ IRound(510 * max(0, min($maxValue, $value) - $minValue) / ($maxValue - $minValue)) - 255;
Loading history...
279
	if ($normalisedValue < 0) {
280
		$r_component = 'ff';
281
		$g_component = dechex(255 + $normalisedValue);
282
		if (strlen($g_component) == 1) {
283
			$g_component = '0' . $g_component;
284
		}
285
	} elseif ($normalisedValue > 0) {
286
		$g_component = 'ff';
287
		$r_component = dechex(255 - $normalisedValue);
288
		if (strlen($r_component) == 1) {
289
			$r_component = '0' . $r_component;
290
		}
291
	} else {
292
		$r_component = 'ff';
293
		$g_component = 'ff';
294
	}
295
	$colour = $r_component . $g_component . '00';
296
	return '<span style="color:#' . $colour . '">' . $text . '</span>';
297
}
298
299
function get_colored_text(float $value, string $text = null): string {
300
	return get_colored_text_range($value, 300, $text, -300);
301
}
302
303
function word_filter(string $string): string {
304
	static $words;
305
306
	if (!is_array($words)) {
307
		$db = Database::getInstance();
308
		$dbResult = $db->read('SELECT word_value, word_replacement FROM word_filter');
309
		$words = [];
310
		foreach ($dbResult->records() as $dbRecord) {
311
			$row = $dbRecord->getRow();
312
			$words[] = ['word_value' => '/' . str_replace('/', '\/', $row['word_value']) . '/i', 'word_replacement' => $row['word_replacement']];
313
		}
314
	}
315
316
	foreach ($words as $word) {
317
		$string = preg_replace($word['word_value'], $word['word_replacement'], $string);
318
	}
319
320
	return $string;
321
}
322
323
// choose correct pluralization based on amount
324
function pluralise(int|float $amount, string $word, bool $includeAmount = true): string {
325
	$result = $word;
326
	if ($amount != 1) {
327
		$result .= 's';
328
	}
329
	if ($includeAmount) {
330
		$result = $amount . ' ' . $result;
331
	}
332
	return $result;
333
}
334
335
/**
336
 * This function is a hack around the old style http forward mechanism.
337
 * It is also responsible for setting most of the global variables
338
 * (see loader.php for the initialization of the globals).
339
 */
340
function do_voodoo(): never {
341
	$session = Session::getInstance();
342
	$var = $session->getCurrentVar();
343
344
	// create account object
345
	$account = $session->getAccount();
346
347
	if ($session->hasGame()) {
348
		// Get the nominal player information (this may change after locking).
349
		// We don't force a reload here in case we don't need to lock.
350
		$player = $session->getPlayer();
351
		$sectorID = $player->getSectorID();
352
353
		if (!$session->ajax //AJAX should never do anything that requires a lock.
354
			//&& ($var->file == 'current_sector.php' || $var->file == 'map_local.php') //Neither should CS or LM and they gets loaded a lot so should reduce lag issues with big groups.
355
		) {
356
			// We skip locking if we've already failed to display error page
357
			$lock = SectorLock::getInstance();
358
			if (!$lock->hasFailed() && $lock->acquireForPlayer($player)) {
359
				// Reload var info in case it changed between grabbing lock.
360
				$session->fetchVarInfo();
361
				if ($session->hasCurrentVar() === false) {
362
					if (ENABLE_DEBUG) {
363
						$db = Database::getInstance();
364
						$db->insert('debug', [
365
							'debug_type' => $db->escapeString('SPAM'),
366
							'account_id' => $db->escapeNumber($account->getAccountID()),
367
							'value' => 0,
368
							'value_2' => 0,
369
						]);
370
					}
371
					throw new UserError('Please do not spam click!');
372
				}
373
				$var = $session->getCurrentVar();
374
375
				// Reload player now that we have a lock.
376
				$player = $session->getPlayer(true);
377
				if ($player->getSectorID() != $sectorID) {
378
					// Player sector changed after reloading! Release lock and try again.
379
					$lock->release();
380
					do_voodoo();
381
				}
382
			}
383
		}
384
385
		// update turns on that player
386
		$player->updateTurns();
387
388
		// Check if we need to redirect to a different page
389
		if (!$var->skipRedirect && !$session->ajax) {
390
			if ($player->getGame()->hasEnded()) {
391
				(new GameLeaveProcessor(new GamePlay(errorMessage: 'The game has ended.')))->go();
392
			}
393
			if ($player->isDead()) {
394
				(new DeathProcessor())->go();
395
			}
396
			if ($player->getNewbieWarning() && $player->getNewbieTurns() <= NEWBIE_TURNS_WARNING_LIMIT) {
397
				(new NewbieWarningProcessor())->go();
398
			}
399
		}
400
	}
401
402
	// Execute the engine files.
403
	// This is where the majority of the page-specific work is performed.
404
	$var->process();
405
406
	// Populate the template
407
	$template = Template::getInstance();
408
	if (isset($player)) {
409
		$template->assign('UnderAttack', $var->showUnderAttack($player, $session->ajax));
410
	}
411
412
	//Nothing below this point should require the lock.
413
	saveAllAndReleaseLock();
414
415
	$template->assign('TemplateBody', $var->file);
416
	if (isset($player)) {
417
		$template->assign('ThisSector', $player->getSector());
418
		$template->assign('ThisPlayer', $player);
419
		$template->assign('ThisShip', $player->getShip());
420
	}
421
	$template->assign('ThisAccount', $account);
422
	if ($account->getCssLink() != null) {
423
		$template->assign('ExtraCSSLink', $account->getCssLink());
424
	}
425
	doSkeletonAssigns($template);
426
427
	// Set ajax refresh time
428
	$ajaxRefresh = $account->isUseAJAX();
429
	if ($ajaxRefresh) {
430
		// If we can refresh, specify the refresh interval in millisecs
431
		if (isset($player) && $player->canFight()) {
432
			$ajaxRefresh = AJAX_UNPROTECTED_REFRESH_TIME;
433
		} else {
434
			$ajaxRefresh = AJAX_DEFAULT_REFRESH_TIME;
435
		}
436
	}
437
	$template->assign('AJAX_ENABLE_REFRESH', $ajaxRefresh);
438
439
	$template->display('skeleton.php', $session->ajax);
440
441
	$session->update();
442
443
	exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
444
}
445
446
function saveAllAndReleaseLock(bool $updateSession = true): void {
447
	// Only save if we have a lock.
448
	$lock = SectorLock::getInstance();
449
	if ($lock->isActive()) {
450
		SmrSector::saveSectors();
451
		SmrShip::saveShips();
452
		SmrPlayer::savePlayers();
453
		// Skip any caching classes that haven't even been loaded yet
454
		if (class_exists(SmrForce::class, false)) {
455
			SmrForce::saveForces();
456
		}
457
		if (class_exists(SmrPort::class, false)) {
458
			SmrPort::savePorts();
459
		}
460
		if (class_exists(SmrPlanet::class, false)) {
461
			SmrPlanet::savePlanets();
462
		}
463
		if (class_exists(WeightedRandom::class, false)) {
464
			WeightedRandom::saveWeightedRandoms();
465
		}
466
		if ($updateSession) {
467
			//Update session here to make sure current page $var is up to date before releasing lock.
468
			Session::getInstance()->update();
469
		}
470
		$lock->release();
471
	}
472
}
473
474
function doTickerAssigns(Template $template, AbstractSmrPlayer $player, Database $db): void {
475
	//any ticker news?
476
	if ($player->hasTickers()) {
477
		$ticker = [];
478
		$max = Epoch::time() - 60;
479
		$dateFormat = $player->getAccount()->getDateTimeFormat();
480
		if ($player->hasTicker('NEWS')) {
481
			//get recent news (5 mins)
482
			$dbResult = $db->read('SELECT time,news_message FROM news WHERE game_id = ' . $db->escapeNumber($player->getGameID()) . ' AND time >= ' . $max . ' ORDER BY time DESC LIMIT 4');
483
			foreach ($dbResult->records() as $dbRecord) {
484
				$ticker[] = [
485
					'Time' => date($dateFormat, $dbRecord->getInt('time')),
486
					'Message' => $dbRecord->getString('news_message'),
487
				];
488
			}
489
		}
490
		if ($player->hasTicker('SCOUT')) {
491
			$dbResult = $db->read('SELECT message_text,send_time FROM message
492
						WHERE account_id=' . $db->escapeNumber($player->getAccountID()) . '
493
						AND game_id=' . $db->escapeNumber($player->getGameID()) . '
494
						AND message_type_id=' . $db->escapeNumber(MSG_SCOUT) . '
495
						AND send_time>=' . $db->escapeNumber($max) . '
496
						AND sender_id NOT IN (SELECT account_id FROM player_has_ticker WHERE type=' . $db->escapeString('BLOCK') . ' AND expires > ' . $db->escapeNumber(Epoch::time()) . ' AND game_id = ' . $db->escapeNumber($player->getGameID()) . ') AND receiver_delete = \'FALSE\'
497
						ORDER BY send_time DESC
498
						LIMIT 4');
499
			foreach ($dbResult->records() as $dbRecord) {
500
				$ticker[] = [
501
					'Time' => date($dateFormat, $dbRecord->getInt('send_time')),
502
					'Message' => $dbRecord->getString('message_text'),
503
				];
504
			}
505
		}
506
		$template->assign('Ticker', $ticker);
507
	}
508
}
509
510
function doSkeletonAssigns(Template $template): void {
511
	$session = Session::getInstance();
512
	$account = $session->getAccount();
513
	$db = Database::getInstance();
514
515
	$template->assign('CSSLink', $account->getCssUrl());
516
	$template->assign('CSSColourLink', $account->getCssColourUrl());
517
518
	$template->assign('FontSize', $account->getFontSize() - 20);
519
	$template->assign('timeDisplay', date($account->getDateTimeFormatSplit(), Epoch::time()));
520
521
	$container = new HallOfFameAll();
522
	$template->assign('HallOfFameLink', $container->href());
523
524
	$template->assign('AccountID', $account->getAccountID());
525
	$template->assign('PlayGameLink', (new GameLeaveProcessor(new GamePlay()))->href());
526
527
	$template->assign('LogoutLink', (new LogoffProcessor())->href());
528
529
	$container = new GameLeaveProcessor(new AdminTools());
530
	$template->assign('AdminToolsLink', $container->href());
531
532
	$container = new Preferences();
533
	$template->assign('PreferencesLink', $container->href());
534
535
	$container = new AlbumEdit();
536
	$template->assign('EditPhotoLink', $container->href());
537
538
	$container = new BugReport();
539
	$template->assign('ReportABugLink', $container->href());
540
541
	$container = new ContactForm();
542
	$template->assign('ContactFormLink', $container->href());
543
544
	$container = new ChatJoin();
545
	$template->assign('IRCLink', $container->href());
546
547
	$container = new Donation();
548
	$template->assign('DonateLink', $container->href());
549
550
	if ($session->hasGame()) {
551
		$player = $session->getPlayer();
552
		$template->assign('GameName', SmrGame::getGame($session->getGameID())->getName());
553
		$template->assign('GameID', $session->getGameID());
554
555
		$template->assign('PlotCourseLink', Globals::getPlotCourseHREF());
556
557
		$template->assign('TraderLink', Globals::getTraderStatusHREF());
558
559
		$template->assign('PoliticsLink', Globals::getCouncilHREF($player->getRaceID()));
560
561
		$container = new CombatLogList();
562
		$template->assign('CombatLogsLink', $container->href());
563
564
		$template->assign('PlanetLink', Globals::getPlanetListHREF($player->getAllianceID()));
565
566
		$container = new ForcesList();
567
		$template->assign('ForcesLink', $container->href());
568
569
		$template->assign('MessagesLink', Globals::getViewMessageBoxesHREF());
570
571
		$container = new NewsReadCurrent();
572
		$template->assign('ReadNewsLink', $container->href());
573
574
		$container = new CurrentEditionProcessor();
575
		$template->assign('GalacticPostLink', $container->href());
576
577
		$container = new SearchForTrader();
578
		$template->assign('SearchForTraderLink', $container->href());
579
580
		$container = new PlayerExperience();
581
		$template->assign('RankingsLink', $container->href());
582
583
		$container = new HallOfFameAll($player->getGameID());
584
		$template->assign('CurrentHallOfFameLink', $container->href());
585
586
		$unreadMessages = [];
587
		$dbResult = $db->read('SELECT message_type_id,COUNT(*) FROM player_has_unread_messages WHERE ' . $player->getSQL() . ' GROUP BY message_type_id');
588
		foreach ($dbResult->records() as $dbRecord) {
589
			$messageTypeID = $dbRecord->getInt('message_type_id');
590
			$container = new MessageView($messageTypeID);
591
			$unreadMessages[] = [
592
				'href' => $container->href(),
593
				'num' => $dbRecord->getInt('COUNT(*)'),
594
				'alt' => Messages::getMessageTypeNames($messageTypeID),
595
				'img' => Messages::getMessageTypeImage($messageTypeID),
596
			];
597
		}
598
		$template->assign('UnreadMessages', $unreadMessages);
599
600
		$container = new SearchForTraderResult($player->getPlayerID());
601
		$template->assign('PlayerNameLink', $container->href());
602
603
		if (is_array(Globals::getHiddenPlayers()) && in_array($player->getAccountID(), Globals::getHiddenPlayers())) {
604
			$template->assign('PlayerInvisible', true);
605
		}
606
607
		// ******* Hardware *******
608
		$container = new HardwareConfigure();
609
		$template->assign('HardwareLink', $container->href());
610
611
		// ******* Forces *******
612
		$template->assign('ForceDropLink', (new ForcesDrop())->href());
613
614
		$ship = $player->getShip();
615
		$var = Session::getInstance()->getCurrentVar();
616
		if ($ship->hasMines()) {
617
			$container = new ForcesDropProcessor($player->getAccountID(), referrer: $var::class, dropMines: 1);
618
			$template->assign('DropMineLink', $container->href());
619
		}
620
		if ($ship->hasCDs()) {
621
			$container = new ForcesDropProcessor($player->getAccountID(), referrer: $var::class, dropCDs: 1);
622
			$template->assign('DropCDLink', $container->href());
623
		}
624
		if ($ship->hasSDs()) {
625
			$container = new ForcesDropProcessor($player->getAccountID(), referrer: $var::class, dropSDs: 1);
626
			$template->assign('DropSDLink', $container->href());
627
		}
628
629
		$template->assign('CargoJettisonLink', (new CargoDump())->href());
630
631
		$template->assign('WeaponReorderLink', (new WeaponReorder())->href());
632
633
	}
634
635
	// ------- VOTING --------
636
	$voteLinks = [];
637
	foreach (VoteSite::cases() as $site) {
638
		$link = new VoteLink($site, $account->getAccountID(), $session->getGameID());
639
		$voteLinks[] = [
640
			'img' => $link->getImg(),
641
			'url' => $link->getUrl(),
642
			'sn' => $link->getSN(),
643
		];
644
	}
645
	$template->assign('VoteLinks', $voteLinks);
646
647
	// Determine the minimum time until the next vote across all sites
648
	$minVoteWait = VoteLink::getMinTimeUntilFreeTurns($account->getAccountID(), $session->getGameID());
649
	$template->assign('TimeToNextVote', in_time_or_now($minVoteWait, true));
650
651
	// ------- VERSION --------
652
	$dbResult = $db->read('SELECT * FROM version ORDER BY went_live DESC LIMIT 1');
653
	$version = '';
654
	if ($dbResult->hasRecord()) {
655
		$dbRecord = $dbResult->record();
656
		$container = new ChangelogView();
657
		$version = create_link($container, 'v' . $dbRecord->getInt('major_version') . '.' . $dbRecord->getInt('minor_version') . '.' . $dbRecord->getInt('patch_level'));
658
	}
659
660
	$template->assign('Version', $version);
661
	$template->assign('CurrentYear', date('Y', Epoch::time()));
662
}
663
664
/**
665
 * Convert an integer number of seconds into a human-readable time for
666
 * grammatically-correct use in a sentence, i.e. prefixed with "in" when
667
 * the amount is positive.
668
 */
669
function in_time_or_now(int $seconds, bool $short = false): string {
670
	$result = format_time($seconds, $short);
671
	if ($seconds > 0) {
672
		$result = 'in ' . $result;
673
	}
674
	return $result;
675
}
676
677
/**
678
 * Convert an integer number of seconds into a human-readable time.
679
 * Seconds are omitted to avoid frequent and disruptive ajax updates.
680
 * Use short=true to use 1-letter units (e.g. "1h and 3m").
681
 * If seconds is negative, will append "ago" to the result.
682
 * If seconds is zero, will return only "now".
683
 * If seconds is <60, will prefix "less than" or "<" (HTML-safe).
684
 */
685
function format_time(int $seconds, bool $short = false): string {
686
	if ($seconds == 0) {
687
		return 'now';
688
	}
689
690
	if ($seconds < 0) {
691
		$past = true;
692
		$seconds = abs($seconds);
693
	} else {
694
		$past = false;
695
	}
696
697
	$minutes = ceil($seconds / 60);
698
	$hours = 0;
699
	$days = 0;
700
	$weeks = 0;
701
	if ($minutes >= 60) {
702
		$hours = floor($minutes / 60);
703
		$minutes = $minutes % 60;
704
	}
705
	if ($hours >= 24) {
706
		$days = floor($hours / 24);
707
		$hours = $hours % 24;
708
	}
709
	if ($days >= 7) {
710
		$weeks = floor($days / 7);
711
		$days = $days % 7;
712
	}
713
	$times = [
714
		'week' => $weeks,
715
		'day' => $days,
716
		'hour' => $hours,
717
		'minute' => $minutes,
718
	];
719
	$parts = [];
720
	foreach ($times as $unit => $amount) {
721
		if ($amount > 0) {
722
			if ($short) {
723
				$parts[] = $amount . $unit[0];
724
			} else {
725
				$parts[] = pluralise($amount, $unit);
726
			}
727
		}
728
	}
729
730
	if (count($parts) == 1) {
731
		$result = $parts[0];
732
	} else {
733
		// e.g. 5h, 10m and 30s
734
		$result = implode(', ', array_slice($parts, 0, -1)) . ' and ' . end($parts);
735
	}
736
737
	if ($seconds < 60) {
738
		$result = ($short ? '&lt;' : 'less than ') . $result;
739
	}
740
741
	if ($past) {
742
		$result .= ' ago';
743
	}
744
	return $result;
745
}
746
747
function number_colour_format(float $number, bool $justSign = false): string {
748
	$formatted = '<span';
749
	if ($number > 0) {
750
		$formatted .= ' class="green">+';
751
	} elseif ($number < 0) {
752
		$formatted .= ' class="red">-';
753
	} else {
754
		$formatted .= '>';
755
	}
756
	if ($justSign === false) {
757
		$decimalPlaces = 0;
758
		$pos = strpos((string)$number, '.');
759
		if ($pos !== false) {
760
			$decimalPlaces = strlen(substr((string)$number, $pos + 1));
761
		}
762
		$formatted .= number_format(abs($number), $decimalPlaces);
763
	}
764
	$formatted .= '</span>';
765
	return $formatted;
766
}
767
768
769
/**
770
 * Randomly choose an array key weighted by the array values.
771
 * Probabilities are relative to the total weight. For example:
772
 *
773
 * array(
774
 *    'A' => 1, // 10% chance
775
 *    'B' => 3, // 30% chance
776
 *    'C' => 6, // 60% chance
777
 * );
778
 *
779
 * @template T of array-key
780
 * @param array<T, float> $choices
781
 * @return T
782
 */
783
function getWeightedRandom(array $choices): string|int {
784
	// Normalize the weights so that their sum is 1
785
	$sumWeight = array_sum($choices);
786
	foreach ($choices as $key => $weight) {
787
		$choices[$key] = $weight / $sumWeight;
788
	}
789
790
	// Generate a random number between 0 and 1
791
	$rand = rand() / getrandmax();
792
793
	// Subtract weights from the random number until it is negative,
794
	// then return the key associated with that weight.
795
	foreach ($choices as $key => $weight) {
796
		$rand -= $weight;
797
		if ($rand <= 0) {
798
			return $key;
799
		}
800
	}
801
	throw new Exception('Internal error computing weights');
802
}
803