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
Pull Request — main (#1446)
by Dan
06:16
created

handleUserError()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 16
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 29
rs 9.7333
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\Race;
10
use Smr\SectorLock;
11
use Smr\Session;
12
use Smr\Template;
13
use Smr\VoteLink;
14
use Smr\VoteSite;
15
16
function parseBoolean(mixed $check): bool {
17
	// Only negative strings are not implicitly converted to the correct bool
18
	if (is_string($check) && (strcasecmp($check, 'NO') == 0 || strcasecmp($check, 'FALSE') == 0)) {
19
		return false;
20
	}
21
	return (bool)$check;
22
}
23
24
function linkCombatLog(int $logID): string {
25
	$container = Page::create('combat_log_viewer_verify.php');
26
	$container['log_id'] = $logID;
27
	return '<a href="' . $container->href() . '"><img src="images/notify.gif" width="14" height="11" border="0" title="View the combat log" /></a>';
28
}
29
30
/**
31
 * Converts a BBCode tag into some other text depending on the tag and value.
32
 * This is called in two stages: first with action BBCODE_CHECK (where the
33
 * returned value must be a boolean) and second, if the first check passes,
34
 * with action BBCODE_OUTPUT.
35
 *
36
 * @param array<string, string> $tagParams
37
 */
38
function smrBBCode(\Nbbc\BBCode $bbParser, int $action, string $tagName, string $default, array $tagParams, string $tagContent): bool|string {
0 ignored issues
show
Unused Code introduced by
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

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

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

241
	$normalisedValue = /** @scrutinizer ignore-call */ IRound(510 * max(0, min($maxValue, $value) - $minValue) / ($maxValue - $minValue)) - 255;
Loading history...
242
	if ($normalisedValue < 0) {
243
		$r_component = 'ff';
244
		$g_component = dechex(255 + $normalisedValue);
245
		if (strlen($g_component) == 1) {
246
			$g_component = '0' . $g_component;
247
		}
248
	} elseif ($normalisedValue > 0) {
249
		$g_component = 'ff';
250
		$r_component = dechex(255 - $normalisedValue);
251
		if (strlen($r_component) == 1) {
252
			$r_component = '0' . $r_component;
253
		}
254
	} else {
255
		$r_component = 'ff';
256
		$g_component = 'ff';
257
	}
258
	$colour = $r_component . $g_component . '00';
259
	return '<span style="color:#' . $colour . '">' . $text . '</span>';
260
}
261
262
function get_colored_text(float $value, string $text = null): string {
263
	return get_colored_text_range($value, 300, $text, -300);
264
}
265
266
function word_filter(string $string): string {
267
	static $words;
268
269
	if (!is_array($words)) {
270
		$db = Database::getInstance();
271
		$dbResult = $db->read('SELECT word_value, word_replacement FROM word_filter');
272
		$words = [];
273
		foreach ($dbResult->records() as $dbRecord) {
274
			$row = $dbRecord->getRow();
275
			$words[] = ['word_value' => '/' . str_replace('/', '\/', $row['word_value']) . '/i', 'word_replacement' => $row['word_replacement']];
276
		}
277
	}
278
279
	foreach ($words as $word) {
280
		$string = preg_replace($word['word_value'], $word['word_replacement'], $string);
281
	}
282
283
	return $string;
284
}
285
286
// choose correct pluralization based on amount
287
function pluralise(int|float $amount, string $word, bool $includeAmount = true): string {
288
	$result = $word;
289
	if ($amount != 1) {
290
		$result .= 's';
291
	}
292
	if ($includeAmount) {
293
		$result = $amount . ' ' . $result;
294
	}
295
	return $result;
296
}
297
298
/**
299
 * This function is a hack around the old style http forward mechanism.
300
 * It is also responsible for setting most of the global variables
301
 * (see loader.php for the initialization of the globals).
302
 */
303
function do_voodoo(): never {
304
	$session = Session::getInstance();
305
	$var = $session->getCurrentVar();
306
307
	if (!defined('AJAX_CONTAINER')) {
308
		define('AJAX_CONTAINER', isset($var['AJAX']) && $var['AJAX'] === true);
309
	}
310
311
	if (!AJAX_CONTAINER && USING_AJAX && $session->hasChangedSN()) {
312
		exit;
0 ignored issues
show
Best Practice introduced by
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...
313
	}
314
	//ob_clean();
315
316
	// create account object
317
	$account = $session->getAccount();
318
319
	if ($session->hasGame()) {
320
		// Get the nominal player information (this may change after locking).
321
		// We don't force a reload here in case we don't need to lock.
322
		$player = $session->getPlayer();
323
		$sectorID = $player->getSectorID();
324
325
		if (!USING_AJAX //AJAX should never do anything that requires a lock.
326
			//&& ($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.
327
		) {
328
			// We skip locking if we've already failed to display error page
329
			$lock = SectorLock::getInstance();
330
			if (!$lock->hasFailed() && $lock->acquireForPlayer($player)) {
331
				// Reload var info in case it changed between grabbing lock.
332
				$session->fetchVarInfo();
333
				if ($session->hasCurrentVar() === false) {
334
					if (ENABLE_DEBUG) {
335
						$db = Database::getInstance();
336
						$db->insert('debug', [
337
							'debug_type' => $db->escapeString('SPAM'),
338
							'account_id' => $db->escapeNumber($account->getAccountID()),
339
							'value' => 0,
340
							'value_2' => 0,
341
						]);
342
					}
343
					throw new UserError('Please do not spam click!');
344
				}
345
				$var = $session->getCurrentVar();
346
347
				// Reload player now that we have a lock.
348
				$player = $session->getPlayer(true);
349
				if ($player->getSectorID() != $sectorID) {
350
					// Player sector changed after reloading! Release lock and try again.
351
					$lock->release();
352
					do_voodoo();
353
				}
354
			}
355
		}
356
357
		// update turns on that player
358
		$player->updateTurns();
359
360
		// Check if we need to redirect to a different page
361
		if (!$var->skipRedirect && !USING_AJAX) {
362
			if ($player->getGame()->hasEnded()) {
363
				Page::create('game_leave_processing.php', ['forward_to' => 'game_play.php', 'errorMsg' => 'The game has ended.'], skipRedirect: true)->go();
364
			}
365
			if ($player->isDead()) {
366
				Page::create('death_processing.php', skipRedirect: true)->go();
367
			}
368
			if ($player->getNewbieWarning() && $player->getNewbieTurns() <= NEWBIE_TURNS_WARNING_LIMIT) {
369
				Page::create('newbie_warning_processing.php', skipRedirect: true)->go();
370
			}
371
		}
372
	}
373
374
	// Execute the engine files.
375
	// This is where the majority of the page-specific work is performed.
376
	$var->process();
377
378
	// Populate the template
379
	$template = Template::getInstance();
380
	if (isset($player)) {
381
		$template->assign('UnderAttack', $player->removeUnderAttack());
382
	}
383
384
	//Nothing below this point should require the lock.
385
	saveAllAndReleaseLock();
386
387
	$template->assign('TemplateBody', $var->file);
388
	if (isset($player)) {
389
		$template->assign('ThisSector', $player->getSector());
390
		$template->assign('ThisPlayer', $player);
391
		$template->assign('ThisShip', $player->getShip());
392
	}
393
	$template->assign('ThisAccount', $account);
394
	if ($account->getCssLink() != null) {
395
		$template->assign('ExtraCSSLink', $account->getCssLink());
396
	}
397
	doSkeletonAssigns($template);
398
399
	// Set ajax refresh time
400
	$ajaxRefresh = $account->isUseAJAX();
401
	if ($ajaxRefresh) {
402
		// If we can refresh, specify the refresh interval in millisecs
403
		if (isset($player) && $player->canFight()) {
404
			$ajaxRefresh = AJAX_UNPROTECTED_REFRESH_TIME;
405
		} else {
406
			$ajaxRefresh = AJAX_DEFAULT_REFRESH_TIME;
407
		}
408
	}
409
	$template->assign('AJAX_ENABLE_REFRESH', $ajaxRefresh);
410
411
	$template->display('skeleton.php', USING_AJAX || AJAX_CONTAINER);
412
413
	$session->update();
414
415
	exit;
0 ignored issues
show
Best Practice introduced by
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...
416
}
417
418
function saveAllAndReleaseLock(bool $updateSession = true): void {
419
	// Only save if we have a lock.
420
	$lock = SectorLock::getInstance();
421
	if ($lock->isActive()) {
422
		SmrSector::saveSectors();
423
		SmrShip::saveShips();
424
		SmrPlayer::savePlayers();
425
		// Skip any caching classes that haven't even been loaded yet
426
		if (class_exists(SmrForce::class, false)) {
427
			SmrForce::saveForces();
428
		}
429
		if (class_exists(SmrPort::class, false)) {
430
			SmrPort::savePorts();
431
		}
432
		if (class_exists(SmrPlanet::class, false)) {
433
			SmrPlanet::savePlanets();
434
		}
435
		if (class_exists(WeightedRandom::class, false)) {
436
			WeightedRandom::saveWeightedRandoms();
437
		}
438
		if ($updateSession) {
439
			//Update session here to make sure current page $var is up to date before releasing lock.
440
			Session::getInstance()->update();
441
		}
442
		$lock->release();
443
	}
444
}
445
446
function doTickerAssigns(Template $template, AbstractSmrPlayer $player, Database $db): void {
447
	//any ticker news?
448
	if ($player->hasTickers()) {
449
		$ticker = [];
450
		$max = Epoch::time() - 60;
451
		$dateFormat = $player->getAccount()->getDateTimeFormat();
452
		if ($player->hasTicker('NEWS')) {
453
			//get recent news (5 mins)
454
			$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');
455
			foreach ($dbResult->records() as $dbRecord) {
456
				$ticker[] = [
457
					'Time' => date($dateFormat, $dbRecord->getInt('time')),
458
					'Message' => $dbRecord->getString('news_message'),
459
				];
460
			}
461
		}
462
		if ($player->hasTicker('SCOUT')) {
463
			$dbResult = $db->read('SELECT message_text,send_time FROM message
464
						WHERE account_id=' . $db->escapeNumber($player->getAccountID()) . '
465
						AND game_id=' . $db->escapeNumber($player->getGameID()) . '
466
						AND message_type_id=' . $db->escapeNumber(MSG_SCOUT) . '
467
						AND send_time>=' . $db->escapeNumber($max) . '
468
						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\'
469
						ORDER BY send_time DESC
470
						LIMIT 4');
471
			foreach ($dbResult->records() as $dbRecord) {
472
				$ticker[] = [
473
					'Time' => date($dateFormat, $dbRecord->getInt('send_time')),
474
					'Message' => $dbRecord->getString('message_text'),
475
				];
476
			}
477
		}
478
		$template->assign('Ticker', $ticker);
479
	}
480
}
481
482
function doSkeletonAssigns(Template $template): void {
483
	$session = Session::getInstance();
484
	$account = $session->getAccount();
485
	$db = Database::getInstance();
486
487
	$template->assign('CSSLink', $account->getCssUrl());
488
	$template->assign('CSSColourLink', $account->getCssColourUrl());
489
490
	$template->assign('FontSize', $account->getFontSize() - 20);
491
	$template->assign('timeDisplay', date($account->getDateTimeFormatSplit(), Epoch::time()));
492
493
	$container = Page::create('hall_of_fame_new.php');
494
	$template->assign('HallOfFameLink', $container->href());
495
496
	$template->assign('AccountID', $account->getAccountID());
497
	$template->assign('PlayGameLink', Page::create('game_leave_processing.php', ['forward_to' => 'game_play.php'])->href());
498
499
	$template->assign('LogoutLink', Page::create('logoff.php')->href());
500
501
	$container = Page::create('game_leave_processing.php', ['forward_to' => 'admin/admin_tools.php']);
502
	$template->assign('AdminToolsLink', $container->href());
503
504
	$container = Page::create('preferences.php');
505
	$template->assign('PreferencesLink', $container->href());
506
507
	$container = Page::create('album_edit.php');
508
	$template->assign('EditPhotoLink', $container->href());
509
510
	$container = Page::create('bug_report.php');
511
	$template->assign('ReportABugLink', $container->href());
512
513
	$container = Page::create('contact.php');
514
	$template->assign('ContactFormLink', $container->href());
515
516
	$container = Page::create('chat_rules.php');
517
	$template->assign('IRCLink', $container->href());
518
519
	$container = Page::create('donation.php');
520
	$template->assign('DonateLink', $container->href());
521
522
	if ($session->hasGame()) {
523
		$player = $session->getPlayer();
524
		$template->assign('GameName', SmrGame::getGame($session->getGameID())->getName());
525
		$template->assign('GameID', $session->getGameID());
526
527
		$template->assign('PlotCourseLink', Globals::getPlotCourseHREF());
528
529
		$template->assign('TraderLink', Globals::getTraderStatusHREF());
530
531
		$template->assign('PoliticsLink', Globals::getPoliticsHREF());
532
533
		$container = Page::create('combat_log_list.php');
534
		$template->assign('CombatLogsLink', $container->href());
535
536
		$template->assign('PlanetLink', Globals::getPlanetListHREF($player->getAllianceID()));
537
538
		$container = Page::create('forces_list.php');
539
		$template->assign('ForcesLink', $container->href());
540
541
		$template->assign('MessagesLink', Globals::getViewMessageBoxesHREF());
542
543
		$container = Page::create('news_read_current.php');
544
		$template->assign('ReadNewsLink', $container->href());
545
546
		$container = Page::create('galactic_post_current.php');
547
		$template->assign('GalacticPostLink', $container->href());
548
549
		$container = Page::create('trader_search.php');
550
		$template->assign('SearchForTraderLink', $container->href());
551
552
		$container = Page::create('rankings_player_experience.php');
553
		$template->assign('RankingsLink', $container->href());
554
555
		$container = Page::create('hall_of_fame_new.php');
556
		$container['game_id'] = $player->getGameID();
557
		$template->assign('CurrentHallOfFameLink', $container->href());
558
559
		$unreadMessages = [];
560
		$dbResult = $db->read('SELECT message_type_id,COUNT(*) FROM player_has_unread_messages WHERE ' . $player->getSQL() . ' GROUP BY message_type_id');
561
		$container = Page::create('message_view.php');
562
		foreach ($dbResult->records() as $dbRecord) {
563
			$messageTypeID = $dbRecord->getInt('message_type_id');
564
			$container['folder_id'] = $messageTypeID;
565
			$unreadMessages[] = [
566
				'href' => $container->href(),
567
				'num' => $dbRecord->getInt('COUNT(*)'),
568
				'alt' => Messages::getMessageTypeNames($messageTypeID),
569
				'img' => Messages::getMessageTypeImage($messageTypeID),
570
			];
571
		}
572
		$template->assign('UnreadMessages', $unreadMessages);
573
574
		$container = Page::create('trader_search_result.php');
575
		$container['player_id'] = $player->getPlayerID();
576
		$template->assign('PlayerNameLink', $container->href());
577
578
		if (is_array(Globals::getHiddenPlayers()) && in_array($player->getAccountID(), Globals::getHiddenPlayers())) {
579
			$template->assign('PlayerInvisible', true);
580
		}
581
582
		// ******* Hardware *******
583
		$container = Page::create('configure_hardware.php');
584
		$template->assign('HardwareLink', $container->href());
585
586
		// ******* Forces *******
587
		$template->assign('ForceDropLink', Page::create('forces_drop.php')->href());
588
589
		$ship = $player->getShip();
590
		$var = Session::getInstance()->getCurrentVar();
591
		if ($ship->hasMines()) {
592
			$container = Page::create('forces_drop_processing.php');
593
			$container['owner_id'] = $player->getAccountID();
594
			$container['drop_mines'] = 1;
595
			$container['referrer'] = $var->file;
596
			$template->assign('DropMineLink', $container->href());
597
		}
598
		if ($ship->hasCDs()) {
599
			$container = Page::create('forces_drop_processing.php');
600
			$container['owner_id'] = $player->getAccountID();
601
			$container['drop_combat_drones'] = 1;
602
			$container['referrer'] = $var->file;
603
			$template->assign('DropCDLink', $container->href());
604
		}
605
606
		if ($ship->hasSDs()) {
607
			$container = Page::create('forces_drop_processing.php');
608
			$container['owner_id'] = $player->getAccountID();
609
			$container['drop_scout_drones'] = 1;
610
			$container['referrer'] = $var->file;
611
			$template->assign('DropSDLink', $container->href());
612
		}
613
614
		$template->assign('CargoJettisonLink', Page::create('cargo_dump.php')->href());
615
616
		$template->assign('WeaponReorderLink', Page::create('weapon_reorder.php')->href());
617
618
	}
619
620
	// ------- VOTING --------
621
	$voteLinks = [];
622
	foreach (VoteSite::cases() as $site) {
623
		$link = new VoteLink($site, $account->getAccountID(), $session->getGameID());
624
		$voteLinks[] = [
625
			'img' => $link->getImg(),
626
			'url' => $link->getUrl(),
627
			'sn' => $link->getSN(),
628
		];
629
	}
630
	$template->assign('VoteLinks', $voteLinks);
631
632
	// Determine the minimum time until the next vote across all sites
633
	$minVoteWait = VoteLink::getMinTimeUntilFreeTurns($account->getAccountID(), $session->getGameID());
634
	if ($minVoteWait <= 0) {
635
		$template->assign('TimeToNextVote', 'now');
636
	} else {
637
		$template->assign('TimeToNextVote', 'in ' . format_time($minVoteWait, true));
638
	}
639
640
	// ------- VERSION --------
641
	$dbResult = $db->read('SELECT * FROM version ORDER BY went_live DESC LIMIT 1');
642
	$version = '';
643
	if ($dbResult->hasRecord()) {
644
		$dbRecord = $dbResult->record();
645
		$container = Page::create('changelog_view.php');
646
		$version = create_link($container, 'v' . $dbRecord->getInt('major_version') . '.' . $dbRecord->getInt('minor_version') . '.' . $dbRecord->getInt('patch_level'));
647
	}
648
649
	$template->assign('Version', $version);
650
	$template->assign('CurrentYear', date('Y', Epoch::time()));
651
}
652
653
/**
654
 * Convert an integer number of seconds into a human-readable time.
655
 * Seconds are omitted to avoid frequent and disruptive ajax updates.
656
 * Use short=true to use 1-letter units (e.g. "1h and 3m").
657
 * If seconds is negative, will append "ago" to the result.
658
 * If seconds is zero, will return only "now".
659
 * If seconds is <60, will prefix "less than" or "<" (HTML-safe).
660
 */
661
function format_time(int $seconds, bool $short = false): string {
662
	if ($seconds == 0) {
663
		return 'now';
664
	}
665
666
	if ($seconds < 0) {
667
		$past = true;
668
		$seconds = abs($seconds);
669
	} else {
670
		$past = false;
671
	}
672
673
	$minutes = ceil($seconds / 60);
674
	$hours = 0;
675
	$days = 0;
676
	$weeks = 0;
677
	if ($minutes >= 60) {
678
		$hours = floor($minutes / 60);
679
		$minutes = $minutes % 60;
680
	}
681
	if ($hours >= 24) {
682
		$days = floor($hours / 24);
683
		$hours = $hours % 24;
684
	}
685
	if ($days >= 7) {
686
		$weeks = floor($days / 7);
687
		$days = $days % 7;
688
	}
689
	$times = [
690
		'week' => $weeks,
691
		'day' => $days,
692
		'hour' => $hours,
693
		'minute' => $minutes,
694
	];
695
	$parts = [];
696
	foreach ($times as $unit => $amount) {
697
		if ($amount > 0) {
698
			if ($short) {
699
				$parts[] = $amount . $unit[0];
700
			} else {
701
				$parts[] = pluralise($amount, $unit);
702
			}
703
		}
704
	}
705
706
	if (count($parts) == 1) {
707
		$result = $parts[0];
708
	} else {
709
		// e.g. 5h, 10m and 30s
710
		$result = implode(', ', array_slice($parts, 0, -1)) . ' and ' . end($parts);
711
	}
712
713
	if ($seconds < 60) {
714
		$result = ($short ? '&lt;' : 'less than ') . $result;
715
	}
716
717
	if ($past) {
718
		$result .= ' ago';
719
	}
720
	return $result;
721
}
722
723
function number_colour_format(float $number, bool $justSign = false): string {
724
	$formatted = '<span';
725
	if ($number > 0) {
726
		$formatted .= ' class="green">+';
727
	} elseif ($number < 0) {
728
		$formatted .= ' class="red">-';
729
	} else {
730
		$formatted .= '>';
731
	}
732
	if ($justSign === false) {
733
		$decimalPlaces = 0;
734
		$pos = strpos((string)$number, '.');
735
		if ($pos !== false) {
736
			$decimalPlaces = strlen(substr((string)$number, $pos + 1));
737
		}
738
		$formatted .= number_format(abs($number), $decimalPlaces);
739
	}
740
	$formatted .= '</span>';
741
	return $formatted;
742
}
743
744
745
/**
746
 * Randomly choose an array key weighted by the array values.
747
 * Probabilities are relative to the total weight. For example:
748
 *
749
 * array(
750
 *    'A' => 1, // 10% chance
751
 *    'B' => 3, // 30% chance
752
 *    'C' => 6, // 60% chance
753
 * );
754
 *
755
 * @template T of array-key
756
 * @param array<T, float> $choices
757
 * @return T
758
 */
759
function getWeightedRandom(array $choices): string|int {
760
	// Normalize the weights so that their sum is 1
761
	$sumWeight = array_sum($choices);
762
	foreach ($choices as $key => $weight) {
763
		$choices[$key] = $weight / $sumWeight;
764
	}
765
766
	// Generate a random number between 0 and 1
767
	$rand = rand() / getrandmax();
768
769
	// Subtract weights from the random number until it is negative,
770
	// then return the key associated with that weight.
771
	foreach ($choices as $key => $weight) {
772
		$rand -= $weight;
773
		if ($rand <= 0) {
774
			return $key;
775
		}
776
	}
777
	throw new Exception('Internal error computing weights');
778
}
779