diff --git a/public/booru.php b/public/booru.php index 601450c..8961584 100644 --- a/public/booru.php +++ b/public/booru.php @@ -1,7 +1,4 @@ 'eu', 'JP' => 'jp', 'US' => 'na', +]); +define('SP2_FEST_REGIONS_ALIAS_REVERSE', [ + 'eu' => 'EU', 'jp' => 'JP', 'na' => 'US', +]); +define('SP2_LOCALES_ALIAS', [ + 'en-US' => 'en', 'en-GB' => 'en', + 'es-ES' => 'es', 'es-MX' => 'es', + 'fr-FR' => 'fr', 'fr-CA' => 'fr', + 'de-DE' => 'de', 'nl-NL' => 'nl', + 'it-IT' => 'it', 'ru-RU' => 'ru', + 'ja-JP' => 'ja', +]); + +define('SP_GAME_SP3', 's3'); +define('SP_GAME_SP2', 's2'); +define('SP_GAMES', [SP_GAME_SP3, SP_GAME_SP2]); + +define('SP_GAME_INFOS', [ + SP_GAME_SP3 => [ + 'name' => 'Splatoon 3', + 'colour' => 0xFFEFFE65, + 'variant' => SP_GAME_SP3, + 'types' => SP3_TYPES, + 'locales' => SP3_LOCALES, + 'locale' => SP3_DEFAULT_LOCALE, + 'localeUrlFormat' => SP3_LOCALE, + 'schedUrls' => ['all' => SP3_SCHED], + 'schedFilters' => SP3_SCHED_FILTER, + 'festUrl' => SP3_FESTS, + 'festRegions' => SP3_FEST_REGIONS, + ], + SP_GAME_SP2 => [ + 'name' => 'Splatoon 2', + 'colour' => 0xFFFF4506, + 'variant' => SP_GAME_SP2, + 'types' => SP2_TYPES, + 'locales' => SP2_LOCALES, + 'localeAliases' => SP2_LOCALES_ALIAS, + 'locale' => SP2_DEFAULT_LOCALE, + 'localeUrlFormat' => SP2_LOCALE, + 'schedUrls' => ['vs' => SP2_SCHED, 'co' => SP2_COOP_SCHED], + 'schedFilters' => SP2_SCHED_FILTER, + 'festUrl' => SP2_FESTS, + 'festRegions' => SP2_FEST_REGIONS, + 'festRegionAliases' => SP2_FEST_REGIONS_ALIAS, + 'festRegionAliasesReverse' => SP2_FEST_REGIONS_ALIAS_REVERSE, + ], +]); + +function httpRequest(string $url): mixed { + $curl = curl_init($url); + curl_setopt_array($curl, [ + CURLOPT_AUTOREFERER => true, + CURLOPT_CERTINFO => false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + CURLOPT_PATH_AS_IS => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 2, + CURLOPT_TIMEOUT => 5, + CURLOPT_USERAGENT => 'Satori/20230203 (+https://fii.moe/satori)', + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + $response = json_decode(curl_exec($curl)); + curl_close($curl); + + return $response; +} + +function cached(string $name, callable $match, callable $create): mixed { + $fileName = sys_get_temp_dir() . '/satori-splatoon-' . $name . '.tmp'; + + if(is_file($fileName) && $match(filemtime($fileName), $fileName)) + return unserialize(file_get_contents($fileName)); + + $output = $create(); + file_put_contents($fileName, serialize($output)); + return $output; +} + +function cache_lifetime_15min($mtime): bool { + return is_int($mtime) + && floor($mtime / 900) >= floor(time() / 900); +} + +function splatoon2_format_date(int $time): string { + return str_replace('+00:00', 'Z', date(DATE_ISO8601_EXPANDED, $time)); +} + +function splatoon2_translate_ruleset(string $id): string { + if($id === 'tower_control') + return 'LOFT'; + if($id === 'splat_zones') + return 'AREA'; + if($id === 'clam_blitz') + return 'CLAM'; + if($id === 'rainmaker') + return 'GOAL'; + if($id === 'turf_war') + return 'TURF_WAR'; + return $id; +} + +function splatoon2_get_mode_colour(string $id): int { + if($id === 'regular') + return 0xFF19D719; + if($id === 'gachi') + return 0xFFF54910; + if($id === 'league') + return 0xFFF02D7D; + if($id === 'coop') + return 0xFFFF5600; + return 0xFFFFFFFF; +} + +function splatoon2_ruleset_short(string $id): string { + if($id === 'tower_control') + return 'TC'; + if($id === 'splat_zones') + return 'SZ'; + if($id === 'clam_blitz') + return 'CB'; + if($id === 'rainmaker') + return 'RM'; + if($id === 'turf_war') + return 'TW'; + if($id === 'coop') + return 'SR'; + return $id; +} + +function splatoon3_ruleset_short(string $id): string { + if($id === 'LOFT') + return 'TC'; + if($id === 'AREA') + return 'SZ'; + if($id === 'CLAM') + return 'CB'; + if($id === 'GOAL') + return 'RM'; + if($id === 'TURF_WAR') + return 'TW'; + if($id === 'coop') + return 'SR'; + if($id === 'bigrun') + return 'BR'; + return $id; +} +date_default_timezone_set('utc'); +header('Content-Type: application/json; charset=utf-8'); + +$gameId = (string)filter_input(INPUT_GET, 'g'); +if(!in_array($gameId, SP_GAMES)) + die('{"error":"game:id"}'); + +$gameInfo = SP_GAME_INFOS[$gameId] ?? null; +if(!isset($gameInfo)) + die('{"error":"game:info"}'); + +$typeId = (string)filter_input(INPUT_GET, 't'); +if(empty($typeId) || !in_array($typeId, $gameInfo['types'])) + die('{"error":"type:id"}'); + +$localeId = (string)filter_input(INPUT_GET, 'l'); +if(empty($localeId)) + $localeId = $gameInfo['locale']; +elseif(!in_array($localeId, $gameInfo['locales'])) { + if(isset($gameInfo['localeAliases']) && array_key_exists($localeId, $gameInfo['localeAliases'])) + $localeId = $gameInfo['localeAliases'][$localeId]; + else die('{"error":"locale:id"}'); +} + +$localeInfo = cached(sprintf('%s-locale-%s', $gameId, $localeId), 'cache_lifetime_15min', function() use($gameInfo, $localeId) { + if(empty($gameInfo['localeUrlFormat'])) + die('{"error":"locale:url"}'); + + $localeInfo = httpRequest(sprintf($gameInfo['localeUrlFormat'], $localeId)); + if(!is_object($localeInfo)) + die('{"error":"locale:info"}'); + + return $localeInfo; +}); + +if($typeId === SP_TYPE_SCHED) { // SCHEDULE SHIT + $filters = array_filter(explode(',', (string)filter_input(INPUT_GET, 'sf'))); + $filterAll = empty($filters); + foreach($filters as $filter) + if(!in_array($filter, $gameInfo['schedFilters'])) + die('{"error":"schedules:filter"}'); + + $schedRaw = (object)cached(sprintf('%s-sched', $gameId), 'cache_lifetime_15min', function() use($gameInfo) { + if(empty($gameInfo['schedUrls'])) + die('{"error":"schedules:url"}'); + + $output = []; + foreach($gameInfo['schedUrls'] as $name => $url) + $output[$name] = httpRequest($url); + + return $output; + }); + + $gameModes = []; + $schedules = []; + + if($gameInfo['variant'] === SP_GAME_SP3) { + $data = $schedRaw->all->data; + + $includeRegular = $filterAll || in_array('regular', $filters); + $includeSeries = $filterAll || in_array('series', $filters); + $includeOpen = $filterAll || in_array('open', $filters); + $includeRanked = $filterAll || in_array('bankara', $filters); + $includeX = $filterAll || in_array('x', $filters); + $includeLeague = $filterAll || in_array('league', $filters); + $includeSalmon = $filterAll || in_array('coop', $filters); + $includeBigRun = $filterAll || in_array('bigrun', $filters); + $includeFest = $filterAll || in_array('fest', $filters); + $includeSeries = $includeSeries || $includeRanked; + $includeOpen = $includeOpen || $includeRanked; + $includeRanked = $includeSeries || $includeOpen; + + function splatoon3_schedule_gen($target, $node): void { + $target->start = $node->startTime; + $target->end = $node->endTime; + } + + function splatoon3_schedule_vs($target, $settings): void { + global $localeInfo; + + $target->variant = 'vs'; + + $target->ruleset = [ + 'id' => $settings->vsRule->rule, + 'name' => $localeInfo->rules->{$settings->vsRule->id}?->name ?? $settings->vsRule->name, + 'short' => splatoon3_ruleset_short($settings->vsRule->rule), + ]; + + $target->stages = []; + foreach($settings->vsStages as $stage) + $target->stages[] = [ + 'id' => (string)$stage->vsStageId, + 'name' => $localeInfo->stages->{$stage->id}?->name ?? $stage->name, + 'image' => $stage->image->url, + ]; + } + + if($includeRegular && !empty($data->regularSchedules->nodes)) { + $schedule = []; + + foreach($data->regularSchedules->nodes as $node) { + if(empty($node->regularMatchSetting)) + continue; + + $schedule[] = $item = new stdClass; + splatoon3_schedule_gen($item, $node); + splatoon3_schedule_vs($item, $node->regularMatchSetting); + } + + if(!empty($schedule)) { + $gameModes[] = [ + 'id' => 'regular', + 'name' => 'Regular Battle', + 'colour' => 0xFF19D719, + ]; + + $schedules[] = [ + 'mode' => 'regular', + 'schedule' => $schedule, + ]; + } + } + + if($includeRanked && !empty($data->bankaraSchedules->nodes)) { + $series = []; + $open = []; + + foreach($data->bankaraSchedules->nodes as $node) { + if(empty($node->bankaraMatchSettings)) + continue; + + foreach($node->bankaraMatchSettings as $settings) { + if($settings->mode === 'CHALLENGE') { + $series[] = $item = new stdClass; + } elseif($settings->mode === 'OPEN') { + $open[] = $item = new stdClass; + } else continue; + + splatoon3_schedule_gen($item, $node); + splatoon3_schedule_vs($item, $settings); + } + } + + if($includeSeries && !empty($series)) { + if(empty($gameModes['bankara'])) + $gameModes['bankara'] = [ + 'id' => 'bankara', + 'name' => 'Anarchy Battle', + 'colour' => 0xFFF54910, + 'variants' => [], + ]; + $gameModes['bankara']['variants'][] = [ + 'id' => 'CHALLENGE', + 'name' => 'Series', + 'colour' => 0xFF603BFF, + ]; + + $schedules[] = [ + 'mode' => 'bankara', + 'variant' => 'CHALLENGE', + 'schedule' => $series, + ]; + } + + if($includeOpen && !empty($open)) { + if(empty($gameModes['bankara'])) + $gameModes['bankara'] = [ + 'id' => 'bankara', + 'name' => 'Anarchy Battle', + 'colour' => 0xFFF54910, + 'variants' => [], + ]; + $gameModes['bankara']['variants'][] = [ + 'id' => 'OPEN', + 'name' => 'Open', + 'colour' => 0xFF603BFF, + ]; + + $schedules[] = [ + 'mode' => 'bankara', + 'variant' => 'OPEN', + 'schedule' => $open, + ]; + } + } + + if($includeX && !empty($data->xSchedules->nodes)) { + $schedule = []; + + foreach($data->xSchedules->nodes as $node) { + if(empty($node->xMatchSetting)) + continue; + + $schedule[] = $item = new stdClass; + splatoon3_schedule_gen($item, $node); + splatoon3_schedule_vs($item, $node->xMatchSetting); + } + + if(!empty($schedule)) { + $gameModes[] = [ + 'id' => 'x', + 'name' => 'X Battle', + 'colour' => 0xFF0FDB9B, + ]; + + $schedules[] = [ + 'mode' => 'x', + 'schedule' => $schedule, + ]; + } + } + + if($includeLeague && !empty($data->leagueSchedules->nodes)) { + $schedule = []; + + foreach($data->leagueSchedules->nodes as $node) { + if(empty($node->leagueMatchSetting)) + continue; + + $schedule[] = $item = new stdClass; + splatoon3_schedule_gen($item, $node); + splatoon3_schedule_vs($item, $node->leagueMatchSetting); + } + + if(!empty($schedule)) { + $gameModes[] = [ + 'id' => 'league', + 'name' => 'League Battle', + 'colour' => 0xFFF02D7D, + ]; + + $schedules[] = [ + 'mode' => 'league', + 'schedule' => $schedule, + ]; + } + } + + if($includeFest && !empty($data->festSchedules->nodes)) { + $schedule = []; + + // i have no idea what the format of this is gonna be yet + // implement this when there's a fest again lol + /*foreach($data->festSchedules->nodes as $node) { + if(empty($node->festMatchSetting)) + continue; + + $schedule[] = $item = new stdClass; + splatoon3_schedule_gen($item, $node); + splatoon3_schedule_vs($item, $node->festMatchSetting); + }*/ + + if(!empty($schedule)) { + $gameModes[] = [ + 'id' => 'fest', + 'name' => 'Splatfest Battle', + 'colour' => 0xFF71717A, + ]; + + $schedules[] = [ + 'mode' => 'fest', + 'schedule' => $schedule, + ]; + } + } + + if($includeSalmon && !empty($data->coopGroupingSchedule->regularSchedules->nodes)) { + $schedule = []; + + $nodes = $data->coopGroupingSchedule->regularSchedules->nodes; + foreach($nodes as $node) { + if(empty($node->setting)) + continue; + + $schedule[] = $item = new stdClass; + splatoon3_schedule_gen($item, $node); + $item->variant = 'coop'; + + $item->stages = [ + [ + 'name' => $node->setting->coopStage->name, + 'image' => $node->setting->coopStage->image->url, + ], + ]; + + $item->weapons = []; + foreach($node->setting->weapons as $weapon) + $item->weapons[] = [ + 'name' => $localeInfo->weapons->{$weapon->__splatoon3ink_id}?->name ?? $weapon->name, + 'image' => $weapon->image->url, + ]; + } + + if(!empty($schedule)) { + $gameModes[] = [ + 'id' => 'coop', + 'name' => 'Salmon Run', + 'colour' => 0xFFFF5600, + ]; + + $schedules[] = [ + 'mode' => 'coop', + 'schedule' => $schedule, + ]; + } + } + + if($includeBigRun && !empty($data->coopGroupingSchedule->bigRunSchedules->nodes)) { + $schedule = []; + + // i have no idea what the format of this is gonna be yet + // implement this when there's a big run again lol + $nodes = $data->coopGroupingSchedule->bigRunSchedules->nodes; + + if(!empty($schedule)) { + $gameModes[] = [ + 'id' => 'bigrun', + 'name' => 'Big Run', + 'colour' => 0xFFFF5600, // reinvestigate this + ]; + + $schedule[] = [ + 'mode' => 'bigrun', + 'schedule' => $schedule, + ]; + } + } + } elseif($gameInfo['variant'] === SP_GAME_SP2) { + $vs = $schedRaw->vs; + $coop = $schedRaw->co; + + $includeRegular = $filterAll || in_array('regular', $filters); + $includeRanked = $filterAll || in_array('gachi', $filters); + $includeLeague = $filterAll || in_array('league', $filters); + $includeSalmon = $filterAll || in_array('coop', $filters); + + function splatoon2_schedule_gen($target, $node): void { + $target->start = splatoon2_format_date($node->start_time); + $target->end = splatoon2_format_date($node->end_time); + } + + function splatoon2_schedule_vs($target, $node): void { + global $localeInfo, $gameModes; + + if(!array_key_exists($node->game_mode->key, $gameModes)) + $gameModes[$node->game_mode->key] = [ + 'id' => $node->game_mode->key, + 'name' => $localeInfo->game_modes->{$node->game_mode->key}?->name ?? $node->game_mode->name, + 'colour' => splatoon2_get_mode_colour($node->game_mode->key), + ]; + + $target->variant = 'vs'; + + $target->ruleset = [ + 'id' => splatoon2_translate_ruleset($node->rule->key), + 'name' => $localeInfo->rules->{$node->rule->key}?->name ?? $node->rule->name, + 'short' => splatoon2_ruleset_short($node->rule->key), + ]; + + $target->stages = []; + foreach(['stage_a', 'stage_b', 'stage_c'] as $stage) { + $stage = $node->{$stage} ?? null; + if(empty($stage)) + continue; + + $target->stages[] = [ + 'id' => $stage->id, + 'name' => $localeInfo->stages->{$stage->id}?->name ?? $stage->name, + 'image' => sprintf(SP2_IMAGE_FORMAT, $stage->image), + ]; + } + } + + if($includeRegular && !empty($vs->regular)) { + $schedule = []; + + foreach($vs->regular as $node) { + if(empty($node->rule)) + continue; + + $schedule[] = $item = new stdClass; + splatoon2_schedule_gen($item, $node); + splatoon2_schedule_vs($item, $node); + } + + if(!empty($schedule)) + $schedules[] = [ + 'mode' => 'regular', + 'schedule' => $schedule, + ]; + } + + if($includeRanked && !empty($vs->gachi)) { + $schedule = []; + + foreach($vs->gachi as $node) { + if(empty($node->rule)) + continue; + + $schedule[] = $item = new stdClass; + splatoon2_schedule_gen($item, $node); + splatoon2_schedule_vs($item, $node); + } + + if(!empty($schedule)) + $schedules[] = [ + 'mode' => 'gachi', + 'schedule' => $schedule, + ]; + } + + if($includeLeague && !empty($vs->league)) { + $schedule = []; + + foreach($vs->league as $node) { + if(empty($node->rule)) + continue; + + $schedule[] = $item = new stdClass; + splatoon2_schedule_gen($item, $node); + splatoon2_schedule_vs($item, $node); + } + + if(!empty($schedule)) + $schedules[] = [ + 'mode' => 'league', + 'schedule' => $schedule, + ]; + } + + if($includeSalmon && !empty($coop->schedules)) { + $schedule = []; + + foreach($coop->schedules as $key => $node) { + $schedule[] = $item = new stdClass; + splatoon2_schedule_gen($item, $node); + $item->variant = 'coop'; + + $details = $coop->details[$key] ?? null; + if(!empty($details)) { + $item->stages = [ + [ + 'name' => $details->stage->name, + 'image' => sprintf(SP2_IMAGE_FORMAT, $details->stage->image), + ], + ]; + + + $item->weapons = []; + foreach($details->weapons as $weapon) { + if($weapon->id === '-1') { + $weaponImage = $weapon->coop_special_weapon->image; + $weaponName = $localeInfo->coop_special_weapons->{$weaponImage}?->name ?? $weapon->coop_special_weapon->name; + } else { + $weaponName = $localeInfo->weapons->{$weapon->weapon->id}?->name ?? $weapon->weapon->name; + $weaponImage = $weapon->weapon->image; + } + + $item->weapons[] = [ + 'id' => $weapon->id, + 'name' => $weaponName, + 'image' => sprintf(SP2_IMAGE_FORMAT, $weaponImage), + ]; + } + } + } + + if(!empty($schedule)) { + $gameModes[] = [ + 'id' => 'coop', + 'name' => 'Salmon Run', + 'colour' => splatoon2_get_mode_colour('coop'), + ]; + + $schedules[] = [ + 'mode' => 'coop', + 'schedule' => $schedule, + ]; + } + } + } else die('{"error":"schedules:variant"}'); + + echo json_encode([ + 'game' => [ + 'id' => $gameId, + 'name' => $gameInfo['name'], + 'colour' => $gameInfo['colour'], + 'variant' => $gameInfo['variant'], + ], + 'modes' => array_values($gameModes), + 'schedules' => $schedules, + ]); + exit; +} + +if($typeId === SP_TYPE_FESTS) { // SPLATFEST SHIT + $regions = array_filter(explode(',', (string)filter_input(INPUT_GET, 'fr'))); + $regionsAll = empty($regions); + foreach($regions as $key => $region) { + if(!empty($gameInfo['festRegionAliases']) && array_key_exists($region, $gameInfo['festRegionAliases'])) + $region = $gameInfo['festRegionAliases'][$region]; + if(!in_array($region, $gameInfo['festRegions'])) + die('{"error":"fests:region"}'); + $regions[$key] = $region; + } + + if($regionsAll) + $regions = $gameInfo['festRegions']; + + $festsRaw = (object)cached(sprintf('%s-fests', $gameId), 'cache_lifetime_15min', function() use($gameInfo) { + if(empty($gameInfo['festUrl'])) + die('{"error":"fests:url"}'); + + return httpRequest($gameInfo['festUrl']); + }); + + $fests = []; + + if($gameInfo['variant'] === SP_GAME_SP3) { + foreach($regions as $region) { + if(empty($festsRaw->{$region}->data->festRecords->nodes)) + continue; + $nodes = $festsRaw->{$region}->data->festRecords->nodes; + + if(!empty($gameInfo['festRegionAliasesReverse']) && array_key_exists($region, $gameInfo['festRegionAliasesReverse'])) + $region = $gameInfo['festRegionAliasesReverse'][$region]; + + foreach($nodes as $node) { + $festId = $node->image->url; + if(empty($fests[$festId])) + $fests[$festId] = [ + 'regions' => [], + ]; + + $fests[$festId]['regions'][] = $festInfo = new stdClass; + $festInfo->id = $node->id; + $festInfo->region = $region; + $festInfo->state = $node->state; + $festInfo->start = $node->startTime; + $festInfo->end = $node->endTime; + $festInfo->title = $node->title; + $festInfo->image = $node->image->url; + $festInfo->can_vote = $node->isVotable; + $festInfo->teams = []; + + foreach($node->teams as $rawTeam) { + $festInfo->teams[] = $team = new stdClass; + $team->id = $rawTeam->id; + $team->name = $rawTeam->teamName; + $team->image = $rawTeam->image->url; + $team->colour = (round($rawTeam->color->a * 0xFF) << 24) + | (round($rawTeam->color->r * 0xFF) << 16) + | (round($rawTeam->color->g * 0xFF) << 8) + | round($rawTeam->color->b * 0xFF); + + $team->has_results = !empty($rawTeam->result); + if(!$team->has_results) + continue; + + $team->is_winner = $rawTeam->result->isWinner; + $team->results = []; + + foreach(['horagai', 'vote', 'regularContribution', 'challengeContribution', 'tricolorContribution'] as $rateType) { + if(!isset($rawTeam->result->{$rateType . 'Ratio'})) + continue; + + $rateCat = $rateType; + if(str_ends_with($rateCat, 'Contribution')) + $rateCat = substr($rateCat, 0, -12); + + $team->results[] = [ + 'category' => $rateCat, + 'ratio' => $rawTeam->result->{$rateType . 'Ratio'}, + 'is_top' => $rawTeam->result->{'is' . ucfirst($rateType) . 'RatioTop'}, + ]; + } + } + } + } + } elseif($gameInfo['variant'] === SP_GAME_SP2) { + foreach($regions as $region) { + if(empty($festsRaw->{$region}->festivals)) + continue; + $festivals = $festsRaw->{$region}->festivals; + $results = $festsRaw->{$region}->results; + + if(!empty($gameInfo['festRegionAliasesReverse']) && array_key_exists($region, $gameInfo['festRegionAliasesReverse'])) + $region = $gameInfo['festRegionAliasesReverse'][$region]; + + foreach($festivals as $festId => $festRaw) { + if(empty($fests[$festId])) + $fests[$festId] = [ + 'regions' => [], + ]; + + $festNames = $localeInfo->festivals->{$festRaw->festival_id}->names ?? null; + if(empty($festNames)) + continue; + + foreach($results as $findResultRaw) + if($findResultRaw->festival_id === $festRaw->festival_id) { + $resultsRaw = $findResultRaw; + break; + } + + $time = time(); + $state = ''; + if($time > $festRaw->times->end) + $state = 'CLOSED'; + elseif($time > $festRaw->times->start) + $state = 'OPEN'; // this is an assumption + else + $state = 'SCHEDULED'; + + $fests[$festId]['regions'][] = $festInfo = new stdClass; + $festInfo->id = base64_encode(sprintf('FestTeam-%s:JEA-%05d', $region, $festRaw->festival_id)); + $festInfo->region = $region; + $festInfo->state = $state; + $festInfo->start = splatoon2_format_date($festRaw->times->start); + $festInfo->end = splatoon2_format_date($festRaw->times->end); + $festInfo->title = sprintf('%s vs %s', $festNames->alpha_short, $festNames->bravo_short); + $festInfo->image = sprintf(SP2_IMAGE_FORMAT, $festRaw->images->panel); + $festInfo->can_vote = $state !== 'CLOSED'; + $festInfo->teams = []; + $festInfo->special_stages = [ + [ + 'id' => $festRaw->special_stage->id, + 'name' => $localeInfo->stages->{$festRaw->special_stage->id}?->name ?? $festRaw->special_stage->name, + 'image' => sprintf(SP2_IMAGE_FORMAT, $festRaw->special_stage->image), + ], + ]; + + $tmpResults = []; + + foreach(['alpha', 'bravo'] as $teamId => $teamName) { + $festInfo->teams[] = $team = new stdClass; + $team->id = base64_encode(sprintf('FestTeam-%s:JEA-%05d:%s', $region, $festInfo->id, ucfirst($teamName))); + $team->name = $festNames->{$teamName . '_long'}; + $team->image = sprintf(SP2_IMAGE_FORMAT, $festRaw->images->{$teamName}); + + $rawColour = $festRaw->colors->{$teamName}; + $team->colour = (round($rawColour->a * 0xFF) << 24) + | (round($rawColour->r * 0xFF) << 16) + | (round($rawColour->g * 0xFF) << 8) + | round($rawColour->b * 0xFF); + + $team->has_results = !empty($resultsRaw); + if(!$team->has_results) + continue; + + $team->is_winner = $resultsRaw->summary->total === $teamId; + $team->results = []; + foreach(['challenge', 'vote', 'regular', 'solo', 'team'] as $rateType) { + $rawRate = $resultsRaw->rates->{$rateType}->{$teamName} ?? null; + if(empty($rawRate)) + continue; + + if(!isset($tmpResults[$rateType])) + $tmpResults[$rateType] = $rawRate > $resultsRaw->rates->{$rateType}->{'bravo'} ?? 0; + + $team->results[] = [ + 'category' => $rateType, + 'ratio' => $rawRate / 10000, + 'is_top' => $tmpResults[$rateType], + ]; + + $tmpResults[$rateType] = !$tmpResults[$rateType]; + } + } + + unset($tmpResults); + } + } + } else die('{"error":"fests:variant"}'); + + echo json_encode([ + 'game' => [ + 'id' => $gameId, + 'name' => $gameInfo['name'], + 'colour' => $gameInfo['colour'], + 'variant' => $gameInfo['variant'], + ], + 'fests' => array_values($fests), + ]); + exit; +}