*/ function comparisonIntegerList(mixed $value, string $label): array { if ($value === null) { return []; } if (!is_array($value)) { throw new \InvalidArgumentException("{$label} must be an array"); } $result = []; foreach ($value as $entry) { if (!is_int($entry) || $entry < 0) { throw new \InvalidArgumentException("{$label} entries must be non-negative integers"); } $result[$entry] = $entry; } ksort($result, SORT_NUMERIC); return array_values($result); } function comparisonJsonValue(mixed $value): mixed { if (!is_string($value) || $value === '') { return $value; } try { return json_decode($value, true, flags: JSON_THROW_ON_ERROR); } catch (\JsonException) { return $value; } } function comparisonOptionalCode(mixed $value): ?string { if (!is_string($value) || $value === '' || $value === 'None') { return null; } return $value; } /** @return list */ function comparisonGeneralCooldownSelectors(mixed $value, string $label): array { if ($value === null) { return []; } if (!is_array($value)) { throw new \InvalidArgumentException("{$label} must be an array"); } $result = []; foreach ($value as $entry) { if (!is_array($entry)) { throw new \InvalidArgumentException("{$label} entries must be objects"); } $generalId = $entry['generalId'] ?? null; $actionName = $entry['actionName'] ?? null; if (!is_int($generalId) || $generalId < 1 || !is_string($actionName) || $actionName === '') { throw new \InvalidArgumentException( "{$label} entries require a positive generalId and non-empty actionName", ); } $result["{$generalId}:{$actionName}"] = [ 'generalId' => $generalId, 'actionName' => $actionName, ]; } ksort($result, SORT_STRING); return array_values($result); } /** @return list */ function comparisonNationCooldownSelectors(mixed $value, string $label): array { if ($value === null) { return []; } if (!is_array($value)) { throw new \InvalidArgumentException("{$label} must be an array"); } $result = []; foreach ($value as $entry) { if (!is_array($entry)) { throw new \InvalidArgumentException("{$label} entries must be objects"); } $nationId = $entry['nationId'] ?? null; $actionName = $entry['actionName'] ?? null; if (!is_int($nationId) || $nationId < 1 || !is_string($actionName) || $actionName === '') { throw new \InvalidArgumentException( "{$label} entries require a positive nationId and non-empty actionName", ); } $result["{$nationId}:{$actionName}"] = [ 'nationId' => $nationId, 'actionName' => $actionName, ]; } ksort($result, SORT_STRING); return array_values($result); } /** @return list */ function comparisonDiplomacyPairSelectors(mixed $value, string $label): array { if ($value === null) { return []; } if (!is_array($value)) { throw new \InvalidArgumentException("{$label} must be an array"); } $result = []; foreach ($value as $entry) { if (!is_array($entry)) { throw new \InvalidArgumentException("{$label} entries must be objects"); } $fromNationId = $entry['fromNationId'] ?? null; $toNationId = $entry['toNationId'] ?? null; if (!is_int($fromNationId) || $fromNationId < 1 || !is_int($toNationId) || $toNationId < 1) { throw new \InvalidArgumentException( "{$label} entries require positive fromNationId and toNationId", ); } $result["{$fromNationId}:{$toNationId}"] = [ 'fromNationId' => $fromNationId, 'toNationId' => $toNationId, ]; } ksort($result, SORT_STRING); return array_values($result); } /** @param array $row */ function comparisonPickRow(array $row, array $mapping, array $jsonKeys = []): array { $result = []; foreach ($mapping as $canonical => $legacy) { if (!array_key_exists($legacy, $row)) { continue; } $value = $row[$legacy]; if (in_array($legacy, $jsonKeys, true)) { $value = comparisonJsonValue($value); } $result[$canonical] = $value; } return $result; } /** @param list $ids */ function comparisonRowsById(string $table, string $idColumn, array $ids): array { $db = DB::db(); $rows = []; foreach ($ids as $id) { $row = $db->queryFirstRow( "SELECT * FROM `{$table}` WHERE `{$idColumn}` = %i", $id, ); if ($row !== null) { $rows[] = $row; } } return $rows; } function comparisonTurnStateSnapshot(array $request): array { $observe = $request['observe'] ?? []; if (!is_array($observe)) { throw new \InvalidArgumentException('observe must be an object'); } $generalIds = comparisonIntegerList($observe['generalIds'] ?? [], 'generalIds'); $cityIds = comparisonIntegerList($observe['cityIds'] ?? [], 'cityIds'); $nationIds = comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds'); $logAfterId = $observe['logAfterId'] ?? 0; $messageAfterId = $observe['messageAfterId'] ?? 0; $generalCooldownSelectors = comparisonGeneralCooldownSelectors( $observe['generalCooldowns'] ?? [], 'generalCooldowns', ); $nationCooldownSelectors = comparisonNationCooldownSelectors( $observe['nationCooldowns'] ?? [], 'nationCooldowns', ); $diplomacyPairSelectors = comparisonDiplomacyPairSelectors( $observe['diplomacyPairs'] ?? [], 'diplomacyPairs', ); if (!is_int($logAfterId) || $logAfterId < 0 || !is_int($messageAfterId) || $messageAfterId < 0) { throw new \InvalidArgumentException('logAfterId and messageAfterId must be non-negative integers'); } $db = DB::db(); $game = KVStorage::getStorage($db, 'game_env'); $game->resetCache(); $worldValues = $game->getValues([ 'year', 'month', 'turnterm', 'turntime', 'isunited', 'scenario', 'init_year', 'init_month', 'develcost', ]); $nextExecuteStorage = KVStorage::getStorage($db, 'next_execute'); $nextExecuteStorage->resetCache(); $generalCooldowns = array_map( static function (array $selector) use ($nextExecuteStorage): array { $key = "next_execute_{$selector['generalId']}_{$selector['actionName']}"; $value = $nextExecuteStorage->getValue($key); return [ ...$selector, 'nextAvailableTurn' => is_int($value) ? $value : null, ]; }, $generalCooldownSelectors, ); $nationCooldowns = array_map( static function (array $selector) use ($db): array { $nationStorage = KVStorage::getStorage($db, $selector['nationId'], 'nation_env'); $nationStorage->resetCache(); $value = $nationStorage->getValue("next_execute_{$selector['actionName']}"); return [ ...$selector, 'nextAvailableTurn' => is_int($value) ? $value : null, ]; }, $nationCooldownSelectors, ); $generals = array_map( static function (array $row): array { $result = comparisonPickRow( $row, [ 'id' => 'no', 'name' => 'name', 'nationId' => 'nation', 'cityId' => 'city', 'troopId' => 'troop', 'leadership' => 'leadership', 'strength' => 'strength', 'intelligence' => 'intel', 'experience' => 'experience', 'dedication' => 'dedication', 'expLevel' => 'explevel', 'officerLevel' => 'officer_level', 'injury' => 'injury', 'gold' => 'gold', 'rice' => 'rice', 'crew' => 'crew', 'crewTypeId' => 'crewtype', 'train' => 'train', 'atmos' => 'atmos', 'age' => 'age', 'npcState' => 'npc', 'turnTime' => 'turntime', 'recentWarTime' => 'recent_war', 'lastTurn' => 'last_turn', 'meta' => 'aux', 'leadershipExp' => 'leadership_exp', 'strengthExp' => 'strength_exp', 'intelExp' => 'intel_exp', 'dex1' => 'dex1', 'dex2' => 'dex2', 'dex3' => 'dex3', 'dex4' => 'dex4', 'dex5' => 'dex5', 'killTurn' => 'killturn', 'mySet' => 'myset', 'specialDomestic' => 'special', 'specialWar' => 'special2', 'personality' => 'personal', 'itemHorse' => 'horse', 'itemWeapon' => 'weapon', 'itemBook' => 'book', 'itemExtra' => 'item', ], ['last_turn', 'aux'], ); foreach ([ 'specialDomestic', 'specialWar', 'personality', 'itemHorse', 'itemWeapon', 'itemBook', 'itemExtra', ] as $key) { $result[$key] = comparisonOptionalCode($result[$key] ?? null); } return $result; }, comparisonRowsById('general', 'no', $generalIds), ); $rankTypes = array_map( static fn(RankColumn $column): string => $column->value, RankColumn::cases(), ); $rankData = $generalIds === [] ? [] : iterator_to_array($db->query( 'SELECT general_id AS generalId, nation_id AS nationId, `type`, `value`' . ' FROM rank_data WHERE general_id IN %li AND `type` IN %ls' . ' ORDER BY general_id, `type`', $generalIds, $rankTypes, )); $cities = array_map( static fn(array $row): array => comparisonPickRow( $row, [ 'id' => 'city', 'name' => 'name', 'nationId' => 'nation', 'level' => 'level', 'population' => 'pop', 'populationMax' => 'pop_max', 'agriculture' => 'agri', 'agricultureMax' => 'agri_max', 'commerce' => 'comm', 'commerceMax' => 'comm_max', 'security' => 'secu', 'securityMax' => 'secu_max', 'supplyState' => 'supply', 'frontState' => 'front', 'defence' => 'def', 'defenceMax' => 'def_max', 'wall' => 'wall', 'wallMax' => 'wall_max', 'state' => 'state', 'term' => 'term', 'trust' => 'trust', 'trade' => 'trade', 'conflict' => 'conflict', 'officerSet' => 'officer_set', ], ['conflict'], ), comparisonRowsById('city', 'city', $cityIds), ); $nations = array_map( static function (array $row) use ($db): array { $projected = comparisonPickRow( $row, [ 'id' => 'nation', 'name' => 'name', 'color' => 'color', 'capitalCityId' => 'capital', 'gold' => 'gold', 'rice' => 'rice', 'tech' => 'tech', 'level' => 'level', 'typeCode' => 'type', 'generalCount' => 'gennum', 'power' => 'power', 'war' => 'war', 'diplomacyLimit' => 'surlimit', 'capitalRevision' => 'capset', 'strategicCommandLimit' => 'strategic_cmd_limit', 'meta' => 'aux', ], ['aux'], ); $nationStor = KVStorage::getStorage($db, (int)$row['nation'], 'nation_env'); $receivedAssist = $nationStor->getValue('recv_assist'); if (is_array($receivedAssist) && $receivedAssist !== []) { $projected['meta']['recv_assist'] = $receivedAssist; } $respondedAssist = $nationStor->getValue('resp_assist'); if (is_array($respondedAssist) && $respondedAssist !== []) { $projected['meta']['resp_assist'] = $respondedAssist; } return $projected; }, comparisonRowsById('nation', 'nation', $nationIds), ); $diplomacyPairs = []; foreach ($nationIds as $fromNationId) { foreach ($nationIds as $toNationId) { if ($fromNationId === $toNationId) { continue; } $diplomacyPairs["{$fromNationId}:{$toNationId}"] = [ 'fromNationId' => $fromNationId, 'toNationId' => $toNationId, ]; } } foreach ($diplomacyPairSelectors as $pair) { $diplomacyPairs["{$pair['fromNationId']}:{$pair['toNationId']}"] = $pair; } ksort($diplomacyPairs, SORT_STRING); $diplomacy = []; foreach ($diplomacyPairs as $pair) { $row = $db->queryFirstRow( 'SELECT me, you, state, term, dead FROM diplomacy WHERE me = %i AND you = %i', $pair['fromNationId'], $pair['toNationId'], ); if ($row !== null) { $diplomacy[] = comparisonPickRow($row, [ 'fromNationId' => 'me', 'toNationId' => 'you', 'state' => 'state', 'term' => 'term', 'dead' => 'dead', ]); } } $generalTurns = []; foreach ($generalIds as $generalId) { foreach ($db->query( 'SELECT general_id, turn_idx, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx', $generalId, ) as $row) { $generalTurns[] = comparisonPickRow( $row, ['generalId' => 'general_id', 'turnIndex' => 'turn_idx', 'action' => 'action', 'args' => 'arg'], ['arg'], ); } } $nationTurns = []; foreach ($nationIds as $nationId) { foreach ($db->query( 'SELECT nation_id, officer_level, turn_idx, action, arg FROM nation_turn WHERE nation_id = %i ORDER BY officer_level, turn_idx', $nationId, ) as $row) { $nationTurns[] = comparisonPickRow( $row, [ 'nationId' => 'nation_id', 'officerLevel' => 'officer_level', 'turnIndex' => 'turn_idx', 'action' => 'action', 'args' => 'arg', ], ['arg'], ); } } $logs = []; if ($generalIds !== []) { foreach ($db->query( 'SELECT id, general_id, log_type, year, month, text FROM general_record WHERE id > %i ORDER BY id', $logAfterId, ) as $row) { if ((int)$row['general_id'] !== 0 && !in_array((int)$row['general_id'], $generalIds, true)) { continue; } $logs[] = comparisonPickRow($row, [ 'id' => 'id', 'generalId' => 'general_id', 'category' => 'log_type', 'year' => 'year', 'month' => 'month', 'text' => 'text', ]); } } $messages = array_map( static fn(array $row): array => comparisonPickRow( $row, [ 'id' => 'id', 'mailbox' => 'mailbox', 'type' => 'type', 'sourceId' => 'src', 'destinationId' => 'dest', 'createdAt' => 'time', 'payload' => 'message', ], ['message'], ), $db->query( 'SELECT id, mailbox, type, src, dest, time, message FROM message WHERE id > %i ORDER BY id', $messageAfterId, ), ); return [ 'schemaVersion' => 1, 'engine' => 'ref', 'world' => [ 'year' => (int)$worldValues['year'], 'month' => (int)$worldValues['month'], 'tickMinutes' => (int)$worldValues['turnterm'], 'turnTime' => (string)$worldValues['turntime'], 'isUnited' => (int)$worldValues['isunited'], 'scenarioId' => (int)$worldValues['scenario'], 'initYear' => (int)$worldValues['init_year'], 'initMonth' => (int)$worldValues['init_month'], 'develCost' => (int)$worldValues['develcost'], 'generalCooldowns' => $generalCooldowns, 'nationCooldowns' => $nationCooldowns, ], 'generals' => $generals, 'rankData' => $rankData, 'cities' => $cities, 'nations' => $nations, 'diplomacy' => $diplomacy, 'generalTurns' => $generalTurns, 'nationTurns' => $nationTurns, 'logs' => $logs, 'messages' => $messages, 'watermarks' => [ 'logId' => (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM general_record') ?? 0), 'messageId' => (int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM message') ?? 0), ], ]; } function comparisonTurnStateSnapshotMain(): void { try { $input = stream_get_contents(STDIN); $request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR); if (!is_array($request)) { throw new \InvalidArgumentException('request must be an object'); } echo json_encode( comparisonTurnStateSnapshot($request), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION, ), PHP_EOL; } catch (\Throwable $throwable) { fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL); exit(1); } } if (realpath((string)($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) { comparisonTurnStateSnapshotMain(); }