* @license MIT License * @version GIT: * @link https://git.keithsolomon.net/keith/Spacetraders */ require_once __DIR__ . '/lib/spacetraders-api.php'; require_once __DIR__ . '/lib/spacetraders-storage.php'; /** * Filter only mining ships. * * @param array> $ships * * @return array> */ function filterMiningShips( array $ships ): array { return array_values( array_filter( $ships, static function ( array $ship ): bool { $role = strtoupper( (string) ( $ship['registration']['role'] ?? '' ) ); return $role === 'EXCAVATOR'; } ) ); } /** * Build contract delivery targets for outstanding delivery requirements. * * @param array> $activeContracts * * @return array> */ function buildContractDeliveryTargets( array $activeContracts ): array { $targetsByKey = array(); foreach ( $activeContracts as $contract ) { $contractId = (string) ( $contract['id'] ?? '' ); $deliveries = (array) ( $contract['terms']['deliver'] ?? array() ); if ($contractId === '' || empty( $deliveries ) ) { continue; } foreach ( $deliveries as $delivery ) { $tradeSymbol = (string) ( $delivery['tradeSymbol'] ?? '' ); $destinationSymbol = (string) ( $delivery['destinationSymbol'] ?? '' ); $unitsRequired = (int) ( $delivery['unitsRequired'] ?? 0 ); $unitsFulfilled = (int) ( $delivery['unitsFulfilled'] ?? 0 ); $remainingUnits = max( 0, $unitsRequired - $unitsFulfilled ); if ($tradeSymbol === '' || $destinationSymbol === '' || $remainingUnits <= 0 ) { continue; } $key = $contractId . '|' . $destinationSymbol . '|' . $tradeSymbol; if (! isset( $targetsByKey[ $key ] ) ) { $targetsByKey[ $key ] = array( 'key' => $key, 'contractId' => $contractId, 'destinationSymbol' => $destinationSymbol, 'tradeSymbol' => $tradeSymbol, 'remainingUnits' => 0, ); } $targetsByKey[ $key ]['remainingUnits'] += $remainingUnits; } } $targets = array_values( $targetsByKey ); usort( $targets, static function ( array $left, array $right ): int { $leftSort = (string) ( $left['destinationSymbol'] ?? '' ) . '|' . (string) ( $left['tradeSymbol'] ?? '' ) . '|' . (string) ( $left['contractId'] ?? '' ); $rightSort = (string) ( $right['destinationSymbol'] ?? '' ) . '|' . (string) ( $right['tradeSymbol'] ?? '' ) . '|' . (string) ( $right['contractId'] ?? '' ); return strcmp( $leftSort, $rightSort ); } ); return $targets; } $config = require __DIR__ . '/lib/project-config.php'; $storage = new SpacetradersStorage( $config['db_path'] ); $token = $storage->getAgentToken(); $statusMessage = ''; $errorMessage = ''; $actionResults = array(); $agent = array(); $ships = array(); $miningShips = array(); $activeContracts = array(); $marketWaypoints = array(); $contractDeliveryTargets = array(); $selectedMarketWaypoint = ''; $selectedContractDeliveryKey = ''; if (! is_string( $token ) || trim( $token ) === '' ) { $envToken = getenv( 'SPACETRADERS_TOKEN' ); if (is_string( $envToken ) && trim( $envToken ) !== '' ) { $token = trim( $envToken ); $storage->setAgentToken( $token ); } } if (! is_string( $token ) || trim( $token ) === '' ) { $tokenError = 'No token found. Set one in config.php or SPACETRADERS_TOKEN.'; } if (! isset( $tokenError ) ) { $client = new SpacetradersApi( trim( $token ), $config['api_base_url'], (int) $config['api_timeout'], $storage, (int) $config['cache_ttl'] ); } try { if (! isset( $tokenError ) ) { $agentResponse = $client->getMyAgent(); $shipsResponse = $client->listMyShips(); $contractsResponse = $client->listMyContracts(); $agent = $agentResponse['data'] ?? $agentResponse; $ships = $shipsResponse['data'] ?? $shipsResponse; $contracts = $contractsResponse['data'] ?? $contractsResponse; $miningShips = filterMiningShips( $ships ); $activeContracts = array_values( array_filter( (array) $contracts, static function ( array $contract ): bool { return (bool) ( $contract['accepted'] ?? false ) && ! (bool) ( $contract['fulfilled'] ?? false ); } ) ); $currentSystemSymbol = ''; if (! empty( $miningShips ) && isset( $miningShips[0]['nav']['systemSymbol'] ) ) { $currentSystemSymbol = (string) $miningShips[0]['nav']['systemSymbol']; } elseif (! empty( $ships ) && isset( $ships[0]['nav']['systemSymbol'] ) ) { $currentSystemSymbol = (string) $ships[0]['nav']['systemSymbol']; } elseif (isset( $agent['headquarters'] ) && is_string( $agent['headquarters'] ) ) { $hqParts = explode( '-', $agent['headquarters'] ); if (count( $hqParts ) >= 2 ) { $currentSystemSymbol = $hqParts[0] . '-' . $hqParts[1]; } } if ($currentSystemSymbol !== '' ) { $page = 1; $total = 0; $waypoints = array(); do { $waypointsResponse = $client->listWaypoints( $currentSystemSymbol, array( 'page' => $page, 'limit' => 20, ) ); $pageData = $waypointsResponse['data'] ?? array(); if (! is_array( $pageData ) || empty( $pageData ) ) { break; } $waypoints = array_merge( $waypoints, $pageData ); $total = (int) ( $waypointsResponse['meta']['total'] ?? count( $waypoints ) ); $page++; } while (count( $waypoints ) < $total); foreach ( $waypoints as $waypoint ) { foreach ( (array) ( $waypoint['traits'] ?? array() ) as $trait ) { if ((string) ( $trait['symbol'] ?? '' ) === 'MARKETPLACE' ) { $marketWaypoints[] = (string) ( $waypoint['symbol'] ?? '' ); break; } } } } $selectedMarketWaypoint = isset( $_POST['market_waypoint'] ) ? trim( (string) $_POST['market_waypoint'] ) : ( $marketWaypoints[0] ?? '' ); $contractDeliveryTargets = buildContractDeliveryTargets( $activeContracts ); $selectedContractDeliveryKey = isset( $_POST['contract_delivery_key'] ) ? trim( (string) $_POST['contract_delivery_key'] ) : (string) ( $contractDeliveryTargets[0]['key'] ?? '' ); if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_POST['fleet_action'] ) ) { $fleetAction = (string) $_POST['fleet_action']; $successCount = 0; $selectedContractDeliveryTarget = null; $remainingContractUnits = 0; foreach ( $contractDeliveryTargets as $target ) { if ((string) ( $target['key'] ?? '' ) === $selectedContractDeliveryKey ) { $selectedContractDeliveryTarget = $target; $remainingContractUnits = (int) ( $target['remainingUnits'] ?? 0 ); break; } } foreach ( $miningShips as $fleetShip ) { $shipSymbol = (string) ( $fleetShip['symbol'] ?? '' ); if ($shipSymbol === '' ) { continue; } try { $shipResponse = $client->getShip( $shipSymbol ); $shipData = $shipResponse['data'] ?? array(); $shipStatus = (string) ( $shipData['nav']['status'] ?? '' ); $waypointSymbol = (string) ( $shipData['nav']['waypointSymbol'] ?? '' ); if ($fleetAction === 'excavate_all' ) { if ($shipStatus === 'IN_TRANSIT' ) { $actionResults[] = $shipSymbol . ': skipped (in transit).'; continue; } if ($shipStatus === 'DOCKED' ) { $client->orbitShip( $shipSymbol ); } $client->extractResources( $shipSymbol ); $actionResults[] = $shipSymbol . ': extracting.'; $successCount++; } elseif ($fleetAction === 'dock_refuel_all' ) { if ($shipStatus === 'IN_TRANSIT' ) { $actionResults[] = $shipSymbol . ': skipped (in transit).'; continue; } if ($shipStatus !== 'DOCKED' ) { $client->dockShip( $shipSymbol ); } $client->refuelShip( $shipSymbol ); $actionResults[] = $shipSymbol . ': docked/refueled.'; $successCount++; } elseif ($fleetAction === 'move_and_sell_all' ) { if ($selectedMarketWaypoint === '' ) { $actionResults[] = $shipSymbol . ': no market waypoint selected.'; continue; } if ($shipStatus === 'IN_TRANSIT' ) { $actionResults[] = $shipSymbol . ': already in transit.'; continue; } if ($waypointSymbol !== $selectedMarketWaypoint ) { if ($shipStatus === 'DOCKED' ) { $client->orbitShip( $shipSymbol ); } $client->navigateShip( $shipSymbol, $selectedMarketWaypoint ); $actionResults[] = $shipSymbol . ': navigating to ' . $selectedMarketWaypoint . '.'; $successCount++; continue; } if ($shipStatus !== 'DOCKED' ) { $client->dockShip( $shipSymbol ); } $inventory = (array) ( $shipData['cargo']['inventory'] ?? array() ); $soldItems = 0; foreach ( $inventory as $item ) { $tradeSymbol = (string) ( $item['symbol'] ?? '' ); $units = (int) ( $item['units'] ?? 0 ); if ($tradeSymbol === '' || $units <= 0 ) { continue; } try { $client->sellCargo( $shipSymbol, $tradeSymbol, $units ); $soldItems++; } catch (SpacetradersApiException $e) { // Ignore individual sell failures and continue. } } $actionResults[] = $shipSymbol . ': sold ' . $soldItems . ' cargo item type(s).'; $successCount++; } elseif ($fleetAction === 'sell_cargo_here_all' ) { if ($shipStatus === 'IN_TRANSIT' ) { $actionResults[] = $shipSymbol . ': skipped (in transit).'; continue; } if ($shipStatus !== 'DOCKED' ) { $client->dockShip( $shipSymbol ); } $inventory = (array) ( $shipData['cargo']['inventory'] ?? array() ); $soldItems = 0; foreach ( $inventory as $item ) { $tradeSymbol = (string) ( $item['symbol'] ?? '' ); $units = (int) ( $item['units'] ?? 0 ); if ($tradeSymbol === '' || $units <= 0 ) { continue; } try { $client->sellCargo( $shipSymbol, $tradeSymbol, $units ); $soldItems++; } catch (SpacetradersApiException $e) { // Continue selling other items. } } $actionResults[] = $shipSymbol . ': sold ' . $soldItems . ' cargo item type(s) at current waypoint.'; $successCount++; } elseif ($fleetAction === 'jettison_cargo_all' ) { if ($shipStatus === 'IN_TRANSIT' ) { $actionResults[] = $shipSymbol . ': skipped (in transit).'; continue; } $inventory = (array) ( $shipData['cargo']['inventory'] ?? array() ); $jettisonedItems = 0; foreach ( $inventory as $item ) { $tradeSymbol = (string) ( $item['symbol'] ?? '' ); $units = (int) ( $item['units'] ?? 0 ); if ($tradeSymbol === '' || $units <= 0 ) { continue; } try { $client->jettisonCargo( $shipSymbol, $tradeSymbol, $units ); $jettisonedItems++; } catch (SpacetradersApiException $e) { // Continue jettisoning other items. } } $actionResults[] = $shipSymbol . ': jettisoned ' . $jettisonedItems . ' cargo item type(s).'; $successCount++; } elseif ($fleetAction === 'navigate_contract_delivery' ) { if (! is_array( $selectedContractDeliveryTarget ) ) { $actionResults[] = $shipSymbol . ': no contract delivery target selected.'; continue; } if ($shipStatus === 'IN_TRANSIT' ) { $actionResults[] = $shipSymbol . ': already in transit.'; continue; } $destinationSymbol = (string) ( $selectedContractDeliveryTarget['destinationSymbol'] ?? '' ); if ($destinationSymbol === '' ) { $actionResults[] = $shipSymbol . ': delivery destination is missing.'; continue; } if ($waypointSymbol !== $destinationSymbol ) { if ($shipStatus === 'DOCKED' ) { $client->orbitShip( $shipSymbol ); } $client->navigateShip( $shipSymbol, $destinationSymbol ); $actionResults[] = $shipSymbol . ': navigating to contract delivery waypoint ' . $destinationSymbol . '.'; $successCount++; } else { $actionResults[] = $shipSymbol . ': already at contract delivery waypoint.'; } } elseif ($fleetAction === 'deliver_contract_goods' ) { if (! is_array( $selectedContractDeliveryTarget ) ) { $actionResults[] = $shipSymbol . ': no contract delivery target selected.'; continue; } if ($shipStatus === 'IN_TRANSIT' ) { $actionResults[] = $shipSymbol . ': skipped (in transit).'; continue; } $contractId = (string) ( $selectedContractDeliveryTarget['contractId'] ?? '' ); $destinationSymbol = (string) ( $selectedContractDeliveryTarget['destinationSymbol'] ?? '' ); $tradeSymbol = (string) ( $selectedContractDeliveryTarget['tradeSymbol'] ?? '' ); if ($contractId === '' || $destinationSymbol === '' || $tradeSymbol === '' ) { $actionResults[] = $shipSymbol . ': selected contract delivery target is incomplete.'; continue; } if ($remainingContractUnits <= 0 ) { $actionResults[] = $shipSymbol . ': nothing remaining to deliver for this target.'; continue; } if ($waypointSymbol !== $destinationSymbol ) { $actionResults[] = $shipSymbol . ': not at delivery waypoint (' . $destinationSymbol . ').'; continue; } if ($shipStatus !== 'DOCKED' ) { $client->dockShip( $shipSymbol ); } $inventory = (array) ( $shipData['cargo']['inventory'] ?? array() ); $availableUnits = 0; foreach ( $inventory as $item ) { if ((string) ( $item['symbol'] ?? '' ) === $tradeSymbol ) { $availableUnits = (int) ( $item['units'] ?? 0 ); break; } } if ($availableUnits <= 0 ) { $actionResults[] = $shipSymbol . ': no ' . $tradeSymbol . ' in cargo.'; continue; } $deliverUnits = min( $availableUnits, $remainingContractUnits ); if ($deliverUnits <= 0 ) { $actionResults[] = $shipSymbol . ': no units available to deliver.'; continue; } $client->deliverContractCargo( $contractId, $shipSymbol, $tradeSymbol, $deliverUnits ); $remainingContractUnits -= $deliverUnits; $actionResults[] = $shipSymbol . ': delivered ' . $deliverUnits . ' of ' . $tradeSymbol . ' for contract ' . $contractId . '.'; $successCount++; } } catch (SpacetradersApiException $e) { $actionResults[] = $shipSymbol . ': ' . $e->getMessage(); } } if ($successCount > 0 ) { $storage->clearAllCache(); } $statusMessage = 'Action complete for ' . $successCount . ' ship(s).'; $shipsResponse = $client->listMyShips(); $contractsResponse = $client->listMyContracts(); $ships = $shipsResponse['data'] ?? $shipsResponse; $contracts = $contractsResponse['data'] ?? $contractsResponse; $miningShips = filterMiningShips( $ships ); $activeContracts = array_values( array_filter( (array) $contracts, static function ( array $contract ): bool { return (bool) ( $contract['accepted'] ?? false ) && ! (bool) ( $contract['fulfilled'] ?? false ); } ) ); $contractDeliveryTargets = buildContractDeliveryTargets( $activeContracts ); $selectedContractDeliveryKey = isset( $_POST['contract_delivery_key'] ) ? trim( (string) $_POST['contract_delivery_key'] ) : (string) ( $contractDeliveryTargets[0]['key'] ?? '' ); } if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_POST['ship_action'] ) && in_array( (string) $_POST['ship_action'], array( 'sell_ship_cargo', 'jettison_ship_cargo' ), true ) && isset( $_POST['ship_symbol'] )) { $shipAction = (string) $_POST['ship_action']; $sellShipSymbol = trim( (string) $_POST['ship_symbol'] ); if ($sellShipSymbol !== '' ) { try { $shipResponse = $client->getShip( $sellShipSymbol ); $shipData = $shipResponse['data'] ?? array(); $shipStatus = (string) ( $shipData['nav']['status'] ?? '' ); if ($shipStatus === 'IN_TRANSIT' ) { throw new SpacetradersApiException( 'Ship is in transit and cargo action is unavailable right now.' ); } if ($shipAction === 'sell_ship_cargo' && $shipStatus !== 'DOCKED' ) { $client->dockShip( $sellShipSymbol ); } $inventory = (array) ( $shipData['cargo']['inventory'] ?? array() ); $handledItems = 0; foreach ( $inventory as $item ) { $tradeSymbol = (string) ( $item['symbol'] ?? '' ); $units = (int) ( $item['units'] ?? 0 ); if ($tradeSymbol === '' || $units <= 0 ) { continue; } try { if ($shipAction === 'sell_ship_cargo' ) { $client->sellCargo( $sellShipSymbol, $tradeSymbol, $units ); } else { $client->jettisonCargo( $sellShipSymbol, $tradeSymbol, $units ); } $handledItems++; } catch (SpacetradersApiException $e) { // Continue processing other items. } } if ($shipAction === 'sell_ship_cargo' ) { $statusMessage = $sellShipSymbol . ': sold ' . $handledItems . ' cargo item type(s).'; } else { $statusMessage = $sellShipSymbol . ': jettisoned ' . $handledItems . ' cargo item type(s).'; } $storage->clearAllCache(); $shipsResponse = $client->listMyShips(); $ships = $shipsResponse['data'] ?? $shipsResponse; $miningShips = filterMiningShips( $ships ); } catch (SpacetradersApiException $e) { $errorMessage = $sellShipSymbol . ': ' . $e->getMessage(); } } } } } catch (SpacetradersApiException $e) { $errorMessage = $e->getMessage(); } /** * Format a string by replacing underscores with spaces and capitalizing the first letter. * * @param string $value The string to format. * @return string The formatted string. */ function formatString( $value ): string { return ucfirst( strtolower( str_replace( '_', ' ', $value ) ) ); } $deliveryReadyByTradeSymbol = array(); foreach ( $miningShips as $miningShip ) { $inventory = (array) ( $miningShip['cargo']['inventory'] ?? array() ); foreach ( $inventory as $item ) { $tradeSymbol = (string) ( $item['symbol'] ?? '' ); $units = (int) ( $item['units'] ?? 0 ); if ($tradeSymbol === '' || $units <= 0 ) { continue; } if (! isset( $deliveryReadyByTradeSymbol[ $tradeSymbol ] ) ) { $deliveryReadyByTradeSymbol[ $tradeSymbol ] = 0; } $deliveryReadyByTradeSymbol[ $tradeSymbol ] += $units; } } ?> Spacetraders - Mining Fleet

Spacetraders - Mining Fleet

Global Controls

Action Results

Mining Ships:

Active Contracts

No active contracts.

Contract:

Type:

Deadline:

Payment: +

Deliveries:

  • : / to | Ready: $deliverableNow ) : ?> ( in cargo)
No mining ships found (role: EXCAVATOR).

Status:

Waypoint:

Fuel: /

Cargo: /

Navigation Timer:

Cooldown:

Cargo Contents:

  • :