diff --git a/all-markets.php b/all-markets.php new file mode 100644 index 0000000..b7bbea1 --- /dev/null +++ b/all-markets.php @@ -0,0 +1,635 @@ + + * @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'; + +/** + * Resolve the active system symbol. + * + * @param array> $ships Ship records. + * @param array $agent Agent record. + * + * @return string + */ +function resolveCurrentSystemSymbol( array $ships, array $agent ): string { + if (! empty( $ships ) ) { + $shipSystem = (string) ( $ships[0]['nav']['systemSymbol'] ?? '' ); + if ($shipSystem !== '' ) { + return $shipSystem; + } + } + + $headquarters = (string) ( $agent['headquarters'] ?? '' ); + if ($headquarters === '' ) { + return ''; + } + + $hqParts = explode( '-', $headquarters ); + if (count( $hqParts ) < 2 ) { + return ''; + } + + return $hqParts[0] . '-' . $hqParts[1]; +} + +/** + * Collect all marketplace waypoints in a system. + * + * @param SpacetradersApi $client API client. + * @param string $systemSymbol System symbol. + * + * @return array + */ +function collectMarketplaceWaypoints( SpacetradersApi $client, string $systemSymbol ): array { + $waypoints = array(); + $page = 1; + $total = 0; + + do { + $waypointsResponse = $client->listWaypoints( + $systemSymbol, + array( + 'page' => $page, + 'limit' => 20, + ) + ); + + $pageData = $waypointsResponse['data'] ?? array(); + if (! is_array( $pageData ) || empty( $pageData ) ) { + break; + } + + foreach ( $pageData as $waypoint ) { + $waypointSymbol = (string) ( $waypoint['symbol'] ?? '' ); + if ($waypointSymbol === '' ) { + continue; + } + + foreach ( (array) ( $waypoint['traits'] ?? array() ) as $trait ) { + if ((string) ( $trait['symbol'] ?? '' ) === 'MARKETPLACE' ) { + $waypoints[] = $waypointSymbol; + break; + } + } + } + + $total = (int) ( $waypointsResponse['meta']['total'] ?? count( $waypoints ) ); + $page++; + } while ( ( $page - 1 ) * 20 < $total ); + + sort( $waypoints ); + return array_values( array_unique( $waypoints ) ); +} + +/** + * Find candidate probe ships. + * + * @param array> $ships Ship records. + * + * @return array> + */ +function getProbeShips( array $ships ): array { + $probeShips = array(); + + foreach ( $ships as $ship ) { + $frameSymbol = strtoupper( (string) ( $ship['frame']['symbol'] ?? '' ) ); + $role = strtoupper( (string) ( $ship['registration']['role'] ?? '' ) ); + + if (str_contains( $frameSymbol, 'PROBE' ) || $role === 'SATELLITE' ) { + $probeShips[] = $ship; + } + } + + return $probeShips; +} + +/** + * Convert market payload into export rows including prices. + * + * @param array $marketData Market payload. + * + * @return array> + */ +function buildExportsWithPrices( array $marketData ): array { + $exports = (array) ( $marketData['exports'] ?? array() ); + $tradeGoods = (array) ( $marketData['tradeGoods'] ?? array() ); + $tradeGoodsBySymbol = array(); + + foreach ( $tradeGoods as $tradeGood ) { + $symbol = (string) ( $tradeGood['symbol'] ?? '' ); + if ($symbol !== '' ) { + $tradeGoodsBySymbol[ $symbol ] = $tradeGood; + } + } + + $rows = array(); + foreach ( $exports as $export ) { + $symbol = (string) ( $export['symbol'] ?? '' ); + if ($symbol === '' ) { + continue; + } + + $tradeGood = (array) ( $tradeGoodsBySymbol[ $symbol ] ?? array() ); + $rows[] = array( + 'symbol' => $symbol, + 'purchasePrice' => (int) ( $tradeGood['purchasePrice'] ?? 0 ), + 'sellPrice' => (int) ( $tradeGood['sellPrice'] ?? 0 ), + 'supply' => (string) ( $tradeGood['supply'] ?? '' ), + ); + } + + usort( + $rows, + static function ( array $left, array $right ): int { + return strcmp( + (string) ( $left['symbol'] ?? '' ), + (string) ( $right['symbol'] ?? '' ) + ); + } + ); + + return $rows; +} + +/** + * Scan one full probe step: scan current market (if marketplace), then navigate to next target. + * + * @param SpacetradersApi $client API client. + * @param SpacetradersStorage $storage Storage handler. + * @param string $systemSymbol System symbol. + * @param string $probeShipSymbol Probe ship symbol. + * @param array $marketWaypoints Marketplace waypoints. + * + * @return array + */ +function runProbeMarketScanStep( + SpacetradersApi $client, + SpacetradersStorage $storage, + string $systemSymbol, + string $probeShipSymbol, + array $marketWaypoints +): array { + $messages = array(); + + if ($probeShipSymbol === '' ) { + return array( + 'messages' => array( 'No probe ship selected.' ), + 'error' => true, + ); + } + + if (empty( $marketWaypoints ) ) { + return array( + 'messages' => array( 'No marketplace waypoints were found in this system.' ), + 'error' => true, + ); + } + + $probeResponse = $client->getShip( $probeShipSymbol ); + $probeData = (array) ( $probeResponse['data'] ?? array() ); + $shipStatus = (string) ( $probeData['nav']['status'] ?? '' ); + $currentWaypoint = (string) ( $probeData['nav']['waypointSymbol'] ?? '' ); + + if ($shipStatus === 'IN_TRANSIT' ) { + $arrival = (string) ( $probeData['nav']['route']['arrival'] ?? '' ); + $messages[] = $arrival !== '' ? + 'Probe is already in transit. Arrival: ' . date( 'Y-m-d H:i:s', strtotime( $arrival ) ) . '.' : + 'Probe is already in transit.'; + return array( + 'messages' => $messages, + 'error' => false, + ); + } + + $currentIndex = array_search( $currentWaypoint, $marketWaypoints, true ); + + if ($currentIndex === false ) { + $firstWaypoint = (string) ( $marketWaypoints[0] ?? '' ); + if ($firstWaypoint === '' ) { + return array( + 'messages' => array( 'No marketplace waypoints were found in this system.' ), + 'error' => true, + ); + } + + if ($shipStatus === 'DOCKED' ) { + $client->orbitShip( $probeShipSymbol ); + } + + $client->navigateShip( $probeShipSymbol, $firstWaypoint ); + $storage->clearAllCache(); + $messages[] = 'Probe navigating to ' . $firstWaypoint . '. Run scan again after arrival.'; + + return array( + 'messages' => $messages, + 'error' => false, + ); + } + + if ($shipStatus !== 'DOCKED' ) { + $client->dockShip( $probeShipSymbol ); + $shipStatus = 'DOCKED'; + } + + try { + $marketResponse = $client->getWaypointMarket( $systemSymbol, $currentWaypoint ); + $marketData = (array) ( $marketResponse['data'] ?? array() ); + $exports = buildExportsWithPrices( $marketData ); + + $storage->upsertMarketScanWaypoint( + $systemSymbol, + $currentWaypoint, + $exports, + $probeShipSymbol, + '' + ); + + $messages[] = 'Scanned ' . $currentWaypoint . ' (' . count( $exports ) . ' exports).'; + } catch (SpacetradersApiException $e) { + $storage->upsertMarketScanWaypoint( + $systemSymbol, + $currentWaypoint, + array(), + $probeShipSymbol, + $e->getMessage() + ); + $messages[] = 'Scan failed at ' . $currentWaypoint . ': ' . $e->getMessage(); + } + + if (count( $marketWaypoints ) <= 1 ) { + $messages[] = 'Scan cycle complete for all marketplace waypoints in ' . $systemSymbol . '.'; + return array( + 'messages' => $messages, + 'error' => false, + ); + } + + $nextIndex = ( (int) $currentIndex + 1 ) % count( $marketWaypoints ); + $nextWaypoint = (string) ( $marketWaypoints[ $nextIndex ] ?? '' ); + + if ($nextWaypoint !== '' && $nextWaypoint !== $currentWaypoint ) { + if ($shipStatus === 'DOCKED' ) { + $client->orbitShip( $probeShipSymbol ); + } + + $client->navigateShip( $probeShipSymbol, $nextWaypoint ); + $storage->clearAllCache(); + $messages[] = 'Probe navigating to ' . $nextWaypoint . '. Run scan again after arrival.'; + } else { + $messages[] = 'Scan cycle complete for all marketplace waypoints in ' . $systemSymbol . '.'; + } + + return array( + 'messages' => $messages, + 'error' => false, + ); +} + +$config = require __DIR__ . '/lib/project-config.php'; + +$storage = new SpacetradersStorage( $config['db_path'] ); +$token = $storage->getAgentToken(); + +$statusMessages = array(); +$errorMessage = ''; +$agent = array(); +$ships = array(); +$probeShips = array(); +$selectedProbeSymbol = ''; +$currentSystemSymbol = ''; +$marketWaypoints = array(); +$scanRows = array(); +$itemSearch = trim( (string) ( $_GET['item'] ?? '' ) ); + +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(); + + $agent = $agentResponse['data'] ?? $agentResponse; + $ships = $shipsResponse['data'] ?? $shipsResponse; + $probeShips = getProbeShips( $ships ); + $currentSystemSymbol = resolveCurrentSystemSymbol( $ships, $agent ); + + foreach ( $probeShips as $probeShip ) { + $probeSymbol = (string) ( $probeShip['symbol'] ?? '' ); + if ($probeSymbol !== '' ) { + $selectedProbeSymbol = $probeSymbol; + break; + } + } + + if (isset( $_POST['probe_ship_symbol'] ) ) { + $postedProbeSymbol = trim( (string) $_POST['probe_ship_symbol'] ); + foreach ( $probeShips as $probeShip ) { + if ((string) ( $probeShip['symbol'] ?? '' ) === $postedProbeSymbol ) { + $selectedProbeSymbol = $postedProbeSymbol; + break; + } + } + } + + if ($currentSystemSymbol !== '' ) { + $marketWaypoints = collectMarketplaceWaypoints( $client, $currentSystemSymbol ); + } + + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset( $_POST['run_probe_scan'] ) ) { + $scanResult = runProbeMarketScanStep( + $client, + $storage, + $currentSystemSymbol, + $selectedProbeSymbol, + $marketWaypoints + ); + + foreach ( (array) ( $scanResult['messages'] ?? array() ) as $message ) { + $statusMessages[] = (string) $message; + } + + if ((bool) ( $scanResult['error'] ?? false ) ) { + $errorMessage = ! empty( $statusMessages ) ? $statusMessages[0] : 'Probe scan failed.'; + $statusMessages = array(); + } else { + $shipsResponse = $client->listMyShips(); + $ships = $shipsResponse['data'] ?? $shipsResponse; + $probeShips = getProbeShips( $ships ); + } + } + + if ($currentSystemSymbol !== '' ) { + $scanRows = $storage->getMarketScanWaypointsBySystem( $currentSystemSymbol ); + } + } +} catch (SpacetradersApiException $e) { + $errorMessage = $e->getMessage(); +} + +$scanRowsByWaypoint = array(); +foreach ( $scanRows as $scanRow ) { + $scanWaypointSymbol = (string) ( $scanRow['waypoint_symbol'] ?? '' ); + if ($scanWaypointSymbol !== '' ) { + $scanRowsByWaypoint[ $scanWaypointSymbol ] = $scanRow; + } +} + +$marketCards = array(); +foreach ( $marketWaypoints as $waypointSymbol ) { + $scanRow = (array) ( $scanRowsByWaypoint[ $waypointSymbol ] ?? array() ); + $exports = (array) ( $scanRow['exports'] ?? array() ); + $matchesFilter = true; + + if ($itemSearch !== '' ) { + $matchesFilter = false; + $needle = strtoupper( $itemSearch ); + + foreach ( $exports as $export ) { + $symbol = strtoupper( (string) ( $export['symbol'] ?? '' ) ); + if ($symbol !== '' && str_contains( $symbol, $needle ) ) { + $matchesFilter = true; + break; + } + } + } + + if (! $matchesFilter ) { + continue; + } + + $marketCards[] = array( + 'waypointSymbol' => $waypointSymbol, + 'exports' => $exports, + 'updatedAt' => (int) ( $scanRow['updated_at'] ?? 0 ), + 'error' => (string) ( $scanRow['error_message'] ?? '' ), + ); +} + +$selectedProbeShip = array(); +foreach ( $probeShips as $probeShip ) { + if ((string) ( $probeShip['symbol'] ?? '' ) === $selectedProbeSymbol ) { + $selectedProbeShip = $probeShip; + break; + } +} + +$selectedProbeStatus = (string) ( $selectedProbeShip['nav']['status'] ?? '' ); +$selectedProbeArrivalIso = (string) ( $selectedProbeShip['nav']['route']['arrival'] ?? '' ); +$selectedProbeWaypoint = (string) ( $selectedProbeShip['nav']['waypointSymbol'] ?? '' ); +?> + + + + + + + Spacetraders - All Markets + + + + + + +

Spacetraders - All Markets

+ +

+ Back to Markets +

+ + +
+ +
+ + + + + + +
+ +
+ + + +
+ +

+ +
+ + +
+

System:

+

Marketplace Waypoints:

+

Probe Ships:

+
+ +
+ +
+ + +
+ +
+ + +
+

Selected Probe:

+

Current Waypoint:

+

+ Transit Timer: + +

+
+ + +
+
+ + +
+ + Clear +
+ + +
+ No market cards match the current filters. Run a scan and/or broaden your search. +
+ +
+ + +
+

+

+ Last Scan: + 0 ? htmlspecialchars( date( 'Y-m-d H:i:s', $cardUpdatedAt ) ) : 'Not scanned yet'; ?> +

+ + +

+ + + +

No export price data yet.

+ +
    + +
  • +

    +

    Buy:

    +

    Sell:

    +
  • + +
+ +
+ +
+ + + + + diff --git a/lib/spacetraders-storage.php b/lib/spacetraders-storage.php index 4db53b4..a8b7a99 100644 --- a/lib/spacetraders-storage.php +++ b/lib/spacetraders-storage.php @@ -68,6 +68,23 @@ class SpacetradersStorage { 'CREATE INDEX IF NOT EXISTS idx_api_cache_expires ON api_cache (expires_at)' ); + + $this->db->exec( + 'CREATE TABLE IF NOT EXISTS market_scan_waypoints ( + system_symbol TEXT NOT NULL, + waypoint_symbol TEXT NOT NULL, + exports_json TEXT NOT NULL, + probe_ship_symbol TEXT NOT NULL, + error_message TEXT NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (system_symbol, waypoint_symbol) + )' + ); + + $this->db->exec( + 'CREATE INDEX IF NOT EXISTS idx_market_scan_system_updated + ON market_scan_waypoints (system_symbol, updated_at)' + ); } /** @@ -243,4 +260,99 @@ class SpacetradersStorage { public function clearAllCache(): void { $this->db->exec( 'DELETE FROM api_cache' ); } + + /** + * Store scanned export data for a marketplace waypoint. + * + * @param string $systemSymbol System symbol. + * @param string $waypointSymbol Waypoint symbol. + * @param array> $exports Scanned export records. + * @param string $probeShipSymbol Probe ship symbol used for scan. + * @param string $errorMessage Optional scan error message. + * + * @return void + */ + public function upsertMarketScanWaypoint( + string $systemSymbol, + string $waypointSymbol, + array $exports, + string $probeShipSymbol = '', + string $errorMessage = '' + ): void { + $exportsJson = json_encode( array_values( $exports ) ); + if ($exportsJson === false ) { + return; + } + + $stmt = $this->db->prepare( + 'INSERT INTO market_scan_waypoints ( + system_symbol, + waypoint_symbol, + exports_json, + probe_ship_symbol, + error_message, + updated_at + ) + VALUES ( + :system_symbol, + :waypoint_symbol, + :exports_json, + :probe_ship_symbol, + :error_message, + :updated_at + ) + ON CONFLICT(system_symbol, waypoint_symbol) DO UPDATE SET + exports_json = excluded.exports_json, + probe_ship_symbol = excluded.probe_ship_symbol, + error_message = excluded.error_message, + updated_at = excluded.updated_at' + ); + + $stmt->execute( + array( + ':system_symbol' => $systemSymbol, + ':waypoint_symbol' => $waypointSymbol, + ':exports_json' => $exportsJson, + ':probe_ship_symbol' => $probeShipSymbol, + ':error_message' => $errorMessage, + ':updated_at' => time(), + ) + ); + } + + /** + * Load scanned market export data for a system. + * + * @param string $systemSymbol System symbol. + * + * @return array> + */ + public function getMarketScanWaypointsBySystem( string $systemSymbol ): array { + $stmt = $this->db->prepare( + 'SELECT + system_symbol, + waypoint_symbol, + exports_json, + probe_ship_symbol, + error_message, + updated_at + FROM market_scan_waypoints + WHERE system_symbol = :system_symbol + ORDER BY waypoint_symbol ASC' + ); + $stmt->execute( array( ':system_symbol' => $systemSymbol ) ); + $rows = $stmt->fetchAll(); + + if (! is_array( $rows ) ) { + return array(); + } + + foreach ( $rows as &$row ) { + $decodedExports = json_decode( (string) ( $row['exports_json'] ?? '[]' ), true ); + $row['exports'] = is_array( $decodedExports ) ? $decodedExports : array(); + } + unset( $row ); + + return $rows; + } } diff --git a/main-menu.php b/main-menu.php index 7ba02e1..c07b853 100644 --- a/main-menu.php +++ b/main-menu.php @@ -68,6 +68,8 @@ try { | Markets | + All Markets + | Buy Ships | Mining Fleet diff --git a/market.php b/market.php index 106a75f..98185a3 100644 --- a/market.php +++ b/market.php @@ -137,6 +137,10 @@ try {

Spacetraders - Markets

+

+ View All Markets +

+