257 lines
7.1 KiB
PHP
257 lines
7.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Build the static 100th-season all-star pool from a tab-separated query result.
|
|
*
|
|
* Usage:
|
|
* mariadb --batch --raw --skip-column-names DATABASE \
|
|
* < src/centennial_allstar_candidates.sql \
|
|
* | php src/build_centennial_allstar_pool.php OUTPUT.json
|
|
*/
|
|
|
|
const OUTPUT_COLUMNS = [
|
|
'generalName',
|
|
'leadership',
|
|
'strength',
|
|
'intel',
|
|
'specialDomestic',
|
|
'dex',
|
|
'imgsvr',
|
|
'picture',
|
|
'sourcePhase',
|
|
'sourceServerId',
|
|
'sourceGeneralNo',
|
|
'selectionReasons',
|
|
];
|
|
|
|
const EXCLUDED_EVENT_PHASES = [
|
|
5, 10, 15, 20, 25, 30, 35, 40, 45, 50,
|
|
55, 60, 65, 70, 75, 80, 85, 90, 95,
|
|
];
|
|
|
|
const LEGACY_SPECIAL_WAR_MAP = [
|
|
40 => 'che_event_귀병',
|
|
41 => 'che_event_신산',
|
|
42 => 'che_event_환술',
|
|
43 => 'che_event_집중',
|
|
44 => 'che_event_신중',
|
|
45 => 'che_event_반계',
|
|
50 => 'che_event_보병',
|
|
51 => 'che_event_궁병',
|
|
52 => 'che_event_기병',
|
|
53 => 'che_event_공성',
|
|
60 => 'che_event_돌격',
|
|
61 => 'che_event_무쌍',
|
|
62 => 'che_event_견고',
|
|
63 => 'che_event_위압',
|
|
70 => 'che_event_저격',
|
|
71 => 'che_event_필살',
|
|
72 => 'che_event_징병',
|
|
73 => 'che_event_의술',
|
|
74 => 'che_event_격노',
|
|
75 => 'che_event_척사',
|
|
];
|
|
|
|
function fail(string $message): never
|
|
{
|
|
fwrite(STDERR, "error: {$message}\n");
|
|
exit(1);
|
|
}
|
|
|
|
function firstInt(array $data, array $keys): int
|
|
{
|
|
foreach ($keys as $key) {
|
|
if (array_key_exists($key, $data) && is_numeric($data[$key])) {
|
|
return (int) $data[$key];
|
|
}
|
|
}
|
|
fail('missing numeric field: ' . implode(' or ', $keys));
|
|
}
|
|
|
|
function normalizeEventSpecial(mixed $rawSpecial): ?string
|
|
{
|
|
if (is_numeric($rawSpecial)) {
|
|
return LEGACY_SPECIAL_WAR_MAP[(int) $rawSpecial] ?? null;
|
|
}
|
|
if (
|
|
!is_string($rawSpecial)
|
|
|| $rawSpecial === ''
|
|
|| $rawSpecial === '0'
|
|
|| strcasecmp($rawSpecial, 'none') === 0
|
|
) {
|
|
return null;
|
|
}
|
|
if (str_starts_with($rawSpecial, 'che_event_')) {
|
|
return $rawSpecial;
|
|
}
|
|
if (str_starts_with($rawSpecial, 'che_')) {
|
|
return 'che_event_' . substr($rawSpecial, strlen('che_'));
|
|
}
|
|
fail("unknown historical war special: {$rawSpecial}");
|
|
}
|
|
|
|
function decodeSourceRow(string $line, int $lineNo): array
|
|
{
|
|
$fields = explode("\t", rtrim($line, "\r\n"), 6);
|
|
if (count($fields) !== 6) {
|
|
fail("line {$lineNo}: expected 6 tab-separated fields, got " . count($fields));
|
|
}
|
|
|
|
[$phase, $serverId, $generalNo, $name, $rawData, $rawReasons] = $fields;
|
|
$data = json_decode($rawData, true, 512, JSON_THROW_ON_ERROR);
|
|
if (!is_array($data)) {
|
|
fail("line {$lineNo}: general data is not an object");
|
|
}
|
|
|
|
$phaseNo = (int) $phase;
|
|
$sourceGeneralNo = (int) $generalNo;
|
|
if ($phaseNo < 1 || $phaseNo > 99 || $sourceGeneralNo <= 2) {
|
|
fail("line {$lineNo}: invalid phase/general number");
|
|
}
|
|
|
|
$sourceName = trim($name);
|
|
if ($sourceName === '') {
|
|
fail("line {$lineNo}: empty general name");
|
|
}
|
|
$generalName = sprintf('%d·%s', $phaseNo, $sourceName);
|
|
if (mb_strlen($generalName) > 32) {
|
|
fail("line {$lineNo}: generated name exceeds 32 characters: {$generalName}");
|
|
}
|
|
|
|
$dex = [
|
|
firstInt($data, ['dex1', 'dex0']),
|
|
firstInt($data, ['dex2', 'dex10']),
|
|
firstInt($data, ['dex3', 'dex20']),
|
|
firstInt($data, ['dex4', 'dex30']),
|
|
firstInt($data, ['dex5', 'dex40']),
|
|
];
|
|
foreach ($dex as $value) {
|
|
if ($value < 0) {
|
|
fail("line {$lineNo}: negative dex value");
|
|
}
|
|
}
|
|
|
|
$reasons = $rawReasons === '' ? [] : explode(',', $rawReasons);
|
|
sort($reasons, SORT_STRING);
|
|
|
|
return [
|
|
$generalName,
|
|
firstInt($data, ['leadership', 'leader']),
|
|
firstInt($data, ['strength', 'power']),
|
|
firstInt($data, ['intel']),
|
|
normalizeEventSpecial($data['special2'] ?? null),
|
|
$dex,
|
|
firstInt($data, ['imgsvr']),
|
|
is_string($data['picture'] ?? null) && $data['picture'] !== ''
|
|
? $data['picture']
|
|
: 'default.jpg',
|
|
$phaseNo,
|
|
$serverId,
|
|
$sourceGeneralNo,
|
|
$reasons,
|
|
];
|
|
}
|
|
|
|
if ($argc !== 2) {
|
|
fail('usage: php build_centennial_allstar_pool.php OUTPUT.json');
|
|
}
|
|
|
|
$outputPath = $argv[1];
|
|
$rows = [];
|
|
$seenSources = [];
|
|
$nameIndexes = [];
|
|
$phaseCounts = [];
|
|
$chiefCounts = [];
|
|
$reasonCounts = [];
|
|
$lineNo = 0;
|
|
|
|
while (($line = fgets(STDIN)) !== false) {
|
|
$lineNo++;
|
|
if (trim($line) === '') {
|
|
continue;
|
|
}
|
|
try {
|
|
$row = decodeSourceRow($line, $lineNo);
|
|
} catch (JsonException $e) {
|
|
fail("line {$lineNo}: invalid JSON: {$e->getMessage()}");
|
|
}
|
|
|
|
$sourceKey = "{$row[8]}:{$row[10]}";
|
|
if (isset($seenSources[$sourceKey])) {
|
|
fail("line {$lineNo}: duplicate source general {$sourceKey}");
|
|
}
|
|
$seenSources[$sourceKey] = true;
|
|
$nameIndexes[$row[0]][] = count($rows);
|
|
$phaseCounts[$row[8]] = ($phaseCounts[$row[8]] ?? 0) + 1;
|
|
foreach ($row[11] as $reason) {
|
|
$reasonGroup = str_starts_with($reason, 'chief:') ? 'chief' : 'hall';
|
|
$reasonCounts[$reasonGroup] = ($reasonCounts[$reasonGroup] ?? 0) + 1;
|
|
if ($reasonGroup === 'chief') {
|
|
$chiefCounts[$row[8]] = ($chiefCounts[$row[8]] ?? 0) + 1;
|
|
}
|
|
}
|
|
$rows[] = $row;
|
|
}
|
|
|
|
if ($rows === []) {
|
|
fail('no input rows');
|
|
}
|
|
foreach ($nameIndexes as $generalName => $indexes) {
|
|
if (count($indexes) === 1) {
|
|
continue;
|
|
}
|
|
foreach ($indexes as $index) {
|
|
$rows[$index][0] .= "#{$rows[$index][10]}";
|
|
if (mb_strlen($rows[$index][0]) > 32) {
|
|
fail("disambiguated name exceeds 32 characters: {$rows[$index][0]}");
|
|
}
|
|
}
|
|
}
|
|
ksort($phaseCounts, SORT_NUMERIC);
|
|
$expectedPhases = array_values(array_diff(range(1, 99), EXCLUDED_EVENT_PHASES));
|
|
if (array_keys($phaseCounts) !== $expectedPhases) {
|
|
fail('input does not cover every non-event phase from 1 through 99');
|
|
}
|
|
foreach ($expectedPhases as $phase) {
|
|
if (($chiefCounts[$phase] ?? 0) !== 8) {
|
|
fail("phase {$phase}: expected 8 unification chiefs including the ruler");
|
|
}
|
|
}
|
|
|
|
$payload = [
|
|
'excludedEventPhases' => EXCLUDED_EVENT_PHASES,
|
|
'columns' => OUTPUT_COLUMNS,
|
|
'data' => $rows,
|
|
];
|
|
$encoded = json_encode(
|
|
$payload,
|
|
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
|
|
) . "\n";
|
|
|
|
$outputDir = dirname($outputPath);
|
|
if (!is_dir($outputDir)) {
|
|
fail("output directory does not exist: {$outputDir}");
|
|
}
|
|
$tempPath = tempnam($outputDir, basename($outputPath) . '.tmp.');
|
|
if ($tempPath === false) {
|
|
fail("could not create temporary output in {$outputDir}");
|
|
}
|
|
if (file_put_contents($tempPath, $encoded) === false || !rename($tempPath, $outputPath)) {
|
|
@unlink($tempPath);
|
|
fail("could not write output: {$outputPath}");
|
|
}
|
|
|
|
fwrite(
|
|
STDERR,
|
|
sprintf(
|
|
"wrote %d candidates across phases %d-%d (%d hall reasons, %d chief reasons)\n",
|
|
count($rows),
|
|
min(array_keys($phaseCounts)),
|
|
max(array_keys($phaseCounts)),
|
|
$reasonCounts['hall'] ?? 0,
|
|
$reasonCounts['chief'] ?? 0,
|
|
)
|
|
);
|