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 — live ( 05ca5f...631a24 )
by Dan
05:49
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