Scrutinizer GitHub App not installed

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

Install GitHub App

Failed Conditions
Push — main ( 2c9538...02418a )
by Dan
38s queued 18s
created

src/lib/Default/smr.inc.php (1 issue)

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