feat: replace game schedules with logical ticks
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# 논리 game clock 운영
|
||||
|
||||
게임 진행 시각은 `36,000,000 tick × 완료 턴 + 현재 세부 tick`으로 저장합니다.
|
||||
표시 시각만 `game_env.clock_base_time`을 기준으로 달력 시각으로 투영합니다.
|
||||
|
||||
## 기존 DB migration
|
||||
|
||||
웹과 턴 daemon을 먼저 중지하고 HWE DB SQL dump를 만든 다음 상태를 확인합니다.
|
||||
|
||||
```bash
|
||||
php scripts/migrate-game-clock.php --status
|
||||
php scripts/migrate-game-clock.php --apply --backup=/absolute/path/to/hwe-before-clock.sql
|
||||
```
|
||||
|
||||
적용 명령은 비어 있지 않은 절대경로 backup과 `GAME` lock을 요구합니다. Aria
|
||||
DDL은 transaction rollback이 되지 않으므로 기존 날짜 컬럼과 `game_env` 값은
|
||||
`*_wall_backup`으로 남깁니다. 새 코드 검증이 끝나기 전에는 이 값을 제거하지
|
||||
마세요. 복구할 때는 PHP/daemon을 중지하고 명령에 지정했던 전체 SQL dump를
|
||||
복원하는 것이 기준 절차입니다.
|
||||
|
||||
## 시계 조회와 전진
|
||||
|
||||
```bash
|
||||
php scripts/game-clock.php --status
|
||||
php scripts/game-clock.php --advance-turns=12 --apply
|
||||
php scripts/game-clock.php --advance-ticks=36000000 --apply
|
||||
php scripts/game-clock.php --mode=realtime --apply
|
||||
```
|
||||
|
||||
명시적으로 전진하면 시계는 `manual` 모드가 됩니다. 이 모드의 엔진은 실제
|
||||
시각을 읽지 않습니다. `realtime`으로 전환할 때 현재 논리 tick을 새 벽시계
|
||||
anchor에 고정하므로 표시 시각이 튀지 않습니다.
|
||||
|
||||
격리 DB에서 manual clock과 엔진 진행을 함께 확인할 수 있습니다.
|
||||
|
||||
```bash
|
||||
php scripts/verify-game-clock-engine.php --apply --engine-calls=2
|
||||
```
|
||||
|
||||
이 검증기는 manual mode만 허용하고, 엔진 호출 전후 clock tick이 벽시계 때문에
|
||||
변하지 않았는지와 마지막 처리 tick이 현재 tick을 넘지 않았는지 검사합니다.
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use sammo\DB;
|
||||
use sammo\GameClock;
|
||||
use sammo\KVStorage;
|
||||
use sammo\Util;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
||||
$_SERVER['REQUEST_URI'] ??= '/cli/game-clock';
|
||||
|
||||
require dirname(__DIR__) . '/hwe/lib.php';
|
||||
require dirname(__DIR__) . '/hwe/func.php';
|
||||
|
||||
/** @return never */
|
||||
function gameClockUsage(int $exitCode = 0): void
|
||||
{
|
||||
$stream = $exitCode === 0 ? STDOUT : STDERR;
|
||||
fwrite($stream, <<<'TEXT'
|
||||
Usage:
|
||||
php scripts/game-clock.php --status
|
||||
php scripts/game-clock.php --mode=manual|realtime --apply
|
||||
php scripts/game-clock.php --advance-turns=N --apply
|
||||
php scripts/game-clock.php --advance-ticks=N --apply
|
||||
|
||||
Mutation commands require --apply and the GAME lock. Explicit advancement
|
||||
switches the clock to manual mode, so subsequent engine runs never consult the
|
||||
wall clock. Use --mode=realtime to resume wall-clock-paced progression.
|
||||
TEXT);
|
||||
exit($exitCode);
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
function signedIntegerOption(array $options, string $name): int
|
||||
{
|
||||
$value = $options[$name] ?? null;
|
||||
if (!is_string($value) || preg_match('/^-?[0-9]+$/', $value) !== 1) {
|
||||
throw new InvalidArgumentException("--{$name}에는 정수를 지정해야 합니다.");
|
||||
}
|
||||
$result = filter_var($value, FILTER_VALIDATE_INT);
|
||||
if ($result === false) {
|
||||
throw new InvalidArgumentException("--{$name} 값이 INT64 범위를 벗어났습니다.");
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function printGameClockState(KVStorage $gameStor): void
|
||||
{
|
||||
$gameStor->resetCache();
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$nowTick = $clock->nowTick();
|
||||
$state = $gameStor->getValues(['year', 'month', 'turntime', 'turnterm']);
|
||||
printf(
|
||||
"mode=%s game=%d-%02d now_tick=%d now=%s last_tick=%d last=%s turnterm=%dm ticks_per_turn=%d ticks_per_second=%d\n",
|
||||
$clock->getMode(),
|
||||
Util::toInt($state['year']),
|
||||
Util::toInt($state['month']),
|
||||
$nowTick,
|
||||
$clock->formatTick($nowTick, true),
|
||||
Util::toInt($state['turntime']),
|
||||
$clock->formatTick(Util::toInt($state['turntime']), true),
|
||||
Util::toInt($state['turnterm']),
|
||||
GameClock::TICKS_PER_TURN,
|
||||
$clock->ticksPerSecond(),
|
||||
);
|
||||
}
|
||||
|
||||
$options = getopt('', ['help', 'status', 'mode:', 'advance-turns:', 'advance-ticks:', 'apply']);
|
||||
if (isset($options['help'])) {
|
||||
gameClockUsage();
|
||||
}
|
||||
|
||||
$commands = array_filter([
|
||||
'status' => isset($options['status']),
|
||||
'mode' => array_key_exists('mode', $options),
|
||||
'advance-turns' => array_key_exists('advance-turns', $options),
|
||||
'advance-ticks' => array_key_exists('advance-ticks', $options),
|
||||
]);
|
||||
if (count($commands) !== 1) {
|
||||
gameClockUsage(2);
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
if (isset($commands['status'])) {
|
||||
printGameClockState($gameStor);
|
||||
exit(0);
|
||||
}
|
||||
if (!isset($options['apply'])) {
|
||||
fwrite(STDERR, "변경 명령에는 --apply가 필요합니다.\n");
|
||||
exit(2);
|
||||
}
|
||||
if (!\sammo\tryLock()) {
|
||||
fwrite(STDERR, "GAME lock을 획득하지 못했습니다.\n");
|
||||
exit(3);
|
||||
}
|
||||
|
||||
try {
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$currentTick = $clock->nowTick();
|
||||
|
||||
if (isset($commands['mode'])) {
|
||||
$mode = (string)$options['mode'];
|
||||
if (!in_array($mode, [GameClock::MODE_MANUAL, GameClock::MODE_REALTIME], true)) {
|
||||
throw new InvalidArgumentException('--mode는 manual 또는 realtime이어야 합니다.');
|
||||
}
|
||||
$clock->persistTick($gameStor, $currentTick, $mode);
|
||||
} else {
|
||||
if (isset($commands['advance-turns'])) {
|
||||
$nextTick = $clock->addTurns($currentTick, signedIntegerOption($options, 'advance-turns'));
|
||||
} else {
|
||||
$nextTick = GameClock::addTicks($currentTick, signedIntegerOption($options, 'advance-ticks'));
|
||||
}
|
||||
$clock->persistTick($gameStor, $nextTick, GameClock::MODE_MANUAL);
|
||||
}
|
||||
} finally {
|
||||
$gameStor->resetCache();
|
||||
\sammo\unlock();
|
||||
}
|
||||
|
||||
printGameClockState($gameStor);
|
||||
@@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use sammo\DB;
|
||||
use sammo\GameClock;
|
||||
use sammo\Json;
|
||||
use sammo\KVStorage;
|
||||
use sammo\Util;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
||||
$_SERVER['REQUEST_URI'] ??= '/cli/migrate-game-clock';
|
||||
|
||||
require dirname(__DIR__) . '/hwe/lib.php';
|
||||
require dirname(__DIR__) . '/hwe/func.php';
|
||||
|
||||
/** @return never */
|
||||
function migrationUsage(int $exitCode = 0): void
|
||||
{
|
||||
$stream = $exitCode === 0 ? STDOUT : STDERR;
|
||||
fwrite($stream, <<<'TEXT'
|
||||
Usage:
|
||||
php scripts/migrate-game-clock.php --status
|
||||
php scripts/migrate-game-clock.php --apply --backup=/absolute/path/to/pre-migration.sql
|
||||
|
||||
--status is read-only. --apply requires a pre-existing, non-empty SQL backup and
|
||||
the GAME lock. Original DATETIME columns and game_env values are retained with
|
||||
the *_wall_backup suffix so recovery does not depend only on reverse arithmetic.
|
||||
Stop web/daemon traffic before applying; Aria DDL is not transactional.
|
||||
TEXT);
|
||||
exit($exitCode);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
function tableColumnTypes(\MeekroDB $db, string $table): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($db->query("SHOW COLUMNS FROM %b", $table) as $column) {
|
||||
$result[(string)$column['Field']] = strtolower((string)$column['Type']);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function isDateColumn(?string $type): bool
|
||||
{
|
||||
return $type !== null && str_starts_with($type, 'datetime');
|
||||
}
|
||||
|
||||
function isBigIntColumn(?string $type): bool
|
||||
{
|
||||
return $type !== null && str_starts_with($type, 'bigint');
|
||||
}
|
||||
|
||||
/** @return array{state:string,details:array<string,array<string,string>>} */
|
||||
function inspectMigration(\MeekroDB $db): array
|
||||
{
|
||||
$details = [];
|
||||
foreach (['general', 'general_access_log', 'message', 'select_npc_token', 'select_pool', 'ng_auction'] as $table) {
|
||||
$details[$table] = tableColumnTypes($db, $table);
|
||||
}
|
||||
|
||||
$old = isDateColumn($details['general']['turntime'] ?? null)
|
||||
&& isDateColumn($details['general']['recent_war'] ?? null)
|
||||
&& isDateColumn($details['general_access_log']['last_refresh'] ?? null)
|
||||
&& isDateColumn($details['message']['time'] ?? null)
|
||||
&& isDateColumn($details['message']['valid_until'] ?? null)
|
||||
&& isDateColumn($details['select_npc_token']['valid_until'] ?? null)
|
||||
&& isDateColumn($details['select_npc_token']['pick_more_from'] ?? null)
|
||||
&& isDateColumn($details['select_pool']['reserved_until'] ?? null)
|
||||
&& isDateColumn($details['ng_auction']['open_date'] ?? null)
|
||||
&& isDateColumn($details['ng_auction']['close_date'] ?? null);
|
||||
$new = isBigIntColumn($details['general']['turntime'] ?? null)
|
||||
&& isBigIntColumn($details['general']['recent_war'] ?? null)
|
||||
&& isBigIntColumn($details['general_access_log']['last_refresh'] ?? null)
|
||||
&& isBigIntColumn($details['message']['time'] ?? null)
|
||||
&& isBigIntColumn($details['message']['valid_until'] ?? null)
|
||||
&& isBigIntColumn($details['select_npc_token']['valid_until'] ?? null)
|
||||
&& isBigIntColumn($details['select_npc_token']['pick_more_from'] ?? null)
|
||||
&& isBigIntColumn($details['select_pool']['reserved_until'] ?? null)
|
||||
&& isBigIntColumn($details['ng_auction']['open_tick'] ?? null)
|
||||
&& isBigIntColumn($details['ng_auction']['close_tick'] ?? null);
|
||||
|
||||
return [
|
||||
'state' => $old ? 'legacy' : ($new ? 'tick' : 'partial-or-unknown'),
|
||||
'details' => $details,
|
||||
];
|
||||
}
|
||||
|
||||
function printMigrationStatus(\MeekroDB $db): string
|
||||
{
|
||||
$inspection = inspectMigration($db);
|
||||
printf("schema_state=%s\n", $inspection['state']);
|
||||
foreach ($inspection['details'] as $table => $columns) {
|
||||
$interesting = array_filter(
|
||||
$columns,
|
||||
static fn (string $name): bool => preg_match(
|
||||
'/^(turntime|recent_war|last_refresh|time|valid_until|pick_more_from|reserved_until|open_date|close_date|open_tick|close_tick|.*_wall_backup)$/',
|
||||
$name,
|
||||
) === 1,
|
||||
ARRAY_FILTER_USE_KEY,
|
||||
);
|
||||
printf("%s %s\n", $table, Json::encode($interesting));
|
||||
}
|
||||
return $inspection['state'];
|
||||
}
|
||||
|
||||
/** Convert a DATETIME column into a staged BIGINT tick column. */
|
||||
function fillTickColumn(
|
||||
\MeekroDB $db,
|
||||
string $table,
|
||||
string $dateColumn,
|
||||
string $tickColumn,
|
||||
string $baseTime,
|
||||
int $ticksPerSecond,
|
||||
bool $nullable,
|
||||
): void {
|
||||
$nullSql = $nullable ? ' NULL DEFAULT NULL' : ' NOT NULL';
|
||||
$db->query("ALTER TABLE %b ADD COLUMN %b BIGINT{$nullSql}", $table, $tickColumn);
|
||||
$db->query(
|
||||
"UPDATE %b SET %b = (TIMESTAMPDIFF(MICROSECOND, %s, %b) * %i) DIV 1000000",
|
||||
$table,
|
||||
$tickColumn,
|
||||
$baseTime,
|
||||
$dateColumn,
|
||||
$ticksPerSecond,
|
||||
);
|
||||
$missing = Util::toInt($db->queryFirstField(
|
||||
"SELECT COUNT(*) FROM %b WHERE %b IS NOT NULL AND %b IS NULL",
|
||||
$table,
|
||||
$dateColumn,
|
||||
$tickColumn,
|
||||
));
|
||||
if ($missing !== 0) {
|
||||
throw new RuntimeException("{$table}.{$tickColumn} 변환 누락: {$missing}");
|
||||
}
|
||||
}
|
||||
|
||||
function assertSafeTickColumn(\MeekroDB $db, string $table, string $column): void
|
||||
{
|
||||
$unsafe = Util::toInt($db->queryFirstField(
|
||||
'SELECT COUNT(*) FROM %b WHERE %b > %i OR %b < %i',
|
||||
$table,
|
||||
$column,
|
||||
GameClock::MAX_SAFE_TICK,
|
||||
$column,
|
||||
-GameClock::MAX_SAFE_TICK,
|
||||
));
|
||||
if ($unsafe !== 0) {
|
||||
throw new RuntimeException("{$table}.{$column} JavaScript safe integer 초과: {$unsafe}");
|
||||
}
|
||||
}
|
||||
|
||||
$options = getopt('', ['help', 'status', 'apply', 'backup:']);
|
||||
if (isset($options['help'])) {
|
||||
migrationUsage();
|
||||
}
|
||||
if (isset($options['status']) === isset($options['apply'])) {
|
||||
migrationUsage(2);
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
if (isset($options['status'])) {
|
||||
exit(printMigrationStatus($db) === 'partial-or-unknown' ? 2 : 0);
|
||||
}
|
||||
|
||||
$backup = $options['backup'] ?? null;
|
||||
if (!is_string($backup) || $backup === '' || $backup[0] !== '/' || !is_file($backup) || filesize($backup) === 0) {
|
||||
fwrite(STDERR, "--backup에는 적용 직전에 만든 비어 있지 않은 절대경로 SQL 백업을 지정해야 합니다.\n");
|
||||
exit(2);
|
||||
}
|
||||
if (inspectMigration($db)['state'] !== 'legacy') {
|
||||
fwrite(STDERR, "legacy 스키마가 아니므로 적용하지 않습니다. --status 결과를 확인해 주세요.\n");
|
||||
exit(2);
|
||||
}
|
||||
if (!\sammo\tryLock()) {
|
||||
fwrite(STDERR, "GAME lock을 획득하지 못했습니다.\n");
|
||||
exit(3);
|
||||
}
|
||||
|
||||
try {
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$legacy = $gameStor->getValues(['starttime', 'turntime', 'opentime', 'tnmt_time', 'turnterm']);
|
||||
$turnTerm = Util::toInt($legacy['turnterm']);
|
||||
$baseTime = new DateTimeImmutable((string)$legacy['starttime']);
|
||||
$wallNow = GameClock::readWallTime();
|
||||
$conversionClock = new GameClock(
|
||||
$baseTime,
|
||||
$turnTerm,
|
||||
0,
|
||||
GameClock::MODE_REALTIME,
|
||||
$wallNow,
|
||||
static fn (): DateTimeImmutable => $wallNow,
|
||||
);
|
||||
$baseTimeString = $baseTime->format('Y-m-d H:i:s.u');
|
||||
$ticksPerSecond = $conversionClock->ticksPerSecond();
|
||||
|
||||
fillTickColumn($db, 'general', 'turntime', 'turntime_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'general', 'recent_war', 'recent_war_game_tick', $baseTimeString, $ticksPerSecond, true);
|
||||
fillTickColumn($db, 'general_access_log', 'last_refresh', 'last_refresh_game_tick', $baseTimeString, $ticksPerSecond, true);
|
||||
fillTickColumn($db, 'message', 'time', 'time_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'message', 'valid_until', 'valid_until_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
$db->query(
|
||||
'UPDATE message SET valid_until_game_tick = %i WHERE YEAR(valid_until) >= 9000',
|
||||
GameClock::MAX_SAFE_TICK,
|
||||
);
|
||||
fillTickColumn($db, 'select_npc_token', 'valid_until', 'valid_until_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'select_npc_token', 'pick_more_from', 'pick_more_from_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'select_pool', 'reserved_until', 'reserved_until_game_tick', $baseTimeString, $ticksPerSecond, true);
|
||||
fillTickColumn($db, 'ng_auction', 'open_date', 'open_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'ng_auction', 'close_date', 'close_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
foreach ([
|
||||
['general', 'turntime_game_tick'],
|
||||
['general', 'recent_war_game_tick'],
|
||||
['general_access_log', 'last_refresh_game_tick'],
|
||||
['message', 'time_game_tick'],
|
||||
['message', 'valid_until_game_tick'],
|
||||
['select_npc_token', 'valid_until_game_tick'],
|
||||
['select_npc_token', 'pick_more_from_game_tick'],
|
||||
['select_pool', 'reserved_until_game_tick'],
|
||||
['ng_auction', 'open_game_tick'],
|
||||
['ng_auction', 'close_game_tick'],
|
||||
] as [$table, $column]) {
|
||||
assertSafeTickColumn($db, $table, $column);
|
||||
}
|
||||
|
||||
foreach ($db->query('SELECT no, aux FROM general') as $row) {
|
||||
$aux = Json::decode((string)$row['aux']);
|
||||
$nextChange = $aux['next_change'] ?? null;
|
||||
if (is_string($nextChange) && !ctype_digit(ltrim($nextChange, '-'))) {
|
||||
$aux['next_change_wall_backup'] = $nextChange;
|
||||
$aux['next_change'] = $conversionClock->dateTimeToTick(new DateTimeImmutable($nextChange));
|
||||
$db->update('general', ['aux' => Json::encode($aux)], 'no = %i', $row['no']);
|
||||
}
|
||||
}
|
||||
foreach ($db->query('SELECT id, detail FROM ng_auction') as $row) {
|
||||
$detail = Json::decode((string)$row['detail']);
|
||||
$legacyLimit = $detail['availableLatestBidCloseDate'] ?? null;
|
||||
if (is_string($legacyLimit) && $legacyLimit !== '') {
|
||||
$detail['availableLatestBidCloseDateWallBackup'] = $legacyLimit;
|
||||
$detail['availableLatestBidCloseTick'] = $conversionClock->dateTimeToTick(new DateTimeImmutable($legacyLimit));
|
||||
} else {
|
||||
$detail['availableLatestBidCloseTick'] = null;
|
||||
}
|
||||
unset($detail['availableLatestBidCloseDate']);
|
||||
$db->update('ng_auction', ['detail' => Json::encode($detail)], 'id = %i', $row['id']);
|
||||
}
|
||||
foreach ($db->query('SELECT namespace, value FROM nation_env WHERE `key` = %s', 'last천도Trial') as $row) {
|
||||
$trial = Json::decode((string)$row['value']);
|
||||
if (!is_array($trial) || !isset($trial[1]) || !is_string($trial[1]) || ctype_digit(ltrim($trial[1], '-'))) {
|
||||
continue;
|
||||
}
|
||||
$db->insertUpdate('nation_env', [
|
||||
'namespace' => $row['namespace'],
|
||||
'key' => 'last천도Trial_wall_backup',
|
||||
'value' => Json::encode($trial),
|
||||
]);
|
||||
$trial[1] = $conversionClock->dateTimeToTick(new DateTimeImmutable($trial[1]));
|
||||
$db->update(
|
||||
'nation_env',
|
||||
['value' => Json::encode($trial)],
|
||||
'namespace = %i AND `key` = %s',
|
||||
$row['namespace'],
|
||||
'last천도Trial',
|
||||
);
|
||||
}
|
||||
|
||||
$db->query(
|
||||
'ALTER TABLE general '
|
||||
. 'DROP INDEX turntime, DROP INDEX troop, '
|
||||
. 'CHANGE turntime turntime_wall_backup DATETIME(6) NULL DEFAULT NULL, '
|
||||
. 'CHANGE recent_war recent_war_wall_backup DATETIME(6) NULL DEFAULT NULL, '
|
||||
. 'CHANGE turntime_game_tick turntime BIGINT NOT NULL, '
|
||||
. 'CHANGE recent_war_game_tick recent_war BIGINT NULL DEFAULT NULL, '
|
||||
. 'ADD INDEX turntime (turntime, no), ADD INDEX troop (troop, turntime)'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE general_access_log '
|
||||
. 'CHANGE last_refresh last_refresh_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE last_refresh_game_tick last_refresh BIGINT NULL DEFAULT NULL'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE message '
|
||||
. 'CHANGE time time_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE valid_until valid_until_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE time_game_tick time BIGINT NOT NULL, '
|
||||
. 'CHANGE valid_until_game_tick valid_until BIGINT NOT NULL'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE select_npc_token DROP INDEX valid_until, '
|
||||
. 'CHANGE valid_until valid_until_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE pick_more_from pick_more_from_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE valid_until_game_tick valid_until BIGINT NOT NULL, '
|
||||
. 'CHANGE pick_more_from_game_tick pick_more_from BIGINT NOT NULL, '
|
||||
. 'ADD INDEX valid_until (valid_until)'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE select_pool DROP INDEX reserved_until, '
|
||||
. 'CHANGE reserved_until reserved_until_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE reserved_until_game_tick reserved_until BIGINT NULL DEFAULT NULL, '
|
||||
. 'ADD INDEX reserved_until (reserved_until, general_id)'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE ng_auction DROP INDEX by_close, '
|
||||
. 'CHANGE open_date open_date_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE close_date close_date_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE open_game_tick open_tick BIGINT NOT NULL, '
|
||||
. 'CHANGE close_game_tick close_tick BIGINT NOT NULL, '
|
||||
. 'ADD INDEX by_close (finished, type, close_tick)'
|
||||
);
|
||||
|
||||
foreach (['starttime', 'turntime', 'opentime', 'tnmt_time'] as $key) {
|
||||
$value = $legacy[$key] ?? null;
|
||||
$gameStor->{"{$key}_wall_backup"} = $value;
|
||||
$gameStor->{$key} = $value === null || $value === ''
|
||||
? null
|
||||
: $conversionClock->dateTimeToTick(new DateTimeImmutable((string)$value));
|
||||
}
|
||||
GameClock::initializeStorage(
|
||||
$gameStor,
|
||||
$baseTime,
|
||||
$turnTerm,
|
||||
$conversionClock->dateTimeToTick($wallNow),
|
||||
GameClock::MODE_REALTIME,
|
||||
$wallNow,
|
||||
);
|
||||
} finally {
|
||||
\sammo\unlock();
|
||||
}
|
||||
|
||||
if (printMigrationStatus($db) !== 'tick') {
|
||||
fwrite(STDERR, "마이그레이션 후 스키마 검증에 실패했습니다. 백업과 *_wall_backup을 이용해 복구해 주세요.\n");
|
||||
exit(4);
|
||||
}
|
||||
fwrite(STDOUT, "게임 시계 migration 완료. *_wall_backup은 별도 검증 후에만 수동 제거해 주세요.\n");
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use sammo\DB;
|
||||
use sammo\GameClock;
|
||||
use sammo\KVStorage;
|
||||
use sammo\TurnExecutionHelper;
|
||||
use sammo\Util;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
||||
$_SERVER['REQUEST_URI'] ??= '/cli/verify-game-clock-engine';
|
||||
|
||||
require dirname(__DIR__) . '/hwe/lib.php';
|
||||
require dirname(__DIR__) . '/hwe/func.php';
|
||||
|
||||
$options = getopt('', ['apply', 'engine-calls:']);
|
||||
if (!isset($options['apply'])) {
|
||||
fwrite(STDERR, "Usage: php scripts/verify-game-clock-engine.php --apply [--engine-calls=N]\n");
|
||||
exit(2);
|
||||
}
|
||||
$engineCalls = filter_var($options['engine-calls'] ?? '1', FILTER_VALIDATE_INT);
|
||||
if ($engineCalls === false || $engineCalls < 1 || $engineCalls > 1000) {
|
||||
throw new InvalidArgumentException('--engine-calls는 1..1000이어야 합니다.');
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
if ($clock->getMode() !== GameClock::MODE_MANUAL) {
|
||||
throw new RuntimeException('재현 검증은 manual clock에서만 실행할 수 있습니다.');
|
||||
}
|
||||
|
||||
$fixedNowTick = $clock->nowTick();
|
||||
$before = $gameStor->getValues(['year', 'month', 'turntime']);
|
||||
$executedCount = 0;
|
||||
for ($call = 0; $call < $engineCalls; $call++) {
|
||||
$executed = false;
|
||||
$locked = false;
|
||||
TurnExecutionHelper::executeAllCommand($executed, $locked);
|
||||
if ($locked) {
|
||||
throw new RuntimeException('엔진이 GAME lock 또는 동결 상태로 진행하지 못했습니다.');
|
||||
}
|
||||
if ($executed) {
|
||||
$executedCount++;
|
||||
}
|
||||
$gameStor->resetCache();
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
if ($clock->nowTick() !== $fixedNowTick) {
|
||||
throw new RuntimeException('엔진 실행 중 manual clock tick이 실제 시간에 의해 변했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
$after = $gameStor->getValues(['year', 'month', 'turntime']);
|
||||
if (Util::toInt($after['turntime']) > $fixedNowTick) {
|
||||
throw new RuntimeException('마지막 실행 tick이 현재 game tick을 넘어갔습니다.');
|
||||
}
|
||||
printf(
|
||||
"clock_tick=%d clock=%s calls=%d executed_calls=%d before=%d-%02d/%d after=%d-%02d/%d\n",
|
||||
$fixedNowTick,
|
||||
$clock->formatTick($fixedNowTick, true),
|
||||
$engineCalls,
|
||||
$executedCount,
|
||||
Util::toInt($before['year']),
|
||||
Util::toInt($before['month']),
|
||||
Util::toInt($before['turntime']),
|
||||
Util::toInt($after['year']),
|
||||
Util::toInt($after['month']),
|
||||
Util::toInt($after['turntime']),
|
||||
);
|
||||
Reference in New Issue
Block a user