We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.
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
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
|
|||
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
|
|||
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
|
|||
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 |