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

Passed
Push — live ( 90e9aa...655924 )
by Dan
07:29
created

checkStartConditions()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 4
eloc 4
c 1
b 1
f 0
nc 2
nop 1
dl 0
loc 5
rs 10
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
function overrideForward(Page $container) : never {
0 ignored issues
show
Bug introduced by
The type never was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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