diff --git a/hwe/compare/turn_state_snapshot.php b/hwe/compare/turn_state_snapshot.php new file mode 100644 index 00000000..3eb6af4a --- /dev/null +++ b/hwe/compare/turn_state_snapshot.php @@ -0,0 +1,330 @@ + */ +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; + } +} + +/** @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; + 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']); + + $generals = array_map( + static fn(array $row): array => comparisonPickRow( + $row, + [ + 'id' => 'no', + 'name' => 'name', + 'nationId' => 'nation', + 'cityId' => 'city', + 'troopId' => 'troop', + 'leadership' => 'leadership', + 'strength' => 'strength', + 'intelligence' => 'intel', + 'experience' => 'experience', + 'dedication' => 'dedication', + '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', + 'killTurn' => 'killturn', + 'mySet' => 'myset', + ], + ['last_turn', 'aux'], + ), + comparisonRowsById('general', 'no', $generalIds), + ); + + $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', + ], + ), + comparisonRowsById('city', 'city', $cityIds), + ); + + $nations = array_map( + static fn(array $row): array => 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', + 'meta' => 'aux', + ], + ['aux'], + ), + comparisonRowsById('nation', 'nation', $nationIds), + ); + + $diplomacy = []; + foreach ($nationIds as $fromNationId) { + foreach ($nationIds as $toNationId) { + if ($fromNationId === $toNationId) { + continue; + } + $row = $db->queryFirstRow( + 'SELECT me, you, state, term, dead FROM diplomacy WHERE me = %i AND you = %i', + $fromNationId, + $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'], + ], + 'generals' => $generals, + '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), + ], + ]; +} + +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); +}