$rig->loading(), 'telemetry' => $rig->telemetry(), 'trackCount' => $rig->playlistCount(), 'twitchEnabled' => $twitch->enabled(), 'oldenEra' => [ 'gameId' => OldenEra::GAME_ID, 'assetBase' => OldenEra::URL, 'catalog' => $olden->catalog(), 'manifest' => $olden->manifest(), ], ]); } public function search(Request $request, TwitchHelix $twitch): JsonResponse { if (!$twitch->enabled()) { return response()->json(['error' => 'twitch not configured'], 503); } $q = trim((string) $request->query('q', '')); if (mb_strlen($q) < 2) { return response()->json(['matches' => []]); } try { // Use the HTTP API directly to get the top-N candidates, not just // the single best-match the Artisan command uses. $hits = $this->rawSearch($twitch, $q); } catch (Throwable $e) { return response()->json(['error' => $e->getMessage()], 502); } return response()->json(['matches' => $hits]); } public function submit(Request $request, RigData $rig, TwitchHelix $twitch, OldenEra $olden): JsonResponse { $data = $request->validate([ 'game' => 'required|string|max:200', 'gameId' => 'nullable|string|max:32', 'subtitle' => 'nullable|string|max:300', 'countdownMin' => 'required|integer|min:0|max:120', 'camera' => 'required|boolean', 'microphone' => 'required|boolean', 'pushTwitch' => 'nullable|boolean', ]); $manifest = [ 'compiledAt' => gmdate('Y-m-d\TH:i:s\Z'), 'game' => $data['game'], 'gameId' => $data['gameId'] ?? null, 'subtitle' => $data['subtitle'] ?: null, 'countdownMin' => (int) $data['countdownMin'], 'camera' => (bool) $data['camera'], 'microphone' => (bool) $data['microphone'], ]; $path = rtrim(config('rig.storage_data'), '/') . '/loading.json'; @mkdir(dirname($path), 0775, true); file_put_contents( $path, json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n" ); $oldenPath = null; if (($manifest['gameId'] ?? null) === OldenEra::GAME_ID) { $factions = array_keys($olden->catalog()); $extra = $request->validate([ 'olden.color' => 'required|in:red,blue', 'olden.my.faction' => 'required|string|in:' . implode(',', $factions), 'olden.my.hero' => 'required|string|max:120', 'olden.enemy.faction' => 'required|string|in:' . implode(',', $factions), 'olden.enemy.hero' => 'required|string|max:120', 'olden.map' => 'nullable|string|max:200', ])['olden']; $oldenPath = $olden->writeManifest([ 'compiledAt' => gmdate('Y-m-d\TH:i:s\Z'), 'gameId' => OldenEra::GAME_ID, 'color' => $extra['color'], 'my' => $extra['my'], 'enemy' => $extra['enemy'], 'map' => isset($extra['map']) && $extra['map'] !== '' ? $extra['map'] : null, ]); } $twitchStatus = null; if (!empty($data['pushTwitch']) && $twitch->enabled() && !empty($manifest['gameId'])) { try { $title = $manifest['subtitle'] ?: $manifest['game']; $twitch->setChannel($title, $manifest['gameId']); $twitchStatus = "ok: \"{$title}\" / {$manifest['game']}"; } catch (Throwable $e) { $twitchStatus = 'failed: ' . $e->getMessage(); } } return response()->json([ 'ok' => true, 'manifest' => $manifest, 'twitch' => $twitchStatus, 'path' => str_replace(base_path() . '/', '', $path), 'oldenPath' => $oldenPath ? str_replace(base_path() . '/', '', $oldenPath) : null, ]); } /** * Raw search/categories Helix call — same shape as TwitchHelix internally * but returns multiple candidates instead of the first exact match. */ private function rawSearch(TwitchHelix $twitch, string $q): array { $reflection = new \ReflectionClass($twitch); $http = $reflection->getProperty('http')->getValue($twitch); $headers = $reflection->getMethod('headers'); $headers->setAccessible(true); $res = $http->get('search/categories', [ 'headers' => $headers->invoke($twitch), 'query' => ['query' => $q, 'first' => 10], ]); $body = json_decode((string) $res->getBody(), true); $hits = $body['data'] ?? []; // Sort so case-insensitive exact matches surface first. usort($hits, function ($a, $b) use ($q) { $ax = strcasecmp((string) ($a['name'] ?? ''), $q) === 0 ? 0 : 1; $bx = strcasecmp((string) ($b['name'] ?? ''), $q) === 0 ? 0 : 1; return $ax <=> $bx; }); return array_map(fn($r) => [ 'id' => (string) ($r['id'] ?? ''), 'name' => (string) ($r['name'] ?? ''), 'boxArt' => (string) ($r['box_art_url'] ?? ''), ], array_slice($hits, 0, 10)); } }