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 ( 388563...e23a8d )
by Dan
32:02 queued 24:00
created

src/tools/npc/npc.php (4 issues)

1
<?php declare(strict_types=1);
2
3
// Use this exception to help override container forwarding for NPC's
4
class ForwardException extends Exception {}
5
6
// Use this exception to indicate that an NPC has taken its final action
7
class FinalActionException extends Exception {}
8
9
/**
10
 * @return never
11
 */
12
function overrideForward(Page $container) : void {
13
	global $forwardedContainer;
14
	$forwardedContainer = $container;
15
	if ($container['body'] == 'error.php') {
16
		// We hit a create_error - this shouldn't happen for an NPC often,
17
		// for now we want to throw an exception for it for testing.
18
		debug('Hit an error');
19
		throw new Exception($container['message']);
20
	}
21
	// We have to throw the exception to get back up the stack,
22
	// otherwise we quickly hit problems of overflowing the stack.
23
	throw new ForwardException;
24
}
25
const OVERRIDE_FORWARD = true;
26
27
// Must be defined before anything that might throw an exception
28
const NPC_SCRIPT = true;
29
30
// global config
31
require_once(realpath(dirname(__FILE__)) . '/../../bootstrap.php');
32
// bot config
33
require_once(CONFIG . 'npc/config.specific.php');
34
// needed libs
35
require_once(get_file_loc('smr.inc.php'));
36
require_once(get_file_loc('shop_goods.inc.php'));
37
38
// Raise exceptions for all types of errors for improved error reporting
39
// and to attempt to shut down the NPCs cleanly on errors.
40
set_error_handler("exception_error_handler");
41
42
const SHIP_UPGRADE_PATH = array(
43
	RACE_ALSKANT => array(
44
		SHIP_TYPE_TRADE_MASTER,
45
		SHIP_TYPE_DEEP_SPACER,
46
		SHIP_TYPE_DEAL_MAKER,
47
		SHIP_TYPE_TRIP_MAKER,
48
		SHIP_TYPE_SMALL_TIMER
49
	),
50
	RACE_CREONTI => array(
51
		SHIP_TYPE_DEVASTATOR,
52
		SHIP_TYPE_JUGGERNAUT,
53
		SHIP_TYPE_GOLIATH,
54
		SHIP_TYPE_LEVIATHAN,
55
		SHIP_TYPE_MEDIUM_CARGO_HULK
56
	),
57
	RACE_HUMAN => array(
58
		SHIP_TYPE_DESTROYER,
59
		SHIP_TYPE_BORDER_CRUISER,
60
		SHIP_TYPE_AMBASSADOR,
61
		SHIP_TYPE_RENAISSANCE,
62
		SHIP_TYPE_LIGHT_FREIGHTER
63
	),
64
	RACE_IKTHORNE => array(
65
		SHIP_TYPE_MOTHER_SHIP,
66
		SHIP_TYPE_ADVANCED_CARRIER,
67
		SHIP_TYPE_FAVOURED_OFFSPRING,
68
		SHIP_TYPE_PROTO_CARRIER,
69
		SHIP_TYPE_TINY_DELIGHT
70
	),
71
	RACE_SALVENE => array(
72
		SHIP_TYPE_EATER_OF_SOULS,
73
		SHIP_TYPE_RAVAGER,
74
		SHIP_TYPE_PREDATOR,
75
		SHIP_TYPE_DRUDGE,
76
		SHIP_TYPE_HATCHLINGS_DUE
77
	),
78
	RACE_THEVIAN => array(
79
		SHIP_TYPE_ASSAULT_CRAFT,
80
		SHIP_TYPE_CARAPACE,
81
		SHIP_TYPE_BOUNTY_HUNTER,
82
		SHIP_TYPE_EXPEDITER,
83
		SHIP_TYPE_SWIFT_VENTURE
84
	),
85
	RACE_WQHUMAN => array(
86
		SHIP_TYPE_DARK_MIRAGE,
87
		SHIP_TYPE_BLOCKADE_RUNNER,
88
		SHIP_TYPE_ROGUE,
89
		SHIP_TYPE_RESISTANCE,
90
		SHIP_TYPE_SLIP_FREIGHTER
91
	),
92
	RACE_NIJARIN => array(
93
		SHIP_TYPE_FURY,
94
		SHIP_TYPE_VINDICATOR,
95
		SHIP_TYPE_VENGEANCE,
96
		SHIP_TYPE_RETALIATION,
97
		SHIP_TYPE_REDEEMER
98
	)
99
);
100
101
102
try {
103
	NPCStuff();
104
} catch (Throwable $e) {
105
	logException($e);
106
	// Try to shut down cleanly
107
	exitNPC();
108
}
109
110
111
function NPCStuff() : void {
112
	global $previousContainer;
113
114
	$session = Smr\Session::getInstance();
115
	$session->setCurrentVar(new Page()); // initialize empty var
116
117
	debug('Script started');
118
119
	// Load the first available NPC
120
	$changeNPC = true;
121
122
	while (true) {
123
		if ($changeNPC) {
124
			changeNPCLogin();
125
126
			// Reset tracking variables
127
			$changeNPC = false;
128
			$allTradeRoutes = [];
129
			$tradeRoute = null;
130
			$underAttack = false;
131
			$actions = 0;
132
133
			// We chose a new NPC, we don't care what we were doing beforehand.
134
			$previousContainer = null;
135
		}
136
137
		try {
138
			// Avoid infinite loops by restricting the number of actions
139
			if ($actions > NPC_MAX_ACTIONS) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $actions does not seem to be defined for all execution paths leading up to this point.
Loading history...
140
				debug('Reached maximum number of actions: ' . NPC_MAX_ACTIONS);
141
				throw new FinalActionException;
142
			}
143
144
			debug('Action #' . $actions);
145
146
			//We have to reload player on each loop
147
			$player = $session->getPlayer(true);
148
			// Sanity check to be certain we actually have an NPC
149
			if (!$player->isNPC()) {
150
				throw new Exception('Player is not an NPC!');
151
			}
152
			$player->updateTurns();
153
154
			// Are we starting with a new NPC?
155
			if ($actions == 0) {
156
				if ($player->getTurns() <= rand($player->getMaxTurns() / 2, $player->getMaxTurns()) && ($player->hasNewbieTurns() || $player->hasFederalProtection())) {
157
					debug('We don\'t have enough turns to bother starting trading, and we are protected: ' . $player->getTurns());
158
					throw new FinalActionException;
159
				}
160
161
				// Ensure the NPC doesn't think it's under attack at startup,
162
				// since this could cause it to get stuck in a loop in Fed.
163
				$player->removeUnderAttack();
164
165
				// Initialize the trade route for this NPC
166
				$allTradeRoutes = findRoutes($player);
167
				$tradeRoute = changeRoute($allTradeRoutes);
168
169
				// Upgrade ships if we can
170
				checkForShipUpgrade($player);
171
172
				// Start the NPC with max hardware
173
				$player->getShip()->setHardwareToMax();
174
175
				// Equip the ship with as many lasers as it can hold
176
				$weaponIDs = [
177
					WEAPON_TYPE_PLANETARY_PULSE_LASER,
178
					WEAPON_TYPE_HUGE_PULSE_LASER,
179
					WEAPON_TYPE_HUGE_PULSE_LASER,
180
					WEAPON_TYPE_LARGE_PULSE_LASER,
181
					WEAPON_TYPE_LARGE_PULSE_LASER,
182
					WEAPON_TYPE_LARGE_PULSE_LASER,
183
					WEAPON_TYPE_LASER,
184
				];
185
				$player->getShip()->removeAllWeapons();
186
				while ($player->getShip()->hasOpenWeaponSlots()) {
187
					$weapon = SmrWeapon::getWeapon(array_shift($weaponIDs));
188
					$player->getShip()->addWeapon($weapon);
189
				}
190
191
				// Update database (not essential to have a lock here)
192
				$player->update();
193
				$player->getShip()->update();
194
			}
195
196
			if ($player->isDead()) {
197
				debug('Some evil person killed us, let\'s move on now.');
198
				$previousContainer = null; //We died, we don't care what we were doing beforehand.
199
				$tradeRoute = changeRoute($allTradeRoutes, $tradeRoute);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tradeRoute does not seem to be defined for all execution paths leading up to this point.
Loading history...
200
				processContainer(Page::create('death_processing.php'));
201
			}
202
			if ($player->getNewbieTurns() <= NEWBIE_TURNS_WARNING_LIMIT && $player->getNewbieWarning()) {
203
				processContainer(Page::create('newbie_warning_processing.php'));
204
			}
205
206
			if (!$underAttack && $player->isUnderAttack() === true
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $underAttack does not seem to be defined for all execution paths leading up to this point.
Loading history...
207
				&& ($player->hasPlottedCourse() === false || $player->getPlottedCourse()->getEndSector()->offersFederalProtection() === false)) {
208
				// We're under attack and need to plot course to fed.
209
				debug('Under Attack');
210
				$underAttack = true;
211
				processContainer(plotToFed($player));
212
			} elseif ($player->hasPlottedCourse() === true && $player->getPlottedCourse()->getEndSector()->offersFederalProtection()) {
213
				// We have a route to fed to follow
214
				debug('Follow Course: ' . $player->getPlottedCourse()->getNextOnPath());
215
				processContainer(moveToSector($player, $player->getPlottedCourse()->getNextOnPath()));
216
			} elseif ($player->hasPlottedCourse() === true) {
217
				// We have a route to follow
218
				debug('Follow Course: ' . $player->getPlottedCourse()->getNextOnPath());
219
				processContainer(moveToSector($player, $player->getPlottedCourse()->getNextOnPath()));
220
			} elseif ($player->getTurns() < NPC_LOW_TURNS || ($player->getTurns() < $player->getMaxTurns() / 2 && $player->getNewbieTurns() < NPC_LOW_NEWBIE_TURNS) || $underAttack) {
221
				// We're low on turns or have been under attack and need to plot course to fed
222
				if ($player->getTurns() < NPC_LOW_TURNS) {
223
					debug('Low Turns:' . $player->getTurns());
224
				}
225
				if ($underAttack) {
226
					debug('Fedding after attack.');
227
				}
228
				if ($player->hasNewbieTurns()) { //We have newbie turns, we can just wait here.
229
					debug('We have newbie turns, let\'s just switch to another NPC.');
230
					throw new FinalActionException;
231
				}
232
				if ($player->hasFederalProtection()) {
233
					debug('We are in fed, time to switch to another NPC.');
234
					throw new FinalActionException;
235
				}
236
				processContainer(plotToFed($player));
237
			} elseif ($tradeRoute instanceof \Routes\Route) {
238
				debug('Trade Route');
239
				$forwardRoute = $tradeRoute->getForwardRoute();
240
				$returnRoute = $tradeRoute->getReturnRoute();
241
				if ($forwardRoute->getBuySectorId() == $player->getSectorID() || $returnRoute->getBuySectorId() == $player->getSectorID()) {
242
					if ($forwardRoute->getBuySectorId() == $player->getSectorID()) {
243
						$buyRoute = $forwardRoute;
244
						$sellRoute = $returnRoute;
245
					} elseif ($returnRoute->getBuySectorId() == $player->getSectorID()) {
246
						$buyRoute = $returnRoute;
247
						$sellRoute = $forwardRoute;
248
					}
249
250
					$ship = $player->getShip();
251
					if ($ship->getUsedHolds() > 0) {
252
						if ($ship->hasCargo($sellRoute->getGoodID())) { //Sell goods
253
							$goodID = $sellRoute->getGoodID();
254
255
							$port = $player->getSector()->getPort();
256
							$tradeRestriction = $port->getTradeRestriction($player);
257
258
							if ($tradeRestriction === false && $port->getGoodAmount($goodID) >= $ship->getCargo($sellRoute->getGoodID())) { //TODO: Sell what we can rather than forcing sell all at once?
259
								//Sell goods
260
								debug('Sell Goods');
261
								processContainer(tradeGoods($goodID, $player, $port));
262
							} else {
263
								//Move to next route or fed.
264
								if (($tradeRoute = changeRoute($allTradeRoutes)) === null) {
265
									debug('Changing Route Failed');
266
									processContainer(plotToFed($player));
267
								} else {
268
									debug('Route Changed');
269
									throw new ForwardException;
270
								}
271
							}
272
						} elseif ($ship->hasCargo($buyRoute->getGoodID()) === true) { //We've bought goods, plot to sell
273
							debug('Plot To Sell: ' . $buyRoute->getSellSectorId());
274
							processContainer(plotToSector($player, $buyRoute->getSellSectorId()));
275
						} else {
276
							//Dump goods
277
							debug('Dump Goods');
278
							processContainer(dumpCargo($player));
279
						}
280
					} else { //Buy goods
281
						$goodID = $buyRoute->getGoodID();
282
283
						$port = $player->getSector()->getPort();
284
						$tradeRestriction = $port->getTradeRestriction($player);
285
286
						if ($tradeRestriction === false && $port->getGoodAmount($goodID) >= $ship->getEmptyHolds()) { //Buy goods
287
							debug('Buy Goods');
288
							processContainer(tradeGoods($goodID, $player, $port));
289
						} else {
290
							//Move to next route or fed.
291
							if (($tradeRoute = changeRoute($allTradeRoutes)) === null) {
292
								debug('Changing Route Failed');
293
								processContainer(plotToFed($player));
294
							} else {
295
								debug('Route Changed');
296
								throw new ForwardException;
297
							}
298
						}
299
					}
300
				} else {
301
					debug('Plot To Buy: ' . $forwardRoute->getBuySectorId());
302
					processContainer(plotToSector($player, $forwardRoute->getBuySectorId()));
303
				}
304
			} else { //Something weird is going on.. Let's fed and wait.
305
				debug('No actual action? Wtf?');
306
				processContainer(plotToFed($player));
307
			}
308
			/*
309
			else { //Otherwise let's run around at random.
310
				$links = $player->getSector()->getLinks();
311
				$moveTo = $links[array_rand($links)];
312
				debug('Random Wanderings: '.$moveTo);
313
				processContainer(moveToSector($player,$moveTo));
314
			}
315
			*/
316
			throw new Exception('NPC failed to perform any action');
317
		} catch (ForwardException $e) {
318
			$actions++; // we took an action
319
		} catch (FinalActionException $e) {
320
			if ($player->getSector()->offersFederalProtection() && !$player->hasFederalProtection()) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $player does not seem to be defined for all execution paths leading up to this point.
Loading history...
321
				debug('Disarming so we can get Fed protection');
322
				$player->getShip()->setCDs(0);
323
				$player->getShip()->removeAllWeapons();
324
				$player->getShip()->update();
325
			}
326
			// switch to a new NPC if we haven't taken any actions yet
327
			if ($actions > 0) {
328
				debug('We have taken actions and now want to change NPC, let\'s exit and let next script choose a new NPC to reset execution time', getrusage());
329
				exitNPC();
330
			}
331
			$changeNPC = true;
332
		}
333
334
		// Save any changes that we made during this action
335
		global $lock;
336
		if ($lock) { //only save if we have the lock.
337
			SmrSector::saveSectors();
338
			SmrShip::saveShips();
339
			SmrPlayer::savePlayers();
340
			SmrForce::saveForces();
341
			SmrPort::savePorts();
342
			if (class_exists('WeightedRandom', false)) {
343
				WeightedRandom::saveWeightedRandoms();
344
			}
345
			release_lock();
346
		}
347
348
		//Clean up the caches as the data may get changed by other players
349
		clearCaches();
350
351
		//Clear up some global vars to avoid contaminating subsequent pages
352
		global $locksFailed;
353
		$locksFailed = array();
354
		$_REQUEST = array();
355
356
		//Have a sleep between actions
357
		sleepNPC();
358
	}
359
	debug('Actions Finished.');
360
	exitNPC();
361
}
362
363
function clearCaches() : void {
364
	SmrSector::clearCache();
365
	SmrPlayer::clearCache();
366
	SmrShip::clearCache();
367
	SmrForce::clearCache();
368
	SmrPort::clearCache();
369
}
370
371
function debug(string $message, mixed $debugObject = null) : void {
372
	echo date('Y-m-d H:i:s - ') . $message . ($debugObject !== null ?EOL.var_export($debugObject, true) : '') . EOL;
373
	if (NPC_LOG_TO_DATABASE) {
374
		$session = Smr\Session::getInstance();
375
		$accountID = $session->getAccountID();
376
		$var = $session->getCurrentVar();
377
		$db = Smr\Database::getInstance();
378
		$db->write('INSERT INTO npc_logs (script_id, npc_id, time, message, debug_info, var) VALUES (' . (defined('SCRIPT_ID') ?SCRIPT_ID:0) . ', ' . $accountID . ',NOW(),' . $db->escapeString($message) . ',' . $db->escapeString(var_export($debugObject, true)) . ',' . $db->escapeString(var_export($var, true)) . ')');
379
380
		// On the first call to debug, we need to update the script_id retroactively
381
		if (!defined('SCRIPT_ID')) {
382
			define('SCRIPT_ID', $db->getInsertID());
383
			$db->write('UPDATE npc_logs SET script_id=' . SCRIPT_ID . ' WHERE log_id=' . SCRIPT_ID);
384
		}
385
	}
386
}
387
388
/**
389
 * @return never
390
 */
391
function processContainer(Page $container) : void {
392
	global $forwardedContainer, $previousContainer;
393
	$session = Smr\Session::getInstance();
394
	$player = $session->getPlayer();
395
	if ($container == $previousContainer && $forwardedContainer['body'] != 'forces_attack.php') {
396
		debug('We are executing the same container twice?', array('ForwardedContainer' => $forwardedContainer, 'Container' => $container));
397
		if ($player->hasNewbieTurns() || $player->hasFederalProtection()) {
398
			// Only throw the exception if we have protection, otherwise let's hope that the NPC will be able to find its way to safety rather than dying in the open.
399
			throw new Exception('We are executing the same container twice?');
400
		}
401
	}
402
	clearCaches(); //Clear caches of anything we have used for decision making before processing container and getting lock.
403
	$previousContainer = $container;
404
	debug('Executing container', $container);
405
	// The next "page request" must occur at an updated time.
406
	Smr\Epoch::update();
407
	$session->setCurrentVar($container);
408
	acquire_lock($player->getSectorID()); // Lock now to skip var update in do_voodoo
409
	do_voodoo();
410
}
411
412
function sleepNPC() : void {
413
	usleep(rand(MIN_SLEEP_TIME, MAX_SLEEP_TIME)); //Sleep for a random time
414
}
415
416
// Releases an NPC when it is done working
417
function releaseNPC() : void {
418
	$session = Smr\Session::getInstance();
419
	if (!$session->hasAccount()) {
420
		debug('releaseNPC: no NPC to release');
421
		return;
422
	}
423
	$login = $session->getAccount()->getLogin();
424
	$db = Smr\Database::getInstance();
425
	$db->write('UPDATE npc_logins SET working=' . $db->escapeBoolean(false) . ' WHERE login=' . $db->escapeString($login));
426
	if ($db->getChangedRows() > 0) {
427
		debug('Released NPC: ' . $login);
428
	} else {
429
		debug('Failed to release NPC: ' . $login);
430
	}
431
}
432
433
function exitNPC() : void {
434
	debug('Exiting NPC script.');
435
	releaseNPC();
436
	release_lock();
437
	exit;
438
}
439
440
function changeNPCLogin() : void {
441
	// Release previous NPC, if any
442
	releaseNPC();
443
444
	// Lacking a convenient way to get up-to-date turns, order NPCs by how
445
	// recently they have taken an action.
446
	debug('Choosing new NPC');
447
	static $availableNpcs = null;
448
449
	$db = Smr\Database::getInstance();
450
	$session = Smr\Session::getInstance();
451
452
	if (is_null($availableNpcs)) {
453
		// Make sure NPC's have been set up in the database
454
		$dbResult = $db->read('SELECT 1 FROM npc_logins LIMIT 1');
455
		if (!$dbResult->hasRecord()) {
456
			debug('No NPCs have been created yet!');
457
			exitNPC();
458
		}
459
460
		// Make sure to select NPCs from active games only
461
		$dbResult = $db->read('SELECT account_id, game_id FROM player JOIN account USING(account_id) JOIN npc_logins USING(login) JOIN game USING(game_id) WHERE active=\'TRUE\' AND working=\'FALSE\' AND start_time < ' . $db->escapeNumber(Smr\Epoch::time()) . ' AND end_time > ' . $db->escapeNumber(Smr\Epoch::time()) . ' ORDER BY last_turn_update ASC');
462
		foreach ($dbResult->records() as $dbRecord) {
463
			$availableNpcs[] = [
464
				'account_id' => $dbRecord->getInt('account_id'),
465
				'game_id' => $dbRecord->getInt('game_id'),
466
			];
467
		}
468
	}
469
470
	if (empty($availableNpcs)) {
471
		debug('No free NPCs');
472
		exitNPC();
473
	}
474
475
	// Pop an NPC off the top of the stack to activate
476
	$npc = array_shift($availableNpcs);
477
478
	// Update session info for this chosen NPC
479
	$account = SmrAccount::getAccount($npc['account_id']);
480
	$session->setAccount($account);
481
	$session->updateGame($npc['game_id']);
482
483
	$db->write('UPDATE npc_logins SET working=' . $db->escapeBoolean(true) . ' WHERE login=' . $db->escapeString($account->getLogin()));
484
	debug('Chosen NPC: ' . $account->getLogin() . ' (game ' . $session->getGameID() . ')');
485
}
486
487
function tradeGoods(int $goodID, AbstractSmrPlayer $player, SmrPort $port) : Page {
488
	sleepNPC(); //We have an extra sleep at port to make the NPC more vulnerable.
489
	$ship = $player->getShip();
490
	$relations = $player->getRelation($port->getRaceID());
491
492
	$transaction = $port->getGoodTransaction($goodID);
493
494
	if ($transaction === TRADER_BUYS) {
495
		$amount = $ship->getEmptyHolds();
496
	} else {
497
		$amount = $ship->getCargo($goodID);
498
	}
499
500
	$idealPrice = $port->getIdealPrice($goodID, $transaction, $amount, $relations);
501
	$offeredPrice = $port->getOfferPrice($idealPrice, $relations, $transaction);
502
503
	$_REQUEST = ['action' => $transaction];
504
	return Page::create('shop_goods_processing.php', '', array('offered_price'=>$offeredPrice, 'ideal_price'=>$idealPrice, 'amount'=>$amount, 'good_id'=>$goodID, 'bargain_price'=>$offeredPrice));
505
}
506
507
function dumpCargo(SmrPlayer $player) : Page {
508
	$ship = $player->getShip();
509
	$cargo = $ship->getCargo();
510
	debug('Ship Cargo', $cargo);
511
	foreach ($cargo as $goodID => $amount) {
512
		if ($amount > 0) {
513
			return Page::create('cargo_dump_processing.php', '', array('good_id'=>$goodID, 'amount'=>$amount));
514
		}
515
	}
516
	throw new Exception('Called dumpCargo without any cargo!');
517
}
518
519
function plotToSector(SmrPlayer $player, int $sectorID) : Page {
520
	return Page::create('course_plot_processing.php', '', array('from'=>$player->getSectorID(), 'to'=>$sectorID));
521
}
522
523
function plotToFed(SmrPlayer $player) : Page {
524
	debug('Plotting To Fed');
525
526
	// Always drop illegal goods before heading to fed space
527
	if ($player->getShip()->hasIllegalGoods()) {
528
		debug('Dumping illegal goods');
529
		processContainer(dumpCargo($player));
530
	}
531
532
	$fedLocID = $player->getRaceID() + LOCATION_GROUP_RACIAL_BEACONS;
533
	$container = plotToNearest($player, SmrLocation::getLocation($fedLocID));
534
	if ($container === false) {
535
		debug('Plotted to fed whilst in fed, switch NPC and wait for turns');
536
		throw new FinalActionException;
537
	}
538
	return $container;
539
}
540
541
function plotToNearest(AbstractSmrPlayer $player, mixed $realX) : Page|false {
542
	debug('Plotting To: ', $realX); //TODO: Can we make the debug output a bit nicer?
543
544
	if ($player->getSector()->hasX($realX)) { //Check if current sector has what we're looking for before we attempt to plot and get error.
545
		debug('Already available in sector');
546
		return false;
547
	}
548
549
	return Page::create('course_plot_nearest_processing.php', '', array('RealX'=>$realX));
550
}
551
552
function moveToSector(SmrPlayer $player, int $targetSector) : Page {
553
	debug('Moving from #' . $player->getSectorID() . ' to #' . $targetSector);
554
	return Page::create('sector_move_processing.php', '', array('target_sector'=>$targetSector, 'target_page'=>''));
555
}
556
557
function checkForShipUpgrade(AbstractSmrPlayer $player) : void {
558
	foreach (SHIP_UPGRADE_PATH[$player->getRaceID()] as $upgradeShipID) {
559
		if ($player->getShipTypeID() == $upgradeShipID) {
560
			//We can't upgrade, only downgrade.
561
			return;
562
		}
563
		$cost = $player->getShip()->getCostToUpgrade($upgradeShipID);
564
		$balance = $player->getCredits() - $cost;
565
		if ($balance > MINUMUM_RESERVE_CREDITS) {
566
			debug('Upgrading to ship type: ' . $upgradeShipID);
567
			$player->setCredits($balance);
568
			$player->getShip()->setTypeID($upgradeShipID);
569
			return;
570
		}
571
	}
572
}
573
574
function changeRoute(array &$tradeRoutes, Routes\Route $routeToAvoid = null) : ?Routes\Route {
575
	// Remove any route from the pool of available routes if it contains
576
	// either of the sectors in the $routeToAvoid (i.e. we died on it,
577
	// so don't go back!).
578
	if ($routeToAvoid !== null) {
579
		$avoidSectorIDs = array_unique([
580
			$routeToAvoid->getForwardRoute()->getSellSectorId(),
581
			$routeToAvoid->getForwardRoute()->getBuySectorId(),
582
			$routeToAvoid->getReturnRoute()->getSellSectorId(),
583
			$routeToAvoid->getReturnRoute()->getBuySectorId(),
584
		]);
585
		foreach ($tradeRoutes as $key => $route) {
586
			foreach ($avoidSectorIDs as $avoidSectorID) {
587
				if ($route->containsPort($avoidSectorID)) {
588
					unset($tradeRoutes[$key]);
589
					break;
590
				}
591
			}
592
		}
593
	}
594
595
	if (count($tradeRoutes) == 0) {
596
		return null;
597
	}
598
599
	// Pick a random route
600
	$routeKey = array_rand($tradeRoutes);
601
	$tradeRoute = $tradeRoutes[$routeKey];
602
603
	// Remove the route we chose so that we don't pick it again later.
604
	unset($tradeRoutes[$routeKey]);
605
606
	debug('Switched route', $tradeRoute);
607
	return $tradeRoute;
608
}
609
610
function findRoutes(SmrPlayer $player) : array {
611
	debug('Finding Routes');
612
613
	$tradeGoods = array(GOODS_NOTHING => false);
614
	foreach (Globals::getGoods() as $goodID => $good) {
615
		if ($player->meetsAlignmentRestriction($good['AlignRestriction'])) {
616
			$tradeGoods[$goodID] = true;
617
		} else {
618
			$tradeGoods[$goodID] = false;
619
		}
620
	}
621
622
	// Only allow NPCs to trade at ports of their race and neutral ports
623
	$tradeRaces = array();
624
	foreach (Smr\Race::getAllIDs() as $raceID) {
625
		$tradeRaces[$raceID] = false;
626
	}
627
	$tradeRaces[$player->getRaceID()] = true;
628
	$tradeRaces[RACE_NEUTRAL] = true;
629
630
	$galaxy = $player->getSector()->getGalaxy();
631
632
	$maxNumberOfPorts = 2;
633
	$routesForPort = -1;
634
	$numberOfRoutes = 100;
635
	$maxDistance = 15;
636
637
	$startSectorID = $galaxy->getStartSector();
638
	$endSectorID = $galaxy->getEndSector();
639
640
	$db = Smr\Database::getInstance();
641
	$dbResult = $db->read('SELECT routes FROM route_cache WHERE game_id=' . $db->escapeNumber($player->getGameID()) . ' AND max_ports=' . $db->escapeNumber($maxNumberOfPorts) . ' AND goods_allowed=' . $db->escapeObject($tradeGoods) . ' AND races_allowed=' . $db->escapeObject($tradeRaces) . ' AND start_sector_id=' . $db->escapeNumber($startSectorID) . ' AND end_sector_id=' . $db->escapeNumber($endSectorID) . ' AND routes_for_port=' . $db->escapeNumber($routesForPort) . ' AND max_distance=' . $db->escapeNumber($maxDistance));
642
	if ($dbResult->hasRecord()) {
643
		$routes = $dbResult->record()->getObject('routes', true);
644
		debug('Using Cached Routes: #' . count($routes));
645
		return $routes;
646
	} else {
647
		debug('Generating Routes');
648
		$allSectors = array();
649
		foreach (SmrGalaxy::getGameGalaxies($player->getGameID()) as $galaxy) {
650
			$allSectors += $galaxy->getSectors(); //Merge arrays
651
		}
652
653
		$distances = Plotter::calculatePortToPortDistances($allSectors, $maxDistance, $startSectorID, $endSectorID);
654
655
		if ($maxNumberOfPorts == 1) {
656
			$allRoutes = \Routes\RouteGenerator::generateOneWayRoutes($allSectors, $distances, $tradeGoods, $tradeRaces, $routesForPort);
657
		} else {
658
			$allRoutes = \Routes\RouteGenerator::generateMultiPortRoutes($maxNumberOfPorts, $allSectors, $tradeGoods, $tradeRaces, $distances, $routesForPort, $numberOfRoutes);
659
		}
660
661
		unset($distances);
662
663
		$routesMerged = array();
664
		foreach ($allRoutes[\Routes\RouteGenerator::MONEY_ROUTE] as $multi => $routesByMulti) {
665
			$routesMerged += $routesByMulti; //Merge arrays
666
		}
667
668
		unset($allSectors);
669
		SmrPort::clearCache();
670
		SmrSector::clearCache();
671
672
		if (count($routesMerged) == 0) {
673
			debug('Could not find any routes! Try another NPC.');
674
			throw new FinalActionException;
675
		}
676
677
		$db->write('INSERT INTO route_cache ' .
678
				'(game_id, max_ports, goods_allowed, races_allowed, start_sector_id, end_sector_id, routes_for_port, max_distance, routes)' .
679
				' VALUES (' . $db->escapeNumber($player->getGameID()) . ', ' . $db->escapeNumber($maxNumberOfPorts) . ', ' . $db->escapeObject($tradeGoods) . ', ' . $db->escapeObject($tradeRaces) . ', ' . $db->escapeNumber($startSectorID) . ', ' . $db->escapeNumber($endSectorID) . ', ' . $db->escapeNumber($routesForPort) . ', ' . $db->escapeNumber($maxDistance) . ', ' . $db->escapeObject($routesMerged, true) . ')');
680
		debug('Found Routes: #' . count($routesMerged));
681
		return $routesMerged;
682
	}
683
}
684