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

Completed
Push — live ( 4b3996...bd440a )
by Dan
25s queued 18s
created

canWeUNO()   C

Complexity

Conditions 14
Paths 44

Size

Total Lines 50
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 2 Features 0
Metric Value
cc 14
eloc 28
c 5
b 2
f 0
nc 44
nop 2
dl 0
loc 50
rs 6.2666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
/**
7
 * @return 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...
8
 */
9
function overrideForward(Page $container) : void {
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
// needed libs
32
require_once(get_file_loc('smr.inc.php'));
33
require_once(get_file_loc('shop_goods.inc.php'));
34
35
// Raise exceptions for all types of errors for improved error reporting
36
// and to attempt to shut down the NPCs cleanly on errors.
37
set_error_handler("exception_error_handler");
38
39
const SHIP_UPGRADE_PATH = array(
40
	RACE_ALSKANT => array(
41
		SHIP_TYPE_TRADE_MASTER,
42
		SHIP_TYPE_TRIP_MAKER,
43
		SHIP_TYPE_SMALL_TIMER
44
	),
45
	RACE_CREONTI => array(
46
		SHIP_TYPE_LEVIATHAN,
47
		SHIP_TYPE_MEDIUM_CARGO_HULK
48
	),
49
	RACE_HUMAN => array(
50
		SHIP_TYPE_AMBASSADOR,
51
		SHIP_TYPE_RENAISSANCE,
52
		SHIP_TYPE_LIGHT_FREIGHTER
53
	),
54
	RACE_IKTHORNE => array(
55
		SHIP_TYPE_FAVOURED_OFFSPRING,
56
		SHIP_TYPE_PROTO_CARRIER,
57
		SHIP_TYPE_TINY_DELIGHT
58
	),
59
	RACE_SALVENE => array(
60
		SHIP_TYPE_DRUDGE,
61
		SHIP_TYPE_HATCHLINGS_DUE
62
	),
63
	RACE_THEVIAN => array(
64
		SHIP_TYPE_EXPEDITER,
65
		SHIP_TYPE_SWIFT_VENTURE
66
	),
67
	RACE_WQHUMAN => array(
68
		SHIP_TYPE_BLOCKADE_RUNNER,
69
		SHIP_TYPE_NEGOTIATOR,
70
		SHIP_TYPE_SLIP_FREIGHTER
71
	),
72
	RACE_NIJARIN => array(
73
		SHIP_TYPE_VENGEANCE,
74
		SHIP_TYPE_REDEEMER
75
	)
76
);
77
78
79
try {
80
	NPCStuff();
81
} catch (Throwable $e) {
82
	logException($e);
83
	// Try to shut down cleanly
84
	exitNPC();
85
}
86
87
88
function NPCStuff() : void {
89
	global $actions, $previousContainer;
90
91
	$session = Smr\Session::getInstance();
92
	$session->setCurrentVar(new Page()); // initialize empty var
93
94
	debug('Script started');
95
96
	// Make sure NPC's have been set up in the database
97
	$db = Smr\Database::getInstance();
98
	$dbResult = $db->read('SELECT 1 FROM npc_logins LIMIT 1');
99
	if (!$dbResult->hasRecord()) {
100
		debug('No NPCs have been created yet!');
101
		return;
102
	}
103
104
	// Load the first available NPC
105
	try {
106
		changeNPCLogin();
107
	} catch (ForwardException $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
108
109
	$allTradeRoutes = [];
110
	$tradeRoute = null;
111
	$underAttack = false;
112
	$actions = -1;
113
114
	while (true) {
115
		$actions++;
116
117
		// Avoid infinite loops by restricting the number of actions
118
		if ($actions > NPC_MAX_ACTIONS) {
119
			debug('Reached maximum number of actions: ' . NPC_MAX_ACTIONS);
120
			changeNPCLogin();
121
		}
122
123
		try {
124
			debug('Action #' . $actions);
125
126
			//We have to reload player on each loop
127
			$player = $session->getPlayer(true);
128
			// Sanity check to be certain we actually have an NPC
129
			if (!$player->isNPC()) {
130
				throw new Exception('Player is not an NPC!');
131
			}
132
			$player->updateTurns();
133
134
			// Are we starting with a new NPC?
135
			if ($actions == 0) {
136
				if ($player->getTurns() <= rand($player->getMaxTurns() / 2, $player->getMaxTurns()) && ($player->hasNewbieTurns() || $player->hasFederalProtection())) {
137
					debug('We don\'t have enough turns to bother starting trading, and we are protected: ' . $player->getTurns());
138
					changeNPCLogin();
139
				}
140
141
				// Ensure the NPC doesn't think it's under attack at startup,
142
				// since this could cause it to get stuck in a loop in Fed.
143
				$player->removeUnderAttack();
144
				$player->update();
145
146
				// Initialize the trade route for this NPC
147
				$allTradeRoutes = findRoutes($player);
148
				$tradeRoute = changeRoute($allTradeRoutes);
149
			}
150
151
			if ($player->isDead()) {
152
				debug('Some evil person killed us, let\'s move on now.');
153
				$previousContainer = null; //We died, we don't care what we were doing beforehand.
154
				$tradeRoute = changeRoute($allTradeRoutes);
155
				processContainer(Page::create('death_processing.php'));
156
			}
157
			if ($player->getNewbieTurns() <= NEWBIE_TURNS_WARNING_LIMIT && $player->getNewbieWarning()) {
158
				processContainer(Page::create('newbie_warning_processing.php'));
159
			}
160
161
			$var = $session->getCurrentVar();
162
			if (isset($var['url']) && $var['url'] == 'shop_ship_processing.php' && ($container = canWeUNO($player, false)) !== false) {
163
				//We just bought a ship, we should UNO now if we can
164
				processContainer($container);
165
			} elseif (!$underAttack && $player->isUnderAttack() === true
166
				&& ($player->hasPlottedCourse() === false || $player->getPlottedCourse()->getEndSector()->offersFederalProtection() === false)) {
167
				// We're under attack and need to plot course to fed.
168
				debug('Under Attack');
169
				$underAttack = true;
170
				processContainer(plotToFed($player, true));
171
			} elseif ($player->hasPlottedCourse() === true && $player->getPlottedCourse()->getEndSector()->offersFederalProtection()) { //We have a route to fed to follow, figure it's probably a damned sensible thing to follow.
172
				debug('Follow Course: ' . $player->getPlottedCourse()->getNextOnPath());
173
				processContainer(moveToSector($player, $player->getPlottedCourse()->getNextOnPath()));
174
			} elseif (($container = canWeUNO($player, true)) !== false) { //We have money and are at a uno, let's uno!
175
				debug('We\'re UNOing');
176
				processContainer($container);
177
			} elseif ($player->hasPlottedCourse() === true) { //We have a route to follow, figure it's probably a sensible thing to follow.
178
				debug('Follow Course: ' . $player->getPlottedCourse()->getNextOnPath());
179
				processContainer(moveToSector($player, $player->getPlottedCourse()->getNextOnPath()));
180
			} elseif ($player->getTurns() < NPC_LOW_TURNS || ($player->getTurns() < $player->getMaxTurns() / 2 && $player->getNewbieTurns() < NPC_LOW_NEWBIE_TURNS) || $underAttack) { //We're low on turns or have been under attack and need to plot course to fed
181
				if ($player->getTurns() < NPC_LOW_TURNS) {
182
					debug('Low Turns:' . $player->getTurns());
183
				}
184
				if ($underAttack) {
185
					debug('Fedding after attack.');
186
				}
187
				if ($player->hasNewbieTurns()) { //We have newbie turns, we can just wait here.
188
					debug('We have newbie turns, let\'s just switch to another NPC.');
189
					changeNPCLogin();
190
				}
191
				if ($player->hasFederalProtection()) {
192
					debug('We are in fed, time to switch to another NPC.');
193
					changeNPCLogin();
194
				}
195
				$ship = $player->getShip();
196
				processContainer(plotToFed($player, !$ship->hasMaxShields() || !$ship->hasMaxArmour() || !$ship->hasMaxCargoHolds()));
197
			} elseif (($container = checkForShipUpgrade($player)) !== false) { //We have money and are at a uno, let's uno!
198
				debug('We\'re reshipping!');
199
				processContainer($container);
200
			} elseif (($container = canWeUNO($player, false)) !== false) { //We need to UNO and have enough money to do it properly so let's do it sooner rather than later.
201
				debug('We need to UNO, so off we go!');
202
				processContainer($container);
203
			} elseif ($tradeRoute instanceof \Routes\Route) {
204
				debug('Trade Route');
205
				$forwardRoute = $tradeRoute->getForwardRoute();
206
				$returnRoute = $tradeRoute->getReturnRoute();
207
				if ($forwardRoute->getBuySectorId() == $player->getSectorID() || $returnRoute->getBuySectorId() == $player->getSectorID()) {
208
					if ($forwardRoute->getBuySectorId() == $player->getSectorID()) {
209
						$buyRoute = $forwardRoute;
210
						$sellRoute = $returnRoute;
211
					} elseif ($returnRoute->getBuySectorId() == $player->getSectorID()) {
212
						$buyRoute = $returnRoute;
213
						$sellRoute = $forwardRoute;
214
					}
215
216
					$ship = $player->getShip();
217
					if ($ship->getUsedHolds() > 0) {
218
						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...
219
							$goodID = $sellRoute->getGoodID();
220
221
							$port = $player->getSector()->getPort();
222
							$tradeRestriction = $port->getTradeRestriction($player);
223
224
							if ($tradeRestriction === false && $port->getGoodAmount($goodID) >= $ship->getCargo($sellRoute->getGoodID())) { //TODO: Sell what we can rather than forcing sell all at once?
225
								//Sell goods
226
								debug('Sell Goods');
227
								processContainer(tradeGoods($goodID, $player, $port));
228
							} else {
229
								//Move to next route or fed.
230
								if (($tradeRoute = changeRoute($allTradeRoutes)) === false) {
231
									debug('Changing Route Failed');
232
									processContainer(plotToFed($player));
233
								} else {
234
									debug('Route Changed');
235
									throw new ForwardException;
236
								}
237
							}
238
						} 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...
239
							debug('Plot To Sell: ' . $buyRoute->getSellSectorId());
240
							processContainer(plotToSector($player, $buyRoute->getSellSectorId()));
241
						} else {
242
							//Dump goods
243
							debug('Dump Goods');
244
							processContainer(dumpCargo($player));
245
						}
246
					} else { //Buy goods
247
						$goodID = $buyRoute->getGoodID();
248
249
						$port = $player->getSector()->getPort();
250
						$tradeRestriction = $port->getTradeRestriction($player);
251
252
						if ($tradeRestriction === false && $port->getGoodAmount($goodID) >= $ship->getEmptyHolds()) { //Buy goods
253
							debug('Buy Goods');
254
							processContainer(tradeGoods($goodID, $player, $port));
255
						} else {
256
							//Move to next route or fed.
257
							if (($tradeRoute = changeRoute($allTradeRoutes)) === false) {
258
								debug('Changing Route Failed');
259
								processContainer(plotToFed($player));
260
							} else {
261
								debug('Route Changed');
262
								throw new ForwardException;
263
							}
264
						}
265
					}
266
				} else {
267
					debug('Plot To Buy: ' . $forwardRoute->getBuySectorId());
268
					processContainer(plotToSector($player, $forwardRoute->getBuySectorId()));
269
				}
270
			} else { //Something weird is going on.. Let's fed and wait.
271
				debug('No actual action? Wtf?');
272
				processContainer(plotToFed($player));
273
			}
274
			/*
275
			else { //Otherwise let's run around at random.
276
				$links = $player->getSector()->getLinks();
277
				$moveTo = $links[array_rand($links)];
278
				debug('Random Wanderings: '.$moveTo);
279
				processContainer(moveToSector($player,$moveTo));
280
			}
281
			*/
282
			throw new Exception('NPC failed to perform any action');
283
		} catch (ForwardException $e) {
284
			global $lock;
285
			if ($lock) { //only save if we have the lock.
286
				SmrSector::saveSectors();
287
				SmrShip::saveShips();
288
				SmrPlayer::savePlayers();
289
				SmrForce::saveForces();
290
				SmrPort::savePorts();
291
				if (class_exists('WeightedRandom', false)) {
292
					WeightedRandom::saveWeightedRandoms();
293
				}
294
				release_lock();
295
			}
296
297
			//Clean up the caches as the data may get changed by other players
298
			clearCaches();
299
300
			//Clear up some global vars to avoid contaminating subsequent pages
301
			global $locksFailed;
302
			$locksFailed = array();
303
			$_REQUEST = array();
304
305
			//Have a sleep between actions
306
			sleepNPC();
307
		}
308
	}
309
	debug('Actions Finished.');
310
	exitNPC();
311
}
312
313
function clearCaches() : void {
314
	SmrSector::clearCache();
315
	SmrPlayer::clearCache();
316
	SmrShip::clearCache();
317
	SmrForce::clearCache();
318
	SmrPort::clearCache();
319
}
320
321
function debug(string $message, mixed $debugObject = null) : void {
322
	echo date('Y-m-d H:i:s - ') . $message . ($debugObject !== null ?EOL.var_export($debugObject, true) : '') . EOL;
323
	if (NPC_LOG_TO_DATABASE) {
324
		$session = Smr\Session::getInstance();
325
		$accountID = $session->getAccountID();
326
		$var = $session->getCurrentVar();
327
		$db = Smr\Database::getInstance();
328
		$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)) . ')');
329
330
		// On the first call to debug, we need to update the script_id retroactively
331
		if (!defined('SCRIPT_ID')) {
332
			define('SCRIPT_ID', $db->getInsertID());
333
			$db->write('UPDATE npc_logs SET script_id=' . SCRIPT_ID . ' WHERE log_id=' . SCRIPT_ID);
334
		}
335
	}
336
}
337
338
/**
339
 * @return never
340
 */
341
function processContainer(Page $container) : void {
342
	global $forwardedContainer, $previousContainer;
343
	$session = Smr\Session::getInstance();
344
	$player = $session->getPlayer();
345
	if ($container == $previousContainer && $forwardedContainer['body'] != 'forces_attack.php') {
346
		debug('We are executing the same container twice?', array('ForwardedContainer' => $forwardedContainer, 'Container' => $container));
347
		if ($player->hasNewbieTurns() || $player->hasFederalProtection()) {
348
			// 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.
349
			throw new Exception('We are executing the same container twice?');
350
		}
351
	}
352
	clearCaches(); //Clear caches of anything we have used for decision making before processing container and getting lock.
353
	$previousContainer = $container;
354
	debug('Executing container', $container);
355
	// The next "page request" must occur at an updated time.
356
	Smr\Epoch::update();
357
	$session->setCurrentVar($container);
358
	acquire_lock($player->getSectorID()); // Lock now to skip var update in do_voodoo
359
	do_voodoo();
360
}
361
362
function sleepNPC() : void {
363
	usleep(rand(MIN_SLEEP_TIME, MAX_SLEEP_TIME)); //Sleep for a random time
364
}
365
366
// Releases an NPC when it is done working
367
function releaseNPC() : void {
368
	$session = Smr\Session::getInstance();
369
	if (!$session->hasAccount()) {
370
		debug('releaseNPC: no NPC to release');
371
		return;
372
	}
373
	$login = $session->getAccount()->getLogin();
374
	$db = Smr\Database::getInstance();
375
	$db->write('UPDATE npc_logins SET working=' . $db->escapeBoolean(false) . ' WHERE login=' . $db->escapeString($login));
376
	if ($db->getChangedRows() > 0) {
377
		debug('Released NPC: ' . $login);
378
	} else {
379
		debug('Failed to release NPC: ' . $login);
380
	}
381
}
382
383
function exitNPC() : void {
384
	debug('Exiting NPC script.');
385
	releaseNPC();
386
	release_lock();
387
	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...
388
}
389
390
function changeNPCLogin() : void {
391
	global $actions, $previousContainer;
392
	if ($actions > 0) {
393
		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());
394
		exitNPC();
395
	}
396
397
	$actions = -1;
398
399
	// Release previous NPC, if any
400
	releaseNPC();
401
402
	// We chose a new NPC, we don't care what we were doing beforehand.
403
	$previousContainer = null;
404
405
	// Lacking a convenient way to get up-to-date turns, order NPCs by how
406
	// recently they have taken an action.
407
	debug('Choosing new NPC');
408
	static $availableNpcs = null;
409
410
	$db = Smr\Database::getInstance();
411
	$session = Smr\Session::getInstance();
412
413
	if (is_null($availableNpcs)) {
414
		// Make sure to select NPCs from active games only
415
		$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');
416
		foreach ($dbResult->records() as $dbRecord) {
417
			$availableNpcs[] = [
418
				'account_id' => $dbRecord->getInt('account_id'),
419
				'game_id' => $dbRecord->getInt('game_id'),
420
			];
421
		}
422
	}
423
424
	if (empty($availableNpcs)) {
425
		debug('No free NPCs');
426
		exitNPC();
427
	}
428
429
	// Pop an NPC off the top of the stack to activate
430
	$npc = array_shift($availableNpcs);
431
432
	// Update session info for this chosen NPC
433
	$account = SmrAccount::getAccount($npc['account_id']);
434
	$session->setAccount($account);
435
	$session->updateGame($npc['game_id']);
436
437
	$db->write('UPDATE npc_logins SET working=' . $db->escapeBoolean(true) . ' WHERE login=' . $db->escapeString($account->getLogin()));
438
	debug('Chosen NPC: ' . $account->getLogin() . ' (game ' . $session->getGameID() . ')');
439
440
	throw new ForwardException;
441
}
442
443
function canWeUNO(AbstractSmrPlayer $player, bool $oppurtunisticOnly) : Page|false {
444
	if ($player->getCredits() < MINUMUM_RESERVE_CREDITS) {
445
		return false;
446
	}
447
	$ship = $player->getShip();
448
	if ($ship->hasMaxShields() && $ship->hasMaxArmour() && $ship->hasMaxCargoHolds()) {
449
		return false;
450
	}
451
	$sector = $player->getSector();
452
453
	if ($player->getNewbieTurns() > MIN_NEWBIE_TURNS_TO_BUY_CARGO) {
454
		// Buy cargo holds first if we have plenty of newbie turns left.
455
		$hardwareArray = [HARDWARE_CARGO, HARDWARE_ARMOUR, HARDWARE_SHIELDS];
456
	} else {
457
		// We buy armour in preference to shields as it's cheaper.
458
		// We buy cargo holds last if we have no newbie turns because we'd rather not die
459
		$hardwareArray = [HARDWARE_ARMOUR, HARDWARE_SHIELDS, HARDWARE_CARGO];
460
	}
461
462
	foreach ($sector->getLocations() as $location) {
463
		foreach ($hardwareArray as $hardwareID) {
464
			if (!$location->isHardwareSold($hardwareID)) {
465
				continue;
466
			}
467
			$amountCanBuy = IFloor(($player->getCredits() - MINUMUM_RESERVE_CREDITS) / Globals::getHardwareCost($hardwareID));
468
			$amountNeeded = $ship->getType()->getMaxHardware($hardwareID) - $ship->getHardware($hardwareID);
469
			$amount = min($amountCanBuy, $amountNeeded);
470
			if ($amount > 0) {
471
				return doUNO($hardwareID, $amount, $sector->getSectorID());
472
			}
473
		}
474
	}
475
476
	if ($oppurtunisticOnly === true) {
477
		return false;
478
	}
479
480
	if ($player->getCredits() - $ship->getCostToUNO() < MINUMUM_RESERVE_CREDITS) {
481
		return false; //Only do non-oppurtunistic UNO if we have the money to do it properly!
482
	}
483
484
	foreach ($hardwareArray as $hardwareArrayID) {
485
		if (!$ship->hasMaxHardware($hardwareArrayID)) {
486
			$hardwareNeededID = $hardwareArrayID;
0 ignored issues
show
Unused Code introduced by
The assignment to $hardwareNeededID is dead and can be removed.
Loading history...
487
			// It's okay to return false if we're already at a shop, since we
488
			// decided above that we don't want to buy anything here.
489
			return plotToNearest($player, Globals::getHardwareTypes($hardwareArrayID));
490
		}
491
	}
492
	throw new Exception('Should not get here!');
493
}
494
495
function doUNO(int $hardwareID, int $amount, int $sectorID) : Page {
496
	debug('Buying ' . $amount . ' units of "' . Globals::getHardwareName($hardwareID) . '"');
497
	$_REQUEST = [
498
		'amount' => $amount,
499
		'action' => 'Buy',
500
	];
501
	$vars = [
502
		'hardware_id' => $hardwareID,
503
		'LocationID' => $sectorID,
504
	];
505
	return Page::create('shop_hardware_processing.php', '', $vars);
506
}
507
508
function tradeGoods(int $goodID, AbstractSmrPlayer $player, SmrPort $port) : Page {
509
	sleepNPC(); //We have an extra sleep at port to make the NPC more vulnerable.
510
	$ship = $player->getShip();
511
	$relations = $player->getRelation($port->getRaceID());
512
513
	$transaction = $port->getGoodTransaction($goodID);
514
515
	if ($transaction === TRADER_BUYS) {
516
		$amount = $ship->getEmptyHolds();
517
	} else {
518
		$amount = $ship->getCargo($goodID);
519
	}
520
521
	$idealPrice = $port->getIdealPrice($goodID, $transaction, $amount, $relations);
0 ignored issues
show
Bug introduced by
It seems like $transaction can also be of type null; however, parameter $transactionType of AbstractSmrPort::getIdealPrice() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

521
	$idealPrice = $port->getIdealPrice($goodID, /** @scrutinizer ignore-type */ $transaction, $amount, $relations);
Loading history...
522
	$offeredPrice = $port->getOfferPrice($idealPrice, $relations, $transaction);
0 ignored issues
show
Bug introduced by
It seems like $transaction can also be of type null; however, parameter $transactionType of AbstractSmrPort::getOfferPrice() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

522
	$offeredPrice = $port->getOfferPrice($idealPrice, $relations, /** @scrutinizer ignore-type */ $transaction);
Loading history...
523
524
	$_REQUEST = ['action' => $transaction];
525
	return Page::create('shop_goods_processing.php', '', array('offered_price'=>$offeredPrice, 'ideal_price'=>$idealPrice, 'amount'=>$amount, 'good_id'=>$goodID, 'bargain_price'=>$offeredPrice));
526
}
527
528
function dumpCargo(SmrPlayer $player) : Page {
529
	$ship = $player->getShip();
530
	$cargo = $ship->getCargo();
531
	debug('Ship Cargo', $cargo);
532
	foreach ($cargo as $goodID => $amount) {
533
		if ($amount > 0) {
534
			return Page::create('cargo_dump_processing.php', '', array('good_id'=>$goodID, 'amount'=>$amount));
535
		}
536
	}
537
	throw new Exception('Called dumpCargo without any cargo!');
538
}
539
540
function plotToSector(SmrPlayer $player, int $sectorID) : Page {
541
	return Page::create('course_plot_processing.php', '', array('from'=>$player->getSectorID(), 'to'=>$sectorID));
542
}
543
544
function plotToFed(SmrPlayer $player, bool $plotToHQ = false) : Page {
545
	debug('Plotting To Fed', $plotToHQ);
546
547
	// Always drop illegal goods before heading to fed space
548
	if ($player->getShip()->hasIllegalGoods()) {
549
		debug('Dumping illegal goods');
550
		processContainer(dumpCargo($player));
551
	}
552
553
	$fedLocID = $player->getRaceID() + ($plotToHQ ? LOCATION_GROUP_RACIAL_HQS : LOCATION_GROUP_RACIAL_BEACONS);
554
	$container = plotToNearest($player, SmrLocation::getLocation($fedLocID));
555
	if ($container === false) {
556
		debug('Plotted to fed whilst in fed, switch NPC and wait for turns');
557
		changeNPCLogin();
558
	}
559
	return $container;
560
}
561
562
function plotToNearest(AbstractSmrPlayer $player, mixed $realX) : Page|false {
563
	debug('Plotting To: ', $realX); //TODO: Can we make the debug output a bit nicer?
564
565
	if ($player->getSector()->hasX($realX)) { //Check if current sector has what we're looking for before we attempt to plot and get error.
566
		debug('Already available in sector');
567
		return false;
568
	}
569
570
	return Page::create('course_plot_nearest_processing.php', '', array('RealX'=>$realX));
571
}
572
573
function moveToSector(SmrPlayer $player, int $targetSector) : Page {
574
	debug('Moving from #' . $player->getSectorID() . ' to #' . $targetSector);
575
	return Page::create('sector_move_processing.php', '', array('target_sector'=>$targetSector, 'target_page'=>''));
576
}
577
578
function checkForShipUpgrade(AbstractSmrPlayer $player) : Page|false {
579
	foreach (SHIP_UPGRADE_PATH[$player->getRaceID()] as $upgradeShipID) {
580
		if ($player->getShipTypeID() == $upgradeShipID) {
581
			//We can't upgrade, only downgrade.
582
			return false;
583
		}
584
		$cost = $player->getShip()->getCostToUpgrade($upgradeShipID);
585
		if ($cost <= 0 || $player->getCredits() - $cost > MINUMUM_RESERVE_CREDITS) {
586
			return doShipUpgrade($player, $upgradeShipID);
587
		}
588
	}
589
	debug('Could not find a ship on the upgrade path.');
590
	return false;
591
}
592
593
function doShipUpgrade(AbstractSmrPlayer $player, int $upgradeShipID) : Page {
594
	$plotNearest = plotToNearest($player, SmrShipType::get($upgradeShipID));
595
596
	if ($plotNearest === false) { //We're already there!
597
		// We need a LocationID in the var to process the page, but it's only
598
		// used for display, so it doesn't matter for NPCs what the value is.
599
		return Page::create('shop_ship_processing.php', '',
600
			['ship_type_id' => $upgradeShipID, 'LocationID' => -1]);
601
	} //Otherwise return the plot
602
	return $plotNearest;
603
}
604
605
function changeRoute(array &$tradeRoutes) : Routes\Route|false {
606
	if (count($tradeRoutes) == 0) {
607
		return false;
608
	}
609
	$routeKey = array_rand($tradeRoutes);
610
	$tradeRoute = $tradeRoutes[$routeKey];
611
	unset($tradeRoutes[$routeKey]);
612
	debug('Switched route', $tradeRoute);
613
	return $tradeRoute;
614
}
615
616
function findRoutes(SmrPlayer $player) : array {
617
	debug('Finding Routes');
618
619
	$tradeGoods = array(GOODS_NOTHING => false);
620
	foreach (Globals::getGoods() as $goodID => $good) {
621
		if ($player->meetsAlignmentRestriction($good['AlignRestriction'])) {
622
			$tradeGoods[$goodID] = true;
623
		} else {
624
			$tradeGoods[$goodID] = false;
625
		}
626
	}
627
628
	// Only allow NPCs to trade at ports of their race and neutral ports
629
	$tradeRaces = array();
630
	foreach (Smr\Race::getAllIDs() as $raceID) {
631
		$tradeRaces[$raceID] = false;
632
	}
633
	$tradeRaces[$player->getRaceID()] = true;
634
	$tradeRaces[RACE_NEUTRAL] = true;
635
636
	$galaxy = $player->getSector()->getGalaxy();
637
638
	$maxNumberOfPorts = 2;
639
	$routesForPort = -1;
640
	$numberOfRoutes = 100;
641
	$maxDistance = 15;
642
643
	$startSectorID = $galaxy->getStartSector();
644
	$endSectorID = $galaxy->getEndSector();
645
646
	$db = Smr\Database::getInstance();
647
	$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));
648
	if ($dbResult->hasRecord()) {
649
		$routes = $dbResult->record()->getObject('routes', true);
650
		debug('Using Cached Routes: #' . count($routes));
651
		return $routes;
652
	} else {
653
		debug('Generating Routes');
654
		$allSectors = array();
655
		foreach (SmrGalaxy::getGameGalaxies($player->getGameID()) as $galaxy) {
656
			$allSectors += $galaxy->getSectors(); //Merge arrays
657
		}
658
659
		$distances = Plotter::calculatePortToPortDistances($allSectors, $maxDistance, $startSectorID, $endSectorID);
660
661
		if ($maxNumberOfPorts == 1) {
0 ignored issues
show
introduced by
The condition $maxNumberOfPorts == 1 is always false.
Loading history...
662
			$allRoutes = \Routes\RouteGenerator::generateOneWayRoutes($allSectors, $distances, $tradeGoods, $tradeRaces, $routesForPort);
663
		} else {
664
			$allRoutes = \Routes\RouteGenerator::generateMultiPortRoutes($maxNumberOfPorts, $allSectors, $tradeGoods, $tradeRaces, $distances, $routesForPort, $numberOfRoutes);
665
		}
666
667
		unset($distances);
668
669
		$routesMerged = array();
670
		foreach ($allRoutes[\Routes\RouteGenerator::MONEY_ROUTE] as $multi => $routesByMulti) {
671
			$routesMerged += $routesByMulti; //Merge arrays
672
		}
673
674
		unset($allSectors);
675
		SmrPort::clearCache();
676
		SmrSector::clearCache();
677
678
		if (count($routesMerged) == 0) {
679
			debug('Could not find any routes! Try another NPC.');
680
			changeNPCLogin();
681
		}
682
683
		$db->write('INSERT INTO route_cache ' .
684
				'(game_id, max_ports, goods_allowed, races_allowed, start_sector_id, end_sector_id, routes_for_port, max_distance, routes)' .
685
				' 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) . ')');
686
		debug('Found Routes: #' . count($routesMerged));
687
		return $routesMerged;
688
	}
689
}
690